Compare commits

..

37 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
49 changed files with 941 additions and 309 deletions
@@ -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`.
@@ -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
+16 -16
View File
@@ -8,30 +8,30 @@ javaPlatform {
dependencies {
api(platform("com.fasterxml.jackson:jackson-bom:2.18.4.1"))
api(platform("io.micrometer:micrometer-bom:1.14.10"))
api(platform("io.netty:netty-bom:4.1.124.Final"))
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.9"))
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.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.25"))
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.25"))
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.4"))
api(platform("org.mockito:mockito-bom:5.18.0"))
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.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.31.1")
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,7 +129,7 @@ dependencies {
api("org.hibernate:hibernate-core-jakarta:5.6.15.Final")
api("org.hibernate:hibernate-validator:7.0.5.Final")
api("org.hsqldb:hsqldb:2.7.4")
api("org.htmlunit:htmlunit:4.14.0")
api("org.htmlunit:htmlunit:4.16.0")
api("org.javamoney:moneta:1.4.4")
api("org.jruby:jruby:9.4.13.0")
api("org.junit.support:testng-engine:1.0.5")
@@ -137,7 +137,7 @@ dependencies {
api("org.ogce:xpp3:1.1.6")
api("org.python:jython-standalone:2.7.4")
api("org.quartz-scheduler:quartz:2.3.2")
api("org.seleniumhq.selenium:htmlunit3-driver:4.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")
+1 -1
View File
@@ -1,4 +1,4 @@
version=6.2.10
version=6.2.11
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
@@ -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) {
@@ -1458,9 +1457,18 @@ 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
@@ -2268,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 {
@@ -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 {
}
@@ -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;
}
@@ -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()
*/
@@ -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) {
@@ -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,17 +396,26 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
this.active = false;
Set<Thread> threads = this.activeThreads;
if (threads != null) {
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);
}
}
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);
@@ -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;
@@ -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
);
@@ -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);
@@ -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;
}
@@ -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);
@@ -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;
@@ -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);
});
}
}
@@ -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) {
}
}
}
@@ -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.
*/
@@ -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 {