Prior to this commit, the SpelParserConfiguration constructors that
omit an explicit maximumAutoGrowSize left collection auto-growing
effectively unbounded, defaulting to Integer.MAX_VALUE. That default
was inconsistent with the auto-grow limit applied elsewhere in the
framework for data binding (see
DataBinder.DEFAULT_AUTO_GROW_COLLECTION_LIMIT).
To address that, this commit introduces a new
SpelParserConfiguration.DEFAULT_MAX_AUTO_GROW_SIZE constant (set to 256
to match DataBinder.DEFAULT_AUTO_GROW_COLLECTION_LIMIT) and switches
the constructors that previously hard-coded Integer.MAX_VALUE to use
this new default instead. Constructors that accept an explicit
maximumAutoGrowSize are unaffected.
In addition, SpelParserConfiguration now enforces that a user-supplied
maximumAutoGrowSize is not a negative value, consistent with the
preconditions already enforced for maximumExpressionLength,
maximumOperations, maximumBigPowerBits, and maximumNestingDepth. A
value of 0 remains supported (effectively disabling collection
auto-growing) and is now documented as such in the Javadoc.
The Spring Framework reference documentation has also been updated to
describe the new default, and tests have been added to IndexingTests to
verify the default, the ability to override it, and the new
precondition.
Closes gh-36995
This commit introduces support for limiting the structural nesting
depth of a SpEL expression during parsing. Without such a limit, an
expression with deeply nested constructs (for example, inline lists or
maps, parenthesized expressions, ternary or Elvis expressions, or
chained unary operators) can cause SpEL's recursive-descent parser to
throw a StackOverflowError which lacks useful diagnostics for
developer's attempting to assess what went wrong.
With this commit, a nesting-depth counter is now tracked around the
parser's primary recursive entry point (eatExpression()) as well as
around chained unary operators (eatUnaryExpression()), ensuring that
independent, sibling uses of assignment, Elvis, and ternary expressions
do not inadvertently accumulate depth and trip the limit.
If the configured (or default) nesting-depth limit is exceeded during
parsing, a SpelParseException is thrown instead, with a message that
reports the configured limit.
The limit can be configured on a per-use-case basis via
SpelParserConfiguration and defaults to 1000.
Closes gh-36723
In commit 622fc3edf7, I introduced a check in
TypeMappedAnnotation#isSynthesizable() intended to force synthesis when
an attribute value needs to be resolved from a different level of a
multi-level annotation hierarchy whose root annotation does not
redeclare the target attribute itself.
That check tested if `resolvedMirrors.length > 0` for a
meta-annotation; however, resolvedMirrors is always sized according to
the number of attributes declared by the mapped annotation type,
regardless of whether any of those attributes actually participate in
mirroring or an @AliasFor override. As a result, the check effectively
synthesized any meta-annotation that declares at least one attribute,
which reintroduced the unnecessary-synthesis behavior that commit
d6768ccc18 had fixed, merely narrowed to meta-annotations with
attributes.
This commit replaces that overly broad check with a precise one in
AnnotationTypeMapping#computeSynthesizableFlag(), which now also
considers whether any attribute's value must be resolved from a
different annotation in the meta-annotation hierarchy (tracked via
annotationValueSource). This correctly identifies the original
multi-level hierarchy scenario without over-matching on ordinary
meta-annotations that have nothing to merge or override.
See gh-28704
See gh-28716
Closes gh-37135
This commit defers creation of the visited annotation types set until a
cache miss occurs, which avoids allocating a HashSet for every cached
annotation mapping lookup while preserving recursive annotation
handling during mapping creation.
Closes gh-37141
Signed-off-by: GT <gregjotau@gmail.com>
OptionalToObjectConverter.matches() used
TypeDescriptor.getElementTypeDescriptor(), which returns null for an
Optional (element types are only resolved for arrays, streams and
collections). ConversionUtils.canConvertElements() then treats a null
source element type as "maybe" and returns true unconditionally, so
ConversionService.canConvert(Optional<X>, target) reported true even
when X is not convertible to the target -- a violation of the
canConvert contract, since the subsequent conversion fails.
To address that, this commit resolves the Optional's element type from
its generic and checks it against the target, mirroring
ObjectToOptionalConverter. A raw or otherwise unresolved element type
remains permissive.
Closes gh-36913
Signed-off-by: junhyeong9812 <pickjog@gmail.com>
Excluding a path from nohttp scanning has so far required mirroring it
by hand in CheckstyleConventions, in addition to any existing
`.gitignore` entry. However, that extra step is easy to forget, as
happened when the .claude folder was added to `.gitignore` (49d2a202da)
but not to the nohttp excludes (48971139c0), only surfacing later as
an OutOfMemoryError that required a separate heap size increase
(90ad7f947d).
To address that, this commit introduces excludeGitIgnoredPaths() in
CheckstyleConventions, which parses the root `.gitignore` file and
translates its patterns into additional nohttp excludes, so that newly
ignored paths are picked up automatically. Note, however, that the
existing hand-maintained excludes are left in place for entries that
are specific to nohttp and are not otherwise ignored by git.
Closes gh-37164
Prior to this commit, some same-page links in our Antora-based docs
were written using the `xref:` macro, which Antora resolves as a
cross-page reference. However, a same-page target can be misread as a
page reference and break the site build (as nearly happened in
gh-37152), whereas `<<id,text>>` only ever resolves against the current
page's own anchors.
To address that inconsistency and avoid potential future bugs (broken
links), this commit converts all such same-page `xref:` links to
`<<id,text>>`, leaving genuine cross-page `xref:` links are unaffected.
Closes gh-37161
On a developer machine, the nohttp check scans the whole project
directory, and things like local git worktrees can add enough extra
content on disk to push the task past its previous 1g heap limit,
causing an OutOfMemoryError. This goes beyond what was addressed by
excluding the .claude folder from nohttp scanning in 48971139c0.
This commit raises the heap size for checkstyleNohttp specifically
to 1536m, leaving the limit for other Checkstyle tasks unchanged so
as not to increase memory pressure on CI.
Prior to this commit, Property.resolveName() located the "set" prefix
of a write method with String.indexOf(), which matches the token
anywhere in the method name. A write method that merely contains "set"
(for example "offsetX" or "upset") was silently accepted and resolved
to a meaningless property name derived from whatever follows the token,
while only names with no "set" token at all were rejected.
To address that, this commit matches the "set" prefix only at the start
of the method name via startsWith(), so that an
IllegalArgumentException is consistently thrown for any write method
candidate that is not a setter.
See gh-36911
Closes gh-37139
Signed-off-by: junhyeong9812 <pickjog@gmail.com>
Property.resolveName() located the get/is accessor prefix with
String.indexOf, which matches the prefix anywhere in the method name.
A plain accessor whose name embeds such a prefix (for example
budget()) had the wrong portion stripped and resolved to an empty or
wrong property name, which in turn caused the backing field's
annotations to be silently dropped.
Match the get/is prefix only at the start of the method name and do
not strip it when the method is a plain accessor for a data class,
that is, a non-static no-arg method referring to an instance field of
the same name. This supports Java records, Kotlin data classes, and
custom Java data classes alike, without relying on java.lang.Record.
As a consequence, a getter backed by a field of the exact same name
(for example isUrgent()) now resolves to the field name.
Closes gh-36911
Signed-off-by: junhyeong9812 <pickjog@gmail.com>
ReflectivePropertyAccessor's canRead(), read(), and canWrite() methods
previously constructed org.springframework.core.convert.Property
instances without an explicit name, forcing Property#resolveName() to
re-derive the property name from the accessor method via prefix
matching. That heuristic incorrectly resolves record-style and other
prefix-less accessor methods whose names embed or start with "get"/"is"
(for example, budget(), issue(), or island()), and it also normalizes
acronym-style JavaBean properties inconsistently (for example, getURL()
resolves to "uRL" rather than "URL").
By the time these three methods construct a Property, they have already
located the accessor method by searching for exactly the requested
property name, so the resolved name is already known and verified. This
commit passes that name through explicitly via the 4-arg Property
constructor, bypassing Property#resolveName() entirely at these call
sites.
This commit also introduces tests in PropertyAccessTests to cover the
following scenarios:
- A genuine record accessor whose component name embeds or starts with
a "get"/"is" prefix
- The same scenario on a hand-written, non-record "data class"
- The read() call site exercised directly, since it is otherwise
unreachable once canRead() has warmed the cache
- A boolean isXxx() getter, as a plain regression check
- An acronym-style property with a decoy field to prove that the
correct field (and its annotations) is now resolved for both reads
and writes
See gh-36911
Closes gh-37123
TextMessage.getText() and ObjectMessage.getObject() may both return
null per the JMS specification when the message body was never set, but
SimpleMessageConverter.fromMessage() and its parent MessageConverter
interface currently declare a non-null return type despite residing in
an @NullMarked package.
To address that, this commit updates MessageConverter.fromMessage(),
SimpleMessageConverter, and the protected
extractStringFromMessage()/extractSerializableFromMessage() methods to
declare @Nullable accordingly and propagate the resulting nullability
through MessagingMessageConverter and
AbstractAdaptableMessageListener/MessageListenerAdapter, raising a
clear MessageConversionException where a non-null payload is required
by the Message<T> contract.
Closes gh-37148
This commit adds an overloaded `addCronTask()` method to `ScheduledTaskRegistrar`
that allows simpler scheduling of cron tasks with non-default time zones.
Closes gh-36556
Signed-off-by: Vedran Pavic <vedran@vedranpavic.com>
Prior to this commit, only the very first AsynchronousFileChannel#write
call in DataBufferUtils$WriteCompletionHandler#hookOnNext(DataBuffer)
was guarded against exceptions escaping synchronously, and even then
only via `catch (RuntimeException ex)`, per the original fix for
gh-36184.
While widening that guard to match the read side's `catch (Throwable
ex)` combined with `Exceptions.throwIfFatal(ex)` (see gh-37143), we
discovered that completed(Integer, Attachment) contains two more direct
`this.channel.write(...)` calls -- for continuing a partial write and
for advancing to the next ByteBuffer within the same DataBuffer's
iterator -- neither of which was guarded at all. Since completed() is
invoked by the channel's own completion callback, typically on a
different thread than the one that issued the original write, a
synchronous exception escaping either of those calls has no path back
to the FluxSink, and the resulting Flux hangs indefinitely, exactly as
described in gh-37143, for any write that receives a partial OS write
or spans multiple ByteBuffers.
To address that, this commit extracts a private write(ByteBuffer, long,
Attachment) helper that wraps the channel.write(...) call with a
try/catch block, routing any non-fatal Throwable -- via
Exceptions.throwIfFatal() -- to the existing failed(Throwable,
Attachment) handler. All three call sites (hookOnNext() and both
branches in completed()) now go through this helper, ensuring the
Flux always terminates with a proper error signal instead of hanging
silently, regardless of which write attempt fails or which thread it
fails on.
See gh-36184
See gh-37143
Closes gh-37145
Prior to this commit, DataBufferUtils$ReadCompletionHandler#read()
invoked AsynchronousFileChannel#read(ByteBuffer, long, Attachment,
CompletionHandler) without guarding against exceptions thrown directly
by that call. Although that method is documented to report failures
asynchronously via the supplied CompletionHandler, some platform-
specific implementations can instead throw synchronously – for
example, on Windows with JDK 25, when the JDK rejects a ByteBuffer
backed by a closeable shared memory Arena, as produced by Netty 4.2's
off-heap buffer allocation.
When such an exception is thrown from a recursive read() invocation
triggered from completed() – which happens once a resource requires
more than a single chunk – the exception has no path back to the
FluxSink: it escapes on whatever thread invoked the CompletionHandler,
and the resulting Flux never signals onError or onComplete. In
practice, this surfaced as an indefinite hang when serving a Resource
whose HTTP response is not a ZeroCopyHttpOutputMessage, since
ResourceHttpMessageWriter falls back to ResourceEncoder, which reads
the resource via DataBufferUtils.
To address that, this commit wraps the channel.read(...) call in a
try/catch block and routes any non-fatal Throwable to the existing
failed(Throwable, Attachment) handler, via Exceptions.throwIfFatal(),
mirroring the equivalent fix already applied to the write side for
gh-36184. This ensures the allocated DataBuffer is released and the
Flux always terminates with a proper error signal instead of leaking a
buffer or hanging silently.
See gh-36184
Closes gh-37143
The limit was previously enforced in XmlEventDecoder, because it is
what parses incoming buffers. However, the actual caching is in
Jaxb2Decoder, which holds on to XML events, but has no good way to
estimate their size.
After this commit XmlEventDecoder no longer enforces memory limits
for async parsing. It releases each buffer immediately anyway.
Instead XmlEventDecoder is only responsible to update the number
of bytes received via a new ReceivedByteTracker type while
Jaxb2XmlDecoder uses the same to perform limit and reset the
count depending on when it is aggregating XML events.
Closes gh-37031