Compare commits

...

44 Commits

Author SHA1 Message Date
Brian Clozel 0a471c95c1 Release v6.2.13 2025-11-13 09:42:26 +01:00
Brian Clozel da31750e5f Upgrade to Micrometer 1.14.13
Closes gh-35810
2025-11-13 09:15:09 +01:00
Brian Clozel b853203846 Upgrade to Reactor 2024.0.12
Closes gh-35809
2025-11-13 09:14:04 +01:00
Brian Clozel 2b04df045a Upgrade to Jetty 12.0.30
Closes gh-35806
2025-11-12 21:57:31 +01:00
Juergen Hoeller 40544e096f Fix typo in ProxyMethodInvocation javadoc 2025-11-11 19:42:02 +01:00
Juergen Hoeller 23354b0155 Fix doTask method visibility (referring to private Task type)
See gh-35794
2025-11-11 19:41:44 +01:00
Juergen Hoeller 5c5367a1be Polishing 2025-11-11 15:40:08 +01:00
Juergen Hoeller 12dd758158 Provide compute method implementations in ConcurrentReferenceHashMap
Closes gh-35794
2025-11-11 15:39:21 +01:00
Sébastien Deleuze 5aec239261 Add hints for entities package-private methods
Closes gh-35711
2025-11-11 13:52:59 +01:00
Dmitry Sulman d0ff8f9243 Fix HttpServiceMethod for suspending functions returning Flow
Closes gh-35718
Signed-off-by: Dmitry Sulman <dmitry.sulman@gmail.com>
2025-11-11 12:58:42 +01:00
Brian Clozel ba39385cce Use executor for blocking I/O in Reactor request factory
Prior to this commit, the `ReactorClientHttpRequestFactory` and the
`ReactorClientHttpRequest` would use the `Executor` from the current
event loop for performing write operations.
Depending on I/O demand, this work could be blocked and would result in
blocked Netty event loop executors and the HTTP client hanging.

This commit ensures that the client uses a separate Executor for such
operations. If the application does not provide one on the request
factory, a `Schedulers#boundedElastic` instance will be used.

Fixes gh-34707
2025-11-10 16:12:14 +01:00
rstoyanchev e735c2d9c5 Improve Filter overview in reference docs
Closes gh-30454
2025-11-10 14:43:34 +00:00
rstoyanchev 3cb498fa98 Improve docs on streaming via StreamingHttpOutputMessage
Closes gh-35700
2025-11-10 14:43:34 +00:00
Sam Brannen 335a2c4e21 Support response encoding in select and options JSP form tags
Prior to this commit, Spring Framework's JSP form tags supported the
response encoding in most places; however, <form:select> and
<form:options> still did not support the response character encoding.

To address that, this commit updates SelectTag, OptionsTag, and
OptionWriter to provide support for response character encoding in the
`select` and `options` JSP form tags.

See gh-33023
Closes gh-35783
2025-11-10 15:35:13 +01:00
Sam Brannen 1714a00492 Suppress warnings in Gradle build 2025-11-10 14:13:34 +01:00
Chandra Bhan Singh ddb45a9302 Correct documentation formatting for Mono type
Closes gh-35786
Signed-off-by: Chandra Bhan Singh <cbhansingh1@gmail.com>
2025-11-10 12:03:37 +01:00
Juergen Hoeller 38555df3d8 Fix Aware exclusion check to apply to interface method only
See gh-35780
2025-11-08 09:53:17 +01:00
Juergen Hoeller 6f81cb625c Avoid unnecessary bridge method resolution around getMostSpecificMethod
Closes gh-35780
2025-11-08 09:16:31 +01:00
Juergen Hoeller 566078b4e4 Add explicit note on empty constructor
See gh-35773
2025-11-08 09:14:53 +01:00
Brian Clozel f0cf7f19f6 Fix multi-release JAR issue with VirtualThreadDelegate
This commit ensures that both `VirtualThreadDelegate` implementations
expose the same public API. If not, JAR verification fails with the
following message:

```
jar --validate --file spring-core-6.2.13-SNAPSHOT.jar
entry: META-INF/versions/21/org/springframework/core/task/VirtualThreadDelegate.class, contains a class with different api from earlier version
```

Fixes gh-35773
2025-11-07 14:01:51 +01:00
Juergen Hoeller b5008d33ee Upgrade to ASM 9.9 plus lenient version check patch
Closes gh-35763
2025-11-06 10:41:34 +01:00
Juergen Hoeller bce1445d92 Accept fallback match for bean name or method-level qualifier as well
Closes gh-35690
2025-11-05 19:31:53 +01:00
Brian Clozel 285182be27 Relax multiple segment matching constraints in PathPattern
Prior to this commit, gh-35213 allowed wildcard path elments at the
start of path patterns. This came with an additional constraint that
rejected such patterns if the pattern segment following the wildcard one
was not a literal:

* `/**/{name}` was rejected
* `/**/something/{name}` was accepted

The motivation here was to make the performance impact of wildard
patterns as small as possible at runtime.

This commit relaxes this constraint because `/**/*.js` patterns are very
popular in the security space for request matchers.

Closes gh-35686
2025-11-05 18:32:44 +01:00
Brian Clozel 8bb63081a8 Document PathPattern matching for single/multiple segments
This commit improves the reference document to better reflect the
different between `*` or `{name}` on one side, and `**` or `{*path}` on
the other.

The former patterns only consider a single path segment and its content,
while the latter variants consider zero or more path segments. This
explains why `/test/{*path}` can match `/test`.

Closes gh-35727
2025-11-05 14:58:58 +01:00
rstoyanchev a698b1bc0d Refine validation section for controllers
Closes gh-35759
2025-11-05 12:23:43 +00:00
Juergen Hoeller 2060c340ce Upgrade to Caffeine 3.2.3, Selenium 4.38, HtmlUnit 4.18 2025-11-05 12:44:28 +01:00
Juergen Hoeller 4f97c41e58 Avoid javadoc failure for line break with annotation reference
See gh-35736
2025-11-05 12:35:09 +01:00
Juergen Hoeller 0243059f53 Polishing 2025-11-05 12:22:56 +01:00
Juergen Hoeller b027b73a7a Relax configuration class requirement in configurer callback javadoc
Closes gh-35736
2025-11-05 12:22:44 +01:00
Brian Clozel c0429dbb09 Fix file extension resolution for media types with q parameter
Prior to this commit, the `MappingMediaTypeFileExtensionResolver` would
resolve file extensions for a given media type by using a direct lookup
using the given media type provided by the request.
If the request contains a quality parameter like
"application/json;q=0.9", this would not resolve configured file
extensions for this media type.
While other media type parameters can be meaningful, the quality
parameter should not be used for lookups. This commit ensures that the
quality parameter is dropped before performing lookups.

Fixes gh-35754
2025-11-05 10:48:24 +01:00
Brian Clozel d65de19e7d Deprecate setConnectTimeout on HttpComponents client factory
Prior to this commit, the `HttpComponentsClientHttpRequestFactory` would
set the connection timeout on the request configuration. This has been
deprecated by the client itself and this value should be set while
creating the client on the connection manager itself.

This commit deprecates this method, as there is no way for the factory
to set this value anymore.

Closes gh-35748
2025-11-03 16:53:54 +01:00
Sébastien Deleuze 16822c2fd0 Polishing
Closes gh-35746
2025-11-03 11:14:30 +01:00
Moad Elfatihi 983d39a970 Update CI documentation reference in README
Replace obsolete Concourse pipeline reference with GitHub Actions.
The Concourse link returns 404.

See gh-35746
Signed-off-by: Moad ELFATIHI <elfatihi.moad@gmail.com>
2025-11-03 11:14:14 +01:00
Juergen Hoeller 4b7cf85d71 Retrieve cached response body in a thread-safe manner
Closes gh-35745
2025-11-02 20:40:02 +01:00
Juergen Hoeller a33027703d Preserve Connection readOnly state for defaultReadOnly DataSource
Includes DataSourceTransactionManagerTests alignment with main branch.

Closes gh-35743
2025-11-02 17:28:12 +01:00
Sam Brannen ae804cb2d0 Improve @⁠NumberFormat Javadoc
See gh-35742
2025-11-02 16:23:47 +01:00
Stefano Cordio 46503d6a45 Fix typo in @⁠NumberFormat Javadoc
Closes gh-35742

Signed-off-by: Stefano Cordio <stefano.cordio@gmail.com>
2025-11-02 16:14:11 +01:00
Sam Brannen b4a13dddfc Fix JUnit Jupiter discovery issues on 6.2.x 2025-11-01 14:46:23 +01:00
Sam Brannen 661dca5e54 Remove accidental use of SpringExtension in ServletContextAwareBeanWacTests 2025-11-01 14:26:08 +01:00
Sam Brannen b976010a9e Simplify MockMvcClientHttpRequestFactoryTests 2025-11-01 14:26:07 +01:00
Juergen Hoeller 15563ee54e Polishing 2025-11-01 12:43:51 +01:00
Artur Signell 97586b525f fix: Fix PathMatchingResourcePatternResolver to handle absolute paths in JAR manifests
When JAR manifest Class-Path entries contain absolute paths (as Gradle
creates on Windows for long classpaths), PathMatchingResourcePatternResolver
incorrectly rejected them.

Fixes #35730

Signed-off-by: Artur Signell <artur@vaadin.com>
2025-11-01 12:25:20 +01:00
Sam Brannen f89737e398 Upgrade to JUnit 5.14.1
Closes gh-35739
2025-11-01 11:05:43 +01:00
Stéphane Nicoll ecdf069c5d Add AOT support for AutowiredPropertyMarker
Closes gh-35731
2025-10-31 15:30:54 +01:00
75 changed files with 2088 additions and 1471 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ See the [Build from Source](https://github.com/spring-projects/spring-framework/
## Continuous Integration Builds
Information regarding CI builds can be found in the [Spring Framework Concourse pipeline](ci/README.adoc) documentation.
CI builds are defined with [GitHub Actions workflows](.github/workflows).
## Stay in Touch
+1 -1
View File
@@ -97,7 +97,7 @@ configure([rootProject] + javaProjects) { project ->
// TODO Uncomment link to JUnit 5 docs once we execute Gradle with Java 18+.
// See https://github.com/spring-projects/spring-framework/issues/27497
//
// "https://junit.org/junit5/docs/5.14.0/api/",
// "https://junit.org/junit5/docs/5.14.1/api/",
"https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/",
//"https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/",
"https://r2dbc.io/spec/1.0.0.RELEASE/api/",
@@ -89,60 +89,7 @@ Kotlin::
You can map requests by using glob patterns and wildcards:
[cols="2,3,5"]
|===
|Pattern |Description |Example
| `spring`
| Literal pattern
| `+"/spring"+` matches `+"/spring"+`
| `+?+`
| Matches one character
| `+"/pages/t?st.html"+` matches `+"/pages/test.html"+` and `+"/pages/t3st.html"+`
| `+*+`
| Matches zero or more characters within a path segment
| `+"/resources/*.png"+` matches `+"/resources/file.png"+`
`+"/projects/*/versions"+` matches `+"/projects/spring/versions"+` but does not match `+"/projects/spring/boot/versions"+`
| `+**+`
| Matches zero or more path segments
| `+"/resources/**"+` matches `+"/resources/file.png"+` and `+"/resources/images/file.png"+`
`+"/**/resources"+` matches `+"/spring/resources"+` and `+"/spring/framework/resources"+`
`+"/resources/**/file.png"+` is invalid as `+**+` is not allowed in the middle of the path.
`+"/**/{name}/resources"+` is invalid as only a literal pattern is allowed right after `+**+`.
`+"/**/project/{project}/resources"+` is allowed.
`+"/**/spring/**"+` is not allowed, as only a single `+**+`/`+{*path}+` instance is allowed per pattern.
| `+{name}+`
| Matches a path segment and captures it as a variable named "name"
| `+"/projects/{project}/versions"+` matches `+"/projects/spring/versions"+` and captures `+project=spring+`
`+"/projects/{project}/versions"+` does not match `+"/projects/spring/framework/versions"+` as it captures a single path segment.
| `{name:[a-z]+}`
| Matches the regexp `[a-z]+` as a path variable named "name"
| `/projects/{project:[a-z]+}/versions` matches `/projects/spring/versions` but not `/projects/spring1/versions`
| `+{*path}+`
| Matches zero or more path segments and captures it as a variable named "path"
| `+"/resources/{*file}"+` matches `+"/resources/images/file.png"+` and captures `+file=/images/file.png+`
`+"{*path}/resources"+` matches `+"/spring/framework/resources"+` and captures `+path=/spring/framework+`
`+"/resources/{*path}/file.png"+` is invalid as `{*path}` is not allowed in the middle of the path.
`+"/{*path}/{name}/resources"+` is invalid as only a literal pattern is allowed right after `{*path}`.
`+"/{*path}/project/{project}/resources"+` is allowed.
`+"/{*path}/spring/**"+` is not allowed, as only a single `+**+`/`+{*path}+` instance is allowed per pattern.
|===
include::partial$web/uri-patterns.adoc[leveloffset=+1]
Captured URI variables can be accessed with `@PathVariable`, as the following example shows:
@@ -7,19 +7,23 @@ Spring WebFlux has built-in xref:core/validation/validator.adoc[Validation] for
`@RequestMapping` methods, including xref:core/validation/beanvalidation.adoc[Java Bean Validation].
Validation may be applied at one of two levels:
1. xref:web/webflux/controller/ann-methods/modelattrib-method-args.adoc[@ModelAttribute],
1. Java Bean Validation is applied individually to an
xref:web/webflux/controller/ann-methods/modelattrib-method-args.adoc[@ModelAttribute],
xref:web/webflux/controller/ann-methods/requestbody.adoc[@RequestBody], and
xref:web/webflux/controller/ann-methods/multipart-forms.adoc[@RequestPart] argument
resolvers validate a method argument individually if the method parameter is annotated
with Jakarta `@Valid` or Spring's `@Validated`, _AND_ there is no `Errors` or
`BindingResult` parameter immediately after, _AND_ method validation is not needed (to be
discussed next). The exception raised in this case is `WebExchangeBindException`.
xref:web/webflux/controller/ann-methods/multipart-forms.adoc[@RequestPart] method parameter
annotated with `@jakarta.validation.Valid` or Spring's `@Validated` so long as
it is a command object rather than a container such as `Map` or `Collection`, it does not
have `Errors` or `BindingResult` immediately after in the method signature, and does not
otherwise require method validation (see next). `WebExchangeBindException` is the
exception raised when validating a method parameter individually.
2. When `@Constraint` annotations such as `@Min`, `@NotBlank` and others are declared
directly on method parameters, or on the method (for the return value), then method
validation must be applied, and that supersedes validation at the method argument level
because method validation covers both method parameter constraints and nested constraints
via `@Valid`. The exception raised in this case is `HandlerMethodValidationException`.
2. Java Bean Validation is applied to the method when `@Constraint` annotations such as
`@Min`, `@NotBlank` and others are declared directly on method parameters, or on the
method for the return value, and it supersedes any validation that would be applied
otherwise to a method parameter individually because method validation covers both
method parameter constraints and nested constraints via `@Valid`.
`HandlerMethodValidationException` is the exception raised validation is applied
to the method.
Applications must handle both `WebExchangeBindException` and
`HandlerMethodValidationException` as either may be raised depending on the controller
@@ -3,7 +3,10 @@
[.small]#xref:web/webflux/reactive-spring.adoc#webflux-filters[See equivalent in the Reactive stack]#
The `spring-web` module provides some useful filters:
In the Servlet API, you can add a `jakarta.servlet.Filter` to apply interception-style logic
before and after the rest of the processing chain of filters and the target `Servlet`.
The `spring-web` module has a number of built-in `Filter` implementations:
* xref:web/webmvc/filters.adoc#filters-http-put[Form Data]
* xref:web/webmvc/filters.adoc#filters-forwarded-headers[Forwarded Headers]
@@ -11,9 +14,19 @@ The `spring-web` module provides some useful filters:
* xref:web/webmvc/filters.adoc#filters-cors[CORS]
* xref:web/webmvc/filters.adoc#filters.url-handler[URL Handler]
Servlet filters can be configured in the `web.xml` configuration file or using Servlet annotations.
If you are using Spring Boot, you can
{spring-boot-docs}/how-to/webserver.html#howto.webserver.add-servlet-filter-listener.spring-bean[declare them as beans and configure them as part of your application].
There are also base class implementations for use in Spring applications:
* `GenericFilterBean` -- base class for a `Filter` configured as a Spring bean;
integrates with the Spring `ApplicationContext` lifecycle.
* `OncePerRequestFilter` -- extension of `GenericFilterBean` that supports a single
invocation at the start of a request, i.e. during the `REQUEST` dispatch phase, and
ignoring further handling via `FORWARD` dispatches. The filter also provides control
over whether the `Filter` gets involved in `ASYNC` and `ERROR` dispatches.
Servlet filters can be configured in `web.xml` or via Servlet annotations.
In a Spring Boot application , you can
{spring-boot-docs}/how-to/webserver.html#howto.webserver.add-servlet-filter-listener.spring-bean[declare Filter's as beans]
and Boot will have them configured.
[[filters-http-put]]
@@ -422,7 +422,7 @@ reactive types from the controller method.
Reactive return values are handled as follows:
* A single-value promise is adapted to, similar to using `DeferredResult`. Examples
include `CompletionStage` (JDK), Mono` (Reactor), and `Single` (RxJava).
include `CompletionStage` (JDK), `Mono` (Reactor), and `Single` (RxJava).
* A multi-value stream with a streaming media type (such as `application/x-ndjson`
or `text/event-stream`) is adapted to, similar to using `ResponseBodyEmitter` or
`SseEmitter`. Examples include `Flux` (Reactor) or `Observable` (RxJava).
@@ -105,61 +105,7 @@ customizations of path matching options.
You can map requests by using glob patterns and wildcards:
[cols="2,3,5"]
|===
|Pattern |Description |Example
| `spring`
| Literal pattern
| `+"/spring"+` matches `+"/spring"+`
| `+?+`
| Matches one character
| `+"/pages/t?st.html"+` matches `+"/pages/test.html"+` and `+"/pages/t3st.html"+`
| `+*+`
| Matches zero or more characters within a path segment
| `+"/resources/*.png"+` matches `+"/resources/file.png"+`
`+"/projects/*/versions"+` matches `+"/projects/spring/versions"+` but does not match `+"/projects/spring/boot/versions"+`
| `+**+`
| Matches zero or more path segments
| `+"/resources/**"+` matches `+"/resources/file.png"+` and `+"/resources/images/file.png"+`
`+"/**/resources"+` matches `+"/spring/resources"+` and `+"/spring/framework/resources"+`
`+"/resources/**/file.png"+` is invalid as `+**+` is not allowed in the middle of the path.
`+"/**/{name}/resources"+` is invalid as only a literal pattern is allowed right after `+**+`.
`+"/**/project/{project}/resources"+` is allowed.
`+"/**/spring/**"+` is not allowed, as only a single `+**+`/`+{*path}+` instance is allowed per pattern.
| `+{name}+`
| Matches a path segment and captures it as a variable named "name"
| `+"/projects/{project}/versions"+` matches `+"/projects/spring/versions"+` and captures `+project=spring+`
`+"/projects/{project}/versions"+` does not match `+"/projects/spring/framework/versions"+` as it captures a single path segment.
| `{name:[a-z]+}`
| Matches the regexp `"[a-z]+"` as a path variable named "name"
| `"/projects/{project:[a-z]+}/versions"` matches `"/projects/spring/versions"` but not `"/projects/spring1/versions"`
| `+{*path}+`
| Matches zero or more path segments and captures it as a variable named "path"
| `+"/resources/{*file}"+` matches `+"/resources/images/file.png"+` and captures `+file=/images/file.png+`
`+"{*path}/resources"+` matches `+"/spring/framework/resources"+` and captures `+path=/spring/framework+`
`+"/resources/{*path}/file.png"+` is invalid as `{*path}` is not allowed in the middle of the path.
`+"/{*path}/{name}/resources"+` is invalid as only a literal pattern is allowed right after `{*path}`.
`+"/{*path}/project/{project}/resources"+` is allowed.
`+"/{*path}/spring/**"+` is not allowed, as only a single `+**+`/`+{*path}+` instance is allowed per pattern.
|===
include::partial$web/uri-patterns.adoc[leveloffset=+1]
Captured URI variables can be accessed with `@PathVariable`. For example:
@@ -7,22 +7,26 @@ Spring MVC has built-in xref:core/validation/validator.adoc[validation] for
`@RequestMapping` methods, including xref:core/validation/beanvalidation.adoc[Java Bean Validation].
Validation may be applied at one of two levels:
1. xref:web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc[@ModelAttribute],
1. Java Bean Validation is applied individually to an
xref:web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc[@ModelAttribute],
xref:web/webmvc/mvc-controller/ann-methods/requestbody.adoc[@RequestBody], and
xref:web/webmvc/mvc-controller/ann-methods/multipart-forms.adoc[@RequestPart] argument
resolvers validate a method argument individually if the method parameter is annotated
with Jakarta `@Valid` or Spring's `@Validated`, _AND_ there is no `Errors` or
`BindingResult` parameter immediately after, _AND_ method validation is not needed (to be
discussed next). The exception raised in this case is `MethodArgumentNotValidException`.
xref:web/webmvc/mvc-controller/ann-methods/multipart-forms.adoc[@RequestPart] method parameter
annotated with `@jakarta.validation.Valid` or Spring's `@Validated` so long as
it is a command object rather than a container such as `Map` or `Collection`, it does not
have `Errors` or `BindingResult` immediately after in the method signature, and does not
otherwise require method validation (see next). `MethodArgumentNotValidException` is the
exception raised when validating a method parameter individually.
2. When `@Constraint` annotations such as `@Min`, `@NotBlank` and others are declared
directly on method parameters, or on the method (for the return value), then method
validation must be applied, and that supersedes validation at the method argument level
because method validation covers both method parameter constraints and nested constraints
via `@Valid`. The exception raised in this case is `HandlerMethodValidationException`.
2. Java Bean Validation is applied to the method when `@Constraint` annotations such as
`@Min`, `@NotBlank` and others are declared directly on method parameters, or on the
method for the return value, and it supersedes any validation that would be applied
otherwise to a method parameter individually because method validation covers both
method parameter constraints and nested constraints via `@Valid`.
`HandlerMethodValidationException` is the exception raised validation is applied
to the method.
Applications must handle both `MethodArgumentNotValidException` and
`HandlerMethodValidationException` as either may be raised depending on the controller
Applications should handle both `MethodArgumentNotValidException` and
`HandlerMethodValidationException` since either may be raised depending on the controller
method signature. The two exceptions, however are designed to be very similar, and can be
handled with almost identical code. The main difference is that the former is for a single
object while the latter is for a list of method parameters.
@@ -0,0 +1,51 @@
[cols="2,3,5"]
|===
|Pattern |Description |Example
| `spring`
| Literal pattern
| `+"/spring"+` matches `+"/spring"+`
| `+?+`
| Matches one character
| `+"/pages/t?st.html"+` matches `+"/pages/test.html"+` and `+"/pages/t3st.html"+`
| `+*+`
| Matches zero or more characters within a path segment
| `+"/resources/*.png"+` matches `+"/resources/file.png"+`
`+"/projects/*/versions"+` matches `+"/projects/spring/versions"+` but does not match `+"/projects/spring/boot/versions"+`.
`+"/projects/*"+` matches `+"/projects/spring"+` but does not match `+"/projects"+` as the path segment is not present.
| `+**+`
| Matches zero or more path segments
| `+"/resources/**"+` matches `+"/resources"+`, `+"/resources/file.png"+` and `+"/resources/images/file.png"+`
`+"/**/info"+` matches `+"/info"+`, `+"/spring/info"+` and `+"/spring/framework/info"+`
`+"/resources/**/file.png"+` is invalid as `+**+` is not allowed in the middle of the path.
`+"/**/spring/**"+` is not allowed, as only a single `+**+`/`+{*path}+` instance is allowed per pattern.
| `+{name}+`
| Similar to `+*+`, but also captures the path segment as a variable named "name"
| `+"/projects/{project}/versions"+` matches `+"/projects/spring/versions"+` and captures `+project=spring+`
`+"/projects/{project}/versions"+` does not match `+"/projects/spring/framework/versions"+` as it captures a single path segment.
| `{name:[a-z]+}`
| Matches the regexp `"[a-z]+"` as a path variable named "name"
| `"/projects/{project:[a-z]+}/versions"` matches `"/projects/spring/versions"` but not `"/projects/spring1/versions"`
| `+{*path}+`
| Similar to `+**+`, but also captures the path segments as a variable named "path"
| `+"/resources/{*file}"+` matches `+"/resources/images/file.png"+` and captures `+file=/images/file.png+`
`+"{*path}/resources"+` matches `+"/spring/framework/resources"+` and captures `+path=/spring/framework+`
`+"/resources/{*path}/file.png"+` is invalid as `{*path}` is not allowed in the middle of the path.
`+"/{*path}/spring/**"+` is not allowed, as only a single `+**+`/`+{*path}+` instance is allowed per pattern.
|===
+9 -9
View File
@@ -8,25 +8,25 @@ javaPlatform {
dependencies {
api(platform("com.fasterxml.jackson:jackson-bom:2.18.5"))
api(platform("io.micrometer:micrometer-bom:1.14.12"))
api(platform("io.micrometer:micrometer-bom:1.14.13"))
api(platform("io.netty:netty-bom:4.1.128.Final"))
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
api(platform("io.projectreactor:reactor-bom:2024.0.11"))
api(platform("io.projectreactor:reactor-bom:2024.0.12"))
api(platform("io.rsocket:rsocket-bom:1.1.5"))
api(platform("org.apache.groovy:groovy-bom:4.0.29"))
api(platform("org.apache.logging.log4j:log4j-bom:2.21.1"))
api(platform("org.assertj:assertj-bom:3.27.6"))
api(platform("org.eclipse.jetty:jetty-bom:12.0.29"))
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.29"))
api(platform("org.eclipse.jetty:jetty-bom:12.0.30"))
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.30"))
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1"))
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.3"))
api(platform("org.junit:junit-bom:5.14.0"))
api(platform("org.junit:junit-bom:5.14.1"))
api(platform("org.mockito:mockito-bom:5.20.0"))
constraints {
api("com.fasterxml:aalto-xml:1.3.4")
api("com.fasterxml.woodstox:woodstox-core:6.7.0")
api("com.github.ben-manes.caffeine:caffeine:3.2.2")
api("com.github.ben-manes.caffeine:caffeine:3.2.3")
api("com.github.librepdf:openpdf:1.3.43")
api("com.google.code.findbugs:findbugs:3.0.1")
api("com.google.code.findbugs:jsr305:3.0.2")
@@ -129,7 +129,7 @@ dependencies {
api("org.hibernate:hibernate-core-jakarta:5.6.15.Final")
api("org.hibernate:hibernate-validator:7.0.5.Final")
api("org.hsqldb:hsqldb:2.7.4")
api("org.htmlunit:htmlunit:4.17.0")
api("org.htmlunit:htmlunit:4.18.0")
api("org.javamoney:moneta:1.4.4")
api("org.jruby:jruby:9.4.13.0")
api("org.junit.support:testng-engine:1.0.5")
@@ -137,8 +137,8 @@ dependencies {
api("org.ogce:xpp3:1.1.6")
api("org.python:jython-standalone:2.7.4")
api("org.quartz-scheduler:quartz:2.3.2")
api("org.seleniumhq.selenium:htmlunit3-driver:4.36.1")
api("org.seleniumhq.selenium:selenium-java:4.36.0")
api("org.seleniumhq.selenium:htmlunit3-driver:4.38.0")
api("org.seleniumhq.selenium:selenium-java:4.38.0")
api("org.skyscreamer:jsonassert:1.5.3")
api("org.slf4j:slf4j-api:2.0.17")
api("org.testng:testng:7.11.0")
+1 -1
View File
@@ -1,4 +1,4 @@
version=6.2.13-SNAPSHOT
version=6.2.13
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
@@ -62,7 +62,7 @@ public interface ProxyMethodInvocation extends MethodInvocation {
MethodInvocation invocableClone(Object... arguments);
/**
* Set the arguments to be used on subsequent invocations in the any advice
* Set the arguments to be used on subsequent invocations in any advice
* in this chain.
* @param arguments the argument array
*/
@@ -566,7 +566,7 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
}
final List<InjectionMetadata.InjectedElement> elements = new ArrayList<>();
Class<?> targetClass = clazz;
Class<?> targetClass = ClassUtils.getUserClass(clazz);
do {
final List<InjectionMetadata.InjectedElement> fieldElements = new ArrayList<>();
@@ -586,12 +586,11 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
final List<InjectionMetadata.InjectedElement> methodElements = new ArrayList<>();
ReflectionUtils.doWithLocalMethods(targetClass, method -> {
Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
if (method.isBridge()) {
return;
}
MergedAnnotation<?> ann = findAutowiredAnnotation(bridgedMethod);
if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
MergedAnnotation<?> ann = findAutowiredAnnotation(method);
if (ann != null && method.equals(BridgeMethodResolver.getMostSpecificMethod(method, clazz))) {
if (Modifier.isStatic(method.getModifiers())) {
if (logger.isInfoEnabled()) {
logger.info("Autowired annotation is not supported on static methods: " + method);
@@ -609,7 +608,7 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
}
}
boolean required = determineRequiredStatus(ann);
PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method, clazz);
methodElements.add(new AutowiredMethodElement(method, required, pd));
}
});
@@ -180,7 +180,6 @@ public class QualifierAnnotationAutowireCandidateResolver extends GenericTypeAwa
* {@code true} if a qualifier has been found and matched,
* {@code null} if no qualifier has been found at all
*/
@Nullable
protected Boolean checkQualifiers(BeanDefinitionHolder bdHolder, Annotation[] annotationsToSearch) {
boolean qualifierFound = false;
@@ -375,6 +374,14 @@ public class QualifierAnnotationAutowireCandidateResolver extends GenericTypeAwa
return true;
}
}
MethodParameter methodParam = descriptor.getMethodParameter();
if (methodParam != null) {
for (Annotation annotation : methodParam.getMethodAnnotations()) {
if (isQualifier(annotation.annotationType())) {
return true;
}
}
}
return false;
}
@@ -29,6 +29,7 @@ import org.springframework.aot.generate.ValueCodeGenerator.Delegate;
import org.springframework.aot.generate.ValueCodeGeneratorDelegates;
import org.springframework.aot.generate.ValueCodeGeneratorDelegates.CollectionDelegate;
import org.springframework.aot.generate.ValueCodeGeneratorDelegates.MapDelegate;
import org.springframework.beans.factory.config.AutowiredPropertyMarker;
import org.springframework.beans.factory.config.BeanReference;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.config.TypedStringValue;
@@ -57,6 +58,7 @@ abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates {
* <li>{@link LinkedHashMap}</li>
* <li>{@link BeanReference}</li>
* <li>{@link TypedStringValue}</li>
* <li>{@link AutowiredPropertyMarker}</li>
* </ul>
* When combined with {@linkplain ValueCodeGeneratorDelegates#INSTANCES the
* delegates for common value types}, this should be added first as they have
@@ -68,7 +70,8 @@ abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates {
new ManagedMapDelegate(),
new LinkedHashMapDelegate(),
new BeanReferenceDelegate(),
new TypedStringValueDelegate()
new TypedStringValueDelegate(),
new AutowiredPropertyMarkerDelegate()
);
@@ -216,4 +219,20 @@ abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates {
return valueCodeGenerator.generateCode(value);
}
}
/**
* {@link Delegate} for {@link AutowiredPropertyMarker} types.
*/
private static class AutowiredPropertyMarkerDelegate implements Delegate {
@Override
@Nullable
public CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) {
if (value instanceof AutowiredPropertyMarker) {
return CodeBlock.of("$T.INSTANCE", AutowiredPropertyMarker.class);
}
return null;
}
}
}
@@ -1653,8 +1653,8 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
return doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter);
}
@SuppressWarnings("NullAway") // Dataflow analysis limitation
@Nullable
@SuppressWarnings("NullAway")
public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,
@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {
@@ -1991,7 +1991,8 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
DependencyDescriptor fallbackDescriptor = descriptor.forFallbackMatch();
for (String candidate : candidateNames) {
if (!isSelfReference(beanName, candidate) && isAutowireCandidate(candidate, fallbackDescriptor) &&
(!multiple || getAutowireCandidateResolver().hasQualifier(descriptor))) {
(!multiple || matchesBeanName(candidate, descriptor.getDependencyName()) ||
getAutowireCandidateResolver().hasQualifier(descriptor))) {
addCandidateEntry(result, candidate, descriptor, requiredType);
}
}
@@ -2262,12 +2263,12 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
}
/**
* Determine whether the given candidate name matches the bean name or the aliases
* Determine whether the given dependency name matches the bean name or the aliases
* stored in this bean definition.
*/
protected boolean matchesBeanName(String beanName, @Nullable String candidateName) {
return (candidateName != null &&
(candidateName.equals(beanName) || ObjectUtils.containsElement(getAliases(beanName), candidateName)));
protected boolean matchesBeanName(String beanName, @Nullable String dependencyName) {
return (dependencyName != null &&
(dependencyName.equals(beanName) || ObjectUtils.containsElement(getAliases(beanName), dependencyName)));
}
/**
@@ -1275,88 +1275,6 @@ class AutowiredAnnotationBeanPostProcessorTests {
assertThat(bean.getTestBean().get("testBean2")).isNull();
}
@Test
void fieldInjectionWithMap() {
RootBeanDefinition bd = new RootBeanDefinition(MapFieldInjectionBean.class);
bd.setScope(BeanDefinition.SCOPE_PROTOTYPE);
bf.registerBeanDefinition("annotatedBean", bd);
TestBean tb1 = new TestBean("tb1");
TestBean tb2 = new TestBean("tb2");
bf.registerSingleton("testBean1", tb1);
bf.registerSingleton("testBean2", tb2);
bf.registerAlias("testBean1", "testBean");
MapFieldInjectionBean bean = bf.getBean("annotatedBean", MapFieldInjectionBean.class);
assertThat(bean.getTestBeanMap()).hasSize(2);
assertThat(bean.getTestBeanMap()).containsKey("testBean1");
assertThat(bean.getTestBeanMap()).containsKey("testBean2");
assertThat(bean.getTestBeanMap()).containsValue(tb1);
assertThat(bean.getTestBeanMap()).containsValue(tb2);
bean = bf.getBean("annotatedBean", MapFieldInjectionBean.class);
assertThat(bean.getTestBeanMap()).hasSize(2);
assertThat(bean.getTestBeanMap()).containsKey("testBean1");
assertThat(bean.getTestBeanMap()).containsKey("testBean2");
assertThat(bean.getTestBeanMap()).containsValue(tb1);
assertThat(bean.getTestBeanMap()).containsValue(tb2);
}
@Test
void methodInjectionWithMap() {
RootBeanDefinition bd = new RootBeanDefinition(MapMethodInjectionBean.class);
bd.setScope(BeanDefinition.SCOPE_PROTOTYPE);
bf.registerBeanDefinition("annotatedBean", bd);
TestBean tb = new TestBean();
bf.registerSingleton("testBean", tb);
MapMethodInjectionBean bean = bf.getBean("annotatedBean", MapMethodInjectionBean.class);
assertThat(bean.getTestBeanMap()).hasSize(1);
assertThat(bean.getTestBeanMap()).containsKey("testBean");
assertThat(bean.getTestBeanMap()).containsValue(tb);
assertThat(bean.getTestBean()).isSameAs(tb);
bean = bf.getBean("annotatedBean", MapMethodInjectionBean.class);
assertThat(bean.getTestBeanMap()).hasSize(1);
assertThat(bean.getTestBeanMap()).containsKey("testBean");
assertThat(bean.getTestBeanMap()).containsValue(tb);
assertThat(bean.getTestBean()).isSameAs(tb);
}
@Test
void methodInjectionWithMapAndMultipleMatches() {
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(MapMethodInjectionBean.class));
bf.registerBeanDefinition("testBean1", new RootBeanDefinition(TestBean.class));
bf.registerBeanDefinition("testBean2", new RootBeanDefinition(TestBean.class));
assertThatExceptionOfType(UnsatisfiedDependencyException.class).as("should have failed, more than one bean of type")
.isThrownBy(() -> bf.getBean("annotatedBean"))
.satisfies(methodParameterDeclaredOn(MapMethodInjectionBean.class));
}
@Test
void methodInjectionWithMapAndMultipleMatchesButOnlyOneAutowireCandidate() {
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(MapMethodInjectionBean.class));
bf.registerBeanDefinition("testBean1", new RootBeanDefinition(TestBean.class));
RootBeanDefinition rbd2 = new RootBeanDefinition(TestBean.class);
rbd2.setAutowireCandidate(false);
bf.registerBeanDefinition("testBean2", rbd2);
MapMethodInjectionBean bean = bf.getBean("annotatedBean", MapMethodInjectionBean.class);
TestBean tb = bf.getBean("testBean1", TestBean.class);
assertThat(bean.getTestBeanMap()).hasSize(1);
assertThat(bean.getTestBeanMap()).containsKey("testBean1");
assertThat(bean.getTestBeanMap()).containsValue(tb);
assertThat(bean.getTestBean()).isSameAs(tb);
}
@Test
void methodInjectionWithMapAndNoMatches() {
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(MapMethodInjectionBean.class));
MapMethodInjectionBean bean = bf.getBean("annotatedBean", MapMethodInjectionBean.class);
assertThat(bean.getTestBeanMap()).isNull();
assertThat(bean.getTestBean()).isNull();
}
@Test
void constructorInjectionWithTypedMapAsBean() {
RootBeanDefinition bd = new RootBeanDefinition(MapConstructorInjectionBean.class);
@@ -1409,6 +1327,19 @@ class AutowiredAnnotationBeanPostProcessorTests {
@Test
void constructorInjectionWithPlainHashMapAsBean() {
RootBeanDefinition bd = new RootBeanDefinition(NamedMapConstructorInjectionBean.class);
bd.setScope(BeanDefinition.SCOPE_PROTOTYPE);
bf.registerBeanDefinition("annotatedBean", bd);
bf.registerBeanDefinition("testBeanMap", new RootBeanDefinition(HashMap.class));
NamedMapConstructorInjectionBean bean = bf.getBean("annotatedBean", NamedMapConstructorInjectionBean.class);
assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("testBeanMap"));
bean = bf.getBean("annotatedBean", NamedMapConstructorInjectionBean.class);
assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("testBeanMap"));
}
@Test
void constructorInjectionWithQualifiedPlainHashMapAsBean() {
RootBeanDefinition bd = new RootBeanDefinition(QualifiedMapConstructorInjectionBean.class);
bd.setScope(BeanDefinition.SCOPE_PROTOTYPE);
bf.registerBeanDefinition("annotatedBean", bd);
@@ -1497,6 +1428,114 @@ class AutowiredAnnotationBeanPostProcessorTests {
assertThat(bean.getTestBeanSet()).contains(tb1, tb2);
}
@Test
void fieldInjectionWithMap() {
RootBeanDefinition bd = new RootBeanDefinition(MapFieldInjectionBean.class);
bd.setScope(BeanDefinition.SCOPE_PROTOTYPE);
bf.registerBeanDefinition("annotatedBean", bd);
TestBean tb1 = new TestBean("tb1");
TestBean tb2 = new TestBean("tb2");
bf.registerSingleton("testBean1", tb1);
bf.registerSingleton("testBean2", tb2);
bf.registerAlias("testBean1", "testBean");
MapFieldInjectionBean bean = bf.getBean("annotatedBean", MapFieldInjectionBean.class);
assertThat(bean.getTestBeanMap()).hasSize(2);
assertThat(bean.getTestBeanMap()).containsKey("testBean1");
assertThat(bean.getTestBeanMap()).containsKey("testBean2");
assertThat(bean.getTestBeanMap()).containsValue(tb1);
assertThat(bean.getTestBeanMap()).containsValue(tb2);
bean = bf.getBean("annotatedBean", MapFieldInjectionBean.class);
assertThat(bean.getTestBeanMap()).hasSize(2);
assertThat(bean.getTestBeanMap()).containsKey("testBean1");
assertThat(bean.getTestBeanMap()).containsKey("testBean2");
assertThat(bean.getTestBeanMap()).containsValue(tb1);
assertThat(bean.getTestBeanMap()).containsValue(tb2);
}
@Test
void methodInjectionWithMap() {
RootBeanDefinition bd = new RootBeanDefinition(MapMethodInjectionBean.class);
bd.setScope(BeanDefinition.SCOPE_PROTOTYPE);
bf.registerBeanDefinition("annotatedBean", bd);
TestBean tb = new TestBean();
bf.registerSingleton("testBean", tb);
MapMethodInjectionBean bean = bf.getBean("annotatedBean", MapMethodInjectionBean.class);
assertThat(bean.getTestBeanMap()).hasSize(1);
assertThat(bean.getTestBeanMap()).containsKey("testBean");
assertThat(bean.getTestBeanMap()).containsValue(tb);
assertThat(bean.getTestBean()).isSameAs(tb);
bean = bf.getBean("annotatedBean", MapMethodInjectionBean.class);
assertThat(bean.getTestBeanMap()).hasSize(1);
assertThat(bean.getTestBeanMap()).containsKey("testBean");
assertThat(bean.getTestBeanMap()).containsValue(tb);
assertThat(bean.getTestBean()).isSameAs(tb);
}
@Test
void methodInjectionWithMapAndMultipleMatches() {
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(MapMethodInjectionBean.class));
bf.registerBeanDefinition("testBean1", new RootBeanDefinition(TestBean.class));
bf.registerBeanDefinition("testBean2", new RootBeanDefinition(TestBean.class));
assertThatExceptionOfType(UnsatisfiedDependencyException.class).as("should have failed, more than one bean of type")
.isThrownBy(() -> bf.getBean("annotatedBean"))
.satisfies(methodParameterDeclaredOn(MapMethodInjectionBean.class));
}
@Test
void methodInjectionWithMapAndMultipleMatchesButOnlyOneAutowireCandidate() {
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(MapMethodInjectionBean.class));
bf.registerBeanDefinition("testBean1", new RootBeanDefinition(TestBean.class));
RootBeanDefinition rbd2 = new RootBeanDefinition(TestBean.class);
rbd2.setAutowireCandidate(false);
bf.registerBeanDefinition("testBean2", rbd2);
MapMethodInjectionBean bean = bf.getBean("annotatedBean", MapMethodInjectionBean.class);
TestBean tb = bf.getBean("testBean1", TestBean.class);
assertThat(bean.getTestBeanMap()).hasSize(1);
assertThat(bean.getTestBeanMap()).containsKey("testBean1");
assertThat(bean.getTestBeanMap()).containsValue(tb);
assertThat(bean.getTestBean()).isSameAs(tb);
}
@Test
void methodInjectionWithMapAndNoMatches() {
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(MapMethodInjectionBean.class));
MapMethodInjectionBean bean = bf.getBean("annotatedBean", MapMethodInjectionBean.class);
assertThat(bean.getTestBeanMap()).isNull();
assertThat(bean.getTestBean()).isNull();
}
@Test
void methodInjectionWithPlainHashMapAsBean() {
RootBeanDefinition bd = new RootBeanDefinition(NamedMapMethodInjectionBean.class);
bd.setScope(BeanDefinition.SCOPE_PROTOTYPE);
bf.registerBeanDefinition("annotatedBean", bd);
bf.registerBeanDefinition("testBeanMap", new RootBeanDefinition(HashMap.class));
NamedMapMethodInjectionBean bean = bf.getBean("annotatedBean", NamedMapMethodInjectionBean.class);
assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("testBeanMap"));
bean = bf.getBean("annotatedBean", NamedMapMethodInjectionBean.class);
assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("testBeanMap"));
}
@Test
void methodInjectionWithQualifiedPlainHashMapAsBean() {
RootBeanDefinition bd = new RootBeanDefinition(QualifiedMapMethodInjectionBean.class);
bd.setScope(BeanDefinition.SCOPE_PROTOTYPE);
bf.registerBeanDefinition("annotatedBean", bd);
bf.registerBeanDefinition("myTestBeanMap", new RootBeanDefinition(HashMap.class));
QualifiedMapMethodInjectionBean bean = bf.getBean("annotatedBean", QualifiedMapMethodInjectionBean.class);
assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("myTestBeanMap"));
bean = bf.getBean("annotatedBean", QualifiedMapMethodInjectionBean.class);
assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("myTestBeanMap"));
}
@Test
void selfReference() {
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(SelfInjectionBean.class));
@@ -3258,6 +3297,21 @@ class AutowiredAnnotationBeanPostProcessorTests {
}
public static class NamedMapConstructorInjectionBean {
private Map<String, TestBean> testBeanMap;
@Autowired
public NamedMapConstructorInjectionBean(Map<String, TestBean> testBeanMap) {
this.testBeanMap = testBeanMap;
}
public Map<String, TestBean> getTestBeanMap() {
return this.testBeanMap;
}
}
public static class QualifiedMapConstructorInjectionBean {
private Map<String, TestBean> testBeanMap;
@@ -3357,6 +3411,37 @@ class AutowiredAnnotationBeanPostProcessorTests {
}
public static class NamedMapMethodInjectionBean {
private Map<String, TestBean> testBeanMap;
@Autowired
public void setTestBeanMap(Map<String, TestBean> testBeanMap) {
this.testBeanMap = testBeanMap;
}
public Map<String, TestBean> getTestBeanMap() {
return this.testBeanMap;
}
}
public static class QualifiedMapMethodInjectionBean {
private Map<String, TestBean> testBeanMap;
@Autowired
@Qualifier("myTestBeanMap")
public void setTestBeanMap(Map<String, TestBean> testBeanMap) {
this.testBeanMap = testBeanMap;
}
public Map<String, TestBean> getTestBeanMap() {
return this.testBeanMap;
}
}
@SuppressWarnings("serial")
public static class ObjectFactoryFieldInjectionBean implements Serializable {
@@ -16,6 +16,7 @@
package org.springframework.beans.factory.aot;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -34,6 +35,7 @@ import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.generate.MethodReference;
import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator;
import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.beans.factory.config.AutowiredPropertyMarker;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
@@ -626,6 +628,22 @@ class BeanDefinitionMethodGeneratorTests {
-> assertThat(customPropertyValue.value()).isEqualTo("test")));
}
@Test
void generateBeanDefinitionMethodWhenHasAutowiredPropertyGeneratesMethod() {
RootBeanDefinition beanDefinition = (RootBeanDefinition) BeanDefinitionBuilder
.rootBeanDefinition(CustomBean.class).addAutowiredProperty("innerBean")
.getBeanDefinition();
RegisteredBean registeredBean = registerBean(beanDefinition);
BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator(
this.methodGeneratorFactory, registeredBean, null,
Collections.emptyList());
MethodReference method = generator.generateBeanDefinitionMethod(
this.generationContext, this.beanRegistrationsCode);
compile(method, (actual, compiled) ->
assertThat(actual.getPropertyValues().get("innerBean"))
.isSameAs(AutowiredPropertyMarker.INSTANCE));
}
@Test
void generateBeanDefinitionMethodWhenHasAotContributionsAppliesContributions() {
RegisteredBean registeredBean = registerBean(
@@ -39,6 +39,7 @@ import org.springframework.aot.generate.GeneratedClass;
import org.springframework.aot.generate.ValueCodeGenerator;
import org.springframework.aot.generate.ValueCodeGeneratorDelegates;
import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.beans.factory.config.AutowiredPropertyMarker;
import org.springframework.beans.factory.config.BeanReference;
import org.springframework.beans.factory.config.RuntimeBeanNameReference;
import org.springframework.beans.factory.config.RuntimeBeanReference;
@@ -486,4 +487,16 @@ class BeanDefinitionPropertyValueCodeGeneratorDelegatesTests {
}
@Nested
class AutowiredPropertyMarkerTests {
@Test
void generateWhenAutowiredPropertyMarker() {
compile(AutowiredPropertyMarker.INSTANCE, (instance, compiler) ->
assertThat(instance).isInstanceOf(AutowiredPropertyMarker.class)
.isSameAs(AutowiredPropertyMarker.INSTANCE));
}
}
}
@@ -59,6 +59,8 @@ public class TestBean implements BeanNameAware, BeanFactoryAware, ITestBean, IOt
private boolean jedi;
private String favoriteCafé;
private ITestBean spouse;
private String touchy;
@@ -209,6 +211,14 @@ public class TestBean implements BeanNameAware, BeanFactoryAware, ITestBean, IOt
this.jedi = jedi;
}
public String getFavoriteCafé() {
return this.favoriteCafé;
}
public void setFavoriteCafé(String favoriteCafé) {
this.favoriteCafé = favoriteCafé;
}
@Override
public ITestBean getSpouse() {
return this.spouse;
@@ -25,6 +25,7 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.Aware;
import org.springframework.core.MethodClassKey;
import org.springframework.lang.Nullable;
import org.springframework.util.ReflectionUtils;
@@ -97,6 +98,10 @@ public abstract class AbstractFallbackJCacheOperationSource implements JCacheOpe
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
// Skip methods declared on BeanFactoryAware and co.
if (method.getDeclaringClass().isInterface() && Aware.class.isAssignableFrom(method.getDeclaringClass())) {
return null;
}
// The method may be on an interface, but we need metadata from the target class.
// If the target class is null, the method will be unchanged.
@@ -23,14 +23,14 @@ import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.lang.Nullable;
/**
* Interface to be implemented by @{@link org.springframework.context.annotation.Configuration
* Configuration} classes annotated with @{@link EnableCaching} that wish or need to specify
* explicitly how caches are resolved and how keys are generated for annotation-driven
* cache management.
* Interface to be implemented for explicitly specifying how caches are resolved
* and how keys are generated for annotation-driven cache management.
*
* <p>See @{@link EnableCaching} for general examples and context; see
* {@link #cacheManager()}, {@link #cacheResolver()}, {@link #keyGenerator()}, and
* {@link #errorHandler()} for detailed instructions.
* <p>Typically implemented by @{@link org.springframework.context.annotation.Configuration
* Configuration} classes annotated with @{@link EnableCaching}.
* See @{@link EnableCaching} for general examples and context; see
* {@link #cacheManager()}, {@link #cacheResolver()}, {@link #keyGenerator()},
* and {@link #errorHandler()} for detailed instructions.
*
* <p><b>NOTE: A {@code CachingConfigurer} will get initialized early.</b>
* Do not inject common dependencies into autowired fields directly; instead, consider
@@ -27,6 +27,7 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.Aware;
import org.springframework.core.MethodClassKey;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
@@ -139,6 +140,10 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
// Skip methods declared on BeanFactoryAware and co.
if (method.getDeclaringClass().isInterface() && Aware.class.isAssignableFrom(method.getDeclaringClass())) {
return null;
}
// The method may be on an interface, but we need metadata from the target class.
// If the target class is null, the method will be unchanged.
@@ -424,7 +424,7 @@ public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBean
}
List<InjectionMetadata.InjectedElement> elements = new ArrayList<>();
Class<?> targetClass = clazz;
Class<?> targetClass = ClassUtils.getUserClass(clazz);
do {
final List<InjectionMetadata.InjectedElement> currElements = new ArrayList<>();
@@ -455,24 +455,23 @@ public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBean
});
ReflectionUtils.doWithLocalMethods(targetClass, method -> {
Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
if (method.isBridge()) {
return;
}
if (ejbAnnotationType != null && bridgedMethod.isAnnotationPresent(ejbAnnotationType)) {
if (method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
if (ejbAnnotationType != null && method.isAnnotationPresent(ejbAnnotationType)) {
if (method.equals(BridgeMethodResolver.getMostSpecificMethod(method, clazz))) {
if (Modifier.isStatic(method.getModifiers())) {
throw new IllegalStateException("@EJB annotation is not supported on static methods");
}
if (method.getParameterCount() != 1) {
throw new IllegalStateException("@EJB annotation requires a single-arg method: " + method);
}
PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
currElements.add(new EjbRefElement(method, bridgedMethod, pd));
PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method, clazz);
currElements.add(new EjbRefElement(method, method, pd));
}
}
else if (jakartaResourceType != null && bridgedMethod.isAnnotationPresent(jakartaResourceType)) {
if (method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
else if (jakartaResourceType != null && method.isAnnotationPresent(jakartaResourceType)) {
if (method.equals(BridgeMethodResolver.getMostSpecificMethod(method, clazz))) {
if (Modifier.isStatic(method.getModifiers())) {
throw new IllegalStateException("@Resource annotation is not supported on static methods");
}
@@ -481,13 +480,13 @@ public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBean
throw new IllegalStateException("@Resource annotation requires a single-arg method: " + method);
}
if (!this.ignoredResourceTypes.contains(paramTypes[0].getName())) {
PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
currElements.add(new ResourceElement(method, bridgedMethod, pd));
PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method, clazz);
currElements.add(new ResourceElement(method, method, pd));
}
}
}
else if (javaxResourceType != null && bridgedMethod.isAnnotationPresent(javaxResourceType)) {
if (method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
else if (javaxResourceType != null && method.isAnnotationPresent(javaxResourceType)) {
if (method.equals(BridgeMethodResolver.getMostSpecificMethod(method, clazz))) {
if (Modifier.isStatic(method.getModifiers())) {
throw new IllegalStateException("@Resource annotation is not supported on static methods");
}
@@ -496,8 +495,8 @@ public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBean
throw new IllegalStateException("@Resource annotation requires a single-arg method: " + method);
}
if (!this.ignoredResourceTypes.contains(paramTypes[0].getName())) {
PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
currElements.add(new LegacyResourceElement(method, bridgedMethod, pd));
PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method, clazz);
currElements.add(new LegacyResourceElement(method, method, pd));
}
}
}
@@ -25,18 +25,18 @@ import java.lang.annotation.Target;
/**
* Declares that a field or method parameter should be formatted as a number.
*
* <p>Supports formatting by style or custom pattern string. Can be applied
* to any JDK {@code Number} type such as {@code Double} and {@code Long}.
* <p>Supports formatting by style or custom pattern string. Can be applied to
* any JDK {@code Number} types such as {@code Double} and {@code Long}.
*
* <p>For style-based formatting, set the {@link #style} attribute to be the
* desired {@link Style}. For custom formatting, set the {@link #pattern}
* attribute to be the number pattern, such as {@code #, ###.##}.
* <p>For style-based formatting, set the {@link #style} attribute to the desired
* {@link Style}. For custom formatting, set the {@link #pattern} attribute to the
* desired number pattern, such as {@code "#,###.##"}.
*
* <p>Each attribute is mutually exclusive, so only set one attribute per
* annotation instance (the one most convenient one for your formatting needs).
* When the {@link #pattern} attribute is specified, it takes precedence over
* the {@link #style} attribute. When no annotation attributes are specified,
* the default format applied is style-based for either number of currency,
* annotation (the one most convenient for your formatting needs). When the
* {@link #pattern} attribute is specified, it takes precedence over the
* {@link #style} attribute. When no annotation attributes are specified, the
* default format applied is style-based for either number or currency,
* depending on the annotated field or method parameter type.
*
* @author Keith Donald
@@ -50,19 +50,21 @@ import java.lang.annotation.Target;
public @interface NumberFormat {
/**
* The style pattern to use to format the field.
* The style pattern to use to format the field or method parameter.
* <p>Defaults to {@link Style#DEFAULT} for general-purpose number formatting
* for most annotated types, except for money types which default to currency
* formatting. Set this attribute when you wish to format your field in
* accordance with a common style other than the default style.
* formatting.
* <p>Set this attribute when you wish to format your field or method parameter
* in accordance with a common style other than the default style.
*/
Style style() default Style.DEFAULT;
/**
* The custom pattern to use to format the field.
* <p>Defaults to empty String, indicating no custom pattern String has been specified.
* Set this attribute when you wish to format your field in accordance with a
* custom number pattern not represented by a style.
* The custom pattern to use to format the field or method parameter.
* <p>Defaults to an empty String, indicating no custom pattern has been
* specified.
* <p>Set this attribute when you wish to format your field or method parameter
* in accordance with a custom number pattern not represented by a style.
*/
String pattern() default "";
@@ -22,13 +22,18 @@ import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.lang.Nullable;
/**
* Interface to be implemented by @{@link org.springframework.context.annotation.Configuration
* Configuration} classes annotated with @{@link EnableAsync} that wish to customize the
* {@link Executor} instance used when processing async method invocations or the
* {@link AsyncUncaughtExceptionHandler} instance used to process exception thrown from
* async method with {@code void} return type.
* Interface to be implemented for customizing the {@link Executor} instance used when
* processing async method invocations or the {@link AsyncUncaughtExceptionHandler}
* instance used to process exceptions thrown from async methods with a {@code void}
* return type.
*
* <p>See @{@link EnableAsync} for usage examples.
* <p>Typically implemented by @{@link org.springframework.context.annotation.Configuration
* Configuration} classes annotated with @{@link EnableAsync}.
* See the @{@link EnableAsync} javadoc for usage examples.
*
* <p><b>NOTE: An {@code AsyncConfigurer} will get initialized early.</b>
* Do not inject common dependencies into autowired fields directly; instead, consider
* declaring a lazy {@link org.springframework.beans.factory.ObjectProvider} for those.
*
* @author Chris Beams
* @author Stephane Nicoll
@@ -193,12 +193,16 @@ public class ClassReader {
final byte[] classFileBuffer, final int classFileOffset, final boolean checkClassVersion) {
this.classFileBuffer = classFileBuffer;
this.b = classFileBuffer;
// Check the class' major_version. This field is after the magic and minor_version fields, which
// use 4 and 2 bytes respectively.
if (checkClassVersion && readShort(classFileOffset + 6) > Opcodes.V25) {
throw new IllegalArgumentException(
"Unsupported class file major version " + readShort(classFileOffset + 6));
}
// SPRING PATCH: leniently try to parse newer class files as well
// if (checkClassVersion && readShort(classFileOffset + 6) > Opcodes.V26) {
// throw new IllegalArgumentException(
// "Unsupported class file major version " + readShort(classFileOffset + 6));
// }
// END OF PATCH
// Create the constant pool arrays. The constant_pool_count field is after the magic,
// minor_version and major_version fields, which use 4, 2 and 2 bytes respectively.
int constantPoolCount = readUnsignedShort(classFileOffset + 8);
@@ -290,6 +290,7 @@ public interface Opcodes {
int V23 = 0 << 16 | 67;
int V24 = 0 << 16 | 68;
int V25 = 0 << 16 | 69;
int V26 = 0 << 16 | 70;
/**
* Version flag indicating that the class is using 'preview' features.
@@ -1473,7 +1473,7 @@ final class SymbolTable {
/**
* Another entry (and so on recursively) having the same hash code (modulo the size of {@link
* SymbolTable#labelEntries}}) as this one.
* SymbolTable#labelEntries}) as this one.
*/
LabelEntry next;
@@ -100,15 +100,15 @@ public final class BridgeMethodResolver {
}
private static Method resolveBridgeMethod(Method bridgeMethod, Class<?> targetClass) {
boolean localBridge = (targetClass == bridgeMethod.getDeclaringClass());
Class<?> userClass = targetClass;
if (!bridgeMethod.isBridge() && localBridge) {
if (!bridgeMethod.isBridge()) {
userClass = ClassUtils.getUserClass(targetClass);
if (userClass == targetClass) {
return bridgeMethod;
}
}
boolean localBridge = (targetClass == bridgeMethod.getDeclaringClass());
Object cacheKey = (localBridge ? bridgeMethod : new MethodClassKey(bridgeMethod, targetClass));
Method bridgedMethod = cache.get(cacheKey);
if (bridgedMethod == null) {
@@ -118,7 +118,7 @@ public final class BridgeMethodResolver {
ReflectionUtils.doWithMethods(userClass, candidateMethods::add, filter);
if (!candidateMethods.isEmpty()) {
bridgedMethod = (candidateMethods.size() == 1 ? candidateMethods.get(0) :
searchCandidates(candidateMethods, bridgeMethod));
searchCandidates(candidateMethods, bridgeMethod, targetClass));
}
if (bridgedMethod == null) {
// A bridge method was passed in but we couldn't find the bridged method.
@@ -149,14 +149,16 @@ public final class BridgeMethodResolver {
* @return the bridged method, or {@code null} if none found
*/
@Nullable
private static Method searchCandidates(List<Method> candidateMethods, Method bridgeMethod) {
private static Method searchCandidates(
List<Method> candidateMethods, Method bridgeMethod, Class<?> targetClass) {
if (candidateMethods.isEmpty()) {
return null;
}
Method previousMethod = null;
boolean sameSig = true;
for (Method candidateMethod : candidateMethods) {
if (isBridgeMethodFor(bridgeMethod, candidateMethod, bridgeMethod.getDeclaringClass())) {
if (isBridgeMethodFor(bridgeMethod, candidateMethod, targetClass)) {
return candidateMethod;
}
else if (previousMethod != null) {
@@ -172,12 +174,12 @@ public final class BridgeMethodResolver {
* Determines whether the bridge {@link Method} is the bridge for the
* supplied candidate {@link Method}.
*/
static boolean isBridgeMethodFor(Method bridgeMethod, Method candidateMethod, Class<?> declaringClass) {
if (isResolvedTypeMatch(candidateMethod, bridgeMethod, declaringClass)) {
static boolean isBridgeMethodFor(Method bridgeMethod, Method candidateMethod, Class<?> targetClass) {
if (isResolvedTypeMatch(candidateMethod, bridgeMethod, targetClass)) {
return true;
}
Method method = findGenericDeclaration(bridgeMethod);
return (method != null && isResolvedTypeMatch(method, candidateMethod, declaringClass));
return (method != null && isResolvedTypeMatch(method, candidateMethod, targetClass));
}
/**
@@ -186,14 +188,25 @@ public final class BridgeMethodResolver {
* are equal after resolving all types against the declaringType, otherwise
* returns {@code false}.
*/
private static boolean isResolvedTypeMatch(Method genericMethod, Method candidateMethod, Class<?> declaringClass) {
private static boolean isResolvedTypeMatch(Method genericMethod, Method candidateMethod, Class<?> targetClass) {
Type[] genericParameters = genericMethod.getGenericParameterTypes();
if (genericParameters.length != candidateMethod.getParameterCount()) {
return false;
}
Class<?> clazz = targetClass;
while (clazz != null && clazz != Object.class && clazz != genericMethod.getDeclaringClass()) {
if (checkResolvedTypeMatch(genericMethod, candidateMethod, clazz)) {
return true;
}
clazz = clazz.getSuperclass();
}
return false;
}
private static boolean checkResolvedTypeMatch(Method genericMethod, Method candidateMethod, Class<?> clazz) {
Class<?>[] candidateParameters = candidateMethod.getParameterTypes();
for (int i = 0; i < candidateParameters.length; i++) {
ResolvableType genericParameter = ResolvableType.forMethodParameter(genericMethod, i, declaringClass);
ResolvableType genericParameter = ResolvableType.forMethodParameter(genericMethod, i, clazz);
Class<?> candidateParameter = candidateParameters[i];
if (candidateParameter.isArray()) {
// An array type: compare the component type.
@@ -273,7 +286,9 @@ public final class BridgeMethodResolver {
* introduced in Java 6 to fix <a href="https://bugs.openjdk.org/browse/JDK-6342411">
* JDK-6342411</a>.
* @return whether signatures match as described
* @deprecated as of 6.2.13: not necessary anymore due to {@link #getMostSpecificMethod}
*/
@Deprecated(since = "6.2.13", forRemoval = true)
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.
@@ -617,10 +617,12 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
private Set<ClassPathManifestEntry> getClassPathManifestEntriesFromJar(File jar) throws IOException {
URL base = jar.toURI().toURL();
File parent = jar.getAbsoluteFile().getParentFile();
try (JarFile jarFile = new JarFile(jar)) {
Manifest manifest = jarFile.getManifest();
Attributes attributes = (manifest != null ? manifest.getMainAttributes() : null);
String classPath = (attributes != null ? attributes.getValue(Name.CLASS_PATH) : null);
Set<ClassPathManifestEntry> manifestEntries = new LinkedHashSet<>();
if (StringUtils.hasLength(classPath)) {
StringTokenizer tokenizer = new StringTokenizer(classPath);
@@ -630,8 +632,15 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
// See jdk.internal.loader.URLClassPath.JarLoader.tryResolveFile(URL, String)
continue;
}
File candidate = new File(parent, path);
if (candidate.isFile() && candidate.getCanonicalPath().contains(parent.getCanonicalPath())) {
// Handle absolute paths correctly: do not apply parent to absolute paths.
File pathFile = new File(path);
File candidate = (pathFile.isAbsolute() ? pathFile : new File(parent, path));
// For relative paths, enforce security check: must be under parent.
// For absolute paths, just verify file exists (matching JVM behavior).
if (candidate.isFile() && (pathFile.isAbsolute() ||
candidate.getCanonicalPath().contains(parent.getCanonicalPath()))) {
manifestEntries.add(ClassPathManifestEntry.of(candidate, this.useCaches));
}
}
@@ -33,11 +33,13 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiFunction;
import java.util.function.Function;
import org.springframework.lang.Nullable;
/**
* A {@link ConcurrentHashMap} that uses {@link ReferenceType#SOFT soft} or
* A {@link ConcurrentHashMap} variant that uses {@link ReferenceType#SOFT soft} or
* {@linkplain ReferenceType#WEAK weak} references for both {@code keys} and {@code values}.
*
* <p>This class can be used as an alternative to
@@ -365,6 +367,118 @@ public class ConcurrentReferenceHashMap<K, V> extends AbstractMap<K, V> implemen
});
}
@Override
@Nullable
public V computeIfAbsent(@Nullable K key, Function<? super K, ? extends V> mappingFunction) {
return doTask(key, new Task<V>(TaskOption.RESTRUCTURE_BEFORE, TaskOption.RESIZE) {
@Override
protected @Nullable V execute(@Nullable Reference<K, V> ref, @Nullable Entry<K, V> entry, @Nullable Entries<V> entries) {
if (entry != null) {
return entry.getValue();
}
V value = mappingFunction.apply(key);
// Add entry only if not null
if (value != null) {
Assert.state(entries != null, "No entries segment");
entries.add(value);
}
return value;
}
});
}
@Override
@Nullable
public V computeIfPresent(@Nullable K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
return doTask(key, new Task<V>(TaskOption.RESTRUCTURE_BEFORE, TaskOption.RESIZE) {
@Override
protected @Nullable V execute(@Nullable Reference<K, V> ref, @Nullable Entry<K, V> entry, @Nullable Entries<V> entries) {
if (entry != null) {
V oldValue = entry.getValue();
V value = remappingFunction.apply(key, oldValue);
if (value != null) {
// Replace entry
entry.setValue(value);
return value;
}
else {
// Remove entry
if (ref != null) {
ref.release();
}
}
}
return null;
}
});
}
@Override
@Nullable
public V compute(@Nullable K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
return doTask(key, new Task<V>(TaskOption.RESTRUCTURE_BEFORE, TaskOption.RESIZE) {
@Override
protected @Nullable V execute(@Nullable Reference<K, V> ref, @Nullable Entry<K, V> entry, @Nullable Entries<V> entries) {
V oldValue = null;
if (entry != null) {
oldValue = entry.getValue();
}
V value = remappingFunction.apply(key, oldValue);
if (value != null) {
if (entry != null) {
// Replace entry
entry.setValue(value);
}
else {
// Add entry
Assert.state(entries != null, "No entries segment");
entries.add(value);
}
return value;
}
else {
// Remove entry
if (ref != null) {
ref.release();
}
}
return null;
}
});
}
@Override
@Nullable
public V merge(@Nullable K key, @Nullable V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
return doTask(key, new Task<V>(TaskOption.RESTRUCTURE_BEFORE, TaskOption.RESIZE) {
@Override
protected @Nullable V execute(@Nullable Reference<K, V> ref, @Nullable Entry<K, V> entry, @Nullable Entries<V> entries) {
if (entry != null) {
V oldValue = entry.getValue();
V newValue = remappingFunction.apply(oldValue, value);
if (newValue != null) {
// Replace entry
entry.setValue(newValue);
return newValue;
}
else {
// Remove entry
if (ref != null) {
ref.release();
}
return null;
}
}
else {
// Add entry
Assert.state(entries != null, "No entries segment");
entries.add(value);
return value;
}
}
});
}
@Override
public void clear() {
for (Segment segment : this.segments) {
@@ -514,7 +628,7 @@ public class ConcurrentReferenceHashMap<K, V> extends AbstractMap<K, V> implemen
* @return the result of the operation
*/
@Nullable
public <T> T doTask(final int hash, @Nullable final Object key, final Task<T> task) {
private <T> T doTask(final int hash, @Nullable final Object key, final Task<T> task) {
boolean resize = task.hasOption(TaskOption.RESIZE);
if (task.hasOption(TaskOption.RESTRUCTURE_BEFORE)) {
restructureIfNecessary(resize);
@@ -579,7 +693,6 @@ public class ConcurrentReferenceHashMap<K, V> extends AbstractMap<K, V> implemen
}
private void restructure(boolean allowResize, @Nullable Reference<K, V> ref) {
boolean needsResize;
lock();
try {
int expectedCount = this.count.get();
@@ -595,7 +708,7 @@ public class ConcurrentReferenceHashMap<K, V> extends AbstractMap<K, V> implemen
// Estimate new count, taking into account count inside lock and items that
// will be purged.
needsResize = (expectedCount > 0 && expectedCount >= this.resizeThreshold);
boolean needsResize = (expectedCount > 0 && expectedCount >= this.resizeThreshold);
boolean resizing = false;
int restructureSize = this.references.length;
if (allowResize && needsResize && restructureSize < MAXIMUM_SEGMENT_SIZE) {
@@ -636,8 +749,8 @@ public class ConcurrentReferenceHashMap<K, V> extends AbstractMap<K, V> implemen
while (ref != null) {
if (!toPurge.contains(ref)) {
Entry<K, V> entry = ref.get();
// Also filter out null references that are now null
// they should be polled from the queue in a later restructure call.
// Also filter out null references that are now null:
// They should be polled from the queue in a later restructure call.
if (entry != null) {
purgedRef = this.referenceManager.createReference(
entry, ref.getHash(), purgedRef);
@@ -649,7 +762,7 @@ public class ConcurrentReferenceHashMap<K, V> extends AbstractMap<K, V> implemen
this.references[i] = purgedRef;
}
}
this.count.set(Math.max(newCount, 0));
this.count.set(newCount);
}
finally {
unlock();
@@ -30,6 +30,10 @@ final class VirtualThreadDelegate {
private final Thread.Builder threadBuilder = Thread.ofVirtual();
public VirtualThreadDelegate() {
// Matching constructor in dummy version, avoiding jar verification issues.
}
public ThreadFactory virtualThreadFactory() {
return this.threadBuilder.factory();
}
@@ -429,14 +429,14 @@ class BridgeMethodResolverTests {
}
public abstract static class AbstractDateAdder implements Adder<Date> {
public abstract static class AbstractAdder<T extends Serializable> implements Adder<T> {
@Override
public abstract void add(Date date);
public abstract void add(T item);
}
public static class DateAdder extends AbstractDateAdder {
public static class DateAdder extends AbstractAdder<Date> {
@Override
public void add(Date date) {
@@ -337,6 +337,21 @@ class PathMatchingResourcePatternResolverTests {
assertThat(result.replace("\\", "/")).contains("!!!!").contains("/lib/asset.jar!/assets/file.txt");
}
@Test
void javaDashJarFindsAbsoluteClassPathManifestEntries() throws Exception {
Path assetJar = this.temp.resolve("dependency").resolve("asset.jar");
Files.createDirectories(assetJar.getParent());
writeAssetJar(assetJar);
writeApplicationJarWithAbsolutePath(this.temp.resolve("app.jar"), assetJar);
String java = ProcessHandle.current().info().command().get();
Process process = new ProcessBuilder(java, "-jar", "app.jar")
.directory(this.temp.toFile())
.start();
assertThat(process.waitFor()).isZero();
String result = StreamUtils.copyToString(process.getInputStream(), StandardCharsets.UTF_8);
assertThat(result.replace("\\", "/")).contains("!!!!").contains("asset.jar!/assets/file.txt");
}
private void writeAssetJar(Path path) throws Exception {
try (JarOutputStream jar = new JarOutputStream(new FileOutputStream(path.toFile()))) {
jar.putNextEntry(new ZipEntry("assets/"));
@@ -392,6 +407,35 @@ class PathMatchingResourcePatternResolverTests {
assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + path + ResourceUtils.JAR_URL_SEPARATOR).exists()).isTrue();
}
private void writeApplicationJarWithAbsolutePath(Path path, Path assetJar) throws Exception {
Manifest manifest = new Manifest();
Attributes mainAttributes = manifest.getMainAttributes();
mainAttributes.put(Name.CLASS_PATH, buildSpringClassPath() + assetJar.toAbsolutePath());
mainAttributes.put(Name.MAIN_CLASS, ClassPathManifestEntriesTestApplication.class.getName());
mainAttributes.put(Name.MANIFEST_VERSION, "1.0");
try (JarOutputStream jar = new JarOutputStream(new FileOutputStream(path.toFile()), manifest)) {
String appClassResource = ClassUtils.convertClassNameToResourcePath(
ClassPathManifestEntriesTestApplication.class.getName()) + ClassUtils.CLASS_FILE_SUFFIX;
String folder = "";
for (String name : appClassResource.split("/")) {
if (!name.endsWith(ClassUtils.CLASS_FILE_SUFFIX)) {
folder += name + "/";
jar.putNextEntry(new ZipEntry(folder));
jar.closeEntry();
}
else {
jar.putNextEntry(new ZipEntry(folder + name));
try (InputStream in = getClass().getResourceAsStream(name)) {
in.transferTo(jar);
}
jar.closeEntry();
}
}
}
assertThat(new FileSystemResource(path).exists()).isTrue();
assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + path + ResourceUtils.JAR_URL_SEPARATOR).exists()).isTrue();
}
private String buildSpringClassPath() throws Exception {
return copyClasses(PathMatchingResourcePatternResolver.class, "spring-core") +
copyClasses(LogFactory.class, "commons-logging");
@@ -53,7 +53,7 @@ class ConcurrentReferenceHashMapTests {
@Test
void shouldCreateWithDefaults() {
void createWithDefaults() {
ConcurrentReferenceHashMap<Integer, String> map = new ConcurrentReferenceHashMap<>();
assertThat(map.getSegmentsSize()).isEqualTo(16);
assertThat(map.getSegment(0).getSize()).isEqualTo(1);
@@ -61,7 +61,7 @@ class ConcurrentReferenceHashMapTests {
}
@Test
void shouldCreateWithInitialCapacity() {
void createWithInitialCapacity() {
ConcurrentReferenceHashMap<Integer, String> map = new ConcurrentReferenceHashMap<>(32);
assertThat(map.getSegmentsSize()).isEqualTo(16);
assertThat(map.getSegment(0).getSize()).isEqualTo(2);
@@ -69,7 +69,7 @@ class ConcurrentReferenceHashMapTests {
}
@Test
void shouldCreateWithInitialCapacityAndLoadFactor() {
void createWithInitialCapacityAndLoadFactor() {
ConcurrentReferenceHashMap<Integer, String> map = new ConcurrentReferenceHashMap<>(32, 0.5f);
assertThat(map.getSegmentsSize()).isEqualTo(16);
assertThat(map.getSegment(0).getSize()).isEqualTo(2);
@@ -77,7 +77,7 @@ class ConcurrentReferenceHashMapTests {
}
@Test
void shouldCreateWithInitialCapacityAndConcurrentLevel() {
void createWithInitialCapacityAndConcurrentLevel() {
ConcurrentReferenceHashMap<Integer, String> map = new ConcurrentReferenceHashMap<>(16, 2);
assertThat(map.getSegmentsSize()).isEqualTo(2);
assertThat(map.getSegment(0).getSize()).isEqualTo(8);
@@ -85,7 +85,7 @@ class ConcurrentReferenceHashMapTests {
}
@Test
void shouldCreateFullyCustom() {
void createFullyCustom() {
ConcurrentReferenceHashMap<Integer, String> map = new ConcurrentReferenceHashMap<>(5, 0.5f, 3);
// concurrencyLevel of 3 ends up as 4 (nearest power of 2)
assertThat(map.getSegmentsSize()).isEqualTo(4);
@@ -95,28 +95,28 @@ class ConcurrentReferenceHashMapTests {
}
@Test
void shouldNeedNonNegativeInitialCapacity() {
void nonNegativeInitialCapacity() {
assertThatNoException().isThrownBy(() -> new ConcurrentReferenceHashMap<Integer, String>(0, 1));
assertThatIllegalArgumentException().isThrownBy(() -> new ConcurrentReferenceHashMap<Integer, String>(-1, 1))
.withMessageContaining("Initial capacity must not be negative");
}
@Test
void shouldNeedPositiveLoadFactor() {
void positiveLoadFactor() {
assertThatNoException().isThrownBy(() -> new ConcurrentReferenceHashMap<Integer, String>(0, 0.1f, 1));
assertThatIllegalArgumentException().isThrownBy(() -> new ConcurrentReferenceHashMap<Integer, String>(0, 0.0f, 1))
.withMessageContaining("Load factor must be positive");
}
@Test
void shouldNeedPositiveConcurrencyLevel() {
void positiveConcurrencyLevel() {
assertThatNoException().isThrownBy(() -> new ConcurrentReferenceHashMap<Integer, String>(1, 1));
assertThatIllegalArgumentException().isThrownBy(() -> new ConcurrentReferenceHashMap<Integer, String>(1, 0))
.withMessageContaining("Concurrency level must be positive");
}
@Test
void shouldPutAndGet() {
void putAndGet() {
// NOTE we are using mock references so we don't need to worry about GC
assertThat(this.map).isEmpty();
this.map.put(123, "123");
@@ -129,14 +129,14 @@ class ConcurrentReferenceHashMapTests {
}
@Test
void shouldReplaceOnDoublePut() {
void replaceOnDoublePut() {
this.map.put(123, "321");
this.map.put(123, "123");
assertThat(this.map.get(123)).isEqualTo("123");
}
@Test
void shouldPutNullKey() {
void putNullKey() {
assertThat(this.map.get(null)).isNull();
assertThat(this.map.getOrDefault(null, "456")).isEqualTo("456");
this.map.put(null, "123");
@@ -145,7 +145,7 @@ class ConcurrentReferenceHashMapTests {
}
@Test
void shouldPutNullValue() {
void putNullValue() {
assertThat(this.map.get(123)).isNull();
assertThat(this.map.getOrDefault(123, "456")).isEqualTo("456");
this.map.put(123, "321");
@@ -157,12 +157,12 @@ class ConcurrentReferenceHashMapTests {
}
@Test
void shouldGetWithNoItems() {
void getWithNoItems() {
assertThat(this.map.get(123)).isNull();
}
@Test
void shouldApplySupplementalHash() {
void applySupplementalHash() {
Integer key = 123;
this.map.put(key, "123");
assertThat(this.map.getSupplementalHash()).isNotEqualTo(key.hashCode());
@@ -170,7 +170,7 @@ class ConcurrentReferenceHashMapTests {
}
@Test
void shouldGetFollowingNexts() {
void getFollowingNexts() {
// Use loadFactor to disable resize
this.map = new TestWeakConcurrentCache<>(1, 10.0f, 1);
this.map.put(1, "1");
@@ -184,7 +184,7 @@ class ConcurrentReferenceHashMapTests {
}
@Test
void shouldResize() {
void resize() {
this.map = new TestWeakConcurrentCache<>(1, 0.75f, 1);
this.map.put(1, "1");
assertThat(this.map.getSegment(0).getSize()).isEqualTo(1);
@@ -214,7 +214,7 @@ class ConcurrentReferenceHashMapTests {
}
@Test
void shouldPurgeOnGet() {
void purgeOnGet() {
this.map = new TestWeakConcurrentCache<>(1, 0.75f, 1);
for (int i = 1; i <= 5; i++) {
this.map.put(i, String.valueOf(i));
@@ -229,7 +229,7 @@ class ConcurrentReferenceHashMapTests {
}
@Test
void shouldPurgeOnPut() {
void purgeOnPut() {
this.map = new TestWeakConcurrentCache<>(1, 0.75f, 1);
for (int i = 1; i <= 5; i++) {
this.map.put(i, String.valueOf(i));
@@ -245,28 +245,28 @@ class ConcurrentReferenceHashMapTests {
}
@Test
void shouldPutIfAbsent() {
void putIfAbsent() {
assertThat(this.map.putIfAbsent(123, "123")).isNull();
assertThat(this.map.putIfAbsent(123, "123b")).isEqualTo("123");
assertThat(this.map.get(123)).isEqualTo("123");
}
@Test
void shouldPutIfAbsentWithNullValue() {
void putIfAbsentWithNullValue() {
assertThat(this.map.putIfAbsent(123, null)).isNull();
assertThat(this.map.putIfAbsent(123, "123")).isNull();
assertThat(this.map.get(123)).isNull();
}
@Test
void shouldPutIfAbsentWithNullKey() {
void putIfAbsentWithNullKey() {
assertThat(this.map.putIfAbsent(null, "123")).isNull();
assertThat(this.map.putIfAbsent(null, "123b")).isEqualTo("123");
assertThat(this.map.get(null)).isEqualTo("123");
}
@Test
void shouldRemoveKeyAndValue() {
void removeKeyAndValue() {
this.map.put(123, "123");
assertThat(this.map.remove(123, "456")).isFalse();
assertThat(this.map.get(123)).isEqualTo("123");
@@ -276,7 +276,7 @@ class ConcurrentReferenceHashMapTests {
}
@Test
void shouldRemoveKeyAndValueWithExistingNull() {
void removeKeyAndValueWithExistingNull() {
this.map.put(123, null);
assertThat(this.map.remove(123, "456")).isFalse();
assertThat(this.map.get(123)).isNull();
@@ -286,7 +286,7 @@ class ConcurrentReferenceHashMapTests {
}
@Test
void shouldReplaceOldValueWithNewValue() {
void replaceOldValueWithNewValue() {
this.map.put(123, "123");
assertThat(this.map.replace(123, "456", "789")).isFalse();
assertThat(this.map.get(123)).isEqualTo("123");
@@ -295,7 +295,7 @@ class ConcurrentReferenceHashMapTests {
}
@Test
void shouldReplaceOldNullValueWithNewValue() {
void replaceOldNullValueWithNewValue() {
this.map.put(123, null);
assertThat(this.map.replace(123, "456", "789")).isFalse();
assertThat(this.map.get(123)).isNull();
@@ -304,21 +304,61 @@ class ConcurrentReferenceHashMapTests {
}
@Test
void shouldReplaceValue() {
void replaceValue() {
this.map.put(123, "123");
assertThat(this.map.replace(123, "456")).isEqualTo("123");
assertThat(this.map.get(123)).isEqualTo("456");
}
@Test
void shouldReplaceNullValue() {
void replaceNullValue() {
this.map.put(123, null);
assertThat(this.map.replace(123, "456")).isNull();
assertThat(this.map.get(123)).isEqualTo("456");
}
@Test
void shouldGetSize() {
void computeIfAbsent() {
assertThat(this.map.computeIfAbsent(123, k -> "123")).isEqualTo("123");
assertThat(this.map.computeIfAbsent(123, k -> "123b")).isEqualTo("123");
assertThat(this.map.get(123)).isEqualTo("123");
this.map.remove(123);
assertThat(this.map.computeIfAbsent(123, k -> null)).isNull();
assertThat(this.map.containsKey(123)).isFalse();
}
@Test
void computeIfPresent() {
assertThat(this.map.computeIfPresent(123, (k, v) -> "123")).isNull();
this.map.put(123, "123");
assertThat(this.map.computeIfPresent(123, (k, v) -> v + "b")).isEqualTo("123b");
assertThat(this.map.get(123)).isEqualTo("123b");
assertThat(this.map.computeIfPresent(123, (k, v) -> null)).isNull();
assertThat(this.map.containsKey(123)).isFalse();
}
@Test
void compute() {
assertThat(this.map.compute(123, (k, v) -> "123" + v)).isEqualTo("123null");
assertThat(this.map.compute(123, (k, v) -> null)).isNull();
assertThat(this.map.compute(123, (k, v) -> null)).isNull();
assertThat(this.map.compute(123, (k, v) -> "123")).isEqualTo("123");
assertThat(this.map.compute(123, (k, v) -> v + "b")).isEqualTo("123b");
assertThat(this.map.get(123)).isEqualTo("123b");
}
@Test
void merge() {
assertThat(this.map.merge(123, "123", (v1, v2) -> v1 + v2)).isEqualTo("123");
assertThat(this.map.merge(123, null, (v1, v2) -> v1 + v2)).isEqualTo("123null");
assertThat(this.map.merge(123, null, (v1, v2) -> null)).isNull();
assertThat(this.map.merge(123, "123", (v1, v2) -> v1 + v2)).isEqualTo("123");
assertThat(this.map.merge(123, "b", (v1, v2) -> v1 + v2)).isEqualTo("123b");
assertThat(this.map.get(123)).isEqualTo("123b");
}
@Test
void size() {
assertThat(this.map).isEmpty();
this.map.put(123, "123");
this.map.put(123, null);
@@ -327,7 +367,7 @@ class ConcurrentReferenceHashMapTests {
}
@Test
void shouldSupportIsEmpty() {
void isEmpty() {
assertThat(this.map).isEmpty();
this.map.put(123, "123");
this.map.put(123, null);
@@ -336,7 +376,7 @@ class ConcurrentReferenceHashMapTests {
}
@Test
void shouldContainKey() {
void containsKey() {
assertThat(this.map.containsKey(123)).isFalse();
assertThat(this.map.containsKey(456)).isFalse();
this.map.put(123, "123");
@@ -346,7 +386,7 @@ class ConcurrentReferenceHashMapTests {
}
@Test
void shouldContainValue() {
void containsValue() {
assertThat(this.map.containsValue("123")).isFalse();
assertThat(this.map.containsValue(null)).isFalse();
this.map.put(123, "123");
@@ -356,7 +396,7 @@ class ConcurrentReferenceHashMapTests {
}
@Test
void shouldRemoveWhenKeyIsInMap() {
void removeWhenKeyIsInMap() {
this.map.put(123, null);
this.map.put(456, "456");
this.map.put(null, "789");
@@ -367,14 +407,14 @@ class ConcurrentReferenceHashMapTests {
}
@Test
void shouldRemoveWhenKeyIsNotInMap() {
void removeWhenKeyIsNotInMap() {
assertThat(this.map.remove(123)).isNull();
assertThat(this.map.remove(null)).isNull();
assertThat(this.map).isEmpty();
}
@Test
void shouldPutAll() {
void putAll() {
Map<Integer, String> m = new HashMap<>();
m.put(123, "123");
m.put(456, null);
@@ -387,7 +427,7 @@ class ConcurrentReferenceHashMapTests {
}
@Test
void shouldClear() {
void clear() {
this.map.put(123, "123");
this.map.put(456, null);
this.map.put(null, "789");
@@ -399,7 +439,7 @@ class ConcurrentReferenceHashMapTests {
}
@Test
void shouldGetKeySet() {
void keySet() {
this.map.put(123, "123");
this.map.put(456, null);
this.map.put(null, "789");
@@ -411,7 +451,7 @@ class ConcurrentReferenceHashMapTests {
}
@Test
void shouldGetValues() {
void valuesCollection() {
this.map.put(123, "123");
this.map.put(456, null);
this.map.put(null, "789");
@@ -426,7 +466,7 @@ class ConcurrentReferenceHashMapTests {
}
@Test
void shouldGetEntrySet() {
void getEntrySet() {
this.map.put(123, "123");
this.map.put(456, null);
this.map.put(null, "789");
@@ -438,7 +478,7 @@ class ConcurrentReferenceHashMapTests {
}
@Test
void shouldGetEntrySetFollowingNext() {
void getEntrySetFollowingNext() {
// Use loadFactor to disable resize
this.map = new TestWeakConcurrentCache<>(1, 10.0f, 1);
this.map.put(1, "1");
@@ -452,7 +492,7 @@ class ConcurrentReferenceHashMapTests {
}
@Test
void shouldRemoveViaEntrySet() {
void removeViaEntrySet() {
this.map.put(1, "1");
this.map.put(2, "2");
this.map.put(3, "3");
@@ -468,7 +508,7 @@ class ConcurrentReferenceHashMapTests {
}
@Test
void shouldSetViaEntrySet() {
void setViaEntrySet() {
this.map.put(1, "1");
this.map.put(2, "2");
this.map.put(3, "3");
@@ -502,7 +542,7 @@ class ConcurrentReferenceHashMapTests {
}
@Test
void shouldSupportNullReference() {
void supportNullReference() {
// GC could happen during restructure so we must be able to create a reference for a null entry
map.createReferenceManager().createReference(null, 1234, null);
}
@@ -126,6 +126,8 @@ public class DataSourceTransactionManager extends AbstractPlatformTransactionMan
private boolean enforceReadOnly = false;
private volatile @Nullable Boolean defaultReadOnly;
/**
* Create a new {@code DataSourceTransactionManager} instance.
@@ -270,13 +272,18 @@ public class DataSourceTransactionManager extends AbstractPlatformTransactionMan
if (logger.isDebugEnabled()) {
logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
}
if (definition.isReadOnly()) {
checkDefaultReadOnly(newCon);
}
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
}
txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
con = txObject.getConnectionHolder().getConnection();
Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con,
definition.getIsolationLevel(),
(definition.isReadOnly() && !isDefaultReadOnly()));
txObject.setPreviousIsolationLevel(previousIsolationLevel);
txObject.setReadOnly(definition.isReadOnly());
@@ -381,8 +388,9 @@ public class DataSourceTransactionManager extends AbstractPlatformTransactionMan
if (txObject.isMustRestoreAutoCommit()) {
con.setAutoCommit(true);
}
DataSourceUtils.resetConnectionAfterTransaction(
con, txObject.getPreviousIsolationLevel(), txObject.isReadOnly());
DataSourceUtils.resetConnectionAfterTransaction(con,
txObject.getPreviousIsolationLevel(),
(txObject.isReadOnly() && !isDefaultReadOnly()));
}
catch (Throwable ex) {
logger.debug("Could not reset JDBC Connection after transaction", ex);
@@ -399,6 +407,37 @@ public class DataSourceTransactionManager extends AbstractPlatformTransactionMan
}
/**
* Check the default {@link Connection#isReadOnly()} flag on a freshly
* obtained connection from the {@code DataSource}, assuming that the
* same flag applies to all connections obtained from the given setup.
* @param newCon the Connection to check
* @since 6.2.13
* @see #isDefaultReadOnly()
*/
private void checkDefaultReadOnly(Connection newCon) {
if (this.defaultReadOnly == null) {
try {
this.defaultReadOnly = newCon.isReadOnly();
}
catch (Throwable ex) {
logger.debug("Could not determine default JDBC Connection isReadOnly - assuming false", ex);
this.defaultReadOnly = false;
}
}
}
/**
* Check whether the default read-only flag has been determined as {@code true},
* assuming that all encountered connections will be read-only by default and
* therefore do not need explicit {@link Connection#setReadOnly} (re)setting.
* @since 6.2.13
* @see #checkDefaultReadOnly(Connection)
*/
private boolean isDefaultReadOnly() {
return (this.defaultReadOnly == Boolean.TRUE);
}
/**
* Prepare the transactional {@code Connection} right after transaction begin.
* <p>The default implementation executes a "SET TRANSACTION READ ONLY" statement
@@ -170,19 +170,38 @@ public abstract class DataSourceUtils {
* @param definition the transaction definition to apply
* @return the previous isolation level, if any
* @throws SQLException if thrown by JDBC methods
* @see #resetConnectionAfterTransaction
* @see #prepareConnectionForTransaction(Connection, int, boolean)
*/
@Nullable
public static Integer prepareConnectionForTransaction(Connection con, @Nullable TransactionDefinition definition)
throws SQLException {
return prepareConnectionForTransaction(con,
(definition != null ? definition.getIsolationLevel() : TransactionDefinition.ISOLATION_DEFAULT),
(definition != null && definition.isReadOnly()));
}
/**
* Prepare the given Connection with the given transaction semantics.
* @param con the Connection to prepare
* @param isolationLevel the isolation level to apply
* @param setReadOnly whether to set the read-only flag
* @return the previous isolation level, if any
* @throws SQLException if thrown by JDBC methods
* @since 6.2.13
* @see #resetConnectionAfterTransaction(Connection, Integer, boolean)
* @see Connection#setTransactionIsolation
* @see Connection#setReadOnly
*/
@Nullable
public static Integer prepareConnectionForTransaction(Connection con, @Nullable TransactionDefinition definition)
static Integer prepareConnectionForTransaction(Connection con, int isolationLevel, boolean setReadOnly)
throws SQLException {
Assert.notNull(con, "No Connection specified");
boolean debugEnabled = logger.isDebugEnabled();
// Set read-only flag.
if (definition != null && definition.isReadOnly()) {
if (setReadOnly) {
try {
if (debugEnabled) {
logger.debug("Setting JDBC Connection [" + con + "] read-only");
@@ -205,15 +224,14 @@ public abstract class DataSourceUtils {
// Apply specific isolation level, if any.
Integer previousIsolationLevel = null;
if (definition != null && definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) {
if (isolationLevel != TransactionDefinition.ISOLATION_DEFAULT) {
if (debugEnabled) {
logger.debug("Changing isolation level of JDBC Connection [" + con + "] to " +
definition.getIsolationLevel());
logger.debug("Changing isolation level of JDBC Connection [" + con + "] to " + isolationLevel);
}
int currentIsolation = con.getTransactionIsolation();
if (currentIsolation != definition.getIsolationLevel()) {
if (currentIsolation != isolationLevel) {
previousIsolationLevel = currentIsolation;
con.setTransactionIsolation(definition.getIsolationLevel());
con.setTransactionIsolation(isolationLevel);
}
}
@@ -153,6 +153,9 @@ public class LazyConnectionDataSourceProxy extends DelegatingDataSource {
*/
public void setReadOnlyDataSource(@Nullable DataSource readOnlyDataSource) {
this.readOnlyDataSource = readOnlyDataSource;
if (getTargetDataSource() == null) {
setTargetDataSource(readOnlyDataSource);
}
}
/**
@@ -395,7 +398,7 @@ public class LazyConnectionDataSourceProxy extends DelegatingDataSource {
return null;
}
case "isReadOnly" -> {
return this.readOnly;
return (this.readOnly || getTargetDataSource() == readOnlyDataSource);
}
case "setReadOnly" -> {
this.readOnly = (Boolean) args[0];
@@ -25,9 +25,7 @@ import org.mockito.InOrder;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.jdbc.datasource.DataSourceTransactionManagerTests;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.TransactionSystemException;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.transaction.support.TransactionTemplate;
@@ -53,36 +51,14 @@ class JdbcTransactionManagerTests extends DataSourceTransactionManagerTests {
@Override
@Test
protected void testTransactionWithExceptionOnCommit() throws Exception {
protected void transactionWithExceptionOnCommit() throws Exception {
willThrow(new SQLException("Cannot commit")).given(con).commit();
TransactionTemplate tt = new TransactionTemplate(tm);
// plain TransactionSystemException
assertThatExceptionOfType(TransactionSystemException.class).isThrownBy(() ->
tt.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
tt.executeWithoutResult(status -> {
// something transactional
}
}));
assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse();
verify(con).close();
}
@Test
void testTransactionWithDataAccessExceptionOnCommit() throws Exception {
willThrow(new SQLException("Cannot commit")).given(con).commit();
((JdbcTransactionManager) tm).setExceptionTranslator((task, sql, ex) -> new ConcurrencyFailureException(task));
TransactionTemplate tt = new TransactionTemplate(tm);
// specific ConcurrencyFailureException
assertThatExceptionOfType(ConcurrencyFailureException.class).isThrownBy(() ->
tt.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
// something transactional
}
}));
assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse();
@@ -90,17 +66,30 @@ class JdbcTransactionManagerTests extends DataSourceTransactionManagerTests {
}
@Test
void testTransactionWithDataAccessExceptionOnCommitFromLazyExceptionTranslator() throws Exception {
void transactionWithDataAccessExceptionOnCommit() throws Exception {
willThrow(new SQLException("Cannot commit")).given(con).commit();
((JdbcTransactionManager) tm).setExceptionTranslator((task, sql, ex) -> new ConcurrencyFailureException(task));
TransactionTemplate tt = new TransactionTemplate(tm);
// specific ConcurrencyFailureException
assertThatExceptionOfType(ConcurrencyFailureException.class).isThrownBy(() ->
tt.executeWithoutResult(status -> {
// something transactional
}));
assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse();
verify(con).close();
}
@Test
void transactionWithDataAccessExceptionOnCommitFromLazyExceptionTranslator() throws Exception {
willThrow(new SQLException("Cannot commit", "40")).given(con).commit();
TransactionTemplate tt = new TransactionTemplate(tm);
// specific ConcurrencyFailureException
assertThatExceptionOfType(ConcurrencyFailureException.class).isThrownBy(() ->
tt.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
// something transactional
}
tt.executeWithoutResult(status -> {
// something transactional
}));
assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse();
@@ -109,7 +98,7 @@ class JdbcTransactionManagerTests extends DataSourceTransactionManagerTests {
@Override
@Test
protected void testTransactionWithExceptionOnCommitAndRollbackOnCommitFailure() throws Exception {
protected void transactionWithExceptionOnCommitAndRollbackOnCommitFailure() throws Exception {
willThrow(new SQLException("Cannot commit")).given(con).commit();
tm.setRollbackOnCommitFailure(true);
@@ -117,12 +106,9 @@ class JdbcTransactionManagerTests extends DataSourceTransactionManagerTests {
// plain TransactionSystemException
assertThatExceptionOfType(TransactionSystemException.class).isThrownBy(() ->
tt.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
tt.executeWithoutResult(status -> {
// something transactional
}
}));
}));
assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse();
verify(con).rollback();
@@ -131,16 +117,14 @@ class JdbcTransactionManagerTests extends DataSourceTransactionManagerTests {
@Override
@Test
protected void testTransactionWithExceptionOnRollback() throws Exception {
protected void transactionWithExceptionOnRollback() throws Exception {
given(con.getAutoCommit()).willReturn(true);
willThrow(new SQLException("Cannot rollback")).given(con).rollback();
TransactionTemplate tt = new TransactionTemplate(tm);
// plain TransactionSystemException
assertThatExceptionOfType(TransactionSystemException.class).isThrownBy(() ->
tt.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException {
tt.executeWithoutResult(status -> {
assertThat(status.getTransactionName()).isEmpty();
assertThat(status.hasTransaction()).isTrue();
assertThat(status.isNewTransaction()).isTrue();
@@ -151,31 +135,6 @@ class JdbcTransactionManagerTests extends DataSourceTransactionManagerTests {
status.setRollbackOnly();
assertThat(status.isRollbackOnly()).isTrue();
assertThat(status.isCompleted()).isFalse();
}
}));
assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse();
InOrder ordered = inOrder(con);
ordered.verify(con).setAutoCommit(false);
ordered.verify(con).rollback();
ordered.verify(con).setAutoCommit(true);
verify(con).close();
}
@Test
void testTransactionWithDataAccessExceptionOnRollback() throws Exception {
given(con.getAutoCommit()).willReturn(true);
willThrow(new SQLException("Cannot rollback")).given(con).rollback();
((JdbcTransactionManager) tm).setExceptionTranslator((task, sql, ex) -> new ConcurrencyFailureException(task));
TransactionTemplate tt = new TransactionTemplate(tm);
// specific ConcurrencyFailureException
assertThatExceptionOfType(ConcurrencyFailureException.class).isThrownBy(() ->
tt.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException {
status.setRollbackOnly();
}
}));
assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse();
@@ -187,27 +146,43 @@ class JdbcTransactionManagerTests extends DataSourceTransactionManagerTests {
}
@Test
void testTransactionWithDataAccessExceptionOnRollbackFromLazyExceptionTranslator() throws Exception {
void transactionWithDataAccessExceptionOnRollback() throws Exception {
given(con.getAutoCommit()).willReturn(true);
willThrow(new SQLException("Cannot rollback")).given(con).rollback();
((JdbcTransactionManager) tm).setExceptionTranslator((task, sql, ex) -> new ConcurrencyFailureException(task));
TransactionTemplate tt = new TransactionTemplate(tm);
// specific ConcurrencyFailureException
assertThatExceptionOfType(ConcurrencyFailureException.class).isThrownBy(() ->
tt.executeWithoutResult(status -> status.setRollbackOnly()));
assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse();
InOrder ordered = inOrder(con);
ordered.verify(con).setAutoCommit(false);
ordered.verify(con).rollback();
ordered.verify(con).setAutoCommit(true);
verify(con).close();
}
@Test
void transactionWithDataAccessExceptionOnRollbackFromLazyExceptionTranslator() throws Exception {
given(con.getAutoCommit()).willReturn(true);
willThrow(new SQLException("Cannot rollback", "40")).given(con).rollback();
TransactionTemplate tt = new TransactionTemplate(tm);
// specific ConcurrencyFailureException
assertThatExceptionOfType(ConcurrencyFailureException.class).isThrownBy(() ->
tt.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException {
assertThat(status.getTransactionName()).isEmpty();
assertThat(status.hasTransaction()).isTrue();
assertThat(status.isNewTransaction()).isTrue();
assertThat(status.isNested()).isFalse();
assertThat(status.hasSavepoint()).isFalse();
assertThat(status.isReadOnly()).isFalse();
assertThat(status.isRollbackOnly()).isFalse();
status.setRollbackOnly();
assertThat(status.isRollbackOnly()).isTrue();
assertThat(status.isCompleted()).isFalse();
}
tt.executeWithoutResult(status -> {
assertThat(status.getTransactionName()).isEmpty();
assertThat(status.hasTransaction()).isTrue();
assertThat(status.isNewTransaction()).isTrue();
assertThat(status.isNested()).isFalse();
assertThat(status.hasSavepoint()).isFalse();
assertThat(status.isReadOnly()).isFalse();
assertThat(status.isRollbackOnly()).isFalse();
status.setRollbackOnly();
assertThat(status.isRollbackOnly()).isTrue();
assertThat(status.isCompleted()).isFalse();
}));
assertThat(TransactionSynchronizationManager.hasResource(ds)).isFalse();
@@ -64,14 +64,13 @@ import org.springframework.util.ReflectionUtils;
* @author Sebastien Deleuze
* @since 6.0
*/
@SuppressWarnings("unchecked")
class PersistenceManagedTypesBeanRegistrationAotProcessor implements BeanRegistrationAotProcessor {
private static final boolean jpaPresent = ClassUtils.isPresent("jakarta.persistence.Entity",
PersistenceManagedTypesBeanRegistrationAotProcessor.class.getClassLoader());
@Nullable
@Override
@Nullable
public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) {
if (jpaPresent) {
if (PersistenceManagedTypes.class.isAssignableFrom(registeredBean.getBeanClass())) {
@@ -82,12 +81,12 @@ class PersistenceManagedTypesBeanRegistrationAotProcessor implements BeanRegistr
return null;
}
private static final class JpaManagedTypesBeanRegistrationCodeFragments extends BeanRegistrationCodeFragmentsDecorator {
private static final List<Class<? extends Annotation>> CALLBACK_TYPES = List.of(PreUpdate.class,
PostUpdate.class, PrePersist.class, PostPersist.class, PreRemove.class, PostRemove.class, PostLoad.class);
private static final ParameterizedTypeName LIST_OF_STRINGS_TYPE = ParameterizedTypeName.get(List.class, String.class);
private final RegisteredBean registeredBean;
@@ -102,8 +101,8 @@ class PersistenceManagedTypesBeanRegistrationAotProcessor implements BeanRegistr
@Override
public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext,
BeanRegistrationCode beanRegistrationCode,
boolean allowDirectSupplierShortcut) {
BeanRegistrationCode beanRegistrationCode, boolean allowDirectSupplierShortcut) {
PersistenceManagedTypes persistenceManagedTypes = this.registeredBean.getBeanFactory()
.getBean(this.registeredBean.getBeanName(), PersistenceManagedTypes.class);
contributeHints(generationContext.getRuntimeHints(),
@@ -138,9 +137,10 @@ class PersistenceManagedTypesBeanRegistrationAotProcessor implements BeanRegistr
contributeConverterHints(hints, managedClass);
contributeCallbackHints(hints, managedClass);
contributeHibernateHints(hints, classLoader, managedClass);
contributePackagePrivateHints(hints, managedClass);
}
catch (ClassNotFoundException ex) {
throw new IllegalArgumentException("Failed to instantiate the managed class: " + managedClassName, ex);
throw new IllegalArgumentException("Failed to instantiate JPA managed class: " + managedClassName, ex);
}
}
}
@@ -149,7 +149,8 @@ class PersistenceManagedTypesBeanRegistrationAotProcessor implements BeanRegistr
EntityListeners entityListeners = AnnotationUtils.findAnnotation(managedClass, EntityListeners.class);
if (entityListeners != null) {
for (Class<?> entityListener : entityListeners.value()) {
hints.reflection().registerType(entityListener, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS);
hints.reflection().registerType(entityListener,
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS);
}
}
}
@@ -169,12 +170,14 @@ class PersistenceManagedTypesBeanRegistrationAotProcessor implements BeanRegistr
}
Convert convertClassAnnotation = AnnotationUtils.findAnnotation(managedClass, Convert.class);
if (convertClassAnnotation != null) {
reflectionHints.registerType(convertClassAnnotation.converter(), MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
reflectionHints.registerType(convertClassAnnotation.converter(),
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
}
ReflectionUtils.doWithFields(managedClass, field -> {
Convert convertFieldAnnotation = AnnotationUtils.findAnnotation(field, Convert.class);
if (convertFieldAnnotation != null && convertFieldAnnotation.converter() != void.class) {
reflectionHints.registerType(convertFieldAnnotation.converter(), MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
reflectionHints.registerType(convertFieldAnnotation.converter(),
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
}
});
}
@@ -186,11 +189,11 @@ class PersistenceManagedTypesBeanRegistrationAotProcessor implements BeanRegistr
method -> CALLBACK_TYPES.stream().anyMatch(method::isAnnotationPresent));
}
@SuppressWarnings("unchecked")
private void contributeHibernateHints(RuntimeHints hints, @Nullable ClassLoader classLoader, Class<?> managedClass) {
ReflectionHints reflection = hints.reflection();
Class<? extends Annotation> embeddableInstantiatorClass = loadClass("org.hibernate.annotations.EmbeddableInstantiator", classLoader);
Class<? extends Annotation> embeddableInstantiatorClass =
loadClass("org.hibernate.annotations.EmbeddableInstantiator", classLoader);
if (embeddableInstantiatorClass != null) {
registerForReflection(reflection,
AnnotationUtils.findAnnotation(managedClass, embeddableInstantiatorClass), "value");
@@ -204,7 +207,8 @@ class PersistenceManagedTypesBeanRegistrationAotProcessor implements BeanRegistr
AnnotationUtils.findAnnotation(method, embeddableInstantiatorClass), "value"));
}
Class<? extends Annotation> valueGenerationTypeClass = loadClass("org.hibernate.annotations.ValueGenerationType", classLoader);
Class<? extends Annotation> valueGenerationTypeClass =
loadClass("org.hibernate.annotations.ValueGenerationType", classLoader);
if (valueGenerationTypeClass != null) {
ReflectionUtils.doWithFields(managedClass, field -> registerForReflection(reflection,
AnnotationUtils.findAnnotation(field, valueGenerationTypeClass), "generatedBy"));
@@ -212,7 +216,8 @@ class PersistenceManagedTypesBeanRegistrationAotProcessor implements BeanRegistr
AnnotationUtils.findAnnotation(method, valueGenerationTypeClass), "generatedBy"));
}
Class<? extends Annotation> idGeneratorTypeClass = loadClass("org.hibernate.annotations.IdGeneratorType", classLoader);
Class<? extends Annotation> idGeneratorTypeClass =
loadClass("org.hibernate.annotations.IdGeneratorType", classLoader);
if (idGeneratorTypeClass != null) {
ReflectionUtils.doWithFields(managedClass, field -> registerForReflection(reflection,
AnnotationUtils.findAnnotation(field, idGeneratorTypeClass), "value"));
@@ -220,7 +225,8 @@ class PersistenceManagedTypesBeanRegistrationAotProcessor implements BeanRegistr
AnnotationUtils.findAnnotation(method, idGeneratorTypeClass), "value"));
}
Class<? extends Annotation> attributeBinderTypeClass = loadClass("org.hibernate.annotations.AttributeBinderType", classLoader);
Class<? extends Annotation> attributeBinderTypeClass =
loadClass("org.hibernate.annotations.AttributeBinderType", classLoader);
if (attributeBinderTypeClass != null) {
ReflectionUtils.doWithFields(managedClass, field -> registerForReflection(reflection,
AnnotationUtils.findAnnotation(field, attributeBinderTypeClass), "binder"));
@@ -229,6 +235,19 @@ class PersistenceManagedTypesBeanRegistrationAotProcessor implements BeanRegistr
}
}
private void contributePackagePrivateHints(RuntimeHints hints, Class<?> managedClass) {
ReflectionHints reflection = hints.reflection();
ReflectionUtils.doWithMethods(managedClass, method ->
reflection.registerMethod(method, ExecutableMode.INVOKE),
method -> {
int modifiers = method.getModifiers();
return !(java.lang.reflect.Modifier.isProtected(modifiers) ||
java.lang.reflect.Modifier.isPrivate(modifiers) ||
java.lang.reflect.Modifier.isPublic(modifiers));
});
}
@SuppressWarnings("unchecked")
@Nullable
private static Class<? extends Annotation> loadClass(String className, @Nullable ClassLoader classLoader) {
try {
@@ -239,13 +258,14 @@ class PersistenceManagedTypesBeanRegistrationAotProcessor implements BeanRegistr
}
}
@SuppressWarnings("NullAway")
@SuppressWarnings("NullAway") // Not-null assertion performed in ReflectionHints.registerType
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);
Class<?> type = (Class<?>) AnnotationUtils.getAnnotationAttributes(annotation).get(attribute);
reflection.registerType(type, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
}
}
}
@@ -20,7 +20,6 @@ import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
@@ -429,7 +428,7 @@ public class PersistenceAnnotationBeanPostProcessor implements InstantiationAwar
}
List<InjectionMetadata.InjectedElement> elements = new ArrayList<>();
Class<?> targetClass = clazz;
Class<?> targetClass = ClassUtils.getUserClass(clazz);
do {
final List<InjectionMetadata.InjectedElement> currElements = new ArrayList<>();
@@ -445,21 +444,20 @@ public class PersistenceAnnotationBeanPostProcessor implements InstantiationAwar
});
ReflectionUtils.doWithLocalMethods(targetClass, method -> {
Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
if (method.isBridge()) {
return;
}
if ((bridgedMethod.isAnnotationPresent(PersistenceContext.class) ||
bridgedMethod.isAnnotationPresent(PersistenceUnit.class)) &&
method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
if ((method.isAnnotationPresent(PersistenceContext.class) ||
method.isAnnotationPresent(PersistenceUnit.class)) &&
method.equals(BridgeMethodResolver.getMostSpecificMethod(method, clazz))) {
if (Modifier.isStatic(method.getModifiers())) {
throw new IllegalStateException("Persistence annotations are not supported on static methods");
}
if (method.getParameterCount() != 1) {
throw new IllegalStateException("Persistence annotation requires a single-arg method: " + method);
}
PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
currElements.add(new PersistenceElement(method, bridgedMethod, pd));
PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method, clazz);
currElements.add(new PersistenceElement(method, method, pd));
}
});
@@ -0,0 +1,50 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.orm.jpa.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Car {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
@Column
private String model;
Integer getId() {
return id;
}
void setId(Integer id) {
this.id = id;
}
void setModel(String model) {
this.model = model;
}
String getModel() {
return model;
}
}
@@ -38,6 +38,7 @@ import org.springframework.core.test.tools.Compiled;
import org.springframework.core.test.tools.TestCompiler;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.domain.Car;
import org.springframework.orm.jpa.domain.DriversLicense;
import org.springframework.orm.jpa.domain.Employee;
import org.springframework.orm.jpa.domain.EmployeeCategoryConverter;
@@ -66,13 +67,12 @@ class PersistenceManagedTypesBeanRegistrationAotProcessorTests {
GenericApplicationContext context = new AnnotationConfigApplicationContext();
context.registerBean(JpaDomainConfiguration.class);
compile(context, (initializer, compiled) -> {
GenericApplicationContext freshApplicationContext = toFreshApplicationContext(
initializer);
GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer);
PersistenceManagedTypes persistenceManagedTypes = freshApplicationContext.getBean(
"persistenceManagedTypes", PersistenceManagedTypes.class);
assertThat(persistenceManagedTypes.getManagedClassNames()).containsExactlyInAnyOrder(
DriversLicense.class.getName(), Person.class.getName(), Employee.class.getName(),
EmployeeLocationConverter.class.getName());
EmployeeLocationConverter.class.getName(), Car.class.getName());
assertThat(persistenceManagedTypes.getManagedPackages()).isEmpty();
assertThat(freshApplicationContext.getBean(
JpaDomainConfiguration.class).scanningInvoked).isFalse();
@@ -105,6 +105,14 @@ class PersistenceManagedTypesBeanRegistrationAotProcessorTests {
.withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(hints);
assertThat(RuntimeHintsPredicates.reflection().onType(EmployeeLocation.class)
.withMemberCategories(MemberCategory.DECLARED_FIELDS)).accepts(hints);
assertThat(RuntimeHintsPredicates.reflection().onMethod(Car.class, "setId")
.invoke()).accepts(hints);
assertThat(RuntimeHintsPredicates.reflection().onMethod(Car.class, "getId")
.invoke()).accepts(hints);
assertThat(RuntimeHintsPredicates.reflection().onMethod(Car.class, "setModel")
.invoke()).accepts(hints);
assertThat(RuntimeHintsPredicates.reflection().onMethod(Car.class, "getModel")
.invoke()).accepts(hints);
});
}
@@ -121,6 +129,7 @@ class PersistenceManagedTypesBeanRegistrationAotProcessorTests {
@SuppressWarnings("unchecked")
private void compile(GenericApplicationContext applicationContext,
BiConsumer<ApplicationContextInitializer<GenericApplicationContext>, Compiled> result) {
ApplicationContextAotGenerator generator = new ApplicationContextAotGenerator();
TestGenerationContext generationContext = new TestGenerationContext();
generator.processAheadOfTime(applicationContext, generationContext);
@@ -131,6 +140,7 @@ class PersistenceManagedTypesBeanRegistrationAotProcessorTests {
private GenericApplicationContext toFreshApplicationContext(
ApplicationContextInitializer<GenericApplicationContext> initializer) {
GenericApplicationContext freshApplicationContext = new GenericApplicationContext();
initializer.initialize(freshApplicationContext);
freshApplicationContext.refresh();
@@ -144,21 +154,6 @@ class PersistenceManagedTypesBeanRegistrationAotProcessorTests {
result.accept(generationContext.getRuntimeHints());
}
public static class JpaDomainConfiguration extends AbstractEntityManagerWithPackagesToScanConfiguration {
@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 {
@@ -179,13 +174,13 @@ class PersistenceManagedTypesBeanRegistrationAotProcessorTests {
@Bean
public PersistenceManagedTypes persistenceManagedTypes(ResourceLoader resourceLoader) {
this.scanningInvoked = true;
return new PersistenceManagedTypesScanner(resourceLoader)
.scan(packageToScan());
return new PersistenceManagedTypesScanner(resourceLoader).scan(packageToScan());
}
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource,
JpaVendorAdapter jpaVendorAdapter, PersistenceManagedTypes persistenceManagedTypes) {
LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
entityManagerFactoryBean.setDataSource(dataSource);
entityManagerFactoryBean.setJpaVendorAdapter(jpaVendorAdapter);
@@ -194,7 +189,24 @@ class PersistenceManagedTypesBeanRegistrationAotProcessorTests {
}
protected abstract String packageToScan();
}
public static class JpaDomainConfiguration extends AbstractEntityManagerWithPackagesToScanConfiguration {
@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";
}
}
}
@@ -23,6 +23,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.context.testfixture.index.CandidateComponentsTestClassLoader;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.orm.jpa.domain.Car;
import org.springframework.orm.jpa.domain.DriversLicense;
import org.springframework.orm.jpa.domain.Employee;
import org.springframework.orm.jpa.domain.EmployeeLocationConverter;
@@ -52,7 +53,7 @@ class PersistenceManagedTypesScannerTests {
PersistenceManagedTypes managedTypes = this.scanner.scan("org.springframework.orm.jpa.domain");
assertThat(managedTypes.getManagedClassNames()).containsExactlyInAnyOrder(
Person.class.getName(), DriversLicense.class.getName(), Employee.class.getName(),
EmployeeLocationConverter.class.getName());
EmployeeLocationConverter.class.getName(), Car.class.getName());
assertThat(managedTypes.getManagedPackages()).isEmpty();
}
@@ -66,6 +67,7 @@ class PersistenceManagedTypesScannerTests {
verify(filter).matches(DriversLicense.class.getName());
verify(filter).matches(Employee.class.getName());
verify(filter).matches(EmployeeLocationConverter.class.getName());
verify(filter).matches(Car.class.getName());
verifyNoMoreInteractions(filter);
}
@@ -72,6 +72,7 @@ class AfterTestClassSqlScriptsTests extends AbstractTransactionalTests {
}
@Nested
@TestMethodOrder(OrderAnnotation.class)
@Sql(scripts = "recreate-schema.sql", executionPhase = BEFORE_TEST_CLASS)
@Sql(scripts = "drop-schema.sql", executionPhase = AFTER_TEST_CLASS)
class NestedAfterTestClassSqlScriptsTests {
@@ -65,11 +65,9 @@ class AutowiredConfigurationErrorsIntegrationTests {
@ParameterizedTest
@ValueSource(classes = {
StaticAutowiredBeforeAllMethod.class,
StaticAutowiredPrivateBeforeAllMethod.class,
StaticAutowiredAfterAllMethod.class,
AutowiredBeforeEachMethod.class,
AutowiredAfterEachMethod.class,
AutowiredPrivateAfterEachMethod.class,
AutowiredTestMethod.class,
AutowiredRepeatedTestMethod.class,
AutowiredParameterizedTestMethod.class
@@ -168,21 +166,6 @@ class AutowiredConfigurationErrorsIntegrationTests {
}
}
@SpringJUnitConfig(Config.class)
@FailingTestCase
static class StaticAutowiredPrivateBeforeAllMethod {
@Autowired
@BeforeAll
private static void beforeAll(TestInfo testInfo) {
}
@Test
@DisplayName(DISPLAY_NAME)
void test() {
}
}
@SpringJUnitConfig(Config.class)
@TestInstance(PER_CLASS)
@FailingTestCase
@@ -260,22 +243,6 @@ class AutowiredConfigurationErrorsIntegrationTests {
}
}
@SpringJUnitConfig(Config.class)
@FailingTestCase
static class AutowiredPrivateAfterEachMethod {
@Test
@DisplayName(DISPLAY_NAME)
void test() {
}
@Autowired
@AfterEach
private void afterEach(TestInfo testInfo) {
}
}
@SpringJUnitConfig(Config.class)
@FailingTestCase
static class AutowiredTestMethod {
@@ -17,11 +17,8 @@
package org.springframework.test.context.web;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.platform.testkit.engine.EngineTestKit;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
/**
@@ -32,7 +29,6 @@ import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass
* @author Sam Brannen
* @since 4.0.2
*/
@ExtendWith(SpringExtension.class)
class ServletContextAwareBeanWacTests {
@Test
@@ -17,22 +17,16 @@
package org.springframework.test.web.client.samples;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.stereotype.Controller;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig;
import org.springframework.test.web.client.MockMvcClientHttpRequestFactory;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
@@ -52,30 +46,24 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
* @author Rossen Stoyanchev
* @author Juergen Hoeller
*/
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration
public class MockMvcClientHttpRequestFactoryTests {
@SpringJUnitWebConfig
class MockMvcClientHttpRequestFactoryTests {
@Autowired
private WebApplicationContext wac;
private final RestTemplate template;
private RestTemplate template;
@BeforeEach
public void setup() {
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
MockMvcClientHttpRequestFactoryTests(WebApplicationContext wac) {
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
this.template = new RestTemplate(new MockMvcClientHttpRequestFactory(mockMvc));
}
@Test
public void withResult() {
void withResult() {
assertThat(template.getForObject("/foo", String.class)).isEqualTo("bar");
}
@Test
public void withError() {
void withError() {
assertThatExceptionOfType(HttpClientErrorException.class)
.isThrownBy(() -> template.getForEntity("/error", String.class))
.withMessageContaining("400")
@@ -83,7 +71,7 @@ public class MockMvcClientHttpRequestFactoryTests {
}
@Test
public void withErrorAndBody() {
void withErrorAndBody() {
assertThatExceptionOfType(HttpClientErrorException.class)
.isThrownBy(() -> template.getForEntity("/errorbody", String.class))
.withMessageContaining("400")
@@ -92,27 +80,27 @@ public class MockMvcClientHttpRequestFactoryTests {
@EnableWebMvc
@Configuration
@ComponentScan(basePackageClasses = MockMvcClientHttpRequestFactoryTests.class)
@Configuration(proxyBeanMethods = false)
@Import(MyController.class)
static class MyWebConfig implements WebMvcConfigurer {
}
@Controller
static class MyController {
@RequestMapping(value = "/foo", method = RequestMethod.GET)
@GetMapping("/foo")
@ResponseBody
public String handle() {
String handle() {
return "bar";
}
@RequestMapping(value = "/error", method = RequestMethod.GET)
public void handleError(HttpServletResponse response) throws Exception {
@GetMapping("/error")
void handleError(HttpServletResponse response) throws Exception {
response.sendError(400, "some bad request");
}
@RequestMapping(value = "/errorbody", method = RequestMethod.GET)
public void handleErrorWithBody(HttpServletResponse response) throws Exception {
@GetMapping("/errorbody")
void handleErrorWithBody(HttpServletResponse response) throws Exception {
response.sendError(400, "some bad request");
response.getWriter().write("some really bad request");
}
@@ -19,15 +19,17 @@ package org.springframework.transaction.annotation;
import org.springframework.transaction.TransactionManager;
/**
* Interface to be implemented by @{@link org.springframework.context.annotation.Configuration
* Configuration} classes annotated with @{@link EnableTransactionManagement} that wish to
* (or need to) explicitly specify the default {@code PlatformTransactionManager} bean
* (or {@code ReactiveTransactionManager} bean) to be used for annotation-driven
* transaction management, as opposed to the default approach of a by-type lookup.
* One reason this might be necessary is if there are two {@code PlatformTransactionManager}
* beans (or two {@code ReactiveTransactionManager} beans) present in the container.
* Interface to be implemented for explicitly specifying the default
* {@link org.springframework.transaction.PlatformTransactionManager} bean
* (or {@link org.springframework.transaction.ReactiveTransactionManager} bean)
* to be used for annotation-driven transaction management, as opposed to the
* default approach of a by-type lookup. One reason this might be necessary is
* if there are two {@code PlatformTransactionManager} beans (or two
* {@code ReactiveTransactionManager} beans) present in the container.
*
* <p>See @{@link EnableTransactionManagement} for general examples and context;
* <p>Typically implemented by @{@link org.springframework.context.annotation.Configuration
* Configuration} classes annotated with @{@link EnableTransactionManagement}.
* See @{@link EnableTransactionManagement} for general examples and context;
* see {@link #annotationDrivenTransactionManager()} for detailed instructions.
*
* <p><b>NOTE: A {@code TransactionManagementConfigurer} will get initialized early.</b>
@@ -25,6 +25,7 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.Aware;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.core.MethodClassKey;
import org.springframework.lang.Nullable;
@@ -166,6 +167,10 @@ public abstract class AbstractFallbackTransactionAttributeSource
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
// Skip methods declared on BeanFactoryAware and co.
if (method.getDeclaringClass().isInterface() && Aware.class.isAssignableFrom(method.getDeclaringClass())) {
return null;
}
// The method may be on an interface, but we need attributes from the target class.
// If the target class is null, the method will be unchanged.
@@ -20,10 +20,16 @@ import java.io.IOException;
import java.io.OutputStream;
/**
* Represents an HTTP output message that allows for setting a streaming body.
* Note that such messages typically do not support {@link #getBody()} access.
* Contract for {@code HttpOutputMessage} implementations to expose the ability
* to stream request body content by writing to an {@link OutputStream} from
* a callback.
*
* <p>The {@link #setBody(Body)} method provides the option to stream, and is
* mutually exclusive use of {@link #getBody()}, which instead returns an
* {@code OutputStream} that aggregates the request body before sending it.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @since 4.0
* @see #setBody
*/
@@ -31,15 +37,17 @@ public interface StreamingHttpOutputMessage extends HttpOutputMessage {
/**
* Set the streaming body callback for this message.
* <p>Note that this is mutually exclusive with {@link #getBody()}, which
* may instead aggregate the request body before sending it.
* @param body the streaming body callback
*/
void setBody(Body body);
/**
* Defines the contract for bodies that can be written directly to an
* {@link OutputStream}. Useful with HTTP client libraries that provide
* indirect access to an {@link OutputStream} via a callback mechanism.
* Contract to stream request body content to an {@link OutputStream}.
* In some HTTP client libraries this is only possible indirectly through a
* callback mechanism.
*/
@FunctionalInterface
interface Body {
@@ -20,17 +20,24 @@ import java.io.IOException;
import java.io.OutputStream;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.StreamingHttpOutputMessage;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.FastByteArrayOutputStream;
/**
* Abstract base for {@link ClientHttpRequest} that also implement
* {@link StreamingHttpOutputMessage}. Ensures that headers and
* body are not written multiple times.
* Extension of {@link AbstractClientHttpRequest} that adds the ability to stream
* request body content directly to the underlying HTTP client library through
* the {@link StreamingHttpOutputMessage} contract.
*
* <p>It is necessary to call {@link #setBody} and stream the request body through
* a callback for access to the {@code OutputStream}. The alternative to call
* {@link #getBody()} is also supported as a fallback, but that does not stream,
* and returns an aggregating {@code OutputStream} instead.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @since 6.1
*/
abstract class AbstractStreamingClientHttpRequest extends AbstractClientHttpRequest
@@ -43,6 +50,12 @@ abstract class AbstractStreamingClientHttpRequest extends AbstractClientHttpRequ
private FastByteArrayOutputStream bodyStream;
/**
* Implements the {@link HttpOutputMessage} contract for request body content.
* <p>Note that this method does not result in streaming, and the returned
* {@code OutputStream} aggregates the full content in a byte[] before
* sending. To use streaming, call {@link #setBody} instead.
*/
@Override
protected final OutputStream getBodyInternal(HttpHeaders headers) {
Assert.state(this.body == null, "Invoke either getBody or setBody; not both");
@@ -53,6 +66,10 @@ abstract class AbstractStreamingClientHttpRequest extends AbstractClientHttpRequ
return this.bodyStream;
}
/**
* Implements the {@link StreamingHttpOutputMessage} contract for writing
* request body by streaming directly to the underlying HTTP client.
*/
@Override
public final void setBody(Body body) {
Assert.notNull(body, "Body must not be null");
@@ -73,12 +90,14 @@ abstract class AbstractStreamingClientHttpRequest extends AbstractClientHttpRequ
/**
* Abstract template method that writes the given headers and content to the HTTP request.
* @param headers the HTTP headers
* Abstract method for concrete implementations to write the headers and
* {@link StreamingHttpOutputMessage.Body} to the HTTP request.
* @param headers the HTTP headers for the request
* @param body the HTTP body, may be {@code null} if no body was {@linkplain #setBody(Body) set}
* @return the response object for the executed request
* @since 6.1
*/
protected abstract ClientHttpResponse executeInternal(HttpHeaders headers, @Nullable Body body) throws IOException;
protected abstract ClientHttpResponse executeInternal(
HttpHeaders headers, @Nullable Body body) throws IOException;
}
@@ -30,6 +30,7 @@ import org.springframework.util.StreamUtils;
* into memory, thus allowing for multiple invocations of {@link #getBody()}.
*
* @author Arjen Poutsma
* @author Juergen Hoeller
* @since 3.1
*/
final class BufferingClientHttpResponseWrapper implements ClientHttpResponse {
@@ -37,7 +38,7 @@ final class BufferingClientHttpResponseWrapper implements ClientHttpResponse {
private final ClientHttpResponse response;
@Nullable
private byte[] body;
private volatile byte[] body;
BufferingClientHttpResponseWrapper(ClientHttpResponse response) {
@@ -62,10 +63,17 @@ final class BufferingClientHttpResponseWrapper implements ClientHttpResponse {
@Override
public InputStream getBody() throws IOException {
if (this.body == null) {
this.body = StreamUtils.copyToByteArray(this.response.getBody());
byte[] body = this.body;
if (body == null) {
synchronized (this) {
body = this.body;
if (body == null) {
body = StreamUtils.copyToByteArray(this.response.getBody());
this.body = body;
}
}
}
return new ByteArrayInputStream(this.body);
return new ByteArrayInputStream(body);
}
@Override
@@ -33,8 +33,10 @@ import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.classic.methods.HttpPut;
import org.apache.hc.client5.http.classic.methods.HttpTrace;
import org.apache.hc.client5.http.config.Configurable;
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager;
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.ClassicHttpRequest;
@@ -123,7 +125,12 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest
* @param connectTimeout the timeout value in milliseconds
* @see RequestConfig#getConnectTimeout()
* @see SocketConfig#getSoTimeout
* @deprecated as of 6.2.13 in favor of setting it on
* {@link BasicHttpClientConnectionManager#setConnectionConfig(ConnectionConfig) the connection configuration}
* for the {@link org.apache.hc.client5.http.impl.classic.HttpClientBuilder#setConnectionManager(HttpClientConnectionManager)
* connection manager}.
*/
@Deprecated(since = "6.2.13", forRemoval = true)
public void setConnectTimeout(int connectTimeout) {
Assert.isTrue(connectTimeout >= 0, "Timeout must be a non-negative value");
this.connectTimeout = connectTimeout;
@@ -142,7 +149,12 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest
* @since 6.1
* @see RequestConfig#getConnectTimeout()
* @see SocketConfig#getSoTimeout
* @deprecated as of 6.2.13 in favor of setting it on
* {@link BasicHttpClientConnectionManager#setConnectionConfig(ConnectionConfig) the connection configuration}
* for the {@link org.apache.hc.client5.http.impl.classic.HttpClientBuilder#setConnectionManager(HttpClientConnectionManager)
* connection manager}.
*/
@Deprecated(since = "6.2.13", forRemoval = true)
public void setConnectTimeout(Duration connectTimeout) {
Assert.notNull(connectTimeout, "ConnectTimeout must not be null");
Assert.isTrue(!connectTimeout.isNegative(), "Timeout must be a non-negative value");
@@ -21,13 +21,13 @@ import java.io.UncheckedIOException;
import java.net.URI;
import java.time.Duration;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import org.reactivestreams.FlowAdapters;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import reactor.netty.NettyOutbound;
import reactor.netty.http.client.HttpClient;
import reactor.netty.http.client.HttpClientRequest;
@@ -43,6 +43,7 @@ import org.springframework.util.StreamUtils;
*
* @author Arjen Poutsma
* @author Juergen Hoeller
* @author Brian Clozel
* @since 6.1
*/
final class ReactorClientHttpRequest extends AbstractStreamingClientHttpRequest {
@@ -53,6 +54,8 @@ final class ReactorClientHttpRequest extends AbstractStreamingClientHttpRequest
private final URI uri;
private final Executor executor;
@Nullable
private final Duration exchangeTimeout;
@@ -65,19 +68,31 @@ final class ReactorClientHttpRequest extends AbstractStreamingClientHttpRequest
* @since 6.2
*/
public ReactorClientHttpRequest(HttpClient httpClient, HttpMethod method, URI uri) {
this.httpClient = httpClient;
this.method = method;
this.uri = uri;
this.exchangeTimeout = null;
this(httpClient, method, uri, null);
}
/**
* Create an instance.
* <p>If no executor is provided, the request will use an {@link Schedulers#boundedElastic() elastic scheduler}
* for performing blocking I/O operations.
* @param httpClient the client to perform the request with
* @param executor the executor to use
* @param method the HTTP method
* @param uri the URI for the request
* @since 6.2.13
*/
public ReactorClientHttpRequest(HttpClient httpClient, HttpMethod method, URI uri, @Nullable Executor executor) {
this(httpClient, method, uri, executor, null);
}
/**
* Package private constructor for use until exchangeTimeout is removed.
*/
ReactorClientHttpRequest(HttpClient httpClient, HttpMethod method, URI uri, @Nullable Duration exchangeTimeout) {
ReactorClientHttpRequest(HttpClient httpClient, HttpMethod method, URI uri, @Nullable Executor executor, @Nullable Duration exchangeTimeout) {
this.httpClient = httpClient;
this.method = method;
this.uri = uri;
this.executor = (executor != null) ? executor : Schedulers.boundedElastic()::schedule;
this.exchangeTimeout = exchangeTimeout;
}
@@ -92,11 +107,7 @@ final class ReactorClientHttpRequest extends AbstractStreamingClientHttpRequest
public ReactorClientHttpRequest(
HttpClient httpClient, URI uri, HttpMethod method,
@Nullable Duration exchangeTimeout, @Nullable Duration readTimeout) {
this.httpClient = httpClient;
this.method = method;
this.uri = uri;
this.exchangeTimeout = exchangeTimeout;
this(httpClient, method, uri, null, exchangeTimeout);
}
@@ -150,13 +161,10 @@ final class ReactorClientHttpRequest extends AbstractStreamingClientHttpRequest
return Mono.empty();
}
AtomicReference<Executor> executorRef = new AtomicReference<>();
return outbound
.withConnection(connection -> executorRef.set(connection.channel().eventLoop()))
.send(FlowAdapters.toPublisher(new OutputStreamPublisher<>(
os -> body.writeTo(StreamUtils.nonClosing(os)), new ByteBufMapper(outbound),
executorRef.getAndSet(null), null)));
this.executor, null)));
}
static IOException convertException(RuntimeException ex) {
@@ -19,11 +19,13 @@ package org.springframework.http.client;
import java.io.IOException;
import java.net.URI;
import java.time.Duration;
import java.util.concurrent.Executor;
import java.util.function.Function;
import io.netty.channel.ChannelOption;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.scheduler.Schedulers;
import reactor.netty.http.client.HttpClient;
import reactor.netty.resources.ConnectionProvider;
import reactor.netty.resources.LoopResources;
@@ -42,6 +44,7 @@ import org.springframework.util.Assert;
* @author Arjen Poutsma
* @author Juergen Hoeller
* @author Sebastien Deleuze
* @author Brian Clozel
* @since 6.2
*/
public class ReactorClientHttpRequestFactory implements ClientHttpRequestFactory, SmartLifecycle {
@@ -58,6 +61,9 @@ public class ReactorClientHttpRequestFactory implements ClientHttpRequestFactory
@Nullable
private final Function<HttpClient, HttpClient> mapper;
@Nullable
private Executor executor;
@Nullable
private Integer connectTimeout;
@@ -129,6 +135,16 @@ public class ReactorClientHttpRequestFactory implements ClientHttpRequestFactory
return client;
}
/**
* Set the {@code Executor} to use for performing blocking I/O operations.
* <p>If no executor is provided, the request will use an {@link Schedulers#boundedElastic() elastic scheduler}.
* @param executor the executor to use.
* @since 6.2.13
*/
public void setExecutor(Executor executor) {
Assert.notNull(executor, "Executor must not be null");
this.executor = executor;
}
/**
* Set the connect timeout value on the underlying client.
@@ -219,7 +235,7 @@ public class ReactorClientHttpRequestFactory implements ClientHttpRequestFactory
"Expected HttpClient or ResourceFactory and mapper");
client = createHttpClient(this.resourceFactory, this.mapper);
}
return new ReactorClientHttpRequest(client, httpMethod, uri, this.exchangeTimeout);
return new ReactorClientHttpRequest(client, httpMethod, uri, this.executor, this.exchangeTimeout);
}
@@ -94,7 +94,7 @@ public class MappingMediaTypeFileExtensionResolver implements MediaTypeFileExten
@Override
public List<String> resolveFileExtensions(MediaType mediaType) {
List<String> fileExtensions = this.fileExtensions.get(mediaType);
List<String> fileExtensions = this.fileExtensions.get(mediaType.removeQualityValue());
return (fileExtensions != null ? fileExtensions : Collections.emptyList());
}
@@ -19,6 +19,7 @@ package org.springframework.web.client;
import java.io.IOException;
import java.lang.reflect.Type;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.client.ClientHttpRequest;
/**
@@ -44,6 +45,12 @@ public interface RequestCallback {
* Gets called by {@link RestTemplate#execute} with an opened {@code ClientHttpRequest}.
* Does not need to care about closing the request or about handling errors:
* this will all be handled by the {@code RestTemplate}.
* <p><strong>Note:</strong> In order to stream request body content directly
* to the underlying HTTP library, implementations must check if the request
* is an implementation of {@link org.springframework.http.StreamingHttpOutputMessage},
* and set the request body through it. Use of the {@link HttpOutputMessage#getBody()}
* is also supported, but results in full content aggregation prior to execution.
* All built-in request implementations support {@code StreamingHttpOutputMessage}.
* @param request the active HTTP request
* @throws IOException in case of I/O errors
*/
@@ -442,6 +442,8 @@ final class HttpServiceMethod {
@Nullable ReactiveAdapter returnTypeAdapter,
boolean blockForOptional, @Nullable Duration blockTimeout) implements ResponseFunction {
private static final String COROUTINES_FLOW_CLASS_NAME = "kotlinx.coroutines.flow.Flow";
@Override
@Nullable
public Object execute(HttpRequestValues requestValues) {
@@ -472,14 +474,16 @@ final class HttpServiceMethod {
MethodParameter returnParam = new MethodParameter(method, -1);
Class<?> returnType = returnParam.getParameterType();
boolean isSuspending = KotlinDetector.isSuspendingFunction(method);
boolean hasFlowReturnType = COROUTINES_FLOW_CLASS_NAME.equals(returnType.getName());
boolean isUnwrapped = isSuspending && !hasFlowReturnType;
if (isSuspending) {
returnType = Mono.class;
returnType = (hasFlowReturnType ? Flux.class : Mono.class);
}
ReactiveAdapter reactiveAdapter = client.getReactiveAdapterRegistry().getAdapter(returnType);
MethodParameter actualParam = (reactiveAdapter != null ? returnParam.nested() : returnParam.nestedIfOptional());
Class<?> actualType = isSuspending ? actualParam.getParameterType() : actualParam.getNestedParameterType();
Class<?> actualType = isUnwrapped ? actualParam.getParameterType() : actualParam.getNestedParameterType();
Function<HttpRequestValues, Publisher<?>> responseFunction;
if (ClassUtils.isVoidType(actualType)) {
@@ -492,18 +496,18 @@ final class HttpServiceMethod {
responseFunction = client::exchangeForHeadersMono;
}
else if (actualType.equals(ResponseEntity.class)) {
MethodParameter bodyParam = isSuspending ? actualParam : actualParam.nested();
MethodParameter bodyParam = isUnwrapped ? actualParam : actualParam.nested();
Class<?> bodyType = bodyParam.getNestedParameterType();
if (bodyType.equals(Void.class)) {
responseFunction = client::exchangeForBodilessEntityMono;
}
else {
ReactiveAdapter bodyAdapter = client.getReactiveAdapterRegistry().getAdapter(bodyType);
responseFunction = initResponseEntityFunction(client, bodyParam, bodyAdapter, isSuspending);
responseFunction = initResponseEntityFunction(client, bodyParam, bodyAdapter, isUnwrapped);
}
}
else {
responseFunction = initBodyFunction(client, actualParam, reactiveAdapter, isSuspending);
responseFunction = initBodyFunction(client, actualParam, reactiveAdapter, isUnwrapped);
}
return new ReactorExchangeResponseFunction(
@@ -513,7 +517,7 @@ final class HttpServiceMethod {
@SuppressWarnings("ConstantConditions")
private static Function<HttpRequestValues, Publisher<?>> initResponseEntityFunction(
ReactorHttpExchangeAdapter client, MethodParameter methodParam,
@Nullable ReactiveAdapter reactiveAdapter, boolean isSuspending) {
@Nullable ReactiveAdapter reactiveAdapter, boolean isUnwrapped) {
if (reactiveAdapter == null) {
return request -> client.exchangeForEntityMono(
@@ -524,7 +528,7 @@ final class HttpServiceMethod {
"ResponseEntity body must be a concrete value or a multi-value Publisher");
ParameterizedTypeReference<?> bodyType =
ParameterizedTypeReference.forType(isSuspending ? methodParam.nested().getGenericParameterType() :
ParameterizedTypeReference.forType(isUnwrapped ? methodParam.nested().getGenericParameterType() :
methodParam.nested().getNestedGenericParameterType());
// Shortcut for Flux
@@ -185,7 +185,6 @@ class InternalPathPatternParser {
if (this.pathElementStart != -1) {
pushPathElement(createPathElement());
}
verifyPatternElements(this.headPE);
return new PathPattern(pathPattern, this.parser, this.headPE);
}
@@ -441,22 +440,4 @@ class InternalPathPatternParser {
this.capturedVariableNames.add(variableName);
}
private void verifyPatternElements(@Nullable PathElement headPE) {
PathElement currentElement = headPE;
while (currentElement != null) {
if (currentElement instanceof CaptureSegmentsPathElement ||
currentElement instanceof WildcardSegmentsPathElement) {
PathElement nextElement = currentElement.next;
while (nextElement instanceof SeparatorPathElement) {
nextElement = nextElement.next;
}
if (nextElement != null && !(nextElement instanceof LiteralPathElement)) {
throw new PatternParseException(nextElement.pos, this.pathPatternData,
PatternMessage.MULTISEGMENT_PATHELEMENT_NOT_FOLLOWED_BY_LITERAL);
}
}
currentElement = currentElement.next;
}
}
}
@@ -101,7 +101,6 @@ public class PatternParseException extends IllegalArgumentException {
ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR("Char ''{0}'' is not allowed in a captured variable name"),
CANNOT_HAVE_MANY_MULTISEGMENT_PATHELEMENTS("Multiple '{*...}' or '**' pattern elements are not allowed"),
INVALID_LOCATION_FOR_MULTISEGMENT_PATHELEMENT("'{*...}' or '**' pattern elements should be placed at the start or end of the pattern"),
MULTISEGMENT_PATHELEMENT_NOT_FOLLOWED_BY_LITERAL("'{*...}' or '**' pattern elements should be followed by a literal path element"),
BADLY_FORMED_CAPTURE_THE_REST("Expected form when capturing the rest of the path is simply '{*...}'"),
MISSING_REGEX_CONSTRAINT("Missing regex constraint on capture"),
ILLEGAL_DOUBLE_CAPTURE("Not allowed to capture ''{0}'' twice in the same pattern"),
@@ -61,7 +61,7 @@ class HttpComponentsClientHttpRequestFactoryTests extends AbstractHttpRequestFac
}
@Test
@SuppressWarnings("deprecation")
@SuppressWarnings({ "removal", "deprecation" })
void assertCustomConfig() throws Exception {
HttpClient httpClient = HttpClientBuilder.create().build();
HttpComponentsClientHttpRequestFactory hrf = new HttpComponentsClientHttpRequestFactory(httpClient);
@@ -103,7 +103,7 @@ class HttpComponentsClientHttpRequestFactoryTests extends AbstractHttpRequestFac
}
@Test
@SuppressWarnings("deprecation")
@SuppressWarnings({ "removal", "deprecation" })
void localSettingsOverrideClientDefaultSettings() throws Exception {
RequestConfig defaultConfig = RequestConfig.custom()
.setConnectTimeout(1234, MILLISECONDS)
@@ -53,6 +53,14 @@ class MappingMediaTypeFileExtensionResolverTests {
.resolveFileExtensions(MediaType.TEXT_HTML)).isEmpty();
}
@Test
void resolveExtensionsWithQualityParameter() {
List<String> extensions = new MappingMediaTypeFileExtensionResolver(DEFAULT_MAPPINGS)
.resolveFileExtensions(MediaType.parseMediaType("application/json;q=0.9"));
assertThat(extensions).containsExactly("json");
}
@Test // SPR-13747
public void lookupMediaTypeCaseInsensitive() {
assertThat(new MappingMediaTypeFileExtensionResolver(DEFAULT_MAPPINGS).lookupMediaType("JSON"))
@@ -232,7 +232,6 @@ class PathPatternParserTests {
checkError("/{abc}{*foobar}", 1, PatternMessage.CAPTURE_ALL_IS_STANDALONE_CONSTRUCT);
checkError("/{abc}{*foobar}{foo}", 1, PatternMessage.CAPTURE_ALL_IS_STANDALONE_CONSTRUCT);
checkError("/{*foo}/foo/{*bar}", 18, PatternMessage.CANNOT_HAVE_MANY_MULTISEGMENT_PATHELEMENTS);
checkError("/{*foo}/{bar}", 8, PatternMessage.MULTISEGMENT_PATHELEMENT_NOT_FOLLOWED_BY_LITERAL);
checkError("{foo:}", 5, PatternMessage.MISSING_REGEX_CONSTRAINT);
checkError("{foo}_{foo}", 0, PatternMessage.ILLEGAL_DOUBLE_CAPTURE, "foo");
checkError("/{bar}/{bar}", 7, PatternMessage.ILLEGAL_DOUBLE_CAPTURE, "bar");
@@ -149,14 +149,14 @@ class PathPatternTests {
checkMatches("/*/bar", "/foo/bar");
checkNoMatch("/*/bar", "/foo/baz");
checkNoMatch("/*/bar", "//bar");
checkNoMatch("/*/bar", "/bar");
checkMatches("/f*/bar", "/foo/bar");
checkMatches("/*/bar", "/foo/bar");
checkMatches("a/*","a/");
checkMatches("/*","/");
checkMatches("/*/bar", "/foo/bar");
checkNoMatch("/*/bar", "/foo/baz");
checkNoMatch("/*/bar", "/bar");
checkMatches("/f*/bar", "/foo/bar");
checkMatches("/*/bar", "/foo/bar");
checkMatches("/a*b*c*d/bar", "/abcd/bar");
checkMatches("*a*", "testa");
checkMatches("a/*", "a/");
@@ -185,6 +185,16 @@ class PathPatternTests {
checkMatches("/resource/**", "/resource/foobar");
}
@Test
void wildcardSegmentsThenNonLiteral() {
checkMatches("/**/*.js", "/script.js");
checkMatches("/**/*.js", "/js/script.js");
checkMatches("/**/*.js", "/files/js/script.js");
checkMatches("/**/{type}/*.js", "/files/js/script.js");
checkNoMatch("/**/*.js", "/files/style.css");
}
@Test
void antPathMatcherTests() {
// test exact matching
@@ -56,6 +56,10 @@ class KotlinHttpServiceMethodTests {
assertThat(flowBody.toList()).containsExactly("exchange", "For", "Body", "Flux")
verifyClientInvocation("exchangeForBodyFlux", object : ParameterizedTypeReference<String>() {})
val suspendingFlowBody = service.suspendingFlowBody()
assertThat(suspendingFlowBody.toList()).containsExactly("exchange", "For", "Body", "Flux")
verifyClientInvocation("exchangeForBodyFlux", object : ParameterizedTypeReference<String>() {})
val stringEntity = service.stringEntity()
assertThat(stringEntity).isEqualTo(ResponseEntity.ok<String>("exchangeForEntityMono"))
verifyClientInvocation("exchangeForEntityMono", object : ParameterizedTypeReference<String>() {})
@@ -127,6 +131,9 @@ class KotlinHttpServiceMethodTests {
@GetExchange
suspend fun listBody(): MutableList<String>
@GetExchange
suspend fun suspendingFlowBody(): Flow<String>
@GetExchange
suspend fun stringEntity(): ResponseEntity<String>
@@ -43,6 +43,9 @@ public interface RouterFunction<T extends ServerResponse> {
*/
Mono<HandlerFunction<T>> route(ServerRequest request);
// Default methods for composition and filtering
/**
* Return a composed routing function that first invokes this function,
* and then invokes the {@code other} function (of the same response type {@code T})
@@ -99,28 +102,6 @@ public interface RouterFunction<T extends ServerResponse> {
return and(RouterFunctions.nest(predicate, routerFunction));
}
/**
* Filter all {@linkplain HandlerFunction handler functions} routed by this function with the given
* {@linkplain HandlerFilterFunction filter function}.
* @param <S> the filter return type
* @param filterFunction the filter to apply
* @return the filtered routing function
*/
default <S extends ServerResponse> RouterFunction<S> filter(HandlerFilterFunction<T, S> filterFunction) {
return new RouterFunctions.FilteredRouterFunction<>(this, filterFunction);
}
/**
* Accept the given visitor. Default implementation calls
* {@link RouterFunctions.Visitor#unknown(RouterFunction)}; composed {@code RouterFunction}
* implementations are expected to call {@code accept} for all components that make up this
* router function.
* @param visitor the visitor to accept
*/
default void accept(RouterFunctions.Visitor visitor) {
visitor.unknown(this);
}
/**
* Return a new routing function with the given attribute.
* @param name the attribute name
@@ -155,5 +136,27 @@ public interface RouterFunction<T extends ServerResponse> {
return new RouterFunctions.AttributesRouterFunction<>(this, attributes);
}
/**
* Filter all {@linkplain HandlerFunction handler functions} routed by this function
* with the given {@linkplain HandlerFilterFunction filter function}.
* @param <S> the filter return type
* @param filterFunction the filter to apply
* @return the filtered routing function
*/
default <S extends ServerResponse> RouterFunction<S> filter(HandlerFilterFunction<T, S> filterFunction) {
return new RouterFunctions.FilteredRouterFunction<>(this, filterFunction);
}
/**
* Accept the given visitor.
* <p>The default implementation calls
* {@link RouterFunctions.Visitor#unknown(RouterFunction)}; composed {@code RouterFunction}
* implementations are expected to call {@code accept} for all components that make up this
* router function.
* @param visitor the visitor to accept
*/
default void accept(RouterFunctions.Visitor visitor) {
visitor.unknown(this);
}
}
@@ -42,6 +42,9 @@ public interface RouterFunction<T extends ServerResponse> {
*/
Optional<HandlerFunction<T>> route(ServerRequest request);
// Default methods for composition and filtering
/**
* Return a composed routing function that first invokes this function,
* and then invokes the {@code other} function (of the same response type {@code T})
@@ -98,28 +101,6 @@ public interface RouterFunction<T extends ServerResponse> {
return and(RouterFunctions.nest(predicate, routerFunction));
}
/**
* Filter all {@linkplain HandlerFunction handler functions} routed by this function with the given
* {@linkplain HandlerFilterFunction filter function}.
* @param <S> the filter return type
* @param filterFunction the filter to apply
* @return the filtered routing function
*/
default <S extends ServerResponse> RouterFunction<S> filter(HandlerFilterFunction<T, S> filterFunction) {
return new RouterFunctions.FilteredRouterFunction<>(this, filterFunction);
}
/**
* Accept the given visitor. Default implementation calls
* {@link RouterFunctions.Visitor#unknown(RouterFunction)}; composed {@code RouterFunction}
* implementations are expected to call {@code accept} for all components that make up this
* router function.
* @param visitor the visitor to accept
*/
default void accept(RouterFunctions.Visitor visitor) {
visitor.unknown(this);
}
/**
* Return a new routing function with the given attribute.
* @param name the attribute name
@@ -154,5 +135,27 @@ public interface RouterFunction<T extends ServerResponse> {
return new RouterFunctions.AttributesRouterFunction<>(this, attributes);
}
/**
* Filter all {@linkplain HandlerFunction handler functions} routed by this function
* with the given {@linkplain HandlerFilterFunction filter function}.
* @param <S> the filter return type
* @param filterFunction the filter to apply
* @return the filtered routing function
*/
default <S extends ServerResponse> RouterFunction<S> filter(HandlerFilterFunction<T, S> filterFunction) {
return new RouterFunctions.FilteredRouterFunction<>(this, filterFunction);
}
/**
* Accept the given visitor.
* <p>The default implementation calls
* {@link RouterFunctions.Visitor#unknown(RouterFunction)}; composed {@code RouterFunction}
* implementations are expected to call {@code accept} for all components that make up this
* router function.
* @param visitor the visitor to accept
*/
default void accept(RouterFunctions.Visitor visitor) {
visitor.unknown(this);
}
}
@@ -28,6 +28,7 @@ import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.web.servlet.support.BindStatus;
import org.springframework.web.util.HtmlUtils;
/**
* Provides supporting functionality to render a list of '{@code option}'
@@ -102,18 +103,26 @@ class OptionWriter {
private final boolean htmlEscape;
@Nullable
private final String encoding;
/**
* Create a new {@code OptionWriter} for the supplied {@code objectSource}.
* Create a new {@code OptionWriter} for the supplied {@code optionSource}.
* @param optionSource the source of the {@code options} (never {@code null})
* @param bindStatus the {@link BindStatus} for the bound value (never {@code null})
* @param valueProperty the name of the property used to render {@code option} values
* (optional)
* @param labelProperty the name of the property used to render {@code option} labels
* (optional)
* @param htmlEscape whether special characters should be converted into HTML
* character references
* @param encoding the character encoding to use, or {@code null} if response
* encoding should not be used with HTML escaping
*/
public OptionWriter(Object optionSource, BindStatus bindStatus,
@Nullable String valueProperty, @Nullable String labelProperty, boolean htmlEscape) {
@Nullable String valueProperty, @Nullable String labelProperty,
boolean htmlEscape, @Nullable String encoding) {
Assert.notNull(optionSource, "'optionSource' must not be null");
Assert.notNull(bindStatus, "'bindStatus' must not be null");
@@ -122,6 +131,7 @@ class OptionWriter {
this.valueProperty = valueProperty;
this.labelProperty = labelProperty;
this.htmlEscape = htmlEscape;
this.encoding = encoding;
}
@@ -250,7 +260,14 @@ class OptionWriter {
*/
private String getDisplayString(@Nullable Object value) {
PropertyEditor editor = (value != null ? this.bindStatus.findEditor(value.getClass()) : null);
return ValueFormatter.getDisplayString(value, editor, this.htmlEscape);
String displayString = ValueFormatter.getDisplayString(value, editor, false);
return (this.htmlEscape ? htmlEscape(displayString) : displayString);
}
private String htmlEscape(String content) {
return (this.encoding != null ?
HtmlUtils.htmlEscape(content, this.encoding) :
HtmlUtils.htmlEscape(content));
}
/**
@@ -187,6 +187,7 @@ import org.springframework.web.util.TagUtils;
* @author Rob Harrop
* @author Juergen Hoeller
* @author Scott Andrews
* @author Sam Brannen
* @since 2.0
*/
@SuppressWarnings("serial")
@@ -312,7 +313,10 @@ public class OptionsTag extends AbstractHtmlElementTag {
(itemValue != null ? ObjectUtils.getDisplayString(evaluate("itemValue", itemValue)) : null);
String labelProperty =
(itemLabel != null ? ObjectUtils.getDisplayString(evaluate("itemLabel", itemLabel)) : null);
OptionsWriter optionWriter = new OptionsWriter(selectName, itemsObject, valueProperty, labelProperty);
String encodingToUse =
(isResponseEncodedHtmlEscape() ? this.pageContext.getResponse().getCharacterEncoding() : null);
OptionsWriter optionWriter =
new OptionsWriter(selectName, itemsObject, valueProperty, labelProperty, encodingToUse);
optionWriter.writeOptions(tagWriter);
}
return SKIP_BODY;
@@ -353,9 +357,9 @@ public class OptionsTag extends AbstractHtmlElementTag {
private final String selectName;
public OptionsWriter(@Nullable String selectName, Object optionSource,
@Nullable String valueProperty, @Nullable String labelProperty) {
@Nullable String valueProperty, @Nullable String labelProperty, @Nullable String encoding) {
super(optionSource, getBindStatus(), valueProperty, labelProperty, isHtmlEscape());
super(optionSource, getBindStatus(), valueProperty, labelProperty, isHtmlEscape(), encoding);
this.selectName = selectName;
}
@@ -235,6 +235,7 @@ import org.springframework.web.servlet.support.BindStatus;
*
* @author Rob Harrop
* @author Juergen Hoeller
* @author Sam Brannen
* @since 2.0
* @see OptionTag
*/
@@ -418,8 +419,12 @@ public class SelectTag extends AbstractHtmlInputElementTag {
ObjectUtils.getDisplayString(evaluate("itemValue", getItemValue())) : null);
String labelProperty = (getItemLabel() != null ?
ObjectUtils.getDisplayString(evaluate("itemLabel", getItemLabel())) : null);
String encodingToUse = (isResponseEncodedHtmlEscape() ?
this.pageContext.getResponse().getCharacterEncoding() : null);
OptionWriter optionWriter =
new OptionWriter(itemsObject, getBindStatus(), valueProperty, labelProperty, isHtmlEscape()) {
new OptionWriter(itemsObject, getBindStatus(), valueProperty, labelProperty,
isHtmlEscape(), encodingToUse) {
@Override
protected String processOptionValue(String resolvedValue) {
return processFieldValue(selectName, resolvedValue, "option");
@@ -21,6 +21,7 @@ import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -50,6 +51,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Juergen Hoeller
* @author Scott Andrews
* @author Jeremy Grelle
* @author Sam Brannen
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
class OptionsTagTests extends AbstractHtmlElementTagTests {
@@ -114,6 +116,86 @@ class OptionsTagTests extends AbstractHtmlElementTagTests {
assertThat(element.attribute("onclick").getValue()).isEqualTo("CLICK");
}
@Test // gh-35783
void withListWithHtmlEscaping() throws Exception {
getPageContext().setAttribute(
SelectTag.LIST_VALUE_PAGE_ATTRIBUTE, new BindStatus(getRequestContext(), "testBean.country", false));
this.tag.setItems(List.of("café", "Jane \"I Love Cafés\" Smith"));
this.tag.setId("myOption");
var expectedOutput = """
<option id="myOption1" value="caf&eacute;">caf&eacute;</option>
<option id="myOption2" value="Jane &quot;I Love Caf&eacute;s&quot; Smith">Jane &quot;I Love Caf&eacute;s&quot; Smith</option>
""".replace("\n", "");
assertThat(this.tag.doStartTag()).isEqualTo(Tag.SKIP_BODY);
assertThat(getOutput()).isEqualTo(expectedOutput);
}
@Test // gh-35783
void withListWithHtmlEscapingAndCharacterEncoding() throws Exception {
this.getPageContext().getResponse().setCharacterEncoding("UTF-8");
getPageContext().setAttribute(
SelectTag.LIST_VALUE_PAGE_ATTRIBUTE, new BindStatus(getRequestContext(), "testBean.country", false));
this.tag.setItems(List.of("café", "Jane \"I Love Cafés\" Smith"));
this.tag.setId("myOption");
var expectedOutput = """
<option id="myOption1" value="café">café</option>
<option id="myOption2" value="Jane &quot;I Love Cafés&quot; Smith">Jane &quot;I Love Cafés&quot; Smith</option>
""".replace("\n", "");
assertThat(this.tag.doStartTag()).isEqualTo(Tag.SKIP_BODY);
assertThat(getOutput()).isEqualTo(expectedOutput);
}
@Test // gh-35783
void withMapWithHtmlEscaping() throws Exception {
getPageContext().setAttribute(
SelectTag.LIST_VALUE_PAGE_ATTRIBUTE, new BindStatus(getRequestContext(), "testBean.country", false));
var map = new LinkedHashMap<String, String>();
map.put("one", "Jane \"I Love Cafés\" Smith");
map.put("two", "Joe Café");
this.tag.setItems(map);
this.tag.setId("myOption");
var expectedOutput = """
<option id="myOption1" value="one">Jane &quot;I Love Caf&eacute;s&quot; Smith</option>
<option id="myOption2" value="two">Joe Caf&eacute;</option>
""".replace("\n", "");
assertThat(this.tag.doStartTag()).isEqualTo(Tag.SKIP_BODY);
assertThat(getOutput()).isEqualTo(expectedOutput);
}
@Test // gh-35783
void withMapWithHtmlEscapingAndCharacterEncoding() throws Exception {
this.getPageContext().getResponse().setCharacterEncoding("UTF-8");
getPageContext().setAttribute(
SelectTag.LIST_VALUE_PAGE_ATTRIBUTE, new BindStatus(getRequestContext(), "testBean.country", false));
var map = new LinkedHashMap<String, String>();
map.put("one", "Jane \"I Love Cafés\" Smith");
map.put("two", "Joe Café");
this.tag.setItems(map);
this.tag.setId("myOption");
var expectedOutput = """
<option id="myOption1" value="one">Jane &quot;I Love Cafés&quot; Smith</option>
<option id="myOption2" value="two">Joe Café</option>
""".replace("\n", "");
assertThat(this.tag.doStartTag()).isEqualTo(Tag.SKIP_BODY);
assertThat(getOutput()).isEqualTo(expectedOutput);
}
@Test
void withCollectionAndDynamicAttributes() throws Exception {
String dynamicAttribute1 = "attr1";
@@ -23,6 +23,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -54,6 +55,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
* @author Juergen Hoeller
* @author Jeremy Grelle
* @author Dave Syer
* @author Sam Brannen
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public class SelectTagTests extends AbstractFormTagTests {
@@ -77,6 +79,18 @@ public class SelectTagTests extends AbstractFormTagTests {
this.tag.setPageContext(getPageContext());
}
@Override
protected TestBean createTestBean() {
this.bean = new TestBeanWithRealCountry();
this.bean.setName("Rob");
this.bean.setCountry("UK");
this.bean.setSex("M");
this.bean.setMyFloat(Float.valueOf("12.34"));
this.bean.setSomeIntegerArray(new Integer[]{12, 34});
return this.bean;
}
@Test
void dynamicAttributes() throws JspException {
String dynamicAttribute1 = "attr1";
@@ -132,6 +146,78 @@ public class SelectTagTests extends AbstractFormTagTests {
assertList(true);
}
@Test // gh-33023
void withListWithHtmlEscapingInPath() throws Exception {
this.tag.setPath("favoriteCafé");
this.tag.setItems(List.of("Cup of Joe", "Jane's Coffee Shop"));
this.tag.setSize("2");
var expectedOutput = """
<select id="favoriteCaf&eacute;" name="favoriteCaf&eacute;" size="2">
<option value="Cup of Joe">Cup of Joe</option>
<option value="Jane&#39;s Coffee Shop">Jane&#39;s Coffee Shop</option>
</select>
""".replace("\n", "");
assertThat(this.tag.doStartTag()).isEqualTo(Tag.SKIP_BODY);
assertThat(getOutput()).isEqualTo(expectedOutput);
}
@Test // gh-33023
void withListWithHtmlEscapingAndCharacterEncodingInPath() throws Exception {
this.getPageContext().getResponse().setCharacterEncoding("UTF-8");
this.tag.setPath("favoriteCafé");
this.tag.setItems(List.of("Cup of Joe", "Jane's Coffee Shop"));
this.tag.setSize("2");
var expectedOutput = """
<select id="favoriteCafé" name="favoriteCafé" size="2">
<option value="Cup of Joe">Cup of Joe</option>
<option value="Jane&#39;s Coffee Shop">Jane&#39;s Coffee Shop</option>
</select>
""".replace("\n", "");
assertThat(this.tag.doStartTag()).isEqualTo(Tag.SKIP_BODY);
assertThat(getOutput()).isEqualTo(expectedOutput);
}
@Test // gh-35783
void withListWithHtmlEscapingInOptions() throws Exception {
this.tag.setPath("name");
this.tag.setItems(List.of("café", "Jane \"I Love Cafés\" Smith"));
this.tag.setSize("2");
var expectedOutput = """
<select id="name" name="name" size="2">
<option value="caf&eacute;">caf&eacute;</option>
<option value="Jane &quot;I Love Caf&eacute;s&quot; Smith">Jane &quot;I Love Caf&eacute;s&quot; Smith</option>
</select>
""".replace("\n", "");
assertThat(this.tag.doStartTag()).isEqualTo(Tag.SKIP_BODY);
assertThat(getOutput()).isEqualTo(expectedOutput);
}
@Test // gh-35783
void withListWithHtmlEscapingAndCharacterEncodingInOptions() throws Exception {
this.getPageContext().getResponse().setCharacterEncoding("UTF-8");
this.tag.setPath("name");
this.tag.setItems(List.of("café", "Jane \"I Love Cafés\" Smith"));
this.tag.setSize("2");
var expectedOutput = """
<select id="name" name="name" size="2">
<option value="café">café</option>
<option value="Jane &quot;I Love Cafés&quot; Smith">Jane &quot;I Love Cafés&quot; Smith</option>
</select>
""".replace("\n", "");
assertThat(this.tag.doStartTag()).isEqualTo(Tag.SKIP_BODY);
assertThat(getOutput()).isEqualTo(expectedOutput);
}
@Test
void withResolvedList() throws Exception {
this.tag.setPath("country");
@@ -335,8 +421,58 @@ public class SelectTagTests extends AbstractFormTagTests {
void withMap() throws Exception {
this.tag.setPath("sex");
this.tag.setItems(getSexes());
int result = this.tag.doStartTag();
assertThat(result).isEqualTo(Tag.SKIP_BODY);
var expectedOutput = """
<select id="sex" name="sex">
<option value="F">Female</option>
<option value="M" selected="selected">Male</option>
</select>
""".replace("\n", "");
assertThat(this.tag.doStartTag()).isEqualTo(Tag.SKIP_BODY);
assertThat(getOutput()).isEqualTo(expectedOutput);
}
@Test // gh-35783
void withMapWithHtmlEscapingInOptions() throws Exception {
var map = new LinkedHashMap<String, String>();
map.put("F", "Jane \"I Love Cafés\" Smith");
map.put("M", "Joe Café");
this.tag.setPath("sex");
this.tag.setItems(map);
var expectedOutput = """
<select id="sex" name="sex">
<option value="F">Jane &quot;I Love Caf&eacute;s&quot; Smith</option>
<option value="M" selected="selected">Joe Caf&eacute;</option>
</select>
""".replace("\n", "");
assertThat(this.tag.doStartTag()).isEqualTo(Tag.SKIP_BODY);
assertThat(getOutput()).isEqualTo(expectedOutput);
}
@Test // gh-35783
void withMapWithHtmlEscapingAndCharacterEncodingInOptions() throws Exception {
this.getPageContext().getResponse().setCharacterEncoding("UTF-8");
var map = new LinkedHashMap<String, String>();
map.put("F", "Jane \"I Love Cafés\" Smith");
map.put("M", "Joe Café");
this.tag.setPath("sex");
this.tag.setItems(map);
var expectedOutput = """
<select id="sex" name="sex">
<option value="F">Jane &quot;I Love Cafés&quot; Smith</option>
<option value="M" selected="selected">Joe Café</option>
</select>
""".replace("\n", "");
assertThat(this.tag.doStartTag()).isEqualTo(Tag.SKIP_BODY);
assertThat(getOutput()).isEqualTo(expectedOutput);
}
@Test
@@ -958,7 +1094,7 @@ public class SelectTagTests extends AbstractFormTagTests {
}
private Map getSexes() {
Map<String, String> sexes = new HashMap<>();
Map<String, String> sexes = new LinkedHashMap<>();
sexes.put("F", "Female");
sexes.put("M", "Male");
return sexes;
@@ -996,17 +1132,6 @@ public class SelectTagTests extends AbstractFormTagTests {
}
}
@Override
protected TestBean createTestBean() {
this.bean = new TestBeanWithRealCountry();
this.bean.setName("Rob");
this.bean.setCountry("UK");
this.bean.setSex("M");
this.bean.setMyFloat(Float.valueOf("12.34"));
this.bean.setSomeIntegerArray(new Integer[]{12, 34});
return this.bean;
}
private TestBean getTestBean() {
return (TestBean) getPageContext().getRequest().getAttribute(COMMAND_NAME);
}