MockMvcResultMatchers.forwardedUrl() already accepts a @Nullable
expected value to assert that no forwarding occurred, but its
counterpart redirectedUrl() previously did not, even though the
underlying assertEquals() comparison is null-safe and behaves the same
way for redirects.
To address that, this commit adds the same @Nullable declaration to
redirectedUrl() and documents the null semantics in the Javadoc for
both methods.
Closes gh-37230
This commit introduces additional unit tests for CallMetaDataContext's
function return parameter matching in reconcileParameters(), verifying
that the declared return parameter is correctly resolved regardless of
whether it is declared before or after an additional OUT parameter.
See gh-37206
As a follow up to 1b56f58999, this commit introduces
hasUnbalancedBrackets() in PropertyAccessorUtils, which
AbstractNestablePropertyAccessor uses to reject property paths with
unbalanced '[' or ']' brackets by throwing a
NotReadablePropertyException with an informative message, thereby
improving diagnostics for users.
See gh-36999
This commit picks up where 97067f9c9c left off by removing the four
trailing collection-clearing calls in clear(), which are now redundant
since the removal loop already empties contextMap, hierarchyMap,
contextUsageMap, and unusedContexts as an invariant. This commit also
adds regression tests in LruContextCacheTests for closing a context
hierarchy via clear() and reset(), asserting bottom-up close order with
Mockito.inOrder(), and confirming that getParentContextCount() and
getContextUsageCount() return to zero.
See gh-36825
Previously, canonicalPropertyName() located the end of a [key]
expression via a naive indexOf("]") search, while
AbstractNestablePropertyAccessor.getPropertyNameKeyEnd() -- used during
actual property resolution -- tracked bracket nesting depth. This meant
the two methods could disagree on the canonical form of a property
path whose map key itself contains bracket characters (e.g.,
map['key[0]']).
Similarly, getNestedPropertySeparatorIndex() tracked whether a dot
separator occurs inside a [key] expression using a simple boolean
toggle that flips on both '[' and ']', which produces an incorrect
result when a key contains an odd net count of inner bracket
characters.
To address those inconsistencies, this commit extracts the private
getPropertyNameKeyEnd() method from AbstractNestablePropertyAccessor to
a package-private static utility method in PropertyAccessorUtils, so
that canonicalPropertyName() can reuse the same depth-aware bracket
matching, and getNestedPropertySeparatorIndex() has been reworked to
track bracket nesting depth instead of toggling a boolean flag.
Closes gh-36999
Previously, PropertyEditorRegistrySupport.addStrippedPropertyPaths()
recursively enumerated every combination of stripped/retained [key]
segments in a property path, producing 2^n - 1 variants for a path
with n bracket pairs.
To address that, this commit limits the recursion at a depth of 8,
preserving existing behavior for realistic property paths while
bounding the work done for paths with an unusually large number of
bracket segments.
Closes gh-37020
Previously, createMap() invoked createIndexedValue() – and therefore
createObject() for non-simple value types – once per matching parameter
name rather than once per distinct map key, causing redundant nested
object construction for map entries whose value type has multiple
constructor parameters.
To address that, this commit aligns createMap() with createList() and
createArray() by skipping construction for keys that have already been
resolved.
Closes gh-37019
Since we now have an official builder API for SpelParserConfiguration
(introduced in 7.0.10), this commit follows through on the plan stated
in that commit's Javadoc and formally deprecates all 9 overloaded
constructors in SpelParserConfiguration, thereby encouraging users to
benefit from the simplicity of the builder API -- or
SpelParserConfiguration.withDefaults() for the common case -- instead
of having to migrate to the latest-and-greatest full constructor every
time a new configuration property is introduced.
The no-arg constructor points users to withDefaults(), and all other
constructors -- including the canonical 9-parameter constructor --
point to the builder API. Builder.build() has been annotated with
@SuppressWarnings("deprecation"), since it is the sole legitimate
internal caller of the now-deprecated canonical constructor.
The SpelParserConfigurationTests.LegacyConstructorTests nested class
(and its sibling builderAppliesSameDefaultsAsNoArgConstructor() test
method) are annotated with @SuppressWarnings("deprecation"), since they
exist specifically to provide regression coverage for the deprecated
constructors. IndexingTests.MaxAutoGrowSizeTests and
SpelParserTests.MaxNestingDepthTests, on the other hand, were both
introduced before the builder API existed and had no such need for the
legacy constructors, so they have been converted to use the builder API
instead, avoiding the need for any deprecation suppression there.
See gh-37187
Closes gh-37190
Prior to this commit, SpelParserConfiguration exposed 8 overloaded
constructors that accumulated over time as new configuration options
were introduced (auto-grow support since 3.0, maximumExpressionLength
in 5.2.25, maximumOperations in 6.2.19, and maximumBigPowerBits in
7.0.9), culminating in an 8-parameter constructor. This made call sites
hard to read due to unlabeled sequences of booleans and ints, and it
forced users who wanted to override a single setting to also supply
every other value explicitly.
To address that, this commit introduces a builder API in
SpelParserConfiguration, following the pattern already established by
SimpleEvaluationContext's builder API.
Specifically, SpelParserConfiguration.builder() returns a Builder that
is pre-populated with the same defaults as the no-arg constructor,
including the SpringProperties-driven overrides for the default
compiler mode, maximum operations, and maximum big-power bits -- the
latter two are only resolved lazily in build(), so that overriding them
via the builder never triggers an unnecessary SpringProperties lookup.
Each property has a dedicated, named setter (compilerMode(),
compilerClassLoader(), maximumAutoGrowSize(),
maximumExpressionLength(), maximumOperations(), maximumBigPowerBits()),
and the two auto-grow flags are exposed as simple no-arg opt-ins
(autoGrowNullReferences(), autoGrowCollections()) since they both
default to false. build() delegates to the existing canonical
constructor, so validation and defaults remain centralized in one
place.
In addition, a new SpelParserConfiguration.withDefaults() factory
method has been introduced as shorthand for
SpelParserConfiguration.builder().build(), for the common case where
none of the builder's defaults need to be overridden.
As the one deliberate exception to matching the no-arg constructor's
defaults, the builder defaults maximumAutoGrowSize to 256 -- aligned
with DataBinder.DEFAULT_AUTO_GROW_COLLECTION_LIMIT -- rather than the
constructors' Integer.MAX_VALUE. The constructors keep their legacy
default for backward compatibility, but the builder is a new, opt-in
API that is not bound by that compatibility contract.
This change is purely additive: none of the existing constructors have
been modified or deprecated. Deprecating those constructors in favor of
the builder is being deferred to 7.1, since new deprecations should not
be introduced in a patch release. In the meantime, the Javadoc for the
constructors and for the SPRING_EXPRESSION_*_PROPERTY_NAME constants
has been updated to favor the builder (or a specific Builder setter)
instead of the constructors, and the class-level Javadoc now states
that the constructors are planned to be deprecated in favor of the
builder as of Spring Framework 7.1.
SpelExpressionParser's no-arg constructor, ExpressionState's two
convenience constructors, and StandardBeanExpressionResolver's
ClassLoader-based constructor have all been switched from the
SpelParserConfiguration constructors to the builder (or
withDefaults()). This is behaviorally identical in every case:
autoGrowCollections remains false at each of those call sites, and
maximumAutoGrowSize -- the only property whose default differs between
the constructors and the builder -- has no effect when
autoGrowCollections is false.
Tests have been added in a new SpelParserConfigurationTests class to
verify that the builder's defaults match the no-arg constructor (with
the one intentional maximumAutoGrowSize exception called out above),
that custom values are applied correctly, and that invalid values are
rejected. The nested LegacyConstructorTests class provides regression
coverage for each of the legacy constructors, consolidating their usage
in tests to a single class -- which will keep any future deprecation
warnings confined to this class -- and documents that, unlike the
builder, the canonical constructor does not (yet) reject a negative
maximumAutoGrowSize. The remaining incidental usages of the
SpelParserConfiguration constructors throughout EvaluationTests,
IndexingTests, SpelCompilationCoverageTests, SpelReproTests, and
SpelCompilerTests have been converted to use the builder.
Furthermore, the reference documentation has been updated to recommend
the builder and withDefaults() over the constructors, both in prose and
in the Java/Kotlin examples.
Closes gh-37187
Prior to this commit, ConstructorReference.createArray() enforced the
MAX_ARRAY_ELEMENTS threshold for multi-dimensional arrays by checking
only the product of all dimension sizes, which is equivalent to the
total number of leaf-level elements. However, Array.newInstance()
allocates a distinct array object at every nesting level, not just at
the leaf level. For dimensions [d0, d1, ..., dk-1], the total number
of array objects created is 1 + d0 + d0*d1 + ... + d0*d1*...*d(k-2).
As a result, an expression such as new int[262143][1][1]...[1], whose
trailing dimensions are all 1, kept the leaf-element product just
under the threshold while still causing tens of millions of array
objects to be allocated.
To address that, this commit introduces a second running total,
totalArrayObjects, alongside the existing leaf-element product in the
multi-dimensional array construction loop. Both totals are checked
against MAX_ARRAY_ELEMENTS on every iteration, so array constructions
that fan out into an excessive number of array objects are now
rejected even when the leaf-element count remains within bounds.
Note that SimpleEvaluationContext does not permit array construction
in SpEL expressions at all, so this fix effectively only changes
behavior for expressions evaluated via StandardEvaluationContext.
Tests have been added to ArrayConstructorTests to verify that the new
check rejects array constructions with an excessive number of array
objects and that array constructions just under the threshold remain
unaffected.
Closes gh-36998
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
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.
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
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
Prior to this commit, SpEL expression compilation could be silently
activated in a SimpleEvaluationContext via the
`spring.expression.compiler.mode` Spring/system property or
SpelParserConfiguration. Once an expression is compiled, the evaluation
guards enforced during interpreted evaluation are no longer applied,
which is at odds with the restricted intent of SimpleEvaluationContext.
To address that, this commit introduces a mechanism analogous to
isAssignmentEnabled() which disables compilation by default in
SimpleEvaluationContext. Specifically:
- A new isCompilationSupported() default method has been introduced in
the EvaluationContext API, which returns true by default.
- SimpleEvaluationContext overrides isCompilationSupported() to return
false by default. However, compilation can be opted into explicitly
via the new withCompilationSupported() method in the
SimpleEvaluationContext.Builder.
- SpelExpression.checkCompile() now consults isCompilationSupported()
before triggering new compilation, ensuring that evaluation within an
EvaluationContext never produces a compiled form of the expression if
the context's isCompilationSupported() method returns false.
- All eight getValue() variants in SpelExpression now consult
isCompilationSupported() before executing a compiled expression,
ensuring that a compiled expression produced via a different
EvaluationContext is not silently reused if the caller inadvertently
switches to an EvaluationContext that does not support compilation.
Closes gh-37035
This commit introduces a configurable limit on the estimated result size
of BigDecimal and BigInteger power operations within SpEL expressions.
The estimated result size in bits is computed as the product of the base
value's bit length and the exponent. If this limit is exceeded, a
SpelEvaluationException is thrown.
The limit defaults to 1,000,000 bits, which is approximately equivalent
to a decimal number with 300,000 digits, and can be configured either
on a per-use-case basis via the new maximumBigPowerBits constructor
argument in SpelParserConfiguration or globally as a JVM system
property or Spring property named `spring.expression.maxBigPowerBits`.
Parsers intended for trusted internal expressions may supply
Integer.MAX_VALUE to remove the limit entirely.
Closes ch-37034
Prior to this commit, the List branch in
AbstractNestablePropertyAccessor's getPropertyValue() method called
list.get(index) unconditionally after invoking
growCollectionIfNecessary(), which implicitly relied on the list
throwing an IndexOutOfBoundsException for out-of-range access. Such an
exception is caught downstream and wrapped as an
InvalidPropertyException; however, any List implementation whose get()
method allocates elements on demand rather than throwing an
IndexOutOfBoundsException could bypass that check.
This behavior was also inconsistent with the Collection/Iterable branch
in the same method, which already performs an explicit `index >=
collection.size()` bounds check before attempting element access.
To address that, this commit aligns the List branch with the
Collection/Iterable branch by adding an explicit `index < 0 || index >=
list.size()` check immediately after the auto-grow attempt. If the
index remains out of bounds after growCollectionIfNecessary() runs –
for example, because growth was capped by autoGrowCollectionLimit or
auto-growing was disabled – an InvalidPropertyException is now thrown
rather than delegating to list.get() which may or may not throw an
exception.
Closes gh-37036
Prior to this commit, SpEL expression compilation could be silently
activated in a SimpleEvaluationContext via the
`spring.expression.compiler.mode` Spring/system property or
SpelParserConfiguration. Once an expression is compiled, the evaluation
guards enforced during interpreted evaluation are no longer applied,
which is at odds with the restricted intent of SimpleEvaluationContext.
To address that, this commit introduces a mechanism analogous to
isAssignmentEnabled() which disables compilation by default in
SimpleEvaluationContext. Specifically:
- A new isCompilationSupported() default method has been introduced in
the EvaluationContext API, which returns true by default.
- SimpleEvaluationContext overrides isCompilationSupported() to return
false by default. However, compilation can be opted into explicitly
via the new withCompilationSupported() method in the
SimpleEvaluationContext.Builder.
- SpelExpression.checkCompile() now consults isCompilationSupported()
before triggering new compilation, ensuring that evaluation within an
EvaluationContext never produces a compiled form of the expression if
the context's isCompilationSupported() method returns false.
- All eight getValue() variants in SpelExpression now consult
isCompilationSupported() before executing a compiled expression,
ensuring that a compiled expression produced via a different
EvaluationContext is not silently reused if the caller inadvertently
switches to an EvaluationContext that does not support compilation.
Closes gh-37035
This commit introduces a configurable limit on the estimated result size
of BigDecimal and BigInteger power operations within SpEL expressions.
The estimated result size in bits is computed as the product of the base
value's bit length and the exponent. If this limit is exceeded, a
SpelEvaluationException is thrown.
The limit defaults to 1,000,000 bits, which is approximately equivalent
to a decimal number with 300,000 digits, and can be configured either
on a per-use-case basis via the new maximumBigPowerBits constructor
argument in SpelParserConfiguration or globally as a JVM system
property or Spring property named `spring.expression.maxBigPowerBits`.
Parsers intended for trusted internal expressions may supply
Integer.MAX_VALUE to remove the limit entirely.
Closes ch-37034
Prior to this commit, the List branch in
AbstractNestablePropertyAccessor's getPropertyValue() method called
list.get(index) unconditionally after invoking
growCollectionIfNecessary(), which implicitly relied on the list
throwing an IndexOutOfBoundsException for out-of-range access. Such an
exception is caught downstream and wrapped as an
InvalidPropertyException; however, any List implementation whose get()
method allocates elements on demand rather than throwing an
IndexOutOfBoundsException could bypass that check.
This behavior was also inconsistent with the Collection/Iterable branch
in the same method, which already performs an explicit `index >=
collection.size()` bounds check before attempting element access.
To address that, this commit aligns the List branch with the
Collection/Iterable branch by adding an explicit `index < 0 || index >=
list.size()` check immediately after the auto-grow attempt. If the
index remains out of bounds after growCollectionIfNecessary() runs –
for example, because growth was capped by autoGrowCollectionLimit or
auto-growing was disabled – an InvalidPropertyException is now thrown
rather than delegating to list.get() which may or may not throw an
exception.
Closes gh-37036
A user reported confusion between two distinct uses of "wraps" in the
AOP proxy documentation for Bean Overrides: the sense in which a
Mockito spy wraps the original bean instance it was created from, and
the sense in which a Spring AOP proxy wraps the spy in the actual
object graph.
To address that, this commit adds two small diagrams to the general
"Bean Overrides and Spring AOP Proxies" section, illustrating, from a
caller's perspective, the shape of the bean for the
REPLACE/REPLACE_OR_CREATE strategy (no proxy at all) versus the WRAP
strategy (an AOP proxy still created, now wrapping the override
instance instead of the original bean). Both diagrams use the same
generic "override instance" label, since the section is not specific to
Mockito; a Mockito spy created by @MockitoSpyBean is mentioned only as
an example.
The accompanying text is revised to reserve "wraps" for the AOP proxy
relationship and to explicitly call out that a Mockito spy's
relationship to its original bean instance is a separate concern from
AOP proxy nesting.
The @MockitoSpyBean-specific strategy paragraph in the
@MockitoBean/@MockitoSpyBean documentation has also been revised
similarly, and now points to the new diagram.
See gh-37121
This commit documents how the Bean Override support in the TestContext
framework interacts with Spring AOP proxies created for annotations
such as @Transactional, @Cacheable, and @Retryable.
The new "Bean Overrides and Spring AOP Proxies" section in the general
Bean Overriding documentation explains that overrides using the WRAP
strategy (such as @MockitoSpyBean) end up as the target of any AOP
proxy subsequently created for the original bean; whereas, overrides
using the REPLACE or REPLACE_OR_CREATE strategy (such as @TestBean,
@MockitoBean) bypass the container's bean post-processing entirely and
therefore carry no AOP advice at all.
The new "@MockitoSpyBean and Spring AOP Proxies" section documents the
resulting stubbing and verification semantics. Verification via
Mockito's verify() API works transparently regardless of whether it is
invoked on the proxy or on the spy. Stubbing via doReturn(...)/doThrow(...)
is safe for stateless advice such as @Retryable, but can silently
corrupt the spy's configured answers for stateful or memoizing advice
such as @Cacheable, since the invocation used to declare a stub is
intercepted by Mockito before it reaches the spy and returns an empty
value that such advice may then cache.
AopTestUtils.getUltimateTargetObject(...) is documented as the way to
stub directly against the spy in that case.
The same section also documents how to disable the AOP advice for a
test altogether while leaving it in place in production code – for
example, binding a @Retryable attribute to a property placeholder
overridden via @TestPropertySource, or replacing the CacheManager with
a NoOpCacheManager via @TestBean.
Brief cross-referencing notes have also been added to the @TestBean
documentation and to the existing AopTestUtils description in the
"General Testing Utilities" section, to avoid duplicating the
explanation across pages.
In addition, the Javadoc for @MockitoSpyBean now contains a concise
WARNING summarizing these AOP proxy implications and linking to the new
reference documentation section for details.
Closes gh-37121
Property accessors resolved via SpEL's ReflectivePropertyAccessor and
DataBindingPropertyAccessor may be JavaBean-style accessors or plain
accessor methods used to support data classes such as Java records and
Kotlin data classes. However, neither accessor can determine, via
reflection, whether such a method is a side-effect-free read or an
action that happens to return a value. For example, File.delete() is a
public method that returns a boolean and therefore looks like a
plain "property".
To better inform users, this commit updates the Javadoc for
ReflectivePropertyAccessor, DataBindingPropertyAccessor, and
SimpleEvaluationContext (as well as in the SpEL reference
documentation) to clarify that restricting a SimpleEvaluationContext to
read-only data binding governs only whether assignment to a property is
permitted and does not guarantee that reading a property is free of
side effects. The reference documentation's "Security Considerations"
section now also defines what makes a method "accessor-shaped", with
concrete examples of safe versus side-effecting methods that share that
shape (for example, File.delete(), Queue.poll(), and
AtomicInteger.incrementAndGet()).
Building on that clarification, this commit introduces a new "Object
Design" section to the SpEL reference documentation, analogous to the
"Model Design" guidance for web data binding. This new section
recommends that any object reachable from an untrusted SpEL expression
(not only the root object) be a purpose-built, immutable type with a
deliberately limited surface area, and that its accessor-shaped methods
be audited for unsafe side effects. The new section also notes that
reachability is transitive through both property navigation and
indexing (for example, rootObject.child.grandchild or
rootObject.items[0]).
In any case, it remains the responsibility of the code that exposes a
root object or other reachable object to an expression from an
untrusted source to ensure that none of its accessor-shaped methods
perform an unsafe action.
Closes gh-37102
The Javadoc for EvaluationContext, StandardEvaluationContext, and
SimpleEvaluationContext previously repeated the same detailed
explanation of trusted sources and best-effort restrictions in four
places, making it hard to maintain and to digest.
This commit condenses each class-level warning to a succinct summary
that links to the new "Security Considerations" section of the SpEL
reference documentation, which remains the single, detailed source of
truth introduced in 9b42a40a2a.
See gh-36997
This commit adds a "Lifecycle and Reuse" section to the SpEL reference
documentation, immediately following the security considerations
introduced for gh-36997, explaining that AST nodes within a parsed
Expression may cache resolved PropertyAccessor, IndexAccessor,
MethodExecutor, and ConstructorExecutor instances for performance.
We also now document that reusing a parsed Expression across
EvaluationContext instances of the same type and with equivalent
configuration is supported (even if atypical), including when accessors
or resolvers registered with a context change between evaluations, but
that reusing a parsed Expression across contexts with different
security implications (for example, first against a
StandardEvaluationContext and later against a SimpleEvaluationContext)
is not supported, since cached state from a more permissive evaluation
may be reused during a more restrictive one.
The Javadoc for Expression, SpelExpression, EvaluationContext,
StandardEvaluationContext, SimpleEvaluationContext, PropertyAccessor,
IndexAccessor, MethodExecutor, and ConstructorExecutor has also been
updated to make these contracts discoverable via the API as well.
Closes gh-36968
This commit clarifies in the Javadoc for EvaluationContext,
StandardEvaluationContext, and SimpleEvaluationContext (as well as in
the SpEL reference documentation) that StandardEvaluationContext must
never be used to evaluate expressions from an untrusted source, and
that SimpleEvaluationContext's restricted language and feature subset
is only a best-effort measure. The updated documentation also defines a
"trusted" source as a developer or administrator of the application and
points out that it is the responsibility of the code that configures an
EvaluationContext to ensure that no object reachable via the context
exposes dangerous operations.
Closes gh-36997
Prior to this commit, the SpEL Indexer's PropertyAccessorValueRef could
reuse a cached PropertyAccessor for reads and writes even after that
accessor had been removed from the current EvaluationContext, leading
to stale property access if the EvaluationContext changes between
evaluations of the same expression.
This commit aligns PropertyAccessorValueRef with the analogous logic
in PropertyOrFieldReference and Indexer's IndexAccessorValueRef by
verifying that the cached PropertyAccessor is still registered in the
current EvaluationContext before reusing it in getValue() and
setValue().
Closes gh-36986
Prior to this commit, a SpEL ConstructorReference could reuse a cached
ConstructorExecutor even when the current EvaluationContext no longer
had any registered ConstructorResolvers, leading to inconsistent
behavior if the EvaluationContext changes between evaluations of the
same expression.
This commit aligns ConstructorReference with the analogous logic in
PropertyOrFieldReference by discarding the cached ConstructorExecutor
whenever there are no ConstructorResolvers registered in the current
EvaluationContext, ensuring that constructor resolution consistently
fails with a CONSTRUCTOR_NOT_FOUND exception in that scenario.
Closes gh-36985
Prior to this commit, SpEL's InlineList was cached as a mutable list in
compiled mode (i.e., in a static field in the generated byte code).
To address that, this commit modifies InlineList's generateClinitCode()
method so that it wraps both top-level and nested inline lists using
Collections.unmodifiableList(), analogous to what we already do in
createList().
Closes gh-37001
This commit picks up where ce718cf699 left off by ensuring that
findAllLocalMergedAnnotations() also finds annotations declared on
transitive interfaces — that is, on interfaces of the directly
implemented interfaces of the root declaring class.
The previous implementation filtered annotations from a single
TYPE_HIERARCHY search by checking whether the annotation's source was
either the root declaring class or one of its directly declared
interfaces. However, this excluded annotations inherited through an
interface chain such as First -> Second -> Third, where the annotation
is declared on Third but not on Second.
This commit replaces that approach with two targeted searches whose
results are combined:
- DIRECT on the root declaring class, to capture annotations declared
directly on the class (including via composed/meta-annotations)
- TYPE_HIERARCHY on each directly implemented interface, which
naturally traverses the full super-interface chain of each interface
A corresponding test for this scenario has also been added.
Closes gh-36975
Prior to this commit, AnnotationDescriptor's
findAllLocalMergedAnnotations() filtered results using
MergedAnnotationPredicates.firstRunOf(
MergedAnnotation::getAggregateIndex), which retains only annotations
that share the same aggregate index as the first annotation
encountered. Since annotations on the root declaring class have
aggregate index 0 and annotations on interfaces have higher indices,
interface annotations were silently excluded whenever the root
declaring class itself declared the annotation.
This commit fixes the issue by replacing the aggregate-index-based
filter with a source-based filter that explicitly includes annotations
from the root declaring class and from each directly implemented
interface. The Javadoc for findAllLocalMergedAnnotations() has also
been updated to document the ordering guarantee: annotations from the
root declaring class appear first, followed by annotations from
implemented interfaces in declaration order.
Closes gh-36975