Compare commits

...
117 Commits
Author SHA1 Message Date
Brian Clozel 82a6b40b93 Release Spring Framework 7.0.9 2026-08-14 09:12:18 +02:00
Brian Clozel a7b1b59cbd Upgrade to Reactor 2025.0.7
Closes gh-37103
2026-08-14 09:12:18 +02:00
Brian Clozel 996e3d3f18 Upgrade to Micrometer 1.16.7
Closes gh-37104
2026-08-14 09:12:18 +02:00
rstoyanchev 73f5ddddcd Refactor maxInMemory limit handling for async XML parsing
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
2026-08-14 09:11:50 +02:00
rstoyanchev 675f25de72 Leading slash handling in UrlHandlerFilter
Closes gh-37030
2026-08-14 09:11:50 +02:00
rstoyanchev 692dbc9160 Apply ResourceHandlerUtils checks in XsltView
Closes gh-37029
2026-08-14 09:11:50 +02:00
rstoyanchev 8647e90bc7 Consistent maxPartSize check in PartEventHttpMessageReader
Closes gh-37028
2026-08-14 09:11:50 +02:00
rstoyanchev b9379e33d5 Check viewName for special prefixes in UrlFilenameViewController
Closes gh-37027
2026-08-14 09:11:50 +02:00
rstoyanchev a784dbe286 Ensure Payload release on early error in createHeaders
Closes gh-37026
2026-08-14 09:11:50 +02:00
rstoyanchev 3b492f3908 Return sameSite cookie value in Jetty response
Closes gh-37025
2026-08-14 09:11:50 +02:00
rstoyanchev 07cbd482a0 Add preflight handling in RouterFunctionWebHandler
Request predicates support preflight request matching based on the "would be"
request (e.g. target HTTP method) so the actual handler is not meant to be
invoked. That's the case only with a DispatcherHandler setup.

Closes gh-37024
2026-08-14 09:11:50 +02:00
rstoyanchev dadd474d21 Update exception messages in HandshakeWebSocketService
Closes gh-37023
2026-08-14 09:11:50 +02:00
Sam Brannen 0d08f8dfaf Disable SpEL expression compilation by default in SimpleEvaluationContext
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
2026-08-14 09:11:50 +02:00
Sam Brannen baae93f20a Limit result size of BigDecimal/BigInteger power operations in SpEL
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
2026-08-14 09:11:50 +02:00
Sam Brannen d186b381b9 Check list index after auto-grow in AbstractNestablePropertyAccessor
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
2026-08-14 09:11:50 +02:00
Sébastien Deleuze ac0f8be0d8 Ensure consistent EscapedErrors field error escaping
Closes gh-37055
2026-08-14 09:11:50 +02:00
Sébastien Deleuze 6e3dc633f0 Reject backslashes in SpringTemplateLoader template names
Closes gh-37054
2026-08-14 09:11:50 +02:00
Brian Clozel 35921cc01f Centralize Server Sent Event utility methods
Prior to this commit, many classes would support writing Server Sent
Events in some way to the response output stream. This has lead to some
code duplication.

This commit refactors the duplicated code in a shared `SseUtils` class.

Closes gh-37065
2026-08-14 09:11:50 +02:00
Brian Clozel 062032373e Ensure parsing/tostring symmetry in ContentDisposition
Prior to this commit, building a "Content-Disposition" header to a
String and then parsing it back would not always result in the original
header.

This commit ensures that ContentDisposition guarantees this and honors
the "equals" contract.

Fixes gh-37064
2026-08-14 09:11:50 +02:00
Brian Clozel 1994e0ebd0 Escape SSE view fragments
Prior to this commit, the MVC and WebFlux view fragments rendering would
only partially escape rendered view fragments before sending then as SSE
events. This could in some cases break the SSE stream with invalid data.

This commit ensures that the rendered views are properly escaped before
they are sent as SSE events.

Fixes gh-37061
2026-08-14 09:11:50 +02:00
Brian Clozel dc7fc89aec Switch to INTERNAL-SNAPSHOT versions
See gh-37103
See gh-37104
2026-08-14 09:11:49 +02:00
Brian Clozel f2a7f13d13 Prepare 7.0.x-internal branch 2026-08-14 09:11:49 +02:00
Sam Brannen f8a2bdad87 Clarify Bean Overrides and Spring AOP Proxies documentation
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
2026-08-13 18:57:57 +02:00
Juergen Hoeller 9aeda49273 Polishing 2026-08-12 00:08:59 +02:00
Juergen Hoeller abd323d428 Polishing 2026-08-11 23:10:15 +02:00
Juergen Hoeller 176bc2a133 Upgrade to Groovy 5.0.8, Jetty 12.1.12, Netty 4.2.17, Hibernate ORM 7.2.24, Checkstyle 13.10 2026-08-11 23:08:52 +02:00
Sam Brannen 8df51ad6cc Document AOP proxy semantics for Bean Overrides in tests
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
2026-08-09 17:46:28 +03:00
Sam Brannen 957df686c4 Upgrade to Gradle 9.7
Closes gh-36952
2026-08-07 10:43:29 +03:00
rstoyanchev 11bb7b54e5 Ignore an empty port
Closes gh-37117
2026-08-07 10:31:29 +03:00
rstoyanchev 70ca103b22 Polishing in UriComponentsTests
See gh-37117
2026-08-07 10:25:12 +03:00
junhyeong9812 7f1966f5f5 Reset TwoByteMatcher partial match on mismatching byte
DataBufferUtils.TwoByteMatcher inherited AbstractNestedMatcher.match(byte)
without providing the mismatch fallback that its siblings implement
(KnuthMorrisPrattMatcher backtracks via its suffix-prefix table, and
SingleByteMatcher is stateless). As a result, once the first delimiter
byte had matched, the match counter stayed at 1 across any number of
intervening non-matching bytes, so a later occurrence of the second
delimiter byte falsely completed the match.

For a two-byte delimiter such as \r\n this made the matcher report a
match across non-contiguous bytes. CompositeMatcher prefers the longest
delimiter that matches at a position, so the false \r\n match was chosen
over a real single \n, causing StringDecoder to strip two bytes and drop
the character preceding a lone \n whenever a line contained a stray \r.

TwoByteMatcher now overrides match(byte) to reset the counter to 0 when
the incoming byte is not the expected next delimiter byte before
delegating to super.match(), mirroring KnuthMorrisPrattMatcher. A
genuine contiguous delimiter is unaffected.

Signed-off-by: junhyeong9812 <pickjog@gmail.com>
2026-08-05 10:36:05 +02:00
Boris Perović b556766e1a Avoid retaining class files in annotation metadata
ClassFileAnnotationDelegate passed the raw java.lang.classfile
Annotation to MergedAnnotation.of() as the annotation source. That
annotation holds a Utf8Entry, so retaining the metadata of an
annotated class also retained its class file byte[], the parsed
constant pool, and the class model.

Store the declaring class name instead, as the ASM variant does.

See gh-37111

Signed-off-by: Boris Perović <boris.perovic@sysdig.com>
2026-08-05 10:07:44 +02:00
Brian Clozel 0c281fd1ce Fix build SNAPSHOT workflow
This commit ensures that workflow dispatches to docs build only happen
on OSS branches.
This also upgrades the verification project to the latest version.

See gh-37097
2026-08-03 11:36:22 +02:00
Sam Brannen 63f0894621 Improve wording
See gh-37102
2026-08-03 12:34:54 +03:00
Sam Brannen 0c966029e2 Document object design guidelines for SpEL expressions
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
2026-08-03 11:35:05 +03:00
Brian Clozel 05619b7450 Fix docs build trigger
See gh-37097
2026-07-31 18:51:39 +02:00
Brian Clozel f3ba3c9e1f Fix URL fetching from environment in Gradle build plugin
See gh-37097
2026-07-31 18:25:11 +02:00
Brian Clozel 89e62e7e31 Add release train infrastructure
Closes gh-37097
2026-07-31 18:08:03 +02:00
Brian Clozel e4d5ec9c0d Update GitHub workflows and build infrastructure
See gh-37097
2026-07-31 18:05:15 +02:00
Juergen Hoeller f5564e7e31 Consistently use default constants within builder
See gh-36983
2026-07-31 16:27:18 +02:00
Juergen Hoeller 6d04ea9e84 Check existing database transaction against UnitOfWork
See gh-37085
2026-07-31 16:24:09 +02:00
Sam Brannen 0f5bd82c5d Centralize SpEL security documentation in the reference manual
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
2026-07-30 16:23:44 +03:00
Sam Brannen a894818c9e Document lifecycle and reuse contract for SpEL expressions and contexts
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
2026-07-29 22:05:15 +03:00
Sam Brannen 9b42a40a2a Document security implications of evaluating untrusted SpEL expressions
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
2026-07-29 21:21:11 +03:00
rstoyanchev 719311f09b Correct since tags
See gh-37090
2026-07-29 15:18:22 +03:00
Juergen Hoeller 28a78170b5 Upgrade to Tomcat 11.0.24, Jetty 12.1.11, Hibernate ORM 7.2.23, Hibernate Validator 9.1.3, Checkstyle 13.9 2026-07-29 12:14:26 +02:00
Juergen Hoeller 8fa7d88a0c Avoid getConnection lock for existing database transaction
Closes gh-37085
2026-07-29 11:30:17 +02:00
rstoyanchev b1d025d2c6 Update documentation on forwarded headers
See gh-37090
2026-07-27 12:46:43 +03:00
rstoyanchev 181a5d3403 Allow choice between Forwarded and X-Forwarded headers
This commit introduces a constructor argument to select whether
to use the standard "Forwarded" header or the "X-Forwarded-*"
alternative headers. A separate property to control support for
X-Forwarded-Prefix.

Closes gh-37090
2026-07-27 12:46:43 +03:00
Sam Brannen ae4214aa95 Do not reuse cached PropertyAccessor in Indexer without checking EvaluationContext
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
2026-07-26 10:56:44 +03:00
Sam Brannen 4f086322d0 Do not reuse cached ConstructorExecutor without ConstructorResolvers
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
2026-07-25 16:05:23 +03:00
Alexis SEGURA e255ccca7d Update Javadoc for active profile ordering
Following gh-26004, the registration order of active profiles in
@⁠ActiveProfiles is preserved; however, the Javadoc in
MergedContextConfiguration and WebMergedContextConfiguration still
described the old sorting behavior, which is misleading since profile
order is part of the context cache key (equals/hashCode).

This commit updates the Javadoc to document the status quo.

Closes gh-36950

Signed-off-by: Alexis SEGURA <alex.segura06@gmail.com>
2026-07-25 11:04:11 +03:00
Brian Clozel b5de644cbb Do not deploy org.springframework:framework-docs on Central
Fixes gh-36879
2026-07-24 15:15:22 +02:00
Brian Clozel 7de2b24d81 Fix primitive array annotation attributes on Java 24 class reading
Prior to this commit, `ClassFileAnnotationDelegate#parseArrayValue`
would only consider `int[]`, `double[]` and `long[]` array
annotation attributes; other primitive array types like `byte[]`
would use a generic path that would use boxed types.

This commit ensures that a comprehensive pass is made for all
primitive typed arrays. Because the `AnnotationValue` hierarchy
is sealed, we can now maje sure that the implementation is
exhaustive.

Closes gh-37083
2026-07-22 10:50:07 +02:00
Sam Brannen b90624472e Polish Javadoc for ProtobufDecoder 2026-07-21 11:57:20 +03:00
BAE JAE HYEON 1600e2479b Fix Javadoc in ProtobufDecoder's MessageSizeReader API
Previously, the @⁠return tag in MessageSizeReader.readMessageSize() was
followed by a redundant "return", which rendered as "Returns: return
the message size..." in the published API documentation.

Closes gh-37079

Signed-off-by: BAE JAE HYEON <roblery128@gmail.com>
2026-07-21 11:53:24 +03:00
Manu Sridharan 233725c8f5 Add @⁠Nullable annotations when treating Map.remove() as returning @⁠Nullable
Closes gh-37067

Signed-off-by: Manu Sridharan <msridhar@gmail.com>
2026-07-20 11:02:55 +03:00
Brian Clozel 8fb2f72282 Use ASCII chars in Content-Disposition filename parameter
Prior to this commit, gh-36328 avoided using RFC 2047 encoding for the
"filename" parameter and use ISO-8859-1 only. This change unfortunately
caused issues because some implementations might try and detect the
encoding automatically.

This commit restricts the filename parameter to ASCII encoding only by:
* transliterating characters to the closes ASCII character
("é"->"e", "ä"->"ae"...)
* falling back to "_" for other chacacters with non latin alphabet or
  emojis

Closes gh-37062
2026-07-16 19:21:16 +02:00
samlightfoot a2feb9ffe6 Release Jackson BufferRecycler to its pool in encoders
Both encoders acquire a pooled BufferRecycler via
factory._getBufferRecycler() but never return it. Jackson 3 changed the
default pool from a ThreadLocal to a per-factory ConcurrentDequePool that
only refills on an explicit releaseToPool(), so the gap leaves the pool
empty and every encode allocates a fresh recycler and buffers.

Hold the recycler in a local and release it once the generator and byte
builder are done: in encodeValue's finally, and in the streaming path's
doAfterTerminate after generator.close() and byteBuilder.release().
releaseToPool() is idempotent, so sharing the recycler with the generator
cannot double-release.

Closes gh-37059

Signed-off-by: samlightfoot <samueldlightfoot@gmail.com>
2026-07-16 19:00:14 +02:00
Dmitry Sulman 27aa9e46c5 Add awaitEntityWithRetry extension to WebClient.ResponseSpec
See #36771
Closes #36808

Signed-off-by: Dmitry Sulman <dmitry.sulman@gmail.com>
2026-07-16 17:25:29 +02:00
Juergen Hoeller 56d706d591 Skip concurrency limit tests when common pool parallelism is too low 2026-07-16 10:17:29 +02:00
junhyeong9812 bbfe6a0473 Make immediate-cancel task termination test deterministic
The taskTerminationTimeoutWithImmediateCancel test submitted a task and
immediately closed the executor, then asserted that the future was
cancelled. The cancellation flag is set by close() on the calling thread,
while it is checked at the start of the task on a separate worker thread.
With no ordering guarantee between the two, a quickly scheduled worker
could pass the cancellation check before close() set the flag, complete
the trivial task normally, and leave the future uncancelled, making the
test fail intermittently under load.

Override doExecute to capture the task-tracking wrapper instead of
running it on a background thread, then run it on the test thread after
close() has set the cancellation flag. This exercises the same
cancellation path deterministically, with no reliance on thread
scheduling.

Signed-off-by: junhyeong9812 <pickjog@gmail.com>
2026-07-10 20:30:19 +02:00
Sébastien Deleuze 13a43e76cb Fix Javadoc error in RetryPolicy
See gh-36983
2026-07-08 20:46:17 +02:00
Sébastien Deleuze 16e82cd693 Refine JdbcTemplate nullability contribution
Closes gh-37012
2026-07-08 16:52:17 +02:00
Chris 516a2ca511 Make batchArgs contents nullable in batchUpdate
Updated batchUpdate method signatures to allow nullable Object arrays.

See gh-37012

Signed-off-by: Chris <christian@vonrueti.ch>
2026-07-08 15:26:55 +02:00
Yanming Zhou 9726c7ed5f Let composite/filtered collections accept null elements
Closes gh-36923

Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>
2026-07-08 15:17:17 +02:00
Sébastien Deleuze 7e0818f4e9 Align domainToAscii with current WhatWG spec
The WhatWG URL Standard changed its "domain to ASCII" algorithm when
beStrict is false and the domain is an ASCII string, it now returns
the domain lowercased regardless of Unicode ToASCII's outcome, for web
compatibility. Invalid or ambiguous "xn--" (ACE) labels are no longer
rejected or validated; they are lowercased and accepted, matching
browsers and the web-platform-tests URL cases.

This supersedes the earlier spec revision that only lowercased ASCII
domains whose labels did not start with "xn--". Drop the now-obsolete
"xn--" label detection (which was dead code anyway due to a typo) and
unconditionally lowercase ASCII domains.

Closes gh-37018
2026-07-08 15:11:42 +02:00
Sébastien Deleuze ed13afa0d4 Ensure consistent ButtonTag value attribute processing
Closes gh-37017
2026-07-08 15:08:51 +02:00
Juergen Hoeller c08e5e9c1c Upgrade to Log4J 2.26.1, Groovy 5.0.7, Tomcat 11.0.23, Jetty 12.1.10, EclipseLink 5.0.1, Hibernate ORM 7.2.22, Hibernate Validator 9.1.2, Caffeine 3.2.4, Protobuf 4.35.1, Checkstyle 13.7 2026-07-06 12:30:41 +02:00
Juergen Hoeller 97e9ddb2d7 Add constant for default timeout value
Closes gh-36983
2026-07-06 11:57:27 +02:00
Sam Brannen f0e69a702c Ensure SpEL's InlineList is immutable in compiled mode
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
2026-07-06 10:52:54 +02:00
junhyeong9812 d1470bbb25 Register native configuration file when only lambda hints are present
Prior to this commit, FileNativeConfigurationWriter did not write reachability-metadata.json
when a RuntimeHints instance contained only lambda hints, because
NativeConfigurationWriter.hasAnyHint() omitted
ReflectionHints.lambdaHints() from its checks. As a result, lambda
metadata emitted by RuntimeHintsWriter was silently dropped.

This commit addresses that by including lambda hints in hasAnyHint() so
the configuration file is written whenever lambda hints are present.

Closes gh-36989

Signed-off-by: junhyeong9812 <pickjog@gmail.com>
2026-07-02 12:06:24 +02:00
Sébastien Deleuze 347f23a73f Upgrade nullability plugin to 0.0.14
Nullaway 0.13.7 is now the default.

See gh-36850
2026-07-01 15:39:44 +02:00
Manu Sridharan 99b3e205d1 Update to NullAway 0.13.6
Add missing nullness annotation in
UriComponents.VarArgsTemplateVariables to fix new warnings.

Closes gh-36850

Signed-off-by: Manu Sridharan <msridhar@gmail.com>
2026-07-01 15:39:30 +02:00
Sam Brannen 78f05d8f8e Address deprecation warnings
This commit addresses warnings across the code base related to:

- internal and public deprecations in Spring Framework
- deprecated Locale constructors
- deprecated URL constructors
- deprecated Thread#getId method
2026-06-27 18:08:53 +02:00
Sam Brannen 996b337f37 Upgrade to io.spring.develocity.conventions 0.0.25 2026-06-27 17:09:30 +02:00
Sam Brannen 66ffc04fe5 Avoid Gradle deprecation warnings
After the upgrade to Gradle 9.6.0/9.6.1, the Gradle build started
emitting warnings due to use of deprecated APIs.

For example, Project.getProperties() is now annotated as @⁠Deprecated
in Gradle 9.6 and will be removed in Gradle 10.0.

To avoid the warnings, this commit modifies:

- TestConventions to use `project.findProperty(...)` instead of
  `project.getProperties().get(...)`

- framework-api.gradle to use `rootProject.ext.moduleProjects` instead
  of simply `moduleProjects`

- framework-api.gradle and framework-bom.gradle to use
  `project(<projectX>)` instead of simply `<projectX>`

- ide.gradle so that it no longer uses the deprecated `javaRuntimeName`

See gh-36952
2026-06-27 17:02:40 +02:00
Sam Brannen ade96d275d Upgrade to Gradle 9.6.1
Closes gh-36952
2026-06-27 16:16:59 +02:00
Sam Brannen 072fa3f43d Polishing 2026-06-27 16:11:42 +02:00
Sam Brannen 78dcdab3fc Polishing
See gh-36972
2026-06-27 15:19:42 +02:00
junhyeong9812 872b1addeb Write native configuration files as UTF-8
Prior to this commit, FileNativeConfigurationWriter wrote native-image
configuration files using a plain FileWriter, which encodes with the
JVM platform default charset. On a non-UTF-8 platform (for example a
Windows JVM, where the default charset is not UTF-8 prior to JDK 18)
non-ASCII characters in resource patterns or bundle names were written
with the wrong encoding, while GraalVM expects the configuration files
to be UTF-8.

This commit specifies StandardCharsets.UTF_8 explicitly so the files
are always written as UTF-8, consistent with the UTF-8 usage already
present in the aot.generate package.

Closes gh-36972

Signed-off-by: junhyeong9812 <pickjog@gmail.com>
2026-06-27 15:10:30 +02:00
Juergen Hoeller 98d552f15f Add javadoc note on trusted broker setups for Spring JMS 2026-06-26 18:21:00 +02:00
Sam Brannen 4d6e88dc98 Fix off-by-one error in MimeTypeUtils.parseMimeType()
Due to changes made in commit 41cd6879bd, MimeTypeUtils now raises a
StringIndexOutOfBoundsException instead of an InvalidMimeTypeException
when parsing certain invalid mime types -- for example, for a value
wrapped in double quotes which does not contain a ";" character.

To address that minor regression, this commit replaces
`mimeType.charAt(nextIndex - 1) != '\\'` with
`(nextIndex == 0 || mimeType.charAt(nextIndex - 1) != '\\')` to avoid
invoking `String#charAt` with a negative value.

See gh-36730
Closes gh-36971
2026-06-26 17:40:53 +02:00
rstoyanchev 0044c4c8a3 Improve remoteAddress check in TransportHandlingSockJsService
Closes gh-36904
2026-06-25 16:10:34 +01:00
Sam Brannen b00f691655 Preserve parameter order in DefaultServerRequest's ServletParametersMap
Prior to this commit, DefaultServerRequest's ServletParametersMap lost
the original parameter order when entrySet() was invoked.

To address that, this commit revises ServletParametersMap.entrySet() so
that it stores the results in a LinkedHashSet, thereby retaining the
original order.

Closes gh-36966
2026-06-25 13:57:34 +02:00
rstoyanchev 6ac642e301 Remove closeLock from ConcurrentWebSocketSessionDecorator#checkSessionLimits
Closes gh-36909
2026-06-25 12:50:32 +01:00
rstoyanchev 11bdb43aad Polishing in ConcurrentWebSocketSessionDecorator
See gh-36909
2026-06-25 12:50:32 +01:00
Sam Brannen 4074155d76 Polish contribution
See gh-36948
2026-06-25 13:23:49 +02:00
junhyeong9812 1277279527 Ignore DOCTYPE inside a multi-line comment body
XmlValidationModeDetector peeks at the start of an XML document to
choose between DTD- and XSD-based validation, skipping any DOCTYPE that
appears inside an XML comment.

Prior to this commit, consumeCommentTokens() short-circuited a line
with no start or end comment marker by returning it unchanged, even
while already inside a multi-line comment. Such a body line was then
treated as content, so a literal "DOCTYPE" word in the comment body
caused an XSD document to be misdetected as DTD-based.

This commit honors the "in comment" parse state in that early return so
a comment body line is treated as empty content, completing the fix for
gh-27915 which only covered comment markers on the same line.

Closes gh-36948

Signed-off-by: junhyeong9812 <pickjog@gmail.com>
2026-06-25 13:07:57 +02:00
Juergen Hoeller d0331a049a Refine various javadoc notes 2026-06-22 21:52:57 +02:00
Juergen Hoeller 7de9c8d58a Consistently handle input format mismatch 2026-06-22 21:52:18 +02:00
Juergen Hoeller bae022a0fc Perform nextKey within synchronization
Closes gh-36959
2026-06-22 21:51:40 +02:00
Juergen Hoeller 325c3e1ec7 Add support for custom ObjectInputFilter
Closes gh-36958
2026-06-22 21:51:11 +02:00
Juergen Hoeller 67e5ea9509 Revise resource bundle caching for common locales
Closes gh-36957
2026-06-22 21:50:56 +02:00
rstoyanchev 41b8f13e91 Fix checkStyleNoHttp errors
See gh-36893
2026-06-22 14:08:25 +01:00
rstoyanchev 167afd91be Fix delegation in LifecyclePrintWriter
Closes gh-36885
2026-06-22 13:12:50 +01:00
rstoyanchev 12f9a5c2a5 Apply ResourceHandlerUtils to Groovy markup templates
Closes gh-36902
2026-06-22 11:57:12 +01:00
rstoyanchev 1a89363a5c Restore host header initialization in StompBrokerRelayMessageHandler
Closes gh-36907
2026-06-22 11:35:38 +01:00
rstoyanchev 34a307d4a6 Improve requestUri handling in ReactorUriHelper
Closes gh-36893
2026-06-22 11:12:13 +01:00
rstoyanchev 0e4842062b Polishing in ReactorUriHelper
See gh-36893
2026-06-22 11:06:43 +01:00
rstoyanchev 76089feeb4 Refine error handling in JettyWebSocketSession
Closes gh-36891
2026-06-22 10:42:51 +01:00
rstoyanchev ad5bd67b31 Improve ambiguous preflight checks in AbstractHandlerMethodMapping
Closes gh-36903
2026-06-22 10:42:51 +01:00
rstoyanchev 0ee636af7f Improve Javadoc of UrlFilenameViewController
Closes gh-36906
2026-06-22 10:42:51 +01:00
rstoyanchev c64f2ee104 Disable assignment in DefaultSubscriptionRegistry
Closes gh-36924
2026-06-22 10:42:51 +01:00
rstoyanchev 5059bbd58e Improve parameter-based content negotiation
Closes gh-36925
2026-06-22 10:42:51 +01:00
rstoyanchev cc0ca1b6a5 Polishing in MappingContentNegotiationStrategy hierarchy and tests
See gh-36925
2026-06-22 10:42:51 +01:00
Sam Brannen ee81785afc Upgrade to Gradle 9.6
Closes gh-36952
2026-06-19 16:31:38 +02:00
Sam Brannen 846a6a8f7c Document behavior for 0 delay combined with jitter
Closes gh-36946
2026-06-17 12:41:03 +02:00
Sam Brannen 0d706f8da6 Polish contribution
See gh-36932
2026-06-17 12:38:31 +02:00
junhyeong9812 924849f55b Avoid divide-by-zero in ExponentialBackOff jitter
When an ExponentialBackOff is configured with an initialInterval of 0
and a positive jitter, the first nextBackOff() evaluated (jitter *
(interval / initialInterval)) performs integer division by zero
and throws an ArithmeticException.

Both initialInterval = 0 and jitter > 0 are individually accepted
configurations -- with jitter = 0, an initialInterval of 0 already
yields a delay of 0 -- so the combination should not throw.

This commit addresses that by guarding the division so that no jitter
scaling is applied when initialInterval is 0, leaving the behavior for
positive intervals unchanged.

Closes gh-36932

Signed-off-by: junhyeong9812 <pickjog@gmail.com>
2026-06-17 12:09:30 +02:00
gianmarco 136f78ebd0 Fix Javadoc errors in AotContextLoaderTests
Closes gh-36937

Signed-off-by: gianmarco <gianmarco.schifone02@gmail.com>
2026-06-17 11:58:20 +02:00
Yanming Zhou 0fc724b348 Make inner classes in tests static where feasible
Closes gh-36939

Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>
2026-06-17 11:49:12 +02:00
Sam Brannen 292959e2ca Improve nullability for getSession(*) in MockHttpServletRequest
Closes gh-36926
2026-06-15 15:35:17 +02:00
Sam Brannen 5383520388 Sync changes in MockHttpServletRequest to spring-web test fixture 2026-06-15 15:34:32 +02:00
Juergen Hoeller 0b5a9ea30a Adapt query proxy if necessary (for compatibility with Hibernate 8.0)
Closes gh-36878
2026-06-09 22:40:22 +02:00
Brian Clozel 298e1db625 Skip web fonts in module javadocs
Prior to this commit, module javadoc packages like
"spring-core-7.0.8-javadoc.jar" would contain 4MB of web fonts in the
`resource-files/fonts/` folder. This increases significantly (often
doubles) the size of the javadoc JAR for little value.

With this commit, Web fonts are packaged with the aggregated Javadocs
for the entire Spring Framework project, but are skipped for inidividual
modules.

Closes gh-36889
2026-06-08 22:15:27 +02:00
Brian Clozel 3f7f75abdb Next development version (v7.0.9-SNAPSHOT) 2026-06-08 19:50:42 +02:00
298 changed files with 5557 additions and 2100 deletions
@@ -1,20 +0,0 @@
name: Await HTTP Resource
description: 'Waits for an HTTP resource to be available (a HEAD request succeeds)'
inputs:
url:
description: 'URL of the resource to await'
required: true
runs:
using: composite
steps:
- name: Await HTTP resource
shell: bash
run: |
url=${{ inputs.url }}
echo "Waiting for $url"
until curl --fail --head --silent ${{ inputs.url }} > /dev/null
do
echo "."
sleep 60
done
echo "$url is available"
+22
View File
@@ -1,6 +1,18 @@
name: 'Build'
description: 'Builds the project, optionally publishing it to a local deployment repository'
inputs:
commercial-release-repository-url:
description: 'URL of the release repository'
required: false
commercial-repository-password:
description: 'Password for authentication with the commercial repository'
required: false
commercial-repository-username:
description: 'Username for authentication with the commercial repository'
required: false
commercial-snapshot-repository-url:
description: 'URL of the snapshot repository'
required: false
develocity-access-key:
description: 'Access key for authentication with ge.spring.io'
required: false
@@ -46,11 +58,21 @@ runs:
id: build
if: ${{ inputs.publish == 'false' }}
shell: bash
env:
COMMERCIAL_RELEASE_REPO_URL: ${{ inputs.commercial-release-repository-url }}
COMMERCIAL_REPO_PASSWORD: ${{ inputs.commercial-repository-password }}
COMMERCIAL_REPO_USERNAME: ${{ inputs.commercial-repository-username }}
COMMERCIAL_SNAPSHOT_REPO_URL: ${{ inputs.commercial-snapshot-repository-url }}
run: ./gradlew check antora
- name: Publish
id: publish
if: ${{ inputs.publish == 'true' }}
shell: bash
env:
COMMERCIAL_RELEASE_REPO_URL: ${{ inputs.commercial-release-repository-url }}
COMMERCIAL_REPO_PASSWORD: ${{ inputs.commercial-repository-password }}
COMMERCIAL_REPO_USERNAME: ${{ inputs.commercial-repository-username }}
COMMERCIAL_SNAPSHOT_REPO_URL: ${{ inputs.commercial-snapshot-repository-url }}
run: ./gradlew -PdeploymentRepository=$(pwd)/deployment-repository build publishAllPublicationsToDeploymentRepository
- name: Read Version From gradle.properties
id: read-version
@@ -1,6 +1,13 @@
name: Create GitHub Release
description: 'Create the release on GitHub with a changelog'
inputs:
commercial:
description: 'Whether to generate the changelog for the commercial release'
required: true
latest:
description: 'Whether the release is the latest release'
required: false
default: 'false'
milestone:
description: 'Name of the GitHub milestone for which a release will be created'
required: true
@@ -15,13 +22,13 @@ runs:
using: composite
steps:
- name: Generate Changelog
uses: spring-io/github-changelog-generator@86958813a62af8fb223b3fd3b5152035504bcb83 #v0.0.12
uses: spring-io/github-changelog-generator@f7d7a87a3e7c627ecb8c26cf086c38ac5a939721 #v0.0.14
with:
config-file: .github/actions/create-github-release/changelog-generator.yml
config-file: ${{ inputs.commercial && '.github/actions/create-github-release/changelog-generator-commercial.yml' || '.github/actions/create-github-release/changelog-generator-oss.yml' }}
milestone: ${{ inputs.milestone }}
token: ${{ inputs.token }}
- name: Create GitHub Release
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.token }}
run: gh release create ${{ format('v{0}', inputs.milestone) }} --notes-file changelog.md ${{ inputs.pre-release == 'true' && '--prerelease' || '' }}
run: gh release create ${{ format('v{0}', inputs.milestone) }} --notes-file changelog.md ${{ inputs.pre-release == 'true' && '--prerelease' || format('--latest={0}', inputs.latest) }}
@@ -0,0 +1,33 @@
changelog:
repository: spring-projects/spring-framework-commercial
sections:
- title: ":warning: Attention Required"
labels:
- "for: upgrade-attention"
summary:
mode: "member-comment"
config:
prefix: "Attention Required:"
- title: ":star: New Features"
labels:
- "type: enhancement"
- title: ":lady_beetle: Bug Fixes"
labels:
- "type: bug"
- "type: regression"
- title: ":notebook_with_decorative_cover: Documentation"
labels:
- "type: documentation"
- title: ":hammer: Dependency Upgrades"
sort: "title"
labels:
- "type: dependency-upgrade"
contributors:
exclude:
names:
- "bclozel"
- "jhoeller"
- "rstoyanchev"
- "sbrannen"
- "sdeleuze"
- "snicoll"
@@ -27,9 +27,7 @@ changelog:
names:
- "bclozel"
- "jhoeller"
- "poutsma"
- "rstoyanchev"
- "sbrannen"
- "sdeleuze"
- "simonbasle"
- "snicoll"
+10 -12
View File
@@ -29,27 +29,25 @@ runs:
distribution: ${{ inputs.java-early-access == 'true' && 'temurin' || (inputs.java-distribution || 'liberica') }}
java-version: |
${{ inputs.java-early-access == 'true' && format('{0}-ea', inputs.java-version) || inputs.java-version }}
${{ inputs.java-toolchain == 'true' && '17' || '' }}
25
${{ inputs.java-toolchain == 'true' && '25' || '' }}
- name: Set Up Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # 6.2.0
with:
cache-provider: basic
cache-read-only: false
develocity-access-key: ${{ inputs.develocity-access-key }}
develocity-token-expiry: 4
- name: Configure Gradle Properties
shell: bash
run: |
mkdir -p $HOME/.gradle
echo 'systemProp.user.name=spring-builds+github' >> $HOME/.gradle/gradle.properties
echo 'systemProp.org.gradle.internal.launcher.welcomeMessageEnabled=false' >> $HOME/.gradle/gradle.properties
echo 'org.gradle.daemon=false' >> $HOME/.gradle/gradle.properties
echo 'org.gradle.daemon=4' >> $HOME/.gradle/gradle.properties
echo 'systemProp.user.name=spring-builds+github' >> $GRADLE_USER_HOME/gradle.properties
echo 'systemProp.org.gradle.internal.launcher.welcomeMessageEnabled=false' >> $GRADLE_USER_HOME/gradle.properties
echo 'org.gradle.daemon=false' >> $GRADLE_USER_HOME/gradle.properties
- name: Configure Toolchain Properties
if: ${{ inputs.java-toolchain == 'true' }}
shell: bash
run: |
echo toolchainVersion=${{ inputs.java-version }} >> $HOME/.gradle/gradle.properties
echo systemProp.org.gradle.java.installations.auto-detect=false >> $HOME/.gradle/gradle.properties
echo systemProp.org.gradle.java.installations.auto-download=false >> $HOME/.gradle/gradle.properties
echo systemProp.org.gradle.java.installations.paths=${{ format('$JAVA_HOME_{0}_X64', inputs.java-version) }} >> $HOME/.gradle/gradle.properties
echo toolchainVersion=${{ inputs.java-version }} >> $GRADLE_USER_HOME/gradle.properties
echo systemProp.org.gradle.java.installations.auto-detect=false >> $GRADLE_USER_HOME/gradle.properties
echo systemProp.org.gradle.java.installations.auto-download=false >> $GRADLE_USER_HOME/gradle.properties
echo systemProp.org.gradle.java.installations.paths=${{ format('$JAVA_HOME_{0}_X64', inputs.java-version) }} >> $GRADLE_USER_HOME/gradle.properties
@@ -0,0 +1,7 @@
name: Build Release
runs:
using: composite
steps:
- name: Build Release
shell: bash
run: ./gradlew -PdeploymentRepository=$(pwd)/deployment-repository publishAllPublicationsToDeploymentRepository
@@ -0,0 +1,16 @@
artifactory:
artifacts:
- pattern: "/**/framework-api-*.zip"
properties:
zip.deployed: "false"
zip.name: "spring-framework"
- pattern: "/**/framework-api-*-docs.zip"
properties:
zip.type: "docs"
- pattern: "/**/framework-api-*-schema.zip"
properties:
zip.type: "schema"
maven-central:
excludes:
- "org/springframework/framework-api/**"
- "org/springframework/framework-docs/**"
@@ -0,0 +1,7 @@
name: Test Release
runs:
using: composite
steps:
- name: Test Release
shell: bash
run: ./gradlew check
@@ -1,34 +0,0 @@
name: Sync to Maven Central
description: 'Syncs a release to Maven Central and waits for it to be available for use'
inputs:
central-token-password:
description: 'Password for authentication with central.sonatype.com'
required: true
central-token-username:
description: 'Username for authentication with central.sonatype.com'
required: true
jfrog-cli-config-token:
description: 'Config token for the JFrog CLI'
required: true
spring-framework-version:
description: 'Version of Spring Framework that is being synced to Central'
required: true
runs:
using: composite
steps:
- name: Set Up JFrog CLI
uses: jfrog/setup-jfrog-cli@5b06f730cc5a6f55d78b30753f8583454b08c0aa # v4.8.1
env:
JF_ENV_SPRING: ${{ inputs.jfrog-cli-config-token }}
- name: Download Release Artifacts
shell: bash
run: jf rt download --spec ${{ format('{0}/artifacts.spec', github.action_path) }} --spec-vars 'buildName=${{ format('spring-framework-{0}', inputs.spring-framework-version) }};buildNumber=${{ github.run_number }}'
- name: Sync
uses: spring-io/central-publish-action@0c03960e9b16fdfe70e2443e1d5393cbc3a35622 # v0.3.0
with:
token: ${{ inputs.central-token-password }}
token-name: ${{ inputs.central-token-username }}
- name: Await
uses: ./.github/actions/await-http-resource
with:
url: ${{ format('https://repo.maven.apache.org/maven2/org/springframework/spring-context/{0}/spring-context-{0}.jar', inputs.spring-framework-version) }}
@@ -1,20 +0,0 @@
{
"files": [
{
"aql": {
"items.find": {
"$and": [
{
"@build.name": "${buildName}",
"@build.number": "${buildNumber}",
"path": {
"$nmatch": "org/springframework/framework-api/*"
}
}
]
}
},
"target": "nexus/"
}
]
}
+18
View File
@@ -0,0 +1,18 @@
workflow:
generator:
project:
java:
versions:
primary: 25
workflows:
release-train:
build:
env:
COMMERCIAL_REPO_USERNAME: secrets.COMMERCIAL_ARTIFACTORY_USERNAME
COMMERCIAL_REPO_PASSWORD: secrets.COMMERCIAL_ARTIFACTORY_PASSWORD
COMMERCIAL_RELEASE_REPO_URL: vars.COMMERCIAL_RELEASE_REPO_URL
test:
env:
COMMERCIAL_REPO_USERNAME: secrets.COMMERCIAL_ARTIFACTORY_USERNAME
COMMERCIAL_REPO_PASSWORD: secrets.COMMERCIAL_ARTIFACTORY_PASSWORD
COMMERCIAL_RELEASE_REPO_URL: vars.COMMERCIAL_RELEASE_REPO_URL
+4 -16
View File
@@ -7,27 +7,15 @@ on:
push:
branches:
- '*.x'
permissions:
contents: read
jobs:
build:
backport-issue:
permissions:
contents: read
issues: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Set up Java
uses: actions/setup-java@v5
- name: Create Backport Issue
uses: spring-io/backport-bot@v0.0.2
with:
distribution: 'liberica'
java-version: 17
- name: Download BackportBot
run: wget https://github.com/spring-io/backport-bot/releases/download/latest/backport-bot-0.0.1-SNAPSHOT.jar
- name: Backport
env:
GITHUB_EVENT: ${{ toJSON(github.event) }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: java -jar backport-bot-0.0.1-SNAPSHOT.jar --github.accessToken="$GITHUB_TOKEN" --github.event_name "$GITHUB_EVENT_NAME" --github.event "$GITHUB_EVENT"
token: ${{ secrets.GITHUB_TOKEN }}
+32 -13
View File
@@ -2,23 +2,27 @@ name: Build and Deploy Snapshot
on:
push:
branches:
- 7.0.x
- '7.0.x-internal'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
build-and-deploy-snapshot:
name: Build and Deploy Snapshot
if: ${{ github.repository == 'spring-projects/spring-framework' }}
runs-on: ubuntu-latest
if: ${{ github.repository == 'spring-projects/spring-framework' || github.repository == 'spring-projects/spring-framework-commercial' }}
runs-on: ${{ vars.UBUNTU_MEDIUM || 'ubuntu-latest' }}
timeout-minutes: 60
steps:
- name: Check Out Code
uses: actions/checkout@v6
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Build and Publish
id: build-and-publish
uses: ./.github/actions/build
with:
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
commercial-release-repository-url: ${{ vars.COMMERCIAL_RELEASE_REPO_URL }}
commercial-repository-password: ${{ secrets.COMMERCIAL_ARTIFACTORY_PASSWORD }}
commercial-repository-username: ${{ secrets.COMMERCIAL_ARTIFACTORY_USERNAME }}
commercial-snapshot-repository-url: ${{ vars.COMMERCIAL_SNAPSHOT_REPO_URL }}
#develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
publish: true
- name: Deploy
uses: spring-io/artifactory-deploy-action@926d7f7cc810569395346bf3a4d91b380b3e355b # v0.0.4
@@ -27,32 +31,47 @@ jobs:
/**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false
/**/framework-api-*-docs.zip::zip.type=docs
/**/framework-api-*-schema.zip::zip.type=schema
build-name: 'spring-framework-7.0.x'
build-name: ${{ vars.COMMERCIAL && format('spring-framework-commercial-{0}', '7.0.x') || format('spring-framework-{0}', '7.0.x') }}
folder: 'deployment-repository'
password: ${{ secrets.ARTIFACTORY_PASSWORD }}
repository: 'libs-snapshot-local'
project: ${{ vars.COMMERCIAL && 'spring' }}
repository: ${{ vars.COMMERCIAL && 'spring-enterprise-maven-dev-local' || 'libs-snapshot-local' }}
uri: ${{ vars.COMMERCIAL_DEPLOY_REPO_URL || 'https://repo.spring.io' }}
username: ${{ vars.COMMERCIAL && secrets.COMMERCIAL_ARTIFACTORY_USERNAME || secrets.ARTIFACTORY_USERNAME }}
password: ${{ vars.COMMERCIAL && secrets.COMMERCIAL_ARTIFACTORY_PASSWORD || secrets.ARTIFACTORY_PASSWORD }}
signing-key: ${{ secrets.GPG_PRIVATE_KEY }}
signing-passphrase: ${{ secrets.GPG_PASSPHRASE }}
uri: 'https://repo.spring.io'
username: ${{ secrets.ARTIFACTORY_USERNAME }}
- name: Send Notification
if: always()
uses: ./.github/actions/send-notification
with:
build-scan-url: ${{ steps.build-and-publish.outputs.build-scan-url }}
run-name: ${{ format('{0} | Linux | Java 17', github.ref_name) }}
run-name: ${{ format('{0} | Linux | Java 25', github.ref_name) }}
status: ${{ job.status }}
webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }}
outputs:
version: ${{ steps.build-and-publish.outputs.version }}
trigger-docs-build:
name: Trigger Docs Build
needs: build-and-deploy-snapshot
if: ${{ !vars.COMMERCIAL }} # remove when commercial support
permissions:
actions: write
runs-on: ${{ vars.UBUNTU_SMALL || 'ubuntu-latest' }}
steps:
- name: Run Deploy Docs Workflow
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh workflow run deploy-docs.yml --repo ${{ github.repository }} -r docs-build -f build-refname=${{ github.ref_name }}
verify:
name: Verify
needs: build-and-deploy-snapshot
uses: ./.github/workflows/verify.yml
secrets:
commercial-repository-password: ${{ secrets.COMMERCIAL_ARTIFACTORY_PASSWORD }}
commercial-repository-username: ${{ secrets.COMMERCIAL_ARTIFACTORY_USERNAME }}
google-chat-webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }}
repository-password: ${{ secrets.ARTIFACTORY_PASSWORD }}
repository-username: ${{ secrets.ARTIFACTORY_USERNAME }}
opensource-repository-password: ${{ secrets.ARTIFACTORY_PASSWORD }}
opensource-repository-username: ${{ secrets.ARTIFACTORY_USERNAME }}
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
with:
version: ${{ needs.build-and-deploy-snapshot.outputs.version }}
+3 -4
View File
@@ -7,10 +7,9 @@ jobs:
name: Build Pull Request
if: ${{ github.repository == 'spring-projects/spring-framework' }}
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Check Out Code
uses: actions/checkout@v6
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Build
id: build
uses: ./.github/actions/build
@@ -19,7 +18,7 @@ jobs:
uses: ./.github/actions/print-jvm-thread-dumps
- name: Upload Build Reports
if: failure()
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: build-reports
path: '**/build/reports/'
path: '**/build/reports/'
+13 -7
View File
@@ -2,31 +2,33 @@ name: CI
on:
schedule:
- cron: '30 9 * * *'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
permissions:
contents: read
jobs:
ci:
name: '${{ matrix.os.name}} | Java ${{ matrix.java.version}}'
if: ${{ github.repository == 'spring-projects/spring-framework' }}
if: ${{ github.repository == 'spring-projects/spring-framework' || github.repository == 'spring-projects/spring-framework-commercial' }}
runs-on: ${{ matrix.os.id }}
timeout-minutes: 60
strategy:
matrix:
os:
- id: ubuntu-latest
- id: ${{ vars.UBUNTU_MEDIUM || 'ubuntu-latest' }}
name: Linux
java:
- version: 17
toolchain: false
toolchain: true
- version: 21
toolchain: true
- version: 25
toolchain: false
- version: 26
toolchain: true
exclude:
- os:
name: Linux
java:
version: 17
version: 25
steps:
- name: Prepare Windows runner
if: ${{ runner.os == 'Windows' }}
@@ -35,11 +37,15 @@ jobs:
git config --global core.longPaths true
Stop-Service -name Docker
- name: Check Out Code
uses: actions/checkout@v6
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Build
id: build
uses: ./.github/actions/build
with:
commercial-release-repository-url: ${{ vars.COMMERCIAL_RELEASE_REPO_URL }}
commercial-repository-password: ${{ secrets.COMMERCIAL_ARTIFACTORY_PASSWORD }}
commercial-repository-username: ${{ secrets.COMMERCIAL_ARTIFACTORY_USERNAME }}
commercial-snapshot-repository-url: ${{ vars.COMMERCIAL_SNAPSHOT_REPO_URL }}
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
java-early-access: ${{ matrix.java.early-access || 'false' }}
java-distribution: ${{ matrix.java.distribution }}
-35
View File
@@ -1,35 +0,0 @@
name: Deploy Docs
on:
push:
branches:
- 'main'
- '*.x'
- '!gh-pages'
tags:
- 'v*'
repository_dispatch:
types: request-build-reference # legacy
workflow_dispatch:
permissions:
actions: write
jobs:
build:
name: Dispatch docs deployment
if: github.repository_owner == 'spring-projects'
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v6
with:
fetch-depth: 1
ref: docs-build
- name: Dispatch (partial build)
if: github.ref_type == 'branch'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD) -f build-refname=${{ github.ref_name }}
- name: Dispatch (full build)
if: github.ref_type == 'tag'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD)
+39
View File
@@ -0,0 +1,39 @@
name: Release Milestone
on:
push:
tags:
- v7.0.0-M[1-9]
- v7.0.0-RC[1-9]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
trigger-docs-build:
name: Trigger Docs Build
permissions:
actions: write
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Determine Version
id: version
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
- name: Run Deploy Docs Workflow
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh workflow run deploy-docs.yml --repo ${{ github.repository }} -r docs-build -f build-refname=${{ github.ref_name }}
create-github-release:
name: Create GitHub Release
needs:
- trigger-docs-build
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@v6
- name: Create GitHub Release
uses: ./.github/actions/create-github-release
with:
commercial: ${{ vars.COMMERCIAL }}
milestone: ${{ needs.trigger-docs-build.outputs.version }}
pre-release: true
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
+38
View File
@@ -0,0 +1,38 @@
name: Release
on:
push:
tags:
- v7.0.[0-9]+
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
trigger-docs-build:
name: Trigger Docs Build
permissions:
actions: write
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Determine Version
id: version
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
- name: Run Deploy Docs Workflow
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh workflow run deploy-docs.yml --repo ${{ github.repository }} -r docs-build -f build-refname=${{ github.ref_name }}
create-github-release:
name: Create GitHub Release
needs:
- trigger-docs-build
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@v6
- name: Create GitHub Release
uses: ./.github/actions/create-github-release
with:
commercial: ${{ vars.COMMERCIAL }}
latest: true
milestone: ${{ needs.trigger-docs-build.outputs.version }}
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
-95
View File
@@ -1,95 +0,0 @@
name: Release Milestone
on:
push:
tags:
- v7.0.0-M[1-9]
- v7.0.0-RC[1-9]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
build-and-stage-release:
name: Build and Stage Release
if: ${{ github.repository == 'spring-projects/spring-framework' }}
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@v6
- name: Build and Publish
id: build-and-publish
uses: ./.github/actions/build
with:
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
publish: true
- name: Stage Release
uses: spring-io/artifactory-deploy-action@926d7f7cc810569395346bf3a4d91b380b3e355b # v0.0.4
with:
artifact-properties: |
/**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false
/**/framework-api-*-docs.zip::zip.type=docs
/**/framework-api-*-schema.zip::zip.type=schema
build-name: ${{ format('spring-framework-{0}', steps.build-and-publish.outputs.version)}}
folder: 'deployment-repository'
password: ${{ secrets.ARTIFACTORY_PASSWORD }}
repository: 'libs-staging-local'
signing-key: ${{ secrets.GPG_PRIVATE_KEY }}
signing-passphrase: ${{ secrets.GPG_PASSPHRASE }}
uri: 'https://repo.spring.io'
username: ${{ secrets.ARTIFACTORY_USERNAME }}
outputs:
version: ${{ steps.build-and-publish.outputs.version }}
verify:
name: Verify
needs: build-and-stage-release
uses: ./.github/workflows/verify.yml
secrets:
google-chat-webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }}
repository-password: ${{ secrets.ARTIFACTORY_PASSWORD }}
repository-username: ${{ secrets.ARTIFACTORY_USERNAME }}
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
with:
staging: true
version: ${{ needs.build-and-stage-release.outputs.version }}
sync-to-maven-central:
name: Sync to Maven Central
needs:
- build-and-stage-release
- verify
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@v6
- name: Sync to Maven Central
uses: ./.github/actions/sync-to-maven-central
with:
central-token-password: ${{ secrets.CENTRAL_TOKEN_PASSWORD }}
central-token-username: ${{ secrets.CENTRAL_TOKEN_USERNAME }}
jfrog-cli-config-token: ${{ secrets.JF_ARTIFACTORY_SPRING }}
spring-framework-version: ${{ needs.build-and-stage-release.outputs.version }}
promote-release:
name: Promote Release
needs:
- build-and-stage-release
- sync-to-maven-central
runs-on: ubuntu-latest
steps:
- name: Set up JFrog CLI
uses: jfrog/setup-jfrog-cli@5b06f730cc5a6f55d78b30753f8583454b08c0aa # v4.8.1
env:
JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }}
- name: Promote build
run: jfrog rt build-promote ${{ format('spring-framework-{0}', needs.build-and-stage-release.outputs.version)}} ${{ github.run_number }} libs-milestone-local
create-github-release:
name: Create GitHub Release
needs:
- build-and-stage-release
- promote-release
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@v6
- name: Create GitHub Release
uses: ./.github/actions/create-github-release
with:
milestone: ${{ needs.build-and-stage-release.outputs.version }}
pre-release: true
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
+93
View File
@@ -0,0 +1,93 @@
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
name: "Release Train Build"
run-name: "${{ inputs.callback-ref }} Build"
"on":
workflow_dispatch:
inputs:
callback:
description: "Repository to which a callback should be made upon completion"
required: true
type: "string"
callback-ref:
description: "Ref in the callback repository to which a callback should be made upon completion"
required: true
type: "string"
release-train-maven-repository-url:
description: "URL of a Maven repository to be used to resolve artifacts of projects earlier in the train"
required: true
type: "string"
permissions:
contents: "read"
concurrency:
group: "${{ github.workflow }}-${{ github.ref }}"
jobs:
build-release:
name: "Build Release"
runs-on: "ubuntu22-2-8"
steps:
- name: "Prevent Re-runs"
id: "prevent-re-runs"
run: |-
if [ "$GITHUB_RUN_ATTEMPT" -gt 1 ]; then
echo "Re-runs are prohibited. Use the 'Release Train  Retry' workflow to retry build failures"
exit 1
fi
- name: "Set up Java"
id: "set-up-java"
uses: "actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95" # v5.6.0
with:
distribution: "liberica"
java-version: "25"
- name: "Check Out Code"
id: "check-out-code"
uses: "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0" # v7.0.0
- name: "Build Release"
id: "build-release"
uses: "./.github/actions/release-train-build"
env:
COMMERCIAL_RELEASE_REPO_URL: "${{ vars.COMMERCIAL_RELEASE_REPO_URL }}"
COMMERCIAL_REPO_PASSWORD: "${{ secrets.COMMERCIAL_ARTIFACTORY_PASSWORD }}"
COMMERCIAL_REPO_USERNAME: "${{ secrets.COMMERCIAL_ARTIFACTORY_USERNAME }}"
RELEASE_TRAIN_MAVEN_REPOSITORY_PASSWORD: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_MAVEN_REPOSITORY_PASSWORD }}"
RELEASE_TRAIN_MAVEN_REPOSITORY_URL: "${{ inputs.release-train-maven-repository-url }}"
RELEASE_TRAIN_MAVEN_REPOSITORY_USERNAME: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_MAVEN_REPOSITORY_USERNAME }}"
- name: "Upload Deployment Repository"
id: "upload-deployment-repository"
uses: "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" # v7.0.1
with:
name: "deployment-repository"
path: "deployment-repository/**"
- name: "Upload Deployment Spec"
id: "upload-deployment-spec"
uses: "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" # v7.0.1
with:
archive: "false"
if-no-files-found: "ignore"
name: "deployment-spec"
path: ".github/actions/release-train-build/deployment-spec.yml"
- name: "Save Build System Caches"
id: "save-build-system-caches"
uses: "actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9" # v6.1.0
with:
key: "release-train-${{ inputs.callback-ref }}-${{ github.ref_name }}"
path: |-
~/.gradle/caches
~/.gradle/wrapper
- name: "Send Callback"
id: "send-callback"
if: "${{ !cancelled() }}"
env:
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
run: |-
gh workflow run callback \
--repo ${{ inputs.callback }} \
--ref ${{ inputs.callback-ref }} \
--field commit-hash=${{ steps.check-out-code.outputs.commit }} \
--field deployment-repository-artifact-identifier=${{ steps.upload-deployment-repository.outputs.artifact-id }} \
--field deployment-spec-artifact-identifier=${{ steps.upload-deployment-spec.outputs.artifact-id }} \
--field release-branch=${{ github.ref_name }} \
--field release-repository=${{ github.repository }} \
--field result=${{ job.status == 'success' && 'built' || 'build-failed' }} \
--field workflow-run-url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+55
View File
@@ -0,0 +1,55 @@
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
name: "Release Train Join"
run-name: "${{ inputs.release-train }} Join"
"on":
workflow_dispatch:
inputs:
deployment-destination:
description: "Destination to which the release should be deployed"
options:
- "Maven Central"
- "Spring Enterprise"
required: true
type: "choice"
release-train:
description: "Release train"
required: true
type: "string"
release-train-repository:
default: "spring-io/release-train"
description: "Release train repository"
required: true
type: "string"
permissions:
contents: "none"
jobs:
join-release-train:
name: "Join Release Train"
runs-on: "ubuntu-latest"
steps:
- name: "Join Release Train"
id: "join-release-train"
env:
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
run: |-
run_url=$(
gh workflow run join \
--repo ${{ inputs.release-train-repository }} \
--ref ${{ inputs.release-train }} \
--field commit-hash=${{ github.sha }} \
--field deployment-destination=${{ inputs.deployment-destination == 'Maven Central' && 'maven-central' || 'spring-enterprise' }} \
--field release-branch=${{ github.ref_name }} \
--field release-repository=${{ github.repository }}
)
echo "Dispatched workflow run. Waiting for $run_url to complete."
run_id=${run_url##*/}
watch_exit_code=0
gh run watch $run_id --repo ${{ inputs.release-train-repository }} --exit-status --interval=3 > /dev/null 2>&1 || watch_exit_code=$?
if [[ $watch_exit_code -eq 0 ]]; then
echo "Workflow run succeeded."
else
echo "Workflow run failed."
fi
exit $watch_exit_code
+46
View File
@@ -0,0 +1,46 @@
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
name: "Release Train Leave"
run-name: "${{ inputs.release-train }} Leave"
"on":
workflow_dispatch:
inputs:
release-train:
description: "Release train"
required: true
type: "string"
release-train-repository:
default: "spring-io/release-train"
description: "Release train repository"
required: true
type: "string"
permissions:
contents: "none"
jobs:
leave:
name: "Leave"
runs-on: "ubuntu-latest"
steps:
- name: "Leave"
id: "leave"
env:
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
run: |-
run_url=$(
gh workflow run leave \
--repo ${{ inputs.release-train-repository }} \
--ref ${{ inputs.release-train }} \
--field release-branch=${{ github.ref_name }} \
--field release-repository=${{ github.repository }}
)
echo "Dispatched workflow run. Waiting for $run_url to complete."
run_id=${run_url##*/}
watch_exit_code=0
gh run watch $run_id --repo ${{ inputs.release-train-repository }} --exit-status --interval=3 > /dev/null 2>&1 || watch_exit_code=$?
if [[ $watch_exit_code -eq 0 ]]; then
echo "Workflow run succeeded."
else
echo "Workflow run failed."
fi
exit $watch_exit_code
+47
View File
@@ -0,0 +1,47 @@
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
name: "Release Train Ready"
run-name: "${{ inputs.release-train }} Ready"
"on":
workflow_dispatch:
inputs:
release-train:
description: "Release train"
required: true
type: "string"
release-train-repository:
default: "spring-io/release-train"
description: "Release train repository"
required: true
type: "string"
permissions:
contents: "none"
jobs:
ready:
name: "Ready"
runs-on: "ubuntu-latest"
steps:
- name: "Ready"
id: "ready"
env:
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
run: |-
run_url=$(
gh workflow run ready \
--repo ${{ inputs.release-train-repository }} \
--ref ${{ inputs.release-train }} \
--field commit-hash=${{ github.sha }} \
--field release-branch=${{ github.ref_name }} \
--field release-repository=${{ github.repository }}
)
echo "Dispatched workflow run. Waiting for $run_url to complete."
run_id=${run_url##*/}
watch_exit_code=0
gh run watch $run_id --repo ${{ inputs.release-train-repository }} --exit-status --interval=3 > /dev/null 2>&1 || watch_exit_code=$?
if [[ $watch_exit_code -eq 0 ]]; then
echo "Workflow run succeeded."
else
echo "Workflow run failed."
fi
exit $watch_exit_code
+34
View File
@@ -0,0 +1,34 @@
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
name: "Release Train Retry"
run-name: "${{ inputs.release-train }} Retry"
"on":
workflow_dispatch:
inputs:
release-train:
description: "Release train"
required: true
type: "string"
release-train-repository:
default: "spring-io/release-train"
description: "Release train repository"
required: true
type: "string"
permissions:
contents: "none"
jobs:
trigger-retry:
name: "Trigger Retry"
runs-on: "ubuntu-latest"
steps:
- name: "Trigger Retry"
id: "trigger-retry"
env:
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
run: |-
gh workflow run retry \
--repo ${{ inputs.release-train-repository }} \
--ref ${{ inputs.release-train }} \
--field release-branch=${{ github.ref_name }} \
--field release-repository=${{ github.repository }}
+84
View File
@@ -0,0 +1,84 @@
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
name: "Release Train Test"
run-name: "${{ inputs.callback-ref }} Test"
"on":
workflow_dispatch:
inputs:
callback:
description: "Repository to which a callback should be made upon completion"
required: true
type: "string"
callback-ref:
description: "Ref in the callback repository to which a callback should be made upon completion"
required: true
type: "string"
release-train-maven-repository-url:
description: "URL of a Maven repository to be used to resolve artifacts of projects earlier in the train"
required: true
type: "string"
permissions:
contents: "read"
concurrency:
group: "${{ github.workflow }}-${{ github.ref }}"
jobs:
test-release:
name: "Test Release"
runs-on: "ubuntu22-2-8"
steps:
- name: "Prevent Re-runs"
id: "prevent-re-runs"
run: |-
if [ "$GITHUB_RUN_ATTEMPT" -gt 1 ]; then
echo "Re-runs are prohibited. Use the 'Release Train  Retry' workflow to retry test failures"
exit 1
fi
- name: "Set up Java"
id: "set-up-java"
uses: "actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95" # v5.6.0
with:
distribution: "liberica"
java-version: "25"
- name: "Check Out Code"
id: "check-out-code"
uses: "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0" # v7.0.0
- name: "Restore Build System Caches"
id: "restore-build-system-caches"
uses: "actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9" # v6.1.0
with:
key: "release-train-${{ inputs.callback-ref }}-${{ github.ref_name }}"
path: |-
~/.gradle/caches
~/.gradle/wrapper
- name: "Test Release"
id: "test-release"
uses: "./.github/actions/release-train-test"
env:
COMMERCIAL_RELEASE_REPO_URL: "${{ vars.COMMERCIAL_RELEASE_REPO_URL }}"
COMMERCIAL_REPO_PASSWORD: "${{ secrets.COMMERCIAL_ARTIFACTORY_PASSWORD }}"
COMMERCIAL_REPO_USERNAME: "${{ secrets.COMMERCIAL_ARTIFACTORY_USERNAME }}"
RELEASE_TRAIN_MAVEN_REPOSITORY_PASSWORD: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_MAVEN_REPOSITORY_PASSWORD }}"
RELEASE_TRAIN_MAVEN_REPOSITORY_URL: "${{ inputs.release-train-maven-repository-url }}"
RELEASE_TRAIN_MAVEN_REPOSITORY_USERNAME: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_MAVEN_REPOSITORY_USERNAME }}"
- name: "Send Callback"
id: "send-callback"
if: "${{ !cancelled() }}"
env:
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
run: |-
gh workflow run callback \
--repo ${{ inputs.callback }} \
--ref ${{ inputs.callback-ref }} \
--field commit-hash=${{ steps.check-out-code.outputs.commit }} \
--field release-branch=${{ github.ref_name }} \
--field release-repository=${{ github.repository }} \
--field result=${{ job.status == 'success' && 'tested' || 'test-failed' }} \
--field workflow-run-url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: "Upload Build System Reports"
id: "upload-build-system-reports"
if: "${{ failure() }}"
uses: "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" # v7.0.1
with:
name: "build-system-reports"
path: "**/build/reports"
-93
View File
@@ -1,93 +0,0 @@
name: Release
on:
push:
tags:
- v7.0.[0-9]+
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
build-and-stage-release:
name: Build and Stage Release
if: ${{ github.repository == 'spring-projects/spring-framework' }}
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@v6
- name: Build and Publish
id: build-and-publish
uses: ./.github/actions/build
with:
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
publish: true
- name: Stage Release
uses: spring-io/artifactory-deploy-action@926d7f7cc810569395346bf3a4d91b380b3e355b # v0.0.4
with:
artifact-properties: |
/**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false
/**/framework-api-*-docs.zip::zip.type=docs
/**/framework-api-*-schema.zip::zip.type=schema
build-name: ${{ format('spring-framework-{0}', steps.build-and-publish.outputs.version)}}
folder: 'deployment-repository'
password: ${{ secrets.ARTIFACTORY_PASSWORD }}
repository: 'libs-staging-local'
signing-key: ${{ secrets.GPG_PRIVATE_KEY }}
signing-passphrase: ${{ secrets.GPG_PASSPHRASE }}
uri: 'https://repo.spring.io'
username: ${{ secrets.ARTIFACTORY_USERNAME }}
outputs:
version: ${{ steps.build-and-publish.outputs.version }}
verify:
name: Verify
needs: build-and-stage-release
uses: ./.github/workflows/verify.yml
secrets:
google-chat-webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }}
repository-password: ${{ secrets.ARTIFACTORY_PASSWORD }}
repository-username: ${{ secrets.ARTIFACTORY_USERNAME }}
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
with:
staging: true
version: ${{ needs.build-and-stage-release.outputs.version }}
sync-to-maven-central:
name: Sync to Maven Central
needs:
- build-and-stage-release
- verify
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@v6
- name: Sync to Maven Central
uses: ./.github/actions/sync-to-maven-central
with:
central-token-password: ${{ secrets.CENTRAL_TOKEN_PASSWORD }}
central-token-username: ${{ secrets.CENTRAL_TOKEN_USERNAME }}
jfrog-cli-config-token: ${{ secrets.JF_ARTIFACTORY_SPRING }}
spring-framework-version: ${{ needs.build-and-stage-release.outputs.version }}
promote-release:
name: Promote Release
needs:
- build-and-stage-release
- sync-to-maven-central
runs-on: ubuntu-latest
steps:
- name: Set up JFrog CLI
uses: jfrog/setup-jfrog-cli@5b06f730cc5a6f55d78b30753f8583454b08c0aa # v4.8.1
env:
JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }}
- name: Promote build
run: jfrog rt build-promote ${{ format('spring-framework-{0}', needs.build-and-stage-release.outputs.version)}} ${{ github.run_number }} libs-release-local
create-github-release:
name: Create GitHub Release
needs:
- build-and-stage-release
- promote-release
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@v6
- name: Create GitHub Release
uses: ./.github/actions/create-github-release
with:
milestone: ${{ needs.build-and-stage-release.outputs.version }}
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
+24 -13
View File
@@ -12,31 +12,39 @@ on:
required: true
type: string
secrets:
commercial-repository-password:
description: 'Password for authentication with the commercial repository'
required: false
commercial-repository-username:
description: 'Username for authentication with the commercial repository'
required: false
google-chat-webhook-url:
description: 'Google Chat Webhook URL'
required: true
repository-password:
description: 'Password for authentication with the repository'
opensource-repository-password:
description: 'Password for authentication with the open-source repository'
required: false
repository-username:
description: 'Username for authentication with the repository'
opensource-repository-username:
description: 'Username for authentication with the open-source repository'
required: false
token:
description: 'Token to use for authentication with GitHub'
required: true
permissions:
contents: read
jobs:
verify:
name: Verify
runs-on: ubuntu-latest
runs-on: ${{ vars.UBUNTU_SMALL || 'ubuntu-latest' }}
steps:
- name: Check Out Release Verification Tests
uses: actions/checkout@v6
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: 'v0.0.2'
ref: 'v0.0.3'
repository: spring-projects/spring-framework-release-verification
token: ${{ secrets.token }}
- name: Check Out Send Notification Action
uses: actions/checkout@v6
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: send-notification
sparse-checkout: .github/actions/send-notification
@@ -46,8 +54,9 @@ jobs:
distribution: 'liberica'
java-version: 17
- name: Set Up Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
cache-provider: basic
cache-read-only: false
- name: Configure Gradle Properties
shell: bash
@@ -56,15 +65,17 @@ jobs:
echo 'org.gradle.daemon=false' >> $HOME/.gradle/gradle.properties
- name: Run Release Verification Tests
env:
RVT_OSS_REPOSITORY_PASSWORD: ${{ secrets.repository-password }}
RVT_OSS_REPOSITORY_USERNAME: ${{ secrets.repository-username }}
RVT_RELEASE_TYPE: oss
RVT_COMMERCIAL_REPOSITORY_PASSWORD: ${{ secrets.commercial-repository-password }}
RVT_COMMERCIAL_REPOSITORY_USERNAME: ${{ secrets.commercial-repository-username }}
RVT_OSS_REPOSITORY_PASSWORD: ${{ secrets.opensource-repository-password }}
RVT_OSS_REPOSITORY_USERNAME: ${{ secrets.opensource-repository-username }}
RVT_RELEASE_TYPE: ${{ vars.COMMERCIAL && 'commercial' || 'oss' }}
RVT_STAGING: ${{ inputs.staging }}
RVT_VERSION: ${{ inputs.version }}
run: ./gradlew spring-framework-release-verification-tests:test
- name: Upload Build Reports on Failure
if: failure()
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: build-reports
path: '**/build/reports/'
+2 -10
View File
@@ -6,7 +6,7 @@ plugins {
id 'com.github.bjornvester.xjc' version '1.8.2' apply false
id 'com.gradleup.shadow' version "9.2.2" apply false
id 'me.champeau.jmh' version '0.7.2' apply false
id 'io.spring.nullability' version '0.0.11' apply false
id 'io.spring.nullability' version '0.0.14' apply false
}
ext {
@@ -18,16 +18,8 @@ description = "Spring Framework"
configure(allprojects) { project ->
apply plugin: "org.springframework.build.localdev"
apply plugin: "org.springframework.build.repositories"
group = "org.springframework"
repositories {
mavenCentral()
if (version.contains('-')) {
maven { url = "https://repo.spring.io/milestone" }
}
if (version.endsWith('-SNAPSHOT')) {
maven { url = "https://repo.spring.io/snapshot" }
}
}
configurations.all {
resolutionStrategy {
cacheChangingModulesFor 0, "seconds"
+4
View File
@@ -50,6 +50,10 @@ gradlePlugin {
id = "org.springframework.build.multiReleaseJar"
implementationClass = "org.springframework.build.multirelease.MultiReleaseJarPlugin"
}
repositoriesPlugin {
id = "org.springframework.build.repositories"
implementationClass = "org.springframework.build.RepositoriesPlugin"
}
optionalDependenciesPlugin {
id = "org.springframework.build.optional-dependencies"
implementationClass = "org.springframework.build.optional.OptionalDependenciesPlugin"
@@ -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("13.4.2");
checkstyle.setToolVersion("13.10.0");
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
@@ -0,0 +1,98 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.build;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
/**
* Plugin that configures the OSS, commercial and release train repositories in the build.
*
* @author Brian Clozel
*/
public class RepositoriesPlugin implements Plugin <Project> {
@Override
public void apply(Project project) {
configureOssRepositories(project);
configureCommercialRepositories(project);
configureReleaseTrainRepository(project);
}
private void configureOssRepositories(Project project) {
project.getRepositories().mavenCentral();
if (project.getVersion().toString().contains("-")) {
project.getRepositories().maven(repository -> {
repository.setName("spring-oss-milestone");
repository.setUrl("https://repo.spring.io/milestone/");
});
}
if (project.getVersion().toString().endsWith("-SNAPSHOT")) {
project.getRepositories().maven(repository -> {
repository.setName("spring-oss-snapshot");
repository.setUrl("https://repo.spring.io/snapshot/");
});
}
}
private void configureCommercialRepositories(Project project) {
String releaseRepositoryUrl = getEnv("COMMERCIAL_RELEASE_REPO_URL");
if (releaseRepositoryUrl != null) {
project.getRepositories().maven((repository) -> {
repository.setName("spring-commercial-release");
repository.setUrl(releaseRepositoryUrl);
repository.credentials((creds) -> {
creds.setUsername(System.getenv("COMMERCIAL_REPO_USERNAME"));
creds.setPassword(System.getenv("COMMERCIAL_REPO_PASSWORD"));
});
});
}
String snapshotRepositoryUrl = getEnv("COMMERCIAL_SNAPSHOT_REPO_URL");
if (snapshotRepositoryUrl != null && project.getVersion().toString().endsWith("-SNAPSHOT")) {
project.getRepositories().maven((repository) -> {
repository.setName("spring-commercial-snapshot");
repository.setUrl(snapshotRepositoryUrl);
repository.credentials((creds) -> {
creds.setUsername(System.getenv("COMMERCIAL_REPO_USERNAME"));
creds.setPassword(System.getenv("COMMERCIAL_REPO_PASSWORD"));
});
});
}
}
private void configureReleaseTrainRepository(Project project) {
String releaseTrainRepositoryUrl = getEnv("RELEASE_TRAIN_MAVEN_REPOSITORY_URL");
if (releaseTrainRepositoryUrl != null) {
project.getRepositories().maven(repository -> {
repository.setName("spring-release-train");
repository.setUrl(releaseTrainRepositoryUrl);
repository.credentials((creds) -> {
creds.setUsername(System.getenv("RELEASE_TRAIN_MAVEN_REPOSITORY_USERNAME"));
creds.setPassword(System.getenv("RELEASE_TRAIN_MAVEN_REPOSITORY_PASSWORD"));
});
});
}
}
/**
* Returns the environment variable's value, or {@code null} if it is unset or blank.
*/
private static String getEnv(String name) {
String value = System.getenv(name);
return (value != null && !value.isBlank()) ? value : null;
}
}
@@ -71,7 +71,7 @@ class TestConventions {
"junit.platform.discovery.issue.severity.critical", "INFO"
));
if (project.hasProperty("testGroups")) {
test.systemProperty("testGroups", project.getProperties().get("testGroups"));
test.systemProperty("testGroups", project.findProperty("testGroups"));
}
test.jvmArgs(
"--add-opens=java.base/java.lang=ALL-UNNAMED",
@@ -82,7 +82,7 @@ class TestConventions {
private void configureByteBuddyAgent(Project project) {
if (project.hasProperty("byteBuddyVersion")) {
String byteBuddyVersion = (String) project.getProperties().get("byteBuddyVersion");
String byteBuddyVersion = (String) project.findProperty("byteBuddyVersion");
Configuration byteBuddyAgentConfig = project.getConfigurations().create("byteBuddyAgentConfig");
byteBuddyAgentConfig.setTransitive(false);
Dependency byteBuddyAgent = project.getDependencies().create("net.bytebuddy:byte-buddy-agent:" + byteBuddyVersion);
+4 -4
View File
@@ -15,8 +15,8 @@ repositories {
}
dependencies {
moduleProjects.each { moduleProject ->
javadoc moduleProject
rootProject.ext.moduleProjects.each { moduleProject ->
javadoc project(moduleProject.path)
}
}
@@ -52,7 +52,7 @@ javadoc {
// ensure the javadoc process can resolve types compiled from .aj sources
springAspectsOutput
)
classpath += files(moduleProjects.collect { it.sourceSets.main.compileClasspath })
classpath += files(rootProject.ext.moduleProjects.collect { it.sourceSets.main.compileClasspath })
}
}
@@ -96,7 +96,7 @@ tasks.register('schemaZip', Zip) {
description = "Builds -${archiveClassifier} archive containing all " +
"XSDs for deployment at https://springframework.org/schema."
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
moduleProjects.each { module ->
rootProject.ext.moduleProjects.each { module ->
def Properties schemas = new Properties();
module.sourceSets.main.resources.find {
+1 -1
View File
@@ -8,7 +8,7 @@ group = "org.springframework"
dependencies {
constraints {
parent.moduleProjects.sort { "$it.name" }.each {
api it
api project(it.path)
}
}
}
@@ -74,6 +74,12 @@ expressions used in XML bean definitions, `@Value`, etc.
| The mode to use when compiling expressions for the
xref:core/expressions/evaluation.adoc#expressions-compiler-configuration[Spring Expression Language].
| `spring.expression.maxBigPowerBits`
| The default maximum number of bits permitted in the result of a `BigDecimal` or
`BigInteger` power operation within a
xref:core/expressions/evaluation.adoc#expressions-parser-configuration[Spring Expression Language]
expression.
| `spring.expression.maxOperations`
| The default maximum number of operations permitted during
xref:core/expressions/evaluation.adoc#expressions-parser-configuration[Spring Expression Language]
@@ -253,6 +253,175 @@ properties. Alternatively, configure custom accessors via
`SimpleEvaluationContext.forPropertyAccessors(...)`, potentially disable assignment, and
optionally activate method resolution and/or a type converter through the builder.
[[expressions-evaluation-context-security]]
=== Security Considerations
SpEL is a powerful expression language that can invoke constructors and methods, read and
write properties and fields, and reference beans all backed by reflection. Because of
this power, evaluating a SpEL expression obtained from an untrusted source is inherently
dangerous and should generally be avoided, since doing so can effectively grant that
source the ability to execute arbitrary code within the application, regardless of which
`EvaluationContext` implementation is used.
Throughout this section, a source of a SpEL expression is considered "trusted" only if it
is a developer of the application or an administrator responsible for configuring or
operating the application. Any other source of a SpEL expression must be treated as
untrusted for example, an expression supplied by an end user of the application or
received from an external system.
[WARNING]
====
`StandardEvaluationContext` exposes the complete SpEL language and must *never* be used
to evaluate an expression obtained from an untrusted source.
====
Although `SimpleEvaluationContext` restricts the SpEL language to a subset of its
features, that restriction is provided on a best-effort basis and does not guarantee that
expression evaluation is safe. Since an expression can potentially invoke any property,
method, or function reachable via the configured root object, property accessors, method
resolvers, variables, and functions, care must be taken if you choose to evaluate
expressions from an untrusted source. It is therefore the responsibility of the code that
configures an `EvaluationContext` for example, by supplying a root object or by
registering property accessors, resolvers, variables, or functions to ensure that none
of the objects reachable via the context expose operations that would be dangerous if
invoked by an expression from an untrusted source.
Furthermore, a property "getter" reachable from an expression is not necessarily a pure,
side-effect-free read operation. A JavaBean-style accessor (such as `getName()` or
`isActive()`) and a plain accessor method used to support data classes such as Java
records and Kotlin data classes (such as `name()`) are indistinguishable from a method
that performs an action and happens to return a value (that is, a method which is
*accessor-shaped*). For example, the `public boolean delete()` method in `java.io.File`
looks like a plain accessor method to SpEL. Specifically, neither
`ReflectivePropertyAccessor` nor `DataBindingPropertyAccessor` can determine whether such
a method is free of side effects. Moreover, restricting a `SimpleEvaluationContext` to
read-only data binding governs only whether *assignment* to a property is permitted: it
does not verify that reading a property is side-effect-free. When exposing a root object
or other reachable object to an untrusted expression, you must ensure that none of its
accessor-shaped methods perform an action that would be unsafe if triggered by that
expression.
[NOTE]
.What makes a method "accessor-shaped"?
====
A method is accessor-shaped if it is `public`, takes no arguments, and returns a value
the same shape that `ReflectivePropertyAccessor` and `DataBindingPropertyAccessor` look
for when resolving a property "getter" by name. That shape says nothing about whether
invoking the method is actually free of side effects. For example, the following methods
are all accessor-shaped, but only some of them are safe to invoke as a property read.
Side-effect-free (safe to expose as properties):
* `getName()` and `isActive()`: conventional JavaBean-style accessors.
* `name()` and `active()`: plain accessor methods used by data classes such as Java
records and Kotlin data classes.
Side-effecting (unsafe to expose as properties, despite the identical shape):
* `java.io.File#delete()`: deletes the underlying file and returns whether the deletion
succeeded.
* `java.util.Queue#poll()`: removes and returns the head element, mutating the queue.
* `java.util.concurrent.atomic.AtomicInteger#incrementAndGet()`: increments and returns
a counter, mutating it.
If an untrusted expression can reference `someFile.delete`, `someQueue.poll`, or
`someCounter.incrementAndGet` as a property, SpEL invokes the corresponding method just
as readily as it would invoke a genuine getter.
====
[[expressions-evaluation-context-object-design]]
=== Object Design
Similar to the design guidance for
xref:web/webmvc/mvc-data-binding.adoc#mvc-data-binding-design[web data binding], you
should carefully design any object that may be reached from a SpEL expression evaluated
against untrusted input. This applies not only to the root object supplied to an
`EvaluationContext` but also to every object that such an expression can navigate to from
that root object for example, an object returned by a property, a method, an index
operation, a variable, or a function.
When exposing an object to expressions from an untrusted source, consider the following
recommendations.
Use a dedicated type::
Prefer a dedicated type, designed specifically to be evaluated against untrusted
expressions, over passing an existing domain or infrastructure type "as is". A
dedicated type lets you control exactly which properties and methods are reachable from
an expression, rather than exposing the full surface area of a class such as a JPA
entity, `java.io.File`, or a JDBC `Connection` most of which were never designed with
SpEL evaluation in mind.
Prefer immutability::
An immutable type for example, a Java record or a Kotlin data class exposing only
`val` properties rules out property writes and eliminates any concern that a "getter"
might mutate state as a side effect, since there is no mutable state to affect.
Immutability does not, on its own, rule out an accessor-shaped method with an external
side effect (such as a network call or a file system operation), but it removes an
entire class of risk.
Limit scope::
Expose only the properties and methods that the expression is expected to use, and
nothing more. Because a `PropertyAccessor` cannot restrict access to specific
properties or methods on a per-expression basis, every accessor-shaped method reachable
on an exposed object is reachable by any expression that can reach that object
regardless of which property or method the application intended the expression to use.
Audit accessor-shaped methods::
Review every accessor-shaped method exposed by a type before making it reachable from
an untrusted expression, keeping the <<expressions-evaluation-context-security,
security considerations>> discussed above in mind. None of the reachable methods should
perform an action that would be unsafe if triggered by that expression.
[WARNING]
====
These recommendations apply transitively. If the root object exposes a property or method
that returns another object, and an untrusted expression can navigate to it (for example,
`rootObject.child.grandchild`), the nested object is just as reachable as the root object
itself and must meet the same design requirements. The same is true for an object reached
via indexing (for example, `rootObject.items[0]` or `rootObject.items['key']`): whatever
is returned by the index operation is just as reachable as any other nested object.
====
[[expressions-evaluation-context-lifecycle]]
=== Lifecycle and Reuse
For performance, the AST nodes that make up a parsed `Expression` may cache the specific
`PropertyAccessor`, `IndexAccessor`, `MethodExecutor`, or `ConstructorExecutor` that
satisfied a previous evaluation, so that later evaluations of the same node can avoid
asking every registered accessor or resolver in turn. Understanding this caching behavior
is essential to using `Expression` and `EvaluationContext` correctly, in addition to the
<<expressions-evaluation-context-security,security considerations>> discussed previously.
A parsed `Expression` is designed to be created once and evaluated repeatedly, and doing
so is both supported and encouraged. In particular:
* A parsed `Expression` may be evaluated against different root objects, and against
different `EvaluationContext` instances of the *same type and with equivalent
configuration* for example, several `StandardEvaluationContext` instances each
registering the same kind of custom `PropertyAccessor`. Changing the accessors or
resolvers registered with a context between evaluations of the same expression is
atypical and generally not advised, but is expected to work correctly: the registered
state of the *current* context is what is consulted, not a snapshot taken during an
earlier evaluation.
* A parsed `Expression` must *not* be evaluated first against a context with one set of
security implications and later against a context with different, typically more
restrictive, security implications for example, first against a
`StandardEvaluationContext` and later against a `SimpleEvaluationContext`. Doing so is
analogous to executing a database query on behalf of an administrator, caching the
resulting administrator-privileged execution plan, and then reusing that cached plan for
a lower-privileged user while expecting the lower-privileged user's restrictions to
apply: cached state from the first, more permissive evaluation may be reused during the
second, and the second context's restrictions cannot be reliably enforced as a result.
If the same expression string must be evaluated under contexts with different security
implications, parse it into *distinct* `Expression` instances, one per context.
[WARNING]
====
Reusing a single parsed `Expression` across `EvaluationContext` instances with different
security implications is not a supported usage pattern and must be avoided, regardless of
which `EvaluationContext` implementations are involved.
====
[[expressions-type-conversion]]
=== Type Conversion
@@ -405,6 +574,19 @@ property or Spring property named `spring.expression.maxOperations` to the maxim
of operations required by your application (see
xref:appendix.adoc#appendix-spring-properties[Supported Spring Properties]).
In addition, the result of a `BigDecimal` or `BigInteger` power operation within a SpEL
expression cannot exceed 1,000,000 bits by default approximately equivalent to a
decimal number with 300,000 digits. Power operations involving large base values or large
exponents can be computationally expensive, and this limit ensures that evaluations
remain bounded; however, the `maximumBigPowerBits` value is configurable. If you create a
`SpelExpressionParser` programmatically (the recommended approach), you can specify a
custom `maximumBigPowerBits` value when creating the `SpelParserConfiguration` that you
provide to the `SpelExpressionParser`. To remove this limit entirely, pass
`Integer.MAX_VALUE` as the `maximumBigPowerBits` value. If you are not able to configure
an explicit value for `maximumBigPowerBits` via `SpelParserConfiguration`, you can set a
JVM system property or Spring property named `spring.expression.maxBigPowerBits` to the
maximum result size in bits (see xref:appendix.adoc#appendix-spring-properties[Supported
Spring Properties]).
[[expressions-spel-compilation]]
== SpEL Compilation
@@ -81,6 +81,13 @@ public void sendNotification() {
}
----
[NOTE]
====
When `delay` is `0` combined with a positive `jitter`, the delay never grows
regardless of any configured `multiplier`, so the full configured `jitter` is
applied directly as a random delay in the range from `0` to `min(jitter, maxDelay)`.
====
Last but not least, `@Retryable` also works for reactive methods with a reactive return
type, decorating the pipeline with Reactor's retry capabilities:
@@ -263,6 +270,13 @@ and an exponential back-off strategy with a bit of jitter.
() -> jmsClient.destination("notifications").send(...));
----
[NOTE]
====
When `delay` is zero combined with a positive `jitter`, the delay never grows
regardless of any configured `multiplier`, so the full configured `jitter` is
applied directly as a random delay in the range from zero to `min(jitter, maxDelay)`.
====
[TIP]
====
Although the factory methods and builder API for `RetryPolicy` cover most common
@@ -68,12 +68,22 @@ The `@MockitoBean` annotation uses the `REPLACE_OR_CREATE`
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-strategy[strategy for bean overrides].
If a corresponding bean does not exist, a new bean will be created. However, you can
switch to the `REPLACE` strategy by setting the `enforceOverride` attribute to `true`
for example, `@MockitoBean(enforceOverride = true)`.
for example, `@MockitoBean(enforceOverride = true)`. Because this strategy replaces the
bean directly, bypassing the container's normal bean post-processing, the resulting mock
is a bare object: it is never wrapped in a Spring AOP proxy, even if the original bean
would have been — for example, due to `@Transactional`, `@Cacheable`, or `@Retryable`. See
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-aop-proxies[Bean
Overrides and Spring AOP Proxies] for details.
The `@MockitoSpyBean` annotation uses the `WRAP`
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-strategy[strategy],
and the original instance is wrapped in a Mockito spy. This strategy requires that
exactly one candidate bean exists.
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-strategy[strategy]:
an early instance of the original bean is captured and used to create a Mockito spy.
This strategy requires that exactly one candidate bean exists. In contrast to
`@MockitoBean`, if the original bean would have been wrapped in a Spring AOP proxy, that
proxy is still created — but it now wraps the spy instead of the original bean. See
<<spring-testing-annotation-beanoverriding-mockitospybean-aop-proxies,`@MockitoSpyBean` and Spring AOP Proxies>>
for a diagram and further details on the consequences this has for stubbing and
verification.
[TIP]
====
@@ -468,3 +478,284 @@ Kotlin::
TIP: The spies can also be injected into `@Configuration` classes or other test-related
components in the `ApplicationContext` in order to configure them with Mockito's stubbing
APIs.
[[spring-testing-annotation-beanoverriding-mockitospybean-aop-proxies]]
== `@MockitoSpyBean` and Spring AOP Proxies
As explained in
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-aop-proxies[Bean
Overrides and Spring AOP Proxies], if the bean being spied on would normally be wrapped in
a Spring AOP proxy — for example, due to `@Transactional`, `@Cacheable`, or `@Retryable`
— that proxy is still created, with the spy as its target. The bean injected into the
test class and into other beans in the `ApplicationContext` is therefore the proxy, not
the spy itself.
Verification via Mockito's `verify()` API is unaffected by this and works transparently,
regardless of whether it is invoked on the proxy or on the underlying spy.
[[spring-testing-annotation-beanoverriding-mockitospybean-aop-proxies-stubbing]]
=== Stubbing Through the Proxy
Stubbing requires more care than verification, since `Mockito.doReturn(...).when(...)`,
`Mockito.doThrow(...).when(...)`, and similar methods behave differently depending on the
nature of the AOP advice involved when invoked on the proxy.
NOTE: Since `when` is a reserved keyword in Kotlin, the Kotlin examples below use the
`given(...)`, `willReturn(...)`, and `willThrow(...)` methods from `BDDMockito` instead
of `Mockito.doReturn(...).when(...)` and `Mockito.doThrow(...).when(...)`.
Advice that does not retain state between invocations — such as
xref:core/resilience.adoc#resilience-annotations-retryable[`@Retryable`] — has no adverse
effect on stubbing. The following stubbing sequence, invoked on the proxy, behaves exactly
as it would on the underlying spy directly, including triggering a retry when the thrown
exception is encountered.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
doReturn("ok")
.doThrow(new RuntimeException("Message delivery failed"))
.doReturn("ok again")
.when(clientService).sendMessage(any()); // <1>
----
<1> `clientService` is the injected proxy. Since `@Retryable` advice is a stateless
pass-through, each call — including the one that throws — reaches the spy directly.
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
willReturn("ok")
.willThrow(RuntimeException("Message delivery failed"))
.willReturn("ok again")
.given(clientService).sendMessage(any()) // <1>
----
<1> `clientService` is the injected proxy. Since `@Retryable` advice is a stateless
pass-through, each call — including the one that throws — reaches the spy directly.
======
Advice that caches or otherwise memoizes the outcome of an invocation — such as
`@Cacheable` — does not behave the same way. While a `doReturn(...)`, `doThrow(...)`, or
similar declaration is being recorded, Mockito does not invoke the spy's real or
previously stubbed behavior; instead, the invocation used to declare the stubbing returns
an empty value (for example, `null`). If that invocation is made on the proxy, the caching
advice caches this empty value, which then permanently shadows the spy for that
combination of arguments — including for the very invocation that was supposed to
configure the stubbing.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
doReturn(1L).when(dateService).getDate(false); // <1>
dateService.getDate(false); // <2>
----
<1> `dateService` is the injected proxy. This invocation is intercepted by Mockito's
stubbing infrastructure before it reaches the spy, so the caching advice ends up
caching an empty value for argument `false`.
<2> Returns the empty value cached by the previous invocation — not `1L` — because the
cache was already populated.
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
willReturn(1L).given(dateService).getDate(false) // <1>
dateService.getDate(false) // <2>
----
<1> `dateService` is the injected proxy. This invocation is intercepted by Mockito's
stubbing infrastructure before it reaches the spy, so the caching advice ends up
caching an empty value for argument `false`.
<2> Returns the empty value cached by the previous invocation — not `1L` — because the
cache was already populated.
======
To avoid this, stub directly on the spy instead of on the proxy, by unwrapping the proxy
with
{spring-framework-api}/test/util/AopTestUtils.html#getUltimateTargetObject(java.lang.Object)[`AopTestUtils.getUltimateTargetObject(...)`].
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
DateService spy = AopTestUtils.getUltimateTargetObject(dateService);
doReturn(1L).when(spy).getDate(false);
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val spy = AopTestUtils.getUltimateTargetObject<DateService>(dateService)
willReturn(1L).given(spy).getDate(false)
----
======
[[spring-testing-annotation-beanoverriding-mockitospybean-aop-proxies-disabling]]
=== Disabling AOP Advice for Tests
Rather than working around the proxy as shown above, you may instead prefer to disable
the underlying AOP advice for the duration of the test, while keeping `@Retryable`,
`@Cacheable`, or similar annotations in place in production code. Common reasons include
avoiding retry delays that slow down the test suite, or avoiding caching altogether so
that every invocation reaches the spy directly — which also sidesteps the stubbing
pitfall described above, without having to unwrap the proxy at all.
The general technique is to externalize whatever controls the advice's effective behavior
— for example, the number of retry attempts or the `CacheManager` backing `@Cacheable`
— and override that configuration for tests only, typically by using a bean override or a
test-specific property. The proxy and its advice are still created, but their behavior is
simply made a no-op or pure pass-through for the test.
For `@Retryable`, bind the `maxRetriesString` attribute to a property placeholder with a
sensible default (so that production configuration is unaffected if the property is not
set), and override that property in the test with
xref:testing/annotations/integration-spring/annotation-testpropertysource.adoc[`@TestPropertySource`]
so that no retries are attempted.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@Retryable(maxRetriesString = "${sendMessage.maxRetries:3}", delay = 10)
public String sendMessage(String request) {
// ...
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@Retryable(maxRetriesString = "\${sendMessage.maxRetries:3}", delay = 10)
fun sendMessage(request: String): String {
// ...
}
----
======
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@SpringJUnitConfig
@TestPropertySource(properties = "sendMessage.maxRetries = 0") // <1>
class ClientServiceTests {
@MockitoSpyBean
ClientService clientService;
// test case body...
}
----
<1> With no retries permitted, the first (and only) attempt is made, and a thrown
exception propagates immediately, so the spy's stubbing chain behaves exactly as
declared, including for `doThrow(...)` answers.
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@SpringJUnitConfig
@TestPropertySource(properties = ["sendMessage.maxRetries = 0"]) // <1>
class ClientServiceTests {
@MockitoSpyBean
lateinit var clientService: ClientService
// test case body...
}
----
<1> With no retries permitted, the first (and only) attempt is made, and a thrown
exception propagates immediately, so the spy's stubbing chain behaves exactly as
declared, including for `doThrow(...)` answers.
======
For `@Cacheable`, Spring provides
{spring-framework-api}/cache/support/NoOpCacheManager.html[`NoOpCacheManager`] — a
`CacheManager` that accepts cache entries but never actually stores them, so every
invocation results in a cache miss and therefore an invocation of the target method.
Overriding the `CacheManager` bean with a `NoOpCacheManager` — for example, with
xref:testing/annotations/integration-spring/annotation-testbean.adoc[`@TestBean`] —
effectively disables caching for the test without touching the `@Cacheable` annotation in
production code.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@SpringJUnitConfig
class DateServiceTests {
@MockitoSpyBean
DateService dateService;
@TestBean // <1>
CacheManager cacheManager;
static CacheManager cacheManager() { // <2>
return new NoOpCacheManager();
}
@Test
void test() {
doReturn(1L).when(dateService).getDate(false);
assertThat(dateService.getDate(false)).isEqualTo(1L);
doReturn(2L).when(dateService).getDate(false);
assertThat(dateService.getDate(false)).isEqualTo(2L); // <3>
}
}
----
<1> Override the `CacheManager` bean for this test.
<2> Replace it with a `NoOpCacheManager`, so `@Cacheable` never actually caches anything.
<3> No longer masked by a stale cache entry, since every call reaches the spy.
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@SpringJUnitConfig
class DateServiceTests {
@MockitoSpyBean
lateinit var dateService: DateService
@TestBean // <1>
lateinit var cacheManager: CacheManager
companion object {
@JvmStatic
fun cacheManager(): CacheManager { // <2>
return NoOpCacheManager()
}
}
@Test
fun test() {
willReturn(1L).given(dateService).getDate(false)
assertThat(dateService.getDate(false)).isEqualTo(1L)
willReturn(2L).given(dateService).getDate(false)
assertThat(dateService.getDate(false)).isEqualTo(2L) // <3>
}
}
----
<1> Override the `CacheManager` bean for this test.
<2> Replace it with a `NoOpCacheManager`, so `@Cacheable` never actually caches anything.
<3> No longer masked by a stale cache entry, since every call reaches the spy.
======
@@ -171,3 +171,15 @@ Similarly, when overriding a bean created by a `FactoryBean`, the `FactoryBean`
replaced with a singleton bean corresponding to the value returned from the `@TestBean`
factory method.
====
[NOTE]
====
`@TestBean` uses the `REPLACE` or `REPLACE_OR_CREATE`
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-strategy[strategy
for bean overrides], which registers the value returned from the factory method directly
as the bean, bypassing the container's normal bean post-processing. Consequently, none of
the Spring AOP advice that would otherwise apply to the original bean (for example,
`@Transactional`, `@Cacheable`, or `@Retryable`) is present on the override instance. See
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-aop-proxies[Bean
Overrides and Spring AOP Proxies] for details.
====
@@ -90,3 +90,67 @@ Alternatively, the user can directly provide the bean name in the custom annotat
`BeanOverrideProcessor` implementations may also internally compute a bean name based on
a convention or some other method.
====
[[testcontext-bean-overriding-aop-proxies]]
== Bean Overrides and Spring AOP Proxies
Beans in a Spring `ApplicationContext` are frequently wrapped in an AOP proxy — for
example, to support `@Transactional`, `@Cacheable`, or `@Retryable` semantics. Whether an
overridden bean retains such a proxy depends on the `BeanOverrideStrategy` used to create
the override.
* Overrides that use the `REPLACE` or `REPLACE_OR_CREATE` strategy (such as `@TestBean`
and `@MockitoBean`) register their override instance directly as a manual singleton,
which bypasses the container's normal bean post-processing. Consequently, the override
instance is a bare object: none of the AOP advice that would otherwise apply to the
original bean (`@Transactional`, `@Cacheable`, `@Retryable`, method security, and so
on) is present.
* Overrides that use the `WRAP` strategy (such as `@MockitoSpyBean`) capture an early
reference to the original bean and use it to create the override instance, before the
rest of the container's post-processors — including the one responsible for creating
AOP proxies — have run. Consequently, if the original bean would have been proxied,
that proxy is still created, but it now wraps the override instance instead of the
original bean. The bean that ends up in the `ApplicationContext`, and that is injected
into collaborating beans and test classes, is therefore the AOP proxy, with the
override instance as its target — not the bare override instance itself.
The following diagrams illustrate the resulting shape of the bean for each strategy, from
the perspective of a caller invoking a method on the injected bean.
With the `REPLACE` or `REPLACE_OR_CREATE` strategy, there is no AOP proxy at all: the
caller invokes the override instance directly.
[source]
----
caller
[ override instance ]
----
With the `WRAP` strategy, any AOP proxy that would normally have wrapped the original
bean is still created, but now wraps the override instance instead:
[source]
----
caller
[ AOP proxy ] (for example, retry, caching, or transaction advice)
│ delegates to its target
[ override instance ] (for example, a Mockito spy created by @MockitoSpyBean)
----
For a `WRAP`-based override such as `@MockitoSpyBean`, the "wrapping" performed by the
AOP proxy is unrelated to the manner in which the resulting Mockito spy itself "wraps"
the original bean instance it was created from. The proxy shown above determines which
object a caller actually invokes, whereas the spy's relationship to the original
instance only determines what happens when an unstubbed method is invoked on the spy: it
falls through to that instance's real behavior.
This distinction has practical consequences when combining bean overrides with Mockito's
stubbing and verification APIs. See
xref:testing/annotations/integration-spring/annotation-mockitobean.adoc#spring-testing-annotation-beanoverriding-mockitospybean-aop-proxies[`@MockitoSpyBean`
and Spring AOP Proxies] for details.
@@ -99,6 +99,11 @@ mock to configure expectations on it and perform verifications. For Spring's cor
utilities, see {spring-framework-api}/aop/support/AopUtils.html[`AopUtils`] and
{spring-framework-api}/aop/framework/AopProxyUtils.html[`AopProxyUtils`].
TIP: For guidance on using `AopTestUtils` together with `@MockitoSpyBean` when the spied
bean is wrapped in a Spring AOP proxy, see
xref:testing/annotations/integration-spring/annotation-mockitobean.adoc#spring-testing-annotation-beanoverriding-mockitospybean-aop-proxies[`@MockitoSpyBean`
and Spring AOP Proxies].
{spring-framework-api}/test/util/ReflectionTestUtils.html[`ReflectionTestUtils`] is a
collection of reflection-based utility methods. You can use these methods in testing
scenarios where you need to change the value of a constant, set a non-`public` field,
@@ -338,21 +338,26 @@ include::partial$web/forwarded-headers.adoc[]
[[webflux-forwarded-headers-transformer]]
=== ForwardedHeaderTransformer
`ForwardedHeaderTransformer` is a component that modifies the host, port, and scheme of
the request, based on forwarded headers, and then removes those headers. If you declare
it as a bean with the name `forwardedHeaderTransformer`, it will be
`ForwardedHeaderTransformer` is a component that modifies the request to match information
from the standard `"Forwarded"` or `"X-Forwarded"` headers, and also removes those headers
to eliminate further impact. If you declare it as a bean with the name
`forwardedHeaderTransformer`, it will be
xref:web/webflux/reactive-spring.adoc#webflux-web-handler-api-special-beans[detected] and used.
[[webflux-forwarded-headers-security]]
=== Security Considerations
There are security considerations for forwarded headers since an application cannot know
if the headers were added by a proxy, as intended, or by a malicious client. This is why
a proxy at the boundary of trust should be configured to remove untrusted forwarded traffic coming
from the outside. You can also configure the `ForwardedHeaderTransformer` with
`removeOnly=true`, in which case it removes but does not use the headers.
Forwarded headers are intended to be set by trusted proxies and never allowed in from the
outside. A proxy at the edge of trust must remove forwarded headers including both the
standard `"Forwarded"` and `"X-Forwarded"` headers, regardless of which one they use,
to protect applications which may check both.
When creating `ForwardedHeaderTransformer` you can specify whether to use the
standard `"Forwarded"` or `"X-Forwarded"` headers. A separate property on the transformer
lets you turn use of `"X-Forwarded-Prefix"` on and off.
`ForwardedHeaderTransformer` can be configured in `removeOnly` mode, in which case it removes
forwarded headers from the request without using them.
[[webflux-filters]]
== Filters
@@ -24,7 +24,7 @@ ignoring further handling via `FORWARD` dispatches. The filter also provides con
over whether the `Filter` gets involved in `ASYNC` and `ERROR` dispatches.
Servlet filters can be configured in `web.xml` or via Servlet annotations.
In a Spring Boot application , you can
In a Spring Boot application, you can
{spring-boot-docs}/how-to/webserver.html#howto.webserver.add-servlet-filter-listener.spring-bean[declare Filter's as beans]
and Boot will have them configured.
@@ -51,20 +51,29 @@ include::partial$web/forwarded-headers.adoc[]
[[filters-forwarded-headers-non-forwardedheaderfilter]]
=== ForwardedHeaderFilter
`ForwardedHeaderFilter` is a Servlet filter that modifies the request in order to
a) change the host, port, and scheme based on `Forwarded` headers, and b) to remove those
headers to eliminate further impact. The filter relies on wrapping the request, and
therefore it must be ordered ahead of other filters, such as `RequestContextFilter`, that
should work with the modified and not the original request.
`ForwardedHeaderFilter` is a Servlet filter that modifies the request to match information
from the standard `"Forwarded"` or `"X-Forwarded"` headers, and also removes those headers
to eliminate further impact. The filter wraps the request and must be ordered ahead
of other filters such as `RequestContextFilter` in order for all downstream
handlers to see the modified request.
[[filters-forwarded-headers-security]]
=== Security Considerations
There are security considerations for forwarded headers since an application cannot know
if the headers were added by a proxy, as intended, or by a malicious client. This is why
a proxy at the boundary of trust should be configured to remove untrusted `Forwarded`
headers that come from the outside. You can also configure the `ForwardedHeaderFilter`
with `removeOnly=true`, in which case it removes but does not use the headers.
Forwarded headers are intended to be set by trusted proxies and never allowed in from the
outside. A proxy at the edge of trust must remove forwarded headers including both the
standard `"Forwarded"` and `"X-Forwarded"` headers, regardless of which one they use,
to protect applications which may check both.
When creating `ForwardedHeaderFilter` you can specify whether to use the
standard `"Forwarded"` or `"X-Forwarded"` headers. A separate property on the filter
lets you turn use of `"X-Forwarded-Prefix"` on and off.
`ForwardedHeaderFilter` can be configured in `removeOnly` mode, in which case it removes
forwarded headers from the request without using them.
[[filters-forwarded-headers-dispatcher]]
=== Dispatcher Types
@@ -112,8 +112,8 @@ Kotlin::
NOTE: As of 5.1, `ServletUriComponentsBuilder` ignores information from the `Forwarded` and
`X-Forwarded-*` headers, which specify the client-originated address. Consider using the
xref:web/webmvc/filters.adoc#filters-forwarded-headers[`ForwardedHeaderFilter`] to extract and use or to discard
such headers.
xref:web/webmvc/filters.adoc#filters-forwarded-headers[`ForwardedHeaderFilter`]
to extract and use or to discard such headers.
[[mvc-links-to-controllers]]
@@ -261,8 +261,8 @@ Kotlin::
NOTE: As of 5.1, `MvcUriComponentsBuilder` ignores information from the `Forwarded` and
`X-Forwarded-*` headers, which specify the client-originated address. Consider using the
xref:web/webmvc/filters.adoc#filters-forwarded-headers[ForwardedHeaderFilter] to extract and use or to discard
such headers.
xref:web/webmvc/filters.adoc#filters-forwarded-headers[ForwardedHeaderFilter] to extract
and use or to discard such headers.
[[mvc-links-to-controllers-from-views]]
@@ -1,15 +1,30 @@
As a request goes through proxies such as load balancers the host, port, and
scheme may change, and that makes it a challenge to create links that point to the correct
host, port, and scheme from a client perspective.
As a request goes through a chain of proxies, request details such as the scheme, host,
port, remote address, and local address change. Proxies can insert headers that keep track of
the hops, and that can help to restore the request from the original client's perspective.
This allows an application to create self-reference links for external clients.
There are two alternatives for headers that proxies can use:
- {rfc-site}/rfc7239[RFC 7239] defines the `"Forwarded"` HTTP header, a single header with
individual attributes for each component in the chain of proxied requests with the
following syntax.
- `"X-Forwarded-"` prefixed headers are the original approach that predates the standard
and uses a separate header for each request component.
The Spring Framework supports both approaches. Most proxies today support the original
`"X-Forwarded"` headers only as a de facto standard.
WARNING: For maximum security, a proxy at the edge of trust must be configured to reset both
the standard `"Forwarded"` and `"X-Forwarded-"` headers regardless of which ones are chosen
for use. Likewise, when configuring forwarded header handling in Spring, you need to indicate
which type of headers to use. More on security considerations later in this section.
{rfc-site}/rfc7239[RFC 7239] defines the `Forwarded` HTTP header
that proxies can use to provide information about the original request.
[[forwarded-headers-non-standard]]
=== Non-standard Headers
=== X-Forwarded Headers
There are other non-standard headers, too, including `X-Forwarded-Host`, `X-Forwarded-Port`,
`X-Forwarded-Proto`, `X-Forwarded-Ssl`, `X-Forwarded-Prefix`, and `X-Forwarded-For`.
This section describes supported `"X-Forwarded"` headers.
[[x-forwarded-host]]
==== X-Forwarded-Host
+17 -17
View File
@@ -8,15 +8,15 @@ javaPlatform {
dependencies {
api(platform("com.fasterxml.jackson:jackson-bom:2.20.2"))
api(platform("io.micrometer:micrometer-bom:1.16.6"))
api(platform("io.netty:netty-bom:4.2.15.Final"))
api(platform("io.projectreactor:reactor-bom:2025.0.6"))
api(platform("io.micrometer:micrometer-bom:1.16.7"))
api(platform("io.netty:netty-bom:4.2.17.Final"))
api(platform("io.projectreactor:reactor-bom:2025.0.7"))
api(platform("io.rsocket:rsocket-bom:1.1.5"))
api(platform("org.apache.groovy:groovy-bom:5.0.6"))
api(platform("org.apache.logging.log4j:log4j-bom:2.26.0"))
api(platform("org.apache.groovy:groovy-bom:5.0.8"))
api(platform("org.apache.logging.log4j:log4j-bom:2.26.1"))
api(platform("org.assertj:assertj-bom:3.27.7"))
api(platform("org.eclipse.jetty:jetty-bom:12.1.9"))
api(platform("org.eclipse.jetty.ee11:jetty-ee11-bom:12.1.9"))
api(platform("org.eclipse.jetty:jetty-bom:12.1.12"))
api(platform("org.eclipse.jetty.ee11:jetty-ee11-bom:12.1.12"))
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.2"))
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.9.0"))
api(platform("org.junit:junit-bom:6.0.3"))
@@ -26,12 +26,12 @@ dependencies {
constraints {
api("com.fasterxml:aalto-xml:1.3.4")
api("com.fasterxml.woodstox:woodstox-core:6.7.0")
api("com.github.ben-manes.caffeine:caffeine:3.2.3")
api("com.github.ben-manes.caffeine:caffeine:3.2.4")
api("com.github.librepdf:openpdf:1.3.43")
api("com.google.code.findbugs:findbugs:3.0.1")
api("com.google.code.findbugs:jsr305:3.0.2")
api("com.google.code.gson:gson:2.13.2")
api("com.google.protobuf:protobuf-java-util:4.34.1")
api("com.google.protobuf:protobuf-java-util:4.35.1")
api("com.h2database:h2:2.4.240")
api("com.jayway.jsonpath:json-path:2.10.0")
api("com.networknt:json-schema-validator:1.5.3")
@@ -96,10 +96,10 @@ dependencies {
api("org.apache.httpcomponents.client5:httpclient5:5.6")
api("org.apache.httpcomponents.core5:httpcore5-reactive:5.4.2")
api("org.apache.poi:poi-ooxml:5.5.1")
api("org.apache.tomcat.embed:tomcat-embed-core:11.0.22")
api("org.apache.tomcat.embed:tomcat-embed-websocket:11.0.22")
api("org.apache.tomcat:tomcat-util:11.0.22")
api("org.apache.tomcat:tomcat-websocket:11.0.22")
api("org.apache.tomcat.embed:tomcat-embed-core:11.0.24")
api("org.apache.tomcat.embed:tomcat-embed-websocket:11.0.24")
api("org.apache.tomcat:tomcat-util:11.0.24")
api("org.apache.tomcat:tomcat-websocket:11.0.24")
api("org.aspectj:aspectjrt:1.9.25")
api("org.aspectj:aspectjtools:1.9.25")
api("org.aspectj:aspectjweaver:1.9.25")
@@ -110,8 +110,8 @@ dependencies {
api("org.dom4j:dom4j:2.2.0")
api("org.easymock:easymock:5.6.0")
api("org.eclipse.angus:angus-mail:2.0.3")
api("org.eclipse.jetty:jetty-reactive-httpclient:4.1.4")
api("org.eclipse.persistence:org.eclipse.persistence.jpa:5.0.0")
api("org.eclipse.jetty:jetty-reactive-httpclient:4.1.5")
api("org.eclipse.persistence:org.eclipse.persistence.jpa:5.0.1")
api("org.eclipse:yasson:3.0.4")
api("org.ehcache:ehcache:3.10.8")
api("org.ehcache:jcache:1.0.1")
@@ -120,8 +120,8 @@ dependencies {
api("org.glassfish:jakarta.el:4.0.2")
api("org.graalvm.sdk:graal-sdk:22.3.1")
api("org.hamcrest:hamcrest:3.0")
api("org.hibernate.orm:hibernate-core:7.2.17.Final")
api("org.hibernate.validator:hibernate-validator:9.1.0.Final")
api("org.hibernate.orm:hibernate-core:7.2.24.Final")
api("org.hibernate.validator:hibernate-validator:9.1.3.Final")
api("org.hsqldb:hsqldb:2.7.4")
api("org.htmlunit:htmlunit:4.21.0")
api("org.javamoney:moneta:1.4.4")
+1 -1
View File
@@ -1,4 +1,4 @@
version=7.0.8-SNAPSHOT
version=7.0.9
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
-1
View File
@@ -7,7 +7,6 @@ apply plugin: 'eclipse'
eclipse.jdt {
sourceCompatibility = 17
targetCompatibility = 17
javaRuntimeName = "JavaSE-17"
}
// Replace classpath entries with project dependencies (GRADLE-1116)
+2
View File
@@ -84,6 +84,8 @@ javadoc {
// cross-module @see and @link references which are only reachable
// when running the global 'framework-api:javadoc' task.
addBooleanOption('Werror', false)
// do not ship 4MB of web fonts for single modules
addBooleanOption("-no-fonts", true)
}
// Attempt to suppress warnings due to cross-module @see and @link references.
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.0-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
Vendored
+2 -2
View File
@@ -20,7 +20,7 @@
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
# gradlew start up script for POSIX generated by Gradle.
#
# Important for running:
#
@@ -29,7 +29,7 @@
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
# ksh gradlew
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
Vendored
+2 -2
View File
@@ -19,7 +19,7 @@
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem gradlew startup script for Windows
@rem
@rem ##########################################################################
@@ -72,7 +72,7 @@ echo location of your Java installation. 1>&2
@rem Execute Gradle
@rem Execute gradlew
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
+1 -1
View File
@@ -1,5 +1,5 @@
plugins {
id "io.spring.develocity.conventions" version "0.0.22"
id "io.spring.develocity.conventions" version "0.0.25"
}
include "spring-aop"
@@ -649,6 +649,11 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
else if (value instanceof List list) {
int index = Integer.parseInt(key);
growCollectionIfNecessary(list, index, indexedPropertyName.toString(), ph, i + 1);
if (index < 0 || index >= list.size()) {
throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
"Cannot get element with index " + index + " from List of size " +
list.size() + ", accessed using property path '" + propertyName + "'");
}
value = list.get(index);
}
else if (value instanceof Map map) {
@@ -49,7 +49,7 @@ import java.beans.PropertyDescriptor;
public interface BeanWrapper extends ConfigurablePropertyAccessor {
/**
* Specify a limit for array and collection auto-growing.
* Specify a limit for array and collection/set/list auto-growing.
* <p>Default is unlimited on a plain BeanWrapper.
* @since 4.1
*/
@@ -295,8 +295,13 @@ class TypeConverterDelegate {
ClassLoader cl = this.targetObject.getClass().getClassLoader();
try {
Class<?> enumValueType = ClassUtils.forName(enumType, cl);
Field enumField = enumValueType.getField(fieldName);
convertedValue = enumField.get(null);
if (enumValueType.isEnum()) {
Field enumField = enumValueType.getField(fieldName);
convertedValue = enumField.get(null);
}
else if (logger.isTraceEnabled()) {
logger.trace("Specified enum class [" + enumType + "] is not a Java enum");
}
}
catch (ClassNotFoundException ex) {
if (logger.isTraceEnabled()) {
@@ -313,8 +318,7 @@ class TypeConverterDelegate {
if (convertedValue == currentConvertedValue) {
// Try field lookup as fallback: for Java enum or custom enum
// with values defined as static fields. Resulting value still needs
// to be checked, hence we don't return it right away.
// with values defined as static fields.
try {
Field enumField = requiredType.getField(trimmedValue);
ReflectionUtils.makeAccessible(enumField);
@@ -16,6 +16,7 @@
package org.springframework.beans;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -183,7 +184,34 @@ class BeanWrapperAutoGrowingTests {
wrapper.setAutoGrowCollectionLimit(2);
assertThatExceptionOfType(InvalidPropertyException.class)
.isThrownBy(() -> wrapper.getPropertyValue("list[4]"))
.withRootCauseInstanceOf(IndexOutOfBoundsException.class);
.withMessageContainingAll(
"Invalid property 'list[4]'",
"Cannot get element with index 4 from List of size 0");
}
@Test
void getPropertyValueSelfPopulatingListWorksWithinLimit() {
bean.setList(new SelfPopulatingList());
assertThat(wrapper.getPropertyValue("list[2]")).isInstanceOf(Bean.class);
assertThat(bean.getList())
.hasSize(3)
.allSatisfy(entry -> assertThat(entry).isInstanceOf(Bean.class));
}
@Test
void getPropertyValueSelfPopulatingListFailsAgainstLimit() {
bean.setList(new SelfPopulatingList());
wrapper.setAutoGrowCollectionLimit(2);
assertThatExceptionOfType(InvalidPropertyException.class)
.isThrownBy(() -> wrapper.getPropertyValue("list[4]"));
}
@Test
void setPropertyValueSelfPopulatingListFailsAgainstLimitForNestedPath() {
bean.setList(new SelfPopulatingList());
wrapper.setAutoGrowCollectionLimit(2);
assertThatExceptionOfType(InvalidPropertyException.class)
.isThrownBy(() -> wrapper.setPropertyValue("list[4].prop", "test"));
}
@Test
@@ -382,4 +410,24 @@ class BeanWrapperAutoGrowingTests {
}
}
/**
* A {@link List} implementation that creates elements on demand in {@link #get(int)}
* instead of throwing {@link IndexOutOfBoundsException} for out-of-range indexes.
*
* <p>Used to verify that {@link BeanWrapperImpl} does not delegate to
* {@link List#get(int)} for indexes beyond the configured auto-grow limit.
*/
@SuppressWarnings("serial")
private static class SelfPopulatingList extends ArrayList<Bean> {
@Override
public Bean get(int index) {
while (size() <= index) {
add(new Bean());
}
return super.get(index);
}
}
}
@@ -687,7 +687,7 @@ class BeanWrapperGenericsTests {
}
public class Bar implements Foo {
public static class Bar implements Foo {
private double version;
@@ -710,7 +710,7 @@ class BeanWrapperGenericsTests {
}
public class Promotion implements ObjectWithId<Long> {
public static class Promotion implements ObjectWithId<Long> {
private Long id;
@@ -934,7 +934,7 @@ class ExtendedBeanInfoTests {
}
abstract class Library {
abstract static class Library {
public Book getBook() {
return null;
@@ -945,7 +945,7 @@ class ExtendedBeanInfoTests {
}
class LawLibrary extends Library implements TextBookOperations {
static class LawLibrary extends Library implements TextBookOperations {
@Override
public LawBook getBook() {
@@ -16,6 +16,7 @@
package org.springframework.beans.factory.config;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.LinkedHashMap;
@@ -150,7 +151,7 @@ class YamlProcessorTests {
@Test
void customTypeNotSupportedByDefault() throws Exception {
URL url = new URL("https://localhost:9000/");
URL url = URI.create("https://localhost:9000/").toURL();
setYaml("value: !!java.net.URL [\"" + url + "\"]");
assertThatExceptionOfType(ComposerException.class)
.isThrownBy(() -> this.processor.process((properties, map) -> {}))
@@ -161,7 +162,7 @@ class YamlProcessorTests {
void customTypesSupportedDueToExplicitConfiguration() throws Exception {
this.processor.setSupportedTypes(URL.class, String.class);
URL url = new URL("https://localhost:9000/");
URL url = URI.create("https://localhost:9000/").toURL();
setYaml("value: !!java.net.URL [!!java.lang.String [\"" + url + "\"]]");
this.processor.process((properties, map) -> {
@@ -82,7 +82,7 @@ class AutowireUtilsTests {
public interface MyInterfaceType<T> {
}
public class MySimpleInterfaceType implements MyInterfaceType<String> {
public static class MySimpleInterfaceType implements MyInterfaceType<String> {
}
public static class MyTypeWithMethods<T> {
@@ -523,7 +523,7 @@ class BeanFactoryGenericsTests {
new ClassPathResource("genericBeanTests.xml", getClass()));
NamedUrlList list = bf.getBean("list", NamedUrlList.class);
assertThat(list).containsExactly(new URL("http://localhost:8080"));
assertThat(list).containsExactly(URI.create("http://localhost:8080").toURL());
}
@Test
@@ -533,7 +533,7 @@ class BeanFactoryGenericsTests {
new ClassPathResource("genericBeanTests.xml", getClass()));
NamedUrlSet set = bf.getBean("set", NamedUrlSet.class);
assertThat(set).containsExactly(new URL("http://localhost:8080"));
assertThat(set).containsExactly(URI.create("http://localhost:8080").toURL());
}
@Test
@@ -543,7 +543,7 @@ class BeanFactoryGenericsTests {
new ClassPathResource("genericBeanTests.xml", getClass()));
NamedUrlMap map = bf.getBean("map", NamedUrlMap.class);
assertThat(map).containsExactly(entry(10, new URL("http://localhost:8080")));
assertThat(map).containsExactly(entry(10, URI.create("http://localhost:8080").toURL()));
}
@Test
@@ -577,7 +577,7 @@ class BeanFactoryGenericsTests {
new ClassPathResource("genericBeanTests.xml", getClass()));
UrlSet urlSet = bf.getBean("setBean", UrlSet.class);
assertThat(urlSet).containsExactly(new URL("https://www.springframework.org"));
assertThat(urlSet).containsExactly(URI.create("https://www.springframework.org").toURL());
}
/**
@@ -72,6 +72,9 @@ public class SpringTemplateLoader implements TemplateLoader {
if (logger.isDebugEnabled()) {
logger.debug("Looking for FreeMarker template with name [" + name + "]");
}
if (name.indexOf('\\') != -1) {
return null;
}
Resource resource = this.resourceLoader.getResource(this.templateLoaderPath + name);
return (resource.exists() ? resource : null);
}
@@ -0,0 +1,55 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ui.freemarker;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.core.io.DefaultResourceLoader;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link SpringTemplateLoader}.
*
* @author Sébastien Deleuze
*/
class SpringTemplateLoaderTests {
@Test
void findTemplateSourceResolvesTemplateInsidePath(@TempDir Path tempDir) throws Exception {
Path templates = Files.createDirectory(tempDir.resolve("templates"));
Files.writeString(templates.resolve("hello.ftl"), "Hello");
SpringTemplateLoader loader = new SpringTemplateLoader(new DefaultResourceLoader(),
"file:" + templates.toAbsolutePath() + File.separator);
assertThat(loader.findTemplateSource("hello.ftl")).isNotNull();
}
@Test
void findTemplateSourceRejectsBackslash(@TempDir Path tempDir) throws Exception {
Path templates = Files.createDirectory(tempDir.resolve("templates"));
Files.writeString(tempDir.resolve("other.txt"), "other");
SpringTemplateLoader loader = new SpringTemplateLoader(new DefaultResourceLoader(),
"file:" + templates.toAbsolutePath() + File.separator);
assertThat(loader.findTemplateSource("..\\other.txt")).isNull();
}
}
@@ -17,15 +17,19 @@
package org.springframework.context.support;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Base class for message source implementations, providing support infrastructure
@@ -41,6 +45,9 @@ import org.springframework.util.ObjectUtils;
*/
public abstract class MessageSourceSupport {
static final Set<Locale> JVM_LOCALES = Arrays.stream(Locale.getAvailableLocales()).
filter(l -> StringUtils.hasLength(l.getLanguage())).collect(Collectors.toSet());
private static final MessageFormat INVALID_MESSAGE_FORMAT = new MessageFormat("");
/** Logger available to subclasses. */
@@ -116,22 +123,17 @@ public abstract class MessageSourceSupport {
if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) {
return msg;
}
Map<Locale, MessageFormat> messageFormatsPerLocale = this.messageFormatsPerMessage
.computeIfAbsent(msg, key -> new ConcurrentHashMap<>());
MessageFormat messageFormat = messageFormatsPerLocale.computeIfAbsent(locale, key -> {
try {
return createMessageFormat(msg, locale);
}
catch (IllegalArgumentException ex) {
// Invalid message format - probably not intended for formatting,
// rather using a message structure with no arguments involved...
if (isAlwaysUseMessageFormat()) {
throw ex;
}
// Silently proceed with raw message if format not enforced...
return INVALID_MESSAGE_FORMAT;
}
});
MessageFormat messageFormat;
if (locale != null && JVM_LOCALES.contains(locale)) {
Map<Locale, MessageFormat> messageFormatsPerLocale = this.messageFormatsPerMessage
.computeIfAbsent(msg, key -> new ConcurrentHashMap<>());
messageFormat = messageFormatsPerLocale.computeIfAbsent(locale, key -> resolveMessageFormat(msg, key));
}
else {
messageFormat = resolveMessageFormat(msg, locale);
}
if (messageFormat == INVALID_MESSAGE_FORMAT) {
return msg;
}
@@ -140,6 +142,29 @@ public abstract class MessageSourceSupport {
}
}
/**
* Resolve a {@code MessageFormat} for the given message and Locale.
* @param msg the message to create a {@code MessageFormat} for
* @param locale the Locale to create a {@code MessageFormat} for
* @return the {@code MessageFormat} instance, or otherwise
* {@link #INVALID_MESSAGE_FORMAT} if not resolvable
* @see #createMessageFormat
*/
private MessageFormat resolveMessageFormat(String msg, @Nullable Locale locale) {
try {
return createMessageFormat(msg, locale);
}
catch (IllegalArgumentException ex) {
// Invalid message format - probably not intended for formatting,
// rather using a message structure with no arguments involved...
if (isAlwaysUseMessageFormat()) {
throw ex;
}
// Silently proceed with raw message if format not enforced...
return INVALID_MESSAGE_FORMAT;
}
}
/**
* Create a {@code MessageFormat} for the given message and Locale.
* @param msg the message to create a {@code MessageFormat} for
@@ -40,6 +40,7 @@ import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ConcurrentLruCache;
import org.springframework.util.DefaultPropertiesPersister;
import org.springframework.util.PropertiesPersister;
import org.springframework.util.StringUtils;
@@ -110,15 +111,19 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
private ResourceLoader resourceLoader = new DefaultResourceLoader();
// Cache to hold filename lists per Locale
// Cache to hold filename lists per Locale.
private final ConcurrentMap<String, Map<Locale, List<String>>> cachedFilenames = new ConcurrentHashMap<>();
// Cache to hold already loaded properties per filename
// Cache to hold already loaded properties per filename.
private final ConcurrentMap<String, PropertiesHolder> cachedProperties = new ConcurrentHashMap<>();
// Cache to hold already loaded properties per filename
// Cache to hold already merged properties per Locale.
private final ConcurrentMap<Locale, PropertiesHolder> cachedMergedProperties = new ConcurrentHashMap<>();
// Cache to hold merged properties per non-JVM Locale.
private final ConcurrentLruCache<Locale, PropertiesHolder> customLocaleProperties =
new ConcurrentLruCache<>(64, locale -> mergeProperties(collectPropertiesToMerge(locale)));
/**
* Set the list of supported file extensions.
@@ -197,20 +202,16 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
protected @Nullable String resolveCodeWithoutArguments(String code, Locale locale) {
if (getCacheMillis() < 0) {
PropertiesHolder propHolder = getMergedProperties(locale);
String result = propHolder.getProperty(code);
if (result != null) {
return result;
}
return propHolder.getProperty(code);
}
else {
for (String basename : getBasenameSet()) {
List<String> filenames = calculateAllFilenames(basename, locale);
for (String filename : filenames) {
PropertiesHolder propHolder = getProperties(filename);
String result = propHolder.getProperty(code);
if (result != null) {
return result;
}
for (String basename : getBasenameSet()) {
List<String> filenames = calculateAllFilenames(basename, locale);
for (String filename : filenames) {
PropertiesHolder propHolder = getProperties(filename, locale);
String result = propHolder.getProperty(code);
if (result != null) {
return result;
}
}
}
@@ -225,20 +226,16 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
protected @Nullable MessageFormat resolveCode(String code, Locale locale) {
if (getCacheMillis() < 0) {
PropertiesHolder propHolder = getMergedProperties(locale);
MessageFormat result = propHolder.getMessageFormat(code, locale);
if (result != null) {
return result;
}
return propHolder.getMessageFormat(code, locale);
}
else {
for (String basename : getBasenameSet()) {
List<String> filenames = calculateAllFilenames(basename, locale);
for (String filename : filenames) {
PropertiesHolder propHolder = getProperties(filename);
MessageFormat result = propHolder.getMessageFormat(code, locale);
if (result != null) {
return result;
}
for (String basename : getBasenameSet()) {
List<String> filenames = calculateAllFilenames(basename, locale);
for (String filename : filenames) {
PropertiesHolder propHolder = getProperties(filename, locale);
MessageFormat result = propHolder.getMessageFormat(code, locale);
if (result != null) {
return result;
}
}
}
@@ -261,12 +258,18 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
if (mergedHolder != null) {
return mergedHolder;
}
mergedHolder = mergeProperties(collectPropertiesToMerge(locale));
PropertiesHolder existing = this.cachedMergedProperties.putIfAbsent(locale, mergedHolder);
if (existing != null) {
mergedHolder = existing;
if (JVM_LOCALES.contains(locale)) {
mergedHolder = mergeProperties(collectPropertiesToMerge(locale));
PropertiesHolder existing = this.cachedMergedProperties.putIfAbsent(locale, mergedHolder);
if (existing != null) {
mergedHolder = existing;
}
return mergedHolder;
}
else {
return this.customLocaleProperties.get(locale);
}
return mergedHolder;
}
/**
@@ -285,7 +288,7 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
List<String> filenames = calculateAllFilenames(basenames[i], locale);
for (int j = filenames.size() - 1; j >= 0; j--) {
String filename = filenames.get(j);
PropertiesHolder propHolder = getProperties(filename);
PropertiesHolder propHolder = getProperties(filename, locale);
if (propHolder.getProperties() != null) {
holders.add(propHolder);
}
@@ -334,11 +337,11 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
}
}
// Filenames for given Locale
// Filenames for given Locale.
List<String> filenames = new ArrayList<>(7);
filenames.addAll(calculateFilenamesForLocale(basename, locale));
// Filenames for default Locale, if any
// Filenames for default Locale, if any.
Locale defaultLocale = getDefaultLocale();
if (defaultLocale != null && !defaultLocale.equals(locale)) {
List<String> fallbackFilenames = calculateFilenamesForLocale(basename, defaultLocale);
@@ -350,24 +353,27 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
}
}
// Filename for default bundle file
// Filename for default bundle file.
filenames.add(basename);
if (localeMap == null) {
localeMap = new ConcurrentHashMap<>();
Map<Locale, List<String>> existing = this.cachedFilenames.putIfAbsent(basename, localeMap);
if (existing != null) {
localeMap = existing;
if (JVM_LOCALES.contains(locale)) {
if (localeMap == null) {
localeMap = new ConcurrentHashMap<>();
Map<Locale, List<String>> existing = this.cachedFilenames.putIfAbsent(basename, localeMap);
if (existing != null) {
localeMap = existing;
}
}
localeMap.put(locale, filenames);
}
localeMap.put(locale, filenames);
return filenames;
}
/**
* Calculate the filenames for the given bundle basename and Locale,
* appending language code, country code, and variant code.
* <p>For example, basename "messages", Locale "de_AT_oo" &rarr; "messages_de_AT_OO",
* <p>For example, basename "messages", Locale "de_AT_OO" &rarr; "messages_de_AT_OO",
* "messages_de_AT", "messages_de".
* <p>Follows the rules defined by {@link java.util.Locale#toString()}.
* @param basename the basename of the bundle
@@ -402,6 +408,22 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
}
/**
* Get a PropertiesHolder for the given filename, either from the
* cache or freshly loaded.
* @param filename the bundle filename (basename + Locale)
* @param locale the requested locale (for cache filtering)
* @return the current PropertiesHolder for the bundle
* @see #getProperties(String)
*/
private PropertiesHolder getProperties(String filename, Locale locale) {
PropertiesHolder propHolder = getProperties(filename);
if (propHolder.getProperties() == null && !JVM_LOCALES.contains(locale)) {
this.cachedProperties.remove(filename);
}
return propHolder;
}
/**
* Get a PropertiesHolder for the given filename, either from the
* cache or freshly loaded.
@@ -188,36 +188,35 @@ public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSou
* found for the given basename and Locale
*/
protected @Nullable ResourceBundle getResourceBundle(String basename, Locale locale) {
if (getCacheMillis() >= 0) {
if (getCacheMillis() >= 0 || !JVM_LOCALES.contains(locale)) {
// Fresh ResourceBundle.getBundle call in order to let ResourceBundle
// do its native caching, at the expense of more extensive lookup steps.
return doGetBundle(basename, locale);
}
else {
// Cache forever: prefer locale cache over repeated getBundle calls.
Map<Locale, ResourceBundle> localeMap = this.cachedResourceBundles.get(basename);
if (localeMap != null) {
ResourceBundle bundle = localeMap.get(locale);
if (bundle != null) {
return bundle;
}
}
try {
ResourceBundle bundle = doGetBundle(basename, locale);
if (localeMap == null) {
localeMap = this.cachedResourceBundles.computeIfAbsent(basename, bn -> new ConcurrentHashMap<>());
}
localeMap.put(locale, bundle);
// Cache forever: prefer local cache over repeated getBundle calls.
Map<Locale, ResourceBundle> localeMap = this.cachedResourceBundles.get(basename);
if (localeMap != null) {
ResourceBundle bundle = localeMap.get(locale);
if (bundle != null) {
return bundle;
}
catch (MissingResourceException ex) {
if (logger.isWarnEnabled()) {
logger.warn("ResourceBundle [" + basename + "] not found for MessageSource: " + ex.getMessage());
}
// Assume bundle not found
// -> do NOT throw the exception to allow for checking parent message source.
return null;
}
try {
ResourceBundle bundle = doGetBundle(basename, locale);
if (localeMap == null) {
localeMap = this.cachedResourceBundles.computeIfAbsent(basename, bn -> new ConcurrentHashMap<>());
}
localeMap.put(locale, bundle);
return bundle;
}
catch (MissingResourceException ex) {
if (logger.isWarnEnabled()) {
logger.warn("ResourceBundle [" + basename + "] not found for MessageSource: " + ex.getMessage());
}
// Assume bundle not found
// -> do NOT throw the exception to allow for checking parent message source.
return null;
}
}
@@ -311,6 +310,11 @@ public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSou
protected @Nullable MessageFormat getMessageFormat(ResourceBundle bundle, String code, Locale locale)
throws MissingResourceException {
if (!JVM_LOCALES.contains(locale)) {
String msg = getStringOrNull(bundle, code);
return (msg != null ? createMessageFormat(msg, locale) : null);
}
Map<String, Map<Locale, MessageFormat>> codeMap = this.cachedBundleMessageFormats.get(bundle);
Map<Locale, MessageFormat> localeMap = null;
if (codeMap != null) {
@@ -197,6 +197,10 @@ public @interface Retryable {
* and {@code delay + jitter} but never below the base {@link #delay()} or
* above {@link #maxDelay()}. If a multiplier is specified, it is applied
* to the jitter value as well.
* <p>When {@link #delay()} is {@code 0} combined with a positive jitter,
* the delay never grows regardless of any configured multiplier, so the
* full configured jitter is applied directly as a random delay in the range
* from {@code 0} to {@code min(jitter, maxDelay)}.
* <p>The time unit is milliseconds by default but can be overridden via
* {@link #timeUnit}.
* <p>The default is 0 (no jitter).
@@ -270,12 +270,12 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
}
/**
* Specify the limit for array and collection auto-growing.
* Specify the limit for array and collection/set/list auto-growing.
* <p>Default is 256, preventing OutOfMemoryErrors in case of large indexes.
* Raise this limit if your auto-growing needs are unusually high.
* <p>Used for setter injection via {@link #bind(PropertyValues)};
* not applicable to field injection, and not to constructor binding
* via {@link #construct} either.
* not applicable to map properties and not to field injection and
* constructor binding via {@link #construct} either.
* @see #initBeanPropertyAccess()
* @see org.springframework.beans.BeanWrapper#setAutoGrowCollectionLimit
*/
@@ -326,6 +326,8 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
/**
* Initialize direct field access for this DataBinder,
* as alternative to the default bean property access.
* <p><b>NOTE: This is an advanced option for trusted scenarios.</b>
* Do not use direct field access for data binding from untrusted sources.
* @see #initBeanPropertyAccess()
* @see #createDirectFieldBindingResult()
*/
@@ -97,6 +97,7 @@ class ResourceBundleMessageSourceTests {
doTestMessageAccess(true, false, true, true, false);
}
@SuppressWarnings("deprecation") // for Locale constructors on JDK 19
protected void doTestMessageAccess(
boolean reloadable, boolean fallbackToSystemLocale,
boolean expectGermanFallback, boolean useCodeAsDefaultMessage, boolean alwaysUseMessageFormat) {
@@ -406,6 +407,7 @@ class ResourceBundleMessageSourceTests {
}
@Test
@SuppressWarnings("deprecation") // for Locale constructors on JDK 19
void reloadableResourceBundleMessageSourceFileNameCalculation() {
ReloadableResourceBundleMessageSource ms = new ReloadableResourceBundleMessageSource();
@@ -225,6 +225,7 @@ class MBeanClientInterceptorTests extends AbstractMBeanServerTests {
}
@Test
@SuppressWarnings("deprecation") // for Thread.getId() on JDK 19
void mxBeanOperationAccess() throws Exception {
MBeanClientInterceptor interceptor = new MBeanClientInterceptor();
interceptor.setServer(ManagementFactory.getPlatformMBeanServer());
@@ -19,6 +19,7 @@ package org.springframework.resilience;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test;
@@ -120,6 +121,10 @@ class ConcurrencyLimitTests {
@Test
void withPostProcessorForMethodWithRejection() throws Exception{
if (ForkJoinPool.getCommonPoolParallelism() < 4) {
return; // not enough concurrency possible
}
AnnotatedMethodBean proxy = createProxy(AnnotatedMethodBean.class);
AnnotatedMethodBean target = (AnnotatedMethodBean) AopProxyUtils.getSingletonTarget(proxy);
@@ -128,7 +133,7 @@ class ConcurrencyLimitTests {
futures.add(CompletableFuture.runAsync(proxy::rejectingOperation));
}
Thread.sleep(10);
for (int i = 2; i < 10; i++) {
for (int i = 2; i < 4; i++) {
futures.add(CompletableFuture.runAsync(() ->
assertThatExceptionOfType(InvocationRejectedException.class).isThrownBy(proxy::rejectingOperation)
.withMessageContaining(AnnotatedMethodBean.class.getName() + ".rejectingOperation")
@@ -159,6 +164,10 @@ class ConcurrencyLimitTests {
@Test
void withPostProcessorForClassWithRejection() throws Exception {
if (ForkJoinPool.getCommonPoolParallelism() < 4) {
return; // not enough concurrency possible
}
AnnotatedClassBeanWithRejection proxy = createProxy(AnnotatedClassBeanWithRejection.class);
AnnotatedClassBeanWithRejection target = (AnnotatedClassBeanWithRejection) AopProxyUtils.getSingletonTarget(proxy);
@@ -174,7 +183,7 @@ class ConcurrencyLimitTests {
assertThatExceptionOfType(InvocationRejectedException.class).isThrownBy(proxy::otherOperation)
.withMessageContaining(AnnotatedClassBeanWithRejection.class.getName())
.satisfies(ex -> assertThat(ex.getTarget() == target))));
for (int i = 0; i < 10; i++) {
for (int i = 0; i < 4; i++) {
futures.add(CompletableFuture.runAsync(proxy::overrideOperation));
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
@@ -95,7 +95,7 @@ class ScheduledTaskTests {
assertThat(scheduledTask.nextExecution()).isNull();
}
class CountingRunnable implements Runnable {
static class CountingRunnable implements Runnable {
int executionCount;
@@ -525,7 +525,7 @@ class SpringValidatorAdapterTests {
}
public class BeanWithListElementConstraint {
public static class BeanWithListElementConstraint {
@Valid
private List<@NotNull String> property;
@@ -540,7 +540,7 @@ class SpringValidatorAdapterTests {
}
public class BeanWithMapEntryConstraint {
public static class BeanWithMapEntryConstraint {
@Valid
private Map<@NotNull String, @NotNull String> property;
@@ -20,6 +20,7 @@ import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.function.Consumer;
@@ -59,7 +60,7 @@ public class FileNativeConfigurationWriter extends NativeConfigurationWriter {
protected void writeTo(String fileName, Consumer<BasicJsonWriter> writer) {
try {
File file = createIfNecessary(fileName);
try (FileWriter out = new FileWriter(file)) {
try (FileWriter out = new FileWriter(file, StandardCharsets.UTF_8)) {
writer.accept(createJsonWriter(out));
}
}
@@ -46,6 +46,7 @@ public abstract class NativeConfigurationWriter {
private boolean hasAnyHint(RuntimeHints hints) {
return (hints.proxies().jdkProxyHints().findAny().isPresent() ||
hints.reflection().typeHints().findAny().isPresent() ||
hints.reflection().lambdaHints().findAny().isPresent() ||
hints.resources().resourcePatternHints().findAny().isPresent() ||
hints.resources().resourceBundleHints().findAny().isPresent() ||
hints.jni().typeHints().findAny().isPresent() ||
@@ -44,6 +44,7 @@ import org.jspecify.annotations.Nullable;
* @see org.springframework.beans.factory.support.DefaultListableBeanFactory#STRICT_LOCKING_PROPERTY_NAME
* @see org.springframework.core.env.AbstractEnvironment#IGNORE_GETENV_PROPERTY_NAME
* @see org.springframework.expression.spel.SpelParserConfiguration#SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME
* @see org.springframework.expression.spel.SpelParserConfiguration#SPRING_EXPRESSION_MAX_BIG_POWER_BITS_PROPERTY_NAME
* @see org.springframework.expression.spel.SpelParserConfiguration#SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME
* @see org.springframework.jdbc.core.StatementCreatorUtils#IGNORE_GETPARAMETERTYPE_PROPERTY_NAME
* @see org.springframework.jndi.JndiLocatorDelegate#IGNORE_JNDI_PROPERTY_NAME
@@ -82,6 +82,7 @@ public abstract class AbstractDataBufferDecoder<T> extends AbstractDecoder<T> {
@Override
@SuppressWarnings("deprecation")
public Flux<T> decode(Publisher<DataBuffer> input, ResolvableType elementType,
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
@@ -90,6 +91,7 @@ public abstract class AbstractDataBufferDecoder<T> extends AbstractDecoder<T> {
}
@Override
@SuppressWarnings("deprecation")
public Mono<T> decodeToMono(Publisher<DataBuffer> input, ResolvableType elementType,
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
@@ -917,6 +917,14 @@ public abstract class DataBufferUtils {
super(delimiter);
Assert.isTrue(delimiter.length == 2, "Expected a 2-byte delimiter");
}
@Override
public boolean match(byte b) {
if (getMatches() > 0 && b != delimiter()[getMatches()]) {
setMatches(0);
}
return super.match(b);
}
}
@@ -100,20 +100,20 @@ public class LocalizedResourceHelper {
String variant = locale.getVariant();
// Check for file with language, country and variant localization.
if (variant.length() > 0) {
if (!variant.isEmpty()) {
String location =
name + this.separator + lang + this.separator + country + this.separator + variant + extension;
resource = this.resourceLoader.getResource(location);
}
// Check for file with language and country localization.
if ((resource == null || !resource.exists()) && country.length() > 0) {
if ((resource == null || !resource.exists()) && !country.isEmpty()) {
String location = name + this.separator + lang + this.separator + country + extension;
resource = this.resourceLoader.getResource(location);
}
// Check for document with language localization.
if ((resource == null || !resource.exists()) && lang.length() > 0) {
if ((resource == null || !resource.exists()) && !lang.isEmpty()) {
String location = name + this.separator + lang + extension;
resource = this.resourceLoader.getResource(location);
}
@@ -611,6 +611,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
}
}
@SuppressWarnings("deprecation") // for deprecated URL constructor on JDK 20
private Set<ClassPathManifestEntry> getClassPathManifestEntriesFromJar(File jar) throws IOException {
URL base = jar.toURI().toURL();
File parent = jar.getAbsoluteFile().getParentFile();
@@ -67,7 +67,7 @@ public interface RetryPolicy {
* @see Builder#timeout(Duration)
*/
default Duration getTimeout() {
return Duration.ZERO;
return Builder.DEFAULT_TIMEOUT;
}
/**
@@ -158,23 +158,33 @@ public interface RetryPolicy {
public static final long DEFAULT_DELAY = 1000;
/**
* The default {@linkplain #maxDelay(Duration) max delay}: {@value} ms.
* @see Long#MAX_VALUE
* The default {@linkplain #jitter(Duration) jitter}: {@value}.
* @since 7.0.9
*/
public static final long DEFAULT_MAX_DELAY = Long.MAX_VALUE;
public static final long DEFAULT_JITTER = 0;
/**
* The default {@linkplain #multiplier(double) multiplier}: {@value}.
*/
public static final double DEFAULT_MULTIPLIER = 1.0;
/**
* The default {@linkplain #maxDelay(Duration) max delay}: {@value} ms.
* @see Long#MAX_VALUE
*/
public static final long DEFAULT_MAX_DELAY = Long.MAX_VALUE;
/**
* The default {@linkplain #timeout(Duration) timeout}: {@link Duration#ZERO}.
* @since 7.0.9
*/
public static final Duration DEFAULT_TIMEOUT = Duration.ZERO;
private @Nullable BackOff backOff;
private @Nullable Long maxRetries;
private Duration timeout = Duration.ZERO;
private @Nullable Duration delay;
private @Nullable Duration jitter;
@@ -183,6 +193,8 @@ public interface RetryPolicy {
private @Nullable Duration maxDelay;
private Duration timeout = DEFAULT_TIMEOUT;
private final Set<Class<? extends Throwable>> includes = new LinkedHashSet<>();
private final Set<Class<? extends Throwable>> excludes = new LinkedHashSet<>();
@@ -234,24 +246,6 @@ public interface RetryPolicy {
return this;
}
/**
* Specify a timeout for the maximum amount of elapsed time allowed for
* the initial invocation and any subsequent retry attempts, including
* delays.
* <p>The default is {@link Duration#ZERO}, which signals that no timeout
* should be applied.
* <p>The supplied value will override any previously configured value.
* @param timeout the timeout, typically in milliseconds or seconds;
* must be greater than or equal to zero
* @return this {@code Builder} instance for chained method invocations
* @since 7.0.2
*/
public Builder timeout(Duration timeout) {
assertIsNotNegative("timeout", timeout);
this.timeout = timeout;
return this;
}
/**
* Specify the base delay after the initial invocation.
* <p>If a {@linkplain #multiplier(double) multiplier} is specified, this
@@ -281,6 +275,11 @@ public interface RetryPolicy {
* {@linkplain #maxDelay(Duration) max delay}.
* <p>If a {@linkplain #multiplier(double) multiplier} is specified, it
* is applied to the jitter value as well.
* <p>When the configured {@linkplain #delay(Duration) delay} is zero
* combined with a positive jitter, the delay never grows regardless of
* any configured multiplier, so the full configured jitter is applied
* directly as a random delay in the range from zero to
* {@code min(jitter, maxDelay)}.
* <p>The default is no jitter.
* <p>The supplied value will override any previously configured value.
* <p>You should not specify this configuration option if you have
@@ -342,6 +341,24 @@ public interface RetryPolicy {
return this;
}
/**
* Specify a timeout for the maximum amount of elapsed time allowed for
* the initial invocation and any subsequent retry attempts, including
* delays.
* <p>The default is {@link Duration#ZERO}, which signals that no timeout
* should be applied.
* <p>The supplied value will override any previously configured value.
* @param timeout the timeout, typically in milliseconds or seconds;
* must be greater than or equal to zero
* @return this {@code Builder} instance for chained method invocations
* @since 7.0.2
*/
public Builder timeout(Duration timeout) {
assertIsNotNegative("timeout", timeout);
this.timeout = timeout;
return this;
}
/**
* Specify the types of exceptions for which the {@link RetryPolicy}
* should retry a failed operation.
@@ -472,11 +489,9 @@ public interface RetryPolicy {
ExponentialBackOff exponentialBackOff = new ExponentialBackOff();
exponentialBackOff.setMaxAttempts(this.maxRetries != null ? this.maxRetries : DEFAULT_MAX_RETRIES);
exponentialBackOff.setInitialInterval(this.delay != null ? this.delay.toMillis() : DEFAULT_DELAY);
exponentialBackOff.setMaxInterval(this.maxDelay != null ? this.maxDelay.toMillis() : DEFAULT_MAX_DELAY);
exponentialBackOff.setJitter(this.jitter != null ? this.jitter.toMillis() : DEFAULT_JITTER);
exponentialBackOff.setMultiplier(this.multiplier != null ? this.multiplier : DEFAULT_MULTIPLIER);
if (this.jitter != null) {
exponentialBackOff.setJitter(this.jitter.toMillis());
}
exponentialBackOff.setMaxInterval(this.maxDelay != null ? this.maxDelay.toMillis() : DEFAULT_MAX_DELAY);
backOff = exponentialBackOff;
}
return new DefaultRetryPolicy(this.includes, this.excludes, this.predicate, this.timeout, backOff);
@@ -18,6 +18,7 @@ package org.springframework.core.serializer;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputFilter;
import java.io.ObjectInputStream;
import org.jspecify.annotations.Nullable;
@@ -38,6 +39,8 @@ public class DefaultDeserializer implements Deserializer<Object> {
private final @Nullable ClassLoader classLoader;
private final @Nullable ObjectInputFilter objectInputFilter;
/**
* Create a {@code DefaultDeserializer} with default {@link ObjectInputStream}
@@ -45,6 +48,7 @@ public class DefaultDeserializer implements Deserializer<Object> {
*/
public DefaultDeserializer() {
this.classLoader = null;
this.objectInputFilter = null;
}
/**
@@ -56,6 +60,21 @@ public class DefaultDeserializer implements Deserializer<Object> {
*/
public DefaultDeserializer(@Nullable ClassLoader classLoader) {
this.classLoader = classLoader;
this.objectInputFilter = null;
}
/**
* Create a {@code DefaultDeserializer} for using an {@link ObjectInputStream}
* with the given {@code ClassLoader}.
* @param classLoader the ClassLoader to use
* @param objectInputFilter a custom ObjectInputFilter to apply
* @since 7.0.9
* @see ConfigurableObjectInputStream#ConfigurableObjectInputStream(InputStream, ClassLoader)
* @see ObjectInputStream#setObjectInputFilter
*/
public DefaultDeserializer(@Nullable ClassLoader classLoader, @Nullable ObjectInputFilter objectInputFilter) {
this.classLoader = classLoader;
this.objectInputFilter = objectInputFilter;
}
@@ -65,10 +84,20 @@ public class DefaultDeserializer implements Deserializer<Object> {
* @since 6.2.19
* @see ConfigurableObjectInputStream#ConfigurableObjectInputStream(InputStream, ClassLoader)
*/
public @Nullable ClassLoader getClassLoader() {
public final @Nullable ClassLoader getClassLoader() {
return this.classLoader;
}
/**
* Return the {@link ObjectInputFilter} to apply to the {@link ObjectInputStream},
* if any.
* @since 7.0.9
* @see ObjectInputStream#setObjectInputFilter
*/
public final @Nullable ObjectInputFilter getObjectInputFilter() {
return this.objectInputFilter;
}
/**
* Read from the supplied {@code InputStream} and deserialize the contents
@@ -78,6 +107,9 @@ public class DefaultDeserializer implements Deserializer<Object> {
@Override
public Object deserialize(InputStream inputStream) throws IOException {
ObjectInputStream objectInputStream = new ConfigurableObjectInputStream(inputStream, this.classLoader);
if (this.objectInputFilter != null) {
objectInputStream.setObjectInputFilter(this.objectInputFilter);
}
try {
return objectInputStream.readObject();
}
@@ -28,10 +28,11 @@ import org.jspecify.annotations.Nullable;
* exposed through {@link CompositeMap#values()}.
*
* @author Arjen Poutsma
* @author Yanming Zhou
* @since 6.2
* @param <E> the type of elements maintained by this collection
*/
class CompositeCollection<E> implements Collection<E> {
class CompositeCollection<E extends @Nullable Object> implements Collection<E> {
private final Collection<E> first;
@@ -21,6 +21,8 @@ import java.util.LinkedHashSet;
import java.util.NoSuchElementException;
import java.util.Set;
import org.jspecify.annotations.Nullable;
/**
* Composite iterator that combines multiple other iterators,
* as registered via {@link #add(Iterator)}.
@@ -30,10 +32,11 @@ import java.util.Set;
*
* @author Erwin Vervaet
* @author Juergen Hoeller
* @author Yanming Zhou
* @since 3.0
* @param <E> the element type
*/
public class CompositeIterator<E> implements Iterator<E> {
public class CompositeIterator<E extends @Nullable Object> implements Iterator<E> {
private final Set<Iterator<E>> iterators = new LinkedHashSet<>();
@@ -32,17 +32,18 @@ import org.jspecify.annotations.Nullable;
* {@link CollectionUtils#compositeMap(Map, Map, BiFunction, Consumer)}.
*
* @author Arjen Poutsma
* @author Yanming Zhou
* @since 6.2
* @param <K> the type of keys maintained by this map
* @param <V> the type of mapped values
*/
final class CompositeMap<K, V> implements Map<K, V> {
final class CompositeMap<K, V extends @Nullable Object> implements Map<K, V> {
private final Map<K,V> first;
private final Map<K, V> first;
private final Map<K,V> second;
private final Map<K, V> second;
private final @Nullable BiFunction<K,V,V> putFunction;
private final @Nullable BiFunction<K, V, V> putFunction;
private final @Nullable Consumer<Map<K, V>> putAllFunction;
@@ -53,7 +54,7 @@ final class CompositeMap<K, V> implements Map<K, V> {
CompositeMap(Map<K, V> first, Map<K, V> second,
@Nullable BiFunction<K, V, V> putFunction,
@Nullable Consumer<Map<K,V>> putAllFunction) {
@Nullable Consumer<Map<K, V>> putAllFunction) {
Assert.notNull(first, "First must not be null");
Assert.notNull(second, "Second must not be null");
@@ -106,7 +107,7 @@ final class CompositeMap<K, V> implements Map<K, V> {
}
@Override
public @Nullable V put(K key, V value) {
public V put(K key, V value) {
if (this.putFunction == null) {
throw new UnsupportedOperationException();
}
@@ -25,10 +25,11 @@ import org.jspecify.annotations.Nullable;
* {@link CompositeMap#keySet()} and {@link CompositeMap#entrySet()}.
*
* @author Arjen Poutsma
* @author Yanming Zhou
* @since 6.2
* @param <E> the type of elements maintained by this set
*/
final class CompositeSet<E> extends CompositeCollection<E> implements Set<E> {
final class CompositeSet<E extends @Nullable Object> extends CompositeCollection<E> implements Set<E> {
CompositeSet(Set<E> first, Set<E> second) {
super(first, second);
@@ -118,7 +118,7 @@ public abstract class FileSystemUtils {
/**
* Recursively copy the contents of the {@code src} file/directory
* to the {@code dest} file/directory.
* to the {@code dest} file/directory, including symbolic links.
* @param src the source directory
* @param dest the destination directory
* @throws IOException in the case of I/O errors
@@ -21,15 +21,18 @@ import java.util.Collection;
import java.util.Iterator;
import java.util.function.Predicate;
import org.jspecify.annotations.Nullable;
/**
* Collection that filters out values that do not match a predicate.
* This type is used by {@link CompositeMap}.
*
* @author Arjen Poutsma
* @author Yanming Zhou
* @since 6.2
* @param <E> the type of elements maintained by this collection
*/
class FilteredCollection<E> extends AbstractCollection<E> {
class FilteredCollection<E extends @Nullable Object> extends AbstractCollection<E> {
private final Collection<E> delegate;
@@ -28,10 +28,11 @@ import org.jspecify.annotations.Nullable;
* <p>This type is used by {@link CompositeMap}.
*
* @author Arjen Poutsma
* @author Yanming Zhou
* @since 6.2
* @param <E> the type of elements returned by this iterator
*/
final class FilteredIterator<E> implements Iterator<E> {
final class FilteredIterator<E extends @Nullable Object> implements Iterator<E> {
private final Iterator<E> delegate;
@@ -56,12 +57,11 @@ final class FilteredIterator<E> implements Iterator<E> {
}
@Override
public E next() {
public @Nullable E next() {
if (!this.hasNext && !setNext()) {
throw new NoSuchElementException();
}
this.hasNext = false;
Assert.state(this.next != null, "Next should not be null");
return this.next;
}
@@ -28,11 +28,12 @@ import org.jspecify.annotations.Nullable;
* This type is used by {@link CompositeMap}.
*
* @author Arjen Poutsma
* @author Yanming Zhou
* @since 6.2
* @param <K> the type of keys maintained by this map
* @param <V> the type of mapped values
*/
final class FilteredMap<K, V> extends AbstractMap<K, V> {
final class FilteredMap<K, V extends @Nullable Object> extends AbstractMap<K, V> {
private final Map<K, V> delegate;
@@ -26,10 +26,11 @@ import org.jspecify.annotations.Nullable;
* This type is used by {@link CompositeMap}.
*
* @author Arjen Poutsma
* @author Yanming Zhou
* @since 6.2
* @param <E> the type of elements maintained by this set
*/
final class FilteredSet<E> extends FilteredCollection<E> implements Set<E> {
final class FilteredSet<E extends @Nullable Object> extends FilteredCollection<E> implements Set<E> {
public FilteredSet(Set<E> delegate, Predicate<E> filter) {
super(delegate, filter);
@@ -187,7 +187,7 @@ public abstract class MimeTypeUtils {
/**
* Parse the given String into a single {@code MimeType}.
* Recently parsed {@code MimeType} are cached for further retrieval.
* <p>Recently parsed {@code MimeType} values are cached for future retrieval.
* @param mimeType the string to parse
* @return the mime type
* @throws InvalidMimeTypeException if the string cannot be parsed
@@ -238,7 +238,7 @@ public abstract class MimeTypeUtils {
break;
}
}
else if (ch == '"' && mimeType.charAt(nextIndex - 1) != '\\') {
else if (ch == '"' && (nextIndex == 0 || mimeType.charAt(nextIndex - 1) != '\\')) {
quoted = !quoted;
}
nextIndex++;
@@ -132,7 +132,7 @@ public abstract class StringUtils {
*/
@Contract("null -> false")
public static boolean hasLength(@Nullable CharSequence str) {
return (str != null && !str.isEmpty()); // as of JDK 15
return (str != null && !str.isEmpty());
}
/**
@@ -956,6 +956,7 @@ public abstract class StringUtils {
String country = tokens[1];
validateLocalePart(country);
String variant = Arrays.stream(tokens).skip(2).collect(Collectors.joining(delimiter));
validateLocalePart(variant);
return new Locale(language, country, variant);
}
@@ -154,6 +154,10 @@ public class ExponentialBackOff implements BackOff {
* {@code initialInterval} or above {@code maxInterval}.
* <p>If a {@code multiplier} is specified, it is applied to the jitter value
* as well.
* <p>When {@code initialInterval} is {@code 0} combined with a positive
* jitter, the interval never grows regardless of any configured multiplier,
* so the full configured jitter is applied directly as a random interval in
* the range from {@code 0} to {@code min(jitter, maxInterval)}.
* @param jitter the jitter value in milliseconds
* @since 7.0
*/
@@ -311,7 +315,9 @@ public class ExponentialBackOff implements BackOff {
long jitter = getJitter();
if (jitter > 0) {
long initialInterval = getInitialInterval();
long applicableJitter = jitter * (interval / initialInterval);
// When initialInterval is 0 the interval never grows, so the scale factor
// stays at its baseline value of 1 and the full configured jitter is applied.
long applicableJitter = jitter * (initialInterval > 0 ? (interval / initialInterval) : 1);
long min = Math.max(interval - applicableJitter, initialInterval);
long max = Math.min(interval + applicableJitter, getMaxInterval());
return min + (long) (Math.random() * (max - min));
@@ -151,7 +151,9 @@ public class XmlValidationModeDetector {
private String consumeCommentTokens(String line) {
int indexOfStartComment = line.indexOf(START_COMMENT);
if (indexOfStartComment == -1 && !line.contains(END_COMMENT)) {
return line;
// If we are inside a multi-line comment, the entire line is comment
// data and must not be treated as content.
return (this.inComment ? "" : line);
}
String result = "";
@@ -27,7 +27,6 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jspecify.annotations.Nullable;
@@ -77,7 +76,7 @@ abstract class ClassFileAnnotationDelegate {
}
}
Map<String, Object> compactedAttributes = (attributes.isEmpty() ? Collections.emptyMap() : attributes);
return MergedAnnotation.of(classLoader, new Source(annotation), annotationType, compactedAttributes);
return MergedAnnotation.of(classLoader, new Source(className), annotationType, compactedAttributes);
}
catch (ClassNotFoundException | LinkageError ex) {
// Non-loadable annotation type -> ignore.
@@ -108,27 +107,13 @@ abstract class ClassFileAnnotationDelegate {
}
private static Object parseArrayValue(String className, @Nullable ClassLoader classLoader, AnnotationValue.OfArray arrayValue) {
if (arrayValue.values().isEmpty()) {
return new Object[0];
}
Stream<AnnotationValue> stream = arrayValue.values().stream();
switch (arrayValue.values().getFirst()) {
case AnnotationValue.OfInt _ -> {
return stream.map(AnnotationValue.OfInt.class::cast).mapToInt(AnnotationValue.OfInt::intValue).toArray();
}
case AnnotationValue.OfDouble _ -> {
return stream.map(AnnotationValue.OfDouble.class::cast).mapToDouble(AnnotationValue.OfDouble::doubleValue).toArray();
}
case AnnotationValue.OfLong _ -> {
return stream.map(AnnotationValue.OfLong.class::cast).mapToLong(AnnotationValue.OfLong::longValue).toArray();
}
default -> {
Class<?> arrayElementType = resolveArrayElementType(arrayValue.values(), classLoader);
return stream
.map(rawValue -> readAnnotationValue(className, rawValue, classLoader))
.toArray(length -> (Object[]) Array.newInstance(arrayElementType, length));
}
List<AnnotationValue> values = arrayValue.values();
Class<?> arrayElementType = (values.isEmpty() ? Object.class : resolveArrayElementType(values, classLoader));
Object array = Array.newInstance(arrayElementType, values.size());
for (int i = 0; i < values.size(); i++) {
Array.set(array, i, readAnnotationValue(className, values.get(i), classLoader));
}
return array;
}
@SuppressWarnings("unchecked")
@@ -143,28 +128,25 @@ abstract class ClassFileAnnotationDelegate {
}
private static Class<?> resolveArrayElementType(List<AnnotationValue> values, @Nullable ClassLoader classLoader) {
AnnotationValue firstValue = values.getFirst();
switch (firstValue) {
case AnnotationValue.OfConstant constantValue -> {
return constantValue.resolvedValue().getClass();
}
case AnnotationValue.OfAnnotation _ -> {
return MergedAnnotation.class;
}
case AnnotationValue.OfClass _ -> {
return String.class;
}
case AnnotationValue.OfEnum enumValue -> {
return loadEnumClass(enumValue, classLoader);
}
default -> {
return Object.class;
}
}
return switch (values.getFirst()) {
case AnnotationValue.OfByte _ -> byte.class;
case AnnotationValue.OfChar _ -> char.class;
case AnnotationValue.OfDouble _ -> double.class;
case AnnotationValue.OfFloat _ -> float.class;
case AnnotationValue.OfInt _ -> int.class;
case AnnotationValue.OfLong _ -> long.class;
case AnnotationValue.OfShort _ -> short.class;
case AnnotationValue.OfBoolean _ -> boolean.class;
case AnnotationValue.OfString _ -> String.class;
case AnnotationValue.OfAnnotation _ -> MergedAnnotation.class;
case AnnotationValue.OfClass _ -> String.class;
case AnnotationValue.OfEnum enumValue -> loadEnumClass(enumValue, classLoader);
case AnnotationValue.OfArray _ -> Object.class;
};
}
record Source(Annotation annotation) {
record Source(String className) {
}
}
@@ -18,12 +18,13 @@ package org.springframework.aot.nativex;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import org.json.JSONException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.skyscreamer.jsonassert.JSONAssert;
@@ -63,7 +64,7 @@ class FileNativeConfigurationWriterTests {
}
@Test
void serializationConfig() throws IOException, JSONException {
void serializationConfig() throws Exception {
FileNativeConfigurationWriter generator = new FileNativeConfigurationWriter(tempDir);
RuntimeHints hints = new RuntimeHints();
ReflectionHints reflectionHints = hints.reflection();
@@ -81,7 +82,7 @@ class FileNativeConfigurationWriterTests {
}
@Test
void proxyConfig() throws IOException, JSONException {
void proxyConfig() throws Exception {
FileNativeConfigurationWriter generator = new FileNativeConfigurationWriter(tempDir);
RuntimeHints hints = new RuntimeHints();
ProxyHints proxyHints = hints.proxies();
@@ -99,7 +100,7 @@ class FileNativeConfigurationWriterTests {
}
@Test
void reflectionConfig() throws IOException, JSONException {
void reflectionConfig() throws Exception {
FileNativeConfigurationWriter generator = new FileNativeConfigurationWriter(tempDir);
RuntimeHints hints = new RuntimeHints();
ReflectionHints reflectionHints = hints.reflection();
@@ -137,8 +138,36 @@ class FileNativeConfigurationWriterTests {
""");
}
@Test // gh-36989
void lambdaConfig() throws Exception {
FileNativeConfigurationWriter generator = new FileNativeConfigurationWriter(tempDir);
RuntimeHints hints = new RuntimeHints();
hints.reflection().registerLambda(Integer.class, builder -> builder
.withDeclaringMethod("getCell", Integer.class, Integer.class)
.withInterfaces(Supplier.class));
generator.write(hints);
assertEquals("""
{
"reflection": [
{
"type": {
"lambda": {
"declaringClass": "java.lang.Integer",
"declaringMethod": {
"name": "getCell",
"parameterTypes": [ "java.lang.Integer", "java.lang.Integer" ]
},
"interfaces": [ "java.util.function.Supplier" ]
}
}
}
]
}
""");
}
@Test
void jniConfig() throws IOException, JSONException {
void jniConfig() throws Exception {
// same format as reflection so just test basic file generation
FileNativeConfigurationWriter generator = new FileNativeConfigurationWriter(tempDir);
RuntimeHints hints = new RuntimeHints();
@@ -157,7 +186,7 @@ class FileNativeConfigurationWriterTests {
}
@Test
void resourceConfig() throws IOException, JSONException {
void resourceConfig() throws Exception {
FileNativeConfigurationWriter generator = new FileNativeConfigurationWriter(tempDir);
RuntimeHints hints = new RuntimeHints();
ResourceHints resourceHints = hints.resources();
@@ -176,6 +205,17 @@ class FileNativeConfigurationWriterTests {
}""");
}
@Test // gh-36972
void resourceConfigWithNonAsciiPatternIsWrittenAsUtf8() throws IOException {
FileNativeConfigurationWriter generator = new FileNativeConfigurationWriter(tempDir);
RuntimeHints hints = new RuntimeHints();
hints.resources().registerPattern("com/example/café/**");
generator.write(hints);
Path jsonFile = tempDir.resolve("META-INF").resolve("native-image").resolve("reachability-metadata.json");
byte[] content = Files.readAllBytes(jsonFile);
assertThat(content).containsSequence("café".getBytes(StandardCharsets.UTF_8));
}
@Test
void namespace() {
String groupId = "foo.bar";
@@ -190,7 +230,8 @@ class FileNativeConfigurationWriterTests {
assertThat(jsonFile.toFile()).exists();
}
private void assertEquals(String expectedString) throws IOException, JSONException {
private static void assertEquals(String expectedString) throws Exception {
Path jsonFile = tempDir.resolve("META-INF").resolve("native-image").resolve("reachability-metadata.json");
String content = Files.readString(jsonFile);
JSONAssert.assertEquals(expectedString, content, JSONCompareMode.NON_EXTENSIBLE);

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