Compare commits

..

160 Commits

Author SHA1 Message Date
Brian Clozel 3591f1e9fb Release v7.0.2 2025-12-11 16:53:45 +01:00
Juergen Hoeller e2c9dc7138 Revert to previous behavior for 7.0.2 (based on Boot/Data impact)
Preparing the revised behavior for 7.1 instead.

See gh-31456
2025-12-11 15:37:41 +01:00
Sam Brannen 1818161f58 Ensure bottom-up semantics in resolveDefaultContextConfigurationAttributes()
Closes gh-31456
2025-12-11 13:37:32 +01:00
Sam Brannen 8916ee9f81 Set inheritLocations to true in ContextConfigurationAttributes constructor
Closes gh-36000
2025-12-11 13:06:50 +01:00
Brian Clozel d835fe311d Do not send null HTTP header value in JdkClientHttpRequest
Prior to this commit, the `JdkClientHttpRequest` would add all values
from `HttpHeaders` to the native request builder. This could cause
`NullPointerException` being thrown at runtime because the `HttpClient`
does not support that.

This commit replicates a fix that was applied to the
`SimpleClientHttpRequest`, turning null values into empty "".

Fixes gh-35996
2025-12-11 09:45:30 +01:00
rstoyanchev 0eefac21c9 Polishing contribution
Closes gh-35758
2025-12-10 18:17:12 +00:00
Samuel Gulliksson e99791f289 Improve i18n-support for NoResourceFoundException.
Return the requested resource as ErrorResponse.getDetailMessageArguments,
making it usable with message customization and i18n.

See gh-35758

Signed-off-by: Samuel Gulliksson <samuel.gulliksson@gmail.com>
2025-12-10 18:17:12 +00:00
Sam Brannen 658775b914 Avoid unnecessary list creation & processing in AbstractTestContextBootstrapper
Closes gh-35995
2025-12-10 18:56:57 +01:00
Sam Brannen ea7a1d789e Resolve ContextLoader only once in AbstractTestContextBootstrapper
Closes gh-35994
2025-12-10 18:56:50 +01:00
Sam Brannen 4ae471df01 Resolve all default context configuration within @⁠Nested hierarchy
Prior to this commit, if an enclosing test class (such as one annotated
with @⁠SpringBootTest or simply @⁠ExtendWith(SpringExtension.class))
was not annotated with @⁠ContextConfiguration (or @⁠Import with
@⁠SpringBootTest), the ApplicationContext loaded for a @⁠Nested test
class would not use any default context configuration for the enclosing
test class.

Effectively, a default XML configuration file or static nested
@⁠Configuration class for the enclosing test class was not discovered
by the AbstractTestContextBootstrapper when attempting to build the
MergedContextConfiguration (application context cache key).

To address that, this commit introduces a new
resolveDefaultContextConfigurationAttributes() method in
ContextLoaderUtils which is responsible for creating instances of
ContextConfigurationAttributes for all superclasses and enclosing
classes. This effectively enables AbstractTestContextBootstrapper to
delegate to the resolved SmartContextLoader to properly detect a
default XML configuration file or static nested @⁠Configuration class
even if such classes are not annotated with @⁠ContextConfiguration.

Closes gh-31456
2025-12-10 18:56:42 +01:00
Sam Brannen 75e3f44a7b Update Javadoc due to changes in retry support
See gh-35940
2025-12-10 16:45:43 +01:00
Juergen Hoeller adcd7cb4cb Introduce RetryListener#onRetryableExecution callback with RetryState
Closes gh-35940
2025-12-10 15:09:33 +01:00
Sam Brannen d0be180a69 Make @​Retryable and RetryTemplate timeout tests more robust
See gh-35963
2025-12-10 14:22:42 +01:00
Sam Brannen 3731fed4ca Revise MultiValueMapCollector implementation and tests
See https://github.com/spring-projects/spring-data-commons/issues/3420
Closes gh-35958
2025-12-10 13:35:00 +01:00
u214578 bfc02cda0c Introduce MultiValueMapCollector for use with streams
See https://github.com/spring-projects/spring-data-commons/issues/3420
See gh-35958

Signed-off-by: Florian Hof <florian.hof@sbb.ch>
2025-12-10 13:28:59 +01:00
Sam Brannen f734d04a0d Fix grammar 2025-12-10 13:28:59 +01:00
Sam Brannen ce4c9ebe3c Polishing 2025-12-10 12:31:26 +01:00
Brian Clozel 7c4801304d Upgrade to Reactor 2025.0.1
Closes gh-35985
2025-12-09 22:19:53 +01:00
Brian Clozel 2731183420 Upgrade to Micrometer 1.16.1
Closes gh-35984
2025-12-09 22:18:03 +01:00
Sam Brannen 38cf4ab3fc Make RetryTemplate timeout tests more robust
See gh-35963
2025-12-09 17:03:41 +01:00
Sam Brannen 2137ec70d2 Make @​Retryable timeout tests more robust
See gh-35963
2025-12-09 16:56:53 +01:00
Sam Brannen 61201db704 Improve error message for preemptive timeout in RetryTemplate
The error message in such cases now indicates that the retry process
is being aborted preemptively due to pending sleep time.

For example:

  Retry policy for operation 'myMethod' would exceed timeout (5 ms) due
  to pending sleep time (10 ms); preemptively aborting execution

See gh-35963
2025-12-09 16:46:44 +01:00
Sam Brannen 2643c6212f Delete obsolete test code 2025-12-09 16:39:20 +01:00
Sam Brannen b46328a000 Fix broken assertions in @​Retryable timeout tests
See gh-35963
2025-12-09 16:38:05 +01:00
Sam Brannen cc4c693db7 Log RetryException for @Retryable methods
To improve diagnostics, this commit logs a DEBUG message including the
RetryException thrown by RetryTemplate when it's used behind the scenes
for @Retryable method invocations.

Closes gh-35983
2025-12-09 16:24:00 +01:00
Sam Brannen a206ea8b12 Make @​Retryable timeout tests more robust
See gh-35963
2025-12-09 16:10:43 +01:00
Sam Brannen 9f1d9fe82c Support timeouts in @​Retryable and RetryPolicy
Specifically, this commit introduces:

- timeout and timeoutString attributes in @​Retryable

- a default getTimeout() method in RetryPolicy

- a timeout() method in RetryPolicy.Builder

- an onRetryPolicyTimeout() callback in RetryListener

- support for checking exceeded timeouts in RetryTemplate (also used
  for imperative method invocations with @​Retryable)

- support for checking exceeded timeouts in reactive pipelines with
  @​Retryable

Closes gh-35963
2025-12-09 14:06:35 +01:00
Juergen Hoeller ab33000750 Do not keep target connection after failed settings
Includes aligned setReadOnly exception suppression.

Closes gh-35980
2025-12-09 12:59:27 +01:00
Juergen Hoeller e2ab9cd5da Use composed cache key for different SmartFactoryBean object types
Closes gh-35974
2025-12-09 12:59:19 +01:00
Sam Brannen 103db5429a Use AtomicIntegerAssert 2025-12-09 12:43:18 +01:00
Sam Brannen 68f8139206 Polishing 2025-12-08 17:11:38 +01:00
Sam Brannen a9da900b5b Fix test logic 2025-12-08 17:10:27 +01:00
Brian Clozel e7e4b3559a Polishing contribution
Closes gh-35978
2025-12-08 16:10:20 +01:00
Johnny Lim feb77f924a Fix SubscriberInputStream.resume()
See gh-35978

Signed-off-by: Johnny Lim <izeye@naver.com>
2025-12-08 16:10:05 +01:00
Brian Clozel 12c3dc0cbe Fix compressed HEAD requests handling in JDK client
Prior to this commit, the `JdkClientHttpRequestFactory` would support
decompressing gziped/deflate encoded response bodies but would fail if
the response has no body but has a "Content-Encoding" response header.
This happens as a response to HEAD requests.

This commit ensures that only responses with actual message bodies are
decompressed.

Fixes gh-35966
2025-12-08 15:23:55 +01:00
Juergen Hoeller df27627516 Accept assignable match for covariant return type
See gh-35936
2025-12-08 12:41:43 +01:00
Sam Brannen 939aa84214 Revise type-level nullability in ConvertingComparator
This commit revises the type-level nullability declaration in
ConvertingComparator in order to adapt to recent nullability changes in
Converter.

This commit also revises the workaround in commit 53d9ba879d.

See gh-35947
2025-12-08 11:22:14 +01:00
Brian Clozel 9f77f401ad Complete Propagator.Getter implementation
As of Micrometer Tracing 1.6.0, the `Propagator.Getter` interface
adds a new `getAll` method with a default implementation return a
singleton collection.

This commit adds the missing implementation override in both Servlet and
Reactor web server contexts.

Fixes gh-35965
2025-12-08 11:07:30 +01:00
Sébastien Deleuze f19f1a667c Add support for package-private BeanRegistrar
Closes gh-35803
2025-12-05 17:04:51 +01:00
Sébastien Deleuze 69207c6c16 Introduce BeanFactoryInitializationCode#getClassName
See gh-35803
2025-12-05 17:04:51 +01:00
Sébastien Deleuze f61832ecb8 Fix indentation 2025-12-05 17:04:51 +01:00
Juergen Hoeller 14790493f4 Upgrade to Log4J 2.25.2, Groovy 5.0.3, Jetty 12.1.5, Jetty Reactive HttpClient 4.1.4, Checkstyle 12.2 2025-12-05 17:03:57 +01:00
Juergen Hoeller 53d9ba879d Suppress NullAway warning for nullable Annotation return value 2025-12-05 16:53:24 +01:00
Juergen Hoeller 9d2d9ae0ff Reinstate stopOrder test
See gh-35964
2025-12-05 16:30:00 +01:00
Juergen Hoeller c74af40288 Stop already started Lifecycle beans on cancelled refresh
Closes gh-35964
2025-12-05 16:14:27 +01:00
Juergen Hoeller 196c1dd51c Handle absolute file URLs in getClassPathManifestEntriesFromJar
Closes gh-35682
2025-12-05 16:13:30 +01:00
Sam Brannen 44689b9ec0 Simplify RestTestClientIntegrationTests and remove duplicate field 2025-12-05 16:00:42 +01:00
Sam Brannen b916dc962e Document that annotations which reference types not present are ignored
Add warnings to the class-level Javadoc for MergedAnnotations,
AnnotatedTypeMetadata, AnnotationMetadata, and MethodMetadata to point
out that annotations may be ignored if their attributes reference types
that are not present in the classpath.

Closes gh-35959
2025-12-05 15:50:02 +01:00
Tran Ngoc Nhan 87d0113b25 Correct Reactive Libraries reference link
Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2025-12-05 14:09:02 +00:00
rstoyanchev cee185ce3b Append query on redirect in UrlHandlerFilter
Closes gh-35873
2025-12-05 12:35:32 +00:00
rstoyanchev e45727d4fa Polishing in InMemoryWebSessionStore
Closes gh-35866
2025-12-05 12:29:11 +00:00
Mengqi Xu bc088ebdef Allow configuring default maxIdleTime on InMemoryWebSessionStore.
See gh-35866

Signed-off-by: Mengqi Xu <2663479778@qq.com>
2025-12-05 12:28:37 +00:00
Sam Brannen f1cf91c9f9 Polishing 2025-12-05 12:22:24 +01:00
Sébastien Deleuze d36475cb4c Use flexible generic type level nullability in Converter
Allow defining converters that return non-null values by
leveraging flexible generic type level nullability in
org.springframework.core.convert.converter.Converter.

Closes gh-35947
2025-12-05 07:28:52 +01:00
Sébastien Deleuze 8b6d7e0da7 Refine KotlinDetector#hasSerializableAnnotation
Refine KotlinDetector#hasSerializableAnnotation in order to detect
deeply nested generic types.

Closes gh-35960
2025-12-04 15:54:03 +01:00
Sam Brannen 0a86a7e3a8 Use == instead of instanceof for primitive array type checks
Closes gh-35962
2025-12-04 15:06:08 +01:00
rstoyanchev 8edde374bb Lower bad requests to DEBUG level in AbstractHandshakeHandler
Closes gh-35930
2025-12-04 09:07:55 +00:00
rstoyanchev 35b58cd0ee Avoid http - web.utils package cycle
Closes gh-35952
2025-12-04 09:07:55 +00:00
Juergen Hoeller dfc900c7c4 Add @Override to existing resetCaches() methods
See gh-35845
See gh-35840
2025-12-04 07:09:21 +01:00
Juergen Hoeller 07c0213f20 Polishing 2025-12-04 00:12:31 +01:00
Juergen Hoeller 667851c0fa Avoid computeIfAbsent for createMappings which calls back into same map
Closes gh-35944
2025-12-04 00:12:22 +01:00
Juergen Hoeller 96aadc2b12 Add resetCaches() method to general CacheManager interface
Closes gh-35845
See gh-35840
2025-12-04 00:12:13 +01:00
Sam Brannen 2c6ccaea05 Use simple type name for meta-annotation in log message
In contrast to the previous commit, a warning similar to the following
is now logged in such cases.

WARN o.s.c.a.MergedAnnotation -
  Failed to introspect meta-annotation @MyAnnotation on class
  example.Config: java.lang.TypeNotPresentException:
  Type example.OptionalDependency not present

See gh-35927
2025-12-03 17:15:53 +01:00
Sam Brannen 62d09be2ae Log warning if meta-annotation is ignored due to types not present
Prior to this commit, if a meta-annotation could not be loaded because
its attributes referenced types not present in the classpath, the
meta-annotation was silently ignored.

To improve diagnostics for such use cases, this commit introduces WARN
support in IntrospectionFailureLogger and revises AttributeMethods.canLoad()
to log a warning if a meta-annotation is ignored due to an exception
thrown while attempting to load its attributes.

For example, a warning similar to the following is now logged in such
cases.

WARN o.s.c.a.MergedAnnotation -
  Failed to introspect meta-annotation @example.MyAnnotation on class
  example.Config: java.lang.TypeNotPresentException:
  Type example.OptionalDependency not present

This commit also improves log messages in AnnotationTypeMappings.

Closes gh-35927
2025-12-03 17:00:24 +01:00
Stéphane Nicoll 47f65b3dff Use correct product name for IntelliJ IDEA 2025-12-02 16:54:56 +01:00
Sam Brannen c7e24a5c26 Update antora-extensions to 1.14.7
Closes gh-35949
2025-12-02 15:33:56 +01:00
Sam Brannen a493bd959d Fix nullability in TestContextAnnotationUtils.findAnnotationDescriptor() 2025-12-02 15:32:32 +01:00
Juergen Hoeller 92e9543ad4 Select method with resolved return type match (among multiple candidates)
Removes unnecessary array type check for parameters of candidate methods.

Closes gh-35936
2025-12-01 23:25:59 +01:00
ivonaest 68231aa08c Upgrade json-path to 2.10.0
Closes gh-35924
Signed-off-by: ivonaest <ivona.cvija@est.tech>
2025-12-01 15:27:45 +01:00
Juergen Hoeller 71d18ebabc Improve debug log for received message 2025-12-01 15:03:08 +01:00
Juergen Hoeller 3ccb0786db Clear remaining invoker resources when releasing shared Connection
Closes gh-35932
2025-12-01 15:03:00 +01:00
rstoyanchev 15c16d2c3a FragmentsRendering exposes its fragments
Closes gh-35775
2025-12-01 11:17:40 +00:00
John Niang 5dcb85db8d BindingContext constructor uses given ReactiveAdapterRegistry arg
Closes gh-35771

Signed-off-by: John Niang <johnniang@foxmail.com>
2025-12-01 11:07:11 +00:00
rstoyanchev 4ae03ecd40 Use channelId for ReactorNettyWebSocketSession's id
Closes gh-35883
2025-12-01 11:01:04 +00:00
jnizet 1603045c7d Avoid passing null as argument marked as non-nullable
Closes gh-35933
Signed-off-by: jnizet <jb@ninja-squad.com>
2025-12-01 10:29:20 +01:00
Juergen Hoeller ace2b2bac9 Upgrade to Hibernate ORM 7.2.0.CR3 2025-11-30 11:32:33 +01:00
Juergen Hoeller f082c435e1 Polishing 2025-11-30 11:31:10 +01:00
Juergen Hoeller c6810334ca Move extended web data binders to common support packages
This resolves cyclic dependencies between function and annotation-based packages.

See gh-35800
2025-11-30 11:31:04 +01:00
Juergen Hoeller 3b90311c17 Revise ApplicationContext#getId() nullability to non-null
Closes gh-35925
2025-11-28 13:58:11 +01:00
Sam Brannen 946fc65712 Stop linking to 6.1.22-SNAPSHOT reference docs
With this commit, we now include snapshots for main (which currently
correlates to 7.0.x), 6.2.x, and 7.0.x to 9.*.x.

Closes gh-35923

(cherry picked from commit 305a512a55)
2025-11-28 13:00:22 +01:00
Juergen Hoeller fd25e2f468 Use concurrent set for reactive transaction synchronizations
Closes gh-35921
2025-11-28 10:24:21 +01:00
Sébastien Deleuze 46ceedf992 Refine AbstractKotlinSerializationHttpMessageConverter#canWrite
AbstractKotlinSerializationHttpMessageConverter#writeInternal is able to
resolve the ResolvableType from the Object parameter when the provided
one via the ResolvableType parameter is not resolvable, but
AbstractKotlinSerializationHttpMessageConverter#canWrite lacks of
such capability.

This commit refines
AbstractKotlinSerializationHttpMessageConverter#canWrite to resolve the
ResolvableType from the Class<?> parameter when the provided one via the
ResolvableType parameter is not resolvable.

Closes gh-35920
2025-11-27 16:52:45 +01:00
Brian Clozel a4b3111928 Make HttpEntity headers mutables
Since its inception, instantiating an `HttpEntity` makes its
`HttpHeaders` read-only. While immutability is an interesting design
principle, here we shouldn't enforce this.

For example, developers can expect to instantiate a `ResponseEntity`
and still mutate its headers.

Closes gh-35888
2025-11-27 16:37:47 +01:00
Juergen Hoeller 4c4161c8c0 Cache resolved singleton beans in injected Provider instance
Includes alignment for direct Optional injection points, consistently registering an autowiredBeanNames entry for an Optional as well as a non-Optional injection result.

Closes gh-35373
Closes gh-35919
2025-11-27 16:01:57 +01:00
Juergen Hoeller 61d5413c23 Tighten cacheable decision behind @Lazy injection point
Closes gh-35917
2025-11-27 16:00:43 +01:00
Brian Clozel 08e6d762d2 Always buffer response body in RestTestClient
Prior to this commit, `RestTestClient` tests could only perform
expectations on the response without consuming the body. In this case,
the client could leak HTTP connections with the underlying HTTP library
because the response was not entirely read.

This commit ensures that the response is always fully drained before
performing expectations. The client is configured to buffer the response
content, so further body expectations are always possible.

Fixes gh-35784
2025-11-27 15:50:00 +01:00
Sébastien Deleuze 7a19cbb452 Change AbstractSmartHttpMessageConverter canRead/canWrite overrides
Extending AbstractSmartHttpMessageConverter typically requires to
override both Class and ResolvableType variants of canRead. This was not
intended as SmartHttpMessageConverter interface has default methods
doing the conversion from Class parameters to ResolvableType ones, but
AbstractHttpMessageConverter overrides it.

This commit changes AbstractSmartHttpMessageConverter canRead/canWrite
overrides from ResolvableType to Class ones that delegate to the
ResolvableType variants. It also refines
AbstractJacksonHttpMessageConverter accordingly.

Closes gh-35916
2025-11-27 15:38:35 +01:00
Juergen Hoeller 2f1e1d483a Remove early adaptation to JTA 2.1 setReadOnly method
The UserTransaction read-only semantics are still in discussion. If they turn out to be stricter than Spring's read-only hint, we should only apply them when configured with an explicit enforceReadOnly=true flag at the Spring JtaTransactionManager level (similar to the same-named flag in DataSourceTransactionManager).

See gh-35915
See gh-35633
2025-11-27 13:16:45 +01:00
Sam Brannen 24d152cdab Convert sentence to tip in Kotlin testing chapter
This commit also moves the text to a more appropriate section of the
chapter.
2025-11-27 12:37:06 +01:00
Sam Brannen cc530d4df2 Support RuntimeBeanReference(name, type) in AOT-generated code
The RuntimeBeanReference(name, type) constructor was introduced in 7.0;
however, BeanDefinitionPropertyValueCodeGeneratorDelegates in our AOT
infrastructure was not updated to support the new constructor.

This commit revises BeanDefinitionPropertyValueCodeGeneratorDelegates
to ensure that AOT generated code for a RuntimeBeanReference uses the
RuntimeBeanReference(name, type) constructor, thereby retaining the
originally configured bean name.

See gh-35101
Closes gh-35913
2025-11-27 11:37:46 +01:00
Juergen Hoeller 6c3132cb8c Narrow method annotation check in hasQualifier to setter methods
Closes gh-35908
2025-11-26 23:01:55 +01:00
Juergen Hoeller a15274d431 Revise volatile access to singletonInstance field
For defensiveness against a singletonInstance/initialized visibility mismatch, we accept the locking overhead for pre-initialized null values (where we need the initialized field) in favor of a defensive fast path for non-null values (where we only need the singletonInstance field).

Closes gh-35905
2025-11-26 17:20:11 +01:00
Tran Ngoc Nhan 45d4fd3b7e Fix broken Javadoc links to methods
Closes gh-35899
Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2025-11-26 17:05:16 +01:00
Sam Brannen 3fd5d69939 Remove JUnit 4 based meta-annotation example
This commit also converts the examples from XML configuration files to
@⁠Configuration classes.

Closes gh-35903
2025-11-26 16:37:04 +01:00
Sébastien Deleuze 8647c44364 Add dynamic ObjectToObjectConverter hints
This commit refines BindingReflectionHintsRegistrar with additional
dynamic hints for application-defined types, main core Java conversion
ones being already covered by ObjectToObjectConverterRuntimeHints.

Closes gh-35847
2025-11-26 16:17:41 +01:00
Sam Brannen 6504177e7b Refer to "Spring Tools" instead of "Spring Tools for Eclipse"
Closes gh-35901
2025-11-26 16:11:41 +01:00
Sam Brannen c1125699bb Document that the SpringExtension requires JUnit Jupiter 6.0 or higher
Closes gh-35900
2025-11-26 15:47:21 +01:00
Juergen Hoeller f58d0f6aae Consistent namespace element declarations 2025-11-26 15:09:05 +01:00
Juergen Hoeller c1b6bfb681 Expose non-existent resources at the end of the sorted result
Closes gh-35895
2025-11-26 15:08:24 +01:00
Juergen Hoeller 9d4abb63d8 Clarify JMS sessionTransacted flag for local versus global transaction
Closes gh-35897
2025-11-26 15:06:22 +01:00
rstoyanchev a9a404266c Data binding from pathvars and headers in fn handlers
Closes gh-35800
2025-11-26 12:30:48 +00:00
rstoyanchev b29f4ed37e Minor refactoring in DefaultServerRequest
Ensure bind logic is shared across DefaultServerRequest and
BuiltServerRequest.

See gh-35800
2025-11-26 12:30:48 +00:00
rstoyanchev 4847ee80b0 Add required type to TypeMismatchException message args
Closes gh-35837
2025-11-26 12:30:48 +00:00
Sam Brannen f4ee120a42 Use current links to JUnit documentation
Closes gh-35892
2025-11-26 13:22:26 +01:00
Sam Brannen e625a28f6d Fix formatting for backticks in Kotlin docs 2025-11-26 13:15:56 +01:00
Sam Brannen 45c1cd9295 Link to Spring Framework Artifacts wiki page
This commit revises the Integration Testing chapter to reference the
"Spring Framework Artifacts" wiki page instead of the nonexistent
"Dependency Management" section of the reference manual.

Closes gh-35890
2025-11-26 12:55:01 +01:00
Sébastien Deleuze 2641b5d783 Polishing
See gh-35861
2025-11-26 10:27:01 +01:00
Sébastien Deleuze dc0c463137 Keep using ZoneId.of("GMT") in HttpHeaders
Otherwise some header values are changed from "GMT" to "Z" and
some tests are broken. We don't want to change the current
runtime behavior, so this commit reverts the related change to
keep using ZoneId.of("GMT") in HttpHeaders.

Closes gh-35861
2025-11-26 10:27:00 +01:00
Vincent Potucek 83bbf16e6c Modernize java.time API usages
See gh-35861
Signed-off-by: Vincent Potucek <vpotucek@me.com>
2025-11-26 10:24:34 +01:00
Juergen Hoeller 3686b89ab5 Revise proxyTargetClass handling in ResilientMethodsConfiguration and ProxyAsyncConfiguration
An annotation-specified proxyTargetClass attribute must only be applied when true, otherwise we need to participate in global defaulting.

Closes gh-35863
2025-11-25 18:03:04 +01:00
Sébastien Deleuze 85ca9f46dd Support reading unresolvable types in AbstractJacksonHttpMessageConverter
Closes gh-35889
2025-11-25 16:00:04 +01:00
Sébastien Deleuze e2287020c3 Polishing
See gh-35889
2025-11-25 16:00:04 +01:00
Sébastien Deleuze 9e18c75ff1 Fix ApiVersionConfigurer Kotlin code sample
Closes gh-35887
2025-11-25 14:51:31 +01:00
Sébastien Deleuze fb9f31d101 Update reference documentation to use Jackson 3
Closes gh-35886
2025-11-25 13:31:39 +01:00
Sébastien Deleuze 4ee0a8ee33 Refine Kotlin Serialization codec type checks
ServerSentEvent and String checks, removed from
KotlinSerializationSupport in Spring Framework 7.0, are reintroduced by
this commit at the right level (KotlinSerializationSupport for
ServerSentEvent and KotlinSerializationString(Decoder|Encoder) for
String).

Closes gh-35885
2025-11-25 13:31:39 +01:00
Sébastien Deleuze f3908832c5 Add WebFlux SSE support with GSON
Closes gh-35884
2025-11-25 13:31:39 +01:00
Sébastien Deleuze c10266e57e Fix handling of ServerSentEvent with Jackson encoder
This commit fixes ServerSentEvent handling with Jackson encoder
when no Accept header is specified.

It also moves the String checks to the JSON codec level, as they do not
make sense for binary formats.

Closes gh-35872
2025-11-25 13:31:38 +01:00
Sébastien Deleuze 1b6b163f24 Polishing 2025-11-25 12:37:45 +01:00
github-actions[bot] 8642a39a03 Update Antora Spring UI to v0.4.25
Closes gh-35876

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-11-24 14:44:34 +01:00
github-actions[bot] f62cc98a97 Update Antora Spring UI to v0.4.22
Closes gh-35860
2025-11-24 10:12:11 +01:00
Tran Ngoc Nhan c599775a9e Refine nullability of Assert#noNullElements
Closes gh-35868
Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2025-11-24 09:42:44 +01:00
Brian Clozel 4bc97ad532 Next development version (v7.0.2-SNAPSHOT) 2025-11-20 11:53:27 +01:00
Sam Brannen abec289e9f Stop mentioning non-existent NestedServletException
NestedServletException has been removed from the framework.
2025-11-19 17:32:19 +01:00
Juergen Hoeller 3026f0a49b Lazily initialize ProblemDetail for picking up actual status code
Closes gh-35829
2025-11-19 17:21:32 +01:00
Sam Brannen 9fe4e7798d Fix link to MockMvc test in HtmlUnit section
See gh-35853
2025-11-19 17:17:50 +01:00
Sam Brannen d178930186 Polishing 2025-11-19 17:11:17 +01:00
Tran Ngoc Nhan 91d2a51f3f Fix cross-reference links in HtmlUnit sections
Closes gh-35853

Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2025-11-19 16:53:49 +01:00
Juergen Hoeller f456674529 Polishing 2025-11-19 16:19:14 +01:00
Juergen Hoeller 35b8fbf901 Remove javadoc references to deprecated PropertiesBeanDefinitionReader
Closes gh-35836
2025-11-19 16:18:56 +01:00
Sébastien Deleuze 1d1851f48a Refine RestOperations Kotlin extensions nullability
This commit aligns RestOperationsExtensions.kt nullability with the
Java APIs one, like what has been done in gh-35846 for JdbcOperations.

Closes gh-35852
2025-11-19 16:08:51 +01:00
Sébastien Deleuze 23f0cfb925 Fix JdbcOperations Kotlin extensions
This commit updates JdbcOperationsExtensions.kt to:
 - Properly use the spread operator for invoking Java methods with
   a varargs parameter
 - Align JdbcOperationsExtensions return values nullability
   with the Java API (breaking change)
 - Use varargs where Java counterpart does (breaking change, undo some
   changes from gh-34668)
 - Use nullable args instead of non-nullable ones

 Closes gh-35846
2025-11-19 15:55:00 +01:00
Brian Clozel 2cca56064e Restrict "validateMultiReleaseJar" task to Java 25+
This commit configures the "validateMultiReleaseJar" Gradle task only if
the current Java runtime for Gradle is 25+.

Closes gh-35850
2025-11-19 13:55:27 +01:00
Yanming Zhou e0353fe75f Fix buildSrc:test failing on JDKs before JDK 25 with Non-English locale
See https://github.com/spring-projects/spring-framework/issues/35777#issuecomment-3551591336

Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>
2025-11-19 17:43:12 +08:00
rstoyanchev f80b79bc45 Configure JsonPath in RestTestClient with MappingProvider
Closes gh-35793
2025-11-18 15:46:57 +00:00
rstoyanchev adffd3dcf6 Fix JsonConverterDelegate initialization in RestTestClient
If the RestClient was built with default message converters, then
in mutate, the saved builder also has 0 converters, and adding a
interferes with default registrations.

We need to check if there are no converters at all, and if so
use the default registrations.

See gh-35793
2025-11-18 15:46:57 +00:00
rstoyanchev 284a28659d Add isEmpty() to HttpMessageConverters
See gh-35793
2025-11-18 15:46:57 +00:00
Juergen Hoeller 57a1d4007b Fix getCacheNames() concurrent access in NoOpCacheManager
Closes gh-35842
2025-11-18 13:37:39 +01:00
Juergen Hoeller bc3431f435 Add resetCaches() method to Caffeine/ConcurrentMapCacheManager
Closes gh-35840
2025-11-18 13:37:14 +01:00
Juergen Hoeller de5b9aab55 Narrow Aware interface exclusion check to BeanFactoryAware only
Closes gh-35835
2025-11-18 13:36:29 +01:00
Sébastien Deleuze f72891c24d Upgrade to Gradle 9.2.1
Closes gh-35839
2025-11-18 10:13:56 +01:00
Sam Brannen fac8708857 Migrate CronTriggerTests to @⁠ParameterizedClass
This commit migrates CronTriggerTests to @⁠ParameterizedClass as a proof
of concept for how this can be applied across the code base.

See gh-35833
2025-11-17 16:36:57 +01:00
Sam Brannen 0342cd0904 Merge HtmlCharacterEntityDecoderTests into HtmlUtilsTests
See gh-35711
2025-11-17 15:27:51 +01:00
Brian Clozel e0fd42981a Fix compilation warnings
See gh-35711
2025-11-17 15:24:44 +01:00
Brian Clozel 87d95dc30a Polishing contribution
Closes gh-35477
2025-11-17 15:07:48 +01:00
potato 47de8b05e6 Fix HtmlUtils unescape for supplementary chars
See gh-35477

Signed-off-by: potato <65760583+juntae6942@users.noreply.github.com>
2025-11-17 15:07:42 +01:00
Patrick Strawderman 3b6be3d4d3 Fix single-check idiom in UnmodifiableMultiValueMap
Read the respective fields only once in the values(), entrySet(), and
keySet() methods.

Closes gh-35822

Signed-off-by: Patrick Strawderman <pstrawderman@netflix.com>
2025-11-17 15:02:53 +01:00
github-actions[bot] 6115c3966c Update Antora Spring UI to v0.4.20
Closes gh-35813
2025-11-17 14:23:26 +01:00
Sam Brannen 09a8bbc0c7 Polish contribution
See gh-35817
2025-11-17 12:20:12 +01:00
Patrick Strawderman ed75906834 Fix Spliterator characteristics in ConcurrentReferenceHashMap
The Spliterators returned by values, entrySet, and keySet incorrectly
reported the SIZED characteristic, instead of CONCURRENT. This could
lead to bugs when the map is concurrently modified during a stream
operation.

For keySet and values, the incorrect characteristics are inherited from
AbstractMap, so to rectify that the respective methods are overridden,
and custom collections are provided that report the correct Spliterator
characteristics.

Closes gh-35817

Signed-off-by: Patrick Strawderman <pstrawderman@netflix.com>
2025-11-17 11:55:38 +01:00
Sébastien Deleuze 4756a4be23 Polishing
See gh-35820
2025-11-17 11:33:56 +01:00
Tran Ngoc Nhan 40c5c5d5f0 Add Kotlin code samples for KT-22208
Closes gh-35820
Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2025-11-17 11:33:56 +01:00
Sébastien Deleuze 6b8552b4c1 Refine ParameterizedPreparedStatementSetter nullability
This commit refines ParameterizedPreparedStatementSetter nullability
to ensure consistency with JdbcTemplate#batchUpdate.

Closes gh-35749
2025-11-17 10:31:56 +01:00
Brian Clozel f42eea183e Polishing tests
Closes gh-35678
2025-11-17 09:39:50 +01:00
Sam Brannen ebcfe113d1 Disable flaky failWhileSendingMultipartRequest() test
See gh-35678
2025-11-15 15:55:10 +01:00
Sam Brannen b133c2b346 Document semantics of SpringExtension.getApplicationContext()
Since getApplicationContext() was originally not intended to be part of
the public API, its Javadoc is intentionally sparse. However, since it
is actually a public API used by third parties, this commit improves the
documentation for getApplicationContext() by pointing out that invoking
the method actually results in the context being eagerly loaded, which
may not be desired.

This commit also updates the Javadoc for supportsParameter() along the
same lines.

Closes gh-35764
2025-11-15 15:44:21 +01:00
Sam Brannen 648629fab2 Polish Javadoc for SpringExtension 2025-11-15 15:42:32 +01:00
Sam Brannen f2bf227cd0 Upgrade to testng-engine 1.1.0 2025-11-15 12:42:24 +01:00
Brian Clozel 1260081436 Add WebClient integration test for multipart
This commit adds an integration test for `WebClient`, specifically
testing that a failure happening while pulishing the request body is
reported on the main reactive pipeline.

See gh-35678
2025-11-14 16:34:15 +01:00
Tran Ngoc Nhan 18d8d458e3 Fix typos in tests
Closes gh-35815

Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2025-11-13 13:43:54 +01:00
Brian Clozel 1aa520162b Next development version (v7.0.1-SNAPSHOT) 2025-11-13 11:33:12 +01:00
324 changed files with 5003 additions and 1889 deletions
+1 -1
View File
@@ -38,7 +38,7 @@ bin
.springBeans
spring-*/src/main/java/META-INF/MANIFEST.MF
# IDEA artifacts and output dirs
# IntelliJ IDEA artifacts and output dirs
*.iml
*.ipr
*.iws
+1 -1
View File
@@ -120,7 +120,7 @@ source code into your IDE.
The wiki pages
[Code Style](https://github.com/spring-projects/spring-framework/wiki/Code-Style) and
[IntelliJ IDEA Editor Settings](https://github.com/spring-projects/spring-framework/wiki/IntelliJ-IDEA-Editor-Settings)
define the source file coding standards we use along with some IDEA editor settings we customize.
define the source file coding standards we use along with some IntelliJ editor settings we customize.
### Reference Docs
@@ -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("12.1.2");
checkstyle.setToolVersion("12.2.0");
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
@@ -18,6 +18,7 @@ package org.springframework.build.multirelease;
import javax.inject.Inject;
import org.gradle.api.JavaVersion;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.ConfigurationContainer;
@@ -64,10 +65,12 @@ public class MultiReleaseJarPlugin implements Plugin<Project> {
dependencies,
objects);
TaskProvider<MultiReleaseJarValidateTask> validateJarTask = tasks.register(VALIDATE_JAR_TASK_NAME, MultiReleaseJarValidateTask.class, (task) -> {
task.getJar().set(tasks.named("jar", Jar.class).flatMap(AbstractArchiveTask::getArchiveFile));
task.getJavaLauncher().set(task.getJavaToolchainService().launcherFor(spec -> spec.getLanguageVersion().set(JavaLanguageVersion.of(25))));
});
tasks.named("check", task -> task.dependsOn(validateJarTask));
if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_25)) {
TaskProvider<MultiReleaseJarValidateTask> validateJarTask = tasks.register(VALIDATE_JAR_TASK_NAME, MultiReleaseJarValidateTask.class, (task) -> {
task.getJar().set(tasks.named("jar", Jar.class).flatMap(AbstractArchiveTask::getArchiveFile));
task.getJavaLauncher().set(task.getJavaToolchainService().launcherFor(spec -> spec.getLanguageVersion().set(JavaLanguageVersion.of(25))));
});
tasks.named("check", task -> task.dependsOn(validateJarTask));
}
}
}
@@ -29,6 +29,8 @@ import org.gradle.testkit.runner.GradleRunner;
import org.gradle.testkit.runner.UnexpectedBuildFailure;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
import org.junit.jupiter.api.io.TempDir;
import static org.assertj.core.api.Assertions.assertThat;
@@ -43,10 +45,13 @@ public class MultiReleaseJarPluginTests {
private File buildFile;
private File propertiesFile;
@BeforeEach
void setup(@TempDir File projectDir) {
this.projectDir = projectDir;
this.buildFile = new File(this.projectDir, "build.gradle");
this.propertiesFile = new File(this.projectDir, "gradle.properties");
}
@Test
@@ -119,6 +124,7 @@ public class MultiReleaseJarPluginTests {
}
@Test
@DisabledForJreRange(max = JRE.JAVA_24, disabledReason = "'jar --validate' is available as of Java 25")
void validateJar() throws IOException {
writeBuildFile("""
plugins {
@@ -131,6 +137,9 @@ public class MultiReleaseJarPluginTests {
}
multiRelease { releaseVersions 17 }
""");
writeGradleProperties("""
org.gradle.jvmargs=-Duser.language=en
""");
writeClass("src/main/java17", "Main.java", """
public class Main {
@@ -152,6 +161,12 @@ public class MultiReleaseJarPluginTests {
}
}
private void writeGradleProperties(String properties) throws IOException {
try (PrintWriter out = new PrintWriter(new FileWriter(this.propertiesFile))) {
out.print(properties);
}
}
private void writeClass(String path, String fileName, String fileContent) throws IOException {
Path folder = this.projectDir.toPath().resolve(path);
Files.createDirectories(folder);
+4 -2
View File
@@ -13,7 +13,9 @@ content:
- url: https://github.com/spring-projects/spring-framework
# Refname matching:
# https://docs.antora.org/antora/latest/playbook/content-refname-matching/
branches: ['main', '{6..9}.+({1..9}).x']
# branches: We include snapshots for main, 6.2.x, and 7.0.x to 9.*.x.
branches: ['main', '6.2.x', '{7..9}.+({0..9}).x']
# tags: We effectively include all releases from 6.0.9 to 9.*.*.
tags: ['v{6..9}.+({0..9}).+({0..9})?(-{RC,M}*)', '!(v6.0.{0..8})', '!(v6.0.0-{RC,M}{0..9})']
start_path: framework-docs
asciidoc:
@@ -36,4 +38,4 @@ runtime:
failure_level: warn
ui:
bundle:
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.19/ui-bundle.zip
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.25/ui-bundle.zip
@@ -50,9 +50,8 @@ XML configuration:
The preceding XML is more succinct. However, typos are discovered at runtime rather than
design time, unless you use an IDE (such as https://www.jetbrains.com/idea/[IntelliJ
IDEA] or the {spring-site-tools}[Spring Tools for Eclipse])
that supports automatic property completion when you create bean definitions. Such IDE
assistance is highly recommended.
IDEA] or the {spring-site-tools}[Spring Tools]) that supports automatic property
completion when you create bean definitions. Such IDE assistance is highly recommended.
You can also configure a `java.util.Properties` instance, as follows:
@@ -338,11 +338,11 @@ In the preceding scenario, using `@Autowired` works well and provides the desire
modularity, but determining exactly where the autowired bean definitions are declared is
still somewhat ambiguous. For example, as a developer looking at `ServiceConfig`, how do
you know exactly where the `@Autowired AccountRepository` bean is declared? It is not
explicit in the code, and this may be just fine. Remember that the
{spring-site-tools}[Spring Tools for Eclipse] provides tooling that
can render graphs showing how everything is wired, which may be all you need. Also,
your Java IDE can easily find all declarations and uses of the `AccountRepository` type
and quickly show you the location of `@Bean` methods that return that type.
explicit in the code, and this may be just fine. Note that the
{spring-site-tools}[Spring Tools] IDE support provides tooling that can render graphs
showing how everything is wired, which may be all you need. Also, your Java IDE can
easily find all declarations and uses of the `AccountRepository` type and quickly show
you the location of `@Bean` methods that return that type.
In cases where this ambiguity is not acceptable and you wish to have direct navigation
from within your IDE from one `@Configuration` class to another, consider autowiring the
@@ -7,14 +7,14 @@ similar to the https://jakarta.ee/specifications/expression-language/[Jakarta Ex
Language] but offers additional features, most notably method invocation and basic string
templating functionality.
While there are several other Java expression languages available -- OGNL, MVEL, and JBoss
EL, to name a few -- the Spring Expression Language was created to provide the Spring
community with a single well supported expression language that can be used across all
the products in the Spring portfolio. Its language features are driven by the
requirements of the projects in the Spring portfolio, including tooling requirements
for code completion support within the {spring-site-tools}[Spring Tools for Eclipse].
That said, SpEL is based on a technology-agnostic API that lets other expression language
implementations be integrated, should the need arise.
While there are several other Java expression languages available -- OGNL, MVEL, and
JBoss EL, to name a few -- the Spring Expression Language was created to provide the
Spring community with a single well supported expression language that can be used across
all the products in the Spring portfolio. Its language features are driven by the
requirements of the projects in the Spring portfolio, including tooling requirements for
code completion within the {spring-site-tools}[Spring Tools] IDE support. That said, SpEL
is based on a technology-agnostic API that lets other expression language implementations
be integrated, should the need arise.
While SpEL serves as the foundation for expression evaluation within the Spring
portfolio, it is not directly tied to Spring and can be used independently. To
@@ -319,9 +319,17 @@ progresses.
== Testing
This section addresses testing with the combination of Kotlin and Spring Framework.
The recommended testing framework is https://junit.org/junit5/[JUnit] along with
The recommended testing framework is https://junit.org/[JUnit] along with
https://mockk.io/[Mockk] for mocking.
[TIP]
====
Kotlin lets you specify meaningful test function names between backticks (```).
For a concrete example, see the `+++`Find all users on HTML page`()+++` test function later
in this section.
====
NOTE: If you are using Spring Boot, see
{spring-boot-docs-ref}/features/kotlin.html#features.kotlin.testing[this related documentation].
@@ -352,7 +360,6 @@ file with a `spring.test.constructor.autowire.mode = all` property.
[[per_class-lifecycle]]
=== `PER_CLASS` Lifecycle
Kotlin lets you specify meaningful test function names between backticks (```).
With JUnit Jupiter, Kotlin test classes can use the `@TestInstance(TestInstance.Lifecycle.PER_CLASS)`
annotation to enable single instantiation of test classes, which allows the use of `@BeforeAll`
and `@AfterAll` annotations on non-static methods, which is a good fit for Kotlin.
@@ -237,8 +237,8 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"]
----
RSocketStrategies strategies = RSocketStrategies.builder()
.encoders(encoders -> encoders.add(new Jackson2CborEncoder()))
.decoders(decoders -> decoders.add(new Jackson2CborDecoder()))
.encoders(encoders -> encoders.add(new JacksonCborEncoder()))
.decoders(decoders -> decoders.add(new JacksonCborDecoder()))
.build();
RSocketRequester requester = RSocketRequester.builder()
@@ -251,8 +251,8 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val strategies = RSocketStrategies.builder()
.encoders { it.add(Jackson2CborEncoder()) }
.decoders { it.add(Jackson2CborDecoder()) }
.encoders { it.add(JacksonCborEncoder()) }
.decoders { it.add(JacksonCborDecoder()) }
.build()
val requester = RSocketRequester.builder()
@@ -681,8 +681,8 @@ Java::
@Bean
public RSocketStrategies rsocketStrategies() {
return RSocketStrategies.builder()
.encoders(encoders -> encoders.add(new Jackson2CborEncoder()))
.decoders(decoders -> decoders.add(new Jackson2CborDecoder()))
.encoders(encoders -> encoders.add(new JacksonCborEncoder()))
.decoders(decoders -> decoders.add(new JacksonCborDecoder()))
.routeMatcher(new PathPatternRouteMatcher())
.build();
}
@@ -703,8 +703,8 @@ Kotlin::
@Bean
fun rsocketStrategies() = RSocketStrategies.builder()
.encoders { it.add(Jackson2CborEncoder()) }
.decoders { it.add(Jackson2CborDecoder()) }
.encoders { it.add(JacksonCborEncoder()) }
.decoders { it.add(JacksonCborDecoder()) }
.routeMatcher(PathPatternRouteMatcher())
.build()
}
@@ -187,7 +187,7 @@ default mode may be set via the
xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism.
The default mode may also be configured as a
https://junit.org/junit5/docs/current/user-guide/#running-tests-config-params[JUnit Platform configuration parameter].
https://docs.junit.org/current/user-guide/#running-tests-config-params[JUnit Platform configuration parameter].
If the `spring.test.constructor.autowire.mode` property is not set, test class
constructors will not be automatically autowired.
@@ -2,11 +2,12 @@
= Meta-Annotation Support for Testing
You can use most test-related annotations as
xref:core/beans/classpath-scanning.adoc#beans-meta-annotations[meta-annotations] to create custom composed
annotations and reduce configuration duplication across a test suite.
xref:core/beans/classpath-scanning.adoc#beans-meta-annotations[meta-annotations] to
create custom composed annotations and reduce configuration duplication across a test
suite.
You can use each of the following as a meta-annotation in conjunction with the
xref:testing/testcontext-framework.adoc[TestContext framework].
For example, you can use each of the following as a meta-annotation in conjunction with
the xref:testing/testcontext-framework.adoc[TestContext framework].
* `@BootstrapWith`
* `@ContextConfiguration`
@@ -37,111 +38,7 @@ xref:testing/testcontext-framework.adoc[TestContext framework].
* `@EnabledIf` _(only supported on JUnit Jupiter)_
* `@DisabledIf` _(only supported on JUnit Jupiter)_
Consider the following example:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@RunWith(SpringRunner.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public class OrderRepositoryTests { }
@RunWith(SpringRunner.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public class UserRepositoryTests { }
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@RunWith(SpringRunner::class)
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
@ActiveProfiles("dev")
@Transactional
class OrderRepositoryTests { }
@RunWith(SpringRunner::class)
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
@ActiveProfiles("dev")
@Transactional
class UserRepositoryTests { }
----
======
If we discover that we are repeating the preceding configuration across our JUnit 4-based
test suite, we can reduce the duplication by introducing a custom composed annotation
that centralizes the common test configuration for Spring, as follows:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public @interface TransactionalDevTestConfig { }
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
@ActiveProfiles("dev")
@Transactional
annotation class TransactionalDevTestConfig { }
----
======
Then we can use our custom `@TransactionalDevTestConfig` annotation to simplify the
configuration of individual JUnit 4 based test classes, as follows:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@RunWith(SpringRunner.class)
@TransactionalDevTestConfig
public class OrderRepositoryTests { }
@RunWith(SpringRunner.class)
@TransactionalDevTestConfig
public class UserRepositoryTests { }
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@RunWith(SpringRunner::class)
@TransactionalDevTestConfig
class OrderRepositoryTests
@RunWith(SpringRunner::class)
@TransactionalDevTestConfig
class UserRepositoryTests
----
======
If we write tests that use JUnit Jupiter, we can reduce code duplication even further,
since annotations in JUnit Jupiter can also be used as meta-annotations. Consider the
following example:
Consider the following test classes that use the `SpringExtension` with JUnit Jupiter:
[tabs]
======
@@ -150,13 +47,13 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"]
----
@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ContextConfiguration(classes = {AppConfig.class, TestDataAccessConfig.class})
@ActiveProfiles("dev")
@Transactional
class OrderRepositoryTests { }
@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ContextConfiguration(classes = {AppConfig.class, TestDataAccessConfig.class})
@ActiveProfiles("dev")
@Transactional
class UserRepositoryTests { }
@@ -167,23 +64,22 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@ExtendWith(SpringExtension::class)
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
@ContextConfiguration(classes = [AppConfig::class, TestDataAccessConfig::class])
@ActiveProfiles("dev")
@Transactional
class OrderRepositoryTests { }
@ExtendWith(SpringExtension::class)
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
@ContextConfiguration(classes = [AppConfig::class, TestDataAccessConfig::class])
@ActiveProfiles("dev")
@Transactional
class UserRepositoryTests { }
----
======
If we discover that we are repeating the preceding configuration across our JUnit
Jupiter-based test suite, we can reduce the duplication by introducing a custom composed
annotation that centralizes the common test configuration for Spring and JUnit Jupiter,
as follows:
If we discover that we are repeating the preceding configuration across our test suite,
we can reduce the duplication by introducing a custom composed annotation that
centralizes the common test configuration for Spring and JUnit Jupiter, as follows:
[tabs]
======
@@ -194,7 +90,7 @@ Java::
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ContextConfiguration(classes = {AppConfig.class, TestDataAccessConfig.class})
@ActiveProfiles("dev")
@Transactional
public @interface TransactionalDevTestConfig { }
@@ -207,7 +103,7 @@ Kotlin::
@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@ExtendWith(SpringExtension::class)
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
@ContextConfiguration(classes = [AppConfig::class, TestDataAccessConfig::class])
@ActiveProfiles("dev")
@Transactional
annotation class TransactionalDevTestConfig { }
@@ -5,24 +5,25 @@ It is important to be able to perform some integration testing without requiring
deployment to your application server or connecting to other enterprise infrastructure.
Doing so lets you test things such as:
* The correct wiring of your Spring IoC container contexts.
* Data access using JDBC or an ORM tool. This can include such things as the correctness
of SQL statements, Hibernate queries, JPA entity mappings, and so forth.
* The correct wiring of your Spring components.
* Data access using JDBC or an ORM tool.
** This can include such things as the correctness of SQL statements, Hibernate queries,
JPA entity mappings, and so forth.
The Spring Framework provides first-class support for integration testing in the
`spring-test` module. The name of the actual JAR file might include the release version
and might also be in the long `org.springframework.test` form, depending on where you get
it from (see the xref:core/beans/dependencies.adoc[section on Dependency Management]
for an explanation). This library includes the `org.springframework.test` package, which
`spring-test` module. The name of the actual JAR file might include the release version,
depending on where you get it from (see the
{spring-framework-wiki}/Spring-Framework-Artifacts[Spring Framework Artifacts] wiki page
for details). This library includes the `org.springframework.test` package, which
contains valuable classes for integration testing with a Spring container. This testing
does not rely on an application server or other deployment environment. Such tests are
slower to run than unit tests but much faster than the equivalent Selenium tests or
remote tests that rely on deployment to an application server.
Unit and integration testing support is provided in the form of the annotation-driven
xref:testing/testcontext-framework.adoc[Spring TestContext Framework]. The TestContext framework is
agnostic of the actual testing framework in use, which allows instrumentation of tests
in various environments, including JUnit, TestNG, and others.
xref:testing/testcontext-framework.adoc[Spring TestContext Framework]. The TestContext
framework is agnostic of the actual testing framework in use, which allows
instrumentation of tests in various environments, including JUnit, TestNG, and others.
The following section provides an overview of the high-level goals of Spring's
integration support, and the rest of this chapter then focuses on dedicated topics:
@@ -184,7 +184,10 @@ Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
// Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed
standaloneSetup(SimpleController())
.alwaysExpect<StandaloneMockMvcBuilder>(status().isOk())
.alwaysExpect<StandaloneMockMvcBuilder>(content().contentType("application/json;charset=UTF-8"))
.build()
----
======
@@ -18,7 +18,7 @@ Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
// Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed
mockMvc = standaloneSetup(PersonController()).addFilters<StandaloneMockMvcBuilder>(CharacterEncodingFilter()).build()
----
======
@@ -159,7 +159,18 @@ Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
// Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed
class MyWebTests {
lateinit var mockMvc: MockMvc
@BeforeEach
fun setup() {
mockMvc = standaloneSetup(AccountController())
.defaultRequest<StandaloneMockMvcBuilder>(get("/")
.contextPath("/app").servletPath("/main")
.accept(MediaType.APPLICATION_JSON)).build()
}
}
----
======
@@ -25,7 +25,13 @@ Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
// Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed
// static import of MockMvcBuilders.standaloneSetup
val mockMvc = standaloneSetup(MusicController())
.defaultRequest<StandaloneMockMvcBuilder>(get("/").accept(MediaType.APPLICATION_JSON))
.alwaysExpect<StandaloneMockMvcBuilder>(status().isOk())
.alwaysExpect<StandaloneMockMvcBuilder>(content().contentType("application/json;charset=UTF-8"))
.build()
----
======
@@ -53,7 +59,13 @@ Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
// Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed
// static import of SharedHttpSessionConfigurer.sharedHttpSession
val mockMvc = MockMvcBuilders.standaloneSetup(TestController())
.apply<StandaloneMockMvcBuilder>(sharedHttpSession())
.build()
// Use mockMvc to perform requests...
----
======
@@ -8,9 +8,9 @@ use https://www.gebish.org/[Geb] to make our tests even Groovy-er.
== Why Geb and MockMvc?
Geb is backed by WebDriver, so it offers many of the
xref:testing/mockmvc/htmlunit/webdriver.adoc#spring-mvc-test-server-htmlunit-webdriver-why[same benefits] that we get from
WebDriver. However, Geb makes things even easier by taking care of some of the
boilerplate code for us.
xref:testing/mockmvc/htmlunit/webdriver.adoc#mockmvc-server-htmlunit-webdriver-why[same benefits]
that we get from WebDriver. However, Geb makes things even easier by taking care of some
of the boilerplate code for us.
[[mockmvc-server-htmlunit-geb-setup]]
== MockMvc and Geb Setup
@@ -28,7 +28,8 @@ def setup() {
----
NOTE: This is a simple example of using `MockMvcHtmlUnitDriverBuilder`. For more advanced
usage, see xref:testing/mockmvc/htmlunit/webdriver.adoc#spring-mvc-test-server-htmlunit-webdriver-advanced-builder[Advanced `MockMvcHtmlUnitDriverBuilder`].
usage, see
xref:testing/mockmvc/htmlunit/webdriver.adoc#mockmvc-server-htmlunit-webdriver-advanced-builder[Advanced `MockMvcHtmlUnitDriverBuilder`].
This ensures that any URL referencing `localhost` as the server is directed to our
`MockMvc` instance without the need for a real HTTP connection. Any other URL is
@@ -62,10 +63,10 @@ forwarded to the current page object. This removes a lot of the boilerplate code
needed when using WebDriver directly.
As with direct WebDriver usage, this improves on the design of our
xref:testing/mockmvc/htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-usage[HtmlUnit test] by using the Page Object
Pattern. As mentioned previously, we can use the Page Object Pattern with HtmlUnit and
WebDriver, but it is even easier with Geb. Consider our new Groovy-based
`CreateMessagePage` implementation:
xref:testing/mockmvc/htmlunit/mah.adoc#mockmvc-server-htmlunit-mah-usage[HtmlUnit test]
by using the Page Object Pattern. As mentioned previously, we can use the Page Object
Pattern with HtmlUnit and WebDriver, but it is even easier with Geb. Consider our new
Groovy-based `CreateMessagePage` implementation:
[source,groovy]
----
@@ -7,8 +7,7 @@ to use the raw HtmlUnit libraries.
[[mockmvc-server-htmlunit-mah-setup]]
== MockMvc and HtmlUnit Setup
First, make sure that you have included a test dependency on
`org.htmlunit:htmlunit`.
First, make sure that you have included a test dependency on `org.htmlunit:htmlunit`.
We can easily create an HtmlUnit `WebClient` that integrates with MockMvc by using the
`MockMvcWebClientBuilder`, as follows:
@@ -45,7 +44,7 @@ Kotlin::
======
NOTE: This is a simple example of using `MockMvcWebClientBuilder`. For advanced usage,
see xref:testing/mockmvc/htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-advanced-builder[Advanced `MockMvcWebClientBuilder`].
see <<mockmvc-server-htmlunit-mah-advanced-builder>>.
This ensures that any URL that references `localhost` as the server is directed to our
`MockMvc` instance without the need for a real HTTP connection. Any other URL is
@@ -77,7 +76,7 @@ Kotlin::
======
NOTE: The default context path is `""`. Alternatively, we can specify the context path,
as described in xref:testing/mockmvc/htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-advanced-builder[Advanced `MockMvcWebClientBuilder`].
as described in <<mockmvc-server-htmlunit-mah-advanced-builder>>.
Once we have a reference to the `HtmlPage`, we can then fill out the form and submit it
to create a message, as the following example shows:
@@ -144,10 +143,10 @@ Kotlin::
======
The preceding code improves on our
xref:testing/mockmvc/htmlunit/why.adoc#spring-mvc-test-server-htmlunit-mock-mvc-test[MockMvc test] in a number of ways.
First, we no longer have to explicitly verify our form and then create a request that
looks like the form. Instead, we request the form, fill it out, and submit it, thereby
significantly reducing the overhead.
xref:testing/mockmvc/htmlunit/why.adoc#mockmvc-server-htmlunit-why[MockMvc test] in a
number of ways. First, we no longer have to explicitly verify our form and then create a
request that looks like the form. Instead, we request the form, fill it out, and submit
it, thereby significantly reducing the overhead.
Another important factor is that https://htmlunit.sourceforge.io/javascript.html[HtmlUnit
uses the Mozilla Rhino engine] to evaluate JavaScript. This means that we can also test
@@ -267,7 +266,19 @@ Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
// Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed
val mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply<DefaultMockMvcBuilder>(springSecurity())
.build()
webClient = MockMvcWebClientBuilder
.mockMvcSetup(mockMvc)
// for illustration only - defaults to ""
.contextPath("")
// By default MockMvc is used for localhost only;
// the following will use MockMvc for example.com and example.org as well
.useMockMvcForHosts("example.com", "example.org")
.build()
----
======
@@ -203,7 +203,7 @@ Kotlin::
======
NOTE: This is a simple example of using `MockMvcHtmlUnitDriverBuilder`. For more advanced
usage, see xref:testing/mockmvc/htmlunit/webdriver.adoc#spring-mvc-test-server-htmlunit-webdriver-advanced-builder[Advanced `MockMvcHtmlUnitDriverBuilder`].
usage, see <<mockmvc-server-htmlunit-webdriver-advanced-builder>>.
The preceding example ensures that any URL that references `localhost` as the server is
directed to our `MockMvc` instance without the need for a real HTTP connection. Any other
@@ -259,10 +259,11 @@ Kotlin::
======
--
This improves on the design of our xref:testing/mockmvc/htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-usage[HtmlUnit test]
This improves on the design of our
xref:testing/mockmvc/htmlunit/mah.adoc#mockmvc-server-htmlunit-mah-usage[HtmlUnit test]
by leveraging the Page Object Pattern. As we mentioned in
xref:testing/mockmvc/htmlunit/webdriver.adoc#mockmvc-server-htmlunit-webdriver-why[Why WebDriver and MockMvc?], we can use the Page Object Pattern
with HtmlUnit, but it is much easier with WebDriver. Consider the following
<<mockmvc-server-htmlunit-webdriver-why>>, we can use the Page Object Pattern with
HtmlUnit, but it is much easier with WebDriver. Consider the following
`CreateMessagePage` implementation:
--
@@ -562,7 +563,19 @@ Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
// Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed
val mockMvc: MockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply<DefaultMockMvcBuilder>(springSecurity())
.build()
driver = MockMvcHtmlUnitDriverBuilder
.mockMvcSetup(mockMvc)
// for illustration only - defaults to ""
.contextPath("")
// By default MockMvc is used for localhost only;
// the following will use MockMvc for example.com and example.org as well
.useMockMvcForHosts("example.com", "example.org")
.build()
----
======
@@ -60,7 +60,7 @@ assume our form looks like the following snippet:
</form>
----
How do we ensure that our form produce the correct request to create a new message? A
How do we ensure that our form produces the correct request to create a new message? A
naive attempt might resemble the following:
[tabs]
@@ -154,7 +154,7 @@ validation.
[[mockmvc-server-htmlunit-why-integration]]
== Integration Testing to the Rescue?
To resolve the issues mentioned earlier, we could perform end-to-end integration testing,
To resolve the issues mentioned above, we could perform end-to-end integration testing,
but this has some drawbacks. Consider testing the view that lets us page through the
messages. We might need the following tests:
@@ -171,7 +171,7 @@ leads to a number of additional challenges:
* Testing can become slow, since each test would need to ensure that the database is in
the correct state.
* Since our database needs to be in a specific state, we cannot run tests in parallel.
* Performing assertions on such items as auto-generated IDs, timestamps, and others can
* Performing assertions on items such as auto-generated IDs, timestamps, and others can
be difficult.
These challenges do not mean that we should abandon end-to-end integration testing
@@ -8,12 +8,15 @@ in JUnit and TestNG.
[[testcontext-junit-jupiter-extension]]
== SpringExtension for JUnit Jupiter
The Spring TestContext Framework offers full integration with the JUnit Jupiter testing
framework, originally introduced in JUnit 5. By annotating test classes with
`@ExtendWith(SpringExtension.class)`, you can implement standard JUnit Jupiter-based unit
and integration tests and simultaneously reap the benefits of the TestContext framework,
such as support for loading application contexts, dependency injection of test instances,
transactional test method execution, and so on.
The `SpringExtension` integrates the Spring TestContext Framework into the JUnit Jupiter
testing framework.
NOTE: As of Spring Framework 7.0, the `SpringExtension` requires JUnit Jupiter 6.0 or higher.
By annotating test classes with `@ExtendWith(SpringExtension.class)`, you can implement
standard JUnit Jupiter-based unit and integration tests and simultaneously reap the
benefits of the TestContext framework, such as support for loading application contexts,
dependency injection of test instances, transactional test method execution, and so on.
Furthermore, thanks to the rich extension API in JUnit Jupiter, Spring provides the
following features above and beyond the feature set that Spring supports for JUnit 4 and
@@ -22,7 +25,7 @@ TestNG:
* Dependency injection for test constructors, test methods, and test lifecycle callback
methods. See xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-di[Dependency
Injection with the `SpringExtension`] for further details.
* Powerful support for link:https://junit.org/junit5/docs/current/user-guide/#extensions-conditions[conditional
* Powerful support for link:https://docs.junit.org/current/user-guide/#extensions-conditions[conditional
test execution] based on SpEL expressions, environment variables, system properties,
and so on. See the documentation for `@EnabledIf` and `@DisabledIf` in
xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations]
@@ -160,7 +163,7 @@ for further details.
=== Dependency Injection with the `SpringExtension`
The `SpringExtension` implements the
link:https://junit.org/junit5/docs/current/user-guide/#extensions-parameter-resolution[`ParameterResolver`]
link:https://docs.junit.org/current/user-guide/#extensions-parameter-resolution[`ParameterResolver`]
extension API from JUnit Jupiter, which lets Spring provide dependency injection for test
constructors, test methods, and test lifecycle callback methods.
@@ -362,7 +362,7 @@ of `PlatformTransactionManager` within the test's `ApplicationContext`, you can
qualifier by using `@Transactional("myTxMgr")` or `@Transactional(transactionManager =
"myTxMgr")`, or `TransactionManagementConfigurer` can be implemented by an
`@Configuration` class. Consult the
{spring-framework-api}/test/context/transaction/TestContextTransactionUtils.html#retrieveTransactionManager-org.springframework.test.context.TestContext-java.lang.String-[javadoc
{spring-framework-api}/test/context/transaction/TestContextTransactionUtils.html#retrieveTransactionManager(org.springframework.test.context.TestContext,java.lang.String)[javadoc
for `TestContextTransactionUtils.retrieveTransactionManager()`] for details on the
algorithm used to look up a transaction manager in the test's `ApplicationContext`.
@@ -75,7 +75,7 @@ infrastructure and controller declarations and use it to handle requests via moc
and response objects, without a running server.
For WebFlux, use the following where the Spring `ApplicationContext` is passed to
{spring-framework-api}/web/server/adapter/WebHttpHandlerBuilder.html#applicationContext-org.springframework.context.ApplicationContext-[WebHttpHandlerBuilder]
{spring-framework-api}/web/server/adapter/WebHttpHandlerBuilder.html#applicationContext(org.springframework.context.ApplicationContext)[WebHttpHandlerBuilder]
to create the xref:web/webflux/reactive-spring.adoc#webflux-web-handler-api[WebHandler chain] to handle
requests:
@@ -44,7 +44,7 @@ rejected. No CORS headers are added to the responses of simple and actual CORS r
and, consequently, browsers reject them.
Each `HandlerMapping` can be
{spring-framework-api}/web/reactive/handler/AbstractHandlerMapping.html#setCorsConfigurations-java.util.Map-[configured]
{spring-framework-api}/web/reactive/handler/AbstractHandlerMapping.html#setCorsConfigurations(java.util.Map)[configured]
individually with URL pattern-based `CorsConfiguration` mappings. In most cases, applications
use the WebFlux Java configuration to declare such mappings, which results in a single,
global map passed to all `HandlerMapping` implementations.
@@ -57,7 +57,7 @@ class- or method-level `@CrossOrigin` annotations (other handlers can implement
The rules for combining global and local configuration are generally additive -- for example,
all global and all local origins. For those attributes where only a single value can be
accepted, such as `allowCredentials` and `maxAge`, the local overrides the global value. See
{spring-framework-api}/web/cors/CorsConfiguration.html#combine-org.springframework.web.cors.CorsConfiguration-[`CorsConfiguration#combine(CorsConfiguration)`]
{spring-framework-api}/web/cors/CorsConfiguration.html#combine(org.springframework.web.cors.CorsConfiguration)[`CorsConfiguration#combine(CorsConfiguration)`]
for more details.
[TIP]
@@ -123,7 +123,7 @@ Both request and response provide {reactive-streams-site}[Reactive Streams] back
against the body streams.
The request body is represented with a Reactor `Flux` or `Mono`.
The response body is represented with any Reactive Streams `Publisher`, including `Flux` and `Mono`.
For more on that, see xref:web-reactive.adoc#webflux-reactive-libraries[Reactive Libraries].
For more on that, see xref:web/webflux-reactive-libraries.adoc[Reactive Libraries].
[[webflux-fn-request]]
=== ServerRequest
@@ -296,7 +296,8 @@ allPartsEvents.windowUntil(PartEvent::isLast)
NOTE: The body contents of the `PartEvent` objects must be completely consumed, relayed, or released to avoid memory leaks.
The following shows how to bind request parameters, including an optional `DataBinder` customization:
The following shows how to bind request parameters, URI variables, or headers via `DataBinder`,
and also shows how to customize the `DataBinder`:
[tabs]
======
@@ -374,14 +375,14 @@ Java::
+
[source,java]
----
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
ServerResponse.ok().hint(JacksonCodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
----
Kotlin::
+
[source,kotlin]
----
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).body(...)
ServerResponse.ok().hint(JacksonCodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).body(...)
----
======
@@ -482,7 +482,7 @@ purposes, it is useful to be able to alternate between rendering a model with an
or as other formats (such as JSON or XML), depending on the content type requested by the client.
To support doing so, Spring WebFlux provides the `HttpMessageWriterView`, which you can use to
plug in any of the available xref:web/webflux/reactive-spring.adoc#webflux-codecs[Codecs] from
`spring-web`, such as `Jackson2JsonEncoder`, `Jackson2SmileEncoder`, or `Jaxb2XmlEncoder`.
`spring-web`, such as `JacksonJsonEncoder`, `JacksonSmileEncoder`, or `Jaxb2XmlEncoder`.
Unlike other view technologies, `HttpMessageWriterView` does not require a `ViewResolver` but is
instead xref:web/webflux/config.adoc#webflux-config-view-resolvers[configured] as a default view.
@@ -135,6 +135,10 @@ Message codes and arguments for each error are also resolved via `MessageSource`
| `+{0}+` the list of global errors, `+{1}+` the list of field errors.
Message codes and arguments for each error are also resolved via `MessageSource`.
| `NoResourceFoundException`
| (default)
| `+{0}+` the request path (or portion of) used to find a resource
|===
NOTE: Unlike other exceptions, the message arguments for
@@ -334,19 +334,8 @@ Kotlin::
`ServerCodecConfigurer` provides a set of default readers and writers. You can use it to add
more readers and writers, customize the default ones, or replace the default ones completely.
For Jackson JSON and XML, consider using
{spring-framework-api}/http/converter/json/Jackson2ObjectMapperBuilder.html[`Jackson2ObjectMapperBuilder`],
which customizes Jackson's default properties with the following ones:
* {jackson-docs}/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/DeserializationFeature.html#FAIL_ON_UNKNOWN_PROPERTIES[`DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES`] is disabled.
* {jackson-docs}/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/MapperFeature.html#DEFAULT_VIEW_INCLUSION[`MapperFeature.DEFAULT_VIEW_INCLUSION`] is disabled.
It also automatically registers the following well-known modules if they are detected on the classpath:
* {jackson-github-org}/jackson-datatype-jsr310[`jackson-datatype-jsr310`]: Support for Java 8 Date and Time API types.
* {jackson-github-org}/jackson-datatype-jdk8[`jackson-datatype-jdk8`]: Support for other Java 8 types, such as `Optional`.
* {jackson-github-org}/jackson-module-kotlin[`jackson-module-kotlin`]: Support for Kotlin classes and data classes.
For Jackson, consider using a Jackson format-specific builder like `JsonMapper.Builder` to configure Jackson's default
properties.
[[webflux-config-view-resolvers]]
== View Resolvers
@@ -489,7 +478,7 @@ Java::
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.freeMarker();
Jackson2JsonEncoder encoder = new Jackson2JsonEncoder();
JacksonJsonEncoder encoder = new JacksonJsonEncoder();
registry.defaultViews(new HttpMessageWriterView(encoder));
}
@@ -508,7 +497,7 @@ Kotlin::
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.freeMarker()
val encoder = Jackson2JsonEncoder()
val encoder = JacksonJsonEncoder()
registry.defaultViews(HttpMessageWriterView(encoder))
}
@@ -701,7 +690,7 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim"]
----
@Configuration
class WebConfiguration : WebMvcConfigurer {
class WebConfiguration : WebFluxConfigurer {
override fun configureApiVersioning(configurer: ApiVersionConfigurer) {
configurer.useRequestHeader("API-Version")
@@ -5,7 +5,7 @@
The following table shows the supported controller method arguments.
Reactive types (Reactor, RxJava, xref:web-reactive.adoc#webflux-reactive-libraries[or other]) are
Reactive types (Reactor, RxJava, xref:web/webflux-reactive-libraries.adoc[or other]) are
supported on arguments that require blocking I/O (for example, reading the request body) to
be resolved. This is marked in the Description column. Reactive types are not expected
on arguments that do not require blocking.
@@ -114,6 +114,6 @@ and others) and is equivalent to `required=false`.
| Any other argument
| If a method argument is not matched to any of the above, it is, by default, resolved as
a `@RequestParam` if it is a simple type, as determined by
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty],
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty],
or as a `@ModelAttribute`, otherwise.
|===
@@ -205,7 +205,7 @@ controller method xref:web/webmvc/mvc-controller/ann-validation.adoc[Validation]
TIP: Using `@ModelAttribute` is optional. By default, any argument that is not a simple
value type as determined by
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty]
_AND_ that is not resolved by any other argument resolver is treated as an implicit `@ModelAttribute`.
WARNING: When compiling to a native image with GraalVM, the implicit `@ModelAttribute`
@@ -74,6 +74,6 @@ When a `@RequestParam` annotation is declared on a `Map<String, String>` or
Note that use of `@RequestParam` is optional -- for example, to set its attributes. By
default, any argument that is a simple value type (as determined by
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty])
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty])
and is not resolved by any other argument resolver is treated as if it were annotated
with `@RequestParam`.
@@ -33,7 +33,7 @@ Kotlin::
----
======
WebFlux supports using a single value xref:web-reactive.adoc#webflux-reactive-libraries[reactive type] to
WebFlux supports using a single value xref:web/webflux-reactive-libraries.adoc[reactive type] to
produce the `ResponseEntity` asynchronously, and/or single and multi-value reactive types
for the body. This allows a variety of async responses with `ResponseEntity` as follows:
@@ -87,6 +87,6 @@ Reactor provides a dedicated operator for that, `Flux#collectList()`.
| Other return values
| If a return value remains unresolved in any other way, it is treated as a model
attribute, unless it is a simple type as determined by
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty],
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty],
in which case it remains unresolved.
|===
@@ -207,7 +207,7 @@ was not provided (for example, model attribute was returned) or an async return
view resolution scenarios. Explore the options in your IDE with code completion.
* `Model`, `Map`: Extra model attributes to be added to the model for the request.
* Any other: Any other return value (except for simple types, as determined by
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty])
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty])
is treated as a model attribute to be added to the model. The attribute name is derived
from the class name by using {spring-framework-api}/core/Conventions.html[conventions],
unless a handler method `@ModelAttribute` annotation is present.
@@ -77,7 +77,7 @@ as input, adapts it to a Reactor type internally, uses that, and returns either
`Flux` or a `Mono` as output. So, you can pass any `Publisher` as input and you can apply
operations on the output, but you need to adapt the output for use with another reactive library.
Whenever feasible (for example, annotated controllers), WebFlux adapts transparently to the use
of RxJava or another reactive library. See xref:web-reactive.adoc#webflux-reactive-libraries[Reactive Libraries] for more details.
of RxJava or another reactive library. See xref:web/webflux-reactive-libraries.adoc[Reactive Libraries] for more details.
NOTE: In addition to Reactive APIs, WebFlux can also be used with
xref:languages/kotlin/coroutines.adoc[Coroutines] APIs in Kotlin which provides a more imperative style of programming.
@@ -148,7 +148,7 @@ RxJava to perform blocking calls on a separate thread but you would not be makin
most of a non-blocking web stack.
* If you have a Spring MVC application with calls to remote services, try the reactive `WebClient`.
You can return reactive types (Reactor, RxJava, xref:web-reactive.adoc#webflux-reactive-libraries[or other])
You can return reactive types (Reactor, RxJava, xref:web/webflux-reactive-libraries.adoc[or other])
directly from Spring MVC controller methods. The greater the latency per call or the
interdependency among calls, the more dramatic the benefits. Spring MVC controllers
can call other reactive components too.
@@ -459,22 +459,22 @@ xref:web/webflux/config.adoc#webflux-config-message-codecs[HTTP message codecs].
JSON and binary JSON ({jackson-github-org}/smile-format-specification[Smile]) are
both supported when the Jackson library is present.
The `Jackson2Decoder` works as follows:
The `JacksonJsonDecoder` works as follows:
* Jackson's asynchronous, non-blocking parser is used to aggregate a stream of byte chunks
into ``TokenBuffer``'s each representing a JSON object.
* Each `TokenBuffer` is passed to Jackson's `ObjectMapper` to create a higher level object.
* Each `TokenBuffer` is passed to Jackson's `JsonMapper` to create a higher level object.
* When decoding to a single-value publisher (for example, `Mono`), there is one `TokenBuffer`.
* When decoding to a multi-value publisher (for example, `Flux`), each `TokenBuffer` is passed to
the `ObjectMapper` as soon as enough bytes are received for a fully formed object. The
the `JsonMapper` as soon as enough bytes are received for a fully formed object. The
input content can be a JSON array, or any
https://en.wikipedia.org/wiki/JSON_streaming[line-delimited JSON] format such as NDJSON,
JSON Lines, or JSON Text Sequences.
The `Jackson2Encoder` works as follows:
The `JacksonJsonEncoder` works as follows:
* For a single value publisher (for example, `Mono`), simply serialize it through the
`ObjectMapper`.
`JsonMapper`.
* For a multi-value publisher with `application/json`, by default collect the values with
`Flux#collectToList()` and then serialize the resulting collection.
* For a multi-value publisher with a streaming media type such as
@@ -482,12 +482,12 @@ The `Jackson2Encoder` works as follows:
flush each value individually using a
https://en.wikipedia.org/wiki/JSON_streaming[line-delimited JSON] format. Other
streaming media types may be registered with the encoder.
* For SSE the `Jackson2Encoder` is invoked per event and the output is flushed to ensure
* For SSE the `JacksonJsonEncoder` is invoked per event and the output is flushed to ensure
delivery without delay.
[NOTE]
====
By default both `Jackson2Encoder` and `Jackson2Decoder` do not support elements of type
By default both `JacksonJsonEncoder` and `JacksonJsonDecoder` do not support elements of type
`String`. Instead the default assumption is that a string or a sequence of strings
represent serialized JSON content, to be rendered by the `CharSequenceEncoder`. If what
you need is to render a JSON array from `Flux<String>`, use `Flux#collectToList()` and
@@ -71,7 +71,7 @@ rejected. No CORS headers are added to the responses of simple and actual CORS r
and, consequently, browsers reject them.
Each `HandlerMapping` can be
{spring-framework-api}/web/servlet/handler/AbstractHandlerMapping.html#setCorsConfigurations-java.util.Map-[configured]
{spring-framework-api}/web/servlet/handler/AbstractHandlerMapping.html#setCorsConfigurations(java.util.Map)[configured]
individually with URL pattern-based `CorsConfiguration` mappings. In most cases, applications
use the MVC Java configuration or the XML namespace to declare such mappings, which results
in a single global map being passed to all `HandlerMapping` instances.
@@ -84,7 +84,7 @@ class- or method-level `@CrossOrigin` annotations (other handlers can implement
The rules for combining global and local configuration are generally additive -- for example,
all global and all local origins. For those attributes where only a single value can be
accepted, for example, `allowCredentials` and `maxAge`, the local overrides the global value. See
{spring-framework-api}/web/cors/CorsConfiguration.html#combine-org.springframework.web.cors.CorsConfiguration-[`CorsConfiguration#combine(CorsConfiguration)`]
{spring-framework-api}/web/cors/CorsConfiguration.html#combine(org.springframework.web.cors.CorsConfiguration)[`CorsConfiguration#combine(CorsConfiguration)`]
for more details.
[TIP]
@@ -184,7 +184,8 @@ val map = request.params()
----
======
The following shows how to bind request parameters, including an optional `DataBinder` customization:
The following shows how to bind request parameters, URI variables, or headers via `DataBinder`,
and also shows how to customize the `DataBinder`:
[tabs]
======
@@ -10,7 +10,7 @@ Spring offers support for the Jackson JSON library.
== Jackson-based JSON MVC Views
[.small]#xref:web/webflux-view.adoc#webflux-view-httpmessagewriter[See equivalent in the Reactive stack]#
The `MappingJackson2JsonView` uses the Jackson library's `ObjectMapper` to render the response
The `JacksonJsonView` uses the Jackson library's `JsonMapper` to render the response
content as JSON. By default, the entire contents of the model map (with the exception of
framework-specific classes) are encoded as JSON. For cases where the contents of the
map need to be filtered, you can specify a specific set of model attributes to encode
@@ -18,17 +18,17 @@ by using the `modelKeys` property. You can also use the `extractValueFromSingleK
property to have the value in single-key models extracted and serialized directly rather
than as a map of model attributes.
You can customize JSON mapping as needed by using Jackson's provided
annotations. When you need further control, you can inject a custom `ObjectMapper`
through the `ObjectMapper` property, for cases where you need to provide custom JSON
serializers and deserializers for specific types.
You can customize JSON mapping as needed by using Jackson's provided annotations. When
you need further control, you can inject a custom `JsonMapper` through the `JsonMapper`
or `JsonMapper.Builder` constructor parameters, for cases where you need to provide
custom JSON serializers and deserializers for specific types.
[[mvc-view-xml-mapping]]
== Jackson-based XML Views
[.small]#xref:web/webflux-view.adoc#webflux-view-httpmessagewriter[See equivalent in the Reactive stack]#
`MappingJackson2XmlView` uses the
`JacksonXmlView` uses the
{jackson-github-org}/jackson-dataformat-xml[Jackson XML extension's] `XmlMapper`
to render the response content as XML. If the model contains multiple entries, you should
explicitly set the object to be serialized by using the `modelKey` bean property. If the
@@ -36,5 +36,5 @@ model contains a single entry, it is serialized automatically.
You can customize XML mapping as needed by using JAXB or Jackson's provided
annotations. When you need further control, you can inject a custom `XmlMapper`
through the `ObjectMapper` property, for cases where custom XML
you need to provide serializers and deserializers for specific types.
created via `XmlMapper.Builder` for cases where custom XML you need to provide
serializers and deserializers for specific types.
@@ -42,25 +42,25 @@ This converter requires a `Marshaller` and `Unmarshaller` before it can be used.
You can inject these through constructor or bean properties.
By default, this converter supports `text/xml` and `application/xml`.
| `MappingJackson2HttpMessageConverter`
| An `HttpMessageConverter` implementation that can read and write JSON by using Jackson's `ObjectMapper`.
| `JacksonJsonHttpMessageConverter`
| An `HttpMessageConverter` implementation that can read and write JSON by using Jackson's `JsonMapper`.
You can customize JSON mapping as needed through the use of Jackson's provided annotations.
When you need further control (for cases where custom JSON serializers/deserializers need to be provided for specific types), you can inject a custom `ObjectMapper` through the `ObjectMapper` property.
By default, this converter supports `application/json`. This requires the `com.fasterxml.jackson.core:jackson-databind` dependency.
When you need further control (for cases where custom JSON serializers/deserializers need to be provided for specific types), you can inject a custom `JsonMapper` through the `JsonMapper` or `JsonMapper.Builder ` constructor parameters.
By default, this converter supports `application/json`. This requires the `tools.jackson.core:jackson-databind` dependency.
| `MappingJackson2XmlHttpMessageConverter`
| `JacksonXmlHttpMessageConverter`
| An `HttpMessageConverter` implementation that can read and write XML by using {jackson-github-org}/jackson-dataformat-xml[Jackson XML] extension's `XmlMapper`.
You can customize XML mapping as needed through the use of JAXB or Jackson's provided annotations.
When you need further control (for cases where custom XML serializers/deserializers need to be provided for specific types), you can inject a custom `XmlMapper` through the `ObjectMapper` property.
By default, this converter supports `application/xml`. This requires the `com.fasterxml.jackson.dataformat:jackson-dataformat-xml` dependency.
When you need further control (for cases where custom XML serializers/deserializers need to be provided for specific types), you can inject a custom `XmlMapper` through the `JsonMapper` or `JsonMapper.Builder` constructor parameters.
By default, this converter supports `application/xml`. This requires the `tools.jackson.dataformat:jackson-dataformat-xml` dependency.
| `KotlinSerializationJsonHttpMessageConverter`
| An `HttpMessageConverter` implementation that can read and write JSON using `kotlinx.serialization`.
This converter is not configured by default, as this conflicts with Jackson.
Developers must configure it as an additional converter ahead of the Jackson one.
| `MappingJackson2CborHttpMessageConverter`
| `com.fasterxml.jackson.dataformat:jackson-dataformat-cbor`
| `JacksonCborHttpMessageConverter`
| `tools.jackson.dataformat:jackson-dataformat-cbor`
| `SourceHttpMessageConverter`
| An `HttpMessageConverter` implementation that can read and write `javax.xml.transform.Source` from the HTTP request and response.
@@ -414,7 +414,7 @@ customize the status and headers of the response.
[.small]#xref:web/webflux/reactive-spring.adoc#webflux-codecs-streaming[See equivalent in the Reactive stack]#
Spring MVC supports use of reactive client libraries in a controller (also read
xref:web-reactive.adoc#webflux-reactive-libraries[Reactive Libraries] in the WebFlux section).
xref:web/webflux-reactive-libraries.adoc[Reactive Libraries] in the WebFlux section).
This includes the `WebClient` from `spring-webflux` and others, such as Spring Data
reactive data repositories. In such scenarios, it is convenient to be able to return
reactive types from the controller method.
@@ -171,11 +171,11 @@ Message codes and arguments for each error are also resolved via `MessageSource`
| `NoResourceFoundException`
| (default)
|
| `+{0}+` the request path (or portion of) used to find a resource
| `TypeMismatchException`
| (default)
| `+{0}+` property name, `+{1}+` property value
| `+{0}+` property name, `+{1}+` property value, `+{2}+` simple name of required type
| `UnsatisfiedServletRequestParameterException`
| (default)
@@ -215,7 +215,7 @@ the content negotiation during the error handling phase will decide which conten
| Any other return value
| If a return value is not matched to any of the above and is not a simple type (as determined by
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]),
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty]),
by default, it is treated as a model attribute to be added to the model. If it is a simple type,
it remains unresolved.
|===
@@ -134,6 +134,6 @@ and others) and is equivalent to `required=false`.
| Any other argument
| If a method argument is not matched to any of the earlier values in this table and it is
a simple type (as determined by
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]),
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty]),
it is resolved as a `@RequestParam`. Otherwise, it is resolved as a `@ModelAttribute`.
|===
@@ -250,7 +250,7 @@ xref:web/webmvc/mvc-controller/ann-validation.adoc[Validation].
TIP: Using `@ModelAttribute` is optional. By default, any parameter that is not a simple
value type as determined by
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty]
_AND_ that is not resolved by any other argument resolver is treated as an implicit `@ModelAttribute`.
WARNING: When compiling to a native image with GraalVM, the implicit `@ModelAttribute`
@@ -117,6 +117,6 @@ Kotlin::
Note that use of `@RequestParam` is optional (for example, to set its attributes).
By default, any argument that is a simple value type (as determined by
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty])
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty])
and is not resolved by any other argument resolver, is treated as if it were annotated
with `@RequestParam`.
@@ -98,6 +98,6 @@ supported for all return values.
| Other return values
| If a return value remains unresolved in any other way, it is treated as a model
attribute, unless it is a simple type as determined by
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty],
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty],
in which case it remains unresolved.
|===
@@ -214,7 +214,7 @@ When multiple patterns match a URL, the best match must be selected. This is don
one of the following depending on whether use of parsed `PathPattern` is enabled for use or not:
* {spring-framework-api}/web/util/pattern/PathPattern.html#SPECIFICITY_COMPARATOR[`PathPattern.SPECIFICITY_COMPARATOR`]
* {spring-framework-api}/util/AntPathMatcher.html#getPatternComparator-java.lang.String-[`AntPathMatcher.getPatternComparator(String path)`]
* {spring-framework-api}/util/AntPathMatcher.html#getPatternComparator(java.lang.String)[`AntPathMatcher.getPatternComparator(String path)`]
Both help to sort patterns with more specific ones on top. A pattern is more specific if
it has a lower count of URI variables (counted as 1), single wildcards (counted as 1),
+1 -1
View File
@@ -4,7 +4,7 @@
"@antora/atlas-extension": "1.0.0-alpha.2",
"@antora/collector-extension": "1.0.0-alpha.3",
"@asciidoctor/tabs": "1.0.0-beta.6",
"@springio/antora-extensions": "1.14.2",
"@springio/antora-extensions": "1.14.7",
"fast-xml-parser": "4.5.2",
"@springio/asciidoctor-extensions": "1.0.0-alpha.10"
}
@@ -20,7 +20,7 @@ import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.docs.testing.mockmvc.assertj.mockmvctestersetup.ApplicationWebConfiguration;
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
import org.springframework.http.converter.AbstractJacksonHttpMessageConverter ;
import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.web.context.WebApplicationContext;
@@ -34,7 +34,7 @@ class AccountControllerIntegrationTests {
AccountControllerIntegrationTests(@Autowired WebApplicationContext wac) {
this.mockMvc = MockMvcTester.from(wac).withHttpMessageConverters(
List.of(wac.getBean(AbstractJackson2HttpMessageConverter.class)));
List.of(wac.getBean(AbstractJacksonHttpMessageConverter.class)));
}
// ...
@@ -21,7 +21,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
import org.springframework.web.servlet.view.json.JacksonJsonView;
@SuppressWarnings("removal")
// tag::snippet[]
@@ -30,7 +30,7 @@ public class FreeMarkerConfiguration implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.enableContentNegotiation(new MappingJackson2JsonView());
registry.enableContentNegotiation(new JacksonJsonView());
registry.freeMarker().cache(false);
}
@@ -19,7 +19,7 @@ package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigviewresolvers;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
import org.springframework.web.servlet.view.json.JacksonJsonView;
@SuppressWarnings("removal")
// tag::snippet[]
@@ -28,7 +28,7 @@ public class WebConfiguration implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.enableContentNegotiation(new MappingJackson2JsonView());
registry.enableContentNegotiation(new JacksonJsonView());
registry.jsp();
}
}
@@ -20,7 +20,7 @@ package org.springframework.docs.testing.mockmvc.assertj.mockmvctestersetup.conv
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.docs.testing.mockmvc.assertj.mockmvctestersetup.ApplicationWebConfiguration
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter
import org.springframework.http.converter.AbstractJacksonHttpMessageConverter
import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig
import org.springframework.test.web.servlet.assertj.MockMvcTester
import org.springframework.web.context.WebApplicationContext
@@ -30,7 +30,7 @@ import org.springframework.web.context.WebApplicationContext
class AccountControllerIntegrationTests(@Autowired wac: WebApplicationContext) {
private val mockMvc = MockMvcTester.from(wac).withHttpMessageConverters(
listOf(wac.getBean(AbstractJackson2HttpMessageConverter::class.java)))
listOf(wac.getBean(AbstractJacksonHttpMessageConverter::class.java)))
// ...
@@ -7,14 +7,14 @@ import org.springframework.context.annotation.Configuration
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer
import org.springframework.web.servlet.view.json.MappingJackson2JsonView
import org.springframework.web.servlet.view.json.JacksonJsonView
// tag::snippet[]
@Configuration
class FreeMarkerConfiguration : WebMvcConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.enableContentNegotiation(MappingJackson2JsonView())
registry.enableContentNegotiation(JacksonJsonView())
registry.freeMarker().cache(false)
}
@@ -21,13 +21,13 @@ package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigviewresolvers
import org.springframework.context.annotation.Configuration
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
import org.springframework.web.servlet.view.json.MappingJackson2JsonView
import org.springframework.web.servlet.view.json.JacksonJsonView
// tag::snippet[]
@Configuration
class WebConfiguration : WebMvcConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.enableContentNegotiation(MappingJackson2JsonView())
registry.enableContentNegotiation(JacksonJsonView())
registry.jsp()
}
}
@@ -12,7 +12,7 @@
<mvc:view-resolvers>
<mvc:content-negotiation>
<mvc:default-views>
<bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>
<bean class="org.springframework.web.servlet.view.json.JacksonJsonView"/>
</mvc:default-views>
</mvc:content-negotiation>
<mvc:freemarker cache-views="false"/>
@@ -12,7 +12,7 @@
<mvc:view-resolvers>
<mvc:content-negotiation>
<mvc:default-views>
<bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>
<bean class="org.springframework.web.servlet.view.json.JacksonJsonView"/>
</mvc:default-views>
</mvc:content-negotiation>
<mvc:jsp/>
+10 -10
View File
@@ -8,15 +8,15 @@ javaPlatform {
dependencies {
api(platform("com.fasterxml.jackson:jackson-bom:2.20.1"))
api(platform("io.micrometer:micrometer-bom:1.16.0"))
api(platform("io.micrometer:micrometer-bom:1.16.1"))
api(platform("io.netty:netty-bom:4.2.7.Final"))
api(platform("io.projectreactor:reactor-bom:2025.0.0"))
api(platform("io.projectreactor:reactor-bom:2025.0.1"))
api(platform("io.rsocket:rsocket-bom:1.1.5"))
api(platform("org.apache.groovy:groovy-bom:5.0.2"))
api(platform("org.apache.logging.log4j:log4j-bom:2.25.1"))
api(platform("org.apache.groovy:groovy-bom:5.0.3"))
api(platform("org.apache.logging.log4j:log4j-bom:2.25.2"))
api(platform("org.assertj:assertj-bom:3.27.6"))
api(platform("org.eclipse.jetty:jetty-bom:12.1.4"))
api(platform("org.eclipse.jetty.ee11:jetty-ee11-bom:12.1.4"))
api(platform("org.eclipse.jetty:jetty-bom:12.1.5"))
api(platform("org.eclipse.jetty.ee11:jetty-ee11-bom:12.1.5"))
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.1"))
@@ -33,7 +33,7 @@ dependencies {
api("com.google.code.gson:gson:2.13.2")
api("com.google.protobuf:protobuf-java-util:4.32.1")
api("com.h2database:h2:2.3.232")
api("com.jayway.jsonpath:json-path:2.9.0")
api("com.jayway.jsonpath:json-path:2.10.0")
api("com.networknt:json-schema-validator:1.5.3")
api("com.oracle.database.jdbc:ojdbc11:21.9.0.0")
api("com.rometools:rome:1.19.0")
@@ -111,7 +111,7 @@ 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.0")
api("org.eclipse.jetty:jetty-reactive-httpclient:4.1.4")
api("org.eclipse.persistence:org.eclipse.persistence.jpa:5.0.0-B11")
api("org.eclipse:yasson:3.0.4")
api("org.ehcache:ehcache:3.10.8")
@@ -121,7 +121,7 @@ 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.0.CR2")
api("org.hibernate.orm:hibernate-core:7.2.0.CR3")
api("org.hibernate.validator:hibernate-validator:9.1.0.Final")
api("org.hsqldb:hsqldb:2.7.4")
api("org.htmlunit:htmlunit:4.18.0")
@@ -129,7 +129,7 @@ dependencies {
api("org.jboss.logging:jboss-logging:3.6.1.Final")
api("org.jruby:jruby:10.0.2.0")
api("org.jspecify:jspecify:1.0.0")
api("org.junit.support:testng-engine:1.0.5")
api("org.junit.support:testng-engine:1.1.0")
api("org.mozilla:rhino:1.7.15")
api("org.ogce:xpp3:1.1.6")
api("org.python:jython-standalone:2.7.4")
+1 -1
View File
@@ -1,4 +1,4 @@
version=7.0.0
version=7.0.2
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
+1 -1
View File
@@ -54,4 +54,4 @@ _When instructed to execute `./gradlew` from the command line, be sure to execut
In any case, please do not check in your own generated `.classpath` file, `.project`
file, or `.settings` folder. You'll notice these files are already intentionally in
`.gitignore`. The same policy holds for IDEA metadata.
`.gitignore`. The same policy holds for IntelliJ IDEA metadata.
@@ -48,7 +48,6 @@ import org.springframework.aop.framework.AopConfigException;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConvertingComparator;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
@@ -84,10 +83,10 @@ public class ReflectiveAspectJAdvisorFactory extends AbstractAspectJAdvisorFacto
// @AfterThrowing methods due to the fact that AspectJAfterAdvice.invoke(MethodInvocation)
// invokes proceed() in a `try` block and only invokes the @After advice method
// in a corresponding `finally` block.
Comparator<Method> adviceKindComparator = new ConvertingComparator<>(
Comparator<Method> adviceKindComparator = new ConvertingComparator<Method, @Nullable Annotation>(
new InstanceComparator<>(
Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class),
(Converter<Method, Annotation>) method -> {
method -> {
AspectJAnnotation ann = AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(method);
return (ann != null ? ann.getAnnotation() : null);
});
@@ -44,7 +44,6 @@ import org.springframework.beans.BeansException;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor;
@@ -296,10 +295,8 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
/**
* Build a cache key for the given bean class and bean name.
* <p>Note: As of 4.2.3, this implementation does not return a concatenated
* class/name String anymore but rather the most efficient cache key possible:
* a plain bean name, prepended with {@link BeanFactory#FACTORY_BEAN_PREFIX}
* in case of a {@code FactoryBean}; or if no bean name specified, then the
* <p>Note: As of 7.0.2, this implementation returns a composed cache key
* for bean class plus bean name; or if no bean name specified, then the
* given bean {@code Class} as-is.
* @param beanClass the bean class
* @param beanName the bean name
@@ -307,8 +304,7 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
*/
protected Object getCacheKey(Class<?> beanClass, @Nullable String beanName) {
if (StringUtils.hasLength(beanName)) {
return (FactoryBean.class.isAssignableFrom(beanClass) ?
BeanFactory.FACTORY_BEAN_PREFIX + beanName : beanName);
return new ComposedCacheKey(beanClass, beanName);
}
else {
return beanClass;
@@ -615,4 +611,12 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
protected abstract Object @Nullable [] getAdvicesAndAdvisorsForBean(Class<?> beanClass, String beanName,
@Nullable TargetSource customTargetSource) throws BeansException;
/**
* Composed cache key for bean class plus bean name.
* @see #getCacheKey(Class, String)
*/
private record ComposedCacheKey(Class<?> beanClass, String beanName) {
}
}
@@ -365,9 +365,12 @@ public class QualifierAnnotationAutowireCandidateResolver extends GenericTypeAwa
}
MethodParameter methodParam = descriptor.getMethodParameter();
if (methodParam != null) {
for (Annotation annotation : methodParam.getMethodAnnotations()) {
if (isQualifier(annotation.annotationType())) {
return true;
Method method = methodParam.getMethod();
if (method == null || void.class == method.getReturnType()) {
for (Annotation annotation : methodParam.getMethodAnnotations()) {
if (isQualifier(annotation.annotationType())) {
return true;
}
}
}
}
@@ -203,8 +203,8 @@ public abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates {
public @Nullable CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) {
if (value instanceof RuntimeBeanReference runtimeBeanReference &&
runtimeBeanReference.getBeanType() != null) {
return CodeBlock.of("new $T($T.class)", RuntimeBeanReference.class,
runtimeBeanReference.getBeanType());
return CodeBlock.of("new $T($S, $T.class)", RuntimeBeanReference.class,
runtimeBeanReference.getBeanName(), runtimeBeanReference.getBeanType());
}
else if (value instanceof BeanReference beanReference) {
return CodeBlock.of("new $T($S)", RuntimeBeanReference.class,
@@ -18,6 +18,7 @@ package org.springframework.beans.factory.aot;
import org.springframework.aot.generate.GeneratedMethods;
import org.springframework.aot.generate.MethodReference;
import org.springframework.javapoet.ClassName;
/**
* Interface that can be used to configure the code that will be generated to
@@ -25,6 +26,7 @@ import org.springframework.aot.generate.MethodReference;
*
* @author Phillip Webb
* @author Stephane Nicoll
* @author Sebastien Deleuze
* @since 6.0
* @see BeanFactoryInitializationAotContribution
*/
@@ -41,6 +43,13 @@ public interface BeanFactoryInitializationCode {
*/
GeneratedMethods getMethods();
/**
* Return the name of the class used by the initializing code.
* @return the generated class name
* @since 7.0.2
*/
ClassName getClassName();
/**
* Add an initializer method call. An initializer can use a flexible signature,
* using any of the following:
@@ -384,7 +384,7 @@ public class InstanceSupplierCodeGenerator {
Visibility visibility = AccessControl.lowest(classAccessControl, memberAccessControl).getVisibility();
return (visibility == Visibility.PUBLIC || (visibility != Visibility.PRIVATE &&
member.getDeclaringClass().getPackageName().equals(this.className.packageName())));
}
}
private CodeBlock generateParameterTypesCode(Class<?>[] parameterTypes) {
CodeBlock.Builder code = CodeBlock.builder();
@@ -33,7 +33,6 @@ import org.springframework.util.StringUtils;
* @author Juergen Hoeller
* @author Rob Harrop
* @since 1.1
* @see PropertiesBeanDefinitionReader
* @see org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader
*/
public abstract class BeanDefinitionReaderUtils {
@@ -43,7 +43,6 @@ import org.springframework.core.AliasRegistry;
* @see DefaultListableBeanFactory
* @see org.springframework.context.support.GenericApplicationContext
* @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader
* @see PropertiesBeanDefinitionReader
*/
public interface BeanDefinitionRegistry extends AliasRegistry {
@@ -1632,7 +1632,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
descriptor.initParameterNameDiscovery(getParameterNameDiscoverer());
if (Optional.class == descriptor.getDependencyType()) {
return createOptionalDependency(descriptor, requestingBeanName);
return createOptionalDependency(descriptor, requestingBeanName, autowiredBeanNames, null);
}
else if (ObjectFactory.class == descriptor.getDependencyType() ||
ObjectProvider.class == descriptor.getDependencyType()) {
@@ -2330,8 +2330,8 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
/**
* Create an {@link Optional} wrapper for the specified dependency.
*/
private Optional<?> createOptionalDependency(
DependencyDescriptor descriptor, @Nullable String beanName, final @Nullable Object... args) {
private Optional<?> createOptionalDependency(DependencyDescriptor descriptor, @Nullable String beanName,
@Nullable Set<String> autowiredBeanNames, @Nullable Object @Nullable [] args) {
DependencyDescriptor descriptorToUse = new NestedDependencyDescriptor(descriptor) {
@Override
@@ -2348,7 +2348,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
return ObjectUtils.isEmpty(args);
}
};
Object result = doResolveDependency(descriptorToUse, beanName, null, null);
Object result = doResolveDependency(descriptorToUse, beanName, autowiredBeanNames, null);
return (result instanceof Optional<?> optional ? optional : Optional.ofNullable(result));
}
@@ -2501,12 +2501,18 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
*/
private class DependencyObjectProvider implements BeanObjectProvider<Object> {
private static final Object NOT_CACHEABLE = new Object();
private static final Object NULL_VALUE = new Object();
private final DependencyDescriptor descriptor;
private final boolean optional;
private final @Nullable String beanName;
private transient volatile @Nullable Object cachedValue;
public DependencyObjectProvider(DependencyDescriptor descriptor, @Nullable String beanName) {
this.descriptor = new NestedDependencyDescriptor(descriptor);
this.optional = (this.descriptor.getDependencyType() == Optional.class);
@@ -2515,22 +2521,17 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
@Override
public Object getObject() throws BeansException {
if (this.optional) {
return createOptionalDependency(this.descriptor, this.beanName);
}
else {
Object result = doResolveDependency(this.descriptor, this.beanName, null, null);
if (result == null) {
throw new NoSuchBeanDefinitionException(this.descriptor.getResolvableType());
}
return result;
Object result = getValue();
if (result == null) {
throw new NoSuchBeanDefinitionException(this.descriptor.getResolvableType());
}
return result;
}
@Override
public Object getObject(final @Nullable Object... args) throws BeansException {
if (this.optional) {
return createOptionalDependency(this.descriptor, this.beanName, args);
return createOptionalDependency(this.descriptor, this.beanName, null, args);
}
else {
DependencyDescriptor descriptorToUse = new DependencyDescriptor(this.descriptor) {
@@ -2551,7 +2552,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
public @Nullable Object getIfAvailable() throws BeansException {
try {
if (this.optional) {
return createOptionalDependency(this.descriptor, this.beanName);
return createOptionalDependency(this.descriptor, this.beanName, null, null);
}
else {
DependencyDescriptor descriptorToUse = new DependencyDescriptor(this.descriptor) {
@@ -2604,7 +2605,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
};
try {
if (this.optional) {
return createOptionalDependency(descriptorToUse, this.beanName);
return createOptionalDependency(descriptorToUse, this.beanName, null, null);
}
else {
return doResolveDependency(descriptorToUse, this.beanName, null, null);
@@ -2630,11 +2631,41 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
}
protected @Nullable Object getValue() throws BeansException {
Object value = this.cachedValue;
if (value == null) {
if (isConfigurationFrozen()) {
Set<String> autowiredBeanNames = new LinkedHashSet<>(2);
value = resolveValue(autowiredBeanNames);
boolean cacheable = false;
if (!autowiredBeanNames.isEmpty()) {
cacheable = true;
for (String autowiredBeanName : autowiredBeanNames) {
if (!containsBean(autowiredBeanName) || !isSingleton(autowiredBeanName)) {
cacheable = false;
}
}
}
this.cachedValue = (cacheable ? (value != null ? value : NULL_VALUE) : NOT_CACHEABLE);
return value;
}
}
else if (value == NULL_VALUE) {
return null;
}
else if (value != NOT_CACHEABLE) {
return value;
}
// Not cacheable -> fresh resolution.
return resolveValue(null);
}
private @Nullable Object resolveValue(@Nullable Set<String> autowiredBeanNames) {
if (this.optional) {
return createOptionalDependency(this.descriptor, this.beanName);
return createOptionalDependency(this.descriptor, this.beanName, autowiredBeanNames, null);
}
else {
return doResolveDependency(this.descriptor, this.beanName, null, null);
return doResolveDependency(this.descriptor, this.beanName, autowiredBeanNames, null);
}
}
@@ -532,7 +532,7 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
/**
* Callback before singleton creation.
* <p>The default implementation register the singleton as currently in creation.
* <p>The default implementation registers the singleton as currently in creation.
* @param beanName the name of the singleton about to be created
* @see #isSingletonCurrentlyInCreation
*/
@@ -582,7 +582,7 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
public void registerContainedBean(String containedBeanName, String containingBeanName) {
synchronized (this.containedBeanMap) {
Set<String> containedBeans =
this.containedBeanMap.computeIfAbsent(containingBeanName, k -> new LinkedHashSet<>(8));
this.containedBeanMap.computeIfAbsent(containingBeanName, key -> new LinkedHashSet<>(8));
if (!containedBeans.add(containedBeanName)) {
return;
}
@@ -601,7 +601,7 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
synchronized (this.dependentBeanMap) {
Set<String> dependentBeans =
this.dependentBeanMap.computeIfAbsent(canonicalName, k -> new LinkedHashSet<>(8));
this.dependentBeanMap.computeIfAbsent(canonicalName, key -> new LinkedHashSet<>(8));
if (!dependentBeans.add(dependentBeanName)) {
return;
}
@@ -609,7 +609,7 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
synchronized (this.dependenciesForBeanMap) {
Set<String> dependenciesForBean =
this.dependenciesForBeanMap.computeIfAbsent(dependentBeanName, k -> new LinkedHashSet<>(8));
this.dependenciesForBeanMap.computeIfAbsent(dependentBeanName, key -> new LinkedHashSet<>(8));
dependenciesForBean.add(canonicalName);
}
}
@@ -76,7 +76,7 @@ import org.springframework.util.StringUtils;
* @since 26.11.2003
* @see DefaultListableBeanFactory
* @deprecated in favor of Spring's common bean definition formats and/or
* custom reader implementations
* custom BeanDefinitionReader implementations
*/
@Deprecated(since = "5.3")
public class PropertiesBeanDefinitionReader extends AbstractBeanDefinitionReader {
@@ -1587,6 +1587,18 @@ class AutowiredAnnotationBeanPostProcessorTests {
ObjectFactoryFieldInjectionBean bean = bf.getBean("annotatedBean", ObjectFactoryFieldInjectionBean.class);
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
}
@Test
void objectFactoryFieldInjectionAgainstFrozen() {
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryFieldInjectionBean.class));
bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class));
bf.freezeConfiguration();
ObjectFactoryFieldInjectionBean bean = bf.getBean("annotatedBean", ObjectFactoryFieldInjectionBean.class);
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
}
@Test
@@ -1596,6 +1608,18 @@ class AutowiredAnnotationBeanPostProcessorTests {
ObjectFactoryConstructorInjectionBean bean = bf.getBean("annotatedBean", ObjectFactoryConstructorInjectionBean.class);
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
}
@Test
void objectFactoryConstructorInjectionAgainstFrozen() {
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryConstructorInjectionBean.class));
bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class));
bf.freezeConfiguration();
ObjectFactoryConstructorInjectionBean bean = bf.getBean("annotatedBean", ObjectFactoryConstructorInjectionBean.class);
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
}
@Test
@@ -2116,8 +2140,8 @@ class AutowiredAnnotationBeanPostProcessorTests {
bf.registerBeanDefinition("factoryBeanDependentBean", new RootBeanDefinition(FactoryBeanDependentBean.class));
bf.registerSingleton("stringFactoryBean", new StringFactoryBean());
final StringFactoryBean factoryBean = (StringFactoryBean) bf.getBean("&stringFactoryBean");
final FactoryBeanDependentBean bean = (FactoryBeanDependentBean) bf.getBean("factoryBeanDependentBean");
StringFactoryBean factoryBean = (StringFactoryBean) bf.getBean("&stringFactoryBean");
FactoryBeanDependentBean bean = (FactoryBeanDependentBean) bf.getBean("factoryBeanDependentBean");
assertThat(factoryBean).as("The singleton StringFactoryBean should have been registered.").isNotNull();
assertThat(bean).as("The factoryBeanDependentBean should have been registered.").isNotNull();
@@ -2728,9 +2752,11 @@ class AutowiredAnnotationBeanPostProcessorTests {
bf.registerSingleton("nonNullBean", "Test");
bf.registerBeanDefinition("mixedNullableInjectionBean",
new RootBeanDefinition(MixedNullableInjectionBean.class));
MixedNullableInjectionBean mixedNullableInjectionBean = bf.getBean(MixedNullableInjectionBean.class);
assertThat(mixedNullableInjectionBean.nonNullBean).isNotNull();
assertThat(mixedNullableInjectionBean.nullableBean).isNull();
assertThat(bf.getDependentBeans("nonNullBean")).contains("mixedNullableInjectionBean");
}
@Test
@@ -2738,9 +2764,11 @@ class AutowiredAnnotationBeanPostProcessorTests {
bf.registerSingleton("nonNullBean", "Test");
bf.registerBeanDefinition("mixedOptionalInjectionBean",
new RootBeanDefinition(MixedOptionalInjectionBean.class));
MixedOptionalInjectionBean mixedOptionalInjectionBean = bf.getBean(MixedOptionalInjectionBean.class);
assertThat(mixedOptionalInjectionBean.nonNullBean).isNotNull();
assertThat(mixedOptionalInjectionBean.nullableBean).isNull();
assertThat(bf.getDependentBeans("nonNullBean")).contains("mixedOptionalInjectionBean");
}
@@ -370,8 +370,25 @@ class InjectAnnotationBeanPostProcessorTests {
ObjectFactoryFieldInjectionBean bean = (ObjectFactoryFieldInjectionBean) bf.getBean("annotatedBean");
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
bean = SerializationTestUtils.serializeAndDeserialize(bean);
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
}
@Test
void testObjectFactoryWithBeanFieldAgainstFrozen() throws Exception {
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryFieldInjectionBean.class));
bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class));
bf.setSerializationId("test");
bf.freezeConfiguration();
ObjectFactoryFieldInjectionBean bean = (ObjectFactoryFieldInjectionBean) bf.getBean("annotatedBean");
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
bean = SerializationTestUtils.serializeAndDeserialize(bean);
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
}
@Test
@@ -382,8 +399,25 @@ class InjectAnnotationBeanPostProcessorTests {
ObjectFactoryMethodInjectionBean bean = (ObjectFactoryMethodInjectionBean) bf.getBean("annotatedBean");
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
bean = SerializationTestUtils.serializeAndDeserialize(bean);
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
}
@Test
void testObjectFactoryWithBeanMethodAgainstFrozen() throws Exception {
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryMethodInjectionBean.class));
bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class));
bf.setSerializationId("test");
bf.freezeConfiguration();
ObjectFactoryMethodInjectionBean bean = (ObjectFactoryMethodInjectionBean) bf.getBean("annotatedBean");
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
bean = SerializationTestUtils.serializeAndDeserialize(bean);
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
}
@Test
@@ -444,8 +478,8 @@ class InjectAnnotationBeanPostProcessorTests {
bf.registerBeanDefinition("factoryBeanDependentBean", new RootBeanDefinition(FactoryBeanDependentBean.class));
bf.registerSingleton("stringFactoryBean", new StringFactoryBean());
final StringFactoryBean factoryBean = (StringFactoryBean) bf.getBean("&stringFactoryBean");
final FactoryBeanDependentBean bean = (FactoryBeanDependentBean) bf.getBean("factoryBeanDependentBean");
StringFactoryBean factoryBean = (StringFactoryBean) bf.getBean("&stringFactoryBean");
FactoryBeanDependentBean bean = (FactoryBeanDependentBean) bf.getBean("factoryBeanDependentBean");
assertThat(factoryBean).as("The singleton StringFactoryBean should have been registered.").isNotNull();
assertThat(bean).as("The factoryBeanDependentBean should have been registered.").isNotNull();
@@ -459,6 +493,7 @@ class InjectAnnotationBeanPostProcessorTests {
NullableFieldInjectionBean bean = (NullableFieldInjectionBean) bf.getBean("annotatedBean");
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
assertThat(bf.getDependentBeans("testBean")).contains("annotatedBean");
}
@Test
@@ -476,6 +511,7 @@ class InjectAnnotationBeanPostProcessorTests {
NullableMethodInjectionBean bean = (NullableMethodInjectionBean) bf.getBean("annotatedBean");
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
assertThat(bf.getDependentBeans("testBean")).contains("annotatedBean");
}
@Test
@@ -494,6 +530,7 @@ class InjectAnnotationBeanPostProcessorTests {
OptionalFieldInjectionBean bean = (OptionalFieldInjectionBean) bf.getBean("annotatedBean");
assertThat(bean.getTestBean()).isPresent();
assertThat(bean.getTestBean().get()).isSameAs(bf.getBean("testBean"));
assertThat(bf.getDependentBeans("testBean")).contains("annotatedBean");
}
@Test
@@ -512,6 +549,7 @@ class InjectAnnotationBeanPostProcessorTests {
OptionalMethodInjectionBean bean = (OptionalMethodInjectionBean) bf.getBean("annotatedBean");
assertThat(bean.getTestBean()).isPresent();
assertThat(bean.getTestBean().get()).isSameAs(bf.getBean("testBean"));
assertThat(bf.getDependentBeans("testBean")).contains("annotatedBean");
}
@Test
@@ -530,6 +568,7 @@ class InjectAnnotationBeanPostProcessorTests {
OptionalListFieldInjectionBean bean = (OptionalListFieldInjectionBean) bf.getBean("annotatedBean");
assertThat(bean.getTestBean()).hasValueSatisfying(list ->
assertThat(list).containsExactly(bf.getBean("testBean", TestBean.class)));
assertThat(bf.getDependentBeans("testBean")).contains("annotatedBean");
}
@Test
@@ -548,6 +587,7 @@ class InjectAnnotationBeanPostProcessorTests {
OptionalListMethodInjectionBean bean = (OptionalListMethodInjectionBean) bf.getBean("annotatedBean");
assertThat(bean.getTestBean()).hasValueSatisfying(list ->
assertThat(list).containsExactly(bf.getBean("testBean", TestBean.class)));
assertThat(bf.getDependentBeans("testBean")).contains("annotatedBean");
}
@Test
@@ -566,6 +606,7 @@ class InjectAnnotationBeanPostProcessorTests {
ProviderOfOptionalFieldInjectionBean bean = (ProviderOfOptionalFieldInjectionBean) bf.getBean("annotatedBean");
assertThat(bean.getTestBean()).isPresent();
assertThat(bean.getTestBean().get()).isSameAs(bf.getBean("testBean"));
assertThat(bf.getDependentBeans("testBean")).doesNotContain("annotatedBean");
}
@Test
@@ -584,6 +625,7 @@ class InjectAnnotationBeanPostProcessorTests {
ProviderOfOptionalMethodInjectionBean bean = (ProviderOfOptionalMethodInjectionBean) bf.getBean("annotatedBean");
assertThat(bean.getTestBean()).isPresent();
assertThat(bean.getTestBean().get()).isSameAs(bf.getBean("testBean"));
assertThat(bf.getDependentBeans("testBean")).doesNotContain("annotatedBean");
}
@Test
@@ -788,7 +830,6 @@ class InjectAnnotationBeanPostProcessorTests {
private ConfigurableListableBeanFactory beanFactory;
public ConstructorResourceInjectionBean() {
throw new UnsupportedOperationException();
}
@@ -458,30 +458,42 @@ class BeanDefinitionPropertyValueCodeGeneratorDelegatesTests {
class BeanReferenceTests {
@Test
void generatedWhenBeanNameReference() {
RuntimeBeanNameReference beanReference = new RuntimeBeanNameReference("test");
void generatedWhenRuntimeBeanNameReference() {
BeanReference beanReference = new RuntimeBeanNameReference("test");
compile(beanReference, (instance, compiler) -> {
RuntimeBeanReference actual = (RuntimeBeanReference) instance;
assertThat(actual.getBeanName()).isEqualTo(beanReference.getBeanName());
assertThat(actual.getBeanName()).as("name").isEqualTo("test");
assertThat(actual.getBeanType()).as("type").isNull();
});
}
@Test
void generatedWhenBeanReferenceByName() {
RuntimeBeanReference beanReference = new RuntimeBeanReference("test");
void generatedWhenRuntimeBeanReferenceByName() {
BeanReference beanReference = new RuntimeBeanReference("test");
compile(beanReference, (instance, compiler) -> {
RuntimeBeanReference actual = (RuntimeBeanReference) instance;
assertThat(actual.getBeanName()).isEqualTo(beanReference.getBeanName());
assertThat(actual.getBeanType()).isEqualTo(beanReference.getBeanType());
assertThat(actual.getBeanName()).as("name").isEqualTo("test");
assertThat(actual.getBeanType()).as("type").isNull();
});
}
@Test
void generatedWhenBeanReferenceByType() {
void generatedWhenRuntimeBeanReferenceByType() {
BeanReference beanReference = new RuntimeBeanReference(String.class);
compile(beanReference, (instance, compiler) -> {
RuntimeBeanReference actual = (RuntimeBeanReference) instance;
assertThat(actual.getBeanType()).isEqualTo(String.class);
assertThat(actual.getBeanName()).as("name").isEqualTo(String.class.getName());
assertThat(actual.getBeanType()).as("type").isEqualTo(String.class);
});
}
@Test // gh-35913
void generatedWhenRuntimeBeanReferenceByNameAndType() {
BeanReference beanReference = new RuntimeBeanReference("test", String.class);
compile(beanReference, (instance, compiler) -> {
RuntimeBeanReference actual = (RuntimeBeanReference) instance;
assertThat(actual.getBeanName()).as("name").isEqualTo("test");
assertThat(actual.getBeanType()).as("type").isEqualTo(String.class);
});
}
@@ -46,14 +46,14 @@ class YamlMapFactoryBeanTests {
@Test
void testSetIgnoreResourceNotFound() {
this.factory.setResolutionMethod(YamlMapFactoryBean.ResolutionMethod.OVERRIDE_AND_IGNORE);
this.factory.setResources(new FileSystemResource("non-exsitent-file.yml"));
this.factory.setResources(new FileSystemResource("non-existent-file.yml"));
assertThat(this.factory.getObject()).isEmpty();
}
@Test
void testSetBarfOnResourceNotFound() {
assertThatIllegalStateException().isThrownBy(() -> {
this.factory.setResources(new FileSystemResource("non-exsitent-file.yml"));
this.factory.setResources(new FileSystemResource("non-existent-file.yml"));
this.factory.getObject().size();
});
}
@@ -51,7 +51,7 @@ class MetadataAttachmentTests {
void metadataIsInherited() {
BeanDefinition beanDefinition = this.beanFactory.getMergedBeanDefinition("testBean2");
assertThat(beanDefinition.getAttribute("foo")).as("Metadata not inherited").isEqualTo("bar");
assertThat(beanDefinition.getAttribute("abc")).as("Child metdata not attached").isEqualTo("123");
assertThat(beanDefinition.getAttribute("abc")).as("Child metadata not attached").isEqualTo("123");
}
@Test
@@ -76,7 +76,7 @@ public class CaffeineCacheManager implements CacheManager {
private boolean allowNullValues = true;
private boolean dynamic = true;
private volatile boolean dynamic = true;
private final Map<String, Cache> cacheMap = new ConcurrentHashMap<>(16);
@@ -101,10 +101,15 @@ public class CaffeineCacheManager implements CacheManager {
/**
* Specify the set of cache names for this CacheManager's 'static' mode.
* <p>The number of caches and their names will be fixed after a call to this method,
* with no creation of further cache regions at runtime.
* <p>Calling this with a {@code null} collection argument resets the
* mode to 'dynamic', allowing for further creation of caches again.
* <p>The number of caches and their names will be fixed after a call
* to this method, with no creation of further cache regions at runtime.
* <p>Note that this method replaces existing caches of the given names
* and prevents the creation of further cache regions from here on - but
* does <i>not</i> remove unrelated existing caches. For a full reset,
* consider calling {@link #resetCaches()} before calling this method.
* <p>Calling this method with a {@code null} collection argument resets
* the mode to 'dynamic', allowing for further creation of caches again.
* @see #resetCaches()
*/
public void setCacheNames(@Nullable Collection<String> cacheNames) {
if (cacheNames != null) {
@@ -244,11 +249,6 @@ public class CaffeineCacheManager implements CacheManager {
}
@Override
public Collection<String> getCacheNames() {
return Collections.unmodifiableSet(this.cacheMap.keySet());
}
@Override
public @Nullable Cache getCache(String name) {
Cache cache = this.cacheMap.get(name);
@@ -258,6 +258,34 @@ public class CaffeineCacheManager implements CacheManager {
return cache;
}
@Override
public Collection<String> getCacheNames() {
return Collections.unmodifiableSet(this.cacheMap.keySet());
}
/**
* Reset this cache manager's caches, removing them completely for on-demand
* re-creation in 'dynamic' mode, or simply clearing their entries otherwise.
* @since 6.2.14
*/
@Override
public void resetCaches() {
this.cacheMap.values().forEach(Cache::clear);
if (this.dynamic) {
this.cacheMap.keySet().retainAll(this.customCacheNames);
}
}
/**
* Remove the specified cache from this cache manager, applying to
* custom caches as well as dynamically registered caches at runtime.
* @param name the name of the cache
* @since 6.1.15
*/
public void removeCache(String name) {
this.customCacheNames.remove(name);
this.cacheMap.remove(name);
}
/**
* Register the given native Caffeine Cache instance with this cache manager,
@@ -301,16 +329,6 @@ public class CaffeineCacheManager implements CacheManager {
this.cacheMap.put(name, adaptCaffeineCache(name, cache));
}
/**
* Remove the specified cache from this cache manager, applying to
* custom caches as well as dynamically registered caches at runtime.
* @param name the name of the cache
* @since 6.1.15
*/
public void removeCache(String name) {
this.customCacheNames.remove(name);
this.cacheMap.remove(name);
}
/**
* Adapt the given new native Caffeine Cache instance to Spring's {@link Cache}
@@ -130,4 +130,17 @@ public class JCacheCacheManager extends AbstractTransactionSupportingCacheManage
return null;
}
@Override
public void resetCaches() {
CacheManager cacheManager = getCacheManager();
if (cacheManager != null && !cacheManager.isClosed()) {
for (String cacheName : cacheManager.getCacheNames()) {
javax.cache.Cache<Object, Object> jcache = cacheManager.getCache(cacheName);
if (jcache != null && !jcache.isClosed()) {
jcache.clear();
}
}
}
}
}
@@ -26,7 +26,7 @@ import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.Aware;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.core.MethodClassKey;
import org.springframework.util.ReflectionUtils;
@@ -95,8 +95,8 @@ public abstract class AbstractFallbackJCacheOperationSource implements JCacheOpe
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
// Skip methods declared on BeanFactoryAware and co.
if (method.getDeclaringClass().isInterface() && Aware.class.isAssignableFrom(method.getDeclaringClass())) {
// Skip setBeanFactory method on BeanFactoryAware.
if (method.getDeclaringClass() == BeanFactoryAware.class) {
return null;
}
@@ -24,7 +24,6 @@ import com.github.benmanes.caffeine.cache.CaffeineSpec;
import org.junit.jupiter.api.Test;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.support.SimpleValueWrapper;
import static org.assertj.core.api.Assertions.assertThat;
@@ -42,7 +41,7 @@ class CaffeineCacheManagerTests {
@Test
@SuppressWarnings("cast")
void dynamicMode() {
CacheManager cm = new CaffeineCacheManager();
CaffeineCacheManager cm = new CaffeineCacheManager();
Cache cache1 = cm.getCache("c1");
assertThat(cache1).isInstanceOf(CaffeineCache.class);
@@ -76,6 +75,14 @@ class CaffeineCacheManagerTests {
cache1.evict("key3");
assertThat(cache1.get("key3", () -> (String) null)).isNull();
assertThat(cache1.get("key3", () -> (String) null)).isNull();
cm.removeCache("c1");
assertThat(cm.getCache("c1")).isNotSameAs(cache1);
assertThat(cm.getCache("c2")).isSameAs(cache2);
cm.resetCaches();
assertThat(cm.getCache("c1")).isNotSameAs(cache1);
assertThat(cm.getCache("c2")).isNotSameAs(cache2);
}
@Test
@@ -131,11 +138,24 @@ class CaffeineCacheManagerTests {
cm.setAllowNullValues(true);
Cache cache1y = cm.getCache("c1");
Cache cache2y = cm.getCache("c2");
cache1y.put("key3", null);
assertThat(cache1y.get("key3").get()).isNull();
cache1y.evict("key3");
assertThat(cache1y.get("key3")).isNull();
cache2y.put("key4", "value4");
assertThat(cache2y.get("key4").get()).isEqualTo("value4");
cm.removeCache("c1");
assertThat(cm.getCache("c1")).isNull();
assertThat(cm.getCache("c2")).isSameAs(cache2y);
assertThat(cache2y.get("key4").get()).isEqualTo("value4");
cm.resetCaches();
assertThat(cm.getCache("c1")).isNull();
assertThat(cm.getCache("c2")).isSameAs(cache2y);
assertThat(cache2y.get("key4")).isNull();
}
@Test
@@ -51,8 +51,7 @@ class JCacheEhCacheApiTests extends AbstractValueAdaptingCacheTests<JCacheCache>
this.cacheManager.createCache(CACHE_NAME_NO_NULL, new MutableConfiguration<>());
this.nativeCache = this.cacheManager.getCache(CACHE_NAME);
this.cache = new JCacheCache(this.nativeCache);
Cache<Object, Object> nativeCacheNoNull =
this.cacheManager.getCache(CACHE_NAME_NO_NULL);
Cache<Object, Object> nativeCacheNoNull = this.cacheManager.getCache(CACHE_NAME_NO_NULL);
this.cacheNoNull = new JCacheCache(nativeCacheNoNull, false);
}
@@ -100,4 +99,18 @@ class JCacheEhCacheApiTests extends AbstractValueAdaptingCacheTests<JCacheCache>
assertThat(cache.get(key).get()).isEqualTo(value);
}
@Test
void resetCaches() {
JCacheCacheManager cm = new JCacheCacheManager(cacheManager);
org.springframework.cache.Cache cache = cm.getCache(CACHE_NAME);
cache.put("key", "value");
assertThat(cm.getCacheNames()).contains(CACHE_NAME);
assertThat(cm.getCache(CACHE_NAME)).isNotNull().isSameAs(cache);
assertThat(cacheManager.getCache(CACHE_NAME).iterator()).hasNext();
cm.resetCaches();
assertThat(cm.getCacheNames()).contains(CACHE_NAME);
assertThat(cm.getCache(CACHE_NAME)).isNotNull().isSameAs(cache);
assertThat(cacheManager.getCache(CACHE_NAME).iterator()).isExhausted();
}
}
@@ -27,6 +27,7 @@ import org.jspecify.annotations.Nullable;
*
* @author Costin Leau
* @author Sam Brannen
* @author Juergen Hoeller
* @since 3.1
*/
public interface CacheManager {
@@ -47,4 +48,31 @@ public interface CacheManager {
*/
Collection<String> getCacheNames();
/**
* Remove all registered caches from this cache manager if possible,
* re-creating them on demand. After this call, {@link #getCacheNames()}
* will possibly be empty and the cache provider will have dropped all
* cache management state.
* <p>Alternatively, an implementation may perform an equivalent reset
* on fixed existing cache regions without actually dropping the cache.
* This behavior will be indicated by {@link #getCacheNames()} still
* exposing a non-empty set of names, whereas the corresponding cache
* regions will not contain cache entries anymore.
* <p>The default implementation calls {@link Cache#clear} on all
* registered caches, retaining all caches as registered, satisfying
* the alternative implementation path above. Custom implementations
* may either drop the actual caches (re-creating them on demand) or
* perform a more exhaustive reset at the actual cache provider level.
* @since 7.0.2
* @see Cache#clear()
*/
default void resetCaches() {
for (String cacheName : getCacheNames()) {
Cache cache = getCache(cacheName);
if (cache != null) {
cache.clear();
}
}
}
}
@@ -177,6 +177,11 @@ public @interface EnableCaching {
* be upgraded to subclass proxying at the same time. This approach has no negative
* impact in practice unless one is explicitly expecting one type of proxy vs another,
* for example, in tests.
* <p>It is usually recommendable to rely on a global default proxy configuration
* instead, with specific proxy requirements for certain beans expressed through
* a {@link org.springframework.context.annotation.Proxyable} annotation on
* the affected bean classes.
* @see org.springframework.aop.config.AopConfigUtils#forceAutoProxyCreatorToUseClassProxying
*/
boolean proxyTargetClass() default false;
@@ -55,7 +55,7 @@ public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderA
private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>(16);
private boolean dynamic = true;
private volatile boolean dynamic = true;
private boolean allowNullValues = true;
@@ -82,10 +82,15 @@ public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderA
/**
* Specify the set of cache names for this CacheManager's 'static' mode.
* <p>The number of caches and their names will be fixed after a call to this method,
* with no creation of further cache regions at runtime.
* <p>Calling this with a {@code null} collection argument resets the
* mode to 'dynamic', allowing for further creation of caches again.
* <p>The number of caches and their names will be fixed after a call
* to this method, with no creation of further cache regions at runtime.
* <p>Note that this method replaces existing caches of the given names
* and prevents the creation of further cache regions from here on - but
* does <i>not</i> remove unrelated existing caches. For a full reset,
* consider calling {@link #resetCaches()} before calling this method.
* <p>Calling this method with a {@code null} collection argument resets
* the mode to 'dynamic', allowing for further creation of caches again.
* @see #resetCaches()
*/
public void setCacheNames(@Nullable Collection<String> cacheNames) {
if (cacheNames != null) {
@@ -160,11 +165,6 @@ public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderA
}
@Override
public Collection<String> getCacheNames() {
return Collections.unmodifiableSet(this.cacheMap.keySet());
}
@Override
public @Nullable Cache getCache(String name) {
Cache cache = this.cacheMap.get(name);
@@ -174,6 +174,24 @@ public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderA
return cache;
}
@Override
public Collection<String> getCacheNames() {
return Collections.unmodifiableSet(this.cacheMap.keySet());
}
/**
* Reset this cache manager's caches, removing them completely for on-demand
* re-creation in 'dynamic' mode, or simply clearing their entries otherwise.
* @since 6.2.14
*/
@Override
public void resetCaches() {
this.cacheMap.values().forEach(Cache::clear);
if (this.dynamic) {
this.cacheMap.clear();
}
}
/**
* Remove the specified cache from this cache manager.
* @param name the name of the cache
@@ -113,7 +113,7 @@ class CacheAdviceParser extends AbstractSingleBeanDefinitionParser {
builder.setUnless(getAttributeValue(opElement, "unless", ""));
builder.setSync(Boolean.parseBoolean(getAttributeValue(opElement, "sync", "false")));
Collection<CacheOperation> col = cacheOpMap.computeIfAbsent(nameHolder, k -> new ArrayList<>(2));
Collection<CacheOperation> col = cacheOpMap.computeIfAbsent(nameHolder, key -> new ArrayList<>(2));
col.add(builder.build());
}
@@ -136,7 +136,7 @@ class CacheAdviceParser extends AbstractSingleBeanDefinitionParser {
builder.setBeforeInvocation(Boolean.parseBoolean(after.trim()));
}
Collection<CacheOperation> col = cacheOpMap.computeIfAbsent(nameHolder, k -> new ArrayList<>(2));
Collection<CacheOperation> col = cacheOpMap.computeIfAbsent(nameHolder, key -> new ArrayList<>(2));
col.add(builder.build());
}
@@ -150,7 +150,7 @@ class CacheAdviceParser extends AbstractSingleBeanDefinitionParser {
parserContext.getReaderContext(), new CachePutOperation.Builder());
builder.setUnless(getAttributeValue(opElement, "unless", ""));
Collection<CacheOperation> col = cacheOpMap.computeIfAbsent(nameHolder, k -> new ArrayList<>(2));
Collection<CacheOperation> col = cacheOpMap.computeIfAbsent(nameHolder, key -> new ArrayList<>(2));
col.add(builder.build());
}
@@ -28,7 +28,7 @@ import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.Aware;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.core.MethodClassKey;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
@@ -137,8 +137,8 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
// Skip methods declared on BeanFactoryAware and co.
if (method.getDeclaringClass().isInterface() && Aware.class.isAssignableFrom(method.getDeclaringClass())) {
// Skip setBeanFactory method on BeanFactoryAware.
if (method.getDeclaringClass() == BeanFactoryAware.class) {
return null;
}
@@ -115,6 +115,13 @@ public abstract class AbstractCacheManager implements CacheManager, Initializing
return this.cacheNames;
}
@Override
public void resetCaches() {
synchronized (this.cacheMap) {
this.cacheMap.values().forEach(Cache::clear);
}
}
// Common cache initialization delegates for subclasses
@@ -119,4 +119,11 @@ public class CompositeCacheManager implements CacheManager, InitializingBean {
return Collections.unmodifiableSet(names);
}
@Override
public void resetCaches() {
for (CacheManager manager : this.cacheManagers) {
manager.resetCaches();
}
}
}
@@ -18,8 +18,6 @@ package org.springframework.cache.support;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@@ -33,44 +31,33 @@ import org.springframework.cache.CacheManager;
* for disabling caching, typically used for backing cache declarations
* without an actual backing store.
*
* <p>Will simply accept any items into the cache not actually storing them.
* <p>This implementation will simply accept any items into the cache,
* not actually storing them.
*
* @author Costin Leau
* @author Stephane Nicoll
* @author Juergen Hoeller
* @since 3.1
* @see NoOpCache
*/
public class NoOpCacheManager implements CacheManager {
private final ConcurrentMap<String, Cache> caches = new ConcurrentHashMap<>(16);
private final Set<String> cacheNames = new LinkedHashSet<>(16);
private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>(16);
/**
* This implementation always returns a {@link Cache} implementation that will not store items.
* Additionally, the request cache will be remembered by the manager for consistency.
*/
@Override
public @Nullable Cache getCache(String name) {
Cache cache = this.caches.get(name);
if (cache == null) {
this.caches.computeIfAbsent(name, NoOpCache::new);
synchronized (this.cacheNames) {
this.cacheNames.add(name);
}
}
return this.caches.get(name);
return this.cacheMap.computeIfAbsent(name, NoOpCache::new);
}
/**
* This implementation returns the name of the caches previously requested.
*/
@Override
public Collection<String> getCacheNames() {
synchronized (this.cacheNames) {
return Collections.unmodifiableSet(this.cacheNames);
}
return Collections.unmodifiableSet(this.cacheMap.keySet());
}
@Override
public void resetCaches() {
this.cacheMap.clear();
}
}
@@ -61,9 +61,9 @@ public interface ApplicationContext extends EnvironmentCapable, ListableBeanFact
/**
* Return the unique id of this application context.
* @return the unique id of the context, or {@code null} if none
* @return the unique id of the context (never null as of 7.0.2)
*/
@Nullable String getId();
String getId();
/**
* Return a name for the deployed application that this context belongs to.
@@ -42,16 +42,20 @@ import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.aop.framework.autoproxy.AutoProxyUtils;
import org.springframework.aot.generate.AccessControl;
import org.springframework.aot.generate.GeneratedClass;
import org.springframework.aot.generate.GeneratedMethod;
import org.springframework.aot.generate.GeneratedMethods;
import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.generate.MethodReference;
import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator;
import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.ReflectionHints;
import org.springframework.aot.hint.ResourceHints;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.TypeReference;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.BeanDefinitionStoreException;
@@ -872,11 +876,14 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
@Override
public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) {
GeneratedMethod generatedMethod = beanFactoryInitializationCode.getMethods().add(
"applyBeanRegistrars", builder -> this.generateApplyBeanRegistrarsMethod(builder, generationContext));
"applyBeanRegistrars", builder -> this.generateApplyBeanRegistrarsMethod(builder,
generationContext, beanFactoryInitializationCode.getClassName()));
beanFactoryInitializationCode.addInitializer(generatedMethod.toMethodReference());
}
private void generateApplyBeanRegistrarsMethod(MethodSpec.Builder method, GenerationContext generationContext) {
private void generateApplyBeanRegistrarsMethod(MethodSpec.Builder method, GenerationContext generationContext,
ClassName className) {
ReflectionHints reflectionHints = generationContext.getRuntimeHints().reflection();
method.addJavadoc("Apply bean registrars.");
method.addModifiers(Modifier.PRIVATE);
@@ -915,7 +922,7 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
}
}
}
method.addCode(generateRegisterCode());
method.addCode(generateRegisterCode(className, generationContext));
}
private void checkUnsupportedFeatures(AbstractBeanDefinition beanDefinition) {
@@ -937,37 +944,79 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
return code.build();
}
private CodeBlock generateRegisterCode() {
private CodeBlock generateRegisterCode(ClassName className, GenerationContext generationContext) {
Builder code = CodeBlock.builder();
Builder metadataReaderFactoryCode = null;
NameAllocator nameAllocator = new NameAllocator();
for (Map.Entry<String, List<BeanRegistrar>> beanRegistrarEntry : this.beanRegistrars.entrySet()) {
for (BeanRegistrar beanRegistrar : beanRegistrarEntry.getValue()) {
String beanRegistrarName = nameAllocator.newName(StringUtils.uncapitalize(beanRegistrar.getClass().getSimpleName()));
code.addStatement("$T $L = new $T()", beanRegistrar.getClass(), beanRegistrarName, beanRegistrar.getClass());
Constructor<?> constructor = BeanUtils.getResolvableConstructor(beanRegistrar.getClass());
boolean visible = isVisible(constructor, className);
if (visible) {
code.addStatement("$T $L = new $T()", beanRegistrar.getClass(), beanRegistrarName, beanRegistrar.getClass());
}
else {
try {
Class<?> configClass = ClassUtils.forName(beanRegistrarEntry.getKey(), beanRegistrar.getClass().getClassLoader());
GeneratedClass generatedClass = generationContext.getGeneratedClasses()
.getOrAddForFeatureComponent("BeanRegistrars", configClass, type ->
type.addJavadoc("Bean registrars for {@link $T}.", configClass)
.addModifiers(Modifier.PUBLIC));
GeneratedMethod generatedMethod = generatedClass.getMethods().add(
"get" + beanRegistrar.getClass().getSimpleName(),
method -> method
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(BeanRegistrar.class)
.addStatement("return new $T()", beanRegistrar.getClass()));
code.addStatement("$T $L = $L", BeanRegistrar.class, beanRegistrarName,
generatedMethod.toMethodReference().toInvokeCodeBlock(ArgumentCodeGenerator.none()));
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException(ex);
}
}
if (beanRegistrar instanceof ImportAware) {
if (metadataReaderFactoryCode == null) {
metadataReaderFactoryCode = CodeBlock.builder();
metadataReaderFactoryCode.addStatement("$T metadataReaderFactory = new $T()",
MetadataReaderFactory.class, CachingMetadataReaderFactory.class);
}
CodeBlock setImportMetadataCode;
if (visible) {
setImportMetadataCode = CodeBlock.builder()
.addStatement("$L.setImportMetadata(metadataReaderFactory.getMetadataReader($S).getAnnotationMetadata())",
beanRegistrarName, beanRegistrarEntry.getKey()).build();
}
else {
setImportMetadataCode = CodeBlock.builder()
.addStatement("(($T)$L).setImportMetadata(metadataReaderFactory.getMetadataReader($S).getAnnotationMetadata())",
ImportAware.class, beanRegistrarName, beanRegistrarEntry.getKey()).build();
}
code.beginControlFlow("try")
.addStatement("$L.setImportMetadata(metadataReaderFactory.getMetadataReader($S).getAnnotationMetadata())",
beanRegistrarName, beanRegistrarEntry.getKey())
.add(setImportMetadataCode)
.nextControlFlow("catch ($T ex)", IOException.class)
.addStatement("throw new $T(\"Failed to read metadata for '$L'\", ex)",
IllegalStateException.class, beanRegistrarEntry.getKey())
.endControlFlow();
}
code.addStatement("$L.register(new $T(($T)$L, $L, $L, $T.class, $L), $L)", beanRegistrarName,
code.addStatement("$L.register(new $T(($T)$L, $L, $L, $L.getClass(), $L), $L)", beanRegistrarName,
BeanRegistryAdapter.class, BeanDefinitionRegistry.class, BeanFactoryInitializationCode.BEAN_FACTORY_VARIABLE,
BeanFactoryInitializationCode.BEAN_FACTORY_VARIABLE, ENVIRONMENT_VARIABLE, beanRegistrar.getClass(),
BeanFactoryInitializationCode.BEAN_FACTORY_VARIABLE, ENVIRONMENT_VARIABLE, beanRegistrarName,
CUSTOMIZER_MAP_VARIABLE, ENVIRONMENT_VARIABLE);
}
}
return (metadataReaderFactoryCode == null ? code.build() : metadataReaderFactoryCode.add(code.build()).build());
}
private boolean isVisible(Constructor<?> ctor, ClassName className) {
AccessControl classAccessControl = AccessControl.forClass(ctor.getDeclaringClass());
AccessControl memberAccessControl = AccessControl.forMember(ctor);
AccessControl.Visibility visibility = AccessControl.lowest(classAccessControl, memberAccessControl).getVisibility();
return (visibility == AccessControl.Visibility.PUBLIC || (visibility != AccessControl.Visibility.PRIVATE &&
ctor.getDeclaringClass().getPackageName().equals(className.packageName())));
}
private CodeBlock generateInitDestroyMethods(String beanName, AbstractBeanDefinition beanDefinition,
String[] methodNames, String method, ReflectionHints reflectionHints) {
@@ -168,22 +168,25 @@ public class ContextAnnotationAutowireCandidateResolver extends QualifierAnnotat
}
}
boolean cacheable = true;
for (String autowiredBeanName : autowiredBeanNames) {
if (!this.beanFactory.containsBean(autowiredBeanName)) {
cacheable = false;
}
else {
if (this.beanName != null) {
this.beanFactory.registerDependentBean(autowiredBeanName, this.beanName);
}
if (!this.beanFactory.isSingleton(autowiredBeanName)) {
boolean cacheable = false;
if (!autowiredBeanNames.isEmpty()) {
cacheable = true;
for (String autowiredBeanName : autowiredBeanNames) {
if (!this.beanFactory.containsBean(autowiredBeanName)) {
cacheable = false;
}
else {
if (this.beanName != null) {
this.beanFactory.registerDependentBean(autowiredBeanName, this.beanName);
}
if (!this.beanFactory.isSingleton(autowiredBeanName)) {
cacheable = false;
}
}
}
if (cacheable) {
this.cachedTarget = target;
}
}
if (cacheable) {
this.cachedTarget = target;
}
return target;
@@ -54,5 +54,4 @@ public @interface Proxyable {
*/
Class<?>[] interfaces() default {};
}
@@ -57,7 +57,7 @@ public class ApplicationContextAotGenerator {
new ApplicationContextInitializationCodeGenerator(applicationContext, generationContext);
DefaultListableBeanFactory beanFactory = applicationContext.getDefaultListableBeanFactory();
new BeanFactoryInitializationAotContributions(beanFactory).applyTo(generationContext, codeGenerator);
return codeGenerator.getGeneratedClass().getName();
return codeGenerator.getClassName();
});
}
@@ -127,8 +127,9 @@ class ApplicationContextInitializationCodeGenerator implements BeanFactoryInitia
return ArgumentCodeGenerator.from(new InitializerMethodArgumentCodeGenerator());
}
GeneratedClass getGeneratedClass() {
return this.generatedClass;
@Override
public ClassName getClassName() {
return this.generatedClass.getName();
}
@Override
@@ -623,12 +623,22 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
finishRefresh();
}
catch (RuntimeException | Error ex ) {
catch (RuntimeException | Error ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}
// Stop already started Lifecycle beans to avoid dangling resources.
if (this.lifecycleProcessor != null && this.lifecycleProcessor.isRunning()) {
try {
this.lifecycleProcessor.stop();
}
catch (Throwable ex2) {
logger.warn("Exception thrown from LifecycleProcessor on cancelled refresh", ex2);
}
}
// Destroy already created singletons to avoid dangling resources.
destroyBeans();

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