Compare commits

...

64 Commits

Author SHA1 Message Date
Brian Clozel 4c13425464 Release v6.2.11 2025-09-11 08:58:32 +02:00
Juergen Hoeller d17601e01c Upgrade to Undertow 2.3.19, RxJava 3.1.11, Aalto 1.3.3 2025-09-10 18:38:20 +02:00
Juergen Hoeller 5b387615c6 Clarify intended nestedTransactionAllowed default in JpaTransactionManager
Closes gh-35212
2025-09-10 18:06:44 +02:00
Sam Brannen 0e3e34bee0 Find annotations on parameters in overridden non-public methods
Prior to this commit, annotations were not found on parameters in an
overridden method unless the method was public. Specifically, the
search algorithm in AnnotatedMethod did not consider a protected or
package-private method in a superclass to be a potential override
candidate. This affects parameter annotation searches in
spring-messaging, spring-webmvc, spring-webflux, and any other
components that use or extend AnnotatedMethod.

To address that, this commit revises the search algorithm in
AnnotatedMethod to consider all non-final declared methods as potential
override candidates, thereby aligning with the search logic in
AnnotationsScanner for the MergedAnnotations API.

Closes gh-35349
2025-09-10 14:24:24 +02:00
Sam Brannen 4745c7cf3c Name local variables consistently 2025-09-10 14:08:20 +02:00
Juergen Hoeller 275fb52ad6 Upgrade to Reactor 2024.0.10 and Micrometer 1.14.11
Closes gh-35454
Closes gh-35455
2025-09-09 18:45:38 +02:00
Juergen Hoeller 7f9aa39748 Polishing 2025-09-09 18:45:04 +02:00
Taeik Lim c788554b1d Avoid thread pinning in SseEmitter, ResponseBodyEmitter
Closes gh-35423

Signed-off-by: Taeik Lim <sibera21@gmail.com>
2025-09-09 16:38:10 +02:00
Sébastien Deleuze 9e8c64011d Make JsonPathAssertions#isEqualTo parameter nullable
Closes gh-35445
2025-09-09 11:25:58 +02:00
Juergen Hoeller ebb8e34570 Upgrade to Jetty 12.0.26, Jetty Reactive HttpClient 4.0.11, Netty 4.1.127, HtmlUnit 4.16 2025-09-08 18:11:27 +02:00
Juergen Hoeller 9ba954c330 Fix FileSystemUtils for Windows/Linux path difference
See gh-35443
2025-09-08 17:51:58 +02:00
Juergen Hoeller ba52164373 Provide graceful fallback for non-default NIO file systems
Closes gh-35443
2025-09-08 17:28:45 +02:00
rstoyanchev 977582fced Document data binding for functional endpoints
Closes gh-35367
2025-09-08 10:12:20 +01:00
Sam Brannen 64721b3bc0 Polish formatting 2025-09-03 16:24:56 +02:00
Sam Brannen 02f0f92a72 Polish contribution
See gh-35400
2025-09-03 16:22:55 +02:00
Lukáš Kvídera db9e938ec4 Detect Informix error codes as DuplicateKeyException
Closes gh-35400

Signed-off-by: Lukáš Kvídera <kvideral@qwsome.eu>
2025-09-03 16:20:19 +02:00
Juergen Hoeller 3a4315bf16 Keep mainThreadPrefix exposed until background init threads finished
Closes gh-35409
2025-09-03 15:45:12 +02:00
Sam Brannen 33fe8d29c1 Document potential need to use Mockito.doXxx() to stub a @⁠MockitoSpyBean
Closes gh-35410
2025-09-03 14:52:58 +02:00
Sam Brannen b741632e99 Polish wording in web sections 2025-08-29 17:25:15 +02:00
Tran Ngoc Nhan cd208797e2 Fix links to Reactive Libraries and RestTemplate
Closes gh-35392

Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2025-08-29 17:23:13 +02:00
Brian Clozel 764336f0f2 Fix Jetty DataBufferFactory memory leak
Prior to this commit, gh-32097 added native support for Jetty for both
client and server integrations. The `JettyDataBufferFactory` was
promoted as a first class citizen, extracted from a private class in the
client support. To accomodate with server-side requirements, an extra
`buffer.retain()` call was performed.
While this is useful for server-side support, this introduced a bug in
the data buffer factory, as wrapping an existing chunk means that this
chunk is already retained.

This commit fixes the buffer factory implementation and moved existing
tests from mocks to actual pooled buffer implementations from Jetty.
The extra `buffer.retain()` is now done from the server support, right
before wrapping the buffer.

Fixes gh-35319
2025-08-27 13:36:34 +02:00
Sam Brannen 4903fee939 Permit @⁠Nullable value in ResponseCookie from*() factory methods
Closes gh-35377
2025-08-26 11:01:29 +02:00
Sam Brannen 6978f0a398 Document terms and units in DataSize.parse(...) methods
Closes gh-35298
2025-08-24 14:02:04 +02:00
Juergen Hoeller f62519bb55 Add cancelRemainingTasksOnClose flag for enforcing early interruption
Closes gh-35372
2025-08-24 10:31:01 +02:00
Juergen Hoeller 0e2af5d113 Avoid default AutoCloseable implementation in ExecutorService on JDK 19+
Consistently calls shutdown() unless a specific close() method has been provided in a subclass.

Closes gh-35316
2025-08-24 10:30:48 +02:00
Daniel Garnier-Moiroux ed7c3d737c Fix broken link in WebDriver docs
Closes gh-35374

Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
2025-08-23 13:41:40 +02:00
Juergen Hoeller 01b24f2644 Upgrade to Protobuf 4.32, HtmlUnit 4.15, Mockito 5.19 2025-08-22 23:05:13 +02:00
Juergen Hoeller 4a4cf8a787 Remove erroneous javadoc symbol 2025-08-22 22:38:34 +02:00
Juergen Hoeller 55181fa1c9 Polishing 2025-08-22 22:00:52 +02:00
Juergen Hoeller 300ae841ce Align setBeanResolver nullability with getBeanResolver
Includes consistent javadoc for all applicable methods.

Closes gh-35371
2025-08-22 22:00:22 +02:00
Juergen Hoeller c248f94e5a Cache bean type next to primary bean names (on singleton creation)
This avoids singleton access for type checks in hasPrimaryConflict.

Closes gh-35330
2025-08-22 21:59:38 +02:00
Sam Brannen 19d5ec6781 Improve documentation for ApplicationEvents to clarify recommended usage
See gh-35335
2025-08-20 16:42:22 +02:00
khj68 c0b71f8999 Improve Javadoc of ApplicationEvents to clarify preferred usage
This commit reorders and clarifies the usage instructions for
ApplicationEvents to:

1. Recommend method parameter injection as the primary approach, since
   ApplicationEvents has a per-method lifecycle
2. Clarify that ApplicationEvents is not a general Spring bean and
   cannot be constructor-injected
3. Explicitly state that field injection is an alternative approach

This addresses confusion where developers expect ApplicationEvents to
behave like a regular Spring bean eligible for constructor injection.

See gh-35297
Closes gh-35335

Signed-off-by: khj68 <junthewise@gmail.com>
2025-08-20 16:31:41 +02:00
Sam Brannen 5d214c2624 Polishing 2025-08-20 16:31:41 +02:00
Sam Brannen 6d710d482a Find annotation on overridden method in type hierarchy with unresolved generics
Prior to this commit, the MergedAnnotations support (specifically
AnnotationsScanner) and AnnotatedMethod did not find annotations on
overridden methods in type hierarchies with unresolved generics.

The reason for this is that ResolvableType.resolve() returns null for
such an unresolved type, which prevents the search algorithms from
considering such methods as override candidates.

For example, given the following type hierarchy, the compiler does not
generate a method corresponding to processOneAndTwo(Long, String) for
GenericInterfaceImpl. Nonetheless, one would expect an invocation of
processOneAndTwo(Long, String) to be @⁠Transactional since it is
effectively an invocation of processOneAndTwo(Long, C) in
GenericAbstractSuperclass, which overrides/implements
processOneAndTwo(A, B) in GenericInterface, which is annotated with
@⁠Transactional.

However, the MergedAnnotations infrastructure currently does not
determine that processOneAndTwo(Long, C) is @⁠Transactional since it is
not able to determine that processOneAndTwo(Long, C) overrides
processOneAndTwo(A, B) because of the unresolved generic C.

interface GenericInterface<A, B> {

    @⁠Transactional
    void processOneAndTwo(A value1, B value2);
}

abstract class GenericAbstractSuperclass<C> implements GenericInterface<Long, C> {

    @⁠Override
    public void processOneAndTwo(Long value1, C value2) {
    }
}

static GenericInterfaceImpl extends GenericAbstractSuperclass<String> {
}

To address such issues, this commit changes the logic in
AnnotationsScanner.hasSameGenericTypeParameters() and
AnnotatedMethod.isOverrideFor() so that they use
ResolvableType.toClass() instead of ResolvableType.resolve(). The
former returns Object.class for an unresolved generic which in turn
allows the search algorithms to properly detect method overrides in
such type hierarchies.

Closes gh-35342
2025-08-19 12:28:12 +02:00
Stefano Cordio ed28390d24 Refine @Contract Javadoc
This commit removes references to the `pure` attribute.
Relates to gh-33820.

Closes gh-35285
Signed-off-by: Stefano Cordio <stefano.cordio@gmail.com>
2025-08-14 16:12:46 +02:00
Stéphane Nicoll e2085063f6 Next development version (v6.2.11-SNAPSHOT) 2025-08-14 09:55:02 +02:00
Stéphane Nicoll edda4731e1 Build against Java 24
Closes gh-35326
2025-08-14 09:06:18 +02:00
Stéphane Nicoll 9fa2d7d190 Upgrade to Jackson 2.18.4.1
Closes gh-35322
2025-08-14 08:38:58 +02:00
Stéphane Nicoll c30427fd4e Upgrade to Netty 4.1.124.Final
Closes gh-35321
2025-08-14 08:38:34 +02:00
Juergen Hoeller 1d908f1847 Upgrade to Reactor 2024.0.9 and Micrometer 1.14.10
Includes Groovy 4.0.28, JRuby 9.4.13, Jetty 12.0.25, Caffeine 3.2.2, Protobuf 4.31.1, Selenium 4.35, HtmlUnit 4.14

Closes gh-35312
Closes gh-35313
2025-08-13 00:04:31 +02:00
Sam Brannen 37b076be51 Support multiple result sets in ScriptUtils.executeSqlScript()
Prior to this commit, ScriptUtils.executeSqlScript() treated every
statement within the script as if it were a single insert/update/delete
statement. This disregarded the fact that the execution of a JDBC
Statement can result in multiple individual statements, some of which
result in a ResultSet and others that result in an update count.

For example, when executing a stored procedure on Sybase, ScriptUtils
did not execute all statements within the stored procedure.

To address that, this commit revises the implementation of
ScriptUtils.executeSqlScript() so that it handles multiple results and
differentiates between result sets and update counts.

Closes gh-35248
2025-08-11 17:44:39 +03:00
Juergen Hoeller a9453a5959 Polishing 2025-08-11 14:32:45 +02:00
Juergen Hoeller 3781ba223e Optimize NIO path resolution in PathEditor
Closes gh-35304
2025-08-11 14:32:39 +02:00
Sam Brannen f11a1e6f82 Polish tests 2025-08-11 11:27:10 +03:00
rstoyanchev ffc785471b Fix checkstyle error 2025-08-08 12:31:13 +01:00
rstoyanchev 6e2fbfe108 Polishing contribution
Closes gh-35232
2025-08-08 11:50:07 +01:00
秦利斌 968e037503 Add documentation of RequestMapping about SpEL
Signed-off-by: 秦利斌 <68638598+Allan-QLB@users.noreply.github.com>
2025-08-08 11:37:25 +01:00
rstoyanchev f0a9f649c1 Allow null in ProblemDetail#type
See gh-35294
2025-08-08 11:36:53 +01:00
rstoyanchev 600d6c6fc0 Update contribution
Closes gh-34721
2025-08-08 11:31:21 +01:00
giampaolo 7a55ce48a9 Handle CancellationException in JdkClientHttpRequest
Handle CancellationException in order to throw an HttpTimeoutException
when the timeout handler caused the cancellation.

See gh-34721

Signed-off-by: giampaolo <giampaorr@gmail.com>

fix: use timeoutHandler with a flag isTimeout

    Closes gh-33973

    Signed-off-by: giampaolo <giampaorr@gmail.com>
2025-08-08 11:31:21 +01:00
Juergen Hoeller 5df9fd4eff Polishing (aligned with main) 2025-08-06 19:01:18 +02:00
Juergen Hoeller da13a24604 Allow any @Transactional propagation for listener with BEFORE_COMMIT phase
Closes gh-35150
2025-08-01 21:15:56 +02:00
Juergen Hoeller 67e88f3c20 Align task execution tracking and thread interruption on shutdown
Closes gh-35254
2025-08-01 21:15:25 +02:00
Patrick Strawderman 24e66b63d1 Refine StringUtils#uriDecode and update documentation
Refine the StringUtils#uriDecode method in the following ways:
- Use a StringBuilder instead of ByteArrayOutputStream, and only decode
  %-encoded sequences.
- Use HexFormat.fromHexDigits to decode hex sequences.
- Decode to a byte array that is only allocated if encoded sequences are
  encountered.

This commit adds another optimization mainly for the use case where
there is no encoded sequence, and updates the Javadoc of both
StringUtils#uriDecode and UriUtils#decode to match the implementation.

Signed-off-by: Patrick Strawderman <pstrawderman@netflix.com>
Co-Authored-by: Sebastien Deleuze <sebastien.deleuze@broadcom.com>

Closes gh-35253
2025-07-29 21:44:26 +02:00
Juergen Hoeller f3832c7262 Add note on SQL types with SqlBinaryValue/SqlCharacterValue
Closes gh-34786
2025-07-28 22:06:38 +02:00
Juergen Hoeller 16e99f289c Accept support for generated keys column name array on HSQLDB/Derby
Closes gh-34790
2025-07-28 22:04:18 +02:00
Juergen Hoeller 642e554c52 Process PostgreSQL-returned catalog/schema names in given case
Closes gh-35064
2025-07-28 20:28:45 +02:00
Juergen Hoeller 4f6304707d Polishing 2025-07-25 22:40:15 +02:00
Juergen Hoeller 3c112703d9 Introduce useCaches flag on UrlResource (for URLConnection access)
Propagated from PathMatchingResourcePatternResolver's setUseCaches.

Closes gh-35218
2025-07-25 22:40:09 +02:00
Juergen Hoeller 8c44a61033 Invalidate cache entries for matching types after singleton creation
Closes gh-35239
2025-07-25 22:38:57 +02:00
Sébastien Deleuze 5e338ef1b8 Make MessageSource locale parameter nullable
Closes gh-35230
2025-07-23 16:02:38 +02:00
Sam Brannen 445da24631 Upgrade to JUnit 5.13.4
Closes gh-35229
2025-07-21 13:17:38 +03:00
Brian Clozel 6d97791f5e Next development version (v6.2.10-SNAPSHOT) 2025-07-17 09:55:21 +02:00
94 changed files with 1535 additions and 549 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
toolchain: false
- version: 21
toolchain: true
- version: 23
- version: 24
toolchain: true
exclude:
- os:
+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.13.3/api/",
// "https://junit.org/junit5/docs/5.13.4/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/",
@@ -1,24 +1,26 @@
[[beans-classpath-scanning]]
= Classpath Scanning and Managed Components
Most examples in this chapter use XML to specify the configuration metadata that produces
each `BeanDefinition` within the Spring container. The previous section
(xref:core/beans/annotation-config.adoc[Annotation-based Container Configuration]) demonstrates how to provide a lot of the configuration
metadata through source-level annotations. Even in those examples, however, the "base"
bean definitions are explicitly defined in the XML file, while the annotations drive only
the dependency injection. This section describes an option for implicitly detecting the
candidate components by scanning the classpath. Candidate components are classes that
match against a filter criteria and have a corresponding bean definition registered with
the container. This removes the need to use XML to perform bean registration. Instead, you
can use annotations (for example, `@Component`), AspectJ type expressions, or your own
Most examples in this chapter use XML to specify the configuration metadata that
produces each `BeanDefinition` within the Spring container. The previous section
(xref:core/beans/annotation-config.adoc[Annotation-based Container Configuration])
demonstrates how to provide a lot of the configuration metadata through source-level
annotations. Even in those examples, however, the "base" bean definitions are explicitly
defined in the XML file, while the annotations drive only the dependency injection.
This section describes an option for implicitly detecting the candidate components by
scanning the classpath. Candidate components are classes that match against a filter
criteria and have a corresponding bean definition registered with the container.
This removes the need to use XML to perform bean registration. Instead, you can use
annotations (for example, `@Component`), AspectJ type expressions, or your own
custom filter criteria to select which classes have bean definitions registered with
the container.
[NOTE]
====
You can define beans using Java rather than using XML files. Take a look at the
`@Configuration`, `@Bean`, `@Import`, and `@DependsOn` annotations for examples of how to
use these features.
`@Configuration`, `@Bean`, `@Import`, and `@DependsOn` annotations for examples
of how to use these features.
====
@@ -830,10 +832,10 @@ definitions, there is no notion of bean definition inheritance, and inheritance
hierarchies at the class level are irrelevant for metadata purposes.
For details on web-specific scopes such as "`request`" or "`session`" in a Spring context,
see xref:core/beans/factory-scopes.adoc#beans-factory-scopes-other[Request, Session, Application, and WebSocket Scopes]. As with the pre-built annotations for those scopes,
you may also compose your own scoping annotations by using Spring's meta-annotation
approach: for example, a custom annotation meta-annotated with `@Scope("prototype")`,
possibly also declaring a custom scoped-proxy mode.
see xref:core/beans/factory-scopes.adoc#beans-factory-scopes-other[Request, Session, Application, and WebSocket Scopes].
As with the pre-built annotations for those scopes, you may also compose your own scoping
annotations by using Spring's meta-annotation approach: for example, a custom annotation
meta-annotated with `@Scope("prototype")`, possibly also declaring a custom scoped-proxy mode.
NOTE: To provide a custom strategy for scope resolution rather than relying on the
annotation-based approach, you can implement the
@@ -875,7 +877,8 @@ Kotlin::
----
When using certain non-singleton scopes, it may be necessary to generate proxies for the
scoped objects. The reasoning is described in xref:core/beans/factory-scopes.adoc#beans-factory-scopes-other-injection[Scoped Beans as Dependencies].
scoped objects. The reasoning is described in
xref:core/beans/factory-scopes.adoc#beans-factory-scopes-other-injection[Scoped Beans as Dependencies].
For this purpose, a scoped-proxy attribute is available on the component-scan
element. The three possible values are: `no`, `interfaces`, and `targetClass`. For example,
the following configuration results in standard JDK dynamic proxies:
@@ -77,6 +77,17 @@ exactly one candidate bean exists.
[TIP]
====
As stated in the documentation for Mockito, there are times when using `Mockito.when()` is
inappropriate for stubbing a spy for example, if calling a real method on a spy results
in undesired side effects.
To avoid such undesired side effects, consider using
`Mockito.doReturn(...).when(spy)...`, `Mockito.doThrow(...).when(spy)...`,
`Mockito.doNothing().when(spy)...`, and similar methods.
====
[NOTE]
====
Only _singleton_ beans can be overridden. Any attempt to override a non-singleton bean
will result in an exception.
@@ -261,7 +261,7 @@ Kotlin::
This improves on the design of our xref:testing/mockmvc/htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-usage[HtmlUnit test]
by leveraging the Page Object Pattern. As we mentioned in
xref:testing/mockmvc/htmlunit/webdriver.adoc#spring-mvc-test-server-htmlunit-webdriver-why[Why WebDriver and MockMvc?], we can use the Page Object Pattern
xref:testing/mockmvc/htmlunit/webdriver.adoc#mockmvc-server-htmlunit-webdriver-why[Why WebDriver and MockMvc?], we can use the Page Object Pattern
with HtmlUnit, but it is much easier with WebDriver. Consider the following
`CreateMessagePage` implementation:
@@ -2,10 +2,10 @@
= Application Events
The TestContext framework provides support for recording
xref:core/beans/context-introduction.adoc#context-functionality-events[application events] published in the
`ApplicationContext` so that assertions can be performed against those events within
tests. All events published during the execution of a single test are made available via
the `ApplicationEvents` API which allows you to process the events as a
xref:core/beans/context-introduction.adoc#context-functionality-events[application events]
published in the `ApplicationContext` so that assertions can be performed against those
events within tests. All events published during the execution of a single test are made
available via the `ApplicationEvents` API which allows you to process the events as a
`java.util.Stream`.
To use `ApplicationEvents` in your tests, do the following.
@@ -16,16 +16,23 @@ To use `ApplicationEvents` in your tests, do the following.
that `ApplicationEventsTestExecutionListener` is registered by default and only needs
to be manually registered if you have custom configuration via
`@TestExecutionListeners` that does not include the default listeners.
* Annotate a field of type `ApplicationEvents` with `@Autowired` and use that instance of
`ApplicationEvents` in your test and lifecycle methods (such as `@BeforeEach` and
`@AfterEach` methods in JUnit Jupiter).
** When using the xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[SpringExtension for JUnit Jupiter], you may declare a method
parameter of type `ApplicationEvents` in a test or lifecycle method as an alternative
to an `@Autowired` field in the test class.
* When using the
xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[SpringExtension for JUnit Jupiter],
declare a method parameter of type `ApplicationEvents` in a `@Test`, `@BeforeEach`, or
`@AfterEach` method.
** Since `ApplicationEvents` is scoped to the lifecycle of the current test method, this
is the recommended approach.
* Alternatively, you can annotate a field of type `ApplicationEvents` with `@Autowired`
and use that instance of `ApplicationEvents` in your test and lifecycle methods.
NOTE: `ApplicationEvents` is registered with the `ApplicationContext` as a _resolvable
dependency_ which is scoped to the lifecycle of the current test method. Consequently,
`ApplicationEvents` cannot be accessed outside the lifecycle of a test method and cannot be
`@Autowired` into the constructor of a test class.
The following test class uses the `SpringExtension` for JUnit Jupiter and
{assertj-docs}[AssertJ] to assert the types of application events
published while invoking a method in a Spring-managed component:
{assertj-docs}[AssertJ] to assert the types of application events published while
invoking a method in a Spring-managed component:
// Don't use "quotes" in the "subs" section because of the asterisks in /* ... */
[tabs]
@@ -38,16 +45,10 @@ Java::
@RecordApplicationEvents // <1>
class OrderServiceTests {
@Autowired
OrderService orderService;
@Autowired
ApplicationEvents events; // <2>
@Test
void submitOrder() {
void submitOrder(@Autowired OrderService service, ApplicationEvents events) { // <2>
// Invoke method in OrderService that publishes an event
orderService.submitOrder(new Order(/* ... */));
service.submitOrder(new Order(/* ... */));
// Verify that an OrderSubmitted event was published
long numEvents = events.stream(OrderSubmitted.class).count(); // <3>
assertThat(numEvents).isEqualTo(1);
@@ -66,16 +67,10 @@ Kotlin::
@RecordApplicationEvents // <1>
class OrderServiceTests {
@Autowired
lateinit var orderService: OrderService
@Autowired
lateinit var events: ApplicationEvents // <2>
@Test
fun submitOrder() {
fun submitOrder(@Autowired service: OrderService, events: ApplicationEvents) { // <2>
// Invoke method in OrderService that publishes an event
orderService.submitOrder(Order(/* ... */))
service.submitOrder(Order(/* ... */))
// Verify that an OrderSubmitted event was published
val numEvents = events.stream(OrderSubmitted::class).count() // <3>
assertThat(numEvents).isEqualTo(1)
@@ -294,7 +294,28 @@ allPartsEvents.windowUntil(PartEvent::isLast)
----
======
Note that the body contents of the `PartEvent` objects must be completely consumed, relayed, or released to avoid memory leaks.
NOTE: The body contents of the `PartEvent` objects must be completely consumed, relayed, or released to avoid memory leaks.
The following shows how to bind request parameters, including an optional `DataBinder` customization:
[tabs]
======
Java::
+
[source,java]
----
Pet pet = request.bind(Pet.class, dataBinder -> dataBinder.setAllowedFields("name"));
----
Kotlin::
+
[source,kotlin]
----
val pet = request.bind(Pet::class.java, {dataBinder -> dataBinder.setAllowedFields("name")})
----
======
[[webflux-fn-response]]
=== ServerResponse
@@ -2,18 +2,18 @@
= WebClient
:page-section-summary-toc: 1
Spring WebFlux includes a client to perform HTTP requests with. `WebClient` has a
functional, fluent API based on Reactor, see xref:web-reactive.adoc#webflux-reactive-libraries[Reactive Libraries],
Spring WebFlux includes a client to perform HTTP requests. `WebClient` has a
functional, fluent API based on Reactor (see xref:web/webflux-reactive-libraries.adoc[Reactive Libraries])
which enables declarative composition of asynchronous logic without the need to deal with
threads or concurrency. It is fully non-blocking, it supports streaming, and relies on
threads or concurrency. It is fully non-blocking, supports streaming, and relies on
the same xref:web/webflux/reactive-spring.adoc#webflux-codecs[codecs] that are also used to encode and
decode request and response content on the server side.
`WebClient` needs an HTTP client library to perform requests with. There is built-in
`WebClient` needs an HTTP client library to perform requests. There is built-in
support for the following:
* {reactor-github-org}/reactor-netty[Reactor Netty]
* {java-api}/java.net.http/java/net/http/HttpClient.html[JDK HttpClient]
* https://github.com/jetty-project/jetty-reactive-httpclient[Jetty Reactive HttpClient]
* https://hc.apache.org/index.html[Apache HttpComponents]
* Others can be plugged via `ClientHttpConnector`.
* Others can be plugged in via `ClientHttpConnector`.
@@ -234,10 +234,13 @@ Kotlin::
======
--
URI path patterns can also have embedded `${...}` placeholders that are resolved on startup
by using `PropertySourcesPlaceholderConfigurer` against local, system, environment, and
other property sources. You can use this, for example, to parameterize a base URL based on
some external configuration.
URI path patterns can also have:
- Embedded `${...}` placeholders that are resolved on startup via
`PropertySourcesPlaceholderConfigurer` against local, system, environment, and
other property sources. This is useful, for example, to parameterize a base URL based on
external configuration.
- SpEL expressions `#{...}`.
NOTE: Spring WebFlux uses `PathPattern` and the `PathPatternParser` for URI path matching support.
Both classes are located in `spring-web` and are expressly designed for use with HTTP URL
@@ -15,27 +15,27 @@ See xref:integration/rest-clients.adoc#rest-restclient[`RestClient`] for more de
[[webmvc-webclient]]
== `WebClient`
`WebClient` is a reactive client to perform HTTP requests with a fluent API.
`WebClient` is a reactive client for making HTTP requests with a fluent API.
See xref:web/webflux-webclient.adoc[WebClient] for more details.
See xref:web/webflux-webclient.adoc[`WebClient`] for more details.
[[webmvc-resttemplate]]
== `RestTemplate`
`RestTemplate` is a synchronous client to perform HTTP requests. It is the original
`RestTemplate` is a synchronous client for making HTTP requests. It is the original
Spring REST client and exposes a simple, template-method API over underlying HTTP client
libraries.
See xref:integration/rest-clients.adoc[REST Endpoints] for details.
See xref:integration/rest-clients.adoc#rest-resttemplate[`RestTemplate`] for details.
[[webmvc-http-interface]]
== HTTP Interface
The Spring Frameworks lets you define an HTTP service as a Java interface with HTTP
The Spring Framework lets you define an HTTP service as a Java interface with HTTP
exchange methods. You can then generate a proxy that implements this interface and
performs the exchanges. This helps to simplify HTTP remote access and provides additional
flexibility for to choose an API style such as synchronous or reactive.
flexibility for choosing an API style such as synchronous or reactive.
See xref:integration/rest-clients.adoc#rest-http-interface[REST Endpoints] for details.
See xref:integration/rest-clients.adoc#rest-http-interface[HTTP Interface] for details.
@@ -184,6 +184,26 @@ val map = request.params()
----
======
The following shows how to bind request parameters, including an optional `DataBinder` customization:
[tabs]
======
Java::
+
[source,java]
----
Pet pet = request.bind(Pet.class, dataBinder -> dataBinder.setAllowedFields("name"));
----
Kotlin::
+
[source,kotlin]
----
val pet = request.bind(Pet::class.java, {dataBinder -> dataBinder.setAllowedFields("name")})
----
======
[[webmvc-fn-response]]
=== ServerResponse
@@ -217,10 +217,13 @@ Kotlin::
----
======
URI path patterns can also have embedded `${...}` placeholders that are resolved on startup
by using `PropertySourcesPlaceholderConfigurer` against local, system, environment, and
other property sources. You can use this, for example, to parameterize a base URL based on
some external configuration.
URI path patterns can also have:
- Embedded `${...}` placeholders that are resolved on startup via
`PropertySourcesPlaceholderConfigurer` against local, system, environment, and
other property sources. This is useful, for example, to parameterize a base URL based on
external configuration.
- SpEL expression `#{...}`.
[[mvc-ann-requestmapping-pattern-comparison]]
+22 -22
View File
@@ -7,31 +7,31 @@ javaPlatform {
}
dependencies {
api(platform("com.fasterxml.jackson:jackson-bom:2.18.4"))
api(platform("io.micrometer:micrometer-bom:1.14.9"))
api(platform("io.netty:netty-bom:4.1.123.Final"))
api(platform("com.fasterxml.jackson:jackson-bom:2.18.4.1"))
api(platform("io.micrometer:micrometer-bom:1.14.11"))
api(platform("io.netty:netty-bom:4.1.127.Final"))
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
api(platform("io.projectreactor:reactor-bom:2024.0.8"))
api(platform("io.projectreactor:reactor-bom:2024.0.10"))
api(platform("io.rsocket:rsocket-bom:1.1.5"))
api(platform("org.apache.groovy:groovy-bom:4.0.27"))
api(platform("org.apache.groovy:groovy-bom:4.0.28"))
api(platform("org.apache.logging.log4j:log4j-bom:2.21.1"))
api(platform("org.assertj:assertj-bom:3.27.3"))
api(platform("org.eclipse.jetty:jetty-bom:12.0.23"))
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.23"))
api(platform("org.eclipse.jetty:jetty-bom:12.0.26"))
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.26"))
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.13.3"))
api(platform("org.mockito:mockito-bom:5.18.0"))
api(platform("org.junit:junit-bom:5.13.4"))
api(platform("org.mockito:mockito-bom:5.19.0"))
constraints {
api("com.fasterxml:aalto-xml:1.3.2")
api("com.fasterxml:aalto-xml:1.3.3")
api("com.fasterxml.woodstox:woodstox-core:6.7.0")
api("com.github.ben-manes.caffeine:caffeine:3.2.1")
api("com.github.ben-manes.caffeine:caffeine:3.2.2")
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")
api("com.google.code.gson:gson:2.13.1")
api("com.google.protobuf:protobuf-java-util:4.30.2")
api("com.google.protobuf:protobuf-java-util:4.32.0")
api("com.h2database:h2:2.3.232")
api("com.jayway.jsonpath:json-path:2.9.0")
api("com.oracle.database.jdbc:ojdbc11:21.9.0.0")
@@ -53,11 +53,11 @@ dependencies {
api("io.r2dbc:r2dbc-h2:1.0.0.RELEASE")
api("io.r2dbc:r2dbc-spi-test:1.0.0.RELEASE")
api("io.r2dbc:r2dbc-spi:1.0.0.RELEASE")
api("io.reactivex.rxjava3:rxjava:3.1.10")
api("io.reactivex.rxjava3:rxjava:3.1.11")
api("io.smallrye.reactive:mutiny:1.10.0")
api("io.undertow:undertow-core:2.3.18.Final")
api("io.undertow:undertow-servlet:2.3.18.Final")
api("io.undertow:undertow-websockets-jsr:2.3.18.Final")
api("io.undertow:undertow-core:2.3.19.Final")
api("io.undertow:undertow-servlet:2.3.19.Final")
api("io.undertow:undertow-websockets-jsr:2.3.19.Final")
api("io.vavr:vavr:0.10.4")
api("jakarta.activation:jakarta.activation-api:2.0.1")
api("jakarta.annotation:jakarta.annotation-api:2.0.0")
@@ -100,7 +100,7 @@ dependencies {
api("org.apache.derby:derbyclient:10.16.1.1")
api("org.apache.derby:derbytools:10.16.1.1")
api("org.apache.httpcomponents.client5:httpclient5:5.5")
api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3.4")
api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3.5")
api("org.apache.poi:poi-ooxml:5.2.5")
api("org.apache.tomcat.embed:tomcat-embed-core:10.1.28")
api("org.apache.tomcat.embed:tomcat-embed-websocket:10.1.28")
@@ -115,7 +115,7 @@ dependencies {
api("org.crac:crac:1.4.0")
api("org.dom4j:dom4j:2.1.4")
api("org.easymock:easymock:5.5.0")
api("org.eclipse.jetty:jetty-reactive-httpclient:4.0.9")
api("org.eclipse.jetty:jetty-reactive-httpclient:4.0.11")
api("org.eclipse.persistence:org.eclipse.persistence.jpa:3.0.4")
api("org.eclipse:yasson:2.0.4")
api("org.ehcache:ehcache:3.10.8")
@@ -129,16 +129,16 @@ 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.13.0")
api("org.htmlunit:htmlunit:4.16.0")
api("org.javamoney:moneta:1.4.4")
api("org.jruby:jruby:9.4.12.0")
api("org.jruby:jruby:9.4.13.0")
api("org.junit.support:testng-engine:1.0.5")
api("org.mozilla:rhino:1.7.15")
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.33.0")
api("org.seleniumhq.selenium:selenium-java:4.34.0")
api("org.seleniumhq.selenium:htmlunit3-driver:4.35.0")
api("org.seleniumhq.selenium:selenium-java:4.35.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.9-SNAPSHOT
version=6.2.11
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
@@ -121,12 +121,12 @@ public abstract class AopConfigUtils {
Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) {
BeanDefinition apcDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
if (!cls.getName().equals(apcDefinition.getBeanClassName())) {
int currentPriority = findPriorityForClass(apcDefinition.getBeanClassName());
BeanDefinition beanDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
if (!cls.getName().equals(beanDefinition.getBeanClassName())) {
int currentPriority = findPriorityForClass(beanDefinition.getBeanClassName());
int requiredPriority = findPriorityForClass(cls);
if (currentPriority < requiredPriority) {
apcDefinition.setBeanClassName(cls.getName());
beanDefinition.setBeanClassName(cls.getName());
}
}
return null;
@@ -134,8 +134,8 @@ public abstract class AopConfigUtils {
RootBeanDefinition beanDefinition = new RootBeanDefinition(cls);
beanDefinition.setSource(source);
beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE);
beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE);
registry.registerBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME, beanDefinition);
return beanDefinition;
}
@@ -117,6 +117,7 @@ public abstract class AbstractAdvisingBeanPostProcessor extends ProxyProcessorSu
}
proxyFactory.addAdvisor(this.advisor);
customizeProxyFactory(proxyFactory);
proxyFactory.setPreFiltered(true);
// Use original ClassLoader if bean class not locally loaded in overriding class loader
ClassLoader classLoader = getProxyClassLoader();
@@ -24,9 +24,9 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.lang.Nullable;
/**
* Extension of {@link AbstractAutoProxyCreator} which implements {@link BeanFactoryAware},
* adds exposure of the original target class for each proxied bean
* ({@link AutoProxyUtils#ORIGINAL_TARGET_CLASS_ATTRIBUTE}),
* Extension of {@link AbstractAdvisingBeanPostProcessor} which implements
* {@link BeanFactoryAware}, adds exposure of the original target class for each
* proxied bean ({@link AutoProxyUtils#ORIGINAL_TARGET_CLASS_ATTRIBUTE}),
* and participates in an externally enforced target-class mode for any given bean
* ({@link AutoProxyUtils#PRESERVE_TARGET_CLASS_ATTRIBUTE}).
* This post-processor is therefore aligned with {@link AbstractAutoProxyCreator}.
@@ -520,8 +520,7 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
* to check whether the bean with the given name matches the specified type. Allow
* additional constraints to be applied to ensure that beans are not created early.
* @param name the name of the bean to query
* @param typeToMatch the type to match against (as a
* {@code ResolvableType})
* @param typeToMatch the type to match against (as a {@code ResolvableType})
* @return {@code true} if the bean type matches, {@code false} if it
* doesn't match or cannot be determined yet
* @throws NoSuchBeanDefinitionException if there is no bean with the given name
@@ -58,6 +58,7 @@ import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.BeanNotOfRequiredTypeException;
import org.springframework.beans.factory.CannotLoadBeanClassException;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InjectionPoint;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
@@ -196,8 +197,8 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
/** Map from bean name to merged BeanDefinitionHolder. */
private final Map<String, BeanDefinitionHolder> mergedBeanDefinitionHolders = new ConcurrentHashMap<>(256);
/** Set of bean definition names with a primary marker. */
private final Set<String> primaryBeanNames = ConcurrentHashMap.newKeySet(16);
/** Map of bean definition names with a primary marker plus corresponding type. */
private final Map<String, Class<?>> primaryBeanNamesWithType = new ConcurrentHashMap<>(16);
/** Map of singleton and non-singleton bean names, keyed by dependency type. */
private final Map<Class<?>, String[]> allBeanNamesByType = new ConcurrentHashMap<>(64);
@@ -1037,7 +1038,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
protected void cacheMergedBeanDefinition(RootBeanDefinition mbd, String beanName) {
super.cacheMergedBeanDefinition(mbd, beanName);
if (mbd.isPrimary()) {
this.primaryBeanNames.add(beanName);
this.primaryBeanNamesWithType.put(beanName, Void.class);
}
}
@@ -1112,11 +1113,10 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);
// Trigger initialization of all non-lazy singleton beans...
List<CompletableFuture<?>> futures = new ArrayList<>();
this.preInstantiationThread.set(PreInstantiation.MAIN);
this.mainThreadPrefix = getThreadNamePrefix();
try {
List<CompletableFuture<?>> futures = new ArrayList<>();
for (String beanName : beanNames) {
RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
if (!mbd.isAbstract() && mbd.isSingleton()) {
@@ -1126,21 +1126,20 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
}
}
}
if (!futures.isEmpty()) {
try {
CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[0])).join();
}
catch (CompletionException ex) {
ReflectionUtils.rethrowRuntimeException(ex.getCause());
}
}
}
finally {
this.mainThreadPrefix = null;
this.preInstantiationThread.remove();
}
if (!futures.isEmpty()) {
try {
CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[0])).join();
}
catch (CompletionException ex) {
ReflectionUtils.rethrowRuntimeException(ex.getCause());
}
}
// Trigger post-initialization callback for all applicable beans...
for (String beanName : beanNames) {
Object singletonInstance = getSingleton(beanName, false);
@@ -1313,7 +1312,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
// Cache a primary marker for the given bean.
if (beanDefinition.isPrimary()) {
this.primaryBeanNames.add(beanName);
this.primaryBeanNamesWithType.put(beanName, Void.class);
}
}
@@ -1405,7 +1404,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
destroySingleton(beanName);
// Remove a cached primary marker for the given bean.
this.primaryBeanNames.remove(beanName);
this.primaryBeanNamesWithType.remove(beanName);
// Notify all post-processors that the specified bean definition has been reset.
for (MergedBeanDefinitionPostProcessor processor : getBeanPostProcessorCache().mergedDefinition) {
@@ -1455,11 +1454,27 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
}
}
@Override
protected void addSingleton(String beanName, Object singletonObject) {
super.addSingleton(beanName, singletonObject);
Predicate<Class<?>> filter = (beanType -> beanType != Object.class && beanType.isInstance(singletonObject));
this.allBeanNamesByType.keySet().removeIf(filter);
this.singletonBeanNamesByType.keySet().removeIf(filter);
if (this.primaryBeanNamesWithType.containsKey(beanName) && singletonObject.getClass() != NullBean.class) {
Class<?> beanType = (singletonObject instanceof FactoryBean<?> fb ?
getTypeForFactoryBean(fb) : singletonObject.getClass());
if (beanType != null) {
this.primaryBeanNamesWithType.put(beanName, beanType);
}
}
}
@Override
public void registerSingleton(String beanName, Object singletonObject) throws IllegalStateException {
super.registerSingleton(beanName, singletonObject);
updateManualSingletonNames(set -> set.add(beanName), set -> !this.beanDefinitionMap.containsKey(beanName));
clearByTypeCache();
}
@Override
@@ -2261,8 +2276,12 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
* not matching the given bean name.
*/
private boolean hasPrimaryConflict(String beanName, Class<?> dependencyType) {
for (String candidate : this.primaryBeanNames) {
if (isTypeMatch(candidate, dependencyType) && !candidate.equals(beanName)) {
for (Map.Entry<String, Class<?>> candidate : this.primaryBeanNamesWithType.entrySet()) {
String candidateName = candidate.getKey();
Class<?> candidateType = candidate.getValue();
if (!candidateName.equals(beanName) && (candidateType != Void.class ?
dependencyType.isAssignableFrom(candidateType) : // cached singleton class for primary bean
isTypeMatch(candidateName, dependencyType))) { // not instantiated yet or not a singleton
return true;
}
}
@@ -23,6 +23,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import org.apache.commons.logging.Log;
@@ -418,14 +419,29 @@ class DisposableBeanAdapter implements DisposableBean, Runnable, Serializable {
String destroyMethodName = beanDefinition.resolvedDestroyMethodName;
if (destroyMethodName == null) {
destroyMethodName = beanDefinition.getDestroyMethodName();
boolean autoCloseable = (AutoCloseable.class.isAssignableFrom(target));
boolean autoCloseable = AutoCloseable.class.isAssignableFrom(target);
boolean executorService = ExecutorService.class.isAssignableFrom(target);
if (AbstractBeanDefinition.INFER_METHOD.equals(destroyMethodName) ||
(destroyMethodName == null && autoCloseable)) {
(destroyMethodName == null && (autoCloseable || executorService))) {
// Only perform destroy method inference in case of the bean
// not explicitly implementing the DisposableBean interface
destroyMethodName = null;
if (!(DisposableBean.class.isAssignableFrom(target))) {
if (autoCloseable) {
if (executorService) {
destroyMethodName = SHUTDOWN_METHOD_NAME;
try {
// On JDK 19+, avoid the ExecutorService-level AutoCloseable default implementation
// which awaits task termination for 1 day, even for delayed tasks such as cron jobs.
// Custom close() implementations in ExecutorService subclasses are still accepted.
if (target.getMethod(CLOSE_METHOD_NAME).getDeclaringClass() != ExecutorService.class) {
destroyMethodName = CLOSE_METHOD_NAME;
}
}
catch (NoSuchMethodException ex) {
// Ignore - stick with shutdown()
}
}
else if (autoCloseable) {
destroyMethodName = CLOSE_METHOD_NAME;
}
else {
@@ -103,7 +103,7 @@ public class PathEditor extends PropertyEditorSupport {
if (resource == null) {
setValue(null);
}
else if (nioPathCandidate && !resource.exists()) {
else if (nioPathCandidate && (!resource.isFile() || !resource.exists())) {
setValue(Paths.get(text).normalize());
}
else {
@@ -3202,6 +3202,29 @@ class DefaultListableBeanFactoryTests {
assertThat(holder.getNonPublicEnum()).isEqualTo(NonPublicEnum.VALUE_1);
}
@Test
void mostSpecificCacheEntryForTypeMatching() {
RootBeanDefinition bd1 = new RootBeanDefinition();
bd1.setFactoryBeanName("config");
bd1.setFactoryMethodName("create");
lbf.registerBeanDefinition("config", new RootBeanDefinition(BeanWithFactoryMethod.class));
lbf.registerBeanDefinition("bd1", bd1);
lbf.registerBeanDefinition("bd2", new RootBeanDefinition(NestedTestBean.class));
lbf.freezeConfiguration();
String[] allBeanNames = lbf.getBeanNamesForType(Object.class);
String[] nestedBeanNames = lbf.getBeanNamesForType(NestedTestBean.class);
assertThat(lbf.getType("bd1")).isEqualTo(TestBean.class);
assertThat(lbf.getBeanNamesForType(TestBean.class)).containsExactly("bd1");
assertThat(lbf.getBeanNamesForType(DerivedTestBean.class)).isEmpty();
lbf.getBean("bd1");
assertThat(lbf.getType("bd1")).isEqualTo(DerivedTestBean.class);
assertThat(lbf.getBeanNamesForType(TestBean.class)).containsExactly("bd1");
assertThat(lbf.getBeanNamesForType(DerivedTestBean.class)).containsExactly("bd1");
assertThat(lbf.getBeanNamesForType(NestedTestBean.class)).isSameAs(nestedBeanNames);
assertThat(lbf.getBeanNamesForType(Object.class)).isSameAs(allBeanNames);
}
private int registerBeanDefinitions(Properties p) {
return registerBeanDefinitions(p, null);
@@ -3418,7 +3441,7 @@ class DefaultListableBeanFactoryTests {
}
public TestBean create() {
TestBean tb = new TestBean();
DerivedTestBean tb = new DerivedTestBean();
tb.setName(this.name);
return tb;
}
@@ -3646,11 +3669,11 @@ class DefaultListableBeanFactoryTests {
private FactoryBean<?> factoryBean;
public final FactoryBean<?> getFactoryBean() {
public FactoryBean<?> getFactoryBean() {
return this.factoryBean;
}
public final void setFactoryBean(final FactoryBean<?> factoryBean) {
public void setFactoryBean(FactoryBean<?> factoryBean) {
this.factoryBean = factoryBean;
}
}
@@ -17,6 +17,7 @@
package org.springframework.beans.factory.support;
import java.lang.reflect.Method;
import java.util.concurrent.ExecutorService;
import org.junit.jupiter.api.Test;
@@ -59,13 +60,38 @@ class RootBeanDefinitionTests {
}
@Test
void resolveDestroyMethodWithMatchingCandidateReplacedInferredVaue() {
void resolveDestroyMethodWithMatchingCandidateReplacedForCloseMethod() {
RootBeanDefinition beanDefinition = new RootBeanDefinition(BeanWithCloseMethod.class);
beanDefinition.setDestroyMethodName(AbstractBeanDefinition.INFER_METHOD);
beanDefinition.resolveDestroyMethodIfNecessary();
assertThat(beanDefinition.getDestroyMethodNames()).containsExactly("close");
}
@Test
void resolveDestroyMethodWithMatchingCandidateReplacedForShutdownMethod() {
RootBeanDefinition beanDefinition = new RootBeanDefinition(BeanWithShutdownMethod.class);
beanDefinition.setDestroyMethodName(AbstractBeanDefinition.INFER_METHOD);
beanDefinition.resolveDestroyMethodIfNecessary();
assertThat(beanDefinition.getDestroyMethodNames()).containsExactly("shutdown");
}
@Test
void resolveDestroyMethodWithMatchingCandidateReplacedForExecutorService() {
RootBeanDefinition beanDefinition = new RootBeanDefinition(BeanImplementingExecutorService.class);
beanDefinition.setDestroyMethodName(AbstractBeanDefinition.INFER_METHOD);
beanDefinition.resolveDestroyMethodIfNecessary();
assertThat(beanDefinition.getDestroyMethodNames()).containsExactly("shutdown");
// even on JDK 19+ where the ExecutorService interface declares a default AutoCloseable implementation
}
@Test
void resolveDestroyMethodWithMatchingCandidateReplacedForAutoCloseableExecutorService() {
RootBeanDefinition beanDefinition = new RootBeanDefinition(BeanImplementingExecutorServiceAndAutoCloseable.class);
beanDefinition.setDestroyMethodName(AbstractBeanDefinition.INFER_METHOD);
beanDefinition.resolveDestroyMethodIfNecessary();
assertThat(beanDefinition.getDestroyMethodNames()).containsExactly("close");
}
@Test
void resolveDestroyMethodWithNoCandidateSetDestroyMethodNameToNull() {
RootBeanDefinition beanDefinition = new RootBeanDefinition(BeanWithNoDestroyMethod.class);
@@ -90,6 +116,25 @@ class RootBeanDefinitionTests {
}
static class BeanWithShutdownMethod {
public void shutdown() {
}
}
abstract static class BeanImplementingExecutorService implements ExecutorService {
}
abstract static class BeanImplementingExecutorServiceAndAutoCloseable implements ExecutorService, AutoCloseable {
@Override
public void close() {
}
}
static class BeanWithNoDestroyMethod {
}
@@ -55,7 +55,7 @@ public interface MessageSource {
* @see java.text.MessageFormat
*/
@Nullable
String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale);
String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, @Nullable Locale locale);
/**
* Try to resolve the message. Treat as an error if the message can't be found.
@@ -71,7 +71,7 @@ public interface MessageSource {
* @see #getMessage(MessageSourceResolvable, Locale)
* @see java.text.MessageFormat
*/
String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;
String getMessage(String code, @Nullable Object[] args, @Nullable Locale locale) throws NoSuchMessageException;
/**
* Try to resolve the message using all the attributes contained within the
@@ -91,6 +91,6 @@ public interface MessageSource {
* @see MessageSourceResolvable#getDefaultMessage()
* @see java.text.MessageFormat
*/
String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;
String getMessage(MessageSourceResolvable resolvable, @Nullable Locale locale) throws NoSuchMessageException;
}
@@ -51,9 +51,9 @@ import org.springframework.core.annotation.AliasFor;
* ignored.
*
* @author Stephane Nicoll
* @since 6.2
* @see Reflective @Reflective
* @see RegisterReflection @RegisterReflection
* @since 6.2
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@@ -1502,17 +1502,17 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
@Override
@Nullable
public String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) {
public String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, @Nullable Locale locale) {
return getMessageSource().getMessage(code, args, defaultMessage, locale);
}
@Override
public String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException {
public String getMessage(String code, @Nullable Object[] args, @Nullable Locale locale) throws NoSuchMessageException {
return getMessageSource().getMessage(code, args, locale);
}
@Override
public String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException {
public String getMessage(MessageSourceResolvable resolvable, @Nullable Locale locale) throws NoSuchMessageException {
return getMessageSource().getMessage(resolvable, locale);
}
@@ -138,7 +138,7 @@ public abstract class AbstractMessageSource extends MessageSourceSupport impleme
@Override
@Nullable
public final String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) {
public final String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, @Nullable Locale locale) {
String msg = getMessageInternal(code, args, locale);
if (msg != null) {
return msg;
@@ -150,7 +150,7 @@ public abstract class AbstractMessageSource extends MessageSourceSupport impleme
}
@Override
public final String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException {
public final String getMessage(String code, @Nullable Object[] args, @Nullable Locale locale) throws NoSuchMessageException {
String msg = getMessageInternal(code, args, locale);
if (msg != null) {
return msg;
@@ -159,11 +159,16 @@ public abstract class AbstractMessageSource extends MessageSourceSupport impleme
if (fallback != null) {
return fallback;
}
throw new NoSuchMessageException(code, locale);
if (locale == null ) {
throw new NoSuchMessageException(code);
}
else {
throw new NoSuchMessageException(code, locale);
}
}
@Override
public final String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException {
public final String getMessage(MessageSourceResolvable resolvable, @Nullable Locale locale) throws NoSuchMessageException {
String[] codes = resolvable.getCodes();
if (codes != null) {
for (String code : codes) {
@@ -177,7 +182,13 @@ public abstract class AbstractMessageSource extends MessageSourceSupport impleme
if (defaultMessage != null) {
return defaultMessage;
}
throw new NoSuchMessageException(!ObjectUtils.isEmpty(codes) ? codes[codes.length - 1] : "", locale);
String code = !ObjectUtils.isEmpty(codes) ? codes[codes.length - 1] : "";
if (locale == null ) {
throw new NoSuchMessageException(code);
}
else {
throw new NoSuchMessageException(code, locale);
}
}
@@ -284,7 +295,7 @@ public abstract class AbstractMessageSource extends MessageSourceSupport impleme
* @see #getDefaultMessage(String)
*/
@Nullable
protected String getDefaultMessage(MessageSourceResolvable resolvable, Locale locale) {
protected String getDefaultMessage(MessageSourceResolvable resolvable, @Nullable Locale locale) {
String defaultMessage = resolvable.getDefaultMessage();
String[] codes = resolvable.getCodes();
if (defaultMessage != null) {
@@ -331,7 +342,7 @@ public abstract class AbstractMessageSource extends MessageSourceSupport impleme
* @return an array of arguments with any MessageSourceResolvables resolved
*/
@Override
protected Object[] resolveArguments(@Nullable Object[] args, Locale locale) {
protected Object[] resolveArguments(@Nullable Object[] args, @Nullable Locale locale) {
if (ObjectUtils.isEmpty(args)) {
return super.resolveArguments(args, locale);
}
@@ -55,7 +55,7 @@ public class DelegatingMessageSource extends MessageSourceSupport implements Hie
@Override
@Nullable
public String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) {
public String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, @Nullable Locale locale) {
if (this.parentMessageSource != null) {
return this.parentMessageSource.getMessage(code, args, defaultMessage, locale);
}
@@ -68,17 +68,22 @@ public class DelegatingMessageSource extends MessageSourceSupport implements Hie
}
@Override
public String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException {
public String getMessage(String code, @Nullable Object[] args, @Nullable Locale locale) throws NoSuchMessageException {
if (this.parentMessageSource != null) {
return this.parentMessageSource.getMessage(code, args, locale);
}
else {
throw new NoSuchMessageException(code, locale);
if (locale == null) {
throw new NoSuchMessageException(code);
}
else {
throw new NoSuchMessageException(code, locale);
}
}
}
@Override
public String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException {
public String getMessage(MessageSourceResolvable resolvable, @Nullable Locale locale) throws NoSuchMessageException {
if (this.parentMessageSource != null) {
return this.parentMessageSource.getMessage(resolvable, locale);
}
@@ -88,7 +93,12 @@ public class DelegatingMessageSource extends MessageSourceSupport implements Hie
}
String[] codes = resolvable.getCodes();
String code = (codes != null && codes.length > 0 ? codes[0] : "");
throw new NoSuchMessageException(code, locale);
if (locale == null) {
throw new NoSuchMessageException(code);
}
else {
throw new NoSuchMessageException(code, locale);
}
}
}
@@ -98,7 +98,7 @@ public abstract class MessageSourceSupport {
* @return the rendered default message (with resolved arguments)
* @see #formatMessage(String, Object[], java.util.Locale)
*/
protected String renderDefaultMessage(String defaultMessage, @Nullable Object[] args, Locale locale) {
protected String renderDefaultMessage(String defaultMessage, @Nullable Object[] args, @Nullable Locale locale) {
return formatMessage(defaultMessage, args, locale);
}
@@ -112,7 +112,7 @@ public abstract class MessageSourceSupport {
* @param locale the Locale used for formatting
* @return the formatted message (with resolved arguments)
*/
protected String formatMessage(String msg, @Nullable Object[] args, Locale locale) {
protected String formatMessage(String msg, @Nullable Object[] args, @Nullable Locale locale) {
if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) {
return msg;
}
@@ -146,7 +146,7 @@ public abstract class MessageSourceSupport {
* @param locale the Locale to create a {@code MessageFormat} for
* @return the {@code MessageFormat} instance
*/
protected MessageFormat createMessageFormat(String msg, Locale locale) {
protected MessageFormat createMessageFormat(String msg, @Nullable Locale locale) {
return new MessageFormat(msg, locale);
}
@@ -158,7 +158,7 @@ public abstract class MessageSourceSupport {
* @param locale the Locale to resolve against
* @return the resolved argument array
*/
protected Object[] resolveArguments(@Nullable Object[] args, Locale locale) {
protected Object[] resolveArguments(@Nullable Object[] args, @Nullable Locale locale) {
return (args != null ? args : new Object[0]);
}
@@ -376,7 +376,7 @@ public class SimpleAsyncTaskScheduler extends SimpleAsyncTaskExecutor implements
@Override
public boolean isRunning() {
return this.triggerLifecycle.isRunning();
return (this.triggerLifecycle.isRunning() || this.fixedDelayLifecycle.isRunning());
}
@Override
@@ -60,12 +60,6 @@ class CacheOperationExpressionEvaluatorTests {
private final AnnotationCacheOperationSource source = new AnnotationCacheOperationSource();
private Collection<CacheOperation> getOps(String name) {
Method method = ReflectionUtils.findMethod(AnnotatedClass.class, name, Object.class, Object.class);
return this.source.getCacheOperations(method, AnnotatedClass.class);
}
@Test
void testMultipleCachingSource() {
Collection<CacheOperation> ops = getOps("multipleCaching");
@@ -144,6 +138,12 @@ class CacheOperationExpressionEvaluatorTests {
assertThat(value).isEqualTo(String.class.getName());
}
private Collection<CacheOperation> getOps(String name) {
Method method = ReflectionUtils.findMethod(AnnotatedClass.class, name, Object.class, Object.class);
return this.source.getCacheOperations(method, AnnotatedClass.class);
}
private EvaluationContext createEvaluationContext(Object result) {
return createEvaluationContext(result, null);
}
@@ -104,6 +104,7 @@ class CachePutEvaluationTests {
assertThat(this.cache.get(anotherValue + 100).get()).as("Wrong value for @CachePut key").isEqualTo(anotherValue);
}
@Configuration
@EnableCaching
static class Config implements CachingConfigurer {
@@ -121,8 +122,10 @@ class CachePutEvaluationTests {
}
@CacheConfig("test")
public static class SimpleService {
private AtomicLong counter = new AtomicLong();
/**
@@ -144,4 +147,5 @@ class CachePutEvaluationTests {
return this.counter.getAndIncrement();
}
}
}
@@ -18,6 +18,7 @@ package org.springframework.core.annotation;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -38,6 +39,7 @@ import org.springframework.util.StringUtils;
* interface-declared parameter annotations from the concrete target method.
*
* @author Juergen Hoeller
* @author Sam Brannen
* @since 6.1
* @see #getMethodAnnotation(Class)
* @see #getMethodParameters()
@@ -181,7 +183,7 @@ public class AnnotatedMethod {
clazz = null;
}
if (clazz != null) {
for (Method candidate : clazz.getMethods()) {
for (Method candidate : clazz.getDeclaredMethods()) {
if (isOverrideFor(candidate)) {
parameterAnnotations.add(candidate.getParameterAnnotations());
}
@@ -194,8 +196,9 @@ public class AnnotatedMethod {
}
private boolean isOverrideFor(Method candidate) {
if (!candidate.getName().equals(this.method.getName()) ||
candidate.getParameterCount() != this.method.getParameterCount()) {
if (Modifier.isPrivate(candidate.getModifiers()) ||
!candidate.getName().equals(this.method.getName()) ||
(candidate.getParameterCount() != this.method.getParameterCount())) {
return false;
}
Class<?>[] paramTypes = this.method.getParameterTypes();
@@ -204,7 +207,7 @@ public class AnnotatedMethod {
}
for (int i = 0; i < paramTypes.length; i++) {
if (paramTypes[i] !=
ResolvableType.forMethodParameter(candidate, i, this.method.getDeclaringClass()).resolve()) {
ResolvableType.forMethodParameter(candidate, i, this.method.getDeclaringClass()).toClass()) {
return false;
}
}
@@ -372,14 +372,14 @@ abstract class AnnotationsScanner {
private static boolean hasSameGenericTypeParameters(
Method rootMethod, Method candidateMethod, Class<?>[] rootParameterTypes) {
Class<?> sourceDeclaringClass = rootMethod.getDeclaringClass();
Class<?> rootDeclaringClass = rootMethod.getDeclaringClass();
Class<?> candidateDeclaringClass = candidateMethod.getDeclaringClass();
if (!candidateDeclaringClass.isAssignableFrom(sourceDeclaringClass)) {
if (!candidateDeclaringClass.isAssignableFrom(rootDeclaringClass)) {
return false;
}
for (int i = 0; i < rootParameterTypes.length; i++) {
Class<?> resolvedParameterType = ResolvableType.forMethodParameter(
candidateMethod, i, sourceDeclaringClass).resolve();
candidateMethod, i, rootDeclaringClass).toClass();
if (rootParameterTypes[i] != resolvedParameterType) {
return false;
}
@@ -361,12 +361,22 @@ public abstract class AbstractFileResolvingResource extends AbstractResource {
* @throws IOException if thrown from URLConnection methods
*/
protected void customizeConnection(URLConnection con) throws IOException {
ResourceUtils.useCachesIfNecessary(con);
useCachesIfNecessary(con);
if (con instanceof HttpURLConnection httpCon) {
customizeConnection(httpCon);
}
}
/**
* Apply {@link URLConnection#setUseCaches useCaches} if necessary.
* @param con the URLConnection to customize
* @since 6.2.10
* @see ResourceUtils#useCachesIfNecessary(URLConnection)
*/
void useCachesIfNecessary(URLConnection con) {
ResourceUtils.useCachesIfNecessary(con);
}
/**
* Customize the given {@link HttpURLConnection} before fetching the resource.
* <p>Can be overridden in subclasses for configuring request headers and timeouts.
@@ -109,7 +109,9 @@ public class FileUrlResource extends UrlResource implements WritableResource {
@Override
public Resource createRelative(String relativePath) throws MalformedURLException {
return new FileUrlResource(createRelativeURL(relativePath));
FileUrlResource resource = new FileUrlResource(createRelativeURL(relativePath));
resource.useCaches = this.useCaches;
return resource;
}
}
@@ -117,8 +117,10 @@ public interface Resource extends InputStreamSource {
/**
* Return a File handle for this resource.
* @throws java.io.FileNotFoundException if the resource cannot be resolved as
* absolute file path, i.e. if the resource is not available in a file system
* <p>Note: This only works for files in the default file system.
* @throws UnsupportedOperationException if the resource is a file but cannot be
* exposed as a {@code java.io.File}; an alternative to {@code FileNotFoundException}
* @throws java.io.FileNotFoundException if the resource cannot be resolved as a file
* @throws IOException in case of general resolution/reading failures
* @see #getInputStream()
*/
@@ -67,6 +67,12 @@ public class UrlResource extends AbstractFileResolvingResource {
@Nullable
private volatile String cleanedUrl;
/**
* Whether to use URLConnection caches ({@code null} means default).
*/
@Nullable
volatile Boolean useCaches;
/**
* Create a new {@code UrlResource} based on the given URL object.
@@ -216,11 +222,22 @@ public class UrlResource extends AbstractFileResolvingResource {
return cleanedUrl;
}
/**
* Set an explicit flag for {@link URLConnection#setUseCaches},
* to be applied for any {@link URLConnection} operation in this resource.
* <p>By default, caching will be applied only to jar resources.
* An explicit {@code true} flag applies caching to all resources, whereas an
* explicit {@code false} flag turns off caching for jar resources as well.
* @since 6.2.10
* @see ResourceUtils#useCachesIfNecessary
*/
public void setUseCaches(boolean useCaches) {
this.useCaches = useCaches;
}
/**
* This implementation opens an InputStream for the given URL.
* <p>It sets the {@code useCaches} flag to {@code false},
* mainly to avoid jar file locking on Windows.
* @see java.net.URL#openConnection()
* @see java.net.URLConnection#setUseCaches(boolean)
* @see java.net.URLConnection#getInputStream()
@@ -251,6 +268,17 @@ public class UrlResource extends AbstractFileResolvingResource {
}
}
@Override
void useCachesIfNecessary(URLConnection con) {
Boolean useCaches = this.useCaches;
if (useCaches != null) {
con.setUseCaches(useCaches);
}
else {
super.useCachesIfNecessary(con);
}
}
/**
* This implementation returns the underlying URL reference.
*/
@@ -305,7 +333,9 @@ public class UrlResource extends AbstractFileResolvingResource {
*/
@Override
public Resource createRelative(String relativePath) throws MalformedURLException {
return new UrlResource(createRelativeURL(relativePath));
UrlResource resource = new UrlResource(createRelativeURL(relativePath));
resource.useCaches = this.useCaches;
return resource;
}
/**
@@ -16,7 +16,6 @@
package org.springframework.core.io.buffer;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -223,17 +222,17 @@ public abstract class DataBufferUtils {
try {
if (resource.isFile()) {
File file = resource.getFile();
Path filePath = resource.getFile().toPath();
return readAsynchronousFileChannel(
() -> AsynchronousFileChannel.open(file.toPath(), StandardOpenOption.READ),
() -> AsynchronousFileChannel.open(filePath, StandardOpenOption.READ),
position, bufferFactory, bufferSize);
}
}
catch (IOException ignore) {
catch (IOException | UnsupportedOperationException ignore) {
// fallback to resource.readableChannel(), below
}
Flux<DataBuffer> result = readByteChannel(resource::readableChannel, bufferFactory, bufferSize);
return position == 0 ? result : skipUntilByteCount(result, position);
return (position == 0 ? result : skipUntilByteCount(result, position));
}
@@ -55,7 +55,6 @@ public final class JettyDataBuffer implements PooledDataBuffer {
this.bufferFactory = bufferFactory;
this.delegate = delegate;
this.chunk = chunk;
this.chunk.retain();
}
JettyDataBuffer(JettyDataBufferFactory bufferFactory, DefaultDataBuffer delegate) {
@@ -260,7 +260,8 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
private PathMatcher pathMatcher = new AntPathMatcher();
private boolean useCaches = true;
@Nullable
private Boolean useCaches;
private final Map<String, Resource[]> rootDirCache = new ConcurrentHashMap<>();
@@ -342,10 +343,12 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
* the {@link JarURLConnection} level as well as within this resolver instance.
* <p>Note that {@link JarURLConnection#setDefaultUseCaches} can be turned off
* independently. This resolver-level setting is designed to only enforce
* {@code JarURLConnection#setUseCaches(false)} if necessary but otherwise
* leaves the JVM-level default in place.
* {@code JarURLConnection#setUseCaches(true/false)} if necessary but otherwise
* leaves the JVM-level default in place (if this setter has not been called).
* <p>As of 6.2.10, this setting propagates to {@link UrlResource#setUseCaches}.
* @since 6.1.19
* @see JarURLConnection#setUseCaches
* @see UrlResource#setUseCaches
* @see #clearCache()
*/
public void setUseCaches(boolean useCaches) {
@@ -355,7 +358,11 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
@Override
public Resource getResource(String location) {
return getResourceLoader().getResource(location);
Resource resource = getResourceLoader().getResource(location);
if (this.useCaches != null && resource instanceof UrlResource urlResource) {
urlResource.setUseCaches(this.useCaches);
}
return resource;
}
@Override
@@ -473,20 +480,27 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
}
}
else {
UrlResource resource = null;
String urlString = url.toString();
String cleanedPath = StringUtils.cleanPath(urlString);
if (!cleanedPath.equals(urlString)) {
// Prefer cleaned URL, aligned with UrlResource#createRelative(String)
try {
// Retain original URL instance, potentially including custom URLStreamHandler.
return new UrlResource(new URL(url, cleanedPath));
resource = new UrlResource(new URL(url, cleanedPath));
}
catch (MalformedURLException ex) {
// Fallback to regular URL construction below...
}
}
// Retain original URL instance, potentially including custom URLStreamHandler.
return new UrlResource(url);
if (resource == null) {
resource = new UrlResource(url);
}
if (this.useCaches != null) {
resource.setUseCaches(this.useCaches);
}
return resource;
}
}
@@ -505,6 +519,9 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
UrlResource jarResource = (ResourceUtils.URL_PROTOCOL_JAR.equals(url.getProtocol()) ?
new UrlResource(url) :
new UrlResource(ResourceUtils.JAR_URL_PREFIX + url + ResourceUtils.JAR_URL_SEPARATOR));
if (this.useCaches != null) {
jarResource.setUseCaches(this.useCaches);
}
if (jarResource.exists()) {
result.add(jarResource);
}
@@ -556,7 +573,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
Set<ClassPathManifestEntry> entries = this.manifestEntriesCache;
if (entries == null) {
entries = getClassPathManifestEntries();
if (this.useCaches) {
if (this.useCaches == null || this.useCaches) {
this.manifestEntriesCache = entries;
}
}
@@ -577,7 +594,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
try {
File jar = new File(path).getAbsoluteFile();
if (jar.isFile() && seen.add(jar)) {
manifestEntries.add(ClassPathManifestEntry.of(jar));
manifestEntries.add(ClassPathManifestEntry.of(jar, this.useCaches));
manifestEntries.addAll(getClassPathManifestEntriesFromJar(jar));
}
}
@@ -616,7 +633,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
}
File candidate = new File(parent, path);
if (candidate.isFile() && candidate.getCanonicalPath().contains(parent.getCanonicalPath())) {
manifestEntries.add(ClassPathManifestEntry.of(candidate));
manifestEntries.add(ClassPathManifestEntry.of(candidate, this.useCaches));
}
}
}
@@ -710,7 +727,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
if (rootDirResources == null) {
// Lookup for specific directory, creating a cache entry for it.
rootDirResources = getResources(rootDirPath);
if (this.useCaches) {
if (this.useCaches == null || this.useCaches) {
this.rootDirCache.put(rootDirPath, rootDirResources);
}
}
@@ -729,7 +746,11 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
if (resolvedUrl != null) {
rootDirUrl = resolvedUrl;
}
rootDirResource = new UrlResource(rootDirUrl);
UrlResource urlResource = new UrlResource(rootDirUrl);
if (this.useCaches != null) {
urlResource.setUseCaches(this.useCaches);
}
rootDirResource = urlResource;
}
if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));
@@ -865,8 +886,8 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
if (con instanceof JarURLConnection jarCon) {
// Should usually be the case for traditional JAR files.
if (!this.useCaches) {
jarCon.setUseCaches(false);
if (this.useCaches != null) {
jarCon.setUseCaches(this.useCaches);
}
try {
jarFile = jarCon.getJarFile();
@@ -931,7 +952,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
}
}
}
if (this.useCaches) {
if (this.useCaches == null || this.useCaches) {
// Cache jar entries in TreeSet for efficient searching on re-encounter.
this.jarEntriesCache.put(jarFileUrl, entriesCache);
}
@@ -1257,10 +1278,10 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
private static final String JARFILE_URL_PREFIX = ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX;
static ClassPathManifestEntry of(File file) throws MalformedURLException {
static ClassPathManifestEntry of(File file, @Nullable Boolean useCaches) throws MalformedURLException {
String path = fixPath(file.getAbsolutePath());
Resource resource = asJarFileResource(path);
Resource alternative = createAlternative(path);
Resource resource = asJarFileResource(path, useCaches);
Resource alternative = createAlternative(path, useCaches);
return new ClassPathManifestEntry(resource, alternative);
}
@@ -1281,18 +1302,22 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
* @return the alternative form or {@code null}
*/
@Nullable
private static Resource createAlternative(String path) {
private static Resource createAlternative(String path, @Nullable Boolean useCaches) {
try {
String alternativePath = path.startsWith("/") ? path.substring(1) : "/" + path;
return asJarFileResource(alternativePath);
return asJarFileResource(alternativePath, useCaches);
}
catch (MalformedURLException ex) {
return null;
}
}
private static Resource asJarFileResource(String path) throws MalformedURLException {
return new UrlResource(JARFILE_URL_PREFIX + path + ResourceUtils.JAR_URL_SEPARATOR);
private static Resource asJarFileResource(String path, @Nullable Boolean useCaches) throws MalformedURLException {
UrlResource resource = new UrlResource(JARFILE_URL_PREFIX + path + ResourceUtils.JAR_URL_SEPARATOR);
if (useCaches != null) {
resource.setUseCaches(useCaches);
}
return resource;
}
}
@@ -92,6 +92,8 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
@Nullable
private Set<Thread> activeThreads;
private boolean cancelRemainingTasksOnClose = false;
private boolean rejectTasksWhenLimitReached = false;
private volatile boolean active = true;
@@ -184,12 +186,33 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
* @param timeout the timeout in milliseconds
* @since 6.1
* @see #close()
* @see #setCancelRemainingTasksOnClose
* @see org.springframework.scheduling.concurrent.ExecutorConfigurationSupport#setAwaitTerminationMillis
*/
public void setTaskTerminationTimeout(long timeout) {
Assert.isTrue(timeout >= 0, "Timeout value must be >=0");
this.taskTerminationTimeout = timeout;
this.activeThreads = (timeout > 0 ? ConcurrentHashMap.newKeySet() : null);
trackActiveThreadsIfNecessary();
}
/**
* Specify whether to cancel remaining tasks on close: that is, whether to
* interrupt any active threads at the time of the {@link #close()} call.
* <p>The default is {@code false}, not tracking active threads at all or
* just interrupting any remaining threads that still have not finished after
* the specified {@link #setTaskTerminationTimeout taskTerminationTimeout}.
* Switch this to {@code true} for immediate interruption on close, either in
* combination with a subsequent termination timeout or without any waiting
* at all, depending on whether a {@code taskTerminationTimeout} has been
* specified as well.
* @since 6.2.11
* @see #close()
* @see #setTaskTerminationTimeout
* @see org.springframework.scheduling.concurrent.ExecutorConfigurationSupport#setWaitForTasksToCompleteOnShutdown
*/
public void setCancelRemainingTasksOnClose(boolean cancelRemainingTasksOnClose) {
this.cancelRemainingTasksOnClose = cancelRemainingTasksOnClose;
trackActiveThreadsIfNecessary();
}
/**
@@ -249,6 +272,15 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
return this.active;
}
/**
* Track active threads only when a task termination timeout has been
* specified or interruption of remaining threads has been requested.
*/
private void trackActiveThreadsIfNecessary() {
this.activeThreads = (this.taskTerminationTimeout > 0 || this.cancelRemainingTasksOnClose ?
ConcurrentHashMap.newKeySet() : null);
}
/**
* Executes the given task, within a concurrency throttle
@@ -353,7 +385,7 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
}
/**
* This close methods tracks the termination of active threads if a concrete
* This close method tracks the termination of active threads if a concrete
* {@link #setTaskTerminationTimeout task termination timeout} has been set.
* Otherwise, it is not necessary to close this executor.
* @since 6.1
@@ -364,15 +396,24 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
this.active = false;
Set<Thread> threads = this.activeThreads;
if (threads != null) {
threads.forEach(Thread::interrupt);
synchronized (threads) {
try {
if (!threads.isEmpty()) {
threads.wait(this.taskTerminationTimeout);
if (this.cancelRemainingTasksOnClose) {
// Early interrupt for remaining tasks on close
threads.forEach(Thread::interrupt);
}
if (this.taskTerminationTimeout > 0) {
synchronized (threads) {
try {
if (!threads.isEmpty()) {
threads.wait(this.taskTerminationTimeout);
}
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
if (!this.cancelRemainingTasksOnClose) {
// Late interrupt for remaining tasks after timeout
threads.forEach(Thread::interrupt);
}
}
}
@@ -51,7 +51,8 @@ import java.lang.annotation.Target;
* <p>The additional return values denote the following:
* <ul>
* <li>{@code fail} - the method throws an exception, if the arguments satisfy argument constraints
* <li>{@code new} - the method returns a non-null new object which is distinct from any other object existing in the heap prior to method execution. If method is also pure, then we can be sure that the new object is not stored to any field/array and will be lost if method return value is not used.
* <li>{@code new} - the method returns a non-null new object which is distinct from any other object existing in the heap prior to method execution.
* If the method has no visible side effects, then we can be sure that the new object is not stored to any field/array and will be lost if the method's return value is not used.
* <li>{@code this} - the method returns its qualifier value (not applicable for static methods)
* <li>{@code param1, param2, ...} - the method returns its first (second, ...) parameter value
* </ul>
@@ -86,13 +86,12 @@ public abstract class FileSystemUtils {
Files.walkFileTree(root, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
public FileVisitResult visitFile(Path file, BasicFileAttributes attr) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
public FileVisitResult postVisitDirectory(Path dir, IOException ex) throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
@@ -127,19 +126,34 @@ public abstract class FileSystemUtils {
BasicFileAttributes srcAttr = Files.readAttributes(src, BasicFileAttributes.class);
if (srcAttr.isDirectory()) {
Files.walkFileTree(src, EnumSet.of(FOLLOW_LINKS), Integer.MAX_VALUE, new SimpleFileVisitor<>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
Files.createDirectories(dest.resolve(src.relativize(dir)));
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.copy(file, dest.resolve(src.relativize(file)), StandardCopyOption.REPLACE_EXISTING);
return FileVisitResult.CONTINUE;
}
});
if (src.getClass() == dest.getClass()) { // dest.resolve(Path) only works for same Path type
Files.walkFileTree(src, EnumSet.of(FOLLOW_LINKS), Integer.MAX_VALUE, new SimpleFileVisitor<>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attr) throws IOException {
Files.createDirectories(dest.resolve(src.relativize(dir)));
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attr) throws IOException {
Files.copy(file, dest.resolve(src.relativize(file)), StandardCopyOption.REPLACE_EXISTING);
return FileVisitResult.CONTINUE;
}
});
}
else { // use dest.resolve(String) for different Path types
Files.walkFileTree(src, EnumSet.of(FOLLOW_LINKS), Integer.MAX_VALUE, new SimpleFileVisitor<>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attr) throws IOException {
Files.createDirectories(dest.resolve(src.relativize(dir).toString()));
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attr) throws IOException {
Files.copy(file, dest.resolve(src.relativize(file).toString()), StandardCopyOption.REPLACE_EXISTING);
return FileVisitResult.CONTINUE;
}
});
}
}
else if (srcAttr.isRegularFile()) {
Files.copy(src, dest);
@@ -16,7 +16,6 @@
package org.springframework.util;
import java.io.ByteArrayOutputStream;
import java.nio.charset.Charset;
import java.util.ArrayDeque;
import java.util.ArrayList;
@@ -25,6 +24,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.Enumeration;
import java.util.HexFormat;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
@@ -803,54 +803,60 @@ public abstract class StringUtils {
}
/**
* Decode the given encoded URI component value. Based on the following rules:
* <ul>
* <li>Alphanumeric characters {@code "a"} through {@code "z"}, {@code "A"} through {@code "Z"},
* and {@code "0"} through {@code "9"} stay the same.</li>
* <li>Special characters {@code "-"}, {@code "_"}, {@code "."}, and {@code "*"} stay the same.</li>
* <li>A sequence "<i>{@code %xy}</i>" is interpreted as a hexadecimal representation of the character.</li>
* <li>For all other characters (including those already decoded), the output is undefined.</li>
* </ul>
* @param source the encoded String
* @param charset the character set
* Decode the given encoded URI component value by replacing "<i>{@code %xy}</i>" sequences
* by an hexadecimal representation of the character in the specified charset, letting other
* characters unchanged.
* @param source the encoded {@code String}
* @param charset the character encoding to use to decode the "<i>{@code %xy}</i>" sequences
* @return the decoded value
* @throws IllegalArgumentException when the given source contains invalid encoded sequences
* @since 5.0
* @see java.net.URLDecoder#decode(String, String)
* @see java.net.URLDecoder#decode(String, String) java.net.URLDecoder#decode for HTML form decoding
*/
public static String uriDecode(String source, Charset charset) {
int length = source.length();
if (length == 0) {
int firstPercentIndex = source.indexOf('%');
if (length == 0 || firstPercentIndex < 0) {
return source;
}
Assert.notNull(charset, "Charset must not be null");
ByteArrayOutputStream baos = new ByteArrayOutputStream(length);
boolean changed = false;
for (int i = 0; i < length; i++) {
int ch = source.charAt(i);
StringBuilder output = new StringBuilder(length);
output.append(source, 0, firstPercentIndex);
byte[] bytes = null;
int i = firstPercentIndex;
while (i < length) {
char ch = source.charAt(i);
if (ch == '%') {
if (i + 2 < length) {
char hex1 = source.charAt(i + 1);
char hex2 = source.charAt(i + 2);
int u = Character.digit(hex1, 16);
int l = Character.digit(hex2, 16);
if (u == -1 || l == -1) {
throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\"");
try {
if (bytes == null) {
bytes = new byte[(length - i) / 3];
}
baos.write((char) ((u << 4) + l));
i += 2;
changed = true;
int pos = 0;
while (i + 2 < length && ch == '%') {
bytes[pos++] = (byte) HexFormat.fromHexDigits(source, i + 1, i + 3);
i += 3;
if (i < length) {
ch = source.charAt(i);
}
}
if (i < length && ch == '%') {
throw new IllegalArgumentException("Incomplete trailing escape (%) pattern");
}
output.append(new String(bytes, 0, pos, charset));
}
else {
catch (NumberFormatException ex) {
throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\"");
}
}
else {
baos.write(ch);
output.append(ch);
i++;
}
}
return (changed ? StreamUtils.copyToString(baos, charset) : source);
return output.toString();
}
/**
@@ -137,14 +137,19 @@ public final class DataSize implements Comparable<DataSize>, Serializable {
}
/**
* Obtain a {@link DataSize} from a text string such as {@code 12MB} using
* Obtain a {@link DataSize} from a text string such as {@code "5MB"} using
* {@link DataUnit#BYTES} if no unit is specified.
* <p>Examples:
* <pre>
* "12KB" -- parses as "12 kilobytes"
* "5MB" -- parses as "5 megabytes"
* "20" -- parses as "20 bytes"
* </pre>
* <h4>Examples</h4>
* <table border="1">
* <tr><th>Text</th><th>Parsed As</th><th>Size in Bytes</th></tr>
* <tr><td>"20"</td><td>20 bytes</td><td>20</td></tr>
* <tr><td>"20B"</td><td>20 bytes</td><td>20</td></tr>
* <tr><td>"12KB"</td><td>12 kilobytes</td><td>12,288</td></tr>
* <tr><td>"5MB"</td><td>5 megabytes</td><td>5,242,880</td></tr>
* </table>
* <p>Note that the terms and units used in the above examples are based on
* <a href="https://en.wikipedia.org/wiki/Binary_prefix">binary prefixes</a>.
* Consult the {@linkplain DataSize class-level Javadoc} for details.
* @param text the text to parse
* @return the parsed {@code DataSize}
* @see #parse(CharSequence, DataUnit)
@@ -154,19 +159,24 @@ public final class DataSize implements Comparable<DataSize>, Serializable {
}
/**
* Obtain a {@link DataSize} from a text string such as {@code 12MB} using
* Obtain a {@link DataSize} from a text string such as {@code "5MB"} using
* the specified default {@link DataUnit} if no unit is specified.
* <p>The string starts with a number followed optionally by a unit matching
* one of the supported {@linkplain DataUnit suffixes}.
* <p>If neither a unit nor a default {@code DataUnit} is specified,
* {@link DataUnit#BYTES} will be inferred.
* <p>Examples:
* <pre>
* "12KB" -- parses as "12 kilobytes"
* "5MB" -- parses as "5 megabytes"
* "20" -- parses as "20 kilobytes" (where the {@code defaultUnit} is {@link DataUnit#KILOBYTES})
* "20" -- parses as "20 bytes" (if the {@code defaultUnit} is {@code null})
* </pre>
* <h4>Examples</h4>
* <table border="1">
* <tr><th>Text</th><th>Default Unit</th><th>Parsed As</th><th>Size in Bytes</th></tr>
* <tr><td>"20"</td><td>{@code null}</td><td>20 bytes</td><td>20</td></tr>
* <tr><td>"20"</td><td>{@link DataUnit#KILOBYTES KILOBYTES}</td><td>20 kilobytes</td><td>20,480</td></tr>
* <tr><td>"20B"</td><td>N/A</td><td>20 bytes</td><td>20</td></tr>
* <tr><td>"12KB"</td><td>N/A</td><td>12 kilobytes</td><td>12,288</td></tr>
* <tr><td>"5MB"</td><td>N/A</td><td>5 megabytes</td><td>5,242,880</td></tr>
* </table>
* <p>Note that the terms and units used in the above examples are based on
* <a href="https://en.wikipedia.org/wiki/Binary_prefix">binary prefixes</a>.
* Consult the {@linkplain DataSize class-level Javadoc} for details.
* @param text the text to parse
* @param defaultUnit the default {@code DataUnit} to use
* @return the parsed {@code DataSize}
@@ -0,0 +1,133 @@
/*
* 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.core.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import org.junit.jupiter.api.Test;
import org.springframework.core.MethodParameter;
import org.springframework.util.ReflectionUtils;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.joining;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link AnnotatedMethod}.
*
* @author Sam Brannen
* @since 6.2.11
*/
class AnnotatedMethodTests {
@Test
void shouldFindAnnotationOnMethodInGenericAbstractSuperclass() {
Method processTwo = getMethod("processTwo", String.class);
AnnotatedMethod annotatedMethod = new AnnotatedMethod(processTwo);
assertThat(annotatedMethod.hasMethodAnnotation(Handler.class)).isTrue();
}
@Test
void shouldFindAnnotationOnMethodInGenericInterface() {
Method processOneAndTwo = getMethod("processOneAndTwo", Long.class, Object.class);
AnnotatedMethod annotatedMethod = new AnnotatedMethod(processOneAndTwo);
assertThat(annotatedMethod.hasMethodAnnotation(Handler.class)).isTrue();
}
@Test
void shouldFindAnnotationOnMethodParameterInGenericAbstractSuperclass() {
// Prerequisites for gh-35349
Method abstractMethod = ReflectionUtils.findMethod(GenericAbstractSuperclass.class, "processTwo", Object.class);
assertThat(abstractMethod).isNotNull();
assertThat(Modifier.isAbstract(abstractMethod.getModifiers())).as("abstract").isTrue();
assertThat(Modifier.isPublic(abstractMethod.getModifiers())).as("public").isFalse();
Method processTwo = getMethod("processTwo", String.class);
AnnotatedMethod annotatedMethod = new AnnotatedMethod(processTwo);
MethodParameter[] methodParameters = annotatedMethod.getMethodParameters();
assertThat(methodParameters).hasSize(1);
assertThat(methodParameters[0].hasParameterAnnotation(Param.class)).isTrue();
}
@Test
void shouldFindAnnotationOnMethodParameterInGenericInterface() {
Method processOneAndTwo = getMethod("processOneAndTwo", Long.class, Object.class);
AnnotatedMethod annotatedMethod = new AnnotatedMethod(processOneAndTwo);
MethodParameter[] methodParameters = annotatedMethod.getMethodParameters();
assertThat(methodParameters).hasSize(2);
assertThat(methodParameters[0].hasParameterAnnotation(Param.class)).isFalse();
assertThat(methodParameters[1].hasParameterAnnotation(Param.class)).isTrue();
}
private static Method getMethod(String name, Class<?>...parameterTypes) {
Class<?> clazz = GenericInterfaceImpl.class;
Method method = ReflectionUtils.findMethod(clazz, name, parameterTypes);
if (method == null) {
String parameterNames = stream(parameterTypes).map(Class::getName).collect(joining(", "));
throw new IllegalStateException("Expected method not found: %s#%s(%s)"
.formatted(clazz.getSimpleName(), name, parameterNames));
}
return method;
}
@Retention(RetentionPolicy.RUNTIME)
@interface Handler {
}
@Retention(RetentionPolicy.RUNTIME)
@interface Param {
}
interface GenericInterface<A, B> {
@Handler
void processOneAndTwo(A value1, @Param B value2);
}
abstract static class GenericAbstractSuperclass<C> implements GenericInterface<Long, C> {
@Override
public void processOneAndTwo(Long value1, C value2) {
}
@Handler
// Intentionally NOT public
abstract void processTwo(@Param C value);
}
static class GenericInterfaceImpl extends GenericAbstractSuperclass<String> {
@Override
void processTwo(String value) {
}
}
}
@@ -945,6 +945,15 @@ class MergedAnnotationsTests {
Order.class).getDistance()).isEqualTo(0);
}
@Test
void getFromMethodWithUnresolvedGenericsInGenericTypeHierarchy() {
// The following method is GenericAbstractSuperclass.processOneAndTwo(java.lang.Long, C),
// where 'C' is an unresolved generic, for which ResolvableType.resolve() returns null.
Method method = ClassUtils.getMethod(GenericInterfaceImpl.class, "processOneAndTwo", Long.class, Object.class);
assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY)
.get(Transactional.class).isDirectlyPresent()).isTrue();
}
@Test
void getFromMethodWithInterfaceOnSuper() throws Exception {
Method method = SubOfImplementsInterfaceWithAnnotatedMethod.class.getMethod("foo");
@@ -3032,6 +3041,26 @@ class MergedAnnotationsTests {
}
}
interface GenericInterface<A, B> {
@Transactional
void processOneAndTwo(A value1, B value2);
}
abstract static class GenericAbstractSuperclass<C> implements GenericInterface<Long, C> {
@Override
public void processOneAndTwo(Long value1, C value2) {
}
}
static class GenericInterfaceImpl extends GenericAbstractSuperclass<String> {
// The compiler does not require us to declare a concrete
// processOneAndTwo(Long, String) method, and we intentionally
// do not declare one here.
}
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@interface MyRepeatableContainer {
@@ -18,33 +18,34 @@ package org.springframework.core.io.buffer;
import java.nio.ByteBuffer;
import org.eclipse.jetty.io.ArrayByteBufferPool;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.RetainableByteBuffer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
/**
* Tests for {@link JettyDataBuffer}
* @author Arjen Poutsma
* @author Brian Clozel
*/
public class JettyDataBufferTests {
private final JettyDataBufferFactory dataBufferFactory = new JettyDataBufferFactory();
private ArrayByteBufferPool.Tracking byteBufferPool = new ArrayByteBufferPool.Tracking();
@Test
void releaseRetainChunk() {
ByteBuffer buffer = ByteBuffer.allocate(3);
Content.Chunk mockChunk = mock();
given(mockChunk.getByteBuffer()).willReturn(buffer);
given(mockChunk.release()).willReturn(false, false, true);
RetainableByteBuffer retainableBuffer = byteBufferPool.acquire(3, false);
ByteBuffer buffer = retainableBuffer.getByteBuffer();
buffer.position(0).limit(1);
Content.Chunk chunk = Content.Chunk.asChunk(buffer, false, retainableBuffer);
JettyDataBuffer dataBuffer = this.dataBufferFactory.wrap(mockChunk);
JettyDataBuffer dataBuffer = this.dataBufferFactory.wrap(chunk);
dataBuffer.retain();
dataBuffer.retain();
assertThat(dataBuffer.release()).isFalse();
@@ -52,8 +53,12 @@ public class JettyDataBufferTests {
assertThat(dataBuffer.release()).isTrue();
assertThatIllegalStateException().isThrownBy(dataBuffer::release);
assertThat(retainableBuffer.isRetained()).isFalse();
assertThat(byteBufferPool.getLeaks()).isEmpty();
}
then(mockChunk).should(times(3)).retain();
then(mockChunk).should(times(3)).release();
@AfterEach
public void tearDown() throws Exception {
this.byteBufferPool.clear();
}
}
@@ -17,20 +17,29 @@
package org.springframework.util;
import java.io.File;
import java.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.Map;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link FileSystemUtils}.
*
* @author Rob Harrop
* @author Sam Brannen
* @author Juergen Hoeller
*/
class FileSystemUtilsTests {
@Test
void deleteRecursively() throws Exception {
File root = new File("./tmp/root");
void deleteRecursively(@TempDir File tempDir) throws Exception {
File root = new File(tempDir, "root");
File child = new File(root, "child");
File grandchild = new File(child, "grandchild");
@@ -53,8 +62,8 @@ class FileSystemUtilsTests {
}
@Test
void copyRecursively() throws Exception {
File src = new File("./tmp/src");
void copyRecursively(@TempDir File tempDir) throws Exception {
File src = new File(tempDir, "src");
File child = new File(src, "child");
File grandchild = new File(child, "grandchild");
@@ -68,27 +77,29 @@ class FileSystemUtilsTests {
assertThat(grandchild).exists();
assertThat(bar).exists();
File dest = new File("./dest");
File dest = new File(tempDir, "/dest");
FileSystemUtils.copyRecursively(src, dest);
assertThat(dest).exists();
assertThat(new File(dest, child.getName())).exists();
assertThat(new File(dest, "child")).exists();
assertThat(new File(dest, "child/bar.txt")).exists();
String destPath = dest.toString().replace('\\', '/');
if (!destPath.startsWith("/")) {
destPath = "/" + destPath;
}
URI uri = URI.create("jar:file:" + destPath + "/archive.zip");
Map<String, String> env = Map.of("create", "true");
FileSystem zipfs = FileSystems.newFileSystem(uri, env);
Path ziproot = zipfs.getPath("/");
FileSystemUtils.copyRecursively(src.toPath(), ziproot);
assertThat(zipfs.getPath("/child")).exists();
assertThat(zipfs.getPath("/child/bar.txt")).exists();
zipfs.close();
FileSystemUtils.deleteRecursively(src);
assertThat(src).doesNotExist();
}
@AfterEach
void tearDown() {
File tmp = new File("./tmp");
if (tmp.exists()) {
FileSystemUtils.deleteRecursively(tmp);
}
File dest = new File("./dest");
if (dest.exists()) {
FileSystemUtils.deleteRecursively(dest);
}
}
}
@@ -129,34 +129,72 @@ public class StandardEvaluationContext implements EvaluationContext {
}
/**
* Specify the default root context object (including a type descriptor)
* against which unqualified properties, methods, etc. should be resolved.
* @param rootObject the root object to use
* @param typeDescriptor a corresponding type descriptor
*/
public void setRootObject(@Nullable Object rootObject, TypeDescriptor typeDescriptor) {
this.rootObject = new TypedValue(rootObject, typeDescriptor);
}
/**
* Specify the default root context object against which unqualified
* properties, methods, etc. should be resolved.
* @param rootObject the root object to use
*/
public void setRootObject(@Nullable Object rootObject) {
this.rootObject = (rootObject != null ? new TypedValue(rootObject) : TypedValue.NULL);
}
/**
* Return the configured default root context object against which unqualified
* properties, methods, etc. should be resolved (can be {@link TypedValue#NULL}).
*/
@Override
public TypedValue getRootObject() {
return this.rootObject;
}
/**
* Set the list of property accessors to use in this evaluation context.
* <p>Replaces any previously configured property accessors.
*/
public void setPropertyAccessors(List<PropertyAccessor> propertyAccessors) {
this.propertyAccessors = propertyAccessors;
}
/**
* Get the list of property accessors configured in this evaluation context.
*/
@Override
public List<PropertyAccessor> getPropertyAccessors() {
return initPropertyAccessors();
}
public void addPropertyAccessor(PropertyAccessor accessor) {
addBeforeDefault(initPropertyAccessors(), accessor);
/**
* Add the supplied property accessor to this evaluation context.
* @param propertyAccessor the property accessor to add
* @see #getPropertyAccessors()
* @see #setPropertyAccessors(List)
* @see #removePropertyAccessor(PropertyAccessor)
*/
public void addPropertyAccessor(PropertyAccessor propertyAccessor) {
addBeforeDefault(initPropertyAccessors(), propertyAccessor);
}
public boolean removePropertyAccessor(PropertyAccessor accessor) {
return initPropertyAccessors().remove(accessor);
/**
* Remove the supplied property accessor from this evaluation context.
* @param propertyAccessor the property accessor to remove
* @return {@code true} if the property accessor was removed, {@code false}
* if the property accessor was not configured in this evaluation context
* @see #getPropertyAccessors()
* @see #setPropertyAccessors(List)
* @see #addPropertyAccessor(PropertyAccessor)
*/
public boolean removePropertyAccessor(PropertyAccessor propertyAccessor) {
return initPropertyAccessors().remove(propertyAccessor);
}
/**
@@ -198,8 +236,8 @@ public class StandardEvaluationContext implements EvaluationContext {
/**
* Remove the supplied index accessor from this evaluation context.
* @param indexAccessor the index accessor to remove
* @return {@code true} if the index accessor was removed, {@code false} if
* the index accessor was not configured in this evaluation context
* @return {@code true} if the index accessor was removed, {@code false}
* if the index accessor was not configured in this evaluation context
* @since 6.2
* @see #getIndexAccessors()
* @see #setIndexAccessors(List)
@@ -209,44 +247,96 @@ public class StandardEvaluationContext implements EvaluationContext {
return initIndexAccessors().remove(indexAccessor);
}
/**
* Set the list of constructor resolvers to use in this evaluation context.
* <p>Replaces any previously configured constructor resolvers.
*/
public void setConstructorResolvers(List<ConstructorResolver> constructorResolvers) {
this.constructorResolvers = constructorResolvers;
}
/**
* Get the list of constructor resolvers to use in this evaluation context.
*/
@Override
public List<ConstructorResolver> getConstructorResolvers() {
return initConstructorResolvers();
}
public void addConstructorResolver(ConstructorResolver resolver) {
addBeforeDefault(initConstructorResolvers(), resolver);
/**
* Add the supplied constructor resolver to this evaluation context.
* @param constructorResolver the constructor resolver to add
* @see #getConstructorResolvers()
* @see #setConstructorResolvers(List)
* @see #removeConstructorResolver(ConstructorResolver)
*/
public void addConstructorResolver(ConstructorResolver constructorResolver) {
addBeforeDefault(initConstructorResolvers(), constructorResolver);
}
public boolean removeConstructorResolver(ConstructorResolver resolver) {
return initConstructorResolvers().remove(resolver);
/**
* Remove the supplied constructor resolver from this evaluation context.
* @param constructorResolver the constructor resolver to remove
* @return {@code true} if the constructor resolver was removed, {@code false}
* if the constructor resolver was not configured in this evaluation context
* @see #getConstructorResolvers()
* @see #setConstructorResolvers(List)
* @see #addConstructorResolver(ConstructorResolver)
*/
public boolean removeConstructorResolver(ConstructorResolver constructorResolver) {
return initConstructorResolvers().remove(constructorResolver);
}
/**
* Set the list of method resolvers to use in this evaluation context.
* <p>Replaces any previously configured method resolvers.
*/
public void setMethodResolvers(List<MethodResolver> methodResolvers) {
this.methodResolvers = methodResolvers;
}
/**
* Get the list of method resolvers to use in this evaluation context.
*/
@Override
public List<MethodResolver> getMethodResolvers() {
return initMethodResolvers();
}
public void addMethodResolver(MethodResolver resolver) {
addBeforeDefault(initMethodResolvers(), resolver);
/**
* Add the supplied method resolver to this evaluation context.
* @param methodResolver the method resolver to add
* @see #getMethodResolvers()
* @see #setMethodResolvers(List)
* @see #removeMethodResolver(MethodResolver)
*/
public void addMethodResolver(MethodResolver methodResolver) {
addBeforeDefault(initMethodResolvers(), methodResolver);
}
/**
* Remove the supplied method resolver from this evaluation context.
* @param methodResolver the method resolver to remove
* @return {@code true} if the method resolver was removed, {@code false}
* if the method resolver was not configured in this evaluation context
* @see #getMethodResolvers()
* @see #setMethodResolvers(List)
* @see #addMethodResolver(MethodResolver)
*/
public boolean removeMethodResolver(MethodResolver methodResolver) {
return initMethodResolvers().remove(methodResolver);
}
public void setBeanResolver(BeanResolver beanResolver) {
/**
* Set the {@link BeanResolver} to use for looking up beans, if any.
*/
public void setBeanResolver(@Nullable BeanResolver beanResolver) {
this.beanResolver = beanResolver;
}
/**
* Get the configured {@link BeanResolver} for looking up beans, if any.
*/
@Override
@Nullable
public BeanResolver getBeanResolver() {
@@ -284,11 +374,17 @@ public class StandardEvaluationContext implements EvaluationContext {
return this.typeLocator;
}
/**
* Set the {@link TypeConverter} for value conversion.
*/
public void setTypeConverter(TypeConverter typeConverter) {
Assert.notNull(typeConverter, "TypeConverter must not be null");
this.typeConverter = typeConverter;
}
/**
* Get the configured {@link TypeConverter} for value conversion.
*/
@Override
public TypeConverter getTypeConverter() {
if (this.typeConverter == null) {
@@ -297,21 +393,33 @@ public class StandardEvaluationContext implements EvaluationContext {
return this.typeConverter;
}
/**
* Set the {@link TypeComparator} for comparing pairs of objects.
*/
public void setTypeComparator(TypeComparator typeComparator) {
Assert.notNull(typeComparator, "TypeComparator must not be null");
this.typeComparator = typeComparator;
}
/**
* Get the configured {@link TypeComparator} for comparing pairs of objects.
*/
@Override
public TypeComparator getTypeComparator() {
return this.typeComparator;
}
/**
* Set the {@link OperatorOverloader} for mathematical operations.
*/
public void setOperatorOverloader(OperatorOverloader operatorOverloader) {
Assert.notNull(operatorOverloader, "OperatorOverloader must not be null");
this.operatorOverloader = operatorOverloader;
}
/**
* Get the configured {@link OperatorOverloader} for mathematical operations.
*/
@Override
public OperatorOverloader getOperatorOverloader() {
return this.operatorOverloader;
@@ -21,7 +21,6 @@ import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
@@ -48,11 +47,6 @@ public class GenericTableMetaDataProvider implements TableMetaDataProvider {
/** Logger available to subclasses. */
protected static final Log logger = LogFactory.getLog(TableMetaDataProvider.class);
/** Database products we know not supporting the use of a String[] for generated keys. */
private static final List<String> productsNotSupportingGeneratedKeysColumnNameArray =
Arrays.asList("Apache Derby", "HSQL Database Engine");
/** The name of the user currently connected. */
@Nullable
private final String userName;
@@ -95,45 +89,14 @@ public class GenericTableMetaDataProvider implements TableMetaDataProvider {
@Override
public void initializeWithMetaData(DatabaseMetaData databaseMetaData) throws SQLException {
try {
if (databaseMetaData.supportsGetGeneratedKeys()) {
logger.debug("GetGeneratedKeys is supported");
setGetGeneratedKeysSupported(true);
}
else {
logger.debug("GetGeneratedKeys is not supported");
setGetGeneratedKeysSupported(false);
}
setGetGeneratedKeysSupported(databaseMetaData.supportsGetGeneratedKeys());
setGeneratedKeysColumnNameArraySupported(isGetGeneratedKeysSupported());
}
catch (SQLException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Error retrieving 'DatabaseMetaData.supportsGetGeneratedKeys': " + ex.getMessage());
}
}
try {
String databaseProductName = databaseMetaData.getDatabaseProductName();
if (productsNotSupportingGeneratedKeysColumnNameArray.contains(databaseProductName)) {
if (logger.isDebugEnabled()) {
logger.debug("GeneratedKeysColumnNameArray is not supported for " + databaseProductName);
}
setGeneratedKeysColumnNameArraySupported(false);
}
else {
if (isGetGeneratedKeysSupported()) {
if (logger.isDebugEnabled()) {
logger.debug("GeneratedKeysColumnNameArray is supported for " + databaseProductName);
}
setGeneratedKeysColumnNameArraySupported(true);
}
else {
setGeneratedKeysColumnNameArraySupported(false);
}
}
}
catch (SQLException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Error retrieving 'DatabaseMetaData.getDatabaseProductName': " + ex.getMessage());
}
}
try {
this.databaseVersion = databaseMetaData.getDatabaseProductVersion();
@@ -225,19 +188,23 @@ public class GenericTableMetaDataProvider implements TableMetaDataProvider {
}
}
/**
* This implementation delegates to {@link #catalogNameToUse}.
*/
@Override
@Nullable
public String metaDataCatalogNameToUse(@Nullable String catalogName) {
return catalogNameToUse(catalogName);
}
/**
* This implementation delegates to {@link #schemaNameToUse}.
* @see #getDefaultSchema()
*/
@Override
@Nullable
public String metaDataSchemaNameToUse(@Nullable String schemaName) {
if (schemaName == null) {
return schemaNameToUse(getDefaultSchema());
}
return schemaNameToUse(schemaName);
return schemaNameToUse(schemaName != null ? schemaName : getDefaultSchema());
}
/**
@@ -401,7 +368,7 @@ public class GenericTableMetaDataProvider implements TableMetaDataProvider {
try {
tableColumns = databaseMetaData.getColumns(
metaDataCatalogName, metaDataSchemaName, metaDataTableName, null);
while (tableColumns.next()) {
while (tableColumns != null && tableColumns.next()) {
String columnName = tableColumns.getString("COLUMN_NAME");
int dataType = tableColumns.getInt("DATA_TYPE");
if (dataType == Types.DECIMAL) {
@@ -19,12 +19,16 @@ package org.springframework.jdbc.core.metadata;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import org.springframework.lang.Nullable;
/**
* The PostgreSQL specific implementation of {@link TableMetaDataProvider}.
* Supports a feature for retrieving generated keys without the JDBC 3.0
* {@code getGeneratedKeys} support.
* {@code getGeneratedKeys} support. Also, it processes PostgreSQL-returned
* catalog and schema names from {@code DatabaseMetaData} in the given case.
*
* @author Thomas Risberg
* @author Juergen Hoeller
* @since 2.5
*/
public class PostgresTableMetaDataProvider extends GenericTableMetaDataProvider {
@@ -34,6 +38,18 @@ public class PostgresTableMetaDataProvider extends GenericTableMetaDataProvider
}
@Override
@Nullable
public String metaDataCatalogNameToUse(@Nullable String catalogName) {
return catalogName;
}
@Override
@Nullable
public String metaDataSchemaNameToUse(@Nullable String schemaName) {
return (schemaName != null ? schemaName : getDefaultSchema());
}
@Override
public boolean isGetGeneratedKeysSimulated() {
return true;
@@ -34,12 +34,18 @@ import org.springframework.lang.Nullable;
*
* <p>Designed for use with {@link org.springframework.jdbc.core.JdbcTemplate}
* as well as {@link org.springframework.jdbc.core.simple.JdbcClient}, to be
* passed in as a parameter value wrapping the target content value. Can be
* combined with {@link org.springframework.jdbc.core.SqlParameterValue} for
* specifying a SQL type, for example,
* passed in as a parameter value wrapping the target content value.
*
* <p>Can be combined with {@link org.springframework.jdbc.core.SqlParameterValue}
* for specifying a SQL type, for example,
* {@code new SqlParameterValue(Types.BLOB, new SqlBinaryValue(myContent))}.
* With most database drivers, the type hint is not actually necessary.
*
* <p>Note: Only specify {@code Types.BLOB} in case of an actual BLOB, preferring
* {@code Types.LONGVARBINARY} otherwise. With PostgreSQL, {@code Types.ARRAY}
* has to be specified for BYTEA columns, rather than {@code Types.BLOB}. This
* is in contrast to {@link SqlLobValue} where byte array handling was lenient.
*
* @author Juergen Hoeller
* @since 6.1.4
* @see SqlCharacterValue
@@ -33,12 +33,17 @@ import org.springframework.lang.Nullable;
*
* <p>Designed for use with {@link org.springframework.jdbc.core.JdbcTemplate}
* as well as {@link org.springframework.jdbc.core.simple.JdbcClient}, to be
* passed in as a parameter value wrapping the target content value. Can be
* combined with {@link org.springframework.jdbc.core.SqlParameterValue} for
* specifying a SQL type, for example,
* passed in as a parameter value wrapping the target content value.
*
* <p>Can be combined with {@link org.springframework.jdbc.core.SqlParameterValue}
* for specifying a SQL type, for example,
* {@code new SqlParameterValue(Types.CLOB, new SqlCharacterValue(myContent))}.
* With most database drivers, the type hint is not actually necessary.
*
* <p>Note: Only specify {@code Types.CLOB} in case of an actual CLOB, preferring
* {@code Types.LONGVARCHAR} otherwise. This is in contrast to {@link SqlLobValue}
* where char sequence handling was lenient.
*
* @author Juergen Hoeller
* @since 6.1.4
* @see SqlBinaryValue
@@ -258,18 +258,29 @@ public abstract class ScriptUtils {
for (String statement : statements) {
stmtNumber++;
try {
stmt.execute(statement);
int rowsAffected = stmt.getUpdateCount();
boolean hasResultSet = stmt.execute(statement);
int updateCount = -1;
if (logger.isDebugEnabled()) {
logger.debug(rowsAffected + " returned as update count for SQL: " + statement);
SQLWarning warningToLog = stmt.getWarnings();
while (warningToLog != null) {
logger.debug("SQLWarning ignored: SQL state '" + warningToLog.getSQLState() +
"', error code '" + warningToLog.getErrorCode() +
"', message [" + warningToLog.getMessage() + "]");
warningToLog = warningToLog.getNextWarning();
}
logSqlWarnings(stmt);
}
do {
if (hasResultSet) {
// We invoke getResultSet() to ensure the JDBC driver processes
// it, but we intentionally ignore the returned ResultSet since
// we cannot do anything meaningful with it here.
stmt.getResultSet();
if (logger.isDebugEnabled()) {
logger.debug("ResultSet returned for SQL: " + statement);
}
}
else {
updateCount = stmt.getUpdateCount();
if (updateCount >= 0 && logger.isDebugEnabled()) {
logger.debug(updateCount + " returned as update count for SQL: " + statement);
}
}
hasResultSet = stmt.getMoreResults();
} while (hasResultSet || updateCount != -1);
}
catch (SQLException ex) {
boolean dropStatement = StringUtils.startsWithIgnoreCase(statement.trim(), "drop");
@@ -307,6 +318,16 @@ public abstract class ScriptUtils {
}
}
private static void logSqlWarnings(Statement stmt) throws SQLException {
SQLWarning warningToLog = stmt.getWarnings();
while (warningToLog != null) {
logger.debug("SQLWarning ignored: SQL state '" + warningToLog.getSQLState() +
"', error code '" + warningToLog.getErrorCode() +
"', message [" + warningToLog.getMessage() + "]");
warningToLog = warningToLog.getNextWarning();
}
}
/**
* Read a script from the provided resource, using the supplied comment prefixes
* and statement separator, and build a {@code String} containing the lines.
@@ -94,7 +94,9 @@ public class SQLStateSQLExceptionTranslator extends AbstractFallbackSQLException
301, // SAP HANA
1062, // MySQL/MariaDB
2601, // MS SQL Server
2627 // MS SQL Server
2627, // MS SQL Server
-239, // Informix
-268 // Informix
);
@@ -74,7 +74,7 @@ class JdbcClientIntegrationTests {
@Test
void updateWithGeneratedKeys() {
int expectedId = 2;
int expectedId = 1;
String firstName = "Jane";
String lastName = "Smith";
@@ -92,7 +92,7 @@ class JdbcClientIntegrationTests {
@Test
void updateWithGeneratedKeysAndKeyColumnNames() {
int expectedId = 2;
int expectedId = 1;
String firstName = "Jane";
String lastName = "Smith";
@@ -110,7 +110,7 @@ class JdbcClientIntegrationTests {
@Test
void updateWithGeneratedKeysUsingNamedParameters() {
int expectedId = 2;
int expectedId = 1;
String firstName = "Jane";
String lastName = "Smith";
@@ -129,7 +129,7 @@ class JdbcClientIntegrationTests {
@Test
void updateWithGeneratedKeysAndKeyColumnNamesUsingNamedParameters() {
int expectedId = 2;
int expectedId = 1;
String firstName = "Jane";
String lastName = "Smith";
@@ -217,7 +217,7 @@ class JdbcClientIntegrationTests {
private static void assertResults(List<User> users) {
assertThat(users).containsExactly(new User(2, "John", "John"), new User(3, "John", "Smith"));
assertThat(users).containsExactly(new User(1, "John", "John"), new User(2, "John", "Smith"));
}
record Name(String name) {}
@@ -323,7 +323,7 @@ class SimpleJdbcInsertIntegrationTests {
protected void insertJaneSmith(SimpleJdbcInsert insert) {
Number id = insert.executeAndReturnKey(Map.of("first_name", "Jane", "last_name", "Smith"));
assertThat(id.intValue()).isEqualTo(2);
assertThat(id.intValue()).isEqualTo(1);
assertNumRows(2);
}
@@ -16,13 +16,24 @@
package org.springframework.jdbc.datasource.init;
import java.nio.charset.StandardCharsets;
import java.sql.SQLException;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.Parameter;
import org.junit.jupiter.params.ParameterizedClass;
import org.junit.jupiter.params.provider.EnumSource;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.jdbc.core.DataClassRowMapper;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assumptions.assumeThat;
import static org.springframework.jdbc.datasource.init.ScriptUtils.executeSqlScript;
/**
@@ -32,16 +43,22 @@ import static org.springframework.jdbc.datasource.init.ScriptUtils.executeSqlScr
* @since 4.0.3
* @see ScriptUtilsTests
*/
@ParameterizedClass
@EnumSource(EmbeddedDatabaseType.class)
class ScriptUtilsIntegrationTests extends AbstractDatabaseInitializationTests {
@Parameter
EmbeddedDatabaseType databaseType;
@Override
protected EmbeddedDatabaseType getEmbeddedDatabaseType() {
return EmbeddedDatabaseType.HSQL;
return this.databaseType;
}
@BeforeEach
void setUpSchema() throws SQLException {
executeSqlScript(db.getConnection(), usersSchema());
executeSqlScript(db.getConnection(), encodedResource(usersSchema()), false, true, "--", null, "/*", "*/");
}
@Test
@@ -59,4 +76,52 @@ class ScriptUtilsIntegrationTests extends AbstractDatabaseInitializationTests {
assertUsersDatabaseCreated("Hoeller", "Brannen");
}
@Test
@SuppressWarnings("unchecked")
void statementWithMultipleResultSets() throws SQLException {
// Derby does not support multiple statements/ResultSets within a single Statement.
assumeThat(this.databaseType).isNotSameAs(EmbeddedDatabaseType.DERBY);
EncodedResource resource = encodedResource(resource("users-data.sql"));
executeSqlScript(db.getConnection(), resource, false, true, "--", null, "/*", "*/");
assertUsersInDatabase(user("Sam", "Brannen"));
resource = encodedResource(inlineResource("""
SELECT last_name FROM users WHERE id = 0;
UPDATE users SET first_name = 'Jane' WHERE id = 0;
UPDATE users SET last_name = 'Smith' WHERE id = 0;
SELECT last_name FROM users WHERE id = 0;
GO
"""));
String separator = "GO\n";
executeSqlScript(db.getConnection(), resource, false, true, "--", separator, "/*", "*/");
assertUsersInDatabase(user("Jane", "Smith"));
}
private void assertUsersInDatabase(User... expectedUsers) {
List<User> users = jdbcTemplate.query("SELECT * FROM users WHERE id = 0",
new DataClassRowMapper<>(User.class));
assertThat(users).containsExactly(expectedUsers);
}
private static EncodedResource encodedResource(Resource resource) {
return new EncodedResource(resource);
}
private static Resource inlineResource(String sql) {
byte[] bytes = sql.getBytes(StandardCharsets.UTF_8);
return new ByteArrayResource(bytes, "inline SQL");
}
private static User user(String firstName, String lastName) {
return new User(0, firstName, lastName);
}
record User(int id, String firstName, String lastName) {
}
}
@@ -90,6 +90,16 @@ class SQLStateSQLExceptionTranslatorTests {
assertTranslation("23000", 301, DuplicateKeyException.class);
}
@Test
void translateDuplicateKeyInformix1() {
assertTranslation("23000", -239, DuplicateKeyException.class);
}
@Test
void translateDuplicateKeyInformix2() {
assertTranslation("23000", -268, DuplicateKeyException.class);
}
@Test
void translateDataAccessResourceFailure() {
assertTranslation("53", DataAccessResourceFailureException.class);
@@ -5,7 +5,7 @@ SET SCHEMA my_schema;
DROP TABLE users IF EXISTS;
CREATE TABLE users (
id INTEGER GENERATED BY DEFAULT AS IDENTITY,
id INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 0) PRIMARY KEY,
first_name VARCHAR(50) NOT NULL,
last_name VARCHAR(50) NOT NULL
);
@@ -1,7 +1,7 @@
DROP TABLE users IF EXISTS;
CREATE TABLE users (
id INTEGER GENERATED BY DEFAULT AS IDENTITY,
id INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 0) PRIMARY KEY,
first_name VARCHAR(50) NOT NULL,
last_name VARCHAR(50) NOT NULL
);
@@ -88,12 +88,12 @@ import org.springframework.util.Assert;
* such a scenario (see container setup).
*
* <p>This transaction manager supports nested transactions via JDBC Savepoints.
* The {@link #setNestedTransactionAllowed} "nestedTransactionAllowed"} flag defaults
* The {@link #setNestedTransactionAllowed "nestedTransactionAllowed"} flag defaults
* to "false", though, as nested transactions will just apply to the JDBC Connection,
* not to the Hibernate Session and its cached entity objects and related context.
* You can manually set the flag to "true" if you want to use nested transactions
* for JDBC access code which participates in Hibernate transactions (provided that
* your JDBC driver supports Savepoints). <i>Note that Hibernate itself does not
* your JDBC driver supports savepoints). <i>Note that Hibernate itself does not
* support nested transactions! Hence, do not expect Hibernate access code to
* semantically participate in a nested transaction.</i>
*
@@ -93,13 +93,14 @@ import org.springframework.util.CollectionUtils;
*
* <p>This transaction manager supports nested transactions via JDBC Savepoints.
* The {@link #setNestedTransactionAllowed "nestedTransactionAllowed"} flag defaults
* to {@code false} though, since nested transactions will just apply to the JDBC
* Connection, not to the JPA EntityManager and its cached entity objects and related
* context. You can manually set the flag to {@code true} if you want to use nested
* transactions for JDBC access code which participates in JPA transactions (provided
* that your JDBC driver supports Savepoints). <i>Note that JPA itself does not support
* nested transactions! Hence, do not expect JPA access code to semantically
* participate in a nested transaction.</i>
* to "true" but should rather be "false", as nested transactions will just apply to
* the JDBC Connection, not to the JPA EntityManager and its cached entity objects
* and related context. As of Spring Framework 7.0, the default will be "false" in
* alignment with other transaction managers, requiring an explicit switch to "true"
* if you want to use nested transactions for JDBC access code which participates
* in JPA transactions (provided that your JDBC driver supports savepoints).
* <i>Note that JPA itself does not support nested transactions! Hence, do not
* expect JPA access code to semantically participate in a nested transaction.</i>
*
* @author Juergen Hoeller
* @since 2.0
@@ -76,7 +76,9 @@ public abstract class ConnectionFactoryUtils {
301, // SAP HANA
1062, // MySQL/MariaDB
2601, // MS SQL Server
2627 // MS SQL Server
2627, // MS SQL Server
-239, // Informix
-268 // Informix
);
@@ -16,6 +16,8 @@
package org.springframework.r2dbc.connection;
import java.util.List;
import io.r2dbc.spi.R2dbcBadGrammarException;
import io.r2dbc.spi.R2dbcDataIntegrityViolationException;
import io.r2dbc.spi.R2dbcException;
@@ -25,6 +27,9 @@ import io.r2dbc.spi.R2dbcRollbackException;
import io.r2dbc.spi.R2dbcTimeoutException;
import io.r2dbc.spi.R2dbcTransientResourceException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.FieldSource;
import org.springframework.dao.CannotAcquireLockException;
import org.springframework.dao.DataAccessResourceFailureException;
@@ -38,6 +43,7 @@ import org.springframework.r2dbc.BadSqlGrammarException;
import org.springframework.r2dbc.UncategorizedR2dbcException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.params.provider.Arguments.arguments;
/**
* Tests for {@link ConnectionFactoryUtils}.
@@ -91,30 +97,25 @@ class ConnectionFactoryUtilsTests {
Exception exception = ConnectionFactoryUtils.convertR2dbcException("", "",
new R2dbcDataIntegrityViolationException());
assertThat(exception).isExactlyInstanceOf(DataIntegrityViolationException.class);
}
exception = ConnectionFactoryUtils.convertR2dbcException("", "",
new R2dbcDataIntegrityViolationException("reason", "23505"));
assertThat(exception).isExactlyInstanceOf(DuplicateKeyException.class);
static final List<Arguments> duplicateKeyErrorCodes = List.of(
arguments("Oracle", "23505", 0),
arguments("Oracle", "23000", 1),
arguments("SAP HANA", "23000", 301),
arguments("MySQL/MariaDB", "23000", 1062),
arguments("MS SQL Server", "23000", 2601),
arguments("MS SQL Server", "23000", 2627),
arguments("Informix", "23000", -239),
arguments("Informix", "23000", -268)
);
exception = ConnectionFactoryUtils.convertR2dbcException("", "",
new R2dbcDataIntegrityViolationException("reason", "23000", 1));
assertThat(exception).as("Oracle").isExactlyInstanceOf(DuplicateKeyException.class);
exception = ConnectionFactoryUtils.convertR2dbcException("", "",
new R2dbcDataIntegrityViolationException("reason", "23000", 301));
assertThat(exception).as("SAP HANA").isExactlyInstanceOf(DuplicateKeyException.class);
exception = ConnectionFactoryUtils.convertR2dbcException("", "",
new R2dbcDataIntegrityViolationException("reason", "23000", 1062));
assertThat(exception).as("MySQL/MariaDB").isExactlyInstanceOf(DuplicateKeyException.class);
exception = ConnectionFactoryUtils.convertR2dbcException("", "",
new R2dbcDataIntegrityViolationException("reason", "23000", 2601));
assertThat(exception).as("MS SQL Server").isExactlyInstanceOf(DuplicateKeyException.class);
exception = ConnectionFactoryUtils.convertR2dbcException("", "",
new R2dbcDataIntegrityViolationException("reason", "23000", 2627));
assertThat(exception).as("MS SQL Server").isExactlyInstanceOf(DuplicateKeyException.class);
@ParameterizedTest
@FieldSource("duplicateKeyErrorCodes")
void shouldTranslateIntegrityViolationExceptionToDuplicateKeyException(String db, String sqlState, int errorCode) {
Exception exception = ConnectionFactoryUtils.convertR2dbcException("", "",
new R2dbcDataIntegrityViolationException("reason", sqlState, errorCode));
assertThat(exception).as(db).isExactlyInstanceOf(DuplicateKeyException.class);
}
@Test
@@ -135,24 +136,27 @@ class ConnectionFactoryUtilsTests {
void messageGeneration() {
Exception exception = ConnectionFactoryUtils.convertR2dbcException("TASK",
"SOME-SQL", new R2dbcTransientResourceException("MESSAGE"));
assertThat(exception).isExactlyInstanceOf(
TransientDataAccessResourceException.class).hasMessage("TASK; SQL [SOME-SQL]; MESSAGE");
assertThat(exception)
.isExactlyInstanceOf(TransientDataAccessResourceException.class)
.hasMessage("TASK; SQL [SOME-SQL]; MESSAGE");
}
@Test
void messageGenerationNullSQL() {
Exception exception = ConnectionFactoryUtils.convertR2dbcException("TASK", null,
new R2dbcTransientResourceException("MESSAGE"));
assertThat(exception).isExactlyInstanceOf(
TransientDataAccessResourceException.class).hasMessage("TASK; MESSAGE");
assertThat(exception)
.isExactlyInstanceOf(TransientDataAccessResourceException.class)
.hasMessage("TASK; MESSAGE");
}
@Test
void messageGenerationNullMessage() {
Exception exception = ConnectionFactoryUtils.convertR2dbcException("TASK",
"SOME-SQL", new R2dbcTransientResourceException());
assertThat(exception).isExactlyInstanceOf(
TransientDataAccessResourceException.class).hasMessage("TASK; SQL [SOME-SQL]; null");
assertThat(exception)
.isExactlyInstanceOf(TransientDataAccessResourceException.class)
.hasMessage("TASK; SQL [SOME-SQL]; null");
}
@@ -67,6 +67,15 @@ import org.springframework.test.context.bean.override.BeanOverride;
* {@link org.springframework.beans.factory.config.ConfigurableListableBeanFactory#registerResolvableDependency(Class, Object)
* registered directly} as resolvable dependencies.
*
* <p><strong>NOTE</strong>: As stated in the documentation for Mockito, there are
* times when using {@code Mockito.when()} is inappropriate for stubbing a spy
* &mdash; for example, if calling a real method on a spy results in undesired
* side effects. To avoid such undesired side effects, consider using
* {@link org.mockito.Mockito#doReturn(Object) Mockito.doReturn(...).when(spy)...},
* {@link org.mockito.Mockito#doThrow(Class) Mockito.doThrow(...).when(spy)...},
* {@link org.mockito.Mockito#doNothing() Mockito.doNothing().when(spy)...}, and
* similar methods.
*
* <p><strong>WARNING</strong>: Using {@code @MockitoSpyBean} in conjunction with
* {@code @ContextHierarchy} can lead to undesirable results since each
* {@code @MockitoSpyBean} will be applied to all context hierarchy levels by default.
@@ -33,14 +33,21 @@ import org.springframework.context.ApplicationEvent;
* to be manually registered if you have custom configuration via
* {@link org.springframework.test.context.TestExecutionListeners @TestExecutionListeners}
* that does not include the default listeners.</li>
* <li>Annotate a field of type {@code ApplicationEvents} with
* <li>With JUnit Jupiter, declare a parameter of type {@code ApplicationEvents}
* in a {@code @Test}, {@code @BeforeEach}, or {@code @AfterEach} method. Since
* {@code ApplicationEvents} is scoped to the lifecycle of the current test method,
* this is the recommended approach.</li>
* <li>Alternatively, you can annotate a field of type {@code ApplicationEvents} with
* {@link org.springframework.beans.factory.annotation.Autowired @Autowired} and
* use that instance of {@code ApplicationEvents} in your test and lifecycle methods.</li>
* <li>With JUnit Jupiter, you may optionally declare a parameter of type
* {@code ApplicationEvents} in a test or lifecycle method as an alternative to
* an {@code @Autowired} field in the test class.</li>
* </ul>
*
* <p>NOTE: {@code ApplicationEvents} is registered with the {@code ApplicationContext} as a
* {@linkplain org.springframework.beans.factory.config.ConfigurableListableBeanFactory#registerResolvableDependency
* resolvable dependency} which is scoped to the lifecycle of the current test method.
* Consequently, {@code ApplicationEvents} cannot be accessed outside the lifecycle of a
* test method and cannot be {@code @Autowired} into the constructor of a test class.
*
* @author Sam Brannen
* @author Oliver Drotbohm
* @since 5.3.3
@@ -56,7 +56,7 @@ public class JsonPathAssertions {
/**
* Applies {@link JsonPathExpectationsHelper#assertValue(String, Object)}.
*/
public WebTestClient.BodyContentSpec isEqualTo(Object expectedValue) {
public WebTestClient.BodyContentSpec isEqualTo(@Nullable Object expectedValue) {
this.pathHelper.assertValue(this.content, expectedValue);
return this.bodySpec;
}
@@ -357,17 +357,17 @@ class StubWebApplicationContext implements WebApplicationContext {
@Override
@Nullable
public String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) {
public String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, @Nullable Locale locale) {
return this.messageSource.getMessage(code, args, defaultMessage, locale);
}
@Override
public String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException {
public String getMessage(String code, @Nullable Object[] args, @Nullable Locale locale) throws NoSuchMessageException {
return this.messageSource.getMessage(code, args, locale);
}
@Override
public String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException {
public String getMessage(MessageSourceResolvable resolvable, @Nullable Locale locale) throws NoSuchMessageException {
return this.messageSource.getMessage(resolvable, locale);
}
@@ -57,7 +57,8 @@ class JsonPathExpectationsHelperTests {
'whitespace': ' ',
'emptyString': '',
'emptyArray': [],
'emptyMap': {}
'emptyMap': {},
'nullValue': null
}""";
private static final String SIMPSONS = """
@@ -249,6 +250,11 @@ class JsonPathExpectationsHelperTests {
new JsonPathExpectationsHelper("$.num").assertValue(CONTENT, 5);
}
@Test
void assertNullValue() {
new JsonPathExpectationsHelper("$.nullValue").assertValue(CONTENT, (Object) null);
}
@Test // SPR-14498
void assertValueWithNumberConversion() {
new JsonPathExpectationsHelper("$.num").assertValue(CONTENT, 5.0);
@@ -20,6 +20,8 @@ import java.lang.reflect.Method;
import org.springframework.context.ApplicationListener;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalApplicationListenerMethodAdapter;
import org.springframework.transaction.event.TransactionalEventListenerFactory;
/**
@@ -37,20 +39,22 @@ public class RestrictedTransactionalEventListenerFactory extends TransactionalEv
@Override
public ApplicationListener<?> createApplicationListener(String beanName, Class<?> type, Method method) {
Transactional txAnn = AnnotatedElementUtils.findMergedAnnotation(method, Transactional.class);
if (txAnn == null) {
txAnn = AnnotatedElementUtils.findMergedAnnotation(type, Transactional.class);
}
if (txAnn != null) {
Propagation propagation = txAnn.propagation();
if (propagation != Propagation.REQUIRES_NEW && propagation != Propagation.NOT_SUPPORTED) {
throw new IllegalStateException("@TransactionalEventListener method must not be annotated with " +
"@Transactional unless when declared as REQUIRES_NEW or NOT_SUPPORTED: " + method);
TransactionalApplicationListenerMethodAdapter adapter =
new TransactionalApplicationListenerMethodAdapter(beanName, type, method);
if (adapter.getTransactionPhase() != TransactionPhase.BEFORE_COMMIT) {
Transactional txAnn = AnnotatedElementUtils.findMergedAnnotation(method, Transactional.class);
if (txAnn == null) {
txAnn = AnnotatedElementUtils.findMergedAnnotation(type, Transactional.class);
}
if (txAnn != null) {
Propagation propagation = txAnn.propagation();
if (propagation != Propagation.REQUIRES_NEW && propagation != Propagation.NOT_SUPPORTED) {
throw new IllegalStateException("@TransactionalEventListener method must not be annotated with " +
"@Transactional unless when declared as REQUIRES_NEW or NOT_SUPPORTED: " + method);
}
}
}
return super.createApplicationListener(beanName, type, method);
return adapter;
}
}
@@ -157,6 +157,13 @@ class TransactionalApplicationListenerMethodAdapterTests {
assertThatNoException().isThrownBy(() -> factory.createApplicationListener("test", SampleEvents.class, m));
}
@Test
void withTransactionalAnnotationBeforeCommit() {
RestrictedTransactionalEventListenerFactory factory = new RestrictedTransactionalEventListenerFactory();
Method m = ReflectionUtils.findMethod(SampleEvents.class, "withTransactionalAnnotationBeforeCommit", String.class);
assertThatNoException().isThrownBy(() -> factory.createApplicationListener("test", SampleEvents.class, m));
}
@Test
void withTransactionalAnnotationOnEnclosingClass() {
RestrictedTransactionalEventListenerFactory factory = new RestrictedTransactionalEventListenerFactory();
@@ -277,6 +284,11 @@ class TransactionalApplicationListenerMethodAdapterTests {
public void withAsyncTransactionalAnnotation(String data) {
}
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
@Transactional
public void withTransactionalAnnotationBeforeCommit(String data) {
}
@Transactional
static class SampleEventsWithTransactionalAnnotation {
@@ -110,7 +110,6 @@ public class ProblemDetail implements Serializable {
* @param type the problem type
*/
public void setType(URI type) {
Assert.notNull(type, "'type' is required");
this.type = type;
}
@@ -251,7 +250,7 @@ public class ProblemDetail implements Serializable {
@Override
public boolean equals(@Nullable Object other) {
return (this == other || (other instanceof ProblemDetail that &&
getType().equals(that.getType()) &&
ObjectUtils.nullSafeEquals(getType(), that.getType()) &&
ObjectUtils.nullSafeEquals(getTitle(), that.getTitle()) &&
this.status == that.status &&
ObjectUtils.nullSafeEquals(this.detail, that.detail) &&
@@ -211,7 +211,7 @@ public final class ResponseCookie extends HttpCookie {
* @return a builder to create the cookie with
* @since 6.0
*/
public static ResponseCookieBuilder from(final String name) {
public static ResponseCookieBuilder from(String name) {
return new DefaultResponseCookieBuilder(name, null, false);
}
@@ -222,7 +222,7 @@ public final class ResponseCookie extends HttpCookie {
* @param value the cookie value
* @return a builder to create the cookie with
*/
public static ResponseCookieBuilder from(final String name, final String value) {
public static ResponseCookieBuilder from(String name, @Nullable String value) {
return new DefaultResponseCookieBuilder(name, value, false);
}
@@ -236,7 +236,7 @@ public final class ResponseCookie extends HttpCookie {
* @return a builder to create the cookie with
* @since 5.2.5
*/
public static ResponseCookieBuilder fromClientResponse(final String name, final String value) {
public static ResponseCookieBuilder fromClientResponse(String name, @Nullable String value) {
return new DefaultResponseCookieBuilder(name, value, true);
}
@@ -425,7 +425,7 @@ public final class ResponseCookie extends HttpCookie {
@Nullable
private String sameSite;
public DefaultResponseCookieBuilder(String name, @Nullable String value, boolean lenient) {
DefaultResponseCookieBuilder(String name, @Nullable String value, boolean lenient) {
this.name = name;
this.value = value;
this.lenient = lenient;
@@ -37,6 +37,7 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Flow;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
@@ -97,12 +98,13 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
@SuppressWarnings("NullAway")
protected ClientHttpResponse executeInternal(HttpHeaders headers, @Nullable Body body) throws IOException {
CompletableFuture<HttpResponse<InputStream>> responseFuture = null;
TimeoutHandler timeoutHandler = null;
try {
HttpRequest request = buildRequest(headers, body);
responseFuture = this.httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream());
if (this.timeout != null) {
TimeoutHandler timeoutHandler = new TimeoutHandler(responseFuture, this.timeout);
timeoutHandler = new TimeoutHandler(responseFuture, this.timeout);
HttpResponse<InputStream> response = responseFuture.get();
InputStream inputStream = timeoutHandler.wrapInputStream(response);
return new JdkClientHttpResponse(response, inputStream);
@@ -120,8 +122,11 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
catch (ExecutionException ex) {
Throwable cause = ex.getCause();
if (cause instanceof CancellationException) {
throw new HttpTimeoutException("Request timed out");
if (cause instanceof CancellationException ce) {
if (timeoutHandler != null) {
timeoutHandler.handleCancellationException(ce);
}
throw new IOException("Request cancelled", cause);
}
if (cause instanceof UncheckedIOException uioEx) {
throw uioEx.getCause();
@@ -136,6 +141,12 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
throw new IOException(cause.getMessage(), cause);
}
}
catch (CancellationException ex) {
if (timeoutHandler != null) {
timeoutHandler.handleCancellationException(ex);
}
throw new IOException("Request cancelled", ex);
}
}
private HttpRequest buildRequest(HttpHeaders headers, @Nullable Body body) {
@@ -234,12 +245,15 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
private final CompletableFuture<Void> timeoutFuture;
private final AtomicBoolean timeout = new AtomicBoolean(false);
private TimeoutHandler(CompletableFuture<HttpResponse<InputStream>> future, Duration timeout) {
this.timeoutFuture = new CompletableFuture<Void>()
.completeOnTimeout(null, timeout.toMillis(), TimeUnit.MILLISECONDS);
this.timeoutFuture.thenRun(() -> {
this.timeout.set(true);
if (future.cancel(true) || future.isCompletedExceptionally() || !future.isDone()) {
return;
}
@@ -250,7 +264,6 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
// ignore
}
});
}
@Nullable
@@ -268,6 +281,12 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
}
};
}
public void handleCancellationException(CancellationException ex) throws HttpTimeoutException {
if (this.timeout.get()) {
throw new HttpTimeoutException(ex.getMessage());
}
}
}
}
@@ -203,8 +203,8 @@ public class ResourceHttpMessageWriter implements HttpMessageWriter<Resource> {
}
return zeroCopyHttpOutputMessage.writeWith(file, pos, count);
}
catch (IOException ex) {
// should not happen
catch (IOException | UnsupportedOperationException ignore) {
// returning null below leads to fallback code path
}
}
return null;
@@ -115,7 +115,10 @@ class JettyCoreServerHttpRequest extends AbstractServerHttpRequest {
// We access the request body as a Flow.Publisher, which is wrapped as an org.reactivestreams.Publisher and
// then wrapped as a Flux.
return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(this.request)))
.map(this.dataBufferFactory::wrap);
.map(chunk -> {
chunk.retain();
return this.dataBufferFactory.wrap(chunk);
});
}
}
@@ -105,16 +105,20 @@ public @interface RequestMapping {
/**
* The path mapping URIs &mdash; for example, {@code "/profile"}.
* <p>Ant-style path patterns are also supported (for example, {@code "/profile/**"}).
* At the method level, relative paths (for example, {@code "edit"}) are supported
* <p>Ant-style path patterns are also supported, e.g. {@code "/profile/**"}.
* At the method level, relative paths, e.g., {@code "edit"} are supported
* within the primary mapping expressed at the type level.
* Path mapping URIs may contain placeholders (for example, <code>"/${profile_path}"</code>).
* Path mapping URIs may contain property placeholders, e.g. <code>"/${profile_path}"</code>,
* and SpEL expressions, e.g. {@code "/profile/#{@bean.property}"}.
* <p><b>Supported at the type level as well as at the method level!</b>
* When used at the type level, all method-level mappings inherit
* this primary mapping, narrowing it for a specific handler method.
* <p><strong>NOTE</strong>: A handler method that is not mapped to any path
* explicitly is effectively mapped to an empty path.
* @since 4.2
* @see org.springframework.beans.factory.config.EmbeddedValueResolver
* @see org.springframework.context.expression.StandardBeanExpressionResolver
* @see org.springframework.context.support.AbstractApplicationContext
*/
@AliasFor("value")
String[] path() default {};
@@ -116,7 +116,7 @@ public abstract class BindErrorUtils {
@Override
@Nullable
protected String getDefaultMessage(MessageSourceResolvable resolvable, Locale locale) {
protected String getDefaultMessage(MessageSourceResolvable resolvable, @Nullable Locale locale) {
String message = super.getDefaultMessage(resolvable, locale);
return (resolvable instanceof FieldError error ? error.getField() + ": " + message : message);
}
@@ -30,7 +30,6 @@ import org.springframework.util.Assert;
*
* @author Rossen Stoyanchev
* @since 6.2
*
* @see <a href="https://www.rfc-editor.org/info/rfc3986">RFC 3986</a>
*/
abstract class RfcUriParser {
@@ -78,10 +77,10 @@ abstract class RfcUriParser {
* @param query the query, if present
* @param fragment the fragment, if present
*/
record UriRecord(@Nullable String scheme, boolean isOpaque,
@Nullable String user, @Nullable String host, @Nullable String port,
@Nullable String path, @Nullable String query, @Nullable String fragment) {
record UriRecord(
@Nullable String scheme, boolean isOpaque,
@Nullable String user, @Nullable String host, @Nullable String port,
@Nullable String path, @Nullable String query, @Nullable String fragment) {
}
@@ -130,6 +129,7 @@ abstract class RfcUriParser {
}
},
HOST_OR_PATH {
@Override
@@ -158,6 +158,7 @@ abstract class RfcUriParser {
}
},
SCHEME_OR_PATH {
@Override
@@ -188,6 +189,7 @@ abstract class RfcUriParser {
}
},
HOST {
@Override
@@ -229,6 +231,7 @@ abstract class RfcUriParser {
}
},
IPV6 {
@Override
@@ -259,6 +262,7 @@ abstract class RfcUriParser {
}
},
PORT {
@Override
@@ -291,6 +295,7 @@ abstract class RfcUriParser {
}
},
PATH {
@Override
@@ -319,6 +324,7 @@ abstract class RfcUriParser {
}
},
QUERY {
@Override
@@ -334,7 +340,9 @@ abstract class RfcUriParser {
}
},
FRAGMENT {
@Override
public void handleNext(InternalParser parser, char c, int i) {
}
@@ -345,6 +353,7 @@ abstract class RfcUriParser {
}
},
WILDCARD {
@Override
@@ -358,6 +367,7 @@ abstract class RfcUriParser {
}
};
/**
* Method to handle each character from the input string.
* @param parser provides access to parsing state, and helper methods
@@ -429,6 +439,7 @@ abstract class RfcUriParser {
this.uri = uri;
}
// Check internal state
public boolean hasScheme() {
@@ -451,6 +462,7 @@ abstract class RfcUriParser {
return (this.index == this.componentIndex);
}
// Top-level parse loop, iterate over chars and delegate to states
public UriRecord parse() {
@@ -475,6 +487,7 @@ abstract class RfcUriParser {
return this.uri.charAt(this.index);
}
// Transitions and index updates
public void advanceTo(State state) {
@@ -500,6 +513,7 @@ abstract class RfcUriParser {
this.index = index;
}
// Component capture
public InternalParser resolveIfOpaque() {
@@ -593,6 +607,7 @@ abstract class RfcUriParser {
return this;
}
// Encoding and curly bracket handling
/**
@@ -643,6 +658,7 @@ abstract class RfcUriParser {
return (this.openCurlyBracketCount > 0);
}
@Override
public String toString() {
return "[State=" + this.state + ", index=" + this.index + ", componentIndex=" + this.componentIndex +
@@ -373,15 +373,16 @@ public abstract class UriUtils {
}
/**
* Decode the given encoded URI component.
* <p>See {@link StringUtils#uriDecode(String, Charset)} for the decoding rules.
* @param source the encoded String
* @param charset the character encoding to use
* Decode the given encoded URI component value by replacing "<i>{@code %xy}</i>" sequences
* by an hexadecimal representation of the character in the specified charset, letting other
* characters unchanged.
* @param source the encoded {@code String}
* @param charset the character encoding to use to decode the "<i>{@code %xy}</i>" sequences
* @return the decoded value
* @throws IllegalArgumentException when the given source contains invalid encoded sequences
* @since 5.0
* @see StringUtils#uriDecode(String, Charset)
* @see java.net.URLDecoder#decode(String, String)
* @see java.net.URLDecoder#decode(String, String) java.net.URLDecoder#decode for HTML form decoding
*/
public static String decode(String source, Charset charset) {
return StringUtils.uriDecode(source, charset);
@@ -0,0 +1,78 @@
/*
* 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.http.client;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpTimeoutException;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.junit.jupiter.api.AutoClose;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Unit tests for {@link JdkClientHttpRequest}.
*/
class JdkClientHttpRequestTests {
private final HttpClient client = mock(HttpClient.class);
@AutoClose("shutdownNow")
private final ExecutorService executor = Executors.newSingleThreadExecutor();
@Test
@SuppressWarnings("unchecked")
void futureCancelledAfterTimeout() {
CompletableFuture<HttpResponse<InputStream>> future = new CompletableFuture<>();
when(client.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))).thenReturn(future);
assertThatThrownBy(() -> createRequest(Duration.ofMillis(10)).executeInternal(new HttpHeaders(), null))
.isExactlyInstanceOf(HttpTimeoutException.class);
}
@Test
@SuppressWarnings("unchecked")
void futureCancelled() {
CompletableFuture<HttpResponse<InputStream>> future = new CompletableFuture<>();
future.cancel(true);
when(client.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))).thenReturn(future);
assertThatThrownBy(() -> createRequest(null).executeInternal(new HttpHeaders(), null))
.isExactlyInstanceOf(IOException.class);
}
private JdkClientHttpRequest createRequest(Duration timeout) {
return new JdkClientHttpRequest(client, URI.create("https://abc.com"), HttpMethod.GET, executor, timeout);
}
}
@@ -35,32 +35,46 @@ import static org.assertj.core.api.Assertions.assertThat;
* Tests for {@link HandlerMethod}.
*
* @author Rossen Stoyanchev
* @author Sam Brannen
*/
class HandlerMethodTests {
@Test
void shouldValidateArgsWithConstraintsDirectlyOnClass() {
void shouldValidateArgsWithConstraintsDirectlyInClass() {
Object target = new MyClass();
testValidateArgs(target, List.of("addIntValue", "addPersonAndIntValue", "addPersons", "addPeople", "addNames"), true);
testValidateArgs(target, List.of("addPerson", "getPerson", "getIntValue", "addPersonNotValidated"), false);
}
@Test
void shouldValidateArgsWithConstraintsOnInterface() {
void shouldValidateArgsWithConstraintsInInterface() {
Object target = new MyInterfaceImpl();
testValidateArgs(target, List.of("addIntValue", "addPersonAndIntValue", "addPersons", "addPeople"), true);
testValidateArgs(target, List.of("addPerson", "addPersonNotValidated", "getPerson", "getIntValue"), false);
}
@Test
void shouldValidateReturnValueWithConstraintsDirectlyOnClass() {
void shouldValidateArgsWithConstraintsInGenericAbstractSuperclass() {
Object target = new GenericInterfaceImpl();
shouldValidateArguments(getHandlerMethod(target, "processTwo", String.class), true);
}
@Test
void shouldValidateArgsWithConstraintsInGenericInterface() {
Object target = new GenericInterfaceImpl();
shouldValidateArguments(getHandlerMethod(target, "processOne", Long.class), false);
shouldValidateArguments(getHandlerMethod(target, "processOneAndTwo", Long.class, Object.class), true);
}
@Test
void shouldValidateReturnValueWithConstraintsDirectlyInClass() {
Object target = new MyClass();
testValidateReturnValue(target, List.of("getPerson", "getIntValue"), true);
testValidateReturnValue(target, List.of("addPerson", "addIntValue", "addPersonNotValidated"), false);
}
@Test
void shouldValidateReturnValueWithConstraintsOnInterface() {
void shouldValidateReturnValueWithConstraintsInInterface() {
Object target = new MyInterfaceImpl();
testValidateReturnValue(target, List.of("getPerson", "getIntValue"), true);
testValidateReturnValue(target, List.of("addPerson", "addIntValue", "addPersonNotValidated"), false);
@@ -97,9 +111,19 @@ class HandlerMethodTests {
assertThat(hm3.getResolvedFromHandlerMethod()).isSameAs(hm1);
}
private static void shouldValidateArguments(HandlerMethod handlerMethod, boolean expected) {
if (expected) {
assertThat(handlerMethod.shouldValidateArguments()).as(handlerMethod.getMethod().getName()).isTrue();
}
else {
assertThat(handlerMethod.shouldValidateArguments()).as(handlerMethod.getMethod().getName()).isFalse();
}
}
private static void testValidateArgs(Object target, List<String> methodNames, boolean expected) {
for (String methodName : methodNames) {
assertThat(getHandlerMethod(target, methodName).shouldValidateArguments()).isEqualTo(expected);
shouldValidateArguments(getHandlerMethod(target, methodName), expected);
}
}
@@ -110,7 +134,11 @@ class HandlerMethodTests {
}
private static HandlerMethod getHandlerMethod(Object target, String methodName) {
Method method = ClassUtils.getMethod(target.getClass(), methodName, (Class<?>[]) null);
return getHandlerMethod(target, methodName, (Class<?>[]) null);
}
private static HandlerMethod getHandlerMethod(Object target, String methodName, Class<?>... parameterTypes) {
Method method = ClassUtils.getMethod(target.getClass(), methodName, parameterTypes);
return new HandlerMethod(target, method).createWithValidateFlags();
}
@@ -236,4 +264,32 @@ class HandlerMethodTests {
}
}
interface GenericInterface<A, B> {
void processOne(@Valid A value1);
void processOneAndTwo(A value1, @Max(42) B value2);
}
abstract static class GenericAbstractSuperclass<C> implements GenericInterface<Long, C> {
@Override
public void processOne(Long value1) {
}
@Override
public void processOneAndTwo(Long value1, C value2) {
}
public abstract void processTwo(@Max(42) C value);
}
static class GenericInterfaceImpl extends GenericAbstractSuperclass<String> {
@Override
public void processTwo(String value) {
}
}
}
@@ -107,12 +107,21 @@ class UriUtilsTests {
assertThat(UriUtils.decode("T%C5%8Dky%C5%8D", CHARSET)).as("Invalid encoded result").isEqualTo("T\u014dky\u014d");
assertThat(UriUtils.decode("/Z%C3%BCrich", CHARSET)).as("Invalid encoded result").isEqualTo("/Z\u00fcrich");
assertThat(UriUtils.decode("T\u014dky\u014d", CHARSET)).as("Invalid encoded result").isEqualTo("T\u014dky\u014d");
assertThat(UriUtils.decode("%20\u2019", CHARSET)).as("Invalid encoded result").isEqualTo(" \u2019");
assertThat(UriUtils.decode("\u015bp\u0159\u00ec\u0144\u0121", CHARSET)).as("Invalid encoded result").isEqualTo("śpřìńġ");
assertThat(UriUtils.decode("%20\u015bp\u0159\u00ec\u0144\u0121", CHARSET)).as("Invalid encoded result").isEqualTo(" śpřìńġ");
}
@Test
void decodeInvalidSequence() {
assertThatIllegalArgumentException().isThrownBy(() ->
UriUtils.decode("foo%2", CHARSET));
assertThatIllegalArgumentException().isThrownBy(() ->
UriUtils.decode("foo%", CHARSET));
assertThatIllegalArgumentException().isThrownBy(() ->
UriUtils.decode("%", CHARSET));
assertThatIllegalArgumentException().isThrownBy(() ->
UriUtils.decode("%zz", CHARSET));
}
@Test
@@ -117,7 +117,6 @@ public class JettyWebSocketHandlerAdapter implements Session.Listener {
private final Callback callback;
public JettyCallbackDataBuffer(DataBuffer delegate, Callback callback) {
Assert.notNull(delegate, "'delegate` must not be null");
Assert.notNull(callback, "Callback must not be null");
@@ -68,6 +68,7 @@ public class JettyWebSocketSession extends AbstractWebSocketSession<Session> {
@Nullable
private final Sinks.Empty<Void> handlerCompletionSink;
public JettyWebSocketSession(Session session, HandshakeInfo info, DataBufferFactory factory) {
this(session, info, factory, null);
}
@@ -107,6 +108,7 @@ public class JettyWebSocketSession extends AbstractWebSocketSession<Session> {
});
}
void handleMessage(WebSocketMessage message) {
Assert.state(this.sink != null, "No sink available");
this.sink.next(message);
@@ -189,7 +191,6 @@ public class JettyWebSocketSession extends AbstractWebSocketSession<Session> {
}
protected Mono<Void> sendMessage(WebSocketMessage message) {
Callback.Completable completable = new Callback.Completable();
DataBuffer dataBuffer = message.getPayload();
Session session = getDelegate();
@@ -245,4 +246,5 @@ public class JettyWebSocketSession extends AbstractWebSocketSession<Session> {
}
return Mono.fromFuture(completable);
}
}
@@ -21,6 +21,8 @@ import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
import org.springframework.http.MediaType;
@@ -62,6 +64,7 @@ import org.springframework.util.ObjectUtils;
* @author Rossen Stoyanchev
* @author Juergen Hoeller
* @author Brian Clozel
* @author Taeik Lim
* @since 4.2
*/
public class ResponseBodyEmitter {
@@ -88,6 +91,8 @@ public class ResponseBodyEmitter {
private final DefaultCallback completionCallback = new DefaultCallback();
/** Guards access to write operations on the response. */
protected final Lock writeLock = new ReentrantLock();
/**
* Create a new ResponseBodyEmitter instance.
@@ -117,36 +122,48 @@ public class ResponseBodyEmitter {
}
synchronized void initialize(Handler handler) throws IOException {
this.handler = handler;
void initialize(Handler handler) throws IOException {
this.writeLock.lock();
try {
sendInternal(this.earlySendAttempts);
}
finally {
this.earlySendAttempts.clear();
}
this.handler = handler;
if (this.complete) {
if (this.failure != null) {
this.handler.completeWithError(this.failure);
try {
sendInternal(this.earlySendAttempts);
}
finally {
this.earlySendAttempts.clear();
}
if (this.complete) {
if (this.failure != null) {
this.handler.completeWithError(this.failure);
}
else {
this.handler.complete();
}
}
else {
this.handler.complete();
this.handler.onTimeout(this.timeoutCallback);
this.handler.onError(this.errorCallback);
this.handler.onCompletion(this.completionCallback);
}
}
else {
this.handler.onTimeout(this.timeoutCallback);
this.handler.onError(this.errorCallback);
this.handler.onCompletion(this.completionCallback);
finally {
this.writeLock.unlock();
}
}
synchronized void initializeWithError(Throwable ex) {
this.complete = true;
this.failure = ex;
this.earlySendAttempts.clear();
this.errorCallback.accept(ex);
void initializeWithError(Throwable ex) {
this.writeLock.lock();
try {
this.complete = true;
this.failure = ex;
this.earlySendAttempts.clear();
this.errorCallback.accept(ex);
}
finally {
this.writeLock.unlock();
}
}
/**
@@ -183,22 +200,28 @@ public class ResponseBodyEmitter {
* @throws IOException raised when an I/O error occurs
* @throws java.lang.IllegalStateException wraps any other errors
*/
public synchronized void send(Object object, @Nullable MediaType mediaType) throws IOException {
public void send(Object object, @Nullable MediaType mediaType) throws IOException {
Assert.state(!this.complete, () -> "ResponseBodyEmitter has already completed" +
(this.failure != null ? " with error: " + this.failure : ""));
if (this.handler != null) {
try {
this.handler.send(object, mediaType);
this.writeLock.lock();
try {
if (this.handler != null) {
try {
this.handler.send(object, mediaType);
}
catch (IOException ex) {
throw ex;
}
catch (Throwable ex) {
throw new IllegalStateException("Failed to send " + object, ex);
}
}
catch (IOException ex) {
throw ex;
}
catch (Throwable ex) {
throw new IllegalStateException("Failed to send " + object, ex);
else {
this.earlySendAttempts.add(new DataWithMediaType(object, mediaType));
}
}
else {
this.earlySendAttempts.add(new DataWithMediaType(object, mediaType));
finally {
this.writeLock.unlock();
}
}
@@ -211,10 +234,16 @@ public class ResponseBodyEmitter {
* @throws java.lang.IllegalStateException wraps any other errors
* @since 6.0.12
*/
public synchronized void send(Set<DataWithMediaType> items) throws IOException {
public void send(Set<DataWithMediaType> items) throws IOException {
Assert.state(!this.complete, () -> "ResponseBodyEmitter has already completed" +
(this.failure != null ? " with error: " + this.failure : ""));
sendInternal(items);
this.writeLock.lock();
try {
sendInternal(items);
}
finally {
this.writeLock.unlock();
}
}
private void sendInternal(Set<DataWithMediaType> items) throws IOException {
@@ -245,10 +274,16 @@ public class ResponseBodyEmitter {
* to complete request processing. It should not be used after container
* related events such as an error while {@link #send(Object) sending}.
*/
public synchronized void complete() {
this.complete = true;
if (this.handler != null) {
this.handler.complete();
public void complete() {
this.writeLock.lock();
try {
this.complete = true;
if (this.handler != null) {
this.handler.complete();
}
}
finally {
this.writeLock.unlock();
}
}
@@ -263,11 +298,17 @@ public class ResponseBodyEmitter {
* container related events such as an error while
* {@link #send(Object) sending}.
*/
public synchronized void completeWithError(Throwable ex) {
this.complete = true;
this.failure = ex;
if (this.handler != null) {
this.handler.completeWithError(ex);
public void completeWithError(Throwable ex) {
this.writeLock.lock();
try {
this.complete = true;
this.failure = ex;
if (this.handler != null) {
this.handler.completeWithError(ex);
}
}
finally {
this.writeLock.unlock();
}
}
@@ -276,8 +317,14 @@ public class ResponseBodyEmitter {
* called from a container thread when an async request times out.
* <p>As of 6.2, one can register multiple callbacks for this event.
*/
public synchronized void onTimeout(Runnable callback) {
this.timeoutCallback.addDelegate(callback);
public void onTimeout(Runnable callback) {
this.writeLock.lock();
try {
this.timeoutCallback.addDelegate(callback);
}
finally {
this.writeLock.unlock();
}
}
/**
@@ -287,8 +334,14 @@ public class ResponseBodyEmitter {
* <p>As of 6.2, one can register multiple callbacks for this event.
* @since 5.0
*/
public synchronized void onError(Consumer<Throwable> callback) {
this.errorCallback.addDelegate(callback);
public void onError(Consumer<Throwable> callback) {
this.writeLock.lock();
try {
this.errorCallback.addDelegate(callback);
}
finally {
this.writeLock.unlock();
}
}
/**
@@ -298,8 +351,14 @@ public class ResponseBodyEmitter {
* detecting that a {@code ResponseBodyEmitter} instance is no longer usable.
* <p>As of 6.2, one can register multiple callbacks for this event.
*/
public synchronized void onCompletion(Runnable callback) {
this.completionCallback.addDelegate(callback);
public void onCompletion(Runnable callback) {
this.writeLock.lock();
try {
this.completionCallback.addDelegate(callback);
}
finally {
this.writeLock.unlock();
}
}
@@ -21,8 +21,6 @@ import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
@@ -46,10 +44,6 @@ public class SseEmitter extends ResponseBodyEmitter {
private static final MediaType TEXT_PLAIN = new MediaType("text", "plain", StandardCharsets.UTF_8);
/** Guards access to write operations on the response. */
private final Lock writeLock = new ReentrantLock();
/**
* Create a new SseEmitter instance.
*/
@@ -393,7 +393,7 @@ class RequestMappingHandlerAdapterTests {
}
private static class SseController {
static class SseController {
public ResponseEntity<?> handle(@RequestParam String q) throws IOException {
if (q.equals("sse")) {
@@ -183,21 +183,19 @@ class PathResourceResolverTests {
private String relativePath;
public TestUrlResource(String path) throws MalformedURLException {
super(path);
}
public String getSavedRelativePath() {
return this.relativePath;
}
@Override
public Resource createRelative(String relativePath) {
this.relativePath = relativePath;
return this;
}
public String getSavedRelativePath() {
return this.relativePath;
}
}
}
@@ -58,6 +58,7 @@ public class JettyWebSocketHandlerAdapter implements Session.Listener {
this.wsSession = wsSession;
}
@Override
public void onWebSocketOpen(Session session) {
try {
@@ -147,4 +148,5 @@ public class JettyWebSocketHandlerAdapter implements Session.Listener {
}
}
}
}
@@ -173,7 +173,6 @@ public class JettyWebSocketSession extends AbstractWebSocketSession<Session> {
return getNativeSession().isOpen();
}
@Override
public void initializeNativeSession(Session session) {
super.initializeNativeSession(session);
@@ -213,7 +212,6 @@ public class JettyWebSocketSession extends AbstractWebSocketSession<Session> {
return Collections.emptyList();
}
@Override
protected void sendTextMessage(TextMessage message) throws IOException {
useSession((session, callback) -> session.sendText(message.getPayload(), callback));
@@ -247,7 +245,6 @@ public class JettyWebSocketSession extends AbstractWebSocketSession<Session> {
}
catch (ExecutionException ex) {
Throwable cause = ex.getCause();
if (cause instanceof IOException ioEx) {
throw ioEx;
}
@@ -263,6 +260,7 @@ public class JettyWebSocketSession extends AbstractWebSocketSession<Session> {
}
}
@FunctionalInterface
private interface SessionConsumer {