Compare commits

...

50 Commits

Author SHA1 Message Date
Stéphane Nicoll d3343db2ff Release v6.1.17 2025-02-13 10:21:48 +01:00
Juergen Hoeller 9d6d67188a Upgrade to Jetty 12.0.16, Netty 4.1.118, XStream 1.4.21, AssertJ 3.27.3, Checkstyle 10.21.2 2025-02-11 22:47:24 +01:00
Juergen Hoeller 24fd0940bd Align with SmartClassLoader handling for AOP proxy classes
Closes gh-34274

(cherry picked from commit f53da04717)
2025-02-11 22:19:43 +01:00
Sam Brannen 5b928f47a8 Finish incomplete sentences
(cherry picked from commit 9d3374b28d)
2025-02-11 12:32:27 +01:00
Stéphane Nicoll 5e52baee86 Upgrade to Reactor 2023.0.15
Closes gh-34406
2025-02-11 11:44:35 +01:00
Stéphane Nicoll c10596a1fd Upgrade to RSocket 1.1.5
Closes gh-34405
2025-02-11 11:43:04 +01:00
Sam Brannen a68b7289f0 Improve warning for unexpected use of value attribute as @⁠Component name
Prior to this commit, if a String 'value' attribute of an annotation
was annotated with @⁠AliasFor and explicitly configured to alias an
attribute other than @⁠Component.value, the value was still used as the
@⁠Component name, but the warning message that was logged stated that
the 'value' attribute should be annotated with
@⁠AliasFor(annotation=Component.class). However, it is not possible to
annotate an annotation attribute twice with @⁠AliasFor.

To address that, this commit revises the logic in
AnnotationBeanNameGenerator so that it issues a log message similar to
the following in such scenarios.

WARN o.s.c.a.AnnotationBeanNameGenerator - Although the 'value'
attribute in @⁠example.MyStereotype declares @⁠AliasFor for an
attribute other than @⁠Component's 'value' attribute, the value is
still used as the @⁠Component name based on convention. As of Spring
Framework 7.0, such a 'value' attribute will no longer be used as the
@⁠Component name.

See gh-34346
Closes gh-34317

(cherry picked from commit 17a94fb110)
2025-02-10 13:32:28 +01:00
Sam Brannen a5b9e667e3 Polishing
(cherry picked from commit 2fcae65853)
2025-02-10 13:32:20 +01:00
Branden Clark 78fe55f4d1 Check hasNext on sessionIds in UserDestinationResult
Closes gh-34333

Signed-off-by: Branden Clark <brandenrayclark@gmail.com>
2025-02-10 11:22:28 +00:00
Brian Clozel 65913c3655 Fix "Nth day of week" Quartz-style cron expressions
Prior to this commit, `CronExpression` would support Quartz-style
expressions with "Nth occurence of a  dayOfWeek" semantics by using the
`TemporalAdjusters.dayOfWeekInMonth` JDK support. This method will
return the Nth occurence starting with the month of the given temporal,
but in some cases will overflow to the next or previous month.
This behavior is not expected for our cron expression support.

This commit ensures that when an overflow happens (meaning, the
resulting date is not in the same month as the input temporal), we
should instead have another attempt at finding a valid month for this
expression.

Fixes gh-34377
2025-02-06 18:34:24 +01:00
Sam Brannen b7a996a64b Clarify component scanning of abstract classes with @⁠Lookup methods
Due to changes in gh-19118, classes that contain @⁠Lookup methods are
no longer required to be concrete classes for use with component
scanning; however, the reference documentation still states that such
classes must not be abstract.

This commit therefore removes the outdated reference documentation and
updates the corresponding Javadoc.

See gh-19118
Closes gh-34367

(cherry picked from commit 819a7c86c1)
2025-02-05 13:44:28 +01:00
Stéphane Nicoll 5b1a7c7f21 Handle arbitrary JoinPoint argument index
Closes gh-34369
2025-02-05 11:55:11 +01:00
Brian Clozel ebd80bdd6c Delete failing Freemarker test
This test was already ignored as of Java 21 because of a Java behavior
change, and now it started failing as of 17.0.14.
This commit removes the test entirely.
2025-02-04 10:03:50 +01:00
Juergen Hoeller a4fc68b8e8 Explicitly set custom ClassLoader on CGLIB Enhancer
Closes gh-34274

(cherry picked from commit 1b18928bf0)
2025-02-03 15:37:04 +01:00
Sam Brannen c333946b0b Restore property binding support for a Map that implements Iterable
The changes in commit c20a2e4763 introduced a regression with regard to
binding to a Map property when the Map also happens to implement
Iterable.

Although that is perhaps not a very common scenario, this commit
reorders the if-blocks in AbstractNestablePropertyAccessor's
getPropertyValue(PropertyTokenHolder) method so that a Map is
considered before an Iterable, thereby allowing an Iterable-Map to be
accessed as a Map.

See gh-907
Closes gh-34332

(cherry picked from commit b9e43d05bd)
2025-01-29 17:46:19 +01:00
rstoyanchev b0a8a3ec5f Enhance DisconnectedClientHelper exception type checks
We now look for the target exception types in cause chain as well,
but return false if we encounter a RestClient or WebClient
exception in the chain.

Closes gh-34264
2025-01-21 12:25:45 +00:00
rstoyanchev bb5be2119c Restore exception phrase in DisconnectedClientHelper
Effectively revert 203fa7, and add implementation comment and tests.

See gh-34264
2025-01-21 12:25:35 +00:00
Brian Clozel cf662368a5 Fix Wrong parentId tracking in JFR application startup
This commit fixes the tracking of the main event parentId for the Java
Flight Recorder implementation variant.

Fixes gh-34254
2025-01-13 20:22:07 +01:00
rstoyanchev e1b06ccfaa Improve logging in ReactiveTypeHandler
See gh-34188
2025-01-06 12:33:38 +00:00
rstoyanchev 9eefdb8041 Synchronize in WebAsyncManager onError/onTimeout
On connection loss, in a race between application thread and onError
callback trying to set the DeferredResult and dispatch, the onError
callback must not exit until dispatch completes. Currently, it may do
so because the DeferredResult has checks to bypasses locking or even
trying to dispatch if result is already set.

Closes gh-34192
2025-01-06 12:33:17 +00:00
rstoyanchev c50cb10964 Minor refactoring in WebAsyncManager
There is no need to set the DeferredResult from WebAsyncManager in an
onError notification because it is already done from the Lifecycle
interceptor in DeferredResult.

See gh-34192
2025-01-06 12:33:02 +00:00
rstoyanchev 245341231f Polishing in WebAsyncManager
See gh-34192
2025-01-06 12:32:40 +00:00
Sébastien Deleuze 875cc828cf Upgrade to Java 17.0.13 2024-12-30 12:14:52 +01:00
Stéphane Nicoll 898d3ec86a Backport tests for exact match resolution
See gh-34124
2024-12-23 12:15:44 +01:00
Tran Ngoc Nhan 8ccaabe778 Fix broken links in the web reference documentation
Backport of gh-34115.

Closes gh-34139
2024-12-23 11:22:17 +01:00
Sam Brannen 9c934b5019 Support varargs-only MethodHandle as SpEL function
Prior to this commit, if a MethodHandle was registered as a custom
function in the Spring Expression Language (SpEL) for a static method
that accepted only a variable argument list (for example,
`static String func(String... args)`), attempting to invoke the
registered function within a SpEL expression resulted in a
ClassCastException because the varargs array was unnecessarily wrapped
in an Object[].

This commit modifies the logic in FunctionReference's internal
executeFunctionViaMethodHandle() method to address that.

Closes gh-34109
2024-12-18 15:34:27 +01:00
Brian Clozel 4b45338ae6 Improve PathMatcher/PatternParser XML configuration
Prior to this commit, the MVC namespace for the XML Spring configuration
model would use the `PathMatcher` bean instance when provided like this:

```
<bean id="pathMatcher" class="org.springframework.util.AntPathMatcher"/>
<mvc:annotation-driven>
  <mvc:path-matching path-matcher="pathMatcher"/>
</mvc:annotation-driven>
<mvc:resources mapping="/resources/**" location="classpath:/static/"/>
```

With this configuration, the handler mapping for annotated controller
would use the given `AntPathMatcher` instance but the handler mapping
for resources would still use the default, which is `PathPatternParser`
since 6.0.

This commit ensures that when a custom `path-matcher` is defined, it's
consistently used for all MVC handler mappings as an alias to the
well-known bean name. This allows to use `AntPathMatcher` consistently
while working on a migration path to `PathPatternParser`

This commit also adds a new XML attribute to the path matching
configuration that makes it possible to use a custom `PathPatternParser`
instance:

```
<bean id="patternParser" class="org.springframework.web.util.pattern.PathPatternParser"/>
<mvc:annotation-driven>
  <mvc:path-matching pattern-parser="patternParser"/>
</mvc:annotation-driven>
```

Closes gh-34102
See gh-34064
2024-12-17 10:30:18 +01:00
Juergen Hoeller 95003e3512 Avoid logger serialization behind shared EntityManager proxy
See gh-34084
2024-12-12 19:52:21 +01:00
Stéphane Nicoll 68368621f3 Prevent execution of Antora jobs on forks
Closes gh-34083
2024-12-12 17:39:59 +01:00
Brian Clozel 890a8f4311 Next development version (v6.1.17-SNAPSHOT) 2024-12-12 10:04:11 +01:00
Juergen Hoeller 0976d1a252 Upgrade to RxJava 3.1.10 and Checkstyle 10.20.2 2024-12-11 17:49:18 +01:00
Juergen Hoeller 2c0ce790d8 Avoid deprecated ListenableFuture name for internal class 2024-12-11 17:47:11 +01:00
Juergen Hoeller d512aafc36 Avoid javadoc references to deprecated types/methods
(cherry picked from commit 68997d8416)
2024-12-11 17:47:03 +01:00
Sam Brannen faa000f330 Log alias removal in DefaultListableBeanFactory
Prior to this commit, information was logged when a bean definition
overrode an existing bean definition, but nothing was logged when the
registration of a bean definition resulted in the removal of an alias.

With this commit, an INFO message is now logged whenever an alias is
removed in DefaultListableBeanFactory.

Closes gh-34070

(cherry picked from commit 41d9f21ab9)
2024-12-11 15:05:42 +01:00
Juergen Hoeller daf1b3d7a7 Remove unnecessary downcast to DefaultListableBeanFactory
See gh-33920
See gh-25952
2024-12-10 22:33:01 +01:00
Juergen Hoeller a48897a241 Log provider setup failure at info level without stacktrace
Closes gh-33979

(cherry picked from commit 3e3ca74020)
2024-12-10 22:28:38 +01:00
Stéphane Nicoll 7ffa12f90f Upgrade to Reactor 2023.0.13
Closes gh-34049
2024-12-10 14:09:57 +01:00
Stéphane Nicoll 0e74ffa144 Start building against Reactor 2023.0.13 snapshots
See gh-34049
2024-12-08 10:47:15 +01:00
Juergen Hoeller 357195289d Unit test for match against unresolvable wildcard
See gh-33982
2024-12-05 17:53:30 +01:00
Sébastien Deleuze 4fbca99f20 Remove unnecessary HandshakeHandlerRuntimeHints
Those hints are not needed anymore as of Spring Framework 6.1.
Backport of gh-34032.

Closes gh-34033
2024-12-05 15:46:31 +01:00
Juergen Hoeller c48ca5151f Upgrade to Gradle 8.11.1 2024-12-04 17:36:11 +01:00
Juergen Hoeller aaf2e8fbe6 Polishing
(cherry picked from commit edf7f3cd43)
2024-12-04 17:35:50 +01:00
Juergen Hoeller 3635ff0c17 Consistent fallback to NoUpgradeStrategyWebSocketService
Closes gh-33970

(cherry picked from commit 58c64cba2c)
2024-12-04 16:55:51 +01:00
Simon Baslé f20e76e227 Upgrade to Undertow 2.3.18.Final
Closes gh-33976
2024-11-27 14:31:46 +01:00
Simon Baslé dd32f95192 Dispatch in UndertowHttpHandlerAdapter
This ensures that the reactive handling of the request is dispatched
from the Undertow IO thread, marking the exchange as async rather than
ending it once the Undertow `handleRequest` method returns.

See gh-33885
Closes gh-33969
2024-11-26 16:58:21 +01:00
Tran Ngoc Nhan 75a920bc9f Fix a typo in the filters documentation
Backport of gh-33959
Closes gh-33971
2024-11-26 16:17:18 +01:00
Sam Brannen 92874adae9 Polish SpEL documentation
(cherry picked from commit 7196f3f554)
2024-11-18 11:43:14 +01:00
Sam Brannen 55167b7f50 Fix SpEL examples in reference guide
Closes gh-33907

(cherry picked from commit a12d40e10b)
2024-11-18 11:43:05 +01:00
Sam Brannen 3129afba19 Upgrade to Gradle 8.11
Closes gh-33895

(cherry picked from commit bb32df0a06)
2024-11-15 16:08:05 +01:00
Sam Brannen 1366d2926b Upgrade to antora-ui-spring version 0.4.18
Closes gh-33892

(cherry picked from commit fe8573c0ab)
2024-11-15 14:34:00 +01:00
90 changed files with 1269 additions and 486 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ permissions:
jobs:
build:
name: Dispatch docs deployment
if: github.repository_owner == 'spring-projects'
if: ${{ github.repository == 'spring-projects/spring-framework' }}
runs-on: ubuntu-latest
steps:
- name: Check out code
+1 -1
View File
@@ -1,3 +1,3 @@
# Enable auto-env through the sdkman_auto_env config
# Add key=value pairs of SDKs to use below
java=17.0.12-librca
java=17.0.13-librca
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -50,7 +50,7 @@ public class CheckstyleConventions {
project.getPlugins().apply(CheckstylePlugin.class);
project.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g"));
CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class);
checkstyle.setToolVersion("10.20.1");
checkstyle.setToolVersion("10.21.2");
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
+1 -1
View File
@@ -36,4 +36,4 @@ runtime:
failure_level: warn
ui:
bundle:
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.17/ui-bundle.zip
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.18/ui-bundle.zip
@@ -120,8 +120,6 @@ dynamically generate a subclass that overrides the method.
subclasses cannot be `final`, and the method to be overridden cannot be `final`, either.
* Unit-testing a class that has an `abstract` method requires you to subclass the class
yourself and to supply a stub implementation of the `abstract` method.
* Concrete methods are also necessary for component scanning, which requires concrete
classes to pick up.
* A further key limitation is that lookup methods do not work with factory methods and
in particular not with `@Bean` methods in configuration classes, since, in that case,
the container is not in charge of creating the instance and therefore cannot create
@@ -293,11 +291,6 @@ Kotlin::
----
======
Note that you should typically declare such annotated lookup methods with a concrete
stub implementation, in order for them to be compatible with Spring's component
scanning rules where abstract classes get ignored by default. This limitation does not
apply to explicitly registered or explicitly imported bean classes.
[TIP]
====
Another way of accessing differently scoped target beans is an `ObjectFactory`/
@@ -12,27 +12,27 @@ Java::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
Inventor einstein = p.parseExpression(
"new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
Inventor einstein = parser.parseExpression(
"new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
.getValue(Inventor.class);
// create new Inventor instance within the add() method of List
p.parseExpression(
"Members.add(new org.spring.samples.spel.inventor.Inventor(
'Albert Einstein', 'German'))").getValue(societyContext);
parser.parseExpression(
"Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))")
.getValue(societyContext);
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
----
val einstein = p.parseExpression(
"new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
val einstein = parser.parseExpression(
"new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
.getValue(Inventor::class.java)
// create new Inventor instance within the add() method of List
p.parseExpression(
"Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))")
parser.parseExpression(
"Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))")
.getValue(societyContext)
----
======
@@ -110,8 +110,8 @@ potentially more efficient use cases if the `MethodHandle` target and parameters
been fully bound prior to registration; however, partially bound handles are also
supported.
Consider the `String#formatted(String, Object...)` instance method, which produces a
message according to a template and a variable number of arguments.
Consider the `String#formatted(Object...)` instance method, which produces a message
according to a template and a variable number of arguments.
You can register and use the `formatted` method as a `MethodHandle`, as the following
example shows:
@@ -151,10 +151,10 @@ Kotlin::
----
======
As hinted above, binding a `MethodHandle` and registering the bound `MethodHandle` is also
supported. This is likely to be more performant if both the target and all the arguments
are bound. In that case no arguments are necessary in the SpEL expression, as the
following example shows:
As mentioned above, binding a `MethodHandle` and registering the bound `MethodHandle` is
also supported. This is likely to be more performant if both the target and all the
arguments are bound. In that case no arguments are necessary in the SpEL expression, as
the following example shows:
[tabs]
======
@@ -168,9 +168,10 @@ Java::
String template = "This is a %s message with %s words: <%s>";
Object varargs = new Object[] { "prerecorded", 3, "Oh Hello World!", "ignored" };
MethodHandle mh = MethodHandles.lookup().findVirtual(String.class, "formatted",
MethodType.methodType(String.class, Object[].class))
MethodType.methodType(String.class, Object[].class))
.bindTo(template)
.bindTo(varargs); //here we have to provide arguments in a single array binding
// Here we have to provide the arguments in a single array binding:
.bindTo(varargs);
context.setVariable("message", mh);
// evaluates to "This is a prerecorded message with 3 words: <Oh Hello World!>"
@@ -189,9 +190,10 @@ Kotlin::
val varargs = arrayOf("prerecorded", 3, "Oh Hello World!", "ignored")
val mh = MethodHandles.lookup().findVirtual(String::class.java, "formatted",
MethodType.methodType(String::class.java, Array<Any>::class.java))
MethodType.methodType(String::class.java, Array<Any>::class.java))
.bindTo(template)
.bindTo(varargs) //here we have to provide arguments in a single array binding
// Here we have to provide the arguments in a single array binding:
.bindTo(varargs)
context.setVariable("message", mh)
// evaluates to "This is a prerecorded message with 3 words: <Oh Hello World!>"
@@ -193,7 +193,7 @@ Kotlin::
[[webtestclient-fn-config]]
=== Bind to Router Function
This setup allows you to test <<web-reactive.adoc#webflux-fn, functional endpoints>> via
This setup allows you to test xref:web/webflux-functional.adoc[functional endpoints] via
mock request and response objects, without a running server.
For WebFlux, use the following which delegates to `RouterFunctions.toWebHandler` to
@@ -1,5 +1,6 @@
[[webflux-cors]]
= CORS
[.small]#xref:web/webmvc-cors.adoc[See equivalent in the Servlet stack]#
Spring WebFlux lets you handle CORS (Cross-Origin Resource Sharing). This section
@@ -364,7 +365,7 @@ Kotlin::
You can apply CORS support through the built-in
{spring-framework-api}/web/cors/reactive/CorsWebFilter.html[`CorsWebFilter`], which is a
good fit with <<webflux-fn, functional endpoints>>.
good fit with xref:web/webflux-functional.adoc[functional endpoints].
NOTE: If you try to use the `CorsFilter` with Spring Security, keep in mind that Spring
Security has {docs-spring-security}/servlet/integrations/cors.html[built-in support] for
@@ -1,5 +1,6 @@
[[webflux-fn]]
= Functional Endpoints
[.small]#xref:web/webmvc-functional.adoc[See equivalent in the Servlet stack]#
Spring WebFlux includes WebFlux.fn, a lightweight functional programming model in which functions
@@ -1,5 +1,6 @@
[[webflux-websocket]]
= WebSockets
[.small]#xref:web/websocket.adoc[See equivalent in the Servlet stack]#
This part of the reference documentation covers support for reactive-stack WebSocket
@@ -101,7 +101,7 @@ On that foundation, Spring WebFlux provides a choice of two programming models:
from the `spring-web` module. Both Spring MVC and WebFlux controllers support reactive
(Reactor and RxJava) return types, and, as a result, it is not easy to tell them apart. One notable
difference is that WebFlux also supports reactive `@RequestBody` arguments.
* <<webflux-fn>>: Lambda-based, lightweight, and functional programming model. You can think of
* xref:web/webflux-functional.adoc[Functional Endpoints]: Lambda-based, lightweight, and functional programming model. You can think of
this as a small library or a set of utilities that an application can use to route and
handle requests. The big difference with annotated controllers is that the application
is in charge of request handling from start to finish versus declaring intent through
@@ -1,5 +1,6 @@
[[mvc-cors]]
= CORS
[.small]#xref:web/webflux-cors.adoc[See equivalent in the Reactive stack]#
Spring MVC lets you handle CORS (Cross-Origin Resource Sharing). This section
@@ -1,6 +1,7 @@
[[webmvc-fn]]
= Functional Endpoints
[.small]#<<web-reactive.adoc#webflux-fn, See equivalent in the Reactive stack>>#
[.small]#xref:web/webflux-functional.adoc[See equivalent in the Reactive stack]#
Spring Web MVC includes WebMvc.fn, a lightweight functional programming model in which functions
are used to route and handle requests and contracts are designed for immutability.
@@ -26,7 +26,7 @@ available through the `ServletRequest.getParameter{asterisk}()` family of method
[[forwarded-headers]]
[[filters-forwarded-headers]]
== Forwarded Headers
[.small]#xref:web/webflux/reactive-spring.adoc#webflux-forwarded-headers[See equivalent in the Reactive stack]#
@@ -1,6 +1,7 @@
[[websocket]]
= WebSockets
:page-section-summary-toc: 1
[.small]#xref:web/webflux-websocket.adoc[See equivalent in the Reactive stack]#
This part of the reference documentation covers support for Servlet stack, WebSocket
+11 -11
View File
@@ -9,15 +9,15 @@ javaPlatform {
dependencies {
api(platform("com.fasterxml.jackson:jackson-bom:2.15.4"))
api(platform("io.micrometer:micrometer-bom:1.12.12"))
api(platform("io.netty:netty-bom:4.1.115.Final"))
api(platform("io.netty:netty-bom:4.1.118.Final"))
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
api(platform("io.projectreactor:reactor-bom:2023.0.12"))
api(platform("io.rsocket:rsocket-bom:1.1.4"))
api(platform("io.projectreactor:reactor-bom:2023.0.15"))
api(platform("io.rsocket:rsocket-bom:1.1.5"))
api(platform("org.apache.groovy:groovy-bom:4.0.24"))
api(platform("org.apache.logging.log4j:log4j-bom:2.21.1"))
api(platform("org.assertj:assertj-bom:3.26.3"))
api(platform("org.eclipse.jetty:jetty-bom:12.0.15"))
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.15"))
api(platform("org.assertj:assertj-bom:3.27.3"))
api(platform("org.eclipse.jetty:jetty-bom:12.0.16"))
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.16"))
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3"))
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.3"))
api(platform("org.junit:junit-bom:5.10.5"))
@@ -44,7 +44,7 @@ dependencies {
api("com.sun.xml.bind:jaxb-impl:3.0.2")
api("com.sun.xml.bind:jaxb-xjc:3.0.2")
api("com.thoughtworks.qdox:qdox:2.1.0")
api("com.thoughtworks.xstream:xstream:1.4.20")
api("com.thoughtworks.xstream:xstream:1.4.21")
api("commons-io:commons-io:2.15.0")
api("de.bechte.junit:junit-hierarchicalcontextrunner:4.12.2")
api("io.micrometer:context-propagation:1.1.1")
@@ -54,11 +54,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.9")
api("io.reactivex.rxjava3:rxjava:3.1.10")
api("io.smallrye.reactive:mutiny:1.10.0")
api("io.undertow:undertow-core:2.3.17.Final")
api("io.undertow:undertow-servlet:2.3.17.Final")
api("io.undertow:undertow-websockets-jsr:2.3.17.Final")
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.vavr:vavr:0.10.4")
api("jakarta.activation:jakarta.activation-api:2.0.1")
api("jakarta.annotation:jakarta.annotation-api:2.0.0")
+1 -1
View File
@@ -1,4 +1,4 @@
version=6.1.16-SNAPSHOT
version=6.1.17
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 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.
@@ -276,14 +276,18 @@ public abstract class AbstractAspectJAdvice implements Advice, AspectJPrecedence
}
if (this.aspectJAdviceMethod.getParameterCount() == this.argumentNames.length + 1) {
// May need to add implicit join point arg name...
Class<?> firstArgType = this.aspectJAdviceMethod.getParameterTypes()[0];
if (firstArgType == JoinPoint.class ||
firstArgType == ProceedingJoinPoint.class ||
firstArgType == JoinPoint.StaticPart.class) {
String[] oldNames = this.argumentNames;
this.argumentNames = new String[oldNames.length + 1];
this.argumentNames[0] = "THIS_JOIN_POINT";
System.arraycopy(oldNames, 0, this.argumentNames, 1, oldNames.length);
for (int i = 0; i < this.aspectJAdviceMethod.getParameterCount(); i++) {
Class<?> argType = this.aspectJAdviceMethod.getParameterTypes()[i];
if (argType == JoinPoint.class ||
argType == ProceedingJoinPoint.class ||
argType == JoinPoint.StaticPart.class) {
String[] oldNames = this.argumentNames;
this.argumentNames = new String[oldNames.length + 1];
System.arraycopy(oldNames, 0, this.argumentNames, 0, i);
this.argumentNames[i] = "THIS_JOIN_POINT";
System.arraycopy(oldNames, i, this.argumentNames, i + 1, oldNames.length - i);
break;
}
}
}
}
@@ -0,0 +1,135 @@
/*
* Copyright 2002-2025 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.aop.aspectj;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.function.Consumer;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link AbstractAspectJAdvice}.
*
* @author Joshua Chen
* @author Stephane Nicoll
*/
class AbstractAspectJAdviceTests {
@Test
void setArgumentNamesFromStringArray_withoutJoinPointParameter() {
AbstractAspectJAdvice advice = getAspectJAdvice("methodWithNoJoinPoint");
assertThat(advice).satisfies(hasArgumentNames("arg1", "arg2"));
}
@Test
void setArgumentNamesFromStringArray_withJoinPointAsFirstParameter() {
AbstractAspectJAdvice advice = getAspectJAdvice("methodWithJoinPointAsFirstParameter");
assertThat(advice).satisfies(hasArgumentNames("THIS_JOIN_POINT", "arg1", "arg2"));
}
@Test
void setArgumentNamesFromStringArray_withJoinPointAsLastParameter() {
AbstractAspectJAdvice advice = getAspectJAdvice("methodWithJoinPointAsLastParameter");
assertThat(advice).satisfies(hasArgumentNames("arg1", "arg2", "THIS_JOIN_POINT"));
}
@Test
void setArgumentNamesFromStringArray_withJoinPointAsMiddleParameter() {
AbstractAspectJAdvice advice = getAspectJAdvice("methodWithJoinPointAsMiddleParameter");
assertThat(advice).satisfies(hasArgumentNames("arg1", "THIS_JOIN_POINT", "arg2"));
}
@Test
void setArgumentNamesFromStringArray_withProceedingJoinPoint() {
AbstractAspectJAdvice advice = getAspectJAdvice("methodWithProceedingJoinPoint");
assertThat(advice).satisfies(hasArgumentNames("THIS_JOIN_POINT", "arg1", "arg2"));
}
@Test
void setArgumentNamesFromStringArray_withStaticPart() {
AbstractAspectJAdvice advice = getAspectJAdvice("methodWithStaticPart");
assertThat(advice).satisfies(hasArgumentNames("THIS_JOIN_POINT", "arg1", "arg2"));
}
private Consumer<AbstractAspectJAdvice> hasArgumentNames(String... argumentNames) {
return advice -> assertThat(advice).extracting("argumentNames")
.asInstanceOf(InstanceOfAssertFactories.array(String[].class))
.containsExactly(argumentNames);
}
private AbstractAspectJAdvice getAspectJAdvice(final String methodName) {
AbstractAspectJAdvice advice = new TestAspectJAdvice(getMethod(methodName),
mock(AspectJExpressionPointcut.class), mock(AspectInstanceFactory.class));
advice.setArgumentNamesFromStringArray("arg1", "arg2");
return advice;
}
private Method getMethod(final String methodName) {
return Arrays.stream(Sample.class.getDeclaredMethods())
.filter(method -> method.getName().equals(methodName)).findFirst()
.orElseThrow();
}
@SuppressWarnings("serial")
public static class TestAspectJAdvice extends AbstractAspectJAdvice {
public TestAspectJAdvice(Method aspectJAdviceMethod, AspectJExpressionPointcut pointcut,
AspectInstanceFactory aspectInstanceFactory) {
super(aspectJAdviceMethod, pointcut, aspectInstanceFactory);
}
@Override
public boolean isBeforeAdvice() {
return false;
}
@Override
public boolean isAfterAdvice() {
return false;
}
}
@SuppressWarnings("unused")
static class Sample {
void methodWithNoJoinPoint(String arg1, String arg2) {
}
void methodWithJoinPointAsFirstParameter(JoinPoint joinPoint, String arg1, String arg2) {
}
void methodWithJoinPointAsLastParameter(String arg1, String arg2, JoinPoint joinPoint) {
}
void methodWithJoinPointAsMiddleParameter(String arg1, JoinPoint joinPoint, String arg2) {
}
void methodWithProceedingJoinPoint(ProceedingJoinPoint joinPoint, String arg1, String arg2) {
}
void methodWithStaticPart(JoinPoint.StaticPart staticPart, String arg1, String arg2) {
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -658,6 +658,14 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
growCollectionIfNecessary(list, index, indexedPropertyName.toString(), ph, i + 1);
value = list.get(index);
}
else if (value instanceof Map map) {
Class<?> mapKeyType = ph.getResolvableType().getNested(i + 1).asMap().resolveGeneric(0);
// IMPORTANT: Do not pass full property name in here - property editors
// must not kick in for map keys but rather only for map values.
TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(mapKeyType);
Object convertedMapKey = convertIfNecessary(null, null, key, mapKeyType, typeDescriptor);
value = map.get(convertedMapKey);
}
else if (value instanceof Iterable iterable) {
// Apply index to Iterator in case of a Set/Collection/Iterable.
int index = Integer.parseInt(key);
@@ -685,14 +693,6 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
currIndex + ", accessed using property path '" + propertyName + "'");
}
}
else if (value instanceof Map map) {
Class<?> mapKeyType = ph.getResolvableType().getNested(i + 1).asMap().resolveGeneric(0);
// IMPORTANT: Do not pass full property name in here - property editors
// must not kick in for map keys but rather only for map values.
TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(mapKeyType);
Object convertedMapKey = convertIfNecessary(null, null, key, mapKeyType, typeDescriptor);
value = map.get(convertedMapKey);
}
else {
throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
"Property referenced in indexed property path '" + propertyName +
@@ -1055,6 +1055,11 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
}
}
else {
if (logger.isInfoEnabled()) {
logger.info("Removing alias '" + beanName + "' for bean '" + aliasedName +
"' due to registration of bean definition for bean '" + beanName + "': [" +
beanDefinition + "]");
}
removeAlias(beanName);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -139,6 +139,7 @@ abstract class AbstractPropertyAccessorTests {
assertThat(accessor.isReadableProperty("list")).isTrue();
assertThat(accessor.isReadableProperty("set")).isTrue();
assertThat(accessor.isReadableProperty("map")).isTrue();
assertThat(accessor.isReadableProperty("iterableMap")).isTrue();
assertThat(accessor.isReadableProperty("myTestBeans")).isTrue();
assertThat(accessor.isReadableProperty("xxx")).isFalse();
@@ -146,6 +147,7 @@ abstract class AbstractPropertyAccessorTests {
assertThat(accessor.isWritableProperty("list")).isTrue();
assertThat(accessor.isWritableProperty("set")).isTrue();
assertThat(accessor.isWritableProperty("map")).isTrue();
assertThat(accessor.isWritableProperty("iterableMap")).isTrue();
assertThat(accessor.isWritableProperty("myTestBeans")).isTrue();
assertThat(accessor.isWritableProperty("xxx")).isFalse();
@@ -161,6 +163,14 @@ abstract class AbstractPropertyAccessorTests {
assertThat(accessor.isReadableProperty("map[key4][0].name")).isTrue();
assertThat(accessor.isReadableProperty("map[key4][1]")).isTrue();
assertThat(accessor.isReadableProperty("map[key4][1].name")).isTrue();
assertThat(accessor.isReadableProperty("map[key999]")).isTrue();
assertThat(accessor.isReadableProperty("iterableMap[key1]")).isTrue();
assertThat(accessor.isReadableProperty("iterableMap[key1].name")).isTrue();
assertThat(accessor.isReadableProperty("iterableMap[key2][0]")).isTrue();
assertThat(accessor.isReadableProperty("iterableMap[key2][0].name")).isTrue();
assertThat(accessor.isReadableProperty("iterableMap[key2][1]")).isTrue();
assertThat(accessor.isReadableProperty("iterableMap[key2][1].name")).isTrue();
assertThat(accessor.isReadableProperty("iterableMap[key999]")).isTrue();
assertThat(accessor.isReadableProperty("myTestBeans[0]")).isTrue();
assertThat(accessor.isReadableProperty("myTestBeans[1]")).isFalse();
assertThat(accessor.isReadableProperty("array[key1]")).isFalse();
@@ -177,6 +187,14 @@ abstract class AbstractPropertyAccessorTests {
assertThat(accessor.isWritableProperty("map[key4][0].name")).isTrue();
assertThat(accessor.isWritableProperty("map[key4][1]")).isTrue();
assertThat(accessor.isWritableProperty("map[key4][1].name")).isTrue();
assertThat(accessor.isWritableProperty("map[key999]")).isTrue();
assertThat(accessor.isWritableProperty("iterableMap[key1]")).isTrue();
assertThat(accessor.isWritableProperty("iterableMap[key1].name")).isTrue();
assertThat(accessor.isWritableProperty("iterableMap[key2][0]")).isTrue();
assertThat(accessor.isWritableProperty("iterableMap[key2][0].name")).isTrue();
assertThat(accessor.isWritableProperty("iterableMap[key2][1]")).isTrue();
assertThat(accessor.isWritableProperty("iterableMap[key2][1].name")).isTrue();
assertThat(accessor.isWritableProperty("iterableMap[key999]")).isTrue();
assertThat(accessor.isReadableProperty("myTestBeans[0]")).isTrue();
assertThat(accessor.isReadableProperty("myTestBeans[1]")).isFalse();
assertThat(accessor.isWritableProperty("array[key1]")).isFalse();
@@ -1394,6 +1412,9 @@ abstract class AbstractPropertyAccessorTests {
assertThat(accessor.getPropertyValue("map[key5[foo]].name")).isEqualTo("name8");
assertThat(accessor.getPropertyValue("map['key5[foo]'].name")).isEqualTo("name8");
assertThat(accessor.getPropertyValue("map[\"key5[foo]\"].name")).isEqualTo("name8");
assertThat(accessor.getPropertyValue("iterableMap[key1].name")).isEqualTo("nameC");
assertThat(accessor.getPropertyValue("iterableMap[key2][0].name")).isEqualTo("nameA");
assertThat(accessor.getPropertyValue("iterableMap[key2][1].name")).isEqualTo("nameB");
assertThat(accessor.getPropertyValue("myTestBeans[0].name")).isEqualTo("nameZ");
MutablePropertyValues pvs = new MutablePropertyValues();
@@ -1408,6 +1429,9 @@ abstract class AbstractPropertyAccessorTests {
pvs.add("map[key4][0].name", "nameA");
pvs.add("map[key4][1].name", "nameB");
pvs.add("map[key5[foo]].name", "name10");
pvs.add("iterableMap[key1].name", "newName1");
pvs.add("iterableMap[key2][0].name", "newName2A");
pvs.add("iterableMap[key2][1].name", "newName2B");
pvs.add("myTestBeans[0].name", "nameZZ");
accessor.setPropertyValues(pvs);
assertThat(tb0.getName()).isEqualTo("name5");
@@ -1427,6 +1451,9 @@ abstract class AbstractPropertyAccessorTests {
assertThat(accessor.getPropertyValue("map[key4][0].name")).isEqualTo("nameA");
assertThat(accessor.getPropertyValue("map[key4][1].name")).isEqualTo("nameB");
assertThat(accessor.getPropertyValue("map[key5[foo]].name")).isEqualTo("name10");
assertThat(accessor.getPropertyValue("iterableMap[key1].name")).isEqualTo("newName1");
assertThat(accessor.getPropertyValue("iterableMap[key2][0].name")).isEqualTo("newName2A");
assertThat(accessor.getPropertyValue("iterableMap[key2][1].name")).isEqualTo("newName2B");
assertThat(accessor.getPropertyValue("myTestBeans[0].name")).isEqualTo("nameZZ");
}
@@ -885,10 +885,15 @@ class DefaultListableBeanFactoryTests {
@Test
void beanDefinitionOverriding() {
lbf.registerBeanDefinition("test", new RootBeanDefinition(TestBean.class));
// Override "test" bean definition.
lbf.registerBeanDefinition("test", new RootBeanDefinition(NestedTestBean.class));
// Temporary "test2" alias for nonexistent bean.
lbf.registerAlias("otherTest", "test2");
// Reassign "test2" alias to "test".
lbf.registerAlias("test", "test2");
// Assign "testX" alias to "test" as well.
lbf.registerAlias("test", "testX");
// Register new "testX" bean definition which also removes the "testX" alias for "test".
lbf.registerBeanDefinition("testX", new RootBeanDefinition(TestBean.class));
assertThat(lbf.getBean("test")).isInstanceOf(NestedTestBean.class);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -49,6 +50,8 @@ public class IndexedTestBean {
private SortedMap sortedMap;
private IterableMap iterableMap;
private MyTestBeans myTestBeans;
@@ -73,6 +76,9 @@ public class IndexedTestBean {
TestBean tb6 = new TestBean("name6", 0);
TestBean tb7 = new TestBean("name7", 0);
TestBean tb8 = new TestBean("name8", 0);
TestBean tbA = new TestBean("nameA", 0);
TestBean tbB = new TestBean("nameB", 0);
TestBean tbC = new TestBean("nameC", 0);
TestBean tbX = new TestBean("nameX", 0);
TestBean tbY = new TestBean("nameY", 0);
TestBean tbZ = new TestBean("nameZ", 0);
@@ -88,6 +94,12 @@ public class IndexedTestBean {
this.map.put("key2", tb5);
this.map.put("key.3", tb5);
List list = new ArrayList();
list.add(tbA);
list.add(tbB);
this.iterableMap = new IterableMap<>();
this.iterableMap.put("key1", tbC);
this.iterableMap.put("key2", list);
list = new ArrayList();
list.add(tbX);
list.add(tbY);
this.map.put("key4", list);
@@ -152,6 +164,14 @@ public class IndexedTestBean {
this.sortedMap = sortedMap;
}
public IterableMap getIterableMap() {
return this.iterableMap;
}
public void setIterableMap(IterableMap iterableMap) {
this.iterableMap = iterableMap;
}
public MyTestBeans getMyTestBeans() {
return myTestBeans;
}
@@ -161,6 +181,15 @@ public class IndexedTestBean {
}
@SuppressWarnings("serial")
public static class IterableMap<K,V> extends LinkedHashMap<K,V> implements Iterable<V> {
@Override
public Iterator<V> iterator() {
return values().iterator();
}
}
public static class MyTestBeans implements Iterable<TestBean> {
private final Collection<TestBean> testBeans;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 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.
@@ -17,6 +17,7 @@
package org.springframework.context.annotation;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
@@ -33,6 +34,7 @@ import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.core.annotation.AliasFor;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotation.Adapt;
@@ -41,6 +43,7 @@ import org.springframework.core.type.AnnotationMetadata;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
@@ -147,16 +150,26 @@ public class AnnotationBeanNameGenerator implements BeanNameGenerator {
Set<String> metaAnnotationTypes = this.metaAnnotationTypesCache.computeIfAbsent(annotationType,
key -> getMetaAnnotationTypes(mergedAnnotation));
if (isStereotypeWithNameValue(annotationType, metaAnnotationTypes, attributes)) {
Object value = attributes.get("value");
Object value = attributes.get(MergedAnnotation.VALUE);
if (value instanceof String currentName && !currentName.isBlank()) {
if (conventionBasedStereotypeCheckCache.add(annotationType) &&
metaAnnotationTypes.contains(COMPONENT_ANNOTATION_CLASSNAME) && logger.isWarnEnabled()) {
logger.warn("""
Support for convention-based stereotype names is deprecated and will \
be removed in a future version of the framework. Please annotate the \
'value' attribute in @%s with @AliasFor(annotation=Component.class) \
to declare an explicit alias for @Component's 'value' attribute."""
.formatted(annotationType));
if (hasExplicitlyAliasedValueAttribute(mergedAnnotation.getType())) {
logger.warn("""
Although the 'value' attribute in @%s declares @AliasFor for an attribute \
other than @Component's 'value' attribute, the value is still used as the \
@Component name based on convention. As of Spring Framework 7.0, such a \
'value' attribute will no longer be used as the @Component name."""
.formatted(annotationType));
}
else {
logger.warn("""
Support for convention-based @Component names is deprecated and will \
be removed in a future version of the framework. Please annotate the \
'value' attribute in @%s with @AliasFor(annotation=Component.class) \
to declare an explicit alias for @Component's 'value' attribute."""
.formatted(annotationType));
}
}
if (beanName != null && !currentName.equals(beanName)) {
throw new IllegalStateException("Stereotype annotations suggest inconsistent " +
@@ -224,7 +237,7 @@ public class AnnotationBeanNameGenerator implements BeanNameGenerator {
annotationType.equals("jakarta.inject.Named") ||
annotationType.equals("javax.inject.Named");
return (isStereotype && attributes.containsKey("value"));
return (isStereotype && attributes.containsKey(MergedAnnotation.VALUE));
}
/**
@@ -255,4 +268,14 @@ public class AnnotationBeanNameGenerator implements BeanNameGenerator {
return StringUtils.uncapitalizeAsProperty(shortClassName);
}
/**
* Determine if the supplied annotation type declares a {@code value()} attribute
* with an explicit alias configured via {@link AliasFor @AliasFor}.
* @since 6.2.3
*/
private static boolean hasExplicitlyAliasedValueAttribute(Class<? extends Annotation> annotationType) {
Method valueAttribute = ReflectionUtils.findMethod(annotationType, MergedAnnotation.VALUE);
return (valueAttribute != null && valueAttribute.isAnnotationPresent(AliasFor.class));
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 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.
@@ -565,9 +565,10 @@ public class ClassPathScanningCandidateComponentProvider implements EnvironmentC
}
/**
* Determine whether the given bean definition qualifies as candidate.
* <p>The default implementation checks whether the class is not an interface
* and not dependent on an enclosing class.
* Determine whether the given bean definition qualifies as a candidate component.
* <p>The default implementation checks whether the class is not dependent on an
* enclosing class as well as whether the class is either concrete (and therefore
* not an interface) or has {@link Lookup @Lookup} methods.
* <p>Can be overridden in subclasses.
* @param beanDefinition the bean definition to check
* @return whether the bean definition qualifies as a candidate component
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -39,7 +39,6 @@ import org.springframework.beans.factory.support.AbstractBeanDefinitionReader;
import org.springframework.beans.factory.support.BeanDefinitionReader;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.context.annotation.ConfigurationCondition.ConfigurationPhase;
@@ -318,7 +317,7 @@ class ConfigurationClassBeanDefinitionReader {
// At this point, it's a top-level override (probably XML), just having been parsed
// before configuration class processing kicks in...
if (this.registry instanceof DefaultListableBeanFactory dlbf && !dlbf.isBeanDefinitionOverridable(beanName)) {
if (!this.registry.isBeanDefinitionOverridable(beanName)) {
throw new BeanDefinitionStoreException(beanMethod.getConfigurationClass().getResource().getDescription(),
beanName, "@Bean definition illegally overridden by existing bean definition: " + existingBeanDef);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -109,8 +109,16 @@ class ConfigurationClassEnhancer {
}
return configClass;
}
try {
Class<?> enhancedClass = createClass(newEnhancer(configClass, classLoader));
// Use original ClassLoader if config class not locally loaded in overriding class loader
if (classLoader instanceof SmartClassLoader smartClassLoader &&
classLoader != configClass.getClassLoader()) {
classLoader = smartClassLoader.getOriginalClassLoader();
}
Enhancer enhancer = newEnhancer(configClass, classLoader);
boolean classLoaderMismatch = (classLoader != null && classLoader != configClass.getClassLoader());
Class<?> enhancedClass = createClass(enhancer, classLoaderMismatch);
if (logger.isTraceEnabled()) {
logger.trace(String.format("Successfully enhanced %s; enhanced class name is: %s",
configClass.getName(), enhancedClass.getName()));
@@ -129,6 +137,9 @@ class ConfigurationClassEnhancer {
*/
private Enhancer newEnhancer(Class<?> configSuperClass, @Nullable ClassLoader classLoader) {
Enhancer enhancer = new Enhancer();
if (classLoader != null) {
enhancer.setClassLoader(classLoader);
}
enhancer.setSuperclass(configSuperClass);
enhancer.setInterfaces(new Class<?>[] {EnhancedConfiguration.class});
enhancer.setUseFactory(false);
@@ -152,8 +163,21 @@ class ConfigurationClassEnhancer {
* Uses enhancer to generate a subclass of superclass,
* ensuring that callbacks are registered for the new subclass.
*/
private Class<?> createClass(Enhancer enhancer) {
Class<?> subclass = enhancer.createClass();
private Class<?> createClass(Enhancer enhancer, boolean fallback) {
Class<?> subclass;
try {
subclass = enhancer.createClass();
}
catch (CodeGenerationException ex) {
if (!fallback) {
throw ex;
}
// Possibly a package-visible @Bean method declaration not accessible
// in the given ClassLoader -> retry with original ClassLoader
enhancer.setClassLoader(null);
subclass = enhancer.createClass();
}
// Registering callbacks statically (as opposed to thread-local)
// is critical for usage in an OSGi environment (SPR-5932)...
Enhancer.registerStaticCallbacks(subclass, CALLBACKS);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -317,8 +317,16 @@ final class QuartzCronField extends CronField {
private static TemporalAdjuster dayOfWeekInMonth(int ordinal, DayOfWeek dayOfWeek) {
TemporalAdjuster adjuster = TemporalAdjusters.dayOfWeekInMonth(ordinal, dayOfWeek);
return temporal -> {
Temporal result = adjuster.adjustInto(temporal);
return rollbackToMidnight(temporal, result);
// TemporalAdjusters can overflow to a different month
// in this case, attempt the same adjustment with the next/previous month
for (int i = 0; i < 12; i++) {
Temporal result = adjuster.adjustInto(temporal);
if (result.get(ChronoField.MONTH_OF_YEAR) == temporal.get(ChronoField.MONTH_OF_YEAR)) {
return rollbackToMidnight(temporal, result);
}
temporal = result;
}
return null;
};
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
package org.springframework.validation.beanvalidation;
import jakarta.validation.ValidationException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
@@ -39,7 +40,13 @@ public class OptionalValidatorFactoryBean extends LocalValidatorFactoryBean {
super.afterPropertiesSet();
}
catch (ValidationException ex) {
LogFactory.getLog(getClass()).debug("Failed to set up a Bean Validation provider", ex);
Log logger = LogFactory.getLog(getClass());
if (logger.isDebugEnabled()) {
logger.debug("Failed to set up a Bean Validation provider", ex);
}
else if (logger.isInfoEnabled()) {
logger.info("Failed to set up a Bean Validation provider: " + ex);
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -168,6 +168,25 @@ class AnnotationBeanNameGeneratorTests {
assertGeneratedName(RestControllerAdviceClass.class, "myRestControllerAdvice");
}
@Test // gh-34317
void generateBeanNameFromStereotypeAnnotationWithStringValueAsExplicitAliasForMetaAnnotationOtherThanComponent() {
// As of Spring Framework 6.2, "enigma" is incorrectly used as the @Component name.
// As of Spring Framework 7.0, the generated name will be "annotationBeanNameGeneratorTests.StereotypeWithoutExplicitName".
assertGeneratedName(StereotypeWithoutExplicitName.class, "enigma");
}
@Test // gh-34317
void generateBeanNameFromStereotypeAnnotationWithStringValueAndExplicitAliasForComponentNameWithBlankName() {
// As of Spring Framework 6.2, "enigma" is incorrectly used as the @Component name.
// As of Spring Framework 7.0, the generated name will be "annotationBeanNameGeneratorTests.StereotypeWithGeneratedName".
assertGeneratedName(StereotypeWithGeneratedName.class, "enigma");
}
@Test // gh-34317
void generateBeanNameFromStereotypeAnnotationWithStringValueAndExplicitAliasForComponentName() {
assertGeneratedName(StereotypeWithExplicitName.class, "explicitName");
}
private void assertGeneratedName(Class<?> clazz, String expectedName) {
BeanDefinition bd = annotatedBeanDef(clazz);
@@ -210,7 +229,7 @@ class AnnotationBeanNameGeneratorTests {
@Retention(RetentionPolicy.RUNTIME)
@Component
@interface ConventionBasedComponent1 {
// This intentionally convention-based. Please do not add @AliasFor.
// This is intentionally convention-based. Please do not add @AliasFor.
// See gh-31093.
String value() default "";
}
@@ -218,7 +237,7 @@ class AnnotationBeanNameGeneratorTests {
@Retention(RetentionPolicy.RUNTIME)
@Component
@interface ConventionBasedComponent2 {
// This intentionally convention-based. Please do not add @AliasFor.
// This is intentionally convention-based. Please do not add @AliasFor.
// See gh-31093.
String value() default "";
}
@@ -260,7 +279,7 @@ class AnnotationBeanNameGeneratorTests {
@Target(ElementType.TYPE)
@Controller
@interface TestRestController {
// This intentionally convention-based. Please do not add @AliasFor.
// This is intentionally convention-based. Please do not add @AliasFor.
// See gh-31093.
String value() default "";
}
@@ -319,7 +338,6 @@ class AnnotationBeanNameGeneratorTests {
String[] basePackages() default {};
}
@TestControllerAdvice(basePackages = "com.example", name = "myControllerAdvice")
static class ControllerAdviceClass {
}
@@ -328,4 +346,56 @@ class AnnotationBeanNameGeneratorTests {
static class RestControllerAdviceClass {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
@interface MetaAnnotationWithStringAttribute {
String attribute() default "";
}
/**
* Custom stereotype annotation which has a {@code String value} attribute that
* is explicitly declared as an alias for an attribute in a meta-annotation
* other than {@link Component @Component}.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Component
@MetaAnnotationWithStringAttribute
@interface MyStereotype {
@AliasFor(annotation = MetaAnnotationWithStringAttribute.class, attribute = "attribute")
String value() default "";
}
@MyStereotype("enigma")
static class StereotypeWithoutExplicitName {
}
/**
* Custom stereotype annotation which is identical to {@link MyStereotype @MyStereotype}
* except that it has a {@link #name} attribute that is an explicit alias for
* {@link Component#value}.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Component
@MetaAnnotationWithStringAttribute
@interface MyNamedStereotype {
@AliasFor(annotation = MetaAnnotationWithStringAttribute.class, attribute = "attribute")
String value() default "";
@AliasFor(annotation = Component.class, attribute = "value")
String name() default "";
}
@MyNamedStereotype(value = "enigma", name ="explicitName")
static class StereotypeWithExplicitName {
}
@MyNamedStereotype(value = "enigma")
static class StereotypeWithGeneratedName {
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -18,11 +18,16 @@ package org.springframework.context.annotation;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.ProtectionDomain;
import java.security.SecureClassLoader;
import org.junit.jupiter.api.Test;
import org.springframework.core.OverridingClassLoader;
import org.springframework.core.SmartClassLoader;
import org.springframework.lang.Nullable;
import org.springframework.util.StreamUtils;
import static org.assertj.core.api.Assertions.assertThat;
@@ -36,19 +41,108 @@ class ConfigurationClassEnhancerTests {
@Test
void enhanceReloadedClass() throws Exception {
ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer();
ClassLoader parentClassLoader = getClass().getClassLoader();
CustomClassLoader classLoader = new CustomClassLoader(parentClassLoader);
ClassLoader classLoader = new CustomSmartClassLoader(parentClassLoader);
Class<?> myClass = parentClassLoader.loadClass(MyConfig.class.getName());
configurationClassEnhancer.enhance(myClass, parentClassLoader);
Class<?> myReloadedClass = classLoader.loadClass(MyConfig.class.getName());
Class<?> enhancedReloadedClass = configurationClassEnhancer.enhance(myReloadedClass, classLoader);
assertThat(enhancedReloadedClass.getClassLoader()).isEqualTo(classLoader);
Class<?> enhancedClass = configurationClassEnhancer.enhance(myClass, parentClassLoader);
assertThat(myClass).isAssignableFrom(enhancedClass);
myClass = classLoader.loadClass(MyConfig.class.getName());
enhancedClass = configurationClassEnhancer.enhance(myClass, classLoader);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader);
assertThat(myClass).isAssignableFrom(enhancedClass);
}
@Test
void withPublicClass() {
ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer();
ClassLoader classLoader = new URLClassLoader(new URL[0], getClass().getClassLoader());
Class<?> enhancedClass = configurationClassEnhancer.enhance(MyConfigWithPublicClass.class, classLoader);
assertThat(MyConfigWithPublicClass.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader);
classLoader = new OverridingClassLoader(getClass().getClassLoader());
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithPublicClass.class, classLoader);
assertThat(MyConfigWithPublicClass.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
classLoader = new CustomSmartClassLoader(getClass().getClassLoader());
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithPublicClass.class, classLoader);
assertThat(MyConfigWithPublicClass.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
classLoader = new BasicSmartClassLoader(getClass().getClassLoader());
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithPublicClass.class, classLoader);
assertThat(MyConfigWithPublicClass.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
}
@Test
void withNonPublicClass() {
ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer();
ClassLoader classLoader = new URLClassLoader(new URL[0], getClass().getClassLoader());
Class<?> enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicClass.class, classLoader);
assertThat(MyConfigWithNonPublicClass.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
classLoader = new OverridingClassLoader(getClass().getClassLoader());
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicClass.class, classLoader);
assertThat(MyConfigWithNonPublicClass.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
classLoader = new CustomSmartClassLoader(getClass().getClassLoader());
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicClass.class, classLoader);
assertThat(MyConfigWithNonPublicClass.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
classLoader = new BasicSmartClassLoader(getClass().getClassLoader());
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicClass.class, classLoader);
assertThat(MyConfigWithNonPublicClass.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
}
@Test
void withNonPublicMethod() {
ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer();
ClassLoader classLoader = new URLClassLoader(new URL[0], getClass().getClassLoader());
Class<?> enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader);
assertThat(MyConfigWithNonPublicMethod.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader);
classLoader = new OverridingClassLoader(getClass().getClassLoader());
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader);
assertThat(MyConfigWithNonPublicMethod.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
classLoader = new CustomSmartClassLoader(getClass().getClassLoader());
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader);
assertThat(MyConfigWithNonPublicMethod.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
classLoader = new BasicSmartClassLoader(getClass().getClassLoader());
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader);
assertThat(MyConfigWithNonPublicMethod.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
}
@Configuration
static class MyConfig {
@Bean
String myBean() {
return "bean";
}
}
@Configuration
public static class MyConfigWithPublicClass {
@Bean
public String myBean() {
return "bean";
@@ -56,9 +150,29 @@ class ConfigurationClassEnhancerTests {
}
static class CustomClassLoader extends SecureClassLoader implements SmartClassLoader {
@Configuration
static class MyConfigWithNonPublicClass {
CustomClassLoader(ClassLoader parent) {
@Bean
public String myBean() {
return "bean";
}
}
@Configuration
public static class MyConfigWithNonPublicMethod {
@Bean
String myBean() {
return "bean";
}
}
static class CustomSmartClassLoader extends SecureClassLoader implements SmartClassLoader {
CustomSmartClassLoader(ClassLoader parent) {
super(parent);
}
@@ -82,6 +196,29 @@ class ConfigurationClassEnhancerTests {
public boolean isClassReloadable(Class<?> clazz) {
return clazz.getName().contains("MyConfig");
}
@Override
public ClassLoader getOriginalClassLoader() {
return getParent();
}
@Override
public Class<?> publicDefineClass(String name, byte[] b, @Nullable ProtectionDomain protectionDomain) {
return defineClass(name, b, 0, b.length, protectionDomain);
}
}
static class BasicSmartClassLoader extends SecureClassLoader implements SmartClassLoader {
BasicSmartClassLoader(ClassLoader parent) {
super(parent);
}
@Override
public Class<?> publicDefineClass(String name, byte[] b, @Nullable ProtectionDomain protectionDomain) {
return defineClass(name, b, 0, b.length, protectionDomain);
}
}
}
@@ -209,18 +209,10 @@ abstract class AbstractSchedulingTaskExecutorTests {
CompletableFuture<?> future1 = executor.submitCompletable(new TestTask(this.testName, -1));
CompletableFuture<?> future2 = executor.submitCompletable(new TestTask(this.testName, -1));
shutdownExecutor();
try {
assertThatExceptionOfType(TimeoutException.class).isThrownBy(() -> {
future1.get(1000, TimeUnit.MILLISECONDS);
}
catch (Exception ex) {
// ignore
}
Awaitility.await()
.atMost(5, TimeUnit.SECONDS)
.pollInterval(10, TimeUnit.MILLISECONDS)
.untilAsserted(() -> assertThatExceptionOfType(TimeoutException.class)
.isThrownBy(() -> future2.get(1000, TimeUnit.MILLISECONDS)));
future2.get(1000, TimeUnit.MILLISECONDS);
});
}
@Test
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -39,6 +39,7 @@ import static java.time.temporal.TemporalAdjusters.next;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link CronExpression}.
* @author Arjen Poutsma
*/
class CronExpressionTests {
@@ -1092,6 +1093,20 @@ class CronExpressionTests {
assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY);
}
@Test
void quartz5thMondayOfTheMonthDayName() {
CronExpression expression = CronExpression.parse("0 0 0 ? * MON#5");
LocalDateTime last = LocalDateTime.of(2025, 1, 1, 0, 0, 0);
// first occurrence of 5 mondays in a month from last
LocalDateTime expected = LocalDateTime.of(2025, 3, 31, 0, 0, 0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(MONDAY);
}
@Test
void quartzFifthWednesdayOfTheMonth() {
CronExpression expression = CronExpression.parse("0 0 0 ? * 3#5");
@@ -309,12 +309,12 @@ public class ResolvableType implements Serializable {
// Deal with wildcard bounds
WildcardBounds ourBounds = WildcardBounds.get(this);
WildcardBounds typeBounds = WildcardBounds.get(other);
WildcardBounds otherBounds = WildcardBounds.get(other);
// In the form X is assignable to <? extends Number>
if (typeBounds != null) {
return (ourBounds != null && ourBounds.isSameKind(typeBounds) &&
ourBounds.isAssignableFrom(typeBounds.getBounds()));
if (otherBounds != null) {
return (ourBounds != null && ourBounds.isSameKind(otherBounds) &&
ourBounds.isAssignableFrom(otherBounds.getBounds()));
}
// In the form <? extends Number> is assignable to X...
@@ -365,8 +365,8 @@ public class ResolvableType implements Serializable {
if (checkGenerics) {
// Recursively check each generic
ResolvableType[] ourGenerics = getGenerics();
ResolvableType[] typeGenerics = other.as(ourResolved).getGenerics();
if (ourGenerics.length != typeGenerics.length) {
ResolvableType[] otherGenerics = other.as(ourResolved).getGenerics();
if (ourGenerics.length != otherGenerics.length) {
return false;
}
if (ourGenerics.length > 0) {
@@ -375,7 +375,7 @@ public class ResolvableType implements Serializable {
}
matchedBefore.put(this.type, other.type);
for (int i = 0; i < ourGenerics.length; i++) {
if (!ourGenerics[i].isAssignableFrom(typeGenerics[i], true, matchedBefore)) {
if (!ourGenerics[i].isAssignableFrom(otherGenerics[i], true, matchedBefore)) {
return false;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -51,10 +51,11 @@ public class FlightRecorderApplicationStartup implements ApplicationStartup {
@Override
public StartupStep start(String name) {
Long parentId = this.currentSteps.getFirst();
long sequenceId = this.currentSequenceId.incrementAndGet();
this.currentSteps.offerFirst(sequenceId);
return new FlightRecorderStartupStep(sequenceId, name,
this.currentSteps.getFirst(), committedStep -> this.currentSteps.removeFirstOccurrence(sequenceId));
parentId, committedStep -> this.currentSteps.removeFirstOccurrence(sequenceId));
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -169,7 +169,7 @@ class ResolvableTypeTests {
@Test
void forInstanceProviderNull() {
ResolvableType type = ResolvableType.forInstance(new MyGenericInterfaceType<String>(null));
ResolvableType type = ResolvableType.forInstance(new MyGenericInterfaceType<>(null));
assertThat(type.getType()).isEqualTo(MyGenericInterfaceType.class);
assertThat(type.resolve()).isEqualTo(MyGenericInterfaceType.class);
}
@@ -1177,6 +1177,20 @@ class ResolvableTypeTests {
assertThatResolvableType(complex4).isNotAssignableFrom(complex3);
}
@Test
void isAssignableFromForUnresolvedWildcards() {
ResolvableType wildcard = ResolvableType.forInstance(new Wildcard<>());
ResolvableType wildcardFixed = ResolvableType.forInstance(new WildcardFixed());
ResolvableType wildcardConcrete = ResolvableType.forClassWithGenerics(Wildcard.class, Number.class);
assertThat(wildcard.isAssignableFrom(wildcardFixed)).isTrue();
assertThat(wildcard.isAssignableFrom(wildcardConcrete)).isTrue();
assertThat(wildcardFixed.isAssignableFrom(wildcard)).isFalse();
assertThat(wildcardFixed.isAssignableFrom(wildcardConcrete)).isFalse();
assertThat(wildcardConcrete.isAssignableFrom(wildcard)).isTrue();
assertThat(wildcardConcrete.isAssignableFrom(wildcardFixed)).isFalse();
}
@Test
void identifyTypeVariable() throws Exception {
Method method = ClassArguments.class.getMethod("typedArgumentFirst", Class.class, Class.class, Class.class);
@@ -1574,7 +1588,6 @@ class ResolvableTypeTests {
}
}
public class MySimpleInterfaceType implements MyInterfaceType<String> {
}
@@ -1584,7 +1597,6 @@ class ResolvableTypeTests {
public abstract class ExtendsMySimpleInterfaceTypeWithImplementsRaw extends MySimpleInterfaceTypeWithImplementsRaw {
}
public class MyCollectionInterfaceType implements MyInterfaceType<Collection<String>> {
}
@@ -1592,20 +1604,17 @@ class ResolvableTypeTests {
public abstract class MySuperclassType<T> {
}
public class MySimpleSuperclassType extends MySuperclassType<String> {
}
public class MyCollectionSuperclassType extends MySuperclassType<Collection<String>> {
}
interface Wildcard<T extends Number> extends List<T> {
public class Wildcard<T extends Number> {
}
interface RawExtendsWildcard extends Wildcard {
public class WildcardFixed extends Wildcard<Integer> {
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -156,6 +156,23 @@ class PropertyPlaceholderHelperTests {
);
}
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("exactMatchPlaceholders")
void placeholdersWithExactMatchAreConsidered(String text, String expected) {
Properties properties = new Properties();
properties.setProperty("prefix://my-service", "example-service");
properties.setProperty("px", "prefix");
properties.setProperty("p1", "${prefix://my-service}");
assertThat(this.helper.replacePlaceholders(text, properties)).isEqualTo(expected);
}
static Stream<Arguments> exactMatchPlaceholders() {
return Stream.of(
Arguments.of("${prefix://my-service}", "example-service"),
Arguments.of("${p1}", "example-service")
);
}
}
PlaceholderResolver mockPlaceholderResolver(String... pairs) {
@@ -226,8 +226,9 @@ public class FunctionReference extends SpelNodeImpl {
ReflectionHelper.convertAllMethodHandleArguments(converter, functionArgs, methodHandle, varArgPosition);
if (isSuspectedVarargs) {
if (declaredParamCount == 1) {
// We only repackage the varargs if it is the ONLY argument -- for example,
if (declaredParamCount == 1 && !methodHandle.isVarargsCollector()) {
// We only repackage the arguments if the MethodHandle accepts a single
// argument AND the MethodHandle is not a "varargs collector" -- for example,
// when we are dealing with a bound MethodHandle.
functionArgs = ReflectionHelper.setupArgumentsForVarargsInvocation(
methodHandle.type().parameterArray(), functionArgs);
@@ -36,6 +36,8 @@ import static org.assertj.core.api.Assertions.assertThatException;
* Tests invocation of constructors.
*
* @author Andy Clement
* @see MethodInvocationTests
* @see VariableAndFunctionTests
*/
class ConstructorInvocationTests extends AbstractExpressionTests {
@@ -46,6 +46,8 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
* @author Andy Clement
* @author Phillip Webb
* @author Sam Brannen
* @see ConstructorInvocationTests
* @see VariableAndFunctionTests
*/
class MethodInvocationTests extends AbstractExpressionTests {
@@ -597,7 +597,8 @@ class SpelDocumentationTests extends AbstractExpressionTests {
MethodHandle methodHandle = MethodHandles.lookup().findVirtual(String.class, "formatted",
MethodType.methodType(String.class, Object[].class))
.bindTo(template)
.bindTo(varargs); // here we have to provide arguments in a single array binding
// Here we have to provide the arguments in a single array binding:
.bindTo(varargs);
context.registerFunction("message", methodHandle);
String message = parser.parseExpression("#message()").getValue(context, String.class);
@@ -108,11 +108,16 @@ class TestScenarioCreator {
"formatObjectVarargs", MethodType.methodType(String.class, String.class, Object[].class));
testContext.registerFunction("formatObjectVarargs", formatObjectVarargs);
// #formatObjectVarargs(format, args...)
// #formatPrimitiveVarargs(format, args...)
MethodHandle formatPrimitiveVarargs = MethodHandles.lookup().findStatic(TestScenarioCreator.class,
"formatPrimitiveVarargs", MethodType.methodType(String.class, String.class, int[].class));
testContext.registerFunction("formatPrimitiveVarargs", formatPrimitiveVarargs);
// #varargsFunctionHandle(args...)
MethodHandle varargsFunctionHandle = MethodHandles.lookup().findStatic(TestScenarioCreator.class,
"varargsFunction", MethodType.methodType(String.class, String[].class));
testContext.registerFunction("varargsFunctionHandle", varargsFunctionHandle);
// #add(int, int)
MethodHandle add = MethodHandles.lookup().findStatic(TestScenarioCreator.class,
"add", MethodType.methodType(int.class, int.class, int.class));
@@ -32,6 +32,8 @@ import static org.springframework.expression.spel.SpelMessage.INCORRECT_NUMBER_O
*
* @author Andy Clement
* @author Sam Brannen
* @see ConstructorInvocationTests
* @see MethodInvocationTests
*/
class VariableAndFunctionTests extends AbstractExpressionTests {
@@ -77,6 +79,8 @@ class VariableAndFunctionTests extends AbstractExpressionTests {
@Test
void functionWithVarargs() {
// static String varargsFunction(String... strings) -> Arrays.toString(strings)
evaluate("#varargsFunction()", "[]", String.class);
evaluate("#varargsFunction(new String[0])", "[]", String.class);
evaluate("#varargsFunction('a')", "[a]", String.class);
@@ -239,6 +243,27 @@ class VariableAndFunctionTests extends AbstractExpressionTests {
evaluate("#formatObjectVarargs('x -> %s %s %s', {'a', 'b', 'c'})", expected, String.class);
}
@Test // gh-34109
void functionViaMethodHandleForStaticMethodThatAcceptsOnlyVarargs() {
// #varargsFunctionHandle: static String varargsFunction(String... strings) -> Arrays.toString(strings)
evaluate("#varargsFunctionHandle()", "[]", String.class);
evaluate("#varargsFunctionHandle(new String[0])", "[]", String.class);
evaluate("#varargsFunctionHandle('a')", "[a]", String.class);
evaluate("#varargsFunctionHandle('a','b','c')", "[a, b, c]", String.class);
evaluate("#varargsFunctionHandle(new String[]{'a','b','c'})", "[a, b, c]", String.class);
// Conversion from int to String
evaluate("#varargsFunctionHandle(25)", "[25]", String.class);
evaluate("#varargsFunctionHandle('b',25)", "[b, 25]", String.class);
evaluate("#varargsFunctionHandle(new int[]{1, 2, 3})", "[1, 2, 3]", String.class);
// Strings that contain a comma
evaluate("#varargsFunctionHandle('a,b')", "[a,b]", String.class);
evaluate("#varargsFunctionHandle('a', 'x,y', 'd')", "[a, x,y, d]", String.class);
// null values
evaluate("#varargsFunctionHandle(null)", "[null]", String.class);
evaluate("#varargsFunctionHandle('a',null,'b')", "[a, null, b]", String.class);
}
@Test
void functionMethodMustBeStatic() throws Exception {
SpelExpressionParser parser = new SpelExpressionParser();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -22,9 +22,7 @@ import org.springframework.messaging.Message;
/**
* Convenient base class for {@link AsyncHandlerMethodReturnValueHandler}
* implementations that support only asynchronous (Future-like) return values
* and merely serve as adapters of such types to Spring's
* {@link org.springframework.util.concurrent.ListenableFuture ListenableFuture}.
* implementations that support only asynchronous (Future-like) return values.
*
* @author Sebastien Deleuze
* @since 4.2
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -572,7 +572,7 @@ public abstract class AbstractMethodMessageHandler<T>
if (returnValue != null && this.returnValueHandlers.isAsyncReturnValue(returnValue, returnType)) {
CompletableFuture<?> future = this.returnValueHandlers.toCompletableFuture(returnValue, returnType);
if (future != null) {
future.whenComplete(new ReturnValueListenableFutureCallback(invocable, message));
future.whenComplete(new ReturnValueCallback(invocable, message));
}
}
else {
@@ -703,13 +703,13 @@ public abstract class AbstractMethodMessageHandler<T>
}
private class ReturnValueListenableFutureCallback implements BiConsumer<Object, Throwable> {
private class ReturnValueCallback implements BiConsumer<Object, Throwable> {
private final InvocableHandlerMethod handlerMethod;
private final Message<?> message;
public ReturnValueListenableFutureCallback(InvocableHandlerMethod handlerMethod, Message<?> message) {
public ReturnValueCallback(InvocableHandlerMethod handlerMethod, Message<?> message) {
this.handlerMethod = handlerMethod;
this.message = message;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -24,8 +24,6 @@ import org.springframework.lang.Nullable;
/**
* An extension of {@link HandlerMethodReturnValueHandler} for handling async,
* Future-like return value types that support success and error callbacks.
* Essentially anything that can be adapted to a
* {@link org.springframework.util.concurrent.ListenableFuture ListenableFuture}.
*
* <p>Implementations should consider extending the convenient base class
* {@link AbstractAsyncReturnValueHandler}.
@@ -71,6 +69,7 @@ public interface AsyncHandlerMethodReturnValueHandler extends HandlerMethodRetur
@Nullable
default org.springframework.util.concurrent.ListenableFuture<?> toListenableFuture(
Object returnValue, MethodParameter returnType) {
CompletableFuture<?> result = toCompletableFuture(returnValue, returnType);
return (result != null ?
new org.springframework.util.concurrent.CompletableToListenableFutureAdapter<>(result) :
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,7 +20,6 @@ import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.logging.Log;
@@ -280,12 +279,10 @@ public class UserDestinationMessageHandler implements MessageHandler, SmartLifec
return this.messagingTemplate;
}
public void send(UserDestinationResult destinationResult, Message<?> message) throws MessagingException {
Set<String> sessionIds = destinationResult.getSessionIds();
Iterator<String> itr = (sessionIds != null ? sessionIds.iterator() : null);
for (String target : destinationResult.getTargetDestinations()) {
String sessionId = (itr != null ? itr.next() : null);
public void send(UserDestinationResult result, Message<?> message) throws MessagingException {
Iterator<String> itr = result.getSessionIds().iterator();
for (String target : result.getTargetDestinations()) {
String sessionId = (itr.hasNext() ? itr.next() : null);
getTemplateToUse(sessionId).send(target, message);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 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.
@@ -44,7 +44,11 @@ public class UserDestinationResult {
private final Set<String> sessionIds;
public UserDestinationResult(String sourceDestination, Set<String> targetDestinations,
/**
* Main constructor.
*/
public UserDestinationResult(
String sourceDestination, Set<String> targetDestinations,
String subscribeDestination, @Nullable String user) {
this(sourceDestination, targetDestinations, subscribeDestination, user, null);
@@ -114,7 +118,6 @@ public class UserDestinationResult {
/**
* Return the session id for the targetDestination.
*/
@Nullable
public Set<String> getSessionIds() {
return this.sessionIds;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -17,6 +17,7 @@
package org.springframework.messaging.simp.user;
import java.nio.charset.StandardCharsets;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
@@ -98,6 +99,26 @@ class UserDestinationMessageHandlerTests {
assertThat(accessor.getFirstNativeHeader(ORIGINAL_DESTINATION)).isEqualTo("/user/queue/foo");
}
@Test
@SuppressWarnings("rawtypes")
void handleMessageWithoutSessionIds() {
UserDestinationResolver resolver = mock();
Message message = createWith(SimpMessageType.MESSAGE, "joe", null, "/user/joe/queue/foo");
UserDestinationResult result = new UserDestinationResult("/queue/foo-user123", Set.of("/queue/foo-user123"), "/user/queue/foo", "joe");
given(resolver.resolveDestination(message)).willReturn(result);
given(this.brokerChannel.send(Mockito.any(Message.class))).willReturn(true);
UserDestinationMessageHandler handler = new UserDestinationMessageHandler(new StubMessageChannel(), this.brokerChannel, resolver);
handler.handleMessage(message);
ArgumentCaptor<Message> captor = ArgumentCaptor.forClass(Message.class);
Mockito.verify(this.brokerChannel).send(captor.capture());
SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.wrap(captor.getValue());
assertThat(accessor.getDestination()).isEqualTo("/queue/foo-user123");
assertThat(accessor.getFirstNativeHeader(ORIGINAL_DESTINATION)).isEqualTo("/user/queue/foo");
}
@Test
@SuppressWarnings("rawtypes")
void handleMessageWithoutActiveSession() {
@@ -185,7 +185,7 @@ public abstract class SharedEntityManagerCreator {
@SuppressWarnings("serial")
private static class SharedEntityManagerInvocationHandler implements InvocationHandler, Serializable {
private final Log logger = LogFactory.getLog(getClass());
private static final Log logger = LogFactory.getLog(SharedEntityManagerInvocationHandler.class);
private final EntityManagerFactory targetFactory;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 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.
@@ -389,8 +389,8 @@ public class TestContextManager {
* have executed, the first caught exception will be rethrown with any
* subsequent exceptions {@linkplain Throwable#addSuppressed suppressed} in
* the first exception.
* <p>Note that registered listeners will be executed in the opposite
* order in which they were registered.
* <p>Note that listeners will be executed in the opposite order in which they
* were registered.
* @param testInstance the current test instance
* @param testMethod the test method which has just been executed on the
* test instance
@@ -459,7 +459,8 @@ public class TestContextManager {
* have executed, the first caught exception will be rethrown with any
* subsequent exceptions {@linkplain Throwable#addSuppressed suppressed} in
* the first exception.
* <p>Note that registered listeners will be executed in the opposite
* <p>Note that listeners will be executed in the opposite order in which they
* were registered.
* @param testInstance the current test instance
* @param testMethod the test method which has just been executed on the
* test instance
@@ -517,7 +518,8 @@ public class TestContextManager {
* have executed, the first caught exception will be rethrown with any
* subsequent exceptions {@linkplain Throwable#addSuppressed suppressed} in
* the first exception.
* <p>Note that registered listeners will be executed in the opposite
* <p>Note that listeners will be executed in the opposite order in which they
* were registered.
* @throws Exception if a registered TestExecutionListener throws an exception
* @since 3.0
* @see #getTestExecutionListeners()
@@ -182,10 +182,10 @@ public class SqlScriptsTestExecutionListener extends AbstractTestExecutionListen
@Override
public void processAheadOfTime(RuntimeHints runtimeHints, Class<?> testClass, ClassLoader classLoader) {
getSqlAnnotationsFor(testClass).forEach(sql ->
registerClasspathResources(getScripts(sql, testClass, null, true), runtimeHints, classLoader));
registerClasspathResources(getScripts(sql, testClass, null, true), runtimeHints, classLoader));
getSqlMethods(testClass).forEach(testMethod ->
getSqlAnnotationsFor(testMethod).forEach(sql ->
registerClasspathResources(getScripts(sql, testClass, testMethod, false), runtimeHints, classLoader)));
getSqlAnnotationsFor(testMethod).forEach(sql ->
registerClasspathResources(getScripts(sql, testClass, testMethod, false), runtimeHints, classLoader)));
}
/**
@@ -853,7 +853,7 @@ public class MediaType extends MimeType implements Serializable {
* <blockquote>audio/basic == text/html</blockquote>
* <blockquote>audio/basic == audio/wave</blockquote>
* @param mediaTypes the list of media types to be sorted
* @deprecated As of 6.0, in favor of {@link MimeTypeUtils#sortBySpecificity(List)}
* @deprecated as of 6.0, in favor of {@link MimeTypeUtils#sortBySpecificity(List)}
*/
@Deprecated(since = "6.0", forRemoval = true)
public static void sortBySpecificity(List<MediaType> mediaTypes) {
@@ -882,7 +882,7 @@ public class MediaType extends MimeType implements Serializable {
* </ol>
* @param mediaTypes the list of media types to be sorted
* @see #getQualityValue()
* @deprecated As of 6.0, with no direct replacement
* @deprecated as of 6.0, with no direct replacement
*/
@Deprecated(since = "6.0", forRemoval = true)
public static void sortByQualityValue(List<MediaType> mediaTypes) {
@@ -895,9 +895,9 @@ public class MediaType extends MimeType implements Serializable {
/**
* Sorts the given list of {@code MediaType} objects by specificity as the
* primary criteria and quality value the secondary.
* @deprecated As of 6.0, in favor of {@link MimeTypeUtils#sortBySpecificity(List)}
* @deprecated as of 6.0, in favor of {@link MimeTypeUtils#sortBySpecificity(List)}
*/
@Deprecated(since = "6.0")
@Deprecated(since = "6.0", forRemoval = true)
public static void sortBySpecificityAndQuality(List<MediaType> mediaTypes) {
Assert.notNull(mediaTypes, "'mediaTypes' must not be null");
if (mediaTypes.size() > 1) {
@@ -908,7 +908,7 @@ public class MediaType extends MimeType implements Serializable {
/**
* Comparator used by {@link #sortByQualityValue(List)}.
* @deprecated As of 6.0, with no direct replacement
* @deprecated as of 6.0, with no direct replacement
*/
@Deprecated(since = "6.0", forRemoval = true)
public static final Comparator<MediaType> QUALITY_VALUE_COMPARATOR = (mediaType1, mediaType2) -> {
@@ -948,7 +948,7 @@ public class MediaType extends MimeType implements Serializable {
/**
* Comparator used by {@link #sortBySpecificity(List)}.
* @deprecated As of 6.0, with no direct replacement
* @deprecated as of 6.0, with no direct replacement
*/
@Deprecated(since = "6.0", forRemoval = true)
@SuppressWarnings("removal")
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -62,16 +62,9 @@ public class SimpleClientHttpRequestFactory implements ClientHttpRequestFactory
/**
* Indicate whether this request factory should buffer the
* {@linkplain ClientHttpRequest#getBody() request body} internally.
* <p>Default is {@code true}. When sending large amounts of data via POST or PUT,
* it is recommended to change this property to {@code false}, so as not to run
* out of memory. This will result in a {@link ClientHttpRequest} that either
* streams directly to the underlying {@link HttpURLConnection} (if the
* {@link org.springframework.http.HttpHeaders#getContentLength() Content-Length}
* is known in advance), or that will use "Chunked transfer encoding"
* (if the {@code Content-Length} is not known in advance).
* @see #setChunkSize(int)
* @see HttpURLConnection#setFixedLengthStreamingMode(int)
* @deprecated since 6.1 requests are never buffered, as if this property is {@code false}
* @deprecated since 6.1 requests are never buffered,
* as if this property is {@code false}
*/
@Deprecated(since = "6.1", forRemoval = true)
public void setBufferRequestBody(boolean bufferRequestBody) {
@@ -80,11 +73,6 @@ public class SimpleClientHttpRequestFactory implements ClientHttpRequestFactory
/**
* Set the number of bytes to write in each chunk when not buffering request
* bodies locally.
* <p>Note that this parameter is only used when
* {@link #setBufferRequestBody(boolean) bufferRequestBody} is set to {@code false},
* and the {@link org.springframework.http.HttpHeaders#getContentLength() Content-Length}
* is not known in advance.
* @see #setBufferRequestBody(boolean)
*/
public void setChunkSize(int chunkSize) {
this.chunkSize = chunkSize;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -67,7 +67,7 @@ public abstract class HttpAccessor {
* @see #createRequest(URI, HttpMethod)
* @see SimpleClientHttpRequestFactory
* @see org.springframework.http.client.HttpComponentsClientHttpRequestFactory
* @see org.springframework.http.client.OkHttp3ClientHttpRequestFactory
* @see org.springframework.http.client.JdkClientHttpRequestFactory
*/
public void setRequestFactory(ClientHttpRequestFactory requestFactory) {
Assert.notNull(requestFactory, "ClientHttpRequestFactory must not be null");
@@ -66,25 +66,27 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle
@Override
public void handleRequest(HttpServerExchange exchange) {
UndertowServerHttpRequest request = null;
try {
request = new UndertowServerHttpRequest(exchange, getDataBufferFactory());
}
catch (URISyntaxException ex) {
if (logger.isWarnEnabled()) {
logger.debug("Failed to get request URI: " + ex.getMessage());
exchange.dispatch(() -> {
UndertowServerHttpRequest request = null;
try {
request = new UndertowServerHttpRequest(exchange, getDataBufferFactory());
}
exchange.setStatusCode(400);
return;
}
ServerHttpResponse response = new UndertowServerHttpResponse(exchange, getDataBufferFactory(), request);
catch (URISyntaxException ex) {
if (logger.isWarnEnabled()) {
logger.debug("Failed to get request URI: " + ex.getMessage());
}
exchange.setStatusCode(400);
return;
}
ServerHttpResponse response = new UndertowServerHttpResponse(exchange, getDataBufferFactory(), request);
if (request.getMethod() == HttpMethod.HEAD) {
response = new HttpHeadResponseDecorator(response);
}
if (request.getMethod() == HttpMethod.HEAD) {
response = new HttpHeadResponseDecorator(response);
}
HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(exchange, request);
this.httpHandler.handle(request, response).subscribe(resultSubscriber);
HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(exchange, request);
this.httpHandler.handle(request, response).subscribe(resultSubscriber);
});
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -288,55 +288,8 @@ public class DeferredResult<T> {
}
final DeferredResultProcessingInterceptor getInterceptor() {
return new DeferredResultProcessingInterceptor() {
@Override
public <S> boolean handleTimeout(NativeWebRequest request, DeferredResult<S> deferredResult) {
boolean continueProcessing = true;
try {
if (timeoutCallback != null) {
timeoutCallback.run();
}
}
finally {
Object value = timeoutResult.get();
if (value != RESULT_NONE) {
continueProcessing = false;
try {
setResultInternal(value);
}
catch (Throwable ex) {
logger.debug("Failed to handle timeout result", ex);
}
}
}
return continueProcessing;
}
@Override
public <S> boolean handleError(NativeWebRequest request, DeferredResult<S> deferredResult, Throwable t) {
try {
if (errorCallback != null) {
errorCallback.accept(t);
}
}
finally {
try {
setResultInternal(t);
}
catch (Throwable ex) {
logger.debug("Failed to handle error result", ex);
}
}
return false;
}
@Override
public <S> void afterCompletion(NativeWebRequest request, DeferredResult<S> deferredResult) {
expired = true;
if (completionCallback != null) {
completionCallback.run();
}
}
};
final DeferredResultProcessingInterceptor getLifecycleInterceptor() {
return new LifecycleInterceptor();
}
@@ -349,4 +302,61 @@ public class DeferredResult<T> {
void handleResult(@Nullable Object result);
}
/**
* Instance interceptor to receive Servlet container notifications.
*/
private class LifecycleInterceptor implements DeferredResultProcessingInterceptor {
@Override
public <S> boolean handleTimeout(NativeWebRequest request, DeferredResult<S> result) {
boolean continueProcessing = true;
try {
if (timeoutCallback != null) {
timeoutCallback.run();
}
}
finally {
Object value = timeoutResult.get();
if (value != RESULT_NONE) {
continueProcessing = false;
try {
setResultInternal(value);
}
catch (Throwable ex) {
logger.debug("Failed to handle timeout result", ex);
}
}
}
return continueProcessing;
}
@Override
public <S> boolean handleError(NativeWebRequest request, DeferredResult<S> result, Throwable t) {
try {
if (errorCallback != null) {
errorCallback.accept(t);
}
}
finally {
try {
setResultInternal(t);
}
catch (Throwable ex) {
logger.debug("Failed to handle error result", ex);
}
}
return false;
}
@Override
public <S> void afterCompletion(NativeWebRequest request, DeferredResult<S> result) {
expired = true;
if (completionCallback != null) {
completionCallback.run();
}
}
}
}
@@ -382,36 +382,6 @@ public final class WebAsyncManager {
}
}
private void setConcurrentResultAndDispatch(@Nullable Object result) {
Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null");
synchronized (WebAsyncManager.this) {
if (!this.state.compareAndSet(State.ASYNC_PROCESSING, State.RESULT_SET)) {
if (logger.isDebugEnabled()) {
logger.debug("Async result already set: [" + this.state.get() +
"], ignored result for " + formatUri(this.asyncWebRequest));
}
return;
}
this.concurrentResult = result;
if (logger.isDebugEnabled()) {
logger.debug("Async result set for " + formatUri(this.asyncWebRequest));
}
if (this.asyncWebRequest.isAsyncComplete()) {
if (logger.isDebugEnabled()) {
logger.debug("Async request already completed for " + formatUri(this.asyncWebRequest));
}
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Performing async dispatch for " + formatUri(this.asyncWebRequest));
}
this.asyncWebRequest.dispatch();
}
}
/**
* Start concurrent request processing and initialize the given
* {@link DeferredResult} with a {@link DeferredResultHandler} that saves
@@ -443,7 +413,7 @@ public final class WebAsyncManager {
}
List<DeferredResultProcessingInterceptor> interceptors = new ArrayList<>();
interceptors.add(deferredResult.getInterceptor());
interceptors.add(deferredResult.getLifecycleInterceptor());
interceptors.addAll(this.deferredResultInterceptors.values());
interceptors.add(timeoutDeferredResultInterceptor);
@@ -455,6 +425,11 @@ public final class WebAsyncManager {
}
try {
interceptorChain.triggerAfterTimeout(this.asyncWebRequest, deferredResult);
synchronized (WebAsyncManager.this) {
// If application thread set the DeferredResult first in a race,
// we must still not return until setConcurrentResultAndDispatch is done
return;
}
}
catch (Throwable ex) {
setConcurrentResultAndDispatch(ex);
@@ -466,10 +441,12 @@ public final class WebAsyncManager {
logger.debug("Servlet container error notification for " + formatUri(this.asyncWebRequest));
}
try {
if (!interceptorChain.triggerAfterError(this.asyncWebRequest, deferredResult, ex)) {
interceptorChain.triggerAfterError(this.asyncWebRequest, deferredResult, ex);
synchronized (WebAsyncManager.this) {
// If application thread set the DeferredResult first in a race,
// we must still not return until setConcurrentResultAndDispatch is done
return;
}
deferredResult.setErrorResult(ex);
}
catch (Throwable interceptorEx) {
setConcurrentResultAndDispatch(interceptorEx);
@@ -508,6 +485,36 @@ public final class WebAsyncManager {
this.asyncWebRequest.startAsync();
}
private void setConcurrentResultAndDispatch(@Nullable Object result) {
Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null");
synchronized (WebAsyncManager.this) {
if (!this.state.compareAndSet(State.ASYNC_PROCESSING, State.RESULT_SET)) {
if (logger.isDebugEnabled()) {
logger.debug("Async result already set: [" + this.state.get() + "], " +
"ignored result for " + formatUri(this.asyncWebRequest));
}
return;
}
this.concurrentResult = result;
if (logger.isDebugEnabled()) {
logger.debug("Async result set for " + formatUri(this.asyncWebRequest));
}
if (this.asyncWebRequest.isAsyncComplete()) {
if (logger.isDebugEnabled()) {
logger.debug("Async request already completed for " + formatUri(this.asyncWebRequest));
}
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Performing async dispatch for " + formatUri(this.asyncWebRequest));
}
this.asyncWebRequest.dispatch();
}
}
private static String formatUri(AsyncWebRequest asyncWebRequest) {
HttpServletRequest request = asyncWebRequest.getNativeRequest(HttpServletRequest.class);
return (request != null ? "\"" + request.getRequestURI() + "\"" : "servlet container");
@@ -517,13 +524,13 @@ public final class WebAsyncManager {
/**
* Represents a state for {@link WebAsyncManager} to be in.
* <p><pre>
* NOT_STARTED <------+
* | |
* v |
* ASYNC_PROCESSING |
* | |
* v |
* RESULT_SET -------+
* +------> NOT_STARTED <------+
* | | |
* | v |
* | ASYNC_PROCESSING |
* | | |
* | v |
* <-------+ RESULT_SET -------+
* </pre>
* @since 5.3.33
*/
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -16,6 +16,7 @@
package org.springframework.web.util;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
@@ -24,12 +25,14 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.core.NestedExceptionUtils;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* Utility methods to assist with identifying and logging exceptions that indicate
* the client has gone away. Such exceptions fill logs with unnecessary stack
* traces. The utility methods help to log a single line message at DEBUG level,
* and a full stacktrace at TRACE level.
* Utility methods to assist with identifying and logging exceptions that
* indicate the server response connection is lost, for example because the
* client has gone away. This class helps to identify such exceptions and
* minimize logging to a single line at DEBUG level, while making the full
* error stacktrace at TRACE level.
*
* @author Rossen Stoyanchev
* @since 6.1
@@ -37,12 +40,28 @@ import org.springframework.util.Assert;
public class DisconnectedClientHelper {
private static final Set<String> EXCEPTION_PHRASES =
Set.of("broken pipe", "connection reset");
Set.of("broken pipe", "connection reset by peer");
private static final Set<String> EXCEPTION_TYPE_NAMES =
Set.of("AbortedException", "ClientAbortException",
"EOFException", "EofException", "AsyncRequestNotUsableException");
private static final Set<Class<?>> CLIENT_EXCEPTION_TYPES = new HashSet<>(2);
static {
try {
ClassLoader classLoader = DisconnectedClientHelper.class.getClassLoader();
CLIENT_EXCEPTION_TYPES.add(ClassUtils.forName(
"org.springframework.web.client.RestClientException", classLoader));
CLIENT_EXCEPTION_TYPES.add(ClassUtils.forName(
"org.springframework.web.reactive.function.client.WebClientException", classLoader));
}
catch (ClassNotFoundException ex) {
// ignore
}
}
private final Log logger;
@@ -79,10 +98,25 @@ public class DisconnectedClientHelper {
* <li>ClientAbortException or EOFException for Tomcat
* <li>EofException for Jetty
* <li>IOException "Broken pipe" or "connection reset by peer"
* <li>SocketException "Connection reset"
* </ul>
*/
public static boolean isClientDisconnectedException(Throwable ex) {
Throwable currentEx = ex;
Throwable lastEx = null;
while (currentEx != null && currentEx != lastEx) {
// Ignore onward connection issues to other servers (500 error)
for (Class<?> exceptionType : CLIENT_EXCEPTION_TYPES) {
if (exceptionType.isInstance(currentEx)) {
return false;
}
}
if (EXCEPTION_TYPE_NAMES.contains(currentEx.getClass().getSimpleName())) {
return true;
}
lastEx = currentEx;
currentEx = currentEx.getCause();
}
String message = NestedExceptionUtils.getMostSpecificCause(ex).getMessage();
if (message != null) {
String text = message.toLowerCase(Locale.ROOT);
@@ -92,7 +126,8 @@ public class DisconnectedClientHelper {
}
}
}
return EXCEPTION_TYPE_NAMES.contains(ex.getClass().getSimpleName());
return false;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -93,7 +93,7 @@ class DeferredResultTests {
DeferredResult<String> result = new DeferredResult<>();
result.onCompletion(() -> sb.append("completion event"));
result.getInterceptor().afterCompletion(null, null);
result.getLifecycleInterceptor().afterCompletion(null, null);
assertThat(result.isSetOrExpired()).isTrue();
assertThat(sb.toString()).isEqualTo("completion event");
@@ -109,7 +109,7 @@ class DeferredResultTests {
result.setResultHandler(handler);
result.onTimeout(() -> sb.append("timeout event"));
result.getInterceptor().handleTimeout(null, null);
result.getLifecycleInterceptor().handleTimeout(null, null);
assertThat(sb.toString()).isEqualTo("timeout event");
assertThat(result.setResult("hello")).as("Should not be able to set result a second time").isFalse();
@@ -127,7 +127,7 @@ class DeferredResultTests {
Exception e = new Exception();
result.onError(t -> sb.append("error event"));
result.getInterceptor().handleError(null, null, e);
result.getLifecycleInterceptor().handleError(null, null, e);
assertThat(sb.toString()).isEqualTo("error event");
assertThat(result.setResult("hello")).as("Should not be able to set result a second time").isFalse();
@@ -0,0 +1,93 @@
/*
* Copyright 2002-2025 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.web.util;
import java.io.EOFException;
import java.io.IOException;
import java.util.List;
import org.apache.catalina.connector.ClientAbortException;
import org.eclipse.jetty.io.EofException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import reactor.netty.channel.AbortedException;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.context.request.async.AsyncRequestNotUsableException;
import org.springframework.web.testfixture.http.MockHttpInputMessage;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link DisconnectedClientHelper}.
* @author Rossen Stoyanchev
*/
public class DisconnectedClientHelperTests {
@ParameterizedTest
@ValueSource(strings = {"broKen pipe", "connection reset By peer"})
void exceptionPhrases(String phrase) {
Exception ex = new IOException(phrase);
assertThat(DisconnectedClientHelper.isClientDisconnectedException(ex)).isTrue();
ex = new IOException(ex);
assertThat(DisconnectedClientHelper.isClientDisconnectedException(ex)).isTrue();
}
@Test
void connectionResetExcluded() {
Exception ex = new IOException("connection reset");
assertThat(DisconnectedClientHelper.isClientDisconnectedException(ex)).isFalse();
}
@ParameterizedTest
@MethodSource("disconnectedExceptions")
void name(Exception ex) {
assertThat(DisconnectedClientHelper.isClientDisconnectedException(ex)).isTrue();
}
static List<Exception> disconnectedExceptions() {
return List.of(
new AbortedException(""), new ClientAbortException(""),
new EOFException(), new EofException(), new AsyncRequestNotUsableException(""));
}
@Test // gh-33064
void nestedDisconnectedException() {
Exception ex = new HttpMessageNotReadableException(
"I/O error while reading input message", new ClientAbortException(),
new MockHttpInputMessage(new byte[0]));
assertThat(DisconnectedClientHelper.isClientDisconnectedException(ex)).isTrue();
}
@Test // gh-34264
void onwardClientDisconnectedExceptionPhrase() {
Exception ex = new ResourceAccessException("I/O error", new EOFException("Connection reset by peer"));
assertThat(DisconnectedClientHelper.isClientDisconnectedException(ex)).isFalse();
}
@Test
void onwardClientDisconnectedExceptionType() {
Exception ex = new ResourceAccessException("I/O error", new EOFException());
assertThat(DisconnectedClientHelper.isClientDisconnectedException(ex)).isFalse();
}
}
@@ -478,9 +478,9 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware {
try {
service = new HandshakeWebSocketService();
}
catch (IllegalStateException ex) {
catch (Throwable ex) {
// Don't fail, test environment perhaps
service = new NoUpgradeStrategyWebSocketService();
service = new NoUpgradeStrategyWebSocketService(ex);
}
}
return service;
@@ -578,9 +578,15 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware {
private static final class NoUpgradeStrategyWebSocketService implements WebSocketService {
private final Throwable ex;
public NoUpgradeStrategyWebSocketService(Throwable ex) {
this.ex = ex;
}
@Override
public Mono<Void> handleRequest(ServerWebExchange exchange, WebSocketHandler webSocketHandler) {
return Mono.error(new IllegalStateException("No suitable RequestUpgradeStrategy"));
return Mono.error(new IllegalStateException("No suitable RequestUpgradeStrategy", this.ex));
}
}
@@ -17,6 +17,7 @@
package org.springframework.web.reactive.result.method.annotation;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -331,6 +332,17 @@ class RequestMappingMessageConversionIntegrationTests extends AbstractRequestMap
assertThat(performPost("/person-transform/flux", JSON, req, JSON, PERSON_LIST).getBody()).isEqualTo(res);
}
@ParameterizedHttpServerTest // see gh-33885
void personTransformWithFluxDelayed(HttpServer httpServer) throws Exception {
startServer(httpServer);
List<?> req = asList(new Person("Robert"), new Person("Marie"));
List<?> res = asList(new Person("ROBERT"), new Person("MARIE"));
assertThat(performPost("/person-transform/flux-delayed", JSON, req, JSON, PERSON_LIST))
.satisfies(r -> assertThat(r.getBody()).isEqualTo(res))
.satisfies(r -> assertThat(r.getHeaders().getContentLength()).isNotZero());
}
@ParameterizedHttpServerTest
void personTransformWithObservable(HttpServer httpServer) throws Exception {
startServer(httpServer);
@@ -632,6 +644,11 @@ class RequestMappingMessageConversionIntegrationTests extends AbstractRequestMap
return persons.map(person -> new Person(person.getName().toUpperCase()));
}
@PostMapping("/flux-delayed")
Flux<Person> transformDelayed(@RequestBody Flux<Person> persons) {
return transformFlux(persons).delayElements(Duration.ofMillis(10));
}
@PostMapping("/observable")
Observable<Person> transformObservable(@RequestBody Observable<Person> persons) {
return persons.map(person -> new Person(person.getName().toUpperCase()));
@@ -28,8 +28,6 @@ import java.util.Map;
import freemarker.template.Configuration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@@ -125,12 +123,6 @@ class FreeMarkerMacroTests {
}
@Test
@DisabledForJreRange(min = JRE.JAVA_21)
public void age() throws Exception {
testMacroOutput("AGE", "99");
}
@Test
void message() throws Exception {
testMacroOutput("MESSAGE", "Howdy Mundo");
@@ -6,9 +6,6 @@ test template for FreeMarker macro support
NAME
${command.name}
AGE
${command.age}
MESSAGE
<@spring.message "hello"/> <@spring.message "world"/>
@@ -401,16 +401,13 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
RootBeanDefinition handlerMappingDef, Element element, ParserContext context) {
Element pathMatchingElement = DomUtils.getChildElementByTagName(element, "path-matching");
Object source = context.extractSource(element);
if (pathMatchingElement != null) {
Object source = context.extractSource(element);
if (pathMatchingElement.hasAttribute("trailing-slash")) {
boolean useTrailingSlashMatch = Boolean.parseBoolean(pathMatchingElement.getAttribute("trailing-slash"));
handlerMappingDef.getPropertyValues().add("useTrailingSlashMatch", useTrailingSlashMatch);
}
boolean preferPathMatcher = false;
if (pathMatchingElement.hasAttribute("suffix-pattern")) {
boolean useSuffixPatternMatch = Boolean.parseBoolean(pathMatchingElement.getAttribute("suffix-pattern"));
handlerMappingDef.getPropertyValues().add("useSuffixPatternMatch", useSuffixPatternMatch);
@@ -435,12 +432,20 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
pathMatcherRef = new RuntimeBeanReference(pathMatchingElement.getAttribute("path-matcher"));
preferPathMatcher = true;
}
pathMatcherRef = MvcNamespaceUtils.registerPathMatcher(pathMatcherRef, context, source);
handlerMappingDef.getPropertyValues().add("pathMatcher", pathMatcherRef);
if (preferPathMatcher) {
pathMatcherRef = MvcNamespaceUtils.registerPathMatcher(pathMatcherRef, context, source);
handlerMappingDef.getPropertyValues().add("pathMatcher", pathMatcherRef);
handlerMappingDef.getPropertyValues().add("patternParser", null);
}
else if (pathMatchingElement.hasAttribute("pattern-parser")) {
RuntimeBeanReference patternParserRef = new RuntimeBeanReference(pathMatchingElement.getAttribute("pattern-parser"));
patternParserRef = MvcNamespaceUtils.registerPatternParser(patternParserRef, context, source);
handlerMappingDef.getPropertyValues().add("patternParser", patternParserRef);
}
}
else {
RuntimeBeanReference pathMatcherRef = MvcNamespaceUtils.registerPathMatcher(null, context, source);
handlerMappingDef.getPropertyValues().add("pathMatcher", pathMatcherRef);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,9 +28,11 @@ import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.lang.Nullable;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
import org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
@@ -39,6 +41,7 @@ import org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter;
import org.springframework.web.servlet.support.SessionFlashMapManager;
import org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator;
import org.springframework.web.util.UrlPathHelper;
import org.springframework.web.util.pattern.PathPatternParser;
/**
* Convenience methods for use in MVC namespace BeanDefinitionParsers.
@@ -64,6 +67,8 @@ public abstract class MvcNamespaceUtils {
private static final String PATH_MATCHER_BEAN_NAME = "mvcPathMatcher";
private static final String PATTERN_PARSER_BEAN_NAME = "mvcPatternParser";
private static final String CORS_CONFIGURATION_BEAN_NAME = "mvcCorsConfigurations";
private static final String HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME = "mvcHandlerMappingIntrospector";
@@ -105,6 +110,18 @@ public abstract class MvcNamespaceUtils {
return new RuntimeBeanReference(URL_PATH_HELPER_BEAN_NAME);
}
/**
* Return the {@link PathMatcher} bean definition if it has been registered
* in the context as an alias with its well-known name, or {@code null}.
*/
@Nullable
static RuntimeBeanReference getCustomPathMatcher(ParserContext context) {
if(context.getRegistry().isAlias(PATH_MATCHER_BEAN_NAME)) {
return new RuntimeBeanReference(PATH_MATCHER_BEAN_NAME);
}
return null;
}
/**
* Adds an alias to an existing well-known name or registers a new instance of a {@link PathMatcher}
* under that well-known name, unless already registered.
@@ -117,6 +134,9 @@ public abstract class MvcNamespaceUtils {
if (context.getRegistry().isAlias(PATH_MATCHER_BEAN_NAME)) {
context.getRegistry().removeAlias(PATH_MATCHER_BEAN_NAME);
}
if (context.getRegistry().containsBeanDefinition(PATH_MATCHER_BEAN_NAME)) {
context.getRegistry().removeBeanDefinition(PATH_MATCHER_BEAN_NAME);
}
context.getRegistry().registerAlias(pathMatcherRef.getBeanName(), PATH_MATCHER_BEAN_NAME);
}
else if (!context.getRegistry().isAlias(PATH_MATCHER_BEAN_NAME) &&
@@ -130,6 +150,60 @@ public abstract class MvcNamespaceUtils {
return new RuntimeBeanReference(PATH_MATCHER_BEAN_NAME);
}
/**
* Return the {@link PathPatternParser} bean definition if it has been registered
* in the context as an alias with its well-known name, or {@code null}.
*/
@Nullable
static RuntimeBeanReference getCustomPatternParser(ParserContext context) {
if (context.getRegistry().isAlias(PATTERN_PARSER_BEAN_NAME)) {
return new RuntimeBeanReference(PATTERN_PARSER_BEAN_NAME);
}
return null;
}
/**
* Adds an alias to an existing well-known name or registers a new instance of a {@link PathPatternParser}
* under that well-known name, unless already registered.
* @return a RuntimeBeanReference to this {@link PathPatternParser} instance
*/
public static RuntimeBeanReference registerPatternParser(@Nullable RuntimeBeanReference patternParserRef,
ParserContext context, @Nullable Object source) {
if (patternParserRef != null) {
if (context.getRegistry().isAlias(PATTERN_PARSER_BEAN_NAME)) {
context.getRegistry().removeAlias(PATTERN_PARSER_BEAN_NAME);
}
context.getRegistry().registerAlias(patternParserRef.getBeanName(), PATTERN_PARSER_BEAN_NAME);
}
else if (!context.getRegistry().isAlias(PATTERN_PARSER_BEAN_NAME) &&
!context.getRegistry().containsBeanDefinition(PATTERN_PARSER_BEAN_NAME)) {
RootBeanDefinition pathMatcherDef = new RootBeanDefinition(PathPatternParser.class);
pathMatcherDef.setSource(source);
pathMatcherDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
context.getRegistry().registerBeanDefinition(PATTERN_PARSER_BEAN_NAME, pathMatcherDef);
context.registerComponent(new BeanComponentDefinition(pathMatcherDef, PATTERN_PARSER_BEAN_NAME));
}
return new RuntimeBeanReference(PATTERN_PARSER_BEAN_NAME);
}
static void configurePathMatching(RootBeanDefinition handlerMappingDef, ParserContext context, @Nullable Object source) {
Assert.isTrue(AbstractHandlerMapping.class.isAssignableFrom(handlerMappingDef.getBeanClass()),
() -> "Handler mapping type [" + handlerMappingDef.getTargetType() + "] not supported");
RuntimeBeanReference customPathMatcherRef = MvcNamespaceUtils.getCustomPathMatcher(context);
RuntimeBeanReference customPatternParserRef = MvcNamespaceUtils.getCustomPatternParser(context);
if (customPathMatcherRef != null) {
handlerMappingDef.getPropertyValues().add("pathMatcher", customPathMatcherRef)
.add("patternParser", null);
}
else if (customPatternParserRef != null) {
handlerMappingDef.getPropertyValues().add("patternParser", customPatternParserRef);
}
else {
handlerMappingDef.getPropertyValues().add("pathMatcher",
MvcNamespaceUtils.registerPathMatcher(null, context, source));
}
}
/**
* Registers an {@link HttpRequestHandlerAdapter} under a well-known
* name unless already registered.
@@ -142,6 +216,7 @@ public abstract class MvcNamespaceUtils {
mappingDef.getPropertyValues().add("order", 2); // consistent with WebMvcConfigurationSupport
RuntimeBeanReference corsRef = MvcNamespaceUtils.registerCorsConfigurations(null, context, source);
mappingDef.getPropertyValues().add("corsConfigurations", corsRef);
configurePathMatching(mappingDef, context, source);
context.getRegistry().registerBeanDefinition(BEAN_NAME_URL_HANDLER_MAPPING_BEAN_NAME, mappingDef);
context.registerComponent(new BeanComponentDefinition(mappingDef, BEAN_NAME_URL_HANDLER_MAPPING_BEAN_NAME));
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -92,7 +92,6 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser {
registerUrlProvider(context, source);
RuntimeBeanReference pathMatcherRef = MvcNamespaceUtils.registerPathMatcher(null, context, source);
RuntimeBeanReference pathHelperRef = MvcNamespaceUtils.registerUrlPathHelper(null, context, source);
String resourceHandlerName = registerResourceHandler(context, element, pathHelperRef, source);
@@ -111,8 +110,8 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser {
RootBeanDefinition handlerMappingDef = new RootBeanDefinition(SimpleUrlHandlerMapping.class);
handlerMappingDef.setSource(source);
handlerMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
handlerMappingDef.getPropertyValues().add("urlMap", urlMap);
handlerMappingDef.getPropertyValues().add("pathMatcher", pathMatcherRef).add("urlPathHelper", pathHelperRef);
handlerMappingDef.getPropertyValues().add("urlMap", urlMap).add("urlPathHelper", pathHelperRef);
MvcNamespaceUtils.configurePathMatching(handlerMappingDef, context, source);
String orderValue = element.getAttribute("order");
// Use a default of near-lowest precedence, still allowing for even lower precedence in other mappings
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -123,7 +123,7 @@ class ViewControllerBeanDefinitionParser implements BeanDefinitionParser {
beanDef.setSource(source);
beanDef.getPropertyValues().add("order", "1");
beanDef.getPropertyValues().add("pathMatcher", MvcNamespaceUtils.registerPathMatcher(null, context, source));
MvcNamespaceUtils.configurePathMatching(beanDef, context, source);
beanDef.getPropertyValues().add("urlPathHelper", MvcNamespaceUtils.registerUrlPathHelper(null, context, source));
RuntimeBeanReference corsConfigurationsRef = MvcNamespaceUtils.registerCorsConfigurations(null, context, source);
beanDef.getPropertyValues().add("corsConfigurations", corsConfigurationsRef);
@@ -67,6 +67,7 @@ final class DefaultAsyncServerResponse extends ErrorHandlingServerResponse imple
this.timeout = timeout;
}
@Override
public ServerResponse block() {
try {
@@ -114,8 +115,8 @@ final class DefaultAsyncServerResponse extends ErrorHandlingServerResponse imple
}
}
@Nullable
@Override
@Nullable
public ModelAndView writeTo(HttpServletRequest request, HttpServletResponse response, Context context)
throws ServletException, IOException {
@@ -169,6 +170,7 @@ final class DefaultAsyncServerResponse extends ErrorHandlingServerResponse imple
return result;
}
@SuppressWarnings({"rawtypes", "unchecked"})
public static AsyncServerResponse create(Object obj, @Nullable Duration timeout) {
Assert.notNull(obj, "Argument to async must not be null");
@@ -192,5 +194,4 @@ final class DefaultAsyncServerResponse extends ErrorHandlingServerResponse imple
throw new IllegalArgumentException("Asynchronous type not supported: " + obj.getClass());
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 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.
@@ -333,11 +333,18 @@ class ReactiveTypeHandler {
this.subscription.request(1);
}
catch (final Throwable ex) {
if (logger.isTraceEnabled()) {
logger.trace("Send for " + this.emitter + " failed: " + ex);
if (logger.isDebugEnabled()) {
logger.debug("Send for " + this.emitter + " failed: " + ex);
}
terminate();
this.emitter.completeWithError(ex);
try {
this.emitter.completeWithError(ex);
}
catch (Exception ex2) {
if (logger.isDebugEnabled()) {
logger.debug("Failure from emitter completeWithError: " + ex2);
}
}
return;
}
}
@@ -347,16 +354,30 @@ class ReactiveTypeHandler {
Throwable ex = this.error;
this.error = null;
if (ex != null) {
if (logger.isTraceEnabled()) {
logger.trace("Publisher for " + this.emitter + " failed: " + ex);
if (logger.isDebugEnabled()) {
logger.debug("Publisher for " + this.emitter + " failed: " + ex);
}
try {
this.emitter.completeWithError(ex);
}
catch (Exception ex2) {
if (logger.isDebugEnabled()) {
logger.debug("Failure from emitter completeWithError: " + ex2);
}
}
this.emitter.completeWithError(ex);
}
else {
if (logger.isTraceEnabled()) {
logger.trace("Publisher for " + this.emitter + " completed");
}
this.emitter.complete();
try {
this.emitter.complete();
}
catch (Exception ex2) {
if (logger.isDebugEnabled()) {
logger.debug("Failure from emitter complete: " + ex2);
}
}
}
return;
}
@@ -89,7 +89,14 @@
<xsd:annotation>
<xsd:documentation><![CDATA[
The bean name of the PathMatcher implementation to use for matching URL paths against registered URL patterns.
Default is AntPathMatcher.
If no bean is provided, the PathPatternParser will be used instead.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="pattern-parser" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
The bean name of the PathPatternParser instance to use for matching URL paths against registered URL patterns.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
@@ -89,6 +89,7 @@ class AnnotationDrivenBeanDefinitionParserTests {
assertThat(hm.useRegisteredSuffixPatternMatch()).isTrue();
assertThat(hm.getUrlPathHelper()).isInstanceOf(TestPathHelper.class);
assertThat(hm.getPathMatcher()).isInstanceOf(TestPathMatcher.class);
assertThat(hm.getPatternParser()).isNull();
List<String> fileExtensions = hm.getContentNegotiationManager().getAllFileExtensions();
assertThat(fileExtensions).containsExactly("xml");
}
@@ -64,6 +64,7 @@ import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConve
import org.springframework.lang.Nullable;
import org.springframework.scheduling.concurrent.ConcurrentTaskExecutor;
import org.springframework.stereotype.Controller;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;
@@ -145,6 +146,7 @@ import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
import org.springframework.web.testfixture.servlet.MockRequestDispatcher;
import org.springframework.web.testfixture.servlet.MockServletContext;
import org.springframework.web.util.UrlPathHelper;
import org.springframework.web.util.pattern.PathPatternParser;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -202,6 +204,8 @@ public class MvcNamespaceTests {
assertThat(mapping).isNotNull();
assertThat(mapping.getOrder()).isEqualTo(0);
assertThat(mapping.getUrlPathHelper().shouldRemoveSemicolonContent()).isTrue();
assertThat(mapping.getPathMatcher()).isEqualTo(appContext.getBean("mvcPathMatcher"));
assertThat(mapping.getPatternParser()).isNotNull();
mapping.setDefaultHandler(handlerMethod);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo.json");
@@ -392,6 +396,8 @@ public class MvcNamespaceTests {
SimpleUrlHandlerMapping resourceMapping = appContext.getBean(SimpleUrlHandlerMapping.class);
assertThat(resourceMapping).isNotNull();
assertThat(resourceMapping.getOrder()).isEqualTo(Ordered.LOWEST_PRECEDENCE - 1);
assertThat(resourceMapping.getPathMatcher()).isNotNull();
assertThat(resourceMapping.getPatternParser()).isNotNull();
BeanNameUrlHandlerMapping beanNameMapping = appContext.getBean(BeanNameUrlHandlerMapping.class);
assertThat(beanNameMapping).isNotNull();
@@ -423,6 +429,31 @@ public class MvcNamespaceTests {
.isInstanceOf(NoResourceFoundException.class);
}
@Test
void testUseDeprecatedPathMatcher() throws Exception {
loadBeanDefinitions("mvc-config-deprecated-path-matcher.xml");
Map<String, AbstractHandlerMapping> handlerMappings = appContext.getBeansOfType(AbstractHandlerMapping.class);
AntPathMatcher mvcPathMatcher = appContext.getBean("pathMatcher", AntPathMatcher.class);
assertThat(handlerMappings).hasSize(4);
handlerMappings.forEach((name, hm) -> {
assertThat(hm.getPathMatcher()).as("path matcher for %s", name).isEqualTo(mvcPathMatcher);
assertThat(hm.getPatternParser()).as("pattern parser for %s", name).isNull();
});
}
@Test
void testUsePathPatternParser() throws Exception {
loadBeanDefinitions("mvc-config-custom-pattern-parser.xml");
PathPatternParser patternParser = appContext.getBean("patternParser", PathPatternParser.class);
Map<String, AbstractHandlerMapping> handlerMappings = appContext.getBeansOfType(AbstractHandlerMapping.class);
assertThat(handlerMappings).hasSize(4);
handlerMappings.forEach((name, hm) -> {
assertThat(hm.getPathMatcher()).as("path matcher for %s", name).isNotNull();
assertThat(hm.getPatternParser()).as("pattern parser for %s", name).isEqualTo(patternParser);
});
}
@Test
void testResourcesWithOptionalAttributes() {
loadBeanDefinitions("mvc-config-resources-optional-attrs.xml");
@@ -600,6 +631,9 @@ public class MvcNamespaceTests {
assertThat(beanNameMapping).isNotNull();
assertThat(beanNameMapping.getOrder()).isEqualTo(2);
assertThat(beanNameMapping.getPathMatcher()).isNotNull();
assertThat(beanNameMapping.getPatternParser()).isNotNull();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("GET");
@@ -895,11 +929,11 @@ public class MvcNamespaceTests {
assertThat(viewController.getUrlPathHelper().getClass()).isEqualTo(TestPathHelper.class);
assertThat(viewController.getPathMatcher().getClass()).isEqualTo(TestPathMatcher.class);
for (SimpleUrlHandlerMapping handlerMapping : appContext.getBeansOfType(SimpleUrlHandlerMapping.class).values()) {
appContext.getBeansOfType(SimpleUrlHandlerMapping.class).forEach((name, handlerMapping) -> {
assertThat(handlerMapping).isNotNull();
assertThat(handlerMapping.getUrlPathHelper().getClass()).isEqualTo(TestPathHelper.class);
assertThat(handlerMapping.getPathMatcher().getClass()).isEqualTo(TestPathMatcher.class);
}
assertThat(handlerMapping.getUrlPathHelper().getClass()).as("path helper for %s", name).isEqualTo(TestPathHelper.class);
assertThat(handlerMapping.getPathMatcher().getClass()).as("path matcher for %s", name).isEqualTo(TestPathMatcher.class);
});
}
@Test
@@ -909,7 +943,7 @@ public class MvcNamespaceTests {
String[] beanNames = appContext.getBeanNamesForType(AbstractHandlerMapping.class);
assertThat(beanNames).hasSize(2);
for (String beanName : beanNames) {
AbstractHandlerMapping handlerMapping = (AbstractHandlerMapping)appContext.getBean(beanName);
AbstractHandlerMapping handlerMapping = (AbstractHandlerMapping) appContext.getBean(beanName);
assertThat(handlerMapping).isNotNull();
DirectFieldAccessor accessor = new DirectFieldAccessor(handlerMapping);
Map<String, CorsConfiguration> configs = ((UrlBasedCorsConfigurationSource) accessor
@@ -934,7 +968,7 @@ public class MvcNamespaceTests {
String[] beanNames = appContext.getBeanNamesForType(AbstractHandlerMapping.class);
assertThat(beanNames).hasSize(2);
for (String beanName : beanNames) {
AbstractHandlerMapping handlerMapping = (AbstractHandlerMapping)appContext.getBean(beanName);
AbstractHandlerMapping handlerMapping = (AbstractHandlerMapping) appContext.getBean(beanName);
assertThat(handlerMapping).isNotNull();
DirectFieldAccessor accessor = new DirectFieldAccessor(handlerMapping);
Map<String, CorsConfiguration> configs = ((UrlBasedCorsConfigurationSource) accessor
@@ -54,6 +54,7 @@ class DefaultServerResponseBuilderTests {
static final ServerResponse.Context EMPTY_CONTEXT = Collections::emptyList;
@Test
@SuppressWarnings("removal")
void status() {
@@ -75,7 +76,6 @@ class DefaultServerResponseBuilderTests {
assertThat(result.cookies().getFirst("foo")).isEqualTo(cookie);
}
@Test
void ok() {
ServerResponse response = ServerResponse.ok().build();
@@ -31,8 +31,6 @@ import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.core.io.ClassPathResource;
@@ -146,12 +144,6 @@ public class FreeMarkerMacroTests {
assertThat(getMacroOutput("NAME")).isEqualTo("Darren");
}
@Test
@DisabledForJreRange(min = JRE.JAVA_21)
public void testAge() throws Exception {
assertThat(getMacroOutput("AGE")).isEqualTo("99");
}
@Test
void testMessage() throws Exception {
assertThat(getMacroOutput("MESSAGE")).isEqualTo("Howdy Mundo");
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<bean id="patternParser" class="org.springframework.web.util.pattern.PathPatternParser"/>
<mvc:annotation-driven>
<mvc:path-matching pattern-parser="patternParser"/>
</mvc:annotation-driven>
<mvc:view-controller path="/foo"/>
<mvc:resources mapping="/resources/**" location="/, classpath:/META-INF/"/>
</beans>
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<bean id="pathMatcher" class="org.springframework.util.AntPathMatcher"/>
<mvc:annotation-driven>
<mvc:path-matching path-matcher="pathMatcher"/>
</mvc:annotation-driven>
<mvc:view-controller path="/foo"/>
<mvc:resources mapping="/resources/**" location="/, classpath:/META-INF/"/>
</beans>
@@ -6,9 +6,6 @@ test template for FreeMarker macro test class
NAME
${command.name}
AGE
${command.age}
MESSAGE
<@spring.message "hello"/> <@spring.message "world"/>
+3 -3
View File
@@ -16,13 +16,13 @@ dependencies {
exclude group: "org.apache.tomcat", module: "tomcat-servlet-api"
exclude group: "org.apache.tomcat", module: "tomcat-websocket-api"
}
optional("org.eclipse.jetty.ee10:jetty-ee10-webapp") {
exclude group: "jakarta.servlet", module: "jakarta.servlet-api"
}
optional("org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-server")
optional("org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jetty-server") {
exclude group: "jakarta.servlet", module: "jakarta.servlet-api"
}
optional("org.eclipse.jetty.ee10:jetty-ee10-webapp") {
exclude group: "jakarta.servlet", module: "jakarta.servlet-api"
}
optional("org.glassfish.tyrus:tyrus-container-servlet")
testImplementation(testFixtures(project(":spring-core")))
testImplementation(testFixtures(project(":spring-web")))
@@ -34,12 +34,10 @@ import org.springframework.web.socket.WebSocketExtension;
*/
public class StandardToWebSocketExtensionAdapter extends WebSocketExtension {
public StandardToWebSocketExtensionAdapter(Extension extension) {
super(extension.getName(), initParameters(extension));
}
private static Map<String, String> initParameters(Extension extension) {
List<Extension.Parameter> parameters = extension.getParameters();
Map<String, String> result = new LinkedCaseInsensitiveMap<>(parameters.size(), Locale.ROOT);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -55,6 +55,7 @@ public class WebSocketToStandardExtensionAdapter implements Extension {
}
}
@Override
public String getName() {
return this.name;
@@ -234,7 +234,7 @@ public class WebSocketStompClient extends StompClientSupport implements SmartLif
/**
* An overloaded version of
* {@link #connect(String, StompSessionHandler, Object...)} that also
* {@link #connectAsync(String, StompSessionHandler, Object...)} that also
* accepts {@link WebSocketHttpHeaders} to use for the WebSocket handshake.
* @param url the url to connect to
* @param handshakeHeaders the headers for the WebSocket handshake
@@ -254,7 +254,7 @@ public class WebSocketStompClient extends StompClientSupport implements SmartLif
/**
* An overloaded version of
* {@link #connect(String, StompSessionHandler, Object...)} that also
* {@link #connectAsync(String, StompSessionHandler, Object...)} that also
* accepts {@link WebSocketHttpHeaders} to use for the WebSocket handshake.
* @param url the url to connect to
* @param handshakeHeaders the headers for the WebSocket handshake
@@ -271,7 +271,7 @@ public class WebSocketStompClient extends StompClientSupport implements SmartLif
/**
* An overloaded version of
* {@link #connect(String, StompSessionHandler, Object...)} that also accepts
* {@link #connectAsync(String, StompSessionHandler, Object...)} that also accepts
* {@link WebSocketHttpHeaders} to use for the WebSocket handshake and
* {@link StompHeaders} for the STOMP CONNECT frame.
* @param url the url to connect to
@@ -293,7 +293,7 @@ public class WebSocketStompClient extends StompClientSupport implements SmartLif
/**
* An overloaded version of
* {@link #connect(String, StompSessionHandler, Object...)} that also accepts
* {@link #connectAsync(String, StompSessionHandler, Object...)} that also accepts
* {@link WebSocketHttpHeaders} to use for the WebSocket handshake and
* {@link StompHeaders} for the STOMP CONNECT frame.
* @param url the url to connect to
@@ -314,7 +314,7 @@ public class WebSocketStompClient extends StompClientSupport implements SmartLif
/**
* An overloaded version of
* {@link #connect(String, WebSocketHttpHeaders, StompSessionHandler, Object...)}
* {@link #connectAsync(String, WebSocketHttpHeaders, StompSessionHandler, Object...)}
* that accepts a fully prepared {@link java.net.URI}.
* @param url the url to connect to
* @param handshakeHeaders the headers for the WebSocket handshake
@@ -334,7 +334,7 @@ public class WebSocketStompClient extends StompClientSupport implements SmartLif
/**
* An overloaded version of
* {@link #connect(String, WebSocketHttpHeaders, StompSessionHandler, Object...)}
* {@link #connectAsync(String, WebSocketHttpHeaders, StompSessionHandler, Object...)}
* that accepts a fully prepared {@link java.net.URI}.
* @param url the url to connect to
* @param handshakeHeaders the headers for the WebSocket handshake
@@ -1,93 +0,0 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.socket.server.support;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.ReflectionHints;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.TypeReference;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
/**
* {@link RuntimeHintsRegistrar} implementation that registers reflection hints
* for {@link AbstractHandshakeHandler}.
*
* @author Sebastien Deleuze
* @since 6.0
*/
class HandshakeHandlerRuntimeHints implements RuntimeHintsRegistrar {
private static final boolean tomcatWsPresent;
private static final boolean jettyWsPresent;
private static final boolean undertowWsPresent;
private static final boolean glassfishWsPresent;
private static final boolean weblogicWsPresent;
private static final boolean websphereWsPresent;
static {
ClassLoader classLoader = AbstractHandshakeHandler.class.getClassLoader();
tomcatWsPresent = ClassUtils.isPresent(
"org.apache.tomcat.websocket.server.WsHttpUpgradeHandler", classLoader);
jettyWsPresent = ClassUtils.isPresent(
"org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServerContainer", classLoader);
undertowWsPresent = ClassUtils.isPresent(
"io.undertow.websockets.jsr.ServerWebSocketContainer", classLoader);
glassfishWsPresent = ClassUtils.isPresent(
"org.glassfish.tyrus.servlet.TyrusHttpUpgradeHandler", classLoader);
weblogicWsPresent = ClassUtils.isPresent(
"weblogic.websocket.tyrus.TyrusServletWriter", classLoader);
websphereWsPresent = ClassUtils.isPresent(
"com.ibm.websphere.wsoc.WsWsocServerContainer", classLoader);
}
@Override
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
ReflectionHints reflectionHints = hints.reflection();
if (tomcatWsPresent) {
registerType(reflectionHints, "org.springframework.web.socket.server.standard.TomcatRequestUpgradeStrategy");
}
else if (jettyWsPresent) {
registerType(reflectionHints, "org.springframework.web.socket.server.jetty.JettyRequestUpgradeStrategy");
}
else if (undertowWsPresent) {
registerType(reflectionHints, "org.springframework.web.socket.server.standard.UndertowRequestUpgradeStrategy");
}
else if (glassfishWsPresent) {
registerType(reflectionHints, "org.springframework.web.socket.server.standard.GlassFishRequestUpgradeStrategy");
}
else if (weblogicWsPresent) {
registerType(reflectionHints, "org.springframework.web.socket.server.standard.WebLogicRequestUpgradeStrategy");
}
else if (websphereWsPresent) {
registerType(reflectionHints, "org.springframework.web.socket.server.standard.WebSphereRequestUpgradeStrategy");
}
}
private void registerType(ReflectionHints reflectionHints, String className) {
reflectionHints.registerType(TypeReference.of(className),
builder -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS));
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -278,6 +278,7 @@ class DefaultTransportRequest implements TransportRequest {
}
}
/**
* Updates the given (global) future based success or failure to connect for
* the entire SockJS request regardless of which transport actually managed
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -116,13 +116,13 @@ public class SockJsClient implements WebSocketClient, Lifecycle {
/**
* The names of HTTP headers that should be copied from the handshake headers
* of each call to {@link SockJsClient#doHandshake(WebSocketHandler, WebSocketHttpHeaders, URI)}
* and also used with other HTTP requests issued as part of that SockJS
* connection, e.g. the initial info request, XHR send or receive requests.
* The names of HTTP headers that should be copied from the handshake headers of each
* call to {@link SockJsClient#execute(WebSocketHandler, WebSocketHttpHeaders, URI)}
* and also used with other HTTP requests issued as part of that SockJS connection,
* for example, the initial info request, XHR send or receive requests.
* <p>By default if this property is not set, all handshake headers are also
* used for other HTTP requests. Set it if you want only a subset of handshake
* headers (e.g. auth headers) to be used for other HTTP requests.
* headers (for example, auth headers) to be used for other HTTP requests.
* @param httpHeaderNames the HTTP header names
*/
public void setHttpHeaderNames(@Nullable String... httpHeaderNames) {
@@ -1,2 +0,0 @@
org.springframework.aot.hint.RuntimeHintsRegistrar= \
org.springframework.web.socket.server.support.HandshakeHandlerRuntimeHints