Compare commits

...

112 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
Brian Clozel 5145cf476f Next development version (v6.1.16-SNAPSHOT) 2024-11-14 15:28:50 +01:00
Juergen Hoeller 6cb41dc5e3 Upgrade to Netty 4.1.115 2024-11-14 12:51:03 +01:00
Sam Brannen 1e95332f62 Fix link to "Resources" section in reference guide
Closes gh-33882
2024-11-14 10:15:34 +01:00
Juergen Hoeller fec6ba4dfe Polishing 2024-11-13 22:26:06 +01:00
Sam Brannen bfde33a514 Document options for Date/Time parsing & formatting issues with JDK 20+
This commit updates Javadoc and the reference guide to document options
for handling date/time parsing and formatting issues on JDK 20 and higher.

A new "Date and Time Formatting with JDK 20 and higher" page has also been
introduced in the wiki.

https://github.com/spring-projects/spring-framework/wiki/Date-and-Time-Formatting-with-JDK-20-and-higher

Closes gh-33151
2024-11-13 16:39:40 +01:00
Juergen Hoeller 14b9865de7 Remove ineffective JettyByteBufferIterator from WebSocket adapter
In JettyWebSocketHandlerAdapter, JettyByteBufferIterator does not actually add extra behavior (in contrast to JettyClientHttpConnector).
2024-11-13 15:10:32 +01:00
Juergen Hoeller 62eb21f938 Add note on declaring autowired fields as ObjectProvider
Closes gh-33834
2024-11-13 15:09:58 +01:00
Juergen Hoeller 01c85b1afb Add explicit note on blocking in case of concurrency limit
Closes gh-33873
2024-11-13 15:09:02 +01:00
Juergen Hoeller df376d9343 Upgrade to Micrometer 1.12.12 and Reactor 2023.0.12
Includes Netty 4.1.114, Jetty 12.0.15, Jetty Reactive HttpClient 4.0.8, RxJava 3.1.9, RSocket 1.1.4, Groovy 4.0.24, JRuby 9.4.9, Checkstyle 10.20.1

Closes gh-33877
Closes gh-33879
2024-11-13 15:08:40 +01:00
Sam Brannen 4d792d0e45 Remove mentions of Joda-Time support
Since Joda-Time support was removed in Spring Framework 6.0, this commit
removes obsolete mentions of Joda-Time in the reference guide and Javadoc.

See gh-27426
Closes gh-33881
2024-11-13 14:16:14 +01:00
Sam Brannen 0a5bd89129 Align JettyByteBufferIterator implementations
Both are now static nested classes.
2024-11-13 12:31:27 +01:00
Tran Ngoc Nhan 53b9a2cb78 Fix formatting issue in validation section of reference guide
Closes gh-33871
2024-11-12 20:49:56 +01:00
rstoyanchev e78179b96e Decode static resource path with UriUtils
Closes gh-33859
2024-11-12 10:16:05 +00:00
Brian Clozel 49a63e2c37 Add tests for gh-33867
Closes gh-33867
2024-11-12 10:02:48 +01:00
ZLATAN628 5666e363d1 Fix wrong uri tag for client observation convention
Prior to this commit, a client sending a request to
"https://example.org" would record the wrong URI tag as
"/https://example.org".

This commit ensures that the scheme+host part is matched correctly in
the default client observation conventions.

See gh-33867
2024-11-12 10:02:39 +01:00
Stéphane Nicoll f06853a339 Merge pull request #33865 from ngocnhan-tran1996
* pr/33865:
  Fix typo in reference documentation

Closes gh-33865
2024-11-10 14:45:49 +09:00
Tran Ngoc Nhan 8afd01ba2c Fix typo in reference documentation
See gh-33865
2024-11-10 14:45:19 +09:00
Sam Brannen 9724f9b9c8 Introduce tests for SpEL PropertyAccessor ordering
Closes gh-33861
2024-11-08 16:19:12 +01:00
Sam Brannen 0d9033592b Document that circular dependencies should be avoided in AOT mode
Closes gh-33786
2024-11-07 12:23:39 +01:00
Sam Brannen fc7b8ae966 Fix anchor name, consistently use title case, and polish wording
(cherry picked from commit 2e6c8daec6)
2024-11-07 12:22:25 +01:00
Sam Brannen c457131f1c Fix heading level for "Programmatic bean registration" in AOT chapter
(cherry picked from commit 9f0dbc4051)
2024-11-07 12:22:02 +01:00
Sam Brannen 39cfe136da Polishing 2024-11-07 11:49:22 +01:00
Sam Brannen 05a880e3b5 Fix XML bean reference example in reference manual
Closes gh-33855
2024-11-07 10:44:01 +01:00
Simon Baslé c93af1f76d Polishing: copyright header year
See gh-33823
2024-11-07 10:04:53 +01:00
Stéphane Nicoll 541866fd70 Merge pull request #33850 from wilkinsona
* pr/33850:
  Polish "Prefer modified resources over the originals in TestCompiler"
  Prefer modified resources over the originals in TestCompiler

Closes gh-33850
2024-11-06 23:11:58 +09:00
Stéphane Nicoll 0219ee656f Polish "Prefer modified resources over the originals in TestCompiler"
See gh-33850
2024-11-06 23:03:11 +09:00
Andy Wilkinson f6e1a5de09 Prefer modified resources over the originals in TestCompiler
Previously, when the test compiler had been seeded with a resource
file, any modifications to this resource performed during
compilation would be lost as this original content would always
be returned.

This commit updates the DynamicJavaFileManager to always store
the dynamic resource in the dynamicResourceFiles map, irrespective
of whether it's being created afresh or from some existing resource
content. This ensures that any modifications made to the resource
can be retrieved later on.

Similarly, DynamicClassLoader has been updated to prefer dynamic
resource files over any original resource files. This ensures that
the resource that it finds reflects any modifications that have
been made to it.

See gh-33850
2024-11-06 23:00:08 +09:00
Simon Baslé 9b3cb15389 Introduce HttpHeaders#headerSet to guarantee case-insensitive iteration
The `HttpHeaders#headerSet` method is intended as a drop-in replacement
for `entrySet` that guarantees a single casing for all header names
reported during the iteration, as the cost of some overhead but with
support for iterator removal and entry value-setting.

The `formatHeaders` static method is also altered to do a similar
deduplication of casing variants, but now additionally mentions
"with native header names [native name set]" if the native name set
contains casing variants.

Closes gh-33823
2024-11-06 11:11:39 +01:00
Hosam Aly 4ef2b429e0 Fix a typo in beanvalidation.adoc
ContraintViolation => ConstraintViolation

Closes gh-33846
2024-11-06 10:09:05 +01:00
Johnny Lim 0beb56a58c Fix indentation to use tabs in Kotlin source files
Closes gh-33840
2024-11-05 10:24:02 +01:00
Sam Brannen 6bd4687706 Update copyright headers
See gh-33839
2024-11-03 16:14:41 +01:00
Tran Ngoc Nhan 07b12666b4 Fix typos in Javadoc and variable names
Closes gh-33839
2024-11-03 16:13:50 +01:00
Stéphane Nicoll 438d6de3c1 Merge pull request #33768 from kunaljani1100
* pr/33768:
  Polish "Add test coverage for DomUtils"
  Add test coverage for DomUtils

Closes gh-33768
2024-11-03 18:16:17 +09:00
Stéphane Nicoll d43126705f Polish "Add test coverage for DomUtils"
See gh-33768
2024-11-03 18:15:34 +09:00
kunaljani1100 57bbf0ca0f Add test coverage for DomUtils
See gh-33768
2024-11-03 13:44:07 +09:00
Sam Brannen 27912b1ed1 Prevent accidental printing to System.err in the codebase
This also revises the Checkstyle rule so that invocations such as
System.out.printf() are also forbidden.
2024-11-01 10:42:31 +01:00
Brian Clozel 8ffbafd384 Prevent accidental Sysouts in the codebase 2024-10-30 10:36:30 +01:00
Juergen Hoeller 022fdcd67e Provide removeCache method on Caffeine/ConcurrentMapCacheManager
Closes gh-33813
2024-10-29 23:02:48 +01:00
Juergen Hoeller 11ebceee38 Call get/setRequestConfig for HttpClient 5.4 compatibility
Includes upgrade to HttpClient 5.4.1 while retaining a HttpClient 5.1 baseline.

Closes gh-33806
2024-10-29 23:01:57 +01:00
Juergen Hoeller fa21dffcf8 Restore traditional AspectJ behavior through "spring.aop.ajc.ignore=true"
Closes gh-33704
2024-10-29 23:01:16 +01:00
Juergen Hoeller 323de1208a Document limited support for lifecycle management
Closes gh-33780
2024-10-28 22:08:41 +01:00
Juergen Hoeller 94d46eba3c Exclusively mention CompletableFuture instead of ListenableFuture
Closes gh-33805
2024-10-28 22:05:10 +01:00
Sam Brannen e340e45f5a Rename aopAvailable constants in TransactionSynchronizationUtils
Closes gh-33796
2024-10-25 14:41:41 +02:00
Brian Clozel a06bbccf9e HttpHeaders.writeableHttpHeaders should unwrap many times
Prior to this commit, the `HttpHeaders.writeableHttpHeaders` would only
consider headers read-only instances that were wrapped once by
`HttpHeaders.readOnlyHttpHeaders`. This does not work when other
`HttpHeaders` wrappers are involved in the chain.

This commit ensures that `writeableHttpHeaders` unwraps all headers
instances down to the actual multivalue map and create a new headers
instance out of it.

Fixes gh-33789
2024-10-25 10:22:12 +02:00
rstoyanchev bbe362c0e6 Allow null from RestClient exchange methods
Closes gh-33779
2024-10-23 19:08:54 +01:00
Brian Clozel 4a81f2c904 Remove HTTP parts workaround for Resin
A workaround was added for the Resin Servlet container in gh-13937.
This avoids attempting to delete parts that are not named, because the
`part.delete()` call would fail for non-file entries. This can be
problematic for files that are unnamed as they might not be removed by
the Framework.

This commit removes this workaround as Resin is not supported anymore.

Fixes gh-33511
2024-10-23 09:24:30 +02:00
Sam Brannen fb0a108254 Improve Javadoc for core SpEL APIs 2024-10-22 13:04:29 +02:00
Sam Brannen c98f314665 Throw ParseException for unsupported character in SpEL expression
Prior to this commit, the SpEL Tokenizer threw an IllegalStateException
when an unsupported character was detected in a SpEL expression;
however, the Javadoc for ExpressionParser.parseExpression() states that
it throws a ParseException if an exception occurred during parsing.

This commit addresses that issue by throwing a SpelParseException for
an unsupported character in a SpEL expression, using a new
UNSUPPORTED_CHARACTER enum constant in SpelMessage.

Closes gh-33767
2024-10-22 13:03:08 +02:00
Sehwan Lim d22924c728 Fix incorrect regex rendering in MVC controller documentation
This commit fixes the issue where the regex pattern in the reference documentation
was not rendering correctly for the `/projects/{project:[a-z]+}/versions` mapping.

Closes gh-33766
2024-10-21 18:32:44 +02:00
Juergen Hoeller e235e661d8 Polishing 2024-10-21 18:13:42 +02:00
Juergen Hoeller 09fe0adb40 Load-time weaving support for WildFly 24+
Closes gh-33728
2024-10-21 18:13:34 +02:00
Brian Clozel 84e762b470 Fix double accounting of JMS process observations
Prior to this commit, the instrumentation of the processing of JMS
messages would happen a different levels of the hierarchy, accounting
for alli known implementations, `SimpleMessageListenerContainer` and
`DefaultMessageListenerContainer` as well as various use cases and
`MessageListener` variants.

Unfortunately, this instrumentation could lead to observing JMS
processing twice in some cases, and would not be consistent about the
scope of what's observed.

This commit moves the instrumentation basics into the
`AbstractMessageListenerContainer` but leaves the actual observation
calls to the public implementations.

Fixes gh-33758
2024-10-21 16:09:58 +02:00
Juergen Hoeller e90a2da05d Clarify defensive impact of allowEagerInit flag for type matching
Closes gh-33740
2024-10-21 15:38:25 +02:00
Sam Brannen d48f388c6a Polish Javadoc for @⁠DateTimeFormat 2024-10-21 12:19:35 +02:00
Sam Brannen bbbb7c396e Update Date/Time formatting tests for pre/post JDK 20
This commit updates our Date/Time formatting/printing tests to
demonstrate that the use of fallback patterns can help mitigate
locale-based parsing/formatting issues beginning with JDK 20.

The documentation within the tests is intentionally rather thorough for
two reasons:

1. We need to understand exactly what it is we are testing and why the
   tests are written that way.

2. We may re-use parts of the documentation and examples in forthcoming
   documentation that we will provide to users.

See gh-33151
2024-10-20 17:35:37 +02:00
Sam Brannen d72c8b32b7 Ignore duplicate @⁠Priority values when determining highest priority
Prior to this commit, DefaultListableBeanFactory's
determineHighestPriorityCandidate() method sometimes failed to
determine the highest priority candidate if duplicate priority
candidates were detected whose priority was not the highest priority in
the candidate set. In addition, the bean registration order affected
the outcome of the algorithm: if the highest priority was detected
before other duplicate priorities were detected, the algorithm
succeeded in determining the highest priority candidate.

This commit addresses those shortcomings by ignoring duplicate
@⁠Priority values unless the duplication is for the highest priority
encountered, in which case a NoUniqueBeanDefinitionException is still
thrown to signal that multiple beans were found with the same "highest
priority".

Closes gh-33733
2024-10-19 14:54:29 +02:00
Brian Clozel 67d78eb61c Avoid Servlet observations failures for invalid HTTP status
Prior to this commit, the `DefaultServerRequestObservationConvention`
for Servlet failed when the HTTP response status was invalid (for
example, set to "0").

This commit catches `IllegalArgumentException` thrown for such invalid
HTTP status and instead returns an unknown outcome for the observation.

Fixes gh-33725
2024-10-18 14:43:46 +02:00
Johnny Lim 73fd9133e9 Fix Javadoc in ReactorNetty2ResourceFactory
See gh-33338
Closes gh-33735
2024-10-18 12:36:15 +02:00
Sébastien Deleuze 5e28a25a30 Add a WebFlux integration test
This commit adds a WebFlux integration test with a request
body and a delayed response in order to test a use case
where we found a regression with Undertow 2.3.18.Final.

For now, Undertow 2.3.17.Final is still used.

Closes gh-33739
2024-10-18 11:13:09 +02:00
Sébastien Deleuze 4c44b91cf9 Speedup WebFlux tests by reducing the interval period
In WebFlux, we have various tests using a period of 50 or 100ms.
We should use a smaller value like 1ms to speedup tests.

Closes gh-33738
2024-10-18 11:12:08 +02:00
Brian Clozel 912c067e23 Fix buffer leak in Jackson2 decoders
Prior to this commit, the Jackson2 decoders (JSON, Smile, CBOR) could
leak buffers in case the decoding operation times out or is cancelled
and some buffers are still in flight.

This commit ensures that buffers are released on cancel signals.

Fixes gh-33731
2024-10-18 10:51:38 +02:00
Stéphane Nicoll a55701588e Next development version (v6.1.15-SNAPSHOT) 2024-10-17 10:28:22 +02:00
207 changed files with 4570 additions and 1390 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.18.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
@@ -23,6 +23,12 @@ The following table lists all currently supported Spring properties.
|===
| Name | Description
| `spring.aop.ajc.ignore`
| Instructs Spring to ignore ajc-compiled aspects for Spring AOP proxying, restoring traditional
Spring behavior for scenarios where both weaving and AspectJ auto-proxying are enabled. See
{spring-framework-api}++/aop/aspectj/annotation/AbstractAspectJAdvisorFactory.html#IGNORE_AJC_PROPERTY_NAME++[`AbstractAspectJAdvisorFactory`]
for details.
| `spring.aot.enabled`
| Indicates the application should run with AOT generated artifacts. See
xref:core/aot.adoc[Ahead of Time Optimizations] and
@@ -32,7 +38,7 @@ for details.
| `spring.beaninfo.ignore`
| Instructs Spring to use the `Introspector.IGNORE_ALL_BEANINFO` mode when calling the
JavaBeans `Introspector`. See
{spring-framework-api}++/beans/StandardBeanInfoFactory.html#IGNORE_BEANINFO_PROPERTY_NAME++[`CachedIntrospectionResults`]
{spring-framework-api}++/beans/StandardBeanInfoFactory.html#IGNORE_BEANINFO_PROPERTY_NAME++[`StandardBeanInfoFactory`]
for details.
| `spring.cache.reactivestreams.ignore`
@@ -49,15 +55,13 @@ for details.
| `spring.context.checkpoint`
| Property that specifies a common context checkpoint. See
xref:integration/checkpoint-restore.adoc#_automatic_checkpointrestore_at_startup[Automatic
checkpoint/restore at startup] and
xref:integration/checkpoint-restore.adoc#_automatic_checkpointrestore_at_startup[Automatic checkpoint/restore at startup] and
{spring-framework-api}++/context/support/DefaultLifecycleProcessor.html#CHECKPOINT_PROPERTY_NAME++[`DefaultLifecycleProcessor`]
for details.
| `spring.context.exit`
| Property for terminating the JVM when the context reaches a specific phase. See
xref:integration/checkpoint-restore.adoc#_automatic_checkpointrestore_at_startup[Automatic
checkpoint/restore at startup] and
xref:integration/checkpoint-restore.adoc#_automatic_checkpointrestore_at_startup[Automatic checkpoint/restore at startup] and
{spring-framework-api}++/context/support/DefaultLifecycleProcessor.html#EXIT_PROPERTY_NAME++[`DefaultLifecycleProcessor`]
for details.
@@ -36,7 +36,7 @@ NOTE: At the moment, AOT is focused on allowing Spring applications to be deploy
We intend to support more JVM-based use cases in future generations.
[[aot.basics]]
== AOT engine overview
== AOT Engine Overview
The entry point of the AOT engine for processing an `ApplicationContext` is `ApplicationContextAotGenerator`. It takes care of the following steps, based on a `GenericApplicationContext` that represents the application to optimize and a {spring-framework-api}/aot/generate/GenerationContext.html[`GenerationContext`]:
@@ -225,7 +225,7 @@ When a `datasource` instance is required, a `BeanInstanceSupplier` is called.
This supplier invokes the `dataSource()` method on the `dataSourceConfiguration` bean.
[[aot.running]]
== Running with AOT optimizations
== Running with AOT Optimizations
AOT is a mandatory step to transform a Spring application to a native executable, so it
is automatically enabled when running in this mode. It is possible to use those optimizations
@@ -244,7 +244,7 @@ However, keep in mind that some optimizations are made at build time based on a
This section lists the best practices that make sure your application is ready for AOT.
[[aot.bestpractices.bean-registration]]
== Programmatic bean registration
=== Programmatic Bean Registration
The AOT engine takes care of the `@Configuration` model and any callback that might be
invoked as part of processing your configuration. If you need to register additional
@@ -266,7 +266,7 @@ notion of a classpath. For cases like this, it is crucial that the scanning happ
build time.
[[aot.bestpractices.bean-type]]
=== Expose The Most Precise Bean Type
=== Expose the Most Precise Bean Type
While your application may interact with an interface that a bean implements, it is still very important to declare the most precise type.
The AOT engine performs additional checks on the bean type, such as detecting the presence of `@Autowired` members or lifecycle callback methods.
@@ -326,21 +326,21 @@ However, this is not a best practice and flagging the preferred constructor with
In case you are working on a code base that you cannot modify, you can set the {spring-framework-api}/beans/factory/support/AbstractBeanDefinition.html#PREFERRED_CONSTRUCTORS_ATTRIBUTE[`preferredConstructors` attribute] on the related bean definition to indicate which constructor should be used.
[[aot.bestpractices.comlext-data-structure]]
=== Avoid Complex Data Structure for Constructor Parameters and Properties
[[aot.bestpractices.complex-data-structures]]
=== Avoid Complex Data Structures for Constructor Parameters and Properties
When crafting a `RootBeanDefinition` programmatically, you are not constrained in terms of types that you can use.
For instance, you may have a custom `record` with several properties that your bean takes as a constructor argument.
While this works fine with the regular runtime, AOT does not know how to generate the code of your custom data structure.
A good rule of thumb is to keep in mind that bean definitions are an abstraction on top of several models.
Rather than using such structure, decomposing to simple types or referring to a bean that is built as such is recommended.
Rather than using such structures, decomposing to simple types or referring to a bean that is built as such is recommended.
As a last resort, you can implement your own `org.springframework.aot.generate.ValueCodeGenerator$Delegate`.
To use it, register its fully qualified name in `META-INF/spring/aot.factories` using the `Delegate` as the key.
[[aot.bestpractices.custom-arguments]]
=== Avoid Creating Bean with Custom Arguments
=== Avoid Creating Beans with Custom Arguments
Spring AOT detects what needs to be done to create a bean and translates that in generated code using an instance supplier.
The container also supports creating a bean with {spring-framework-api}++/beans/factory/BeanFactory.html#getBean(java.lang.String,java.lang.Object...)++[custom arguments] that leads to several issues with AOT:
@@ -352,6 +352,20 @@ For instance, autowiring on fields and methods will be skipped as they are handl
Rather than having prototype-scoped beans created with custom arguments, we recommend a manual factory pattern where a bean is responsible for the creation of the instance.
[[aot.bestpractices.circular-dependencies]]
=== Avoid Circular Dependencies
Certain use cases can result in circular dependencies between one or more beans. With the
regular runtime, it may be possible to wire those circular dependencies via `@Autowired`
on setter methods or fields. However, an AOT-optimized context will fail to start with
explicit circular dependencies.
In an AOT-optimized application, you should therefore strive to avoid circular
dependencies. If that is not possible, you can use `@Lazy` injection points or
`ObjectProvider` to lazily access or retrieve the necessary collaborating beans. See
xref:core/beans/classpath-scanning.adoc#beans-factorybeans-annotations-lazy-injection-points[this tip]
for further information.
[[aot.bestpractices.factory-bean]]
=== FactoryBean
@@ -480,11 +480,15 @@ factory method and other bean definition properties, such as a qualifier value t
the `@Qualifier` annotation. Other method-level annotations that can be specified are
`@Scope`, `@Lazy`, and custom qualifier annotations.
TIP: In addition to its role for component initialization, you can also place the `@Lazy`
[[beans-factorybeans-annotations-lazy-injection-points]]
[TIP]
====
In addition to its role for component initialization, you can also place the `@Lazy`
annotation on injection points marked with `@Autowired` or `@Inject`. In this context,
it leads to the injection of a lazy-resolution proxy. However, such a proxy approach
is rather limited. For sophisticated lazy interactions, in particular in combination
with optional dependencies, we recommend `ObjectProvider<MyTargetBean>` instead.
====
Autowired fields and methods are supported, as previously discussed, with additional
support for autowiring of `@Bean` methods. The following example shows how to do so:
@@ -883,11 +883,12 @@ e.g. for processing all events asynchronously and/or for handling listener excep
== Convenient Access to Low-level Resources
For optimal usage and understanding of application contexts, you should familiarize
yourself with Spring's `Resource` abstraction, as described in xref:web/webflux-webclient/client-builder.adoc#webflux-client-builder-reactor-resources[Resources].
yourself with Spring's `Resource` abstraction, as described in
xref:core/resources.adoc[Resources].
An application context is a `ResourceLoader`, which can be used to load `Resource` objects.
A `Resource` is essentially a more feature rich version of the JDK `java.net.URL` class.
In fact, the implementations of the `Resource` wrap an instance of `java.net.URL`, where
In fact, implementations of `Resource` wrap an instance of `java.net.URL`, where
appropriate. A `Resource` can obtain low-level resources from almost any location in a
transparent fashion, including from the classpath, a filesystem location, anywhere
describable with a standard URL, and some other variations. If the resource location
@@ -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`/
@@ -102,8 +102,8 @@ following snippet:
----
<bean id="theTargetBean" class="..." />
<bean id="client" class="...">
<property name="targetName" value="theTargetBean"/>
<bean id="theClientBean" class="...">
<property name="targetName" ref="theTargetBean"/>
</bean>
----
@@ -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!>"
@@ -196,7 +196,7 @@ Kotlin::
When used as `org.springframework.validation.Validator`, `LocalValidatorFactoryBean`
invokes the underlying `jakarta.validation.Validator`, and then adapts
``ContraintViolation``s to ``FieldError``s, and registers them with the `Errors` object
``ConstraintViolation``s to ``FieldError``s, and registers them with the `Errors` object
passed into the `validate` method.
@@ -280,10 +280,11 @@ Kotlin::
A portable format annotation API exists in the `org.springframework.format.annotation`
package. You can use `@NumberFormat` to format `Number` fields such as `Double` and
`Long`, and `@DateTimeFormat` to format `java.util.Date`, `java.util.Calendar`, `Long`
(for millisecond timestamps) as well as JSR-310 `java.time`.
`Long`, and `@DateTimeFormat` to format fields such as `java.util.Date`,
`java.util.Calendar`, and `Long` (for millisecond timestamps) as well as JSR-310
`java.time` types.
The following example uses `@DateTimeFormat` to format a `java.util.Date` as an ISO Date
The following example uses `@DateTimeFormat` to format a `java.util.Date` as an ISO date
(yyyy-MM-dd):
[tabs]
@@ -309,6 +310,28 @@ Kotlin::
----
======
For further details, see the javadoc for
{spring-framework-api}/format/annotation/DateTimeFormat.html[`@DateTimeFormat`] and
{spring-framework-api}/format/annotation/NumberFormat.html[`@NumberFormat`].
[WARNING]
====
Style-based formatting and parsing rely on locale-sensitive patterns which may change
depending on the Java runtime. Specifically, applications that rely on date, time, or
number parsing and formatting may encounter incompatible changes in behavior when running
on JDK 20 or higher.
Using an ISO standardized format or a concrete pattern that you control allows for
reliable system-independent and locale-independent parsing and formatting of date, time,
and number values.
For `@DateTimeFormat`, the use of fallback patterns can also help to address
compatibility issues.
For further details, see the
https://github.com/spring-projects/spring-framework/wiki/Date-and-Time-Formatting-with-JDK-20-and-higher[Date and Time Formatting with JDK 20 and higher]
page in the Spring Framework wiki.
====
[[format-FormatterRegistry-SPI]]
== The `FormatterRegistry` SPI
@@ -198,7 +198,7 @@ methods it offers can be found in the {spring-framework-api}/validation/Errors.h
Validators may also get locally invoked for the immediate validation of a given object,
not involving a binding process. As of 6.1, this has been simplified through a new
`Validator.validateObject(Object)` method which is available by default now, returning
a simple ´Errors` representation which can be inspected: typically calling `hasErrors()`
a simple `Errors` representation which can be inspected: typically calling `hasErrors()`
or the new `failOnError` method for turning the error summary message into an exception
(e.g. `validator.validateObject(myObject).failOnError(IllegalArgumentException::new)`).
@@ -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
@@ -91,7 +91,7 @@ class WebConfig : WebFluxConfigurer {
[.small]#xref:web/webmvc/mvc-config/conversion.adoc[See equivalent in the Servlet stack]#
By default, formatters for various number and date types are installed, along with support
for customization via `@NumberFormat` and `@DateTimeFormat` on fields.
for customization via `@NumberFormat` and `@DateTimeFormat` on fields and parameters.
To register custom formatters and converters in Java config, use the following:
@@ -356,7 +356,6 @@ which customizes Jackson's default properties with the following ones:
It also automatically registers the following well-known modules if they are detected on the classpath:
* {jackson-github-org}/jackson-datatype-joda[`jackson-datatype-joda`]: Support for Joda-Time types.
* {jackson-github-org}/jackson-datatype-jsr310[`jackson-datatype-jsr310`]: Support for Java 8 Date and Time API types.
* {jackson-github-org}/jackson-datatype-jdk8[`jackson-datatype-jdk8`]: Support for other Java 8 types, such as `Optional`.
* {jackson-github-org}/jackson-module-kotlin[`jackson-module-kotlin`]: Support for Kotlin classes and data classes.
@@ -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]#
@@ -4,7 +4,7 @@
[.small]#xref:web/webflux/config.adoc#webflux-config-conversion[See equivalent in the Reactive stack]#
By default, formatters for various number and date types are installed, along with support
for customization via `@NumberFormat` and `@DateTimeFormat` on fields.
for customization via `@NumberFormat` and `@DateTimeFormat` on fields and parameters.
To register custom formatters and converters in Java config, use the following:
@@ -73,7 +73,6 @@ This builder customizes Jackson's default properties as follows:
It also automatically registers the following well-known modules if they are detected on the classpath:
* {jackson-github-org}/jackson-datatype-joda[jackson-datatype-joda]: Support for Joda-Time types.
* {jackson-github-org}/jackson-datatype-jsr310[jackson-datatype-jsr310]: Support for Java 8 Date and Time API types.
* {jackson-github-org}/jackson-datatype-jdk8[jackson-datatype-jdk8]: Support for other Java 8 types, such as `Optional`.
* {jackson-github-org}/jackson-module-kotlin[`jackson-module-kotlin`]: Support for Kotlin classes and data classes.
@@ -119,7 +119,7 @@ Some example patterns:
* `+"/resources/*.png"+` - match zero or more characters in a path segment
* `+"/resources/**"+` - match multiple path segments
* `+"/projects/{project}/versions"+` - match a path segment and capture it as a variable
* `+"/projects/{project:[a-z]+}/versions"+` - match and capture a variable with a regex
* `++"/projects/{project:[a-z]+}/versions"++` - match and capture a variable with a regex
Captured URI variables can be accessed with `@PathVariable`. For example:
@@ -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
@@ -45,7 +45,7 @@ input is ignored. This is in contrast to property binding which by default binds
request parameter for which there is a matching property.
If neither a dedicated model object nor constructor binding is sufficient, and you must
use property binding, we strongy recommend registering `allowedFields` patterns (case
use property binding, we strongly recommend registering `allowedFields` patterns (case
sensitive) on `WebDataBinder` in order to prevent unexpected properties from being set.
For example:
+17 -17
View File
@@ -8,16 +8,16 @@ javaPlatform {
dependencies {
api(platform("com.fasterxml.jackson:jackson-bom:2.15.4"))
api(platform("io.micrometer:micrometer-bom:1.12.11"))
api(platform("io.netty:netty-bom:4.1.113.Final"))
api(platform("io.micrometer:micrometer-bom:1.12.12"))
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.11"))
api(platform("io.rsocket:rsocket-bom:1.1.3"))
api(platform("org.apache.groovy:groovy-bom:4.0.23"))
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.13"))
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.13"))
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.8")
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")
@@ -101,8 +101,8 @@ dependencies {
api("org.apache.derby:derby:10.16.1.1")
api("org.apache.derby:derbyclient:10.16.1.1")
api("org.apache.derby:derbytools:10.16.1.1")
api("org.apache.httpcomponents.client5:httpclient5:5.4")
api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3")
api("org.apache.httpcomponents.client5:httpclient5:5.4.1")
api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3.1")
api("org.apache.poi:poi-ooxml:5.2.5")
api("org.apache.tomcat.embed:tomcat-embed-core:10.1.28")
api("org.apache.tomcat.embed:tomcat-embed-websocket:10.1.28")
@@ -116,7 +116,7 @@ dependencies {
api("org.codehaus.jettison:jettison:1.5.4")
api("org.crac:crac:1.4.0")
api("org.dom4j:dom4j:2.1.4")
api("org.eclipse.jetty:jetty-reactive-httpclient:4.0.7")
api("org.eclipse.jetty:jetty-reactive-httpclient:4.0.8")
api("org.eclipse.persistence:org.eclipse.persistence.jpa:3.0.4")
api("org.eclipse:yasson:2.0.4")
api("org.ehcache:ehcache:3.10.8")
@@ -131,7 +131,7 @@ dependencies {
api("org.hibernate:hibernate-validator:7.0.5.Final")
api("org.hsqldb:hsqldb:2.7.2")
api("org.javamoney:moneta:1.4.4")
api("org.jruby:jruby:9.4.8.0")
api("org.jruby:jruby:9.4.9.0")
api("org.junit.support:testng-engine:1.0.5")
api("org.mozilla:rhino:1.7.15")
api("org.ogce:xpp3:1.1.6")
+1 -1
View File
@@ -1,4 +1,4 @@
version=6.1.14-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
@@ -56,33 +56,33 @@ import kotlin.annotation.AnnotationTarget.TYPE
@SpringJUnitConfig(InterceptorConfig::class)
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
class AspectJAutoProxyInterceptorKotlinIntegrationTests(
@Autowired val echo: Echo,
@Autowired val firstAdvisor: TestPointcutAdvisor,
@Autowired val secondAdvisor: TestPointcutAdvisor,
@Autowired val countingAspect: CountingAspect,
@Autowired val reactiveTransactionManager: ReactiveCallCountingTransactionManager) {
@Autowired val echo: Echo,
@Autowired val firstAdvisor: TestPointcutAdvisor,
@Autowired val secondAdvisor: TestPointcutAdvisor,
@Autowired val countingAspect: CountingAspect,
@Autowired val reactiveTransactionManager: ReactiveCallCountingTransactionManager) {
@Test
fun `Multiple interceptors with regular function`() {
assertThat(firstAdvisor.interceptor.invocations).isEmpty()
assertThat(secondAdvisor.interceptor.invocations).isEmpty()
val value = "Hello!"
assertThat(echo.echo(value)).isEqualTo(value)
@Test
fun `Multiple interceptors with regular function`() {
assertThat(firstAdvisor.interceptor.invocations).isEmpty()
assertThat(secondAdvisor.interceptor.invocations).isEmpty()
val value = "Hello!"
assertThat(echo.echo(value)).isEqualTo(value)
assertThat(firstAdvisor.interceptor.invocations).singleElement().matches { String::class.java.isAssignableFrom(it) }
assertThat(secondAdvisor.interceptor.invocations).singleElement().matches { String::class.java.isAssignableFrom(it) }
}
}
@Test
fun `Multiple interceptors with suspending function`() {
assertThat(firstAdvisor.interceptor.invocations).isEmpty()
assertThat(secondAdvisor.interceptor.invocations).isEmpty()
val value = "Hello!"
runBlocking {
assertThat(echo.suspendingEcho(value)).isEqualTo(value)
}
@Test
fun `Multiple interceptors with suspending function`() {
assertThat(firstAdvisor.interceptor.invocations).isEmpty()
assertThat(secondAdvisor.interceptor.invocations).isEmpty()
val value = "Hello!"
runBlocking {
assertThat(echo.suspendingEcho(value)).isEqualTo(value)
}
assertThat(firstAdvisor.interceptor.invocations).singleElement().matches { Mono::class.java.isAssignableFrom(it) }
assertThat(secondAdvisor.interceptor.invocations).singleElement().matches { Mono::class.java.isAssignableFrom(it) }
}
}
@Test // gh-33095
fun `Aspect and reactive transactional with suspending function`() {
@@ -113,17 +113,17 @@ class AspectJAutoProxyInterceptorKotlinIntegrationTests(
assertThat(countingAspect.counter).`as`("aspect applied once per key").isEqualTo(2)
}
@Configuration
@EnableAspectJAutoProxy
@EnableTransactionManagement
@Configuration
@EnableAspectJAutoProxy
@EnableTransactionManagement
@EnableCaching
open class InterceptorConfig {
open class InterceptorConfig {
@Bean
open fun firstAdvisor() = TestPointcutAdvisor().apply { order = 0 }
@Bean
open fun firstAdvisor() = TestPointcutAdvisor().apply { order = 0 }
@Bean
open fun secondAdvisor() = TestPointcutAdvisor().apply { order = 1 }
@Bean
open fun secondAdvisor() = TestPointcutAdvisor().apply { order = 1 }
@Bean
open fun countingAspect() = CountingAspect()
@@ -138,34 +138,34 @@ class AspectJAutoProxyInterceptorKotlinIntegrationTests(
return ConcurrentMapCacheManager()
}
@Bean
open fun echo(): Echo {
return Echo()
}
}
@Bean
open fun echo(): Echo {
return Echo()
}
}
class TestMethodInterceptor: MethodInterceptor {
class TestMethodInterceptor: MethodInterceptor {
var invocations: MutableList<Class<*>> = mutableListOf()
var invocations: MutableList<Class<*>> = mutableListOf()
@Suppress("RedundantNullableReturnType")
override fun invoke(invocation: MethodInvocation): Any? {
val result = invocation.proceed()
invocations.add(result!!.javaClass)
return result
}
@Suppress("RedundantNullableReturnType")
override fun invoke(invocation: MethodInvocation): Any? {
val result = invocation.proceed()
invocations.add(result!!.javaClass)
return result
}
}
}
class TestPointcutAdvisor : StaticMethodMatcherPointcutAdvisor(TestMethodInterceptor()) {
class TestPointcutAdvisor : StaticMethodMatcherPointcutAdvisor(TestMethodInterceptor()) {
val interceptor: TestMethodInterceptor
get() = advice as TestMethodInterceptor
val interceptor: TestMethodInterceptor
get() = advice as TestMethodInterceptor
override fun matches(method: Method, targetClass: Class<*>): Boolean {
return targetClass == Echo::class.java && method.name.lowercase().endsWith("echo")
}
}
override fun matches(method: Method, targetClass: Class<*>): Boolean {
return targetClass == Echo::class.java && method.name.lowercase().endsWith("echo")
}
}
@Target(CLASS, FUNCTION, ANNOTATION_CLASS, TYPE)
@Retention(AnnotationRetention.RUNTIME)
@@ -185,16 +185,16 @@ class AspectJAutoProxyInterceptorKotlinIntegrationTests(
}
}
open class Echo {
open class Echo {
open fun echo(value: String): String {
return value
}
open fun echo(value: String): String {
return value
}
open suspend fun suspendingEcho(value: String): String {
delay(1)
return value
}
open suspend fun suspendingEcho(value: String): String {
delay(1)
return value
}
@Transactional
@Counting
@@ -212,6 +212,6 @@ class AspectJAutoProxyInterceptorKotlinIntegrationTests(
return "$value ${cacheCounter++}"
}
}
}
}
@@ -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;
}
}
}
}
@@ -18,6 +18,7 @@ package org.springframework.aop.aspectj.annotation;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.StringTokenizer;
@@ -37,6 +38,7 @@ import org.aspectj.lang.reflect.PerClauseKind;
import org.springframework.aop.framework.AopConfigException;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.SpringProperties;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.lang.Nullable;
@@ -58,6 +60,23 @@ public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFac
private static final Class<?>[] ASPECTJ_ANNOTATION_CLASSES = new Class<?>[] {
Pointcut.class, Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class};
private static final String AJC_MAGIC = "ajc$";
/**
* System property that instructs Spring to ignore ajc-compiled aspects
* for Spring AOP proxying, restoring traditional Spring behavior for
* scenarios where both weaving and AspectJ auto-proxying are enabled.
* <p>The default is "false". Consider switching this to "true" if you
* encounter double execution of your aspects in a given build setup.
* Note that we recommend restructuring your AspectJ configuration to
* avoid such double exposure of an AspectJ aspect to begin with.
* @since 6.1.15
*/
public static final String IGNORE_AJC_PROPERTY_NAME = "spring.aop.ajc.ignore";
private static final boolean shouldIgnoreAjcCompiledAspects =
SpringProperties.getFlag(IGNORE_AJC_PROPERTY_NAME);
/** Logger available to subclasses. */
protected final Log logger = LogFactory.getLog(getClass());
@@ -67,7 +86,8 @@ public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFac
@Override
public boolean isAspect(Class<?> clazz) {
return (AnnotationUtils.findAnnotation(clazz, Aspect.class) != null);
return (AnnotationUtils.findAnnotation(clazz, Aspect.class) != null &&
(!shouldIgnoreAjcCompiledAspects || !compiledByAjc(clazz)));
}
@Override
@@ -114,6 +134,15 @@ public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFac
}
}
private static boolean compiledByAjc(Class<?> clazz) {
for (Field field : clazz.getDeclaredFields()) {
if (field.getName().startsWith(AJC_MAGIC)) {
return true;
}
}
return false;
}
/**
* Enum for AspectJ annotation types.
@@ -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) {
}
}
}
@@ -31,17 +31,17 @@ import kotlin.coroutines.Continuation
*/
class AopUtilsKotlinTests {
@Test
fun `Invoking suspending function should return Mono`() {
val value = "foo"
val method = ReflectionUtils.findMethod(WithoutInterface::class.java, "handle",
@Test
fun `Invoking suspending function should return Mono`() {
val value = "foo"
val method = ReflectionUtils.findMethod(WithoutInterface::class.java, "handle",
String::class. java, Continuation::class.java)!!
val continuation = Continuation<Any>(CoroutineName("test")) { }
val continuation = Continuation<Any>(CoroutineName("test")) { }
val result = AopUtils.invokeJoinpointUsingReflection(WithoutInterface(), method, arrayOf(value, continuation))
assertThat(result).isInstanceOfSatisfying(Mono::class.java) {
assertThat(it.block()).isEqualTo(value)
}
}
assertThat(result).isInstanceOfSatisfying(Mono::class.java) {
assertThat(it.block()).isEqualTo(value)
}
}
@Test
fun `Invoking suspending function on bridged method should return Mono`() {
@@ -54,11 +54,11 @@ class AopUtilsKotlinTests {
}
}
@Suppress("unused")
suspend fun suspendingFunction(value: String): String {
delay(1)
return value
}
@Suppress("unused")
suspend fun suspendingFunction(value: String): String {
delay(1)
return value
}
class WithoutInterface {
suspend fun handle(value: String): String {
@@ -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.
@@ -28,10 +28,10 @@ import org.springframework.scheduling.annotation.Async;
* <p>This aspect routes methods marked with the {@link Async} annotation as well as methods
* in classes marked with the same. Any method expected to be routed asynchronously must
* return either {@code void}, {@link Future}, or a subtype of {@link Future} (in particular,
* Spring's {@link org.springframework.util.concurrent.ListenableFuture}). This aspect,
* therefore, will produce a compile-time error for methods that violate this constraint
* on the return type. If, however, a class marked with {@code @Async} contains a method
* that violates this constraint, it produces only a warning.
* {@link java.util.concurrent.CompletableFuture}). This aspect, therefore, will produce a
* compile-time error for methods that violate this constraint on the return type. If,
* however, a class marked with {@code @Async} contains a method that violates this
* constraint, it produces only a warning.
*
* <p>This aspect needs to be injected with an implementation of a task-oriented
* {@link java.util.concurrent.Executor} to activate it for a specific thread pool,
@@ -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 +
@@ -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.
@@ -92,9 +92,13 @@ public interface ListableBeanFactory extends BeanFactory {
* Return a provider for the specified bean, allowing for lazy on-demand retrieval
* of instances, including availability and uniqueness options.
* @param requiredType type the bean must match; can be an interface or superclass
* @param allowEagerInit whether stream-based access may initialize <i>lazy-init
* singletons</i> and <i>objects created by FactoryBeans</i> (or by factory methods
* with a "factory-bean" reference) for the type check
* @param allowEagerInit whether stream access may introspect <i>lazy-init singletons</i>
* and <i>objects created by FactoryBeans</i> - or by factory methods with a
* "factory-bean" reference - for the type check. Note that FactoryBeans need to be
* eagerly initialized to determine their type: So be aware that passing in "true"
* for this flag will initialize FactoryBeans and "factory-bean" references. Only
* actually necessary initialization for type checking purposes will be performed;
* constructor and method invocations will still be avoided as far as possible.
* @return a corresponding provider handle
* @since 5.3
* @see #getBeanProvider(ResolvableType, boolean)
@@ -112,9 +116,13 @@ public interface ListableBeanFactory extends BeanFactory {
* injection points. For programmatically retrieving a list of beans matching a
* specific type, specify the actual bean type as an argument here and subsequently
* use {@link ObjectProvider#orderedStream()} or its lazy streaming/iteration options.
* @param allowEagerInit whether stream-based access may initialize <i>lazy-init
* singletons</i> and <i>objects created by FactoryBeans</i> (or by factory methods
* with a "factory-bean" reference) for the type check
* @param allowEagerInit whether stream access may introspect <i>lazy-init singletons</i>
* and <i>objects created by FactoryBeans</i> - or by factory methods with a
* "factory-bean" reference - for the type check. Note that FactoryBeans need to be
* eagerly initialized to determine their type: So be aware that passing in "true"
* for this flag will initialize FactoryBeans and "factory-bean" references. Only
* actually necessary initialization for type checking purposes will be performed;
* constructor and method invocations will still be avoided as far as possible.
* @return a corresponding provider handle
* @since 5.3
* @see #getBeanProvider(ResolvableType)
@@ -175,11 +183,13 @@ public interface ListableBeanFactory extends BeanFactory {
* @param type the generically typed class or interface to match
* @param includeNonSingletons whether to include prototype or scoped beans too
* or just singletons (also applies to FactoryBeans)
* @param allowEagerInit whether to initialize <i>lazy-init singletons</i> and
* <i>objects created by FactoryBeans</i> (or by factory methods with a
* "factory-bean" reference) for the type check. Note that FactoryBeans need to be
* @param allowEagerInit whether to introspect <i>lazy-init singletons</i>
* and <i>objects created by FactoryBeans</i> - or by factory methods with a
* "factory-bean" reference - for the type check. Note that FactoryBeans need to be
* eagerly initialized to determine their type: So be aware that passing in "true"
* for this flag will initialize FactoryBeans and "factory-bean" references.
* for this flag will initialize FactoryBeans and "factory-bean" references. Only
* actually necessary initialization for type checking purposes will be performed;
* constructor and method invocations will still be avoided as far as possible.
* @return the names of beans (or objects created by FactoryBeans) matching
* the given object type (including subclasses), or an empty array if none
* @since 5.2
@@ -236,11 +246,13 @@ public interface ListableBeanFactory extends BeanFactory {
* @param type the class or interface to match, or {@code null} for all bean names
* @param includeNonSingletons whether to include prototype or scoped beans too
* or just singletons (also applies to FactoryBeans)
* @param allowEagerInit whether to initialize <i>lazy-init singletons</i> and
* <i>objects created by FactoryBeans</i> (or by factory methods with a
* "factory-bean" reference) for the type check. Note that FactoryBeans need to be
* @param allowEagerInit whether to introspect <i>lazy-init singletons</i>
* and <i>objects created by FactoryBeans</i> - or by factory methods with a
* "factory-bean" reference - for the type check. Note that FactoryBeans need to be
* eagerly initialized to determine their type: So be aware that passing in "true"
* for this flag will initialize FactoryBeans and "factory-bean" references.
* for this flag will initialize FactoryBeans and "factory-bean" references. Only
* actually necessary initialization for type checking purposes will be performed;
* constructor and method invocations will still be avoided as far as possible.
* @return the names of beans (or objects created by FactoryBeans) matching
* the given object type (including subclasses), or an empty array if none
* @see FactoryBean#getObjectType
@@ -300,11 +312,13 @@ public interface ListableBeanFactory extends BeanFactory {
* @param type the class or interface to match, or {@code null} for all concrete beans
* @param includeNonSingletons whether to include prototype or scoped beans too
* or just singletons (also applies to FactoryBeans)
* @param allowEagerInit whether to initialize <i>lazy-init singletons</i> and
* <i>objects created by FactoryBeans</i> (or by factory methods with a
* "factory-bean" reference) for the type check. Note that FactoryBeans need to be
* @param allowEagerInit whether to introspect <i>lazy-init singletons</i>
* and <i>objects created by FactoryBeans</i> - or by factory methods with a
* "factory-bean" reference - for the type check. Note that FactoryBeans need to be
* eagerly initialized to determine their type: So be aware that passing in "true"
* for this flag will initialize FactoryBeans and "factory-bean" references.
* for this flag will initialize FactoryBeans and "factory-bean" references. Only
* actually necessary initialization for type checking purposes will be performed;
* constructor and method invocations will still be avoided as far as possible.
* @return a Map with the matching beans, containing the bean names as
* keys and the corresponding bean instances as values
* @throws BeansException if a bean could not be created
@@ -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);
}
}
@@ -1776,12 +1781,15 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
* @param requiredType the target dependency type to match against
* @return the name of the candidate with the highest priority,
* or {@code null} if none found
* @throws NoUniqueBeanDefinitionException if multiple beans are detected with
* the same highest priority value
* @see #getPriority(Object)
*/
@Nullable
protected String determineHighestPriorityCandidate(Map<String, Object> candidates, Class<?> requiredType) {
String highestPriorityBeanName = null;
Integer highestPriority = null;
boolean highestPriorityConflictDetected = false;
for (Map.Entry<String, Object> entry : candidates.entrySet()) {
String candidateBeanName = entry.getKey();
Object beanInstance = entry.getValue();
@@ -1790,13 +1798,12 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
if (candidatePriority != null) {
if (highestPriority != null) {
if (candidatePriority.equals(highestPriority)) {
throw new NoUniqueBeanDefinitionException(requiredType, candidates.size(),
"Multiple beans found with the same priority ('" + highestPriority +
"') among candidates: " + candidates.keySet());
highestPriorityConflictDetected = true;
}
else if (candidatePriority < highestPriority) {
highestPriorityBeanName = candidateBeanName;
highestPriority = candidatePriority;
highestPriorityConflictDetected = false;
}
}
else {
@@ -1806,6 +1813,13 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
}
}
}
if (highestPriorityConflictDetected) {
throw new NoUniqueBeanDefinitionException(requiredType, candidates.size(),
"Multiple beans found with the same highest priority (" + highestPriority +
") among candidates: " + candidates.keySet());
}
return highestPriorityBeanName;
}
@@ -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);
@@ -1729,18 +1734,73 @@ class DefaultListableBeanFactoryTests {
assertThat(bean.getBeanName()).isEqualTo("bd1");
}
/**
* {@code determineHighestPriorityCandidate()} should reject duplicate
* priorities for the highest priority detected.
*
* @see #getBeanByTypeWithMultipleNonHighestPriorityCandidates()
*/
@Test
void getBeanByTypeWithMultiplePriority() {
void getBeanByTypeWithMultipleHighestPriorityCandidates() {
lbf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE);
RootBeanDefinition bd1 = new RootBeanDefinition(HighPriorityTestBean.class);
RootBeanDefinition bd2 = new RootBeanDefinition(HighPriorityTestBean.class);
RootBeanDefinition bd2 = new RootBeanDefinition(LowPriorityTestBean.class);
RootBeanDefinition bd3 = new RootBeanDefinition(HighPriorityTestBean.class);
lbf.registerBeanDefinition("bd1", bd1);
lbf.registerBeanDefinition("bd2", bd2);
lbf.registerBeanDefinition("bd3", bd3);
assertThatExceptionOfType(NoUniqueBeanDefinitionException.class)
.isThrownBy(() -> lbf.getBean(TestBean.class))
.withMessageContaining("Multiple beans found with the same priority")
.withMessageContaining("5"); // conflicting priority
.withMessageContaining("Multiple beans found with the same highest priority (5) among candidates: ");
}
/**
* {@code determineHighestPriorityCandidate()} should ignore duplicate
* priorities for any priority other than the highest, and the order in
* which beans is declared should not affect the outcome.
*
* @see #getBeanByTypeWithMultipleHighestPriorityCandidates()
*/
@Test // gh-33733
void getBeanByTypeWithMultipleNonHighestPriorityCandidates() {
getBeanByTypeWithMultipleNonHighestPriorityCandidates(
PriorityService1.class,
PriorityService2A.class,
PriorityService2B.class,
PriorityService3.class
);
getBeanByTypeWithMultipleNonHighestPriorityCandidates(
PriorityService3.class,
PriorityService2B.class,
PriorityService2A.class,
PriorityService1.class
);
getBeanByTypeWithMultipleNonHighestPriorityCandidates(
PriorityService2A.class,
PriorityService1.class,
PriorityService2B.class,
PriorityService3.class
);
getBeanByTypeWithMultipleNonHighestPriorityCandidates(
PriorityService2A.class,
PriorityService3.class,
PriorityService1.class,
PriorityService2B.class
);
}
private void getBeanByTypeWithMultipleNonHighestPriorityCandidates(Class<?>... classes) {
lbf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE);
for (Class<?> clazz : classes) {
lbf.registerBeanDefinition(clazz.getSimpleName(), new RootBeanDefinition(clazz));
}
PriorityService bean = lbf.getBean(PriorityService.class);
assertThat(bean).isExactlyInstanceOf(PriorityService1.class);
}
@Test
@@ -3500,6 +3560,26 @@ class DefaultListableBeanFactoryTests {
}
interface PriorityService {
}
@Priority(1)
static class PriorityService1 implements PriorityService {
}
@Priority(2)
static class PriorityService2A implements PriorityService {
}
@Priority(2)
static class PriorityService2B implements PriorityService {
}
@Priority(3)
static class PriorityService3 implements PriorityService {
}
@Priority(5)
private static class HighPriorityTestBean extends TestBean {
}
@@ -45,99 +45,99 @@ import javax.lang.model.element.Modifier
*/
class InstanceSupplierCodeGeneratorKotlinTests {
private val generationContext = TestGenerationContext()
private val generationContext = TestGenerationContext()
@Test
fun generateWhenHasDefaultConstructor() {
val beanDefinition: BeanDefinition = RootBeanDefinition(KotlinTestBean::class.java)
val beanFactory = DefaultListableBeanFactory()
compile(beanFactory, beanDefinition) { instanceSupplier, compiled ->
val bean = getBean<KotlinTestBean>(beanFactory, beanDefinition, instanceSupplier)
Assertions.assertThat(bean).isInstanceOf(KotlinTestBean::class.java)
Assertions.assertThat(compiled.sourceFile).contains("InstanceSupplier.using(KotlinTestBean::new)")
}
Assertions.assertThat(getReflectionHints().getTypeHint(KotlinTestBean::class.java))
.satisfies(hasConstructorWithMode(ExecutableMode.INTROSPECT))
}
@Test
fun generateWhenHasDefaultConstructor() {
val beanDefinition: BeanDefinition = RootBeanDefinition(KotlinTestBean::class.java)
val beanFactory = DefaultListableBeanFactory()
compile(beanFactory, beanDefinition) { instanceSupplier, compiled ->
val bean = getBean<KotlinTestBean>(beanFactory, beanDefinition, instanceSupplier)
Assertions.assertThat(bean).isInstanceOf(KotlinTestBean::class.java)
Assertions.assertThat(compiled.sourceFile).contains("InstanceSupplier.using(KotlinTestBean::new)")
}
Assertions.assertThat(getReflectionHints().getTypeHint(KotlinTestBean::class.java))
.satisfies(hasConstructorWithMode(ExecutableMode.INTROSPECT))
}
@Test
fun generateWhenConstructorHasOptionalParameter() {
val beanDefinition: BeanDefinition = RootBeanDefinition(KotlinTestBeanWithOptionalParameter::class.java)
val beanFactory = DefaultListableBeanFactory()
compile(beanFactory, beanDefinition) { instanceSupplier, compiled ->
val bean: KotlinTestBeanWithOptionalParameter = getBean(beanFactory, beanDefinition, instanceSupplier)
Assertions.assertThat(bean).isInstanceOf(KotlinTestBeanWithOptionalParameter::class.java)
Assertions.assertThat(compiled.sourceFile)
.contains("return BeanInstanceSupplier.<KotlinTestBeanWithOptionalParameter>forConstructor();")
}
Assertions.assertThat<TypeHint>(getReflectionHints().getTypeHint(KotlinTestBeanWithOptionalParameter::class.java))
.satisfies(hasMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS))
}
@Test
fun generateWhenConstructorHasOptionalParameter() {
val beanDefinition: BeanDefinition = RootBeanDefinition(KotlinTestBeanWithOptionalParameter::class.java)
val beanFactory = DefaultListableBeanFactory()
compile(beanFactory, beanDefinition) { instanceSupplier, compiled ->
val bean: KotlinTestBeanWithOptionalParameter = getBean(beanFactory, beanDefinition, instanceSupplier)
Assertions.assertThat(bean).isInstanceOf(KotlinTestBeanWithOptionalParameter::class.java)
Assertions.assertThat(compiled.sourceFile)
.contains("return BeanInstanceSupplier.<KotlinTestBeanWithOptionalParameter>forConstructor();")
}
Assertions.assertThat<TypeHint>(getReflectionHints().getTypeHint(KotlinTestBeanWithOptionalParameter::class.java))
.satisfies(hasMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS))
}
private fun getReflectionHints(): ReflectionHints {
return generationContext.runtimeHints.reflection()
}
private fun getReflectionHints(): ReflectionHints {
return generationContext.runtimeHints.reflection()
}
private fun hasConstructorWithMode(mode: ExecutableMode): ThrowingConsumer<TypeHint> {
return ThrowingConsumer {
Assertions.assertThat(it.constructors()).anySatisfy(hasMode(mode))
}
}
private fun hasConstructorWithMode(mode: ExecutableMode): ThrowingConsumer<TypeHint> {
return ThrowingConsumer {
Assertions.assertThat(it.constructors()).anySatisfy(hasMode(mode))
}
}
private fun hasMemberCategory(category: MemberCategory): ThrowingConsumer<TypeHint> {
return ThrowingConsumer {
Assertions.assertThat(it.memberCategories).contains(category)
}
}
private fun hasMemberCategory(category: MemberCategory): ThrowingConsumer<TypeHint> {
return ThrowingConsumer {
Assertions.assertThat(it.memberCategories).contains(category)
}
}
private fun hasMode(mode: ExecutableMode): ThrowingConsumer<ExecutableHint> {
return ThrowingConsumer {
Assertions.assertThat(it.mode).isEqualTo(mode)
}
}
private fun hasMode(mode: ExecutableMode): ThrowingConsumer<ExecutableHint> {
return ThrowingConsumer {
Assertions.assertThat(it.mode).isEqualTo(mode)
}
}
@Suppress("UNCHECKED_CAST")
private fun <T> getBean(beanFactory: DefaultListableBeanFactory, beanDefinition: BeanDefinition,
instanceSupplier: InstanceSupplier<*>): T {
(beanDefinition as RootBeanDefinition).instanceSupplier = instanceSupplier
beanFactory.registerBeanDefinition("testBean", beanDefinition)
return beanFactory.getBean("testBean") as T
}
@Suppress("UNCHECKED_CAST")
private fun <T> getBean(beanFactory: DefaultListableBeanFactory, beanDefinition: BeanDefinition,
instanceSupplier: InstanceSupplier<*>): T {
(beanDefinition as RootBeanDefinition).instanceSupplier = instanceSupplier
beanFactory.registerBeanDefinition("testBean", beanDefinition)
return beanFactory.getBean("testBean") as T
}
private fun compile(beanFactory: DefaultListableBeanFactory, beanDefinition: BeanDefinition,
result: BiConsumer<InstanceSupplier<*>, Compiled>) {
private fun compile(beanFactory: DefaultListableBeanFactory, beanDefinition: BeanDefinition,
result: BiConsumer<InstanceSupplier<*>, Compiled>) {
val freshBeanFactory = DefaultListableBeanFactory(beanFactory)
freshBeanFactory.registerBeanDefinition("testBean", beanDefinition)
val registeredBean = RegisteredBean.of(freshBeanFactory, "testBean")
val typeBuilder = DeferredTypeBuilder()
val generateClass = generationContext.generatedClasses.addForFeature("TestCode", typeBuilder)
val generator = InstanceSupplierCodeGenerator(
generationContext, generateClass.name,
generateClass.methods, false
)
val instantiationDescriptor = registeredBean.resolveInstantiationDescriptor()
Assertions.assertThat(instantiationDescriptor).isNotNull()
val generatedCode = generator.generateCode(registeredBean, instantiationDescriptor)
typeBuilder.set { type: TypeSpec.Builder ->
type.addModifiers(Modifier.PUBLIC)
type.addSuperinterface(
ParameterizedTypeName.get(
Supplier::class.java,
InstanceSupplier::class.java
)
)
type.addMethod(
MethodSpec.methodBuilder("get")
.addModifiers(Modifier.PUBLIC)
.returns(InstanceSupplier::class.java)
.addStatement("return \$L", generatedCode).build()
)
}
generationContext.writeGeneratedContent()
TestCompiler.forSystem().with(generationContext).compile {
result.accept(it.getInstance(Supplier::class.java).get() as InstanceSupplier<*>, it)
}
}
val freshBeanFactory = DefaultListableBeanFactory(beanFactory)
freshBeanFactory.registerBeanDefinition("testBean", beanDefinition)
val registeredBean = RegisteredBean.of(freshBeanFactory, "testBean")
val typeBuilder = DeferredTypeBuilder()
val generateClass = generationContext.generatedClasses.addForFeature("TestCode", typeBuilder)
val generator = InstanceSupplierCodeGenerator(
generationContext, generateClass.name,
generateClass.methods, false
)
val instantiationDescriptor = registeredBean.resolveInstantiationDescriptor()
Assertions.assertThat(instantiationDescriptor).isNotNull()
val generatedCode = generator.generateCode(registeredBean, instantiationDescriptor)
typeBuilder.set { type: TypeSpec.Builder ->
type.addModifiers(Modifier.PUBLIC)
type.addSuperinterface(
ParameterizedTypeName.get(
Supplier::class.java,
InstanceSupplier::class.java
)
)
type.addMethod(
MethodSpec.methodBuilder("get")
.addModifiers(Modifier.PUBLIC)
.returns(InstanceSupplier::class.java)
.addStatement("return \$L", generatedCode).build()
)
}
generationContext.writeGeneratedContent()
TestCompiler.forSystem().with(generationContext).compile {
result.accept(it.getInstance(Supplier::class.java).get() as InstanceSupplier<*>, it)
}
}
}
@@ -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-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.
@@ -303,6 +303,17 @@ public class CaffeineCacheManager implements CacheManager {
this.cacheMap.put(name, adaptCaffeineCache(name, cache));
}
/**
* Remove the specified cache from this cache manager, applying to
* custom caches as well as dynamically registered caches at runtime.
* @param name the name of the cache
* @since 6.1.15
*/
public void removeCache(String name) {
this.customCacheNames.remove(name);
this.cacheMap.remove(name);
}
/**
* Adapt the given new native Caffeine Cache instance to Spring's {@link Cache}
* abstraction for the specified cache name.
@@ -32,6 +32,10 @@ import org.springframework.lang.Nullable;
* {@link #cacheManager()}, {@link #cacheResolver()}, {@link #keyGenerator()}, and
* {@link #errorHandler()} for detailed instructions.
*
* <p><b>NOTE: A {@code CachingConfigurer} will get initialized early.</b>
* Do not inject common dependencies into autowired fields directly; instead, consider
* declaring a lazy {@link org.springframework.beans.factory.ObjectProvider} for those.
*
* @author Chris Beams
* @author Stephane Nicoll
* @since 3.1
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -175,6 +175,15 @@ public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderA
return cache;
}
/**
* Remove the specified cache from this cache manager.
* @param name the name of the cache
* @since 6.1.15
*/
public void removeCache(String name) {
this.cacheMap.remove(name);
}
private void recreateCaches() {
for (Map.Entry<String, Cache> entry : this.cacheMap.entrySet()) {
entry.setValue(createConcurrentMapCache(entry.getKey()));
@@ -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-2021 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.
@@ -25,15 +25,29 @@ import java.lang.annotation.Target;
/**
* Declares that a field or method parameter should be formatted as a date or time.
*
* <p>Supports formatting by style pattern, ISO date time pattern, or custom format pattern string.
* <p>Formatting applies to parsing a date/time object from a string as well as printing a
* date/time object to a string.
*
* <p>Supports formatting by style pattern, ISO date/time pattern, or custom format pattern string.
* Can be applied to {@link java.util.Date}, {@link java.util.Calendar}, {@link Long} (for
* millisecond timestamps) as well as JSR-310 {@code java.time} value types.
*
* <p>For style-based formatting, set the {@link #style} attribute to the desired style pattern code.
* The first character of the code is the date style, and the second character is the time style.
* Specify a character of 'S' for short style, 'M' for medium, 'L' for long, and 'F' for full.
* The date or time may be omitted by specifying the style character '-' &mdash; for example,
* 'M-' specifies a medium format for the date with no time.
* The date or time may be omitted by specifying the style character '-'. For example,
* 'M-' specifies a medium format for the date with no time. The supported style pattern codes
* correlate to the enum constants defined in {@link java.time.format.FormatStyle}.
*
* <p><strong>WARNING</strong>: Style-based formatting and parsing rely on locale-sensitive
* patterns which may change depending on the Java runtime. Specifically, applications that
* rely on date/time parsing and formatting may encounter incompatible changes in behavior
* when running on JDK 20 or higher. Using an ISO standardized format or a concrete pattern
* that you control allows for reliable system-independent and locale-independent parsing and
* formatting of date/time values. The use of {@linkplain #fallbackPatterns() fallback patterns}
* can also help to address compatibility issues. For further details, see the
* <a href="https://github.com/spring-projects/spring-framework/wiki/Date-and-Time-Formatting-with-JDK-20-and-higher">
* Date and Time Formatting with JDK 20 and higher</a> page in the Spring Framework wiki.
*
* <p>For ISO-based formatting, set the {@link #iso} attribute to the desired {@link ISO} format,
* such as {@link ISO#DATE}.
@@ -65,6 +79,8 @@ import java.lang.annotation.Target;
* @author Juergen Hoeller
* @author Sam Brannen
* @since 3.0
* @see java.text.DateFormat
* @see java.text.SimpleDateFormat
* @see java.time.format.DateTimeFormatter
*/
@Documented
@@ -77,6 +93,8 @@ public @interface DateTimeFormat {
* <p>Defaults to 'SS' for short date, short time. Set this attribute when you
* wish to format your field or method parameter in accordance with a common
* style other than the default style.
* <p>See the {@linkplain DateTimeFormat class-level documentation} for further
* details.
* @see #fallbackPatterns
*/
String style() default "SS";
@@ -93,13 +111,13 @@ public @interface DateTimeFormat {
/**
* The custom pattern to use to format the field or method parameter.
* <p>Defaults to empty String, indicating no custom pattern String has been
* <p>Defaults to an empty String, indicating no custom pattern String has been
* specified. Set this attribute when you wish to format your field or method
* parameter in accordance with a custom date time pattern not represented by
* a style or ISO format.
* <p>Note: This pattern follows the original {@link java.text.SimpleDateFormat} style,
* as also supported by Joda-Time, with strict parsing semantics towards overflows
* (e.g. rejecting a Feb 29 value for a non-leap-year). As a consequence, 'yy'
* <p>Note: This pattern follows the original {@link java.text.SimpleDateFormat}
* style, with strict parsing semantics towards overflows (for example, rejecting
* a {@code Feb 29} value for a non-leap-year). As a consequence, 'yy'
* characters indicate a year in the traditional style, not a "year-of-era" as in the
* {@link java.time.format.DateTimeFormatter} specification (i.e. 'yy' turns into 'uu'
* when going through a {@code DateTimeFormatter} with strict resolution mode).
@@ -121,7 +139,6 @@ public @interface DateTimeFormat {
* or {@link #style} attribute is always used for printing. For details on
* which time zone is used for fallback patterns, see the
* {@linkplain DateTimeFormat class-level documentation}.
* <p>Fallback patterns are not supported for Joda-Time value types.
* @since 5.3.5
*/
String[] fallbackPatterns() default {};
@@ -181,7 +181,6 @@ public class DateFormatter implements Formatter<Date> {
* <li>'F' = Full</li>
* <li>'-' = Omitted</li>
* </ul>
* This method mimics the styles supported by Joda-Time.
* @param stylePattern two characters from the set {"S", "M", "L", "F", "-"}
* @since 3.2
*/
@@ -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.
@@ -116,7 +116,7 @@ public class DateTimeFormatterFactory {
}
/**
* Set the two characters to use to format date values, in Joda-Time style.
* Set the two characters to use to format date values.
* <p>The first character is used for the date style; the second is for
* the time style. Supported characters are:
* <ul>
@@ -126,9 +126,9 @@ public class DateTimeFormatterFactory {
* <li>'F' = Full</li>
* <li>'-' = Omitted</li>
* </ul>
* <p>This method mimics the styles supported by Joda-Time. Note that
* JSR-310 natively favors {@link java.time.format.FormatStyle} as used for
* {@link #setDateStyle}, {@link #setTimeStyle} and {@link #setDateTimeStyle}.
* <p>Note that JSR-310 natively favors {@link java.time.format.FormatStyle}
* as used for {@link #setDateStyle}, {@link #setTimeStyle}, and
* {@link #setDateTimeStyle}.
* @param style two characters from the set {"S", "M", "L", "F", "-"}
*/
public void setStylePattern(String style) {
@@ -38,8 +38,8 @@ abstract class DateTimeFormatterUtils {
* @see ResolverStyle#STRICT
*/
static DateTimeFormatter createStrictDateTimeFormatter(String pattern) {
// Using strict resolution to align with Joda-Time and standard DateFormat behavior:
// otherwise, an overflow like e.g. Feb 29 for a non-leap-year wouldn't get rejected.
// Using strict resolution to align with standard DateFormat behavior:
// otherwise, an overflow like, for example, Feb 29 for a non-leap-year wouldn't get rejected.
// However, with strict resolution, a year digit needs to be specified as 'u'...
String patternToUse = StringUtils.replace(pattern, "yy", "uu");
return DateTimeFormatter.ofPattern(patternToUse).withResolverStyle(ResolverStyle.STRICT);
@@ -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.
@@ -97,7 +97,7 @@ public class DefaultFormattingConversionService extends FormattingConversionServ
/**
* Add formatters appropriate for most environments: including number formatters,
* JSR-354 Money &amp; Currency formatters, JSR-310 Date-Time and/or Joda-Time formatters,
* JSR-354 Money &amp; Currency formatters, and JSR-310 Date-Time formatters,
* depending on the presence of the corresponding API on the classpath.
* @param formatterRegistry the service to register default formatters with
*/
@@ -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.
@@ -32,24 +32,19 @@ import org.springframework.lang.Nullable;
import org.springframework.util.StringValueResolver;
/**
* A factory providing convenient access to a {@code FormattingConversionService}
* configured with converters and formatters for common types such as numbers and
* datetimes.
* A factory providing convenient access to a {@link FormattingConversionService}
* configured with converters and formatters for common types such as numbers, dates,
* and times.
*
* <p>Additional converters and formatters can be registered declaratively through
* {@link #setConverters(Set)} and {@link #setFormatters(Set)}. Another option
* is to register converters and formatters in code by implementing the
* {@link FormatterRegistrar} interface. You can then configure provide the set
* of registrars to use through {@link #setFormatterRegistrars(Set)}.
*
* <p>A good example for registering converters and formatters in code is
* {@code JodaTimeFormatterRegistrar}, which registers a number of
* date-related formatters and converters. For a more detailed list of cases
* see {@link #setFormatterRegistrars(Set)}
* {@link FormatterRegistrar} interface. You can then provide the set of registrars
* to use through {@link #setFormatterRegistrars(Set)}.
*
* <p>Like all {@code FactoryBean} implementations, this class is suitable for
* use when configuring a Spring application context using Spring {@code <beans>}
* XML. When configuring the container with
* XML configuration files. When configuring the container with
* {@link org.springframework.context.annotation.Configuration @Configuration}
* classes, simply instantiate, configure and return the appropriate
* {@code FormattingConversionService} object from a
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 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.instrument.classloading.jboss;
import java.lang.instrument.ClassFileTransformer;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
@@ -26,13 +27,14 @@ import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.function.ThrowingFunction;
/**
* {@link LoadTimeWeaver} implementation for JBoss's instrumentable ClassLoader.
* Thanks to Ales Justin and Marius Bogoevici for the initial prototype.
*
* <p>As of Spring Framework 5.0, this weaver supports WildFly 8+.
* As of Spring Framework 5.1.5, it also supports WildFly 13+.
* <p>This weaver supports WildFly 13-23 (DelegatingClassFileTransformer) as well as
* WildFly 24+ (DelegatingClassTransformer), as of Spring Framework 6.1.15.
*
* @author Costin Leau
* @author Juergen Hoeller
@@ -40,9 +42,15 @@ import org.springframework.util.ReflectionUtils;
*/
public class JBossLoadTimeWeaver implements LoadTimeWeaver {
private static final String DELEGATING_TRANSFORMER_CLASS_NAME =
private static final String LEGACY_DELEGATING_TRANSFORMER_CLASS_NAME =
"org.jboss.as.server.deployment.module.DelegatingClassFileTransformer";
private static final String DELEGATING_TRANSFORMER_CLASS_NAME =
"org.jboss.as.server.deployment.module.DelegatingClassTransformer";
private static final String CLASS_TRANSFORMER_CLASS_NAME =
"org.jboss.modules.ClassTransformer";
private static final String WRAPPER_TRANSFORMER_CLASS_NAME =
"org.jboss.modules.JLIClassTransformer";
@@ -53,6 +61,8 @@ public class JBossLoadTimeWeaver implements LoadTimeWeaver {
private final Method addTransformer;
private final ThrowingFunction<Object, Object> adaptTransformer;
/**
* Create a new instance of the {@link JBossLoadTimeWeaver} class using
@@ -91,18 +101,29 @@ public class JBossLoadTimeWeaver implements LoadTimeWeaver {
wrappedTransformer.setAccessible(true);
suggestedTransformer = wrappedTransformer.get(suggestedTransformer);
}
if (!suggestedTransformer.getClass().getName().equals(DELEGATING_TRANSFORMER_CLASS_NAME)) {
Class<?> transformerType = ClassFileTransformer.class;
if (suggestedTransformer.getClass().getName().equals(LEGACY_DELEGATING_TRANSFORMER_CLASS_NAME)) {
this.adaptTransformer = (t -> t);
}
else if (suggestedTransformer.getClass().getName().equals(DELEGATING_TRANSFORMER_CLASS_NAME)) {
transformerType = classLoader.loadClass(CLASS_TRANSFORMER_CLASS_NAME);
Constructor<?> adaptedTransformer = classLoader.loadClass(WRAPPER_TRANSFORMER_CLASS_NAME)
.getConstructor(ClassFileTransformer.class);
this.adaptTransformer = adaptedTransformer::newInstance;
}
else {
throw new IllegalStateException(
"Transformer not of the expected type DelegatingClassFileTransformer: " +
"Transformer not of expected type DelegatingClass(File)Transformer: " +
suggestedTransformer.getClass().getName());
}
this.delegatingTransformer = suggestedTransformer;
Method addTransformer = ReflectionUtils.findMethod(this.delegatingTransformer.getClass(),
"addTransformer", ClassFileTransformer.class);
"addTransformer", transformerType);
if (addTransformer == null) {
throw new IllegalArgumentException(
"Could not find 'addTransformer' method on JBoss DelegatingClassFileTransformer: " +
"Could not find 'addTransformer' method on JBoss DelegatingClass(File)Transformer: " +
this.delegatingTransformer.getClass().getName());
}
addTransformer.setAccessible(true);
@@ -117,7 +138,7 @@ public class JBossLoadTimeWeaver implements LoadTimeWeaver {
@Override
public void addTransformer(ClassFileTransformer transformer) {
try {
this.addTransformer.invoke(this.delegatingTransformer, transformer);
this.addTransformer.invoke(this.delegatingTransformer, this.adaptTransformer.apply(transformer));
}
catch (Throwable ex) {
throw new IllegalStateException("Could not add transformer on JBoss ClassLoader: " + this.classLoader, ex);
@@ -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.
@@ -35,17 +35,18 @@ import org.springframework.aot.hint.annotation.Reflective;
* <p>In terms of target method signatures, any parameter types are supported.
* However, the return type is constrained to either {@code void} or
* {@link java.util.concurrent.Future}. In the latter case, you may declare the
* more specific {@link org.springframework.util.concurrent.ListenableFuture} or
* {@link java.util.concurrent.CompletableFuture} types which allow for richer
* interaction with the asynchronous task and for immediate composition with
* further processing steps.
* more specific {@link java.util.concurrent.CompletableFuture} type which allows
* for richer interaction with the asynchronous task and for immediate composition
* with further processing steps.
*
* <p>A {@code Future} handle returned from the proxy will be an actual asynchronous
* {@code Future} that can be used to track the result of the asynchronous method
* execution. However, since the target method needs to implement the same signature,
* it will have to return a temporary {@code Future} handle that just passes a value
* through: for example, Spring's {@link AsyncResult}, EJB 3.1's {@link jakarta.ejb.AsyncResult},
* or {@link java.util.concurrent.CompletableFuture#completedFuture(Object)}.
* {@code (Completable)Future} that can be used to track the result of the
* asynchronous method execution. However, since the target method needs to implement
* the same signature, it will have to return a temporary {@code Future} handle that
* just passes a value after computation in the execution thread: typically through
* {@link java.util.concurrent.CompletableFuture#completedFuture(Object)}. The
* provided value will be exposed to the caller through the actual asynchronous
* {@code Future} handle at runtime.
*
* @author Juergen Hoeller
* @author Chris Beams
@@ -78,7 +78,9 @@ import org.springframework.util.ErrorHandler;
* but rather just the hand-off to an execution thread.</b> As a consequence,
* a {@link ScheduledFuture} handle (e.g. from {@link #schedule(Runnable, Instant)})
* represents that hand-off rather than the actual completion of the provided task
* (or series of repeated tasks).
* (or series of repeated tasks). Also, this scheduler participates in lifecycle
* management to a limited degree only, stopping trigger firing and fixed-delay
* task execution but not stopping the execution of handed-off tasks.
*
* <p>As an alternative to the built-in thread-per-task capability, this scheduler
* can also be configured with a separate target executor for scheduled task
@@ -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);
}
}
}
@@ -86,7 +86,7 @@ class CacheOperationExpressionEvaluatorTests {
AnnotatedClass target = new AnnotatedClass();
Method method = ReflectionUtils.findMethod(
AnnotatedClass.class, "multipleCaching", Object.class, Object.class);
Object[] args = new Object[] {new Object(), new Object()};
Object[] args = {"arg1", "arg2"};
Collection<ConcurrentMapCache> caches = Collections.singleton(new ConcurrentMapCache("test"));
EvaluationContext evalCtx = this.eval.createEvaluationContext(caches, method, args,
@@ -155,7 +155,7 @@ class CacheOperationExpressionEvaluatorTests {
AnnotatedClass target = new AnnotatedClass();
Method method = ReflectionUtils.findMethod(
AnnotatedClass.class, "multipleCaching", Object.class, Object.class);
Object[] args = new Object[] {new Object(), new Object()};
Object[] args = new Object[] {"arg1", "arg2"};
Collection<ConcurrentMapCache> caches = Collections.singleton(new ConcurrentMapCache("test"));
return this.eval.createEvaluationContext(
caches, method, args, target, target.getClass(), method, 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.
@@ -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);
}
}
}
@@ -339,6 +339,70 @@ class DateFormattingTests {
assertThat(bindingResult.getFieldValue(propertyName)).isEqualTo("2021-03-02");
}
/**
* {@link SimpleDateBean#styleDateTimeWithFallbackPatternsForPreAndPostJdk20}
* configures "SS" as the date/time style to use. Thus, we have to be aware
* of the following if we do not configure fallback patterns for parsing.
*
* <ul>
* <li>JDK &le; 19 requires a standard space before the "PM".
* <li>JDK &ge; 20 requires a narrow non-breaking space (NNBSP) before the "PM".
* </ul>
*
* <p>To avoid compatibility issues between JDK versions, we have configured
* two fallback patterns which emulate the "SS" style: <code>"MM/dd/yy h:mm a"</code>
* matches against a standard space before the "PM", and <code>"MM/dd/yy h:mm&#92;u202Fa"</code>
* matches against a narrow non-breaking space (NNBSP) before the "PM".
*
* <p>Thus, the following should theoretically be supported on any JDK (or at least
* JDK 17 - 23, where we have tested it).
*
* @see #patternDateTime(String)
*/
@ParameterizedTest(name = "input date: {0}") // gh-33151
@ValueSource(strings = {"10/31/09, 12:00 PM", "10/31/09, 12:00\u202FPM"})
void styleDateTime_PreAndPostJdk20(String propertyValue) {
String propertyName = "styleDateTimeWithFallbackPatternsForPreAndPostJdk20";
MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add(propertyName, propertyValue);
binder.bind(propertyValues);
BindingResult bindingResult = binder.getBindingResult();
assertThat(bindingResult.getErrorCount()).isEqualTo(0);
String value = binder.getBindingResult().getFieldValue(propertyName).toString();
// Since the "SS" style is always used for printing and the underlying format
// changes depending on the JDK version, we cannot be certain that a normal
// space is used before the "PM". Consequently we have to use a regular
// expression to match against any Unicode space character (\p{Zs}).
assertThat(value).startsWith("10/31/09").matches(".+?12:00\\p{Zs}PM");
}
/**
* To avoid the use of Locale-based styles (such as "MM") for
* {@link SimpleDateBean#patternDateTimeWithFallbackPatternForPreAndPostJdk20}, we have configured a
* primary pattern (<code>"MM/dd/yy h:mm a"</code>) that matches against a standard space
* before the "PM" and a fallback pattern (<code>"MM/dd/yy h:mm&#92;u202Fa"</code> that matches
* against a narrow non-breaking space (NNBSP) before the "PM".
*
* <p>Thus, the following should theoretically be supported on any JDK (or at least
* JDK 17 - 23, where we have tested it).
*
* @see #styleDateTime(String)
*/
@ParameterizedTest(name = "input date: {0}") // gh-33151
@ValueSource(strings = {"10/31/09 3:45 PM", "10/31/09 3:45\u202FPM"})
void patternDateTime_PreAndPostJdk20(String propertyValue) {
String propertyName = "patternDateTimeWithFallbackPatternForPreAndPostJdk20";
MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add(propertyName, propertyValue);
binder.bind(propertyValues);
BindingResult bindingResult = binder.getBindingResult();
assertThat(bindingResult.getErrorCount()).isEqualTo(0);
String value = binder.getBindingResult().getFieldValue(propertyName).toString();
// Since the "MM/dd/yy h:mm a" primary pattern is always used for printing, we
// can be certain that a normal space is used before the "PM".
assertThat(value).matches("10/31/09 3:45 PM");
}
@Test
void patternDateWithUnsupportedPattern() {
String propertyValue = "210302";
@@ -389,12 +453,23 @@ class DateFormattingTests {
@DateTimeFormat(style = "S-", fallbackPatterns = { "yyyy-MM-dd", "yyyyMMdd", "yyyy.MM.dd" })
private Date styleDateWithFallbackPatterns;
// "SS" style matches either a standard space or a narrow non-breaking space (NNBSP) before AM/PM,
// depending on the version of the JDK.
// Fallback patterns match a standard space OR a narrow non-breaking space (NNBSP) before AM/PM.
@DateTimeFormat(style = "SS", fallbackPatterns = { "M/d/yy, h:mm a", "M/d/yy, h:mm\u202Fa" })
private Date styleDateTimeWithFallbackPatternsForPreAndPostJdk20;
@DateTimeFormat(pattern = "M/d/yy h:mm")
private Date patternDate;
@DateTimeFormat(pattern = "yyyy-MM-dd", fallbackPatterns = { "M/d/yy", "yyyyMMdd", "yyyy.MM.dd" })
private Date patternDateWithFallbackPatterns;
// Primary pattern matches a standard space before AM/PM.
// Fallback pattern matches a narrow non-breaking space (NNBSP) before AM/PM.
@DateTimeFormat(pattern = "MM/dd/yy h:mm a", fallbackPatterns = "MM/dd/yy h:mm\u202Fa")
private Date patternDateTimeWithFallbackPatternForPreAndPostJdk20;
@DateTimeFormat(iso = ISO.DATE)
private Date isoDate;
@@ -459,6 +534,14 @@ class DateFormattingTests {
this.styleDateWithFallbackPatterns = styleDateWithFallbackPatterns;
}
public Date getStyleDateTimeWithFallbackPatternsForPreAndPostJdk20() {
return this.styleDateTimeWithFallbackPatternsForPreAndPostJdk20;
}
public void setStyleDateTimeWithFallbackPatternsForPreAndPostJdk20(Date styleDateTimeWithFallbackPatternsForPreAndPostJdk20) {
this.styleDateTimeWithFallbackPatternsForPreAndPostJdk20 = styleDateTimeWithFallbackPatternsForPreAndPostJdk20;
}
public Date getPatternDate() {
return this.patternDate;
}
@@ -475,6 +558,14 @@ class DateFormattingTests {
this.patternDateWithFallbackPatterns = patternDateWithFallbackPatterns;
}
public Date getPatternDateTimeWithFallbackPatternForPreAndPostJdk20() {
return this.patternDateTimeWithFallbackPatternForPreAndPostJdk20;
}
public void setPatternDateTimeWithFallbackPatternForPreAndPostJdk20(Date patternDateTimeWithFallbackPatternForPreAndPostJdk20) {
this.patternDateTimeWithFallbackPatternForPreAndPostJdk20 = patternDateTimeWithFallbackPatternForPreAndPostJdk20;
}
public Date getIsoDate() {
return this.isoDate;
}
@@ -605,6 +605,41 @@ class DateTimeFormattingTests {
assertThat(bindingResult.getFieldValue(propertyName)).asString().matches("12:00:00\\p{Zs}PM");
}
/**
* {@link DateTimeBean#styleLocalTimeWithFallbackPatternsForPreAndPostJdk20}
* configures "-M" as the time style to use. Thus, we have to be aware
* of the following if we do not configure fallback patterns for parsing.
*
* <ul>
* <li>JDK &le; 19 requires a standard space before the "PM".
* <li>JDK &ge; 20 requires a narrow non-breaking space (NNBSP) before the "PM".
* </ul>
*
* <p>To avoid compatibility issues between JDK versions, we have configured
* two fallback patterns which emulate the "-M" style: <code>"HH:mm:ss a"</code>
* matches against a standard space before the "PM", and <code>"HH:mm:ss&#92;u202Fa"</code>
* matches against a narrow non-breaking space (NNBSP) before the "PM".
*
* <p>Thus, the following should theoretically be supported on any JDK (or at least
* JDK 17 - 23, where we have tested it).
*/
@ParameterizedTest(name = "input date: {0}") // gh-33151
@ValueSource(strings = { "12:00:00 PM", "12:00:00\u202FPM" })
void styleLocalTime_PreAndPostJdk20(String propertyValue) {
String propertyName = "styleLocalTimeWithFallbackPatternsForPreAndPostJdk20";
MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add(propertyName, propertyValue);
binder.bind(propertyValues);
BindingResult bindingResult = binder.getBindingResult();
assertThat(bindingResult.getErrorCount()).isEqualTo(0);
String value = binder.getBindingResult().getFieldValue(propertyName).toString();
// Since the "-M" style is always used for printing and the underlying format
// changes depending on the JDK version, we cannot be certain that a normal
// space is used before the "PM". Consequently we have to use a regular
// expression to match against any Unicode space character (\p{Zs}).
assertThat(value).matches("12:00:00\\p{Zs}PM");
}
@ParameterizedTest(name = "input date: {0}")
@ValueSource(strings = {"2021-03-02T12:00:00", "2021-03-02 12:00:00", "3/2/21 12:00"})
void isoLocalDateTime(String propertyValue) {
@@ -682,6 +717,12 @@ class DateTimeFormattingTests {
@DateTimeFormat(style = "-M", fallbackPatterns = {"HH:mm:ss", "HH:mm"})
private LocalTime styleLocalTimeWithFallbackPatterns;
// "-M" style matches either a standard space or a narrow non-breaking space (NNBSP) before AM/PM,
// depending on the version of the JDK.
// Fallback patterns match a standard space OR a narrow non-breaking space (NNBSP) before AM/PM.
@DateTimeFormat(style = "-M", fallbackPatterns = {"HH:mm:ss a", "HH:mm:ss\u202Fa"})
private LocalTime styleLocalTimeWithFallbackPatternsForPreAndPostJdk20;
private LocalDateTime localDateTime;
@DateTimeFormat(style = "MM")
@@ -782,6 +823,15 @@ class DateTimeFormattingTests {
this.styleLocalTimeWithFallbackPatterns = styleLocalTimeWithFallbackPatterns;
}
public LocalTime getStyleLocalTimeWithFallbackPatternsForPreAndPostJdk20() {
return this.styleLocalTimeWithFallbackPatternsForPreAndPostJdk20;
}
public void setStyleLocalTimeWithFallbackPatternsForPreAndPostJdk20(
LocalTime styleLocalTimeWithFallbackPatternsForPreAndPostJdk20) {
this.styleLocalTimeWithFallbackPatternsForPreAndPostJdk20 = styleLocalTimeWithFallbackPatternsForPreAndPostJdk20;
}
public LocalDateTime getLocalDateTime() {
return this.localDateTime;
}
@@ -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");
@@ -36,45 +36,45 @@ import org.springframework.validation.beanvalidation.BeanValidationBeanRegistrat
*/
class BeanValidationBeanRegistrationAotProcessorKotlinTests {
private val processor = BeanValidationBeanRegistrationAotProcessor()
private val processor = BeanValidationBeanRegistrationAotProcessor()
private val generationContext: GenerationContext = TestGenerationContext()
private val generationContext: GenerationContext = TestGenerationContext()
@Test
fun shouldProcessMethodParameterLevelConstraint() {
process(MethodParameterLevelConstraint::class.java)
Assertions.assertThat(
RuntimeHintsPredicates.reflection().onType(ExistsValidator::class.java)
.withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)
).accepts(generationContext.runtimeHints)
}
@Test
fun shouldProcessMethodParameterLevelConstraint() {
process(MethodParameterLevelConstraint::class.java)
Assertions.assertThat(
RuntimeHintsPredicates.reflection().onType(ExistsValidator::class.java)
.withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)
).accepts(generationContext.runtimeHints)
}
@Test
fun shouldSkipMethodParameterLevelConstraintWihExtension() {
process(MethodParameterLevelConstraintWithExtension::class.java)
Assertions.assertThat(generationContext.runtimeHints.reflection().typeHints()).isEmpty()
}
@Test
fun shouldSkipMethodParameterLevelConstraintWihExtension() {
process(MethodParameterLevelConstraintWithExtension::class.java)
Assertions.assertThat(generationContext.runtimeHints.reflection().typeHints()).isEmpty()
}
private fun process(beanClass: Class<*>) {
val contribution = createContribution(beanClass)
contribution?.applyTo(generationContext, Mockito.mock())
}
private fun process(beanClass: Class<*>) {
val contribution = createContribution(beanClass)
contribution?.applyTo(generationContext, Mockito.mock())
}
private fun createContribution(beanClass: Class<*>): BeanRegistrationAotContribution? {
val beanFactory = DefaultListableBeanFactory()
beanFactory.registerBeanDefinition(beanClass.name, RootBeanDefinition(beanClass))
return processor.processAheadOfTime(RegisteredBean.of(beanFactory, beanClass.name))
}
private fun createContribution(beanClass: Class<*>): BeanRegistrationAotContribution? {
val beanFactory = DefaultListableBeanFactory()
beanFactory.registerBeanDefinition(beanClass.name, RootBeanDefinition(beanClass))
return processor.processAheadOfTime(RegisteredBean.of(beanFactory, beanClass.name))
}
internal class MethodParameterLevelConstraintWithExtension {
internal class MethodParameterLevelConstraintWithExtension {
@Suppress("unused")
fun hello(name: @Exists String): String {
return name.toHello()
}
@Suppress("unused")
fun hello(name: @Exists String): String {
return name.toHello()
}
private fun String.toHello() =
"Hello $this"
}
private fun String.toHello() =
"Hello $this"
}
}
@@ -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.
@@ -121,14 +121,14 @@ public class DynamicClassLoader extends ClassLoader {
return createResourceUrl(name, () -> classBytes);
}
}
ResourceFile resourceFile = this.resourceFiles.get(name);
if (resourceFile != null) {
return createResourceUrl(resourceFile.getPath(), resourceFile::getBytes);
}
DynamicResourceFileObject dynamicResourceFile = this.dynamicResourceFiles.get(name);
if (dynamicResourceFile != null && dynamicResourceFile.getBytes() != null) {
return createResourceUrl(dynamicResourceFile.getName(), dynamicResourceFile::getBytes);
}
ResourceFile resourceFile = this.resourceFiles.get(name);
if (resourceFile != null) {
return createResourceUrl(resourceFile.getPath(), resourceFile::getBytes);
}
return super.findResource(name);
}
@@ -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.
@@ -72,11 +72,11 @@ class DynamicJavaFileManager extends ForwardingJavaFileManager<JavaFileManager>
@Override
public FileObject getFileForOutput(Location location, String packageName,
String relativeName, FileObject sibling) {
ResourceFile resourceFile = this.resourceFiles.get(relativeName);
if (resourceFile != null) {
return new DynamicResourceFileObject(relativeName, resourceFile.getContent());
}
return this.dynamicResourceFiles.computeIfAbsent(relativeName, DynamicResourceFileObject::new);
return this.dynamicResourceFiles.computeIfAbsent(relativeName, name -> {
ResourceFile resourceFile = this.resourceFiles.get(name);
return (resourceFile != null) ? new DynamicResourceFileObject(name, resourceFile.getContent()) :
new DynamicResourceFileObject(name);
});
}
@Override
@@ -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.
@@ -17,6 +17,7 @@
package org.springframework.core.test.tools;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.EnumSet;
@@ -154,6 +155,23 @@ class DynamicJavaFileManagerTests {
"META-INF/second.properties");
}
@Test
void existingResourceFileCanBeUpdated() throws IOException {
try (InputStream input = getResourceOne().openInputStream()) {
assertThat(input).hasContent("a");
}
try (OutputStream output = getResourceOne().openOutputStream()) {
output.write('b');
}
try (InputStream input = getResourceOne().openInputStream()) {
assertThat(input).hasContent("b");
}
}
private FileObject getResourceOne() {
return this.fileManager.getFileForOutput(this.location, "", "com/example/one/resource.one", null);
}
private void writeDummyBytecode(JavaFileObject fileObject) throws IOException {
try (OutputStream outputStream = fileObject.openOutputStream()) {
StreamUtils.copy(DUMMY_BYTECODE, outputStream);
@@ -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.
@@ -16,7 +16,10 @@
package org.springframework.core.test.tools;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@@ -27,6 +30,8 @@ import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.element.TypeElement;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
import com.example.PublicInterface;
import org.junit.jupiter.api.Test;
@@ -367,6 +372,14 @@ class TestCompilerTests {
});
}
@Test
void getUpdatedResourceAsStream() {
SourceFile sourceFile = SourceFile.of(HELLO_WORLD);
TestCompiler.forSystem().withResources(ResourceFile.of("com/example/resource", new byte[] { 'a' }))
.withProcessors(new ResourceModifyingProcessor()).compile(sourceFile, compiled -> assertThat(
compiled.getClassLoader().getResourceAsStream("com/example/resource")).hasContent("b"));
}
private void assertSuppliesHelloWorld(Compiled compiled) {
assertThat(compiled.getInstance(Supplier.class).get()).isEqualTo("Hello World!");
}
@@ -392,4 +405,26 @@ class TestCompilerTests {
}
}
@SupportedAnnotationTypes("java.lang.Deprecated")
static class ResourceModifyingProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (roundEnv.processingOver()) {
try {
FileObject resource = this.processingEnv.getFiler()
.createResource(StandardLocation.CLASS_OUTPUT, "", "com/example/resource");
try (OutputStream output = resource.openOutputStream()) {
output.write('b');
}
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
return true;
}
}
}
@@ -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.
@@ -23,8 +23,8 @@ import java.util.regex.Pattern;
import org.graalvm.nativeimage.hosted.Feature;
/**
* GraalVM {@link Feature} that substitutes boolean field values that match a certain pattern
* with values pre-computed AOT without causing class build-time initialization.
* GraalVM {@link Feature} that substitutes boolean field values that match certain patterns
* with values pre-computed ahead-of-time without causing class build-time initialization.
*
* <p>It is possible to pass <pre style="code">-Dspring.native.precompute.log=verbose</pre> as a
* <pre style="code">native-image</pre> compiler build argument to display detailed logs
@@ -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-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.
@@ -33,7 +33,7 @@ import org.springframework.lang.Nullable;
*
* <p>First, creates a new Map of the requested targetType with a size equal to the
* size of the source Map. Then copies each element in the source map to the target map.
* Will perform a conversion from the source maps's parameterized K,V types to the target
* Will perform a conversion from the source map's parameterized K,V types to the target
* map's parameterized types K,V if necessary.
*
* @author Keith Donald
@@ -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));
}
}
@@ -46,6 +46,11 @@ import org.springframework.util.concurrent.ListenableFutureTask;
* executing a large number of short-lived tasks. Alternatively, on JDK 21,
* consider setting {@link #setVirtualThreads} to {@code true}.
*
* <p><b>NOTE: This executor does not participate in context-level lifecycle
* management.</b> Tasks on handed-off execution threads cannot be centrally
* stopped and restarted; if such tight lifecycle management is necessary,
* consider a common {@code ThreadPoolTaskExecutor} setup instead.
*
* @author Juergen Hoeller
* @since 2.0
* @see #setVirtualThreads
@@ -191,6 +196,11 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
* The default of -1 indicates no concurrency limit at all.
* <p>This is the equivalent of a maximum pool size in a thread pool,
* preventing temporary overload of the thread management system.
* However, in contrast to a thread pool with a managed task queue,
* this executor will block the submitter until the task can be
* accepted when the configured concurrency limit has been reached.
* If you prefer queue-based task hand-offs without such blocking,
* consider using a {@code ThreadPoolTaskExecutor} instead.
* @see #UNBOUNDED_CONCURRENCY
* @see org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor#setMaxPoolSize
*/
@@ -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.
@@ -85,17 +85,14 @@ public interface ThrowingBiFunction<T, U, R> extends BiFunction<T, U, R> {
*/
default ThrowingBiFunction<T, U, R> throwing(BiFunction<String, Exception, RuntimeException> exceptionWrapper) {
return new ThrowingBiFunction<>() {
@Override
public R applyWithException(T t, U u) throws Exception {
return ThrowingBiFunction.this.applyWithException(t, u);
}
@Override
public R apply(T t, U u) {
return apply(t, u, exceptionWrapper);
}
};
}
@@ -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.
@@ -77,17 +77,14 @@ public interface ThrowingConsumer<T> extends Consumer<T> {
*/
default ThrowingConsumer<T> throwing(BiFunction<String, Exception, RuntimeException> exceptionWrapper) {
return new ThrowingConsumer<>() {
@Override
public void acceptWithException(T t) throws Exception {
ThrowingConsumer.this.acceptWithException(t);
}
@Override
public void accept(T t) {
accept(t, exceptionWrapper);
}
};
}
@@ -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.
@@ -80,17 +80,14 @@ public interface ThrowingFunction<T, R> extends Function<T, R> {
*/
default ThrowingFunction<T, R> throwing(BiFunction<String, Exception, RuntimeException> exceptionWrapper) {
return new ThrowingFunction<>() {
@Override
public R applyWithException(T t) throws Exception {
return ThrowingFunction.this.applyWithException(t);
}
@Override
public R apply(T t) {
return apply(t, exceptionWrapper);
}
};
}
@@ -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.
@@ -25,9 +25,8 @@ import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* A data size, such as '12MB'.
*
* <p>This class models data size in terms of bytes and is immutable and thread-safe.
* A data size, such as '12MB'. This class models data size in terms of
* bytes and is immutable and thread-safe.
*
* <p>The terms and units used in this class are based on
* <a href="https://en.wikipedia.org/wiki/Binary_prefix">binary prefixes</a>
@@ -247,14 +246,14 @@ public final class DataSize implements Comparable<DataSize>, Serializable {
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
if (other == null || getClass() != other.getClass()) {
return false;
}
DataSize that = (DataSize) obj;
DataSize that = (DataSize) other;
return (this.bytes == that.bytes);
}
@@ -279,7 +278,6 @@ public final class DataSize implements Comparable<DataSize>, Serializable {
DataUnit defaultUnitToUse = (defaultUnit != null ? defaultUnit : DataUnit.BYTES);
return (StringUtils.hasLength(suffix) ? DataUnit.fromSuffix(suffix) : defaultUnitToUse);
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 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.
@@ -81,6 +81,7 @@ public enum DataUnit {
return this.size;
}
/**
* Return the {@link DataUnit} matching the specified {@code suffix}.
* @param suffix one of the standard suffixes
@@ -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) {
@@ -0,0 +1,146 @@
/*
* 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.util.xml;
import java.io.ByteArrayInputStream;
import java.util.List;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.junit.jupiter.api.Test;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link DomUtils}.
*
* @author Stephane Nicoll
* @author Kunal Jani
*/
class DomUtilsTests {
private static final Element SCHOOL_ELEMENT = getDocumentElement("""
<?xml version="1.0"?>
<school>TestSchool
<class teacher="Happy Teacher">Test Teacher One</class>
<class teacher="Sad Teacher">Test Teacher Two</class>
<principal>Test Principal</principal>
<guard>Fox Test</guard>
</school>""");
@Test
void getChildElementsByTagNameWithSeveralMatchingTags() {
List<Element> childElements = DomUtils.getChildElementsByTagName(SCHOOL_ELEMENT, "class", "principal");
assertThat(childElements).map(Element::getNodeName).containsExactly("class", "class", "principal");
}
@Test
void getChildElementsByTagNameWhenTagDoesNotExist() {
assertThat(DomUtils.getChildElementsByTagName(SCHOOL_ELEMENT, "teacher")).isEmpty();
}
@Test
void getChildElementByTagNameWithMatchingTag() {
Element principalElement = DomUtils.getChildElementByTagName(SCHOOL_ELEMENT, "principal");
assertThat(principalElement).isNotNull();
assertThat(principalElement.getTextContent()).isEqualTo("Test Principal");
}
@Test
void getChildElementByTagNameWithNonMatchingTag() {
assertThat(DomUtils.getChildElementByTagName(SCHOOL_ELEMENT, "teacher")).isNull();
}
@Test
void getChildElementValueByTagName() {
assertThat(DomUtils.getChildElementValueByTagName(SCHOOL_ELEMENT, "guard")).isEqualTo("Fox Test");
}
@Test
void getChildElementValueByTagNameWithNonMatchingTag() {
assertThat(DomUtils.getChildElementValueByTagName(SCHOOL_ELEMENT, "math tutor")).isNull();
}
@Test
void getChildElements() {
List<Element> childElements = DomUtils.getChildElements(SCHOOL_ELEMENT);
assertThat(childElements).map(Element::getNodeName).containsExactly("class", "class", "principal", "guard");
}
@Test
void getTextValueWithCharacterDataNode() {
assertThat(DomUtils.getTextValue(SCHOOL_ELEMENT)).isEqualToIgnoringWhitespace("TestSchool");
}
@Test
void getTextValueWithCommentInXml() {
Element elementWithComment = getDocumentElement("""
<?xml version="1.0"?>
<state>
<!-- This is a comment -->
<person>Alice</person>
</state>""");
assertThat(DomUtils.getTextValue(elementWithComment)).isBlank();
}
@Test
void getTextValueWithEntityReference() {
Element elementWithEntityReference = getDocumentElement("""
<?xml version="1.0"?>
<state>
&amp;
<person>Alice</person>
</state>""");
assertThat(DomUtils.getTextValue(elementWithEntityReference)).contains("&");
}
@Test
void getTextValueWithEmptyElement() {
Element emptyElement = getDocumentElement("""
<?xml version="1.0"?>
<person></person>""");
assertThat(DomUtils.getTextValue(emptyElement)).isBlank();
}
@Test
void nodeNameEqualsWhenTrue() {
assertThat(DomUtils.nodeNameEquals(SCHOOL_ELEMENT, "school")).isTrue();
}
@Test
void nodeNameEqualsWhenFalse() {
assertThat(DomUtils.nodeNameEquals(SCHOOL_ELEMENT, "college")).isFalse();
}
private static Element getDocumentElement(String xmlContent) {
try {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
Document document = documentBuilder.parse(new ByteArrayInputStream(xmlContent.getBytes()));
return document.getDocumentElement();
}
catch (Exception ex) {
throw new IllegalStateException("Failed to parse xml content:%n%s".formatted(xmlContent), ex);
}
}
}
@@ -22,4 +22,4 @@ package org.springframework.core
* @author Sebastien Deleuze
*/
class KotlinReflectionParameterNameDiscovererTests :
AbstractReflectionParameterNameDiscovererKotlinTests(KotlinReflectionParameterNameDiscoverer())
AbstractReflectionParameterNameDiscovererKotlinTests(KotlinReflectionParameterNameDiscoverer())
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2009 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.
@@ -18,7 +18,8 @@ package org.springframework.expression;
/**
* Parses expression strings into compiled expressions that can be evaluated.
* Supports parsing templates as well as standard expression strings.
*
* <p>Supports parsing template expressions as well as standard expression strings.
*
* @author Keith Donald
* @author Andy Clement
@@ -27,29 +28,31 @@ package org.springframework.expression;
public interface ExpressionParser {
/**
* Parse the expression string and return an Expression object you can use for repeated evaluation.
* <p>Some examples:
* Parse the expression string and return an {@link Expression} object that
* can be used for repeated evaluation.
* <p>Examples:
* <pre class="code">
* 3 + 4
* name.firstName
* </pre>
* @param expressionString the raw expression string to parse
* @return an evaluator for the parsed expression
* @throws ParseException an exception occurred during parsing
* @return an {@code Expression} for the parsed expression
* @throws ParseException if an exception occurred during parsing
*/
Expression parseExpression(String expressionString) throws ParseException;
/**
* Parse the expression string and return an Expression object you can use for repeated evaluation.
* <p>Some examples:
* Parse the expression string and return an {@link Expression} object that
* can be used for repeated evaluation.
* <p>Examples:
* <pre class="code">
* 3 + 4
* name.firstName
* </pre>
* @param expressionString the raw expression string to parse
* @param context a context for influencing this expression parsing routine (optional)
* @return an evaluator for the parsed expression
* @throws ParseException an exception occurred during parsing
* @param context a context for influencing the expression parsing routine
* @return an {@code Expression} for the parsed expression
* @throws ParseException if an exception occurred during parsing
*/
Expression parseExpression(String expressionString, ParserContext context) throws ParseException;
@@ -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.
@@ -47,7 +47,7 @@ public interface ParserContext {
String getExpressionPrefix();
/**
* For template expressions, return the prefix that identifies the end of an
* For template expressions, returns the prefix that identifies the end of an
* expression block within a string. For example: "}"
* @return the suffix that identifies the end of an expression
*/
@@ -55,8 +55,9 @@ public interface ParserContext {
/**
* The default ParserContext implementation that enables template expression
* parsing mode. The expression prefix is "#{" and the expression suffix is "}".
* The default {@link ParserContext} implementation that enables template
* expression parsing.
* <p>The expression prefix is "#{", and the expression suffix is "}".
* @see #isTemplate()
*/
ParserContext TEMPLATE_EXPRESSION = new ParserContext() {
@@ -29,8 +29,11 @@ import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* An expression parser that understands templates. It can be subclassed by expression
* parsers that do not offer first class support for templating.
* Abstract base class for {@linkplain ExpressionParser expression parsers} that
* support templates.
*
* <p>Can be subclassed by expression parsers that offer first class support for
* templating.
*
* @author Keith Donald
* @author Juergen Hoeller
@@ -88,7 +91,7 @@ public abstract class TemplateAwareExpressionParser implements ExpressionParser
* single quote '.
* @param expressionString the expression string
* @return the parsed expressions
* @throws ParseException when the expressions cannot be parsed
* @throws ParseException if the expressions cannot be parsed
*/
private Expression[] parseExpressions(String expressionString, ParserContext context) throws ParseException {
List<Expression> expressions = new ArrayList<>();
@@ -229,7 +232,7 @@ public abstract class TemplateAwareExpressionParser implements ExpressionParser
* @param expressionString the raw expression string to parse
* @param context a context for influencing this expression parsing routine (optional)
* @return an evaluator for the parsed expression
* @throws ParseException an exception occurred during parsing
* @throws ParseException if an exception occurred during parsing
*/
protected abstract Expression doParseExpression(String expressionString, @Nullable ParserContext context)
throws ParseException;
@@ -291,7 +291,11 @@ public enum SpelMessage {
/** @since 6.0.13 */
NEGATIVE_REPEATED_TEXT_COUNT(Kind.ERROR, 1081,
"Repeat count ''{0}'' must not be negative");
"Repeat count ''{0}'' must not be negative"),
/** @since 6.1.15 */
UNSUPPORTED_CHARACTER(Kind.ERROR, 1082,
"Unsupported character ''{0}'' ({1}) encountered in expression");
private final Kind kind;
@@ -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);
@@ -266,9 +266,7 @@ class Tokenizer {
raiseParseException(this.pos, SpelMessage.UNEXPECTED_ESCAPE_CHAR);
break;
default:
throw new IllegalStateException(
"Unsupported character '%s' (%d) encountered at position %d in expression."
.formatted(ch, (int) ch, (this.pos + 1)));
raiseParseException(this.pos + 1, SpelMessage.UNSUPPORTED_CHARACTER, ch, (int) ch);
}
}
}
@@ -42,7 +42,7 @@ public abstract class AbstractExpressionTests {
protected static final boolean SHOULD_NOT_BE_WRITABLE = false;
protected final ExpressionParser parser = new SpelExpressionParser();
protected final SpelExpressionParser parser = new SpelExpressionParser();
protected final StandardEvaluationContext context = TestScenarioCreator.getTestEvaluationContext();
@@ -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 {
@@ -18,12 +18,12 @@ package org.springframework.expression.spel;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.springframework.expression.spel.standard.SpelExpression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Parse some expressions and check we get the AST we expect.
@@ -34,10 +34,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
* @author Andy Clement
* @author Sam Brannen
*/
class ParsingTests {
private final SpelExpressionParser parser = new SpelExpressionParser();
class ParsingTests extends AbstractExpressionTests {
@Nested
class Miscellaneous {
@@ -104,12 +101,14 @@ class ParsingTests {
parseCheck("have乐趣()");
}
@Test
void unsupportedCharactersInIdentifiers() {
// Invalid syntax
assertThatIllegalStateException()
.isThrownBy(() -> parser.parseRaw("apple~banana"))
.withMessage("Unsupported character '~' (126) encountered at position 6 in expression.");
@ParameterizedTest(name = "expression = ''{0}''")
@CsvSource(textBlock = """
apple~banana, ~, 6
map[c], , 5
A § B, §, 3
""")
void unsupportedCharacter(String expression, char ch, int position) {
parseAndCheckError(expression, SpelMessage.UNSUPPORTED_CHARACTER, position, ch, (int) ch);
}
@Test
@@ -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);

Some files were not shown because too many files have changed in this diff Show More