Compare commits

..

107 Commits

Author SHA1 Message Date
Brian Clozel 1526ceec1d Release v7.0.6 2026-03-13 08:56:05 +01:00
Sébastien Deleuze 739d5ba77b Leverage ResourceHandlerUtils in ScriptTemplateView
This commit apply extra checks to ScriptTemplateView resource handling
with ResourceHandlerUtils, consistently with what is done with static
resource handling.

Closes gh-36458
2026-03-12 19:43:17 +01:00
Sébastien Deleuze 19ab92ef59 Restore ScriptTemplateViewTests
Restore both WebMVC and WebFlux variants that were deleted
by mistake in commit 4db2f8ea1b.

This commit also removes the empty resource loader path, as it is not
needed for the main WEB-INF/ use case that is typically configured
explicitly by the user, and not needed to pass the restored tests.

Closes gh-36456
2026-03-12 19:42:28 +01:00
Sam Brannen 04313f062e Improve documentation for FullyQualifiedConfigurationBeanNameGenerator
This commit documents FullyQualifiedConfigurationBeanNameGenerator in
related Javadoc and in the reference manual.

See gh-33448
Closes gh-36455
2026-03-12 17:03:45 +01:00
Sam Brannen e634ced56b Fix log message in ConfigurationClassBeanDefinitionReader
The log message now properly generates the fully-qualified method name
and includes the resolved bean name as well.

Closes gh-36453
2026-03-12 16:23:45 +01:00
Sam Brannen 1502c2296e Remove redundant method invocation 2026-03-12 16:17:23 +01:00
Brian Clozel cc5c7ba186 Fix enclosing class resolution with ClassFile API
Prior to this commit, the `ClassFile` based implementation of
`AnnotationMetadata` would rely on the `NestHost` class element to get
the enclosing class name for a nested class.
This approach works for bytecode emitted by Java11+, which aligns with
our Java17+ runtime policy. But there are cases where bytecode was not
emitted by a Java11+ compiler, such as Kotlin. In this case, the
`NestHost` class element is absent and we should instead use the
`InnerClasses` information to get it.

This commit makes use of `InnerClasses` to get the enclosing class name,
but still uses `NestHost` as a fallback for anonymous classes.

Fixes gh-36451
2026-03-12 10:47:39 +01:00
Sam Brannen 0269eb80da Fix typo and improve Javadoc for ConfigurationBeanNameGenerator 2026-03-11 17:18:11 +01:00
Sam Brannen 9cedcd65ef Polishing 2026-03-11 16:45:30 +01:00
Sam Brannen 4e8acb9ef2 Polish contribution
See gh-36448
2026-03-11 16:45:30 +01:00
gbouwen dbbbf37ac9 Update exception message in JacksonJsonMessageConverter
Closes gh-36448

Signed-off-by: gbouwen <57719521+gbouwen@users.noreply.github.com>
2026-03-11 16:41:19 +01:00
Juergen Hoeller d8216d719b Upgrade to SnakeYAML 2.6, Protobuf 4.34, H2 2.4.240 2026-03-11 15:53:42 +01:00
Juergen Hoeller 63ced3f3b4 Polishing 2026-03-11 13:52:34 +01:00
Juergen Hoeller 89391fd94c Consistently ignore non-loadable annotation types with ClassFile/ASM
Uses ClassFileAnnotationMetadata name for actual AnnotationMetadata.
Moves JSR-305 dependency to compile-only for all spring-core tests.

Closes gh-36432
2026-03-11 13:52:19 +01:00
rstoyanchev 5168e3a38b Polishing contribution
Closes gh-36400
2026-03-10 19:14:41 +00:00
husseinvr97 d4893fb32f Support defaultHtmlEscape in WebFlux
See gh-36400

Signed-off-by: husseinvr97 <husseinmustafabas05@gmail.com>
2026-03-10 19:14:41 +00:00
Juergen Hoeller 104a2033c5 Upgrade to Reactor 2025.0.4 and Micrometer 1.16.4
Includes Jetty 12.1.7, Hibernate ORM 7.2.6, EclipseLink 5.0.0-RC1, Selenium 4.41, Mockito 5.22, Checkstyle 13.3

Closes gh-36443
Closes gh-36444
2026-03-10 20:09:20 +01:00
Brian Clozel cc1514be85 Fix DefaultResponseErrorHandler method visibility
Prior to this commit, the `setMessageConverters` method would have
private visibility. But `initBodyConvertFunction`, which is `protected`,
relies on the message converters being set in the first place.
While this works with `RestTemplate` because this is done automatically,
the `RestClient` does offer a builder method to configure a
`ResponseErrorHandler` and this makes it impossible to configure
converters in this case.

This commit aligns the method visibility by making it protected.

Closes gh-36434
2026-03-10 18:08:09 +01:00
Brian Clozel 6e9758700a Guard against invalid id/event values in Server Sent Events
Prior to this commit, our implementation of Server Sent Events (SSE),
`SseEmitter` (MVC) and `ServerSentEvent` (WebFlux), would not guard
against invalid characters if the application mistakenly inserts such
characters in the `id` or `event` types.
Both implementations would also behave differently when it comes
to escaping comment multi-line events.

This commit ensures that both implementations handle multi-line comment
events and reject invalid characters in id/event types.
This commit also optimizes `String` concatenation and memory usage
when writing data.

Fixes gh-36440
2026-03-10 17:56:58 +01:00
Sam Brannen 37e8aa76e9 Use link for first reference to @⁠Fallback in @⁠Bean Javadoc
See gh-36439
2026-03-10 17:53:33 +01:00
Sam Brannen 27686dc2e2 Document @Fallback alongside Primary in the reference docs and @Bean Javadoc
Closes gh-36439
2026-03-10 17:13:23 +01:00
Sam Brannen 3b8efbe5a6 Document registration recommendations for BeanPostProcessor and BeanFactoryPostProcessor
Closes gh-34964
2026-03-10 13:07:13 +01:00
Sam Brannen 6f2e59a995 Polishing 2026-03-10 13:07:12 +01:00
Sam Brannen 644a20ae2a Polish @⁠Bean Javadoc and reference docs 2026-03-10 12:24:53 +01:00
rstoyanchev 77bc13dc8c Polishing contribution
See gh-36325
2026-03-09 17:52:12 +00:00
rstoyanchev ab9895b5a9 Polishing contribution
Closes gh-36325
2026-03-09 17:49:25 +00:00
Agil ce5c4f3b4b Refactor calculateHashCode method for clarity
This will:
1. Mathematical Distribution (Collision Reduction)
2. Pipelining and CPU Caching
3. Avoiding "Method Heavy" Expressions

See gh-36325

Signed-off-by: Agil <41694337+AgilAghamirzayev@users.noreply.github.com>
2026-03-09 17:38:15 +00:00
rstoyanchev cd0c26b6db Update Javadoc on binding flag in ModelAttribute
Add clarification and mention primary use case.
2026-03-09 14:25:47 +00:00
조형준 1203ace285 Fix typo in HttpExchangeAdapterDecorator Javadoc
Closes gh-36433

Signed-off-by: jun <ryuu.public@gmail.com>
2026-03-09 14:46:20 +01:00
Sam Brannen 1219943e73 Polishing
See gh-36323
2026-03-09 11:15:19 +01:00
Sébastien Deleuze eeddf9a73a Fix transaction managemement snippet and documentation
Closes gh-36323
2026-03-09 10:29:45 +01:00
jisub-dev b2a4bc8900 Add programmatic configuration code snippet
- Extract code examples to separate Java, Kotlin, and XML files
- Add Kotlin configuration sample alongside Java
- Change "Java Config" terminology to "Programmatic Configuration"
- Use include-code directive for better maintainability

See gh-36323
Signed-off-by: jisub-dev <kimjiseob1209@gmail.com>
2026-03-09 10:12:20 +01:00
Sam Brannen 3a266e65d6 Revise nullability in AnnotationConfigContextLoaderUtils 2026-03-08 14:42:17 +01:00
Sam Brannen 463138acbc Resolve context initializers only once in AbstractTestContextBootstrapper
The internal buildMergedContextConfiguration() method in
AbstractTestContextBootstrapper originally resolved the
ApplicationContextInitializer set only once. However, the changes made
in commit 2244461778 introduced a regression resulting in the
initializers being resolved twice: once for validation and once for
actually building the merged context configuration. In addition, the
resolution for validation does not honor the inheritInitializers flag
in ContextConfigurationAttributes.

To address these issues, buildMergedContextConfiguration() once again
resolves the context initializers once via
ApplicationContextInitializerUtils.resolveInitializerClasses().

See gh-18528
Closes gh-36430
2026-03-08 14:32:06 +01:00
Tran Ngoc Nhan b8a4961b81 Remove unused imports in framework-docs examples
Closes gh-36429

Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-03-07 15:11:05 +01:00
rstoyanchev 06d4a8fc4b Fix case sensitivity issue in ServletRequestHeadersAdapter
The RequestHeaderOverrideWrapper did not deduplicate in keySet()
across underlying headers and overrides.

A similar change in size() even if it was working correctly,
to align with keySet and make it more efficient.

Closes gh-36418
2026-03-06 16:51:27 +00:00
Sam Brannen 13b0643828 Invoke resolveContextLoader() only once in AbstractTestContextBootstrapper
See commit c2d0d292bd
See gh-35994
Closes gh-36425
2026-03-06 16:05:30 +01:00
Sam Brannen 548b2167b0 Simplify changes made in commit c2d0d292bd
See gh-36390
2026-03-06 13:13:18 +01:00
froggy0m0 de417beff7 Fix typo in ExceptionMapingComparator name
Closes gh-36422

Signed-off-by: 신성현 <froggy0m0a@gmail.com>
2026-03-06 11:57:14 +01:00
rstoyanchev ccc6789cb5 Update docs for path API version resolver
See gh-36398
2026-03-05 17:21:43 +00:00
rstoyanchev f73302a66e Polishing contribution
Closes gh-36398
2026-03-05 17:12:13 +00:00
Martin Mois 348482b5a0 Add usePathSegment with Predicate<RequestPath>
See gh-36398

Signed-off-by: Martin Mois <martin.mois@gmail.com>
2026-03-05 17:12:13 +00:00
rstoyanchev 25bb264bd9 Polishing in ReactorNettyWebSocketClient
Also, remove local variables that should have been removed when
the corresponding, deprecated setters were removed in
2ed281f6a8.

Closes gh-36370
2026-03-05 17:12:13 +00:00
Artem Voronin 9819e1cad5 Propagate max frame length to WebSocket session
This change ensures that maxFramePayloadLength from
WebsocketClientSpec is passed to ReactorNettyWebSocketSession.

Previously, the session used the default 64KB limit regardless
of client configuration, causing TooLongFrameException when
receiving larger frames from servers like Tomcat or Jetty.

See gh-36370

Signed-off-by: Artem Voronin <artem.voronin.dev@gmail.com>
2026-03-05 17:12:13 +00:00
rstoyanchev ce2402a581 Link to Tomcat issue in ServletResponseHeadersAdapter 2026-03-05 17:12:13 +00:00
Sam Brannen c2d0d292bd Re-resolve ContextLoader in logWarningForIgnoredDefaultConfig()
Ideally, we should be able to reuse the ContextLoader that was resolved
in buildDefaultMergedContextConfiguration(); however, since we have
been informed that some implementations of resolveContextLoader()
mutate the state of the supplied ContextConfigurationAttributes to
detect default configuration classes, we need to invoke
resolveContextLoader() on the completeDefaultConfigAttributesList as
well in order not to break custom TestContextBootstrappers that exhibit
such side effects.

Closes gh-36390
2026-03-05 15:28:54 +01:00
Sam Brannen bbe733def7 Address Checkstyle violation 2026-03-05 11:43:06 +01:00
Stéphane Nicoll 03391dfa94 Switch serializable flag to primitive boolean
This commit improves the change made to TypeHint to use a primitive
boolean for the serializable flag.

See gh-36379
2026-03-05 11:23:51 +01:00
Sam Brannen 50c29e64f8 Polish documentation for FrameworkServlet and HttpServletBean 2026-03-05 11:21:44 +01:00
Sam Brannen 22301fd069 Further align synthesized annotation toString() with modern JDKs
Closes gh-36417
2026-03-04 17:39:02 +01:00
Sam Brannen 11ab0b4351 Upgrade to Gradle 9.4
Closes gh-36416
2026-03-04 13:43:07 +01:00
Juergen Hoeller 4734c15d81 Polishing 2026-03-03 23:28:37 +01:00
Sam Brannen 02af3e7a17 Introduce setDefaultCharset() in AbstractResourceBasedMessageSource
Traditionally, AbstractResourceBasedMessageSource has only had a
setDefaultEncoding() method which accepts the name of the default
encoding character set. However, although we have recently made a
concerted effort within the framework to introduce support for
supplying a Charset instead of a character set's name, we had
overlooked this particular scenario.

In light of that, this commit introduces setDefaultCharset(Charset) and
getDefaultCharset() methods in AbstractResourceBasedMessageSource and
makes direct use of the available Charset in
ReloadableResourceBundleMessageSource and ResourceBundleMessageSource.

Furthermore, although technically a regression in behavior, invoking
setDefaultEncoding() on such MessageSource implementations with an
invalid character set name now results in an immediate
UnsupportedCharsetException at configuration time instead of a
NoSuchMessageException at runtime, which will help users to more easily
detect misconfiguration.

Closes gh-36413
2026-03-03 18:08:21 +01:00
cetf 3c6f1b35ba Fix format string argument count
Fixes: gh-36410
Signed-off-by: cetf <cetf9h@163.com>
2026-03-03 12:14:33 +01:00
Sam Brannen 7a420b28df Revise custom @⁠BootstrapWith use case in test
See https://github.com/spring-projects/spring-boot/issues/15077
See gh-35938
2026-03-02 17:07:30 +01:00
Sam Brannen 63723fadb6 Introduce tests for additional @⁠BootstrapWith use cases
See gh-35938
2026-03-02 16:34:39 +01:00
Sam Brannen ab7a2c4e70 Rename configuration.interfaces package to config.interfaces
This commit renames the
org.springframework.test.context.configuration.interfaces package to
org.springframework.test.context.config.interfaces in order to colocate
"config" related tests under the same package.
2026-03-02 16:07:43 +01:00
dependabot[bot] a87b2e0146 Upgrade fast-xml-parser to 5.3.8
Closes gh-36402

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-01 13:13:43 +01:00
Sam Brannen bd40f0e6bb Fix typo 2026-03-01 13:03:40 +01:00
Sam Brannen 04186fdf0e Consistently refer to URLs and URIs in documentation 2026-03-01 13:03:14 +01:00
Tran Ngoc Nhan b2b731b0ba Fix links to UriComponentsBuilder and polish examples
Closes gh-36403

Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-03-01 12:51:43 +01:00
Juergen Hoeller a3b9098850 Remove prefixed FactoryBean name in ApplicationListenerDetector
Closes gh-36404
2026-03-01 12:08:19 +01:00
Juergen Hoeller 36815f4021 Add support for JPA 4.0 FlushModeType.EXPLICIT
Includes defensive nullable field access in EclipseLinkConnectionHandle.

Closes gh-36401
2026-02-28 19:37:27 +01:00
Sam Brannen 3b1b7ff594 Add Checkstyle suppression 2026-02-28 16:22:57 +01:00
Sam Brannen 95080cabfd Polishing 2026-02-28 16:01:01 +01:00
조형준 017892265e Fix typo in WebMvcConfigurationSupport Javadoc
Closes gh-#36399

Signed-off-by: jun <ryuu.public@gmail.com>
2026-02-28 14:16:14 +01:00
Juergen Hoeller d096a33f13 Add note on test difference against Jackson 3.1.0 2026-02-28 13:36:39 +01:00
Juergen Hoeller b6833ff31f Cancel late-executing tasks within revised closed handling
Closes gh-36362
2026-02-28 13:34:01 +01:00
Stéphane Nicoll 3bc55c77ec Add support for declaring reflection metadata for lambdas
Closes gh-36339
2026-02-27 14:42:46 +01:00
Sam Brannen 824aa137f8 Align implementation of injectDependencies() with injectDependenciesInAotMode() 2026-02-26 18:01:01 +01:00
Sam Brannen 0dc44f79d5 Avoid superfluous MergedContextConfiguration equals() check
See gh-36390
2026-02-26 12:56:38 +01:00
Sam Brannen f481f99516 Avoid unnecessary List and Stream creation
See gh-36390
2026-02-26 12:02:38 +01:00
Sam Brannen 72ed2eaa31 Fix lambda variable names
See gh-36390
2026-02-26 11:57:32 +01:00
Sam Brannen 0676c958f7 Log warning for ignored default context configuration in tests
In Spring Framework 7.1, the TestContext Framework will reliably detect
all default context configuration within the type hierarchy or
enclosing class hierarchy (for @⁠Nested test classes) above a given
test class; however, test suites may encounter failures once we make
that switch in behavior.

In order to help development teams prepare for the switch in 7.1, this
commit logs a warning similar to the following whenever default context
configuration is detected but currently ignored.

  WARN - For test class [org.example.MyTests$NestedTests], the
  following 'default' context configuration classes were detected but
  are currently ignored: org.example.MyTests$Config. In Spring
  Framework 7.1, these classes will no longer be ignored. Please update
  your test configuration accordingly. For details, see:
  https://docs.spring.io/spring-framework/reference/testing/testcontext-framework/ctx-management/default-config.html

See gh-31456
See gh-36392
Closes gh-36390
2026-02-26 11:24:02 +01:00
Sam Brannen 482519d03b Document how to avoid issues w/ ignored default context config in tests
See gh-31456
See gh-36390
Closes gh-36392
2026-02-25 17:36:14 +01:00
Sam Brannen d6b7cea327 Polishing 2026-02-25 17:34:14 +01:00
Sam Brannen 9eea227ab9 Further emphasize @⁠Configuration classes over XML and Groovy in testing chapter
See gh-36393
2026-02-25 15:44:00 +01:00
Sam Brannen 706c98228d Emphasize @⁠Configuration classes over XML and Groovy in testing chapter
Closes gh-36393
2026-02-25 15:26:32 +01:00
rstoyanchev b83cb4a947 Make newRequest methods public
The methods to build a request in RestClientAdapter and WebClientAdapter
are now public to make it easier to create a custom adapter that wraps
the built-in ones and delegates for methods that can be the same.

Closes gh-36374
2026-02-25 12:43:19 +00:00
rstoyanchev 7e79d0e206 Polishing in DefaultRestClientTests 2026-02-25 12:43:19 +00:00
rstoyanchev e58ba2c665 Simplify use of InputStream with RestClient
Closes gh-36380
2026-02-25 12:43:19 +00:00
Sam Brannen 0c9127d111 Use consistent indentation in Gradle build scripts 2026-02-25 11:15:16 +01:00
Sam Brannen c2088517b9 Upgrade to JUnit 6.0.3
Closes gh-36389
2026-02-25 11:05:05 +01:00
Sam Brannen 59b9057e59 Polishing 2026-02-25 10:52:28 +01:00
Sam Brannen 2ec630ef45 Remove obsolete EMPTY_GROUPS constant in InvocableHandlerMethod
See gh-36274
2026-02-25 10:50:45 +01:00
Sam Brannen cf2b59b68b Delete obsolete addCharsetParameter() method
See gh-36318
2026-02-25 10:50:45 +01:00
Sam Brannen f1ca29b05d Move NOTE in "Programmatic Retry Support" to correct location 2026-02-25 10:28:23 +01:00
dependabot[bot] ee6a115a18 Upgrade fast-xml-parser to 5.3.6
Closes gh-36348

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 10:08:56 +01:00
Juergen Hoeller 95a31c4e8c Align constant name in ServletServerHttpResponse
See gh-36385
2026-02-25 09:29:22 +01:00
Brian Clozel e0b54e244e Ignore flushes on ServletServerHttpResponse output stream
Prior to this commit, flush calls on the output stream returned by
`ServletServerHttpResponse#getBody` would be delegated to the Servlet
response output stream.
This can cause performance issues when `HttpMessageConverter` and other
web components write and flush multiple times to the response body.
Here, the Servlet container is in a better position to flush to the
network at the optimal time and buffer the response body until then.

This is particularly true for `HttpMessageConverters` when they flush
many times the output stream, sometimes due to the underlying codec
library. Instead of revisiting the entire message converter contract, we
are here ignoring flush calls to that output stream.

This change does not affect the client side, nor the
`ServletServerHttpResponse#flush` calls.

This commit also introduces a new Spring property
`"spring.http.response.flush.enabled"` that reverts this behavior change
if necessary.

Closes gh-36385
2026-02-24 21:44:48 +01:00
Juergen Hoeller f2b64ad09c Polishing 2026-02-24 17:17:31 +01:00
Juergen Hoeller cff48fff2d Reject late-executing tasks after termination waiting
Closes gh-36362
2026-02-24 17:17:21 +01:00
Juergen Hoeller ab8cb8de6e Add support for non-flushing OutputStream
Avoids decoration for streams that are non-closing/flushing already.

Closes gh-36382
2026-02-24 17:17:04 +01:00
Stéphane Nicoll 434da06d1f Migrate to GraalVM metadata format 1.2.0
This commit adapts RuntimeHints to be compatible with v1.2.0 of the
GraalVM metadata format. The changes are as follows:

* Resources and resource bundles are now defined at the same level. This
is transparent to users.
* Serialization is no longer a top-level concept, but rather an
attribute of a type or proxy. The top-level concept has been deprecated
and the relevant methods have been added.

Closes gh-36379
2026-02-24 17:05:50 +01:00
Brian Clozel a37447e07f Remove duplicate flushes in HttpMessageConverter implementations
Prior to this commit, a few implementations of the `HttpMessageConverter`
contract were inheriting from abstract classes. Those classes were
performing extra `OutputStream#flush` on the response body even though
this is the responsibility of the super class. Such abstract classes
do flush already, after delegating to the `writeInternal` method.

This commit ensures that we remove such extra calls as they tend to
waste resources for no added benefit.

Closes gh-36383
2026-02-24 16:49:37 +01:00
Sam Brannen b1cb9c5052 Polish contribution
See gh-36367
2026-02-23 17:24:13 +01:00
Tran Ngoc Nhan 057633edb5 Polish SpEL operator examples in reference docs
Closes gh-36367

Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-02-23 17:08:22 +01:00
Brian Clozel 7299ff9326 Improve ResourceHttpMessageConverter target type support
This commit updates the target type detection in
`ResourceHttpMessageConverter` to only support target types that are
relevant: `InputStreamResource` for streaming, and types assignable from
`ByteArrayResource` for non-streaming cases.

Closes gh-36368
2026-02-23 11:02:22 +01:00
조형준 53f1656f56 Fix typo in CorsConfiguration Javadoc
Closes gh-36366

Signed-off-by: jun <ryuu.public@gmail.com>
2026-02-22 16:14:45 +01:00
Brian Clozel 2691489381 Fix InvalidMimeTypeException for compatible media types
The `AbstractMessageConverterMethodProcessor` is in charge of handling
controller method return values and to write those as HTTP response
messages. The content negotiation process is an important part.

The `MimeTypeUtils#sortBySpecificity` is in charge of sorting inbound
"Accept" media types by their specificity and reject them if the list
is too large, in order to protect the application from ddos attacks.

Prior to this commit, the content negotiation process would first get
the sorted "Accept" media types, the producible media types as
advertized by message converters - and collect the intersection of both
in a new list (also sorted by specificity). If the "Accept" list is
large enough (but under the limit), the list of compatible media types
could exceed that limit because duplicates could be introduced in that
list: several converters can produce the same content type.

This commit ensures that compatible media types are collected in a set
to avoid duplicates. Without that, exceeding the limit at this point
will throw an `InvalidMimeTypeException` that's not handled by the
processor and result in a server error.

Fixes gh-36300
2026-02-20 18:21:45 +01:00
Tran Ngoc Nhan f103af4982 Remove obsolete space in HandlerMethod.html#assertTargetBean javadoc
Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-02-20 13:43:11 +00:00
rstoyanchev b9a59853fa Use case-insensitive comparator in HttpHeadersAssert
Closes gh-36349
2026-02-20 13:38:51 +00:00
rstoyanchev d4b2a493f9 Fix duplicate header writing in ResponseBodyEmitterReturnValueHandler
Closes gh-36357
2026-02-20 10:45:22 +00:00
rstoyanchev 188cb9b24d Update Javadoc of RestClient.Builder defaultStatusHandler
See gh-36248
2026-02-19 11:42:59 +00:00
rstoyanchev e3568a3f0a Fix generic return type support in HttpServiceMethod
Closes gh-36326
2026-02-18 17:27:02 +00:00
rstoyanchev e106fc0434 Polishing in RestClientAdapterTests and WebClientAdapterTests 2026-02-18 17:27:02 +00:00
Stéphane Nicoll ca3adf43eb Next development version (v7.0.6-SNAPSHOT) 2026-02-18 14:07:54 +01:00
232 changed files with 4831 additions and 1859 deletions
+12 -12
View File
@@ -68,18 +68,18 @@ configure([rootProject] + javaProjects) { project ->
}
ext.javadocLinks = [
"https://docs.oracle.com/en/java/javase/17/docs/api/",
//"https://jakarta.ee/specifications/platform/11/apidocs/",
"https://docs.hibernate.org/orm/7.2/javadocs/",
"https://www.quartz-scheduler.org/api/2.3.0/",
"https://hc.apache.org/httpcomponents-client-5.6.x/current/httpclient5/apidocs/",
"https://projectreactor.io/docs/core/release/api/",
"https://projectreactor.io/docs/test/release/api/",
"https://junit.org/junit4/javadoc/4.13.2/",
"https://docs.junit.org/6.0.2/api/",
"https://www.reactive-streams.org/reactive-streams-1.0.4-javadoc/",
"https://r2dbc.io/spec/1.0.0.RELEASE/api/",
"https://jspecify.dev/docs/api/"
"https://docs.oracle.com/en/java/javase/17/docs/api/",
//"https://jakarta.ee/specifications/platform/11/apidocs/",
"https://docs.hibernate.org/orm/7.2/javadocs/",
"https://www.quartz-scheduler.org/api/2.3.0/",
"https://hc.apache.org/httpcomponents-client-5.6.x/current/httpclient5/apidocs/",
"https://projectreactor.io/docs/core/release/api/",
"https://projectreactor.io/docs/test/release/api/",
"https://junit.org/junit4/javadoc/4.13.2/",
"https://docs.junit.org/6.0.3/api/",
"https://www.reactive-streams.org/reactive-streams-1.0.4-javadoc/",
"https://r2dbc.io/spec/1.0.0.RELEASE/api/",
"https://jspecify.dev/docs/api/"
] as String[]
}
@@ -50,7 +50,7 @@ public class CheckstyleConventions {
project.getPlugins().apply(CheckstylePlugin.class);
project.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g"));
CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class);
checkstyle.setToolVersion("13.2.0");
checkstyle.setToolVersion("13.3.0");
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
+2 -2
View File
@@ -49,8 +49,8 @@ javadoc {
maxMemory = "1024m"
doFirst {
classpath += files(
// ensure the javadoc process can resolve types compiled from .aj sources
springAspectsOutput
// ensure the javadoc process can resolve types compiled from .aj sources
springAspectsOutput
)
classpath += files(moduleProjects.collect { it.sourceSets.main.compileClasspath })
}
+4 -4
View File
@@ -15,8 +15,8 @@ apply from: "${rootDir}/gradle/publications.gradle"
antora {
options = [clean: true, fetch: !project.gradle.startParameter.offline, stacktrace: true]
environment = [
'BUILD_REFNAME': 'HEAD',
'BUILD_VERSION': project.version,
'BUILD_REFNAME': 'HEAD',
'BUILD_VERSION': project.version,
]
}
@@ -49,8 +49,8 @@ tasks.withType(KotlinCompilationTask.class).configureEach {
javaSources.from = []
compilerOptions.jvmTarget = JvmTarget.JVM_17
compilerOptions.freeCompilerArgs.addAll(
"-Xjdk-release=17", // Needed due to https://youtrack.jetbrains.com/issue/KT-49746
"-Xannotation-default-target=param-property" // Upcoming default, see https://youtrack.jetbrains.com/issue/KT-73255
"-Xjdk-release=17", // Needed due to https://youtrack.jetbrains.com/issue/KT-49746
"-Xannotation-default-target=param-property" // Upcoming default, see https://youtrack.jetbrains.com/issue/KT-73255
)
}
+2 -1
View File
@@ -330,9 +330,10 @@
*** xref:testing/testcontext-framework/application-events.adoc[]
*** xref:testing/testcontext-framework/test-execution-events.adoc[]
*** xref:testing/testcontext-framework/ctx-management.adoc[]
**** xref:testing/testcontext-framework/ctx-management/javaconfig.adoc[]
**** xref:testing/testcontext-framework/ctx-management/xml.adoc[]
**** xref:testing/testcontext-framework/ctx-management/groovy.adoc[]
**** xref:testing/testcontext-framework/ctx-management/javaconfig.adoc[]
**** xref:testing/testcontext-framework/ctx-management/default-config.adoc[]
**** xref:testing/testcontext-framework/ctx-management/mixed-config.adoc[]
**** xref:testing/testcontext-framework/ctx-management/context-customizers.adoc[]
**** xref:testing/testcontext-framework/ctx-management/initializers.adoc[]
@@ -81,6 +81,11 @@ resolvable otherwise. See
{spring-framework-api}++/core/env/AbstractEnvironment.html#IGNORE_GETENV_PROPERTY_NAME++[`AbstractEnvironment`]
for details.
| `spring.http.response.flush.enabled`
| Configures the Spring MVC `ServletServerHttpResponse` to allow flushing on the `OutputStream`
returned by `ServletServerHttpResponse#getBody()`. By default, such flush calls are ignored and
only `ServletServerHttpResponse#flush()` will actually flush the response to the network.
| `spring.jdbc.getParameterType.ignore`
| Instructs Spring to ignore `java.sql.ParameterMetaData.getParameterType` completely.
See the note in xref:data-access/jdbc/advanced.adoc#jdbc-batch-list[Batch Operations with a List of Objects].
@@ -151,17 +151,17 @@ injected into a `Set<MovieCatalog>` annotated with `@Qualifier("action")`.
[TIP]
====
Letting qualifier values select against target bean names, within the type-matching
candidates, does not require a `@Qualifier` annotation at the injection point.
If there is no other resolution indicator (such as a qualifier or a primary marker),
for a non-unique dependency situation, Spring matches the injection point name
(that is, the field name or parameter name) against the target bean names and chooses
the same-named candidate, if any (either by bean name or by associated alias).
candidates, does not require a `@Qualifier` annotation at the injection point. If there
is no other resolution indicator (such as a qualifier, a primary marker, or a fallback
marker), for a non-unique dependency situation, Spring matches the injection point name
(that is, the field name or parameter name) against the target bean names and chooses the
same-named candidate, if any (either by bean name or by associated alias).
Since version 6.1, this requires the `-parameters` Java compiler flag to be present.
As of 6.2, the container applies fast shortcut resolution for bean name matches,
bypassing the full type matching algorithm when the parameter name matches the
bean name and no type, qualifier or primary conditions override the match. It is
therefore recommendable for your parameter names to match the target bean names.
Since version 6.1, this requires the `-parameters` Java compiler flag to be present. As
of 6.2, the container applies fast shortcut resolution for bean name matches, bypassing
the full type matching algorithm when the parameter name matches the bean name and no
type, qualifier, primary, or fallback conditions override the match. It is therefore
recommendable for your parameter names to match the target bean names.
====
As an alternative for injection by name, consider the JSR-250 `@Resource` annotation
@@ -309,7 +309,8 @@ set of multiple matches for the specific bean type (as returned by the factory m
Note that the standard `jakarta.annotation.Priority` annotation is not available at the
`@Bean` level, since it cannot be declared on methods. Its semantics can be modeled
through `@Order` values in combination with `@Primary` on a single bean for each type.
through `@Order` values in combination with `@Primary` or `@Fallback` on a single bean
for each type.
====
Even typed `Map` instances can be autowired as long as the expected key type is `String`.
@@ -27,4 +27,5 @@ with the `CustomAutowireConfigurer`
When multiple beans qualify as autowire candidates, the determination of a "`primary`" is
as follows: If exactly one bean definition among the candidates has a `primary`
attribute set to `true`, it is selected.
attribute set to `true`, it is selected. For annotation-based configuration, see
xref:core/beans/annotation-config/autowired-primary.adoc[Fine-tuning with `@Primary` or `@Fallback`].
@@ -556,18 +556,11 @@ Kotlin::
======
If you do not want to rely on the default bean-naming strategy, you can provide a custom
bean-naming strategy. First, implement the
{spring-framework-api}/beans/factory/support/BeanNameGenerator.html[`BeanNameGenerator`]
bean-naming strategy. First, implement either the
{spring-framework-api}/beans/factory/support/BeanNameGenerator.html[`BeanNameGenerator`] or
{spring-framework-api}/context/annotation/ConfigurationBeanNameGenerator.html[`ConfigurationBeanNameGenerator`]
interface, and be sure to include a default no-arg constructor. Then, provide the fully
qualified class name when configuring the scanner, as the following example annotation
and bean definition show.
TIP: If you run into naming conflicts due to multiple autodetected components having the
same non-qualified class name (i.e., classes with identical names but residing in
different packages), you may need to configure a `BeanNameGenerator` that defaults to the
fully qualified class name for the generated bean name. The
`FullyQualifiedAnnotationBeanNameGenerator` located in package
`org.springframework.context.annotation` can be used for such purposes.
qualified class name when configuring the scanner, as the following examples show.
[tabs]
======
@@ -602,6 +595,27 @@ Kotlin::
</beans>
----
[TIP]
====
If you run into naming conflicts due to multiple autodetected components having the same
non-qualified class name (for example, classes with identical names but residing in
different packages), you can configure a `BeanNameGenerator` that defaults to the
fully-qualified class name for the generated bean name. The
`FullyQualifiedAnnotationBeanNameGenerator` can be used for such purposes.
As of Spring Framework 7.0, if you encounter naming conflicts among `@Bean` methods in
`@Configuration` classes, you can alternatively configure a
`ConfigurationBeanNameGenerator` that generates unique bean names for `@Bean` methods.
The `FullyQualifiedConfigurationBeanNameGenerator` can be used to generate
fully-qualified default bean names for `@Bean` methods without an explicit `name`
attribute — for example, `com.example.MyConfig.myBean` for an `@Bean` method named
`myBean()` declared in `@Configuration` class `com.example.MyConfig`.
The `FullyQualifiedAnnotationBeanNameGenerator` and
`FullyQualifiedConfigurationBeanNameGenerator` both reside in the
`org.springframework.context.annotation` package.
====
As a general rule, consider specifying the name with the annotation whenever other
components may be making explicit references to it. On the other hand, the
auto-generated names are adequate whenever the container is responsible for wiring.
@@ -67,6 +67,13 @@ interface, clearly indicating the post-processor nature of that bean. Otherwise,
Since a `BeanPostProcessor` needs to be instantiated early in order to apply to the
initialization of other beans in the context, this early type detection is critical.
Furthermore, when registering a `BeanPostProcessor` via an `@Bean` factory method,
declare the method as `static` and ideally with no dependencies. Doing so avoids eager
initialization of the configuration class and other beans, which would make them
ineligible for full post-processing (such as auto-proxying). See the
"BeanPostProcessor-returning `@Bean` methods" section in the
{spring-framework-api}/context/annotation/Bean.html[`@Bean`] javadoc for details.
[[beans-factory-programmatically-registering-beanpostprocessors]]
.Programmatically registering `BeanPostProcessor` instances
NOTE: While the recommended approach for `BeanPostProcessor` registration is through
@@ -80,7 +87,7 @@ of execution. Note also that `BeanPostProcessor` instances registered programmat
are always processed before those registered through auto-detection, regardless of any
explicit ordering.
.`BeanPostProcessor` instances and AOP auto-proxying
.`BeanPostProcessor` instances and early initialization
[NOTE]
====
Classes that implement the `BeanPostProcessor` interface are special and are treated
@@ -90,17 +97,23 @@ of the `ApplicationContext`. Next, all `BeanPostProcessor` instances are registe
in a sorted fashion and applied to all further beans in the container. Because AOP
auto-proxying is implemented as a `BeanPostProcessor` itself, neither `BeanPostProcessor`
instances nor the beans they directly reference are eligible for auto-proxying and,
thus, do not have aspects woven into them.
thus, do not have aspects woven into them. More generally, any bean that is instantiated
during this early phase is not eligible for full post-processing by all
`BeanPostProcessor` instances.
For any such bean, you should see an informational log message: `Bean someBean is not
eligible for getting processed by all BeanPostProcessor interfaces (for example: not
eligible for auto-proxying)`.
For any such bean, you should see a WARN-level log message similar to the following.
If you have beans wired into your `BeanPostProcessor` by using autowiring or
`@Resource` (which may fall back to autowiring), Spring might access unexpected beans
when searching for type-matching dependency candidates and, therefore, make them
ineligible for auto-proxying or other kinds of bean post-processing. For example, if you
have a dependency annotated with `@Resource` where the field or setter name does not
[quote]
Bean 'someBean' of type [org.example.SomeType] is not eligible for getting processed by
all BeanPostProcessors (for example: not eligible for auto-proxying).
To minimize the number of beans affected, register a `BeanPostProcessor` with a
`static` `@Bean` method that has no dependencies (see the note above). If you have
beans wired into your `BeanPostProcessor` by using autowiring or `@Resource` (which
may fall back to autowiring), Spring might access unexpected beans when searching
for type-matching dependency candidates and, therefore, make them ineligible for
auto-proxying or other kinds of bean post-processing. For example, if you have a
dependency annotated with `@Resource` where the field or setter name does not
directly correspond to the declared name of a bean and no name attribute is used,
Spring accesses other beans for matching them by type.
====
@@ -135,7 +148,7 @@ Java::
}
public Object postProcessAfterInitialization(Object bean, String beanName) {
System.out.println("Bean '" + beanName + "' created : " + bean.toString());
System.out.println("Bean '" + beanName + "' created : " + bean);
return bean;
}
}
@@ -164,7 +177,48 @@ Kotlin::
----
======
The following `beans` element uses the `InstantiationTracingBeanPostProcessor`:
You can register the `InstantiationTracingBeanPostProcessor` with Java configuration
by using a `static` `@Bean` method (recommended to avoid eager initialization of the
configuration class and other beans):
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"]
----
@Configuration
public class AppConfig {
@Bean
public static InstantiationTracingBeanPostProcessor instantiationTracingBeanPostProcessor() {
return new InstantiationTracingBeanPostProcessor();
}
// ... other bean definitions
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages"]
----
@Configuration
class AppConfig {
@Bean
companion object {
@JvmStatic
fun instantiationTracingBeanPostProcessor() = InstantiationTracingBeanPostProcessor()
}
// ... other bean definitions
}
----
======
Alternatively, the `InstantiationTracingBeanPostProcessor` can be registered via the
`bean` element with XML configuration:
[source,xml,indent=0,subs="verbatim,quotes"]
----
@@ -192,7 +246,7 @@ The following `beans` element uses the `InstantiationTracingBeanPostProcessor`:
----
Notice how the `InstantiationTracingBeanPostProcessor` is merely defined. It does not
even have a name, and, because it is a bean, it can be dependency-injected as you would any
even have a name, and, because it is a bean, it can be dependency-injected as with any
other bean. (The preceding configuration also defines a bean that is backed by a Groovy
script.)
@@ -300,6 +354,23 @@ implement the `BeanFactoryPostProcessor` interface. It uses these beans as bean
post-processors, at the appropriate time. You can deploy these post-processor beans as
you would any other bean.
When registering a `BeanFactoryPostProcessor` via an `@Bean` factory method in a
`@Configuration` class, declare the method as `static` to avoid lifecycle conflicts
with annotation processing (such as `@Autowired`, `@Value`, and `@PostConstruct`) in the
configuration class. See the "BeanFactoryPostProcessor-returning `@Bean` methods"
section in the {spring-framework-api}/context/annotation/Bean.html[`@Bean`] javadoc
for details and an example.
For any non-static `@Bean` factory method with a `BeanFactoryPostProcessor` return type,
you should see an INFO-level log message similar to the following.
[quote]
@Bean method MyConfig.myBfpp is non-static and returns an object assignable to Spring's
BeanFactoryPostProcessor interface. This will result in a failure to process annotations
such as @Autowired, @Resource, and @PostConstruct within the method's declaring
@Configuration class. Add the 'static' modifier to this method to avoid these container
lifecycle issues; see @Bean javadoc for complete details.
NOTE: As with ``BeanPostProcessor``s , you typically do not want to configure
``BeanFactoryPostProcessor``s for lazy initialization. If no other bean references a
`Bean(Factory)PostProcessor`, that post-processor will not get instantiated at all.
@@ -4,10 +4,10 @@
`@Bean` is a method-level annotation and a direct analog of the XML `<bean/>` element.
The annotation supports some of the attributes offered by `<bean/>`, such as:
* xref:core/beans/definition.adoc#beans-beanname[name]
* xref:core/beans/factory-nature.adoc#beans-factory-lifecycle-initializingbean[init-method]
* xref:core/beans/factory-nature.adoc#beans-factory-lifecycle-disposablebean[destroy-method]
* xref:core/beans/dependencies/factory-autowire.adoc[autowiring]
* `name`.
You can use the `@Bean` annotation in a `@Configuration`-annotated or in a
`@Component`-annotated class.
@@ -17,9 +17,10 @@ You can use the `@Bean` annotation in a `@Configuration`-annotated or in a
== Declaring a Bean
To declare a bean, you can annotate a method with the `@Bean` annotation. You use this
method to register a bean definition within an `ApplicationContext` of the type
specified as the method's return value. By default, the bean name is the same as
the method name. The following example shows a `@Bean` method declaration:
method to register a bean definition within an `ApplicationContext` of the type specified
by the method's return type. By default, the bean name is the same as the method name
(unless a different xref:#beans-java-customizing-bean-naming[bean name generator] is
configured). The following example shows a `@Bean` method declaration:
[tabs]
======
@@ -126,7 +127,7 @@ Kotlin::
----
======
However, this limits the visibility for advance type prediction to the specified
However, this limits the visibility for advanced type prediction to the specified
interface type (`TransferService`). Then, with the full type (`TransferServiceImpl`)
known to the container only once the affected singleton bean has been instantiated.
Non-lazy singleton beans get instantiated according to their declaration order,
@@ -473,8 +474,15 @@ Kotlin::
== Customizing Bean Naming
By default, configuration classes use a `@Bean` method's name as the name of the
resulting bean. This functionality can be overridden, however, with the `name` attribute,
as the following example shows:
resulting bean. However, as of Spring Framework 7.0, you can change this default strategy
by configuring a custom
{spring-framework-api}/context/annotation/ConfigurationBeanNameGenerator.html[`ConfigurationBeanNameGenerator`]
when bootstrapping the context or configuring component scanning. For example,
{spring-framework-api}/context/annotation/FullyQualifiedConfigurationBeanNameGenerator.html[`FullyQualifiedConfigurationBeanNameGenerator`]
can be used to generate fully-qualified default bean names for `@Bean` methods without an
explicit `name` attribute. For individual `@Bean` methods, the default or
generator-derived name can be overridden with the `name` attribute, as the following
example shows:
[tabs]
======
@@ -505,6 +513,7 @@ Kotlin::
----
======
NOTE: `@Bean("myThing")` is equivalent to `@Bean(name = "myThing")`.
[[beans-java-bean-aliasing]]
== Bean Aliasing
@@ -53,7 +53,7 @@ Kotlin::
val trueValue = parser.parseExpression("'black' < 'block'").getValue(Boolean::class.java)
// uses CustomValue:::compareTo
val trueValue = parser.parseExpression("new CustomValue(1) < new CustomValue(2)").getValue(Boolean::class.java);
val trueValue = parser.parseExpression("new CustomValue(1) < new CustomValue(2)").getValue(Boolean::class.java)
----
======
@@ -167,7 +167,7 @@ Kotlin::
[CAUTION]
====
The syntax for the `between` operator is `<input> between {<range_begin>, <range_end>}`,
which is effectively a shortcut for `<input> >= <range_begin> && <input> \<= <range_end>}`.
which is effectively a shortcut for `<input> >= <range_begin> && <input> \<= <range_end>`.
Consequently, `1 between {1, 5}` evaluates to `true`, while `1 between {5, 1}` evaluates
to `false`.
@@ -312,13 +312,13 @@ Kotlin::
// evaluates to 'a'
val ch = parser.parseExpression("'d' - 3")
.getValue(Character::class.java);
.getValue(Char::class.java)
// -- Repeat --
// evaluates to "abcabc"
val repeated = parser.parseExpression("'abc' * 2")
.getValue(String::class.java);
.getValue(String::class.java)
----
======
@@ -485,7 +485,7 @@ Kotlin::
// -- Operator precedence --
val minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Int::class.java) // -21
val minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Int::class.java) // -21
----
======
@@ -541,32 +541,7 @@ For example, if we want to overload the `ADD` operator to allow two lists to be
concatenated using the `+` sign, we can implement a custom `OperatorOverloader` as
follows.
[source,java,indent=0,subs="verbatim,quotes"]
----
pubic class ListConcatenation implements OperatorOverloader {
@Override
public boolean overridesOperation(Operation operation, Object left, Object right) {
return (operation == Operation.ADD &&
left instanceof List && right instanceof List);
}
@Override
@SuppressWarnings("unchecked")
public Object operate(Operation operation, Object left, Object right) {
if (operation == Operation.ADD &&
left instanceof List list1 && right instanceof List list2) {
List result = new ArrayList(list1);
result.addAll(list2);
return result;
}
throw new UnsupportedOperationException(
"No overload for operation %s and operands [%s] and [%s]"
.formatted(operation, left, right));
}
}
----
include-code::./ListConcatenation[]
If we register `ListConcatenation` as the `OperatorOverloader` in a
`StandardEvaluationContext`, we can then evaluate expressions like `{1, 2, 3} + {4, 5}`
@@ -589,8 +564,8 @@ Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
StandardEvaluationContext context = StandardEvaluationContext()
context.setOperatorOverloader(ListConcatenation())
val context = StandardEvaluationContext()
context.operatorOverloader = ListConcatenation()
// evaluates to a new list: [1, 2, 3, 4, 5]
parser.parseExpression("{1, 2, 3} + {2 + 2, 5}").getValue(context, List::class.java)
@@ -183,9 +183,6 @@ By default, a retryable operation will be retried for any exception thrown: with
3 retry attempts (`maxRetries = 3`) after an initial failure, and a delay of 1 second
between attempts.
If you only need to customize the number of retry attempts, you can use the
`RetryPolicy.withMaxRetries()` factory method as demonstrated below.
[NOTE]
====
A retryable operation will be executed at least once and retried at most `maxRetries`
@@ -196,6 +193,9 @@ For example, if `maxRetries` is set to `4`, the retryable operation will be invo
least once and at most 5 times.
====
If you only need to customize the number of retry attempts, you can use the
`RetryPolicy.withMaxRetries()` factory method as demonstrated below.
[source,java,indent=0,subs="verbatim,quotes"]
----
var retryTemplate = new RetryTemplate(RetryPolicy.withMaxRetries(4)); // <1>
@@ -8,7 +8,7 @@ danger of undue coupling, because code that is meant to be used transactionally
almost always deployed that way anyway.
NOTE: The standard `jakarta.transaction.Transactional` annotation is also supported as
a drop-in replacement to Spring's own annotation. Please refer to the JTA documentation
a drop-in replacement for Spring's own annotation. Please refer to the JTA documentation
for more details.
The ease-of-use afforded by the use of the `@Transactional` annotation is best
@@ -89,47 +89,16 @@ annotation in a `@Configuration` class. See the
{spring-framework-api}/transaction/annotation/EnableTransactionManagement.html[javadoc]
for full details.
In XML configuration, the `<tx:annotation-driven/>` tag provides similar convenience:
The following example shows the configuration needed to enable annotation-driven transaction management:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<!-- from the file 'context.xml' -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
include-code::./AppConfig[tag=snippet,indent=0]
<!-- this is the service object that we want to make transactional -->
<bean id="fooService" class="x.y.service.DefaultFooService"/>
<!-- enable the configuration of transactional behavior based on annotations -->
<!-- a TransactionManager is still required -->
<tx:annotation-driven transaction-manager="txManager"/> <1>
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- (this dependency is defined somewhere else) -->
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- other <bean/> definitions here -->
</beans>
----
<1> The line that makes the bean instance transactional.
TIP: You can omit the `transaction-manager` attribute in the `<tx:annotation-driven/>`
tag if the bean name of the `TransactionManager` that you want to wire in has the name
`transactionManager`. If the `TransactionManager` bean that you want to dependency-inject
has any other name, you have to use the `transaction-manager` attribute, as in the
preceding example.
TIP: In programmatic configuration, the `@EnableTransactionManagement` annotation uses any
`TransactionManager` bean in the context. In XML configuration, you can omit
the `transaction-manager` attribute in the `<tx:annotation-driven/>` tag if the bean
name of the `TransactionManager` that you want to wire in has the name `transactionManager`.
If the `TransactionManager` bean has any other name, you have to use the
`transaction-manager` attribute explicitly, as in the preceding example.
Reactive transactional methods use reactive return types in contrast to imperative
programming arrangements as the following listing shows:
@@ -234,8 +203,10 @@ on an interface, a class definition, or a method on a class. However, the mere p
of the `@Transactional` annotation is not enough to activate the transactional behavior.
The `@Transactional` annotation is merely metadata that can be consumed by corresponding
runtime infrastructure which uses that metadata to configure the appropriate beans with
transactional behavior. In the preceding example, the `<tx:annotation-driven/>` element
switches on actual transaction management at runtime.
transactional behavior. In the preceding examples that use programmatic configuration,
the `@EnableTransactionManagement` annotation switches on actual transaction management
at runtime. Whereas, in the preceding example that uses XML configuration, the
`<tx:annotation-driven/>` element switches on actual transaction management at runtime.
TIP: The Spring team recommends that you annotate methods of concrete classes with the
`@Transactional` annotation, rather than relying on annotated methods in interfaces,
@@ -49,7 +49,6 @@ Kotlin::
<1> Injecting the `ApplicationContext`.
======
Similarly, if your test is configured to load a `WebApplicationContext`, you can inject
the web application context into your test, as follows:
@@ -87,7 +86,6 @@ Kotlin::
<2> Injecting the `WebApplicationContext`.
======
Dependency injection by using `@Autowired` is provided by the
`DependencyInjectionTestExecutionListener`, which is configured by default
(see xref:testing/testcontext-framework/fixture-di.adoc[Dependency Injection of Test Fixtures]).
@@ -96,22 +94,24 @@ Dependency injection by using `@Autowired` is provided by the
Test classes that use the TestContext framework do not need to extend any particular
class or implement a specific interface to configure their application context. Instead,
configuration is achieved by declaring the `@ContextConfiguration` annotation at the
class level. If your test class does not explicitly declare application context resource
locations or component classes, the configured `ContextLoader` determines how to load a
context from a default location or default configuration classes. In addition to context
resource locations and component classes, an application context can also be configured
through application context initializers.
class level. If your test class does not explicitly declare component classes or resource
locations, the configured `ContextLoader` determines how to load a context from _default_
configuration classes or a _default_ location. In addition to component classes and
context resource locations, an application context can also be configured through
xref:testing/testcontext-framework/ctx-management/context-customizers.adoc[context customizers]
or xref:testing/testcontext-framework/ctx-management/initializers.adoc[context initializers].
The following sections explain how to use Spring's `@ContextConfiguration` annotation to
configure a test `ApplicationContext` by using XML configuration files, Groovy scripts,
component classes (typically `@Configuration` classes), or context initializers.
Alternatively, you can implement and configure your own custom `SmartContextLoader` for
advanced use cases.
The following sections explain how to use `@ContextConfiguration` and related annotations
to configure a test `ApplicationContext` by using component classes (typically
`@Configuration` classes), XML configuration files, Groovy scripts, context customizers,
or context initializers. Alternatively, you can implement and configure your own custom
`SmartContextLoader` for advanced use cases.
* xref:testing/testcontext-framework/ctx-management/xml.adoc[Context Configuration with XML resources]
* xref:testing/testcontext-framework/ctx-management/groovy.adoc[Context Configuration with Groovy Scripts]
* xref:testing/testcontext-framework/ctx-management/javaconfig.adoc[Context Configuration with Component Classes]
* xref:testing/testcontext-framework/ctx-management/mixed-config.adoc[Mixing XML, Groovy Scripts, and Component Classes]
* xref:testing/testcontext-framework/ctx-management/xml.adoc[Context Configuration with XML Resources]
* xref:testing/testcontext-framework/ctx-management/groovy.adoc[Context Configuration with Groovy Scripts]
* xref:testing/testcontext-framework/ctx-management/default-config.adoc[Default Context Configuration]
* xref:testing/testcontext-framework/ctx-management/mixed-config.adoc[Mixing Component Classes, XML, and Groovy Scripts]
* xref:testing/testcontext-framework/ctx-management/context-customizers.adoc[Context Configuration with Context Customizers]
* xref:testing/testcontext-framework/ctx-management/initializers.adoc[Context Configuration with Context Initializers]
* xref:testing/testcontext-framework/ctx-management/inheritance.adoc[Context Configuration Inheritance]
@@ -0,0 +1,53 @@
[[testcontext-ctx-management-default-config]]
= Default Context Configuration
As explained in the sections on
xref:testing/testcontext-framework/ctx-management/javaconfig.adoc[component classes],
xref:testing/testcontext-framework/ctx-management/xml.adoc[XML resources], and
xref:testing/testcontext-framework/ctx-management/groovy.adoc[Groovy scripts], the
TestContext framework will attempt to locate _default_ context configuration if you do
not explicitly specify `@Configuration` classes, XML configuration files, or Groovy
scripts from which the test's `ApplicationContext` should be loaded.
However, due to a bug in the detection algorithm, default context configuration for a
superclass or enclosing class is currently ignored if the type hierarchy or enclosing
class hierarchy (for `@Nested` test classes) does not declare `@ContextConfiguration`.
Beginning with Spring Framework 7.1, the TestContext framework will reliably detect
**all** _default_ context configuration within a type hierarchy or enclosing class
hierarchy above a given test class in such scenarios. Consequently, test suites may
encounter issues after the upgrade to 7.1. For example, if a static nested
`@Configuration` class in a superclass or enclosing class is ignored due to the
aforementioned bug, after the bug has been fixed in 7.1 that `@Configuration` class will
no longer be ignored, which may lead to unexpected beans in the resulting
`ApplicationContext` our outright failures in tests.
In the interim, the TestContext framework logs a warning whenever it encounters _default_
context configuration that is currently ignored — for example, a `@Configuration` class
or XML configuration file. The remainder of this section provides guidance on how to
address such issues if you encounter warnings in your test suite.
[TIP]
====
Annotating the affected subclass or `@Nested` class with `@ContextConfiguration` allows
you to take matters into your own hands and specify which classes in the hierarchy are
actually intended to contribute context configuration.
====
If you do not want static nested `@Configuration` classes to be processed, you can:
- Remove the `@Configuration` declaration.
- Apply `@ContextConfiguration` only where you actually want such classes to be processed.
- Move the static nested `@Configuration` classes to standalone top-level classes so that
they cannot be accidentally interpreted as _default_ configuration classes.
Similarly, if you encounter issues with _default_ XML configuration files or Groovy
scripts being detected and you do not want them to be processed, you can:
- Apply `@ContextConfiguration` only where you actually want such resources to be
processed.
- Rename the resource files to something that does not match the default naming
convention (such as `*-context.xml` for XML configuration) so that they cannot be
accidentally interpreted as _default_ configuration files.
- Move the affected resource files to a different package or filesystem location within
your project.
@@ -1,33 +1,32 @@
[[testcontext-ctx-management-mixed-config]]
= Mixing XML, Groovy Scripts, and Component Classes
= Mixing Component Classes, XML, and Groovy Scripts
It may sometimes be desirable to mix XML configuration files, Groovy scripts, and
component classes (typically `@Configuration` classes) to configure an
`ApplicationContext` for your tests. For example, if you use XML configuration in
production, you may decide that you want to use `@Configuration` classes to configure
It may sometimes be desirable to mix component classes (typically `@Configuration`
classes), XML configuration files, or Groovy scripts to configure an `ApplicationContext`
for your tests. For example, if you use XML configuration in production for legacy
reasons, you may decide that you want to use `@Configuration` classes to configure
specific Spring-managed components for your tests, or vice versa.
Furthermore, some third-party frameworks (such as Spring Boot) provide first-class
support for loading an `ApplicationContext` from different types of resources
simultaneously (for example, XML configuration files, Groovy scripts, and
`@Configuration` classes). The Spring Framework, historically, has not supported this for
standard deployments. Consequently, most of the `SmartContextLoader` implementations that
the Spring Framework delivers in the `spring-test` module support only one resource type
for each test context. However, this does not mean that you cannot use both. One
exception to the general rule is that the `GenericGroovyXmlContextLoader` and
simultaneously (for example, `@Configuration` classes, XML configuration files, and
Groovy scripts). The Spring Framework, historically, has not supported this for standard
deployments. Consequently, most of the `SmartContextLoader` implementations that the
Spring Framework delivers in the `spring-test` module support only one resource type for
each test context. However, this does not mean that you cannot use a mixture of resource
types. One exception to the general rule is that the `GenericGroovyXmlContextLoader` and
`GenericGroovyXmlWebContextLoader` support both XML configuration files and Groovy
scripts simultaneously. Furthermore, third-party frameworks may choose to support the
declaration of both `locations` and `classes` through `@ContextConfiguration`, and, with
declaration of both `classes` and `locations` through `@ContextConfiguration`, and, with
the standard testing support in the TestContext framework, you have the following options.
If you want to use resource locations (for example, XML or Groovy) and `@Configuration`
classes to configure your tests, you must pick one as the entry point, and that one must
include or import the other. For example, in XML or Groovy scripts, you can include
`@Configuration` classes by using component scanning or defining them as normal Spring
beans, whereas, in a `@Configuration` class, you can use `@ImportResource` to import XML
configuration files or Groovy scripts. Note that this behavior is semantically equivalent
If you want to use `@Configuration` classes and resource locations (for example, XML or
Groovy) to configure your tests, you must pick one as the entry point, and that one must
import or include the other. For example, in a `@Configuration` class, you can use
`@ImportResource` to import XML configuration files or Groovy scripts; whereas, in XML or
Groovy scripts, you can include `@Configuration` classes by using component scanning or
defining them as normal Spring beans. Note that this behavior is semantically equivalent
to how you configure your application in production: In production configuration, you
define either a set of XML or Groovy resource locations or a set of `@Configuration`
classes from which your production `ApplicationContext` is loaded, but you still have the
freedom to include or import the other type of configuration.
define either a set of `@Configuration` classes or a set of XML or Groovy resource
locations from which your production `ApplicationContext` is loaded, but you still have
the freedom to import or include the other type of configuration.
@@ -1,5 +1,5 @@
[[testcontext-ctx-management-xml]]
= Context Configuration with XML resources
= Context Configuration with XML Resources
To load an `ApplicationContext` for your tests by using XML configuration files, annotate
your test class with `@ContextConfiguration` and configure the `locations` attribute with
@@ -49,10 +49,9 @@ This strategy resolves the API version from a request. The WebFlux config provid
options to resolve from a header, query parameter, media type parameter,
or from the URL path. You can also use a custom `ApiVersionResolver`.
NOTE: The path resolver always resolves the version from the specified path segment, or
raises `InvalidApiVersionException` otherwise, and therefore it cannot yield to other
resolvers.
The path resolver selects the version from a path segment specified by index, or
raises `InvalidApiVersionException`, and therefore never results in `null` (no version)
unless it is configured with a `Predicate<RequestPath>` to determine if a path is versioned.
@@ -251,7 +251,8 @@ Kotlin::
======
TIP: If you need to have a `LocalValidatorFactoryBean` injected somewhere, create a bean and
mark it with `@Primary` in order to avoid conflict with the one declared in the MVC config.
mark it with `@Primary`, or mark the one declared in the MVC config with `@Fallback`, in
order to avoid conflict.
[[webflux-config-content-negotiation]]
@@ -49,10 +49,9 @@ This strategy resolves the API version from a request. The MVC config provides b
options to resolve from a header, query parameter, media type parameter,
or from the URL path. You can also use a custom `ApiVersionResolver`.
NOTE: The path resolver always resolves the version from the specified path segment, or
raises `InvalidApiVersionException` otherwise, and therefore it cannot yield to other
resolvers.
The path resolver selects the version from a path segment specified by index, or
raises `InvalidApiVersionException`, and therefore never results in `null` (no version)
unless it is configured with a `Predicate<RequestPath>` to determine if a path is versioned.
@@ -19,4 +19,5 @@ example shows:
include-code::./MyController[tag=snippet,indent=0]
TIP: If you need to have a `LocalValidatorFactoryBean` injected somewhere, create a bean and
mark it with `@Primary` in order to avoid conflict with the one declared in the MVC configuration.
mark it with `@Primary`, or mark the one declared in the MVC configuration with
`@Fallback`, in order to avoid conflict.
@@ -3,7 +3,7 @@
[.small]#xref:web/webflux/uri-building.adoc[See equivalent in the Reactive stack]#
This section describes various options available in the Spring Framework to work with URI's.
This section describes various options available in the Spring Framework to work with URIs.
include::partial$web/web-uris.adoc[leveloffset=+1]
@@ -2,7 +2,7 @@
= UriComponents
[.small]#Spring MVC and Spring WebFlux#
`UriComponentsBuilder` helps to build URI's from URI templates with variables, as the following example shows:
`UriComponentsBuilder` helps to build URIs from URI templates with variables, as the following example shows:
[tabs]
======
@@ -128,7 +128,7 @@ Kotlin::
= UriBuilder
[.small]#Spring MVC and Spring WebFlux#
<<web-uricomponents, `UriComponentsBuilder`>> implements `UriBuilder`. You can create a
<<uricomponents, `UriComponentsBuilder`>> implements `UriBuilder`. You can create a
`UriBuilder`, in turn, with a `UriBuilderFactory`. Together, `UriBuilderFactory` and
`UriBuilder` provide a pluggable mechanism to build URIs from URI templates, based on
shared configuration, such as a base URL, encoding preferences, and other details.
@@ -247,7 +247,7 @@ and treats deviations from the syntax as illegal.
https://github.com/web-platform-tests/wpt/tree/master/url[URL parsing algorithm] in the
https://url.spec.whatwg.org[WhatWG URL Living standard]. It provides lenient handling of
a wide range of cases of unexpected input. Browsers implement this in order to handle
leniently user typed URL's. For more details, see the URL Living Standard and URL parsing
leniently user typed URLs. For more details, see the URL Living Standard and URL parsing
https://github.com/web-platform-tests/wpt/tree/master/url[test cases].
By default, `RestClient`, `WebClient`, and `RestTemplate` use the RFC parser type, and
@@ -255,10 +255,10 @@ expect applications to provide with URL templates that conform to RFC syntax. To
that you can customize the `UriBuilderFactory` on any of the clients.
Applications and frameworks may further rely on `UriComponentsBuilder` for their own needs
to parse user provided URL's in order to inspect and possibly validated URI components
to parse user provided URLs in order to inspect and possibly validated URI components
such as the scheme, host, port, path, and query. Such components can decide to use the
WhatWG parser type in order to handle URL's more leniently, and to align with the way
browsers parse URI's, in case of a redirect to the input URL or if it is included in a
WhatWG parser type in order to handle URLs more leniently, and to align with the way
browsers parse URIs, in case of a redirect to the input URL or if it is included in a
response to a browser.
@@ -373,14 +373,14 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"]
----
String baseUrl = "https://example.com";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl)
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);
// Customize the RestTemplate..
// Customize the RestTemplate.
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);
// Customize the WebClient..
// Customize the WebClient.
WebClient client = WebClient.builder().uriBuilderFactory(factory).build();
----
@@ -393,12 +393,12 @@ Kotlin::
encodingMode = EncodingMode.TEMPLATE_AND_VALUES
}
// Customize the RestTemplate..
// Customize the RestTemplate.
val restTemplate = RestTemplate().apply {
uriTemplateHandler = factory
}
// Customize the WebClient..
// Customize the WebClient.
val client = WebClient.builder().uriBuilderFactory(factory).build()
----
======
+1 -1
View File
@@ -5,7 +5,7 @@
"@antora/collector-extension": "1.0.2",
"@asciidoctor/tabs": "1.0.0-beta.6",
"@springio/antora-extensions": "1.14.7",
"fast-xml-parser": "5.3.4",
"fast-xml-parser": "5.3.8",
"@springio/asciidoctor-extensions": "1.0.0-alpha.17"
}
}
@@ -0,0 +1,47 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.docs.core.expressions.languageref.expressionsoperatorsoverloaded;
import org.springframework.expression.Operation;
import org.springframework.expression.OperatorOverloader;
import java.util.ArrayList;
import java.util.List;
public class ListConcatenation implements OperatorOverloader {
@Override
public boolean overridesOperation(Operation operation, Object left, Object right) {
return (operation == Operation.ADD && left instanceof List && right instanceof List);
}
@Override
@SuppressWarnings({"rawtypes", "unchecked"})
public Object operate(Operation operation, Object left, Object right) {
if (operation == Operation.ADD &&
left instanceof List list1 && right instanceof List list2) {
List result = new ArrayList(list1);
result.addAll(list2);
return result;
}
throw new UnsupportedOperationException(
"No overload for operation %s and operands [%s] and [%s]"
.formatted(operation, left, right));
}
}
@@ -0,0 +1,42 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.docs.dataaccess.transaction.declarative.transactiondeclarativeannotations;
import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
// tag::snippet[]
@Configuration
@EnableTransactionManagement
public class AppConfig {
@Bean
public FooService fooService() {
return new DefaultFooService();
}
@Bean
public PlatformTransactionManager txManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
// end::snippet[]
@@ -0,0 +1,20 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.docs.dataaccess.transaction.declarative.transactiondeclarativeannotations;
public class DefaultFooService implements FooService {
}
@@ -0,0 +1,20 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.docs.dataaccess.transaction.declarative.transactiondeclarativeannotations;
public interface FooService {
}
@@ -21,7 +21,6 @@ import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.test.web.servlet.assertj.MvcTestResult;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
/**
* @author Stephane Nicoll
@@ -17,9 +17,7 @@
package org.springframework.docs.web.websocket.stomp.websocketstompconfigurationperformance;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration;
@@ -17,9 +17,7 @@
package org.springframework.docs.web.websocket.stomp.websocketstompconfigurationperformance;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration;
@@ -22,7 +22,6 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
// tag::snippet[]
@@ -19,13 +19,8 @@ package org.springframework.docs.web.websocket.stomp.websocketstompmessageflow;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.stereotype.Controller;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
// tag::snippet[]
@Controller
@@ -17,7 +17,6 @@
package org.springframework.docs.web.websocket.stomp.websocketstomporderedmessages;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@@ -0,0 +1,37 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.docs.core.expressions.languageref.expressionsoperatorsoverloaded
import org.springframework.expression.Operation
import org.springframework.expression.OperatorOverloader
class ListConcatenation: OperatorOverloader {
override fun overridesOperation(operation: Operation, left: Any?, right: Any?): Boolean {
return operation == Operation.ADD && left is List<*> && right is List<*>
}
override fun operate(operation: Operation, left: Any?, right: Any?): Any {
if (operation == Operation.ADD && left is List<*> && right is List<*>) {
return left + right
}
throw UnsupportedOperationException(
"No overload for operation $operation and operands [$left] and [$right]")
}
}
@@ -0,0 +1,41 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.docs.dataaccess.transaction.declarative.transactiondeclarativeannotations
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.jdbc.datasource.DataSourceTransactionManager
import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.annotation.EnableTransactionManagement
import javax.sql.DataSource
// tag::snippet[]
@Configuration
@EnableTransactionManagement
class AppConfig {
@Bean
fun fooService(): FooService {
return DefaultFooService()
}
@Bean
fun txManager(dataSource: DataSource): PlatformTransactionManager {
return DataSourceTransactionManager(dataSource)
}
}
// end::snippet[]
@@ -0,0 +1,20 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.docs.dataaccess.transaction.declarative.transactiondeclarativeannotations
class DefaultFooService : FooService {
}
@@ -0,0 +1,20 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.docs.dataaccess.transaction.declarative.transactiondeclarativeannotations
interface FooService {
}
@@ -16,12 +16,9 @@
package org.springframework.docs.testing.mockmvc.assertj.mockmvctesterintegration
import org.assertj.core.api.Assertions
import org.assertj.core.api.Assertions.*
import org.springframework.test.web.servlet.assertj.MockMvcTester
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*
import org.springframework.test.web.servlet.result.MockMvcResultMatchers
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.*
/**
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- tag::snippet[] -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- this is the service object that we want to make transactional -->
<bean id="fooService" class="x.y.service.DefaultFooService"/>
<!-- enable the configuration of transactional behavior based on annotations -->
<!-- a TransactionManager is still required -->
<tx:annotation-driven transaction-manager="txManager"/>
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- (this dependency is defined somewhere else) -->
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- other <bean/> definitions here -->
</beans>
<!-- end::snippet[] -->
+15 -15
View File
@@ -8,19 +8,19 @@ javaPlatform {
dependencies {
api(platform("com.fasterxml.jackson:jackson-bom:2.20.2"))
api(platform("io.micrometer:micrometer-bom:1.16.3"))
api(platform("io.micrometer:micrometer-bom:1.16.4"))
api(platform("io.netty:netty-bom:4.2.10.Final"))
api(platform("io.projectreactor:reactor-bom:2025.0.3"))
api(platform("io.projectreactor:reactor-bom:2025.0.4"))
api(platform("io.rsocket:rsocket-bom:1.1.5"))
api(platform("org.apache.groovy:groovy-bom:5.0.4"))
api(platform("org.apache.logging.log4j:log4j-bom:2.25.3"))
api(platform("org.assertj:assertj-bom:3.27.7"))
api(platform("org.eclipse.jetty:jetty-bom:12.1.6"))
api(platform("org.eclipse.jetty.ee11:jetty-ee11-bom:12.1.6"))
api(platform("org.eclipse.jetty:jetty-bom:12.1.7"))
api(platform("org.eclipse.jetty.ee11:jetty-ee11-bom:12.1.7"))
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.2"))
api(platform("org.mockito:mockito-bom:5.21.0"))
api(platform("org.junit:junit-bom:6.0.3"))
api(platform("org.mockito:mockito-bom:5.22.0"))
api(platform("tools.jackson:jackson-bom:3.0.4"))
constraints {
@@ -31,8 +31,8 @@ dependencies {
api("com.google.code.findbugs:findbugs:3.0.1")
api("com.google.code.findbugs:jsr305:3.0.2")
api("com.google.code.gson:gson:2.13.2")
api("com.google.protobuf:protobuf-java-util:4.33.4")
api("com.h2database:h2:2.3.232")
api("com.google.protobuf:protobuf-java-util:4.34.0")
api("com.h2database:h2:2.4.240")
api("com.jayway.jsonpath:json-path:2.10.0")
api("com.networknt:json-schema-validator:1.5.3")
api("com.oracle.database.jdbc:ojdbc11:21.9.0.0")
@@ -49,7 +49,7 @@ dependencies {
api("de.bechte.junit:junit-hierarchicalcontextrunner:4.12.2")
api("io.mockk:mockk:1.14.5")
api("io.projectreactor.tools:blockhound:1.0.8.RELEASE")
api("io.r2dbc:r2dbc-h2:1.0.0.RELEASE")
api("io.r2dbc:r2dbc-h2:1.1.0.RELEASE")
api("io.r2dbc:r2dbc-spi-test:1.0.0.RELEASE")
api("io.r2dbc:r2dbc-spi:1.0.0.RELEASE")
api("io.reactivex.rxjava3:rxjava:3.1.12")
@@ -94,7 +94,7 @@ dependencies {
api("org.apache.derby:derbyclient:10.16.1.1")
api("org.apache.derby:derbytools:10.16.1.1")
api("org.apache.httpcomponents.client5:httpclient5:5.6")
api("org.apache.httpcomponents.core5:httpcore5-reactive:5.4")
api("org.apache.httpcomponents.core5:httpcore5-reactive:5.4.2")
api("org.apache.poi:poi-ooxml:5.5.1")
api("org.apache.tomcat.embed:tomcat-embed-core:11.0.18")
api("org.apache.tomcat.embed:tomcat-embed-websocket:11.0.18")
@@ -111,7 +111,7 @@ dependencies {
api("org.easymock:easymock:5.6.0")
api("org.eclipse.angus:angus-mail:2.0.3")
api("org.eclipse.jetty:jetty-reactive-httpclient:4.1.4")
api("org.eclipse.persistence:org.eclipse.persistence.jpa:5.0.0-B13")
api("org.eclipse.persistence:org.eclipse.persistence.jpa:5.0.0-RC1")
api("org.eclipse:yasson:3.0.4")
api("org.ehcache:ehcache:3.10.8")
api("org.ehcache:jcache:1.0.1")
@@ -120,7 +120,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.4.Final")
api("org.hibernate.orm:hibernate-core:7.2.6.Final")
api("org.hibernate.validator:hibernate-validator:9.1.0.Final")
api("org.hsqldb:hsqldb:2.7.4")
api("org.htmlunit:htmlunit:4.21.0")
@@ -134,14 +134,14 @@ dependencies {
api("org.python:jython-standalone:2.7.4")
api("org.quartz-scheduler:quartz:2.3.2")
api("org.reactivestreams:reactive-streams:1.0.4")
api("org.seleniumhq.selenium:htmlunit3-driver:4.40.0")
api("org.seleniumhq.selenium:selenium-java:4.40.0")
api("org.seleniumhq.selenium:htmlunit3-driver:4.41.0")
api("org.seleniumhq.selenium:selenium-java:4.41.0")
api("org.skyscreamer:jsonassert:1.5.3")
api("org.testng:testng:7.12.0")
api("org.webjars:underscorejs:1.8.3")
api("org.webjars:webjars-locator-lite:1.1.0")
api("org.xmlunit:xmlunit-assertj:2.10.4")
api("org.xmlunit:xmlunit-matchers:2.10.4")
api("org.yaml:snakeyaml:2.5")
api("org.yaml:snakeyaml:2.6")
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
version=7.0.5
version=7.0.6
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
+1 -1
View File
@@ -61,4 +61,4 @@ void configureDeploymentRepository(Project project) {
}
}
}
}
}
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Vendored
+1 -1
View File
@@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/b631911858264c0b6e4d6603d677ff5218766cee/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -40,6 +40,13 @@ import org.springframework.beans.BeansException;
* A {@code BeanFactoryPostProcessor} may also be registered programmatically
* with a {@code ConfigurableApplicationContext}.
*
* <p>When registering a {@code BeanFactoryPostProcessor} via an {@code @Bean} method
* in a {@code @Configuration} class, use a {@code static} method to avoid eager
* initialization of other beans in the configuration class. See the
* "BeanFactoryPostProcessor-returning {@code @Bean} methods" section in
* {@link org.springframework.context.annotation.Bean @Bean}'s javadoc for details
* and an example.
*
* <h3>Ordering</h3>
* <p>{@code BeanFactoryPostProcessor} beans that are autodetected in an
* {@code ApplicationContext} will be ordered according to
@@ -35,6 +35,14 @@ import org.springframework.beans.BeansException;
* created. A plain {@code BeanFactory} allows for programmatic registration of
* post-processors, applying them to all beans created through the bean factory.
*
* <p>When registering a {@code BeanPostProcessor} via an {@code @Bean} method in
* a {@code @Configuration} class, use a {@code static} method with ideally no
* dependencies in order to avoid eager initialization that can make other beans
* ineligible for full post-processing. See the "BeanPostProcessor-returning
* {@code @Bean} methods" section in
* {@link org.springframework.context.annotation.Bean @Bean}'s javadoc for details
* and an example.
*
* <h3>Ordering</h3>
* <p>{@code BeanPostProcessor} beans that are autodetected in an
* {@code ApplicationContext} will be ordered according to
@@ -23,6 +23,7 @@ import org.springframework.beans.factory.config.BeanDefinition;
*
* @author Juergen Hoeller
* @since 2.0.3
* @see org.springframework.context.annotation.ConfigurationBeanNameGenerator
*/
public interface BeanNameGenerator {
@@ -109,7 +109,9 @@ public class AnnotatedBeanDefinitionReader {
/**
* Set the {@code BeanNameGenerator} to use for detected bean classes.
* <p>The default is a {@link AnnotationBeanNameGenerator}.
* <p>The default is an {@link AnnotationBeanNameGenerator}.
* @see FullyQualifiedAnnotationBeanNameGenerator
* @see FullyQualifiedConfigurationBeanNameGenerator
*/
public void setBeanNameGenerator(@Nullable BeanNameGenerator beanNameGenerator) {
this.beanNameGenerator =
@@ -118,14 +118,20 @@ public class AnnotationConfigApplicationContext extends GenericApplicationContex
/**
* Provide a custom {@link BeanNameGenerator} for use with {@link AnnotatedBeanDefinitionReader}
* and/or {@link ClassPathBeanDefinitionScanner}, if any.
* <p>Default is {@link AnnotationBeanNameGenerator}.
* and/or {@link ClassPathBeanDefinitionScanner}.
* <p>Default is {@code AnnotationBeanNameGenerator}.
* <p>When processing {@link Configuration @Configuration} classes, a
* {@link ConfigurationBeanNameGenerator} (such as
* {@link FullyQualifiedConfigurationBeanNameGenerator}) also determines the
* default names for {@link Bean @Bean} methods without an explicit {@code name}
* attribute.
* <p>Any call to this method must occur prior to calls to {@link #register(Class...)}
* and/or {@link #scan(String...)}.
* @see AnnotatedBeanDefinitionReader#setBeanNameGenerator
* @see ClassPathBeanDefinitionScanner#setBeanNameGenerator
* @see AnnotationBeanNameGenerator
* @see FullyQualifiedAnnotationBeanNameGenerator
* @see FullyQualifiedConfigurationBeanNameGenerator
*/
public void setBeanNameGenerator(BeanNameGenerator beanNameGenerator) {
this.reader.setBeanNameGenerator(beanNameGenerator);
@@ -35,48 +35,47 @@ import org.springframework.core.annotation.AliasFor;
* example:
*
* <pre class="code">
* &#064;Bean
* public MyBean myBean() {
* // instantiate and configure MyBean obj
* return obj;
* }
* </pre>
* &#064;Bean
* public MyBean myBean() {
* // instantiate and configure MyBean obj
* return obj;
* }</pre>
*
* <h3>Bean Names</h3>
*
* <p>While a {@link #name} attribute is available, the default strategy for
* determining the name of a bean is to use the name of the {@code @Bean} method.
* This is convenient and intuitive, but if explicit naming is desired, the
* {@code name} attribute (or its alias {@code value}) may be used. Also note
* that {@code name} accepts an array of Strings, allowing for multiple names
* (i.e. a primary bean name plus one or more aliases) for a single bean.
* This default can be overridden by configuring a {@link ConfigurationBeanNameGenerator}
* &mdash; for example, {@link FullyQualifiedConfigurationBeanNameGenerator} for
* fully-qualified names. If explicit naming is desired for an individual bean, the
* {@code name} attribute (or its alias {@link #value}) may be used. Also note that
* {@code name} accepts an array of Strings, allowing for multiple names (i.e., a
* primary bean name plus one or more aliases) for a single bean.
*
* <pre class="code">
* &#064;Bean({"b1", "b2"}) // bean available as 'b1' and 'b2', but not 'myBean'
* public MyBean myBean() {
* // instantiate and configure MyBean obj
* return obj;
* }
* </pre>
* &#064;Bean({"b1", "b2"}) // bean available as 'b1' and 'b2', but not 'myBean'
* public MyBean myBean() {
* // instantiate and configure MyBean obj
* return obj;
* }</pre>
*
* <h3>Profile, Scope, Lazy, DependsOn, Primary, Order</h3>
* <h3>Profile, Scope, Lazy, DependsOn, Primary, Fallback, Order</h3>
*
* <p>Note that the {@code @Bean} annotation does not provide attributes for profile,
* scope, lazy, depends-on or primary. Rather, it should be used in conjunction with
* {@link Scope @Scope}, {@link Lazy @Lazy}, {@link DependsOn @DependsOn} and
* scope, lazy, depends-on, or primary. Rather, it should be used in conjunction with
* {@link Scope @Scope}, {@link Lazy @Lazy}, {@link DependsOn @DependsOn}, and
* {@link Primary @Primary} annotations to declare those semantics. For example:
*
* <pre class="code">
* &#064;Bean
* &#064;Profile("production")
* &#064;Scope("prototype")
* public MyBean myBean() {
* // instantiate and configure MyBean obj
* return obj;
* }
* </pre>
* &#064;Bean
* &#064;Profile("production")
* &#064;Scope("prototype")
* public MyBean myBean() {
* // instantiate and configure MyBean obj
* return obj;
* }</pre>
*
* The semantics of the above-mentioned annotations match their use at the component
* The semantics of the aforementioned annotations match their use at the component
* class level: {@code @Profile} allows for selective inclusion of certain beans.
* {@code @Scope} changes the bean's scope from singleton to the specified scope.
* {@code @Lazy} only has an actual effect in case of the default singleton scope.
@@ -85,6 +84,9 @@ import org.springframework.core.annotation.AliasFor;
* through direct references, which is typically helpful for singleton startup.
* {@code @Primary} is a mechanism to resolve ambiguity at the injection point level
* if a single target component needs to be injected but several beans match by type.
* {@link Fallback @Fallback} marks a bean as a fallback candidate in such scenarios;
* if all beans but one among multiple matching candidates are marked as fallback, the
* remaining bean will be selected.
*
* <p>Additionally, {@code @Bean} methods may also declare qualifier annotations
* and {@link org.springframework.core.annotation.Order @Order} values, to be
@@ -100,8 +102,8 @@ import org.springframework.core.annotation.AliasFor;
* orthogonal concern determined by dependency relationships and {@code @DependsOn}
* declarations as mentioned above. Also, {@link jakarta.annotation.Priority} is not
* available at this level since it cannot be declared on methods; its semantics can
* be modeled through {@code @Order} values in combination with {@code @Primary} on
* a single bean per type.
* be modeled through {@code @Order} values in combination with {@code @Primary} or
* {@code @Fallback} on a single bean per type.
*
* <h3>{@code @Bean} Methods in {@code @Configuration} Classes</h3>
*
@@ -119,17 +121,17 @@ import org.springframework.core.annotation.AliasFor;
* &#064;Configuration
* public class AppConfig {
*
* &#064;Bean
* public FooService fooService() {
* return new FooService(fooRepository());
* }
* &#064;Bean
* public FooService fooService() {
* return new FooService(fooRepository());
* }
*
* &#064;Bean
* public FooRepository fooRepository() {
* return new JdbcFooRepository(dataSource());
* }
* &#064;Bean
* public FooRepository fooRepository() {
* return new JdbcFooRepository(dataSource());
* }
*
* // ...
* // ...
* }</pre>
*
* <h3>{@code @Bean} <em>Lite</em> Mode</h3>
@@ -158,20 +160,21 @@ import org.springframework.core.annotation.AliasFor;
* <pre class="code">
* &#064;Component
* public class Calculator {
* public int sum(int a, int b) {
* return a+b;
* }
* public int sum(int a, int b) {
* return a+b;
* }
*
* &#064;Bean
* public MyBean myBean() {
* return new MyBean();
* }
* &#064;Bean
* public MyBean myBean() {
* return new MyBean();
* }
* }</pre>
*
* <h3>Bootstrapping</h3>
*
* <p>See the @{@link Configuration} javadoc for further details including how to bootstrap
* the container using {@link AnnotationConfigApplicationContext} and friends.
* <p>See the {@link Configuration @Configuration} javadoc for further details
* including how to bootstrap the container using
* {@link AnnotationConfigApplicationContext} and friends.
*
* <h3>{@code BeanFactoryPostProcessor}-returning {@code @Bean} methods</h3>
*
@@ -183,20 +186,44 @@ import org.springframework.core.annotation.AliasFor;
* lifecycle issues, mark {@code BFPP}-returning {@code @Bean} methods as {@code static}. For example:
*
* <pre class="code">
* &#064;Bean
* public static PropertySourcesPlaceholderConfigurer pspc() {
* // instantiate, configure and return pspc ...
* }
* </pre>
* &#064;Bean
* public static PropertySourcesPlaceholderConfigurer pspc() {
* // instantiate, configure and return pspc ...
* }</pre>
*
* By marking this method as {@code static}, it can be invoked without causing instantiation of its
* declaring {@code @Configuration} class, thus avoiding the above-mentioned lifecycle conflicts.
* declaring {@code @Configuration} class, thus avoiding the aforementioned lifecycle conflicts.
* Note however that {@code static} {@code @Bean} methods will not be enhanced for scoping and AOP
* semantics as mentioned above. This works out in {@code BFPP} cases, as they are not typically
* referenced by other {@code @Bean} methods. As a reminder, an INFO-level log message will be
* issued for any non-static {@code @Bean} methods having a return type assignable to
* {@code BeanFactoryPostProcessor}.
*
* <h3>{@code BeanPostProcessor}-returning {@code @Bean} methods</h3>
*
* <p>Similarly, special consideration must be taken for {@code @Bean} methods that return Spring
* {@link org.springframework.beans.factory.config.BeanPostProcessor BeanPostProcessor}
* ({@code BPP}) types. Because {@code BPP} objects must be instantiated early in the container
* lifecycle, a non-static {@code @Bean} method that returns a {@code BPP} will cause eager
* initialization of its declaring {@code @Configuration} class, which can make other beans in the
* {@code @Configuration} class (as well as depencencies of those beans) ineligible for full
* post-processing. To avoid these lifecycle issues, mark {@code BPP}-returning {@code @Bean}
* methods as {@code static}. For example:
*
* <pre class="code">
* &#064;Bean
* public static MyBeanPostProcessor myBeanPostProcessor() {
* return new MyBeanPostProcessor();
* }</pre>
*
* By marking this method as {@code static}, it can be invoked without causing instantiation of its
* declaring {@code @Configuration} class. Furthermore, the method should ideally not declare any
* dependencies so that the container does not need to instantiate other beans to create the
* post-processor, which would make those beans ineligible for post-processing as well. For any such
* bean, you should see a WARN-level log message similar to the following: "Bean 'someBean' of type
* [org.example.SomeType] is not eligible for getting processed by all BeanPostProcessors (for example:
* not eligible for auto-proxying)".
*
* @author Rod Johnson
* @author Costin Leau
* @author Chris Beams
@@ -208,9 +235,13 @@ import org.springframework.core.annotation.AliasFor;
* @see DependsOn
* @see Lazy
* @see Primary
* @see Fallback
* @see org.springframework.stereotype.Component
* @see org.springframework.beans.factory.annotation.Autowired
* @see org.springframework.beans.factory.annotation.Value
* @see FullyQualifiedConfigurationBeanNameGenerator
* @see AnnotationConfigApplicationContext#setBeanNameGenerator
* @see ComponentScan#nameGenerator()
*/
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@@ -229,10 +260,11 @@ public @interface Bean {
/**
* The name of this bean, or if several names, a primary bean name plus aliases.
* <p>If left unspecified, the name of the bean is the name of the annotated method.
* If specified, the method name is ignored.
* <p>See the "Bean Names" section in the {@linkplain Bean class-level documentation}
* for details on how the bean name is determined if this attribute is left
* unspecified.
* <p>The bean name and aliases may also be configured via the {@link #value}
* attribute if no other attributes are declared.
* attribute.
* @see #value
*/
@AliasFor("value")
@@ -43,11 +43,11 @@ import org.springframework.util.PatternMatchUtils;
* or {@code ApplicationContext}).
*
* <p>Candidate classes are detected through configurable type filters. The
* default filters include classes that are annotated with Spring's
* {@link org.springframework.stereotype.Component @Component},
* default filters include classes that are annotated or meta-annotated with Spring's
* {@link org.springframework.stereotype.Component @Component} annotation, such as the
* {@link org.springframework.stereotype.Repository @Repository},
* {@link org.springframework.stereotype.Service @Service}, or
* {@link org.springframework.stereotype.Controller @Controller} stereotype.
* {@link org.springframework.stereotype.Service @Service}, and
* {@link org.springframework.stereotype.Controller @Controller} stereotypes.
*
* <p>Also supports JSR-330's {@link jakarta.inject.Named} annotations, if available.
*
@@ -204,8 +204,11 @@ public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateCo
}
/**
* Set the BeanNameGenerator to use for detected bean classes.
* <p>Default is a {@link AnnotationBeanNameGenerator}.
* Set the {@link BeanNameGenerator} to use for detected bean classes.
* <p>Default is an {@code AnnotationBeanNameGenerator}.
* @see AnnotationBeanNameGenerator
* @see FullyQualifiedAnnotationBeanNameGenerator
* @see FullyQualifiedConfigurationBeanNameGenerator
*/
public void setBeanNameGenerator(@Nullable BeanNameGenerator beanNameGenerator) {
this.beanNameGenerator =
@@ -109,14 +109,18 @@ public @interface ComponentScan {
/**
* The {@link BeanNameGenerator} class to be used for naming detected components
* within the Spring container.
* <p>The default value of the {@link BeanNameGenerator} interface itself indicates
* <p>The default value of the {@code BeanNameGenerator} interface itself indicates
* that the scanner used to process this {@code @ComponentScan} annotation should
* use its inherited bean name generator, for example, the default
* {@link AnnotationBeanNameGenerator} or any custom instance supplied to the
* application context at bootstrap time.
* application context at bootstrap time. If a {@link ConfigurationBeanNameGenerator}
* is used (such as {@link FullyQualifiedConfigurationBeanNameGenerator}), it
* also affects the default names for {@link Bean @Bean} methods in
* {@link Configuration @Configuration} classes.
* @see AnnotationConfigApplicationContext#setBeanNameGenerator(BeanNameGenerator)
* @see AnnotationBeanNameGenerator
* @see FullyQualifiedAnnotationBeanNameGenerator
* @see FullyQualifiedConfigurationBeanNameGenerator
*/
Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
@@ -437,6 +437,8 @@ public @interface Configuration {
* <p>Alias for {@link Component#value}.
* @return the explicit component name, if any (or empty String otherwise)
* @see AnnotationBeanNameGenerator
* @see FullyQualifiedAnnotationBeanNameGenerator
* @see FullyQualifiedConfigurationBeanNameGenerator
*/
@AliasFor(annotation = Component.class)
String value() default "";
@@ -37,9 +37,10 @@ public interface ConfigurationBeanNameGenerator extends BeanNameGenerator {
/**
* Derive a default bean name for the given {@link Bean @Bean} method,
* providing the {@link Bean#name() name} attribute specified.
* taking into account the specified {@link Bean#name() name} attribute.
* @param beanMethod the method metadata for the {@link Bean @Bean} method
* @param beanName the {@link Bean#name() name} attribute or {@code null} if non is specified
* @param beanName the {@link Bean#name() name} attribute or {@code null} if
* none is specified
* @return the default bean name to use
*/
String deriveBeanName(MethodMetadata beanMethod, @Nullable String beanName);
@@ -209,7 +209,7 @@ class ConfigurationClassBeanDefinitionReader {
String beanName = (explicitNames.length > 0 && StringUtils.hasText(explicitNames[0])) ? explicitNames[0] : null;
String localBeanName = defaultBeanName(beanName, methodName);
beanName = (this.importBeanNameGenerator instanceof ConfigurationBeanNameGenerator cbng ?
cbng.deriveBeanName(metadata, beanName) : defaultBeanName(beanName, methodName));
cbng.deriveBeanName(metadata, beanName) : localBeanName);
if (explicitNames.length > 0) {
// Register aliases even when overridden below.
for (int i = 1; i < explicitNames.length; i++) {
@@ -303,8 +303,8 @@ class ConfigurationClassBeanDefinitionReader {
}
if (logger.isTraceEnabled()) {
logger.trace("Registering bean definition for @Bean method %s.%s()"
.formatted(configClass.getMetadata().getClassName(), beanName));
logger.trace("Registering bean definition for @Bean method %s.%s() with bean name '%s'"
.formatted(configClass.getMetadata().getClassName(), methodName, beanName));
}
this.registry.registerBeanDefinition(beanName, beanDefToRegister);
}
@@ -368,8 +368,8 @@ class ConfigurationClassBeanDefinitionReader {
"@Bean definition illegally overridden by existing bean definition: " + existingBeanDef);
}
if (logger.isDebugEnabled()) {
logger.debug("Skipping bean definition for %s: a definition for bean '%s' already exists. " +
"This top-level bean definition is considered as an override.".formatted(beanMethod, beanName));
logger.debug(("Skipping bean definition for %s: a definition for bean '%s' already exists. " +
"This top-level bean definition is considered as an override.").formatted(beanMethod, beanName));
}
return true;
}
@@ -387,13 +387,13 @@ class ConfigurationClassEnhancer {
// create the bean instance.
if (logger.isInfoEnabled() &&
BeanFactoryPostProcessor.class.isAssignableFrom(beanMethod.getReturnType())) {
logger.info(String.format("@Bean method %s.%s is non-static and returns an object " +
"assignable to Spring's BeanFactoryPostProcessor interface. This will " +
"result in a failure to process annotations such as @Autowired, " +
"@Resource and @PostConstruct within the method's declaring " +
"@Configuration class. Add the 'static' modifier to this method to avoid " +
"these container lifecycle issues; see @Bean javadoc for complete details.",
beanMethod.getDeclaringClass().getSimpleName(), beanMethod.getName()));
logger.info("""
@Bean method %s.%s is non-static and returns an object assignable to Spring's \
BeanFactoryPostProcessor interface. This will result in a failure to process \
annotations such as @Autowired, @Resource, and @PostConstruct within the method's \
declaring @Configuration class. Add the 'static' modifier to this method to avoid \
these container lifecycle issues; see @Bean javadoc for complete details."""
.formatted(beanMethod.getDeclaringClass().getSimpleName(), beanMethod.getName()));
}
return cglibMethodProxy.invokeSuper(enhancedConfigInstance, beanMethodArgs);
}
@@ -242,12 +242,15 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
/**
* Set the {@link BeanNameGenerator} to be used when triggering component scanning
* from {@link Configuration} classes and when registering {@link Import}'ed
* configuration classes. The default is a standard {@link AnnotationBeanNameGenerator}
* for scanned components (compatible with the default in {@link ClassPathBeanDefinitionScanner})
* from {@link Configuration @Configuration} classes and when registering
* {@link Import @Import}'ed configuration classes.
* <p>The default is a standard {@link AnnotationBeanNameGenerator} for scanned
* components (compatible with the default in {@link ClassPathBeanDefinitionScanner})
* and a variant thereof for imported configuration classes (using unique fully-qualified
* class names instead of standard component overriding).
* <p>Note that this strategy does <em>not</em> apply to {@link Bean} methods.
* <p>If the supplied bean name generator is a {@link ConfigurationBeanNameGenerator}
* (such as {@link FullyQualifiedConfigurationBeanNameGenerator}), it also affects the
* default names for {@link Bean @Bean} methods in configuration classes.
* <p>This setter is typically only appropriate when configuring the post-processor as a
* standalone bean definition in XML, for example, not using the dedicated {@code AnnotationConfig*}
* application contexts or the {@code <context:annotation-config>} element. Any bean name
@@ -255,6 +258,9 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
* @since 3.1.1
* @see AnnotationConfigApplicationContext#setBeanNameGenerator(BeanNameGenerator)
* @see AnnotationConfigUtils#CONFIGURATION_BEAN_NAME_GENERATOR
* @see AnnotationBeanNameGenerator
* @see FullyQualifiedAnnotationBeanNameGenerator
* @see FullyQualifiedConfigurationBeanNameGenerator
*/
public void setBeanNameGenerator(BeanNameGenerator beanNameGenerator) {
Assert.notNull(beanNameGenerator, "BeanNameGenerator must not be null");
@@ -16,6 +16,7 @@
package org.springframework.context.support;
import java.nio.charset.Charset;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Set;
@@ -32,6 +33,7 @@ import org.springframework.util.ObjectUtils;
* configuration methods and corresponding semantic definitions.
*
* @author Juergen Hoeller
* @author Sam Brannen
* @since 4.3
* @see ResourceBundleMessageSource
* @see ReloadableResourceBundleMessageSource
@@ -40,7 +42,7 @@ public abstract class AbstractResourceBasedMessageSource extends AbstractMessage
private final Set<String> basenameSet = new LinkedHashSet<>(4);
private @Nullable String defaultEncoding;
private @Nullable Charset defaultCharset;
private boolean fallbackToSystemLocale = true;
@@ -53,7 +55,7 @@ public abstract class AbstractResourceBasedMessageSource extends AbstractMessage
* Set a single basename, following the basic ResourceBundle convention
* of not specifying file extension or language codes. The resource location
* format is up to the specific {@code MessageSource} implementation.
* <p>Regular and XMl properties files are supported: for example, "messages" will find
* <p>Regular and XML properties files are supported: for example, "messages" will find
* a "messages.properties", "messages_en.properties" etc arrangement as well
* as "messages.xml", "messages_en.xml" etc.
* @param basename the single basename
@@ -118,25 +120,52 @@ public abstract class AbstractResourceBasedMessageSource extends AbstractMessage
/**
* Set the default charset to use for parsing properties files.
* Used if no file-specific charset is specified for a file.
* <p>Used if no file-specific charset is specified for a file.
* <p>The effective default is the {@code java.util.Properties}
* default encoding: ISO-8859-1. A {@code null} value indicates
* the platform default encoding.
* <p>Only applies to classic properties files, not to XML files.
* @param defaultEncoding the default charset
* @see #setDefaultCharset(Charset)
*/
public void setDefaultEncoding(@Nullable String defaultEncoding) {
this.defaultEncoding = defaultEncoding;
this.defaultCharset = (defaultEncoding != null ? Charset.forName(defaultEncoding) : null);
}
/**
* Return the default charset to use for parsing properties files, if any.
* @since 4.3
* @see #getDefaultCharset()
*/
protected @Nullable String getDefaultEncoding() {
return this.defaultEncoding;
return (this.defaultCharset != null ? this.defaultCharset.name() : null);
}
/**
* Set the default {@link Charset} to use for parsing properties files.
* <p>Used if no file-specific charset is specified for a file.
* <p>The effective default is the {@code java.util.Properties}
* default encoding: ISO-8859-1. A {@code null} value indicates
* the platform default encoding.
* <p>Only applies to classic properties files, not to XML files.
* @param defaultCharset the default charset
* @since 7.0.6
* @see #setDefaultEncoding(String)
*/
public void setDefaultCharset(@Nullable Charset defaultCharset) {
this.defaultCharset = defaultCharset;
}
/**
* Return the default charset to use for parsing properties files, if any.
* @since 7.0.6
* @see #setDefaultCharset(Charset)
*/
protected @Nullable Charset getDefaultCharset() {
return this.defaultCharset;
}
/**
* Set whether to fall back to the system Locale if no files for a specific
* Locale have been found. Default is "true"; if this is turned off, the only
@@ -24,6 +24,8 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor;
import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor;
import org.springframework.beans.factory.support.RootBeanDefinition;
@@ -98,7 +100,8 @@ class ApplicationListenerDetector implements DestructionAwareBeanPostProcessor,
try {
ApplicationEventMulticaster multicaster = this.applicationContext.getApplicationEventMulticaster();
multicaster.removeApplicationListener(applicationListener);
multicaster.removeApplicationListenerBean(beanName);
multicaster.removeApplicationListenerBean(
bean instanceof FactoryBean ? BeanFactory.FACTORY_BEAN_PREFIX + beanName : beanName);
}
catch (IllegalStateException ex) {
// ApplicationEventMulticaster not initialized yet - no need to remove a listener
@@ -19,6 +19,7 @@ package org.springframework.context.support;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
@@ -80,9 +81,10 @@ import org.springframework.util.StringUtils;
*
* @author Juergen Hoeller
* @author Sebastien Deleuze
* @author Sam Brannen
* @see #setCacheSeconds
* @see #setBasenames
* @see #setDefaultEncoding
* @see #setDefaultCharset
* @see #setFileEncodings
* @see #setPropertiesPersister
* @see #setResourceLoader
@@ -93,10 +95,12 @@ import org.springframework.util.StringUtils;
public class ReloadableResourceBundleMessageSource extends AbstractResourceBasedMessageSource
implements ResourceLoaderAware {
private static final String PROPERTIES_EXTENSION = ".properties";
private static final String XML_EXTENSION = ".xml";
private List<String> fileExtensions = List.of(".properties", XML_EXTENSION);
private List<String> fileExtensions = List.of(PROPERTIES_EXTENSION, XML_EXTENSION);
private @Nullable Properties fileEncodings;
@@ -135,7 +139,7 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
/**
* Set per-file charsets to use for parsing properties files.
* <p>Only applies to classic properties files, not to XML files.
* @param fileEncodings a Properties with filenames as keys and charset
* @param fileEncodings a Properties object with filenames as keys and charset
* names as values. Filenames have to match the basename syntax,
* with optional locale-specific components: for example, "WEB-INF/messages"
* or "WEB-INF/messages_en".
@@ -378,18 +382,18 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
StringBuilder temp = new StringBuilder(basename);
temp.append('_');
if (language.length() > 0) {
if (!language.isEmpty()) {
temp.append(language);
result.add(0, temp.toString());
}
temp.append('_');
if (country.length() > 0) {
if (!country.isEmpty()) {
temp.append(country);
result.add(0, temp.toString());
}
if (variant.length() > 0 && (language.length() > 0 || country.length() > 0)) {
if (!variant.isEmpty() && (!language.isEmpty() || !country.isEmpty())) {
temp.append('_').append(variant);
result.add(0, temp.toString());
}
@@ -558,33 +562,36 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
*/
protected Properties loadProperties(Resource resource, String filename) throws IOException {
Properties props = newProperties();
try (InputStream is = resource.getInputStream()) {
try (InputStream inputStream = resource.getInputStream()) {
String resourceFilename = resource.getFilename();
if (resourceFilename != null && resourceFilename.endsWith(XML_EXTENSION)) {
if (logger.isDebugEnabled()) {
logger.debug("Loading properties [" + resource.getFilename() + "]");
}
this.propertiesPersister.loadFromXml(props, is);
this.propertiesPersister.loadFromXml(props, inputStream);
}
else {
String encoding = null;
Charset charset = null;
if (this.fileEncodings != null) {
encoding = this.fileEncodings.getProperty(filename);
}
if (encoding == null) {
encoding = getDefaultEncoding();
}
if (encoding != null) {
if (logger.isDebugEnabled()) {
logger.debug("Loading properties [" + resource.getFilename() + "] with encoding '" + encoding + "'");
String charsetName = this.fileEncodings.getProperty(filename);
if (charsetName != null) {
charset = Charset.forName(charsetName);
}
this.propertiesPersister.load(props, new InputStreamReader(is, encoding));
}
if (charset == null) {
charset = getDefaultCharset();
}
if (charset != null) {
if (logger.isDebugEnabled()) {
logger.debug("Loading properties [" + resource.getFilename() + "] with encoding '" + charset + "'");
}
this.propertiesPersister.load(props, new InputStreamReader(inputStream, charset));
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Loading properties [" + resource.getFilename() + "]");
}
this.propertiesPersister.load(props, is);
this.propertiesPersister.load(props, inputStream);
}
}
return props;
@@ -22,6 +22,8 @@ import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.Locale;
import java.util.Map;
@@ -57,11 +59,11 @@ import org.springframework.util.ClassUtils;
* This means that "test.theme" is effectively equivalent to "test/theme".
*
* <p>On the classpath, bundle resources will be read with the locally configured
* {@link #setDefaultEncoding encoding}: by default, ISO-8859-1; consider switching
* {@link #setDefaultCharset Charset}: by default, ISO-8859-1; consider switching
* this to UTF-8, or to {@code null} for the platform default encoding. On the JDK 9+
* module path where locally provided {@code ResourceBundle.Control} handles are not
* supported, this MessageSource always falls back to {@link ResourceBundle#getBundle}
* retrieval with the platform default encoding: UTF-8 with a ISO-8859-1 fallback on
* retrieval with the platform default encoding: UTF-8 with an ISO-8859-1 fallback on
* JDK 9+ (configurable through the "java.util.PropertyResourceBundle.encoding" system
* property). Note that {@link #loadBundle(Reader)}/{@link #loadBundle(InputStream)}
* won't be called in this case either, effectively ignoring overrides in subclasses.
@@ -70,6 +72,7 @@ import org.springframework.util.ClassUtils;
* @author Rod Johnson
* @author Juergen Hoeller
* @author Qimiao Chen
* @author Sam Brannen
* @see #setBasenames
* @see ReloadableResourceBundleMessageSource
* @see java.util.ResourceBundle
@@ -83,7 +86,7 @@ public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSou
/**
* Cache to hold loaded ResourceBundles.
* This Map is keyed with the bundle basename, which holds a Map that is
* <p>This Map is keyed with the bundle basename, which holds a Map that is
* keyed with the Locale and in turn holds the ResourceBundle instances.
* This allows for very efficient hash lookups, significantly faster
* than the ResourceBundle class's own cache.
@@ -93,7 +96,7 @@ public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSou
/**
* Cache to hold already generated MessageFormats.
* This Map is keyed with the ResourceBundle, which holds a Map that is
* <p>This Map is keyed with the ResourceBundle, which holds a Map that is
* keyed with the message code, which in turn holds a Map that is keyed
* with the Locale and holds the MessageFormat values. This allows for
* very efficient hash lookups without concatenated keys.
@@ -106,7 +109,7 @@ public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSou
public ResourceBundleMessageSource() {
setDefaultEncoding("ISO-8859-1");
setDefaultCharset(StandardCharsets.ISO_8859_1);
}
@@ -239,12 +242,12 @@ public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSou
catch (UnsupportedOperationException ex) {
// Probably in a Java Module System environment on JDK 9+
this.control = null;
String encoding = getDefaultEncoding();
if (encoding != null && logger.isInfoEnabled()) {
Charset charset = getDefaultCharset();
if (charset != null && logger.isInfoEnabled()) {
logger.info("ResourceBundleMessageSource is configured to read resources with encoding '" +
encoding + "' but ResourceBundle.Control is not supported in current system environment: " +
charset + "' but ResourceBundle.Control is not supported in current system environment: " +
ex.getMessage() + " - falling back to plain ResourceBundle.getBundle retrieval with the " +
"platform default encoding. Consider setting the 'defaultEncoding' property to 'null' " +
"platform default encoding. Consider setting the 'defaultCharset' property to 'null' " +
"for participating in the platform default and therefore avoiding this log message.");
}
}
@@ -256,7 +259,7 @@ public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSou
/**
* Load a property-based resource bundle from the given reader.
* <p>This will be called in case of a {@link #setDefaultEncoding "defaultEncoding"},
* <p>This will be called in case of a {@linkplain #setDefaultCharset "defaultCharset"},
* including {@link ResourceBundleMessageSource}'s default ISO-8859-1 encoding.
* Note that this method can only be called with a {@code ResourceBundle.Control}:
* When running on the JDK 9+ module path where such control handles are not
@@ -276,9 +279,9 @@ public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSou
/**
* Load a property-based resource bundle from the given input stream,
* picking up the default properties encoding on JDK 9+.
* <p>This will only be called with {@link #setDefaultEncoding "defaultEncoding"}
* <p>This will only be called with {@linkplain #setDefaultCharset "defaultCharset"}
* set to {@code null}, explicitly enforcing the platform default encoding
* (which is UTF-8 with a ISO-8859-1 fallback on JDK 9+ but configurable
* (which is UTF-8 with an ISO-8859-1 fallback on JDK 9+ but configurable
* through the "java.util.PropertyResourceBundle.encoding" system property).
* Note that this method can only be called with a {@code ResourceBundle.Control}:
* When running on the JDK 9+ module path where such control handles are not
@@ -404,9 +407,9 @@ public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSou
inputStream = classLoader.getResourceAsStream(resourceName);
}
if (inputStream != null) {
String encoding = getDefaultEncoding();
if (encoding != null) {
try (InputStreamReader bundleReader = new InputStreamReader(inputStream, encoding)) {
Charset charset = getDefaultCharset();
if (charset != null) {
try (InputStreamReader bundleReader = new InputStreamReader(inputStream, charset)) {
return loadBundle(bundleReader);
}
}
@@ -239,8 +239,8 @@ public class ThreadPoolTaskExecutor extends ExecutorConfigurationSupport
* @since 6.1.4
* @see #initiateShutdown()
*/
public void setStrictEarlyShutdown(boolean defaultEarlyShutdown) {
this.strictEarlyShutdown = defaultEarlyShutdown;
public void setStrictEarlyShutdown(boolean strictEarlyShutdown) {
this.strictEarlyShutdown = strictEarlyShutdown;
}
/**
@@ -30,6 +30,7 @@ import org.mockito.ArgumentCaptor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanPostProcessor;
@@ -435,12 +436,15 @@ class ApplicationContextEventTests extends AbstractApplicationEventListenerTests
RootBeanDefinition listener1Def = new RootBeanDefinition(MyOrderedListener1.class);
listener1Def.setDependsOn("nestedChild");
context.registerBeanDefinition("listener1", listener1Def);
context.registerBeanDefinition("listenerFb", new RootBeanDefinition(MyFactoryBeanListener.class));
context.refresh();
MyOrderedListener1 listener1 = context.getBean("listener1", MyOrderedListener1.class);
MyFactoryBeanListener listenerFb = context.getBean("&listenerFb", MyFactoryBeanListener.class);
MyEvent event1 = new MyEvent(context);
context.publishEvent(event1);
assertThat(listener1.seenEvents).contains(event1);
assertThat(listenerFb.seenEvents).contains(event1);
SimpleApplicationEventMulticaster multicaster = context.getBean(SimpleApplicationEventMulticaster.class);
assertThat(multicaster.getApplicationListeners()).isNotEmpty();
@@ -782,6 +786,27 @@ class ApplicationContextEventTests extends AbstractApplicationEventListenerTests
}
public static class MyFactoryBeanListener implements FactoryBean<String>, ApplicationListener<ApplicationEvent> {
public final List<ApplicationEvent> seenEvents = new ArrayList<>();
@Override
public void onApplicationEvent(ApplicationEvent event) {
this.seenEvents.add(event);
}
@Override
public String getObject() {
return "";
}
@Override
public Class<?> getObjectType() {
return String.class;
}
}
public static class EventPublishingBeanPostProcessor implements BeanPostProcessor, ApplicationContextAware {
private ApplicationContext applicationContext;
@@ -16,6 +16,7 @@
package org.springframework.context.support;
import java.nio.charset.UnsupportedCharsetException;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
@@ -30,14 +31,18 @@ import org.springframework.context.MessageSourceResolvable;
import org.springframework.context.NoSuchMessageException;
import org.springframework.context.i18n.LocaleContextHolder;
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Tests for {@link ResourceBundleMessageSource} and {@link ReloadableResourceBundleMessageSource}.
*
* @author Juergen Hoeller
* @author Sebastien Deleuze
* @author Sam Brannen
* @since 03.02.2004
*/
class ResourceBundleMessageSourceTests {
@@ -272,23 +277,28 @@ class ResourceBundleMessageSourceTests {
assertThat(ms.getMessage("code2", null, Locale.GERMAN)).isEqualTo("nachricht2");
}
@Test // gh-36413
void resourceBundleMessageSourceWithInvalidDefaultCharsetName() {
ResourceBundleMessageSource ms = new ResourceBundleMessageSource();
assertThatExceptionOfType(UnsupportedCharsetException.class).isThrownBy(() -> ms.setDefaultEncoding("BOGUS"));
}
@Test
void resourceBundleMessageSourceWithDefaultCharset() {
void resourceBundleMessageSourceWithDefaultCharsetName() {
ResourceBundleMessageSource ms = new ResourceBundleMessageSource();
ms.setBasename("org/springframework/context/support/messages");
ms.setDefaultEncoding("ISO-8859-1");
ms.setDefaultEncoding(ISO_8859_1.name());
assertThat(ms.getMessage("code1", null, Locale.ENGLISH)).isEqualTo("message1");
assertThat(ms.getMessage("code2", null, Locale.GERMAN)).isEqualTo("nachricht2");
}
@Test
void resourceBundleMessageSourceWithInappropriateDefaultCharset() {
@Test // gh-36413
void resourceBundleMessageSourceWithDefaultCharset() {
ResourceBundleMessageSource ms = new ResourceBundleMessageSource();
ms.setBasename("org/springframework/context/support/messages");
ms.setDefaultEncoding("argh");
ms.setFallbackToSystemLocale(false);
assertThatExceptionOfType(NoSuchMessageException.class).isThrownBy(() ->
ms.getMessage("code1", null, Locale.ENGLISH));
ms.setDefaultCharset(ISO_8859_1);
assertThat(ms.getMessage("code1", null, Locale.ENGLISH)).isEqualTo("message1");
assertThat(ms.getMessage("code2", null, Locale.GERMAN)).isEqualTo("nachricht2");
}
@Test
@@ -353,13 +363,13 @@ class ResourceBundleMessageSourceTests {
void reloadableResourceBundleMessageSourceWithDefaultCharset() {
ReloadableResourceBundleMessageSource ms = new ReloadableResourceBundleMessageSource();
ms.setBasename("org/springframework/context/support/messages");
ms.setDefaultEncoding("ISO-8859-1");
ms.setDefaultCharset(ISO_8859_1);
assertThat(ms.getMessage("code1", null, Locale.ENGLISH)).isEqualTo("message1");
assertThat(ms.getMessage("code2", null, Locale.GERMAN)).isEqualTo("nachricht2");
}
@Test
void reloadableResourceBundleMessageSourceWithInappropriateDefaultCharset() {
void reloadableResourceBundleMessageSourceWithInappropriateDefaultCharsetName() {
ReloadableResourceBundleMessageSource ms = new ReloadableResourceBundleMessageSource();
ms.setBasename("org/springframework/context/support/messages");
ms.setDefaultEncoding("unicode");
+2 -2
View File
@@ -12,8 +12,8 @@ dependencies {
jar {
manifest {
attributes(
'Premain-Class': 'org.springframework.aot.agent.RuntimeHintsAgent',
'Can-Redefine-Classes': 'true'
'Premain-Class': 'org.springframework.aot.agent.RuntimeHintsAgent',
'Can-Redefine-Classes': 'true'
)
}
}
@@ -16,7 +16,6 @@
package org.springframework.aot.agent;
import org.springframework.aot.hint.JavaSerializationHint;
import org.springframework.aot.hint.JdkProxyHint;
import org.springframework.aot.hint.ReflectionHints;
import org.springframework.aot.hint.ResourceBundleHint;
@@ -51,7 +50,9 @@ public enum HintType {
/**
* Java serialization hint, as described by {@link org.springframework.aot.hint.JavaSerializationHint}.
*/
JAVA_SERIALIZATION(JavaSerializationHint.class),
@Deprecated(since = "7.0.5", forRemoval = true)
@SuppressWarnings("removal")
JAVA_SERIALIZATION(org.springframework.aot.hint.JavaSerializationHint.class),
/**
* JDK proxies hint, as described by {@link org.springframework.aot.hint.ProxyHints#jdkProxyHints()}.
+1 -2
View File
@@ -92,7 +92,7 @@ dependencies {
optional("org.jetbrains.kotlin:kotlin-stdlib")
optional("org.jetbrains.kotlinx:kotlinx-coroutines-core")
optional("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
testCompileOnly("com.github.ben-manes.caffeine:caffeine")
testCompileOnly("com.google.code.findbugs:jsr305")
testFixturesImplementation("io.projectreactor:reactor-test")
testFixturesImplementation("org.assertj:assertj-core")
testFixturesImplementation("org.junit.jupiter:junit-jupiter")
@@ -100,7 +100,6 @@ dependencies {
testFixturesImplementation("org.xmlunit:xmlunit-assertj")
testImplementation("com.fasterxml.jackson.core:jackson-databind")
testImplementation("com.fasterxml.woodstox:woodstox-core")
testImplementation("com.google.code.findbugs:jsr305")
testImplementation("com.squareup.okhttp3:mockwebserver3")
testImplementation("io.projectreactor:reactor-test")
testImplementation("io.projectreactor.tools:blockhound")
@@ -27,7 +27,9 @@ import org.jspecify.annotations.Nullable;
*
* @author Brian Clozel
* @since 6.0
* @deprecated in favor of {@link TypeHint}
*/
@Deprecated(since = "7.0.6", forRemoval = true)
public final class JavaSerializationHint implements ConditionalHint {
private final TypeReference type;
@@ -37,10 +37,13 @@ public final class JdkProxyHint implements ConditionalHint {
private final @Nullable TypeReference reachableType;
private final boolean javaSerialization;
private JdkProxyHint(Builder builder) {
this.proxiedInterfaces = List.copyOf(builder.proxiedInterfaces);
this.reachableType = builder.reachableType;
this.javaSerialization = builder.javaSerialization;
}
/**
@@ -75,11 +78,21 @@ public final class JdkProxyHint implements ConditionalHint {
return this.reachableType;
}
/**
* Return whether this hint registers the proxy for Java serialization.
* @return whether the proxy is registered for Java serialization
* @since 7.0.6
*/
public boolean hasJavaSerialization() {
return this.javaSerialization;
}
@Override
public boolean equals(@Nullable Object other) {
return (this == other || (other instanceof JdkProxyHint that &&
this.proxiedInterfaces.equals(that.proxiedInterfaces) &&
Objects.equals(this.reachableType, that.reachableType)));
Objects.equals(this.reachableType, that.reachableType) &&
Objects.equals(this.javaSerialization, that.javaSerialization)));
}
@Override
@@ -97,6 +110,8 @@ public final class JdkProxyHint implements ConditionalHint {
private @Nullable TypeReference reachableType;
private boolean javaSerialization;
Builder() {
this.proxiedInterfaces = new LinkedList<>();
}
@@ -131,6 +146,17 @@ public final class JdkProxyHint implements ConditionalHint {
return this;
}
/**
* Specify if this proxy should be registered for Java serialization.
* @param javaSerialization whether to register this proxy for Java serialization
* @return {@code this}, to facilitate method chaining
* @since 7.0.6
*/
public Builder withJavaSerialization(boolean javaSerialization) {
this.javaSerialization = javaSerialization;
return this;
}
/**
* Create a {@link JdkProxyHint} based on the state of this builder.
* @return a JDK proxy hint
@@ -0,0 +1,186 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aot.hint;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.jspecify.annotations.Nullable;
/**
* A hint that describes the need of reflection for a Lambda.
*
* @author Stephane Nicoll
* @since 7.0.6
*/
public final class LambdaHint implements ConditionalHint {
private final TypeReference declaringClass;
private final @Nullable TypeReference reachableType;
private final @Nullable DeclaringMethod declaringMethod;
private final List<TypeReference> interfaces;
private LambdaHint(Builder builder) {
this.declaringClass = builder.declaringClass;
this.reachableType = builder.reachableType;
this.declaringMethod = builder.declaringMethod;
this.interfaces = List.copyOf(builder.interfaces);
}
/**
* Initialize a builder with the class declaring the lambda.
* @param declaringClass the type declaring the lambda
* @return a builder for the hint
*/
public static Builder of(TypeReference declaringClass) {
return new Builder(declaringClass);
}
/**
* Initialize a builder with the class declaring the lambda.
* @param declaringClass the type declaring the lambda
* @return a builder for the hint
*/
public static Builder of(Class<?> declaringClass) {
return new Builder(TypeReference.of(declaringClass));
}
/**
* Return the type declaring the lambda.
* @return the declaring class
*/
public TypeReference getDeclaringClass() {
return this.declaringClass;
}
@Override
public @Nullable TypeReference getReachableType() {
return this.reachableType;
}
/**
* Return the method in which the lambda is defined, if any.
* @return the declaring method
*/
public @Nullable DeclaringMethod getDeclaringMethod() {
return this.declaringMethod;
}
/**
* Return the interfaces that are implemented by the lambda.
* @return the interfaces
*/
public List<TypeReference> getInterfaces() {
return this.interfaces;
}
public static class Builder {
private final TypeReference declaringClass;
private @Nullable TypeReference reachableType;
private @Nullable DeclaringMethod declaringMethod;
private final List<TypeReference> interfaces = new ArrayList<>();
Builder(TypeReference declaringClass) {
this.declaringClass = declaringClass;
}
/**
* Make this hint conditional on the fact that the specified type is in a
* reachable code path from a static analysis point of view.
* @param reachableType the type that should be reachable for this hint to apply
* @return {@code this}, to facilitate method chaining
*/
public Builder onReachableType(TypeReference reachableType) {
this.reachableType = reachableType;
return this;
}
/**
* Make this hint conditional on the fact that the specified type is in a
* reachable code path from a static analysis point of view.
* @param reachableType the type that should be reachable for this hint to apply
* @return {@code this}, to facilitate method chaining
*/
public Builder onReachableType(Class<?> reachableType) {
this.reachableType = TypeReference.of(reachableType);
return this;
}
/**
* Set the method that declares the lambda.
* @param name the name of the method
* @param parameterTypes the parameter types, if any.
* @return {@code this}, to facilitate method chaining
*/
public Builder withDeclaringMethod(String name, List<TypeReference> parameterTypes) {
this.declaringMethod = new DeclaringMethod(name, parameterTypes);
return this;
}
/**
* Set the method that declares the lambda.
* @param name the name of the method
* @param parameterTypes the parameter types, if any.
* @return {@code this}, to facilitate method chaining
*/
public Builder withDeclaringMethod(String name, Class<?>... parameterTypes) {
return withDeclaringMethod(name, Arrays.stream(parameterTypes).map(TypeReference::of).toList());
}
/**
* Add the specified interfaces that the lambda should implement.
* @param interfaces the interfaces the lambda should implement
* @return {@code this}, to facilitate method chaining
*/
public Builder withInterfaces(TypeReference... interfaces) {
this.interfaces.addAll(Arrays.asList(interfaces));
return this;
}
/**
* Add the specified interfaces that the lambda should implement.
* @param interfaces the interfaces the lambda should implement
* @return {@code this}, to facilitate method chaining
*/
public Builder withInterfaces(Class<?>... interfaces) {
this.interfaces.addAll(Arrays.stream(interfaces).map(TypeReference::of).toList());
return this;
}
public LambdaHint build() {
return new LambdaHint(this);
}
}
/**
* Describe a method.
* @param name the name of the method
* @param parameterTypes the parameter types
*/
public record DeclaringMethod(String name, List<TypeReference> parameterTypes) {
}
}
@@ -21,8 +21,10 @@ import java.lang.reflect.Executable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Stream;
@@ -45,6 +47,7 @@ public class ReflectionHints {
private final Map<TypeReference, TypeHint.Builder> types = new HashMap<>();
private final Set<LambdaHint> lambdaHints = new LinkedHashSet<>();
/**
* Return the types that require reflection.
@@ -54,6 +57,16 @@ public class ReflectionHints {
return this.types.values().stream().map(TypeHint.Builder::build);
}
/**
* Return the lambda hints.
* @return a stream of {@link LambdaHint}
* @since 7.0.6
*/
public Stream<LambdaHint> lambdaHints() {
return this.lambdaHints.stream();
}
/**
* Return the reflection hints for the type defined by the specified
* {@link TypeReference}.
@@ -231,6 +244,42 @@ public class ReflectionHints {
typeHint -> typeHint.withMethod(method.getName(), mapParameters(method), mode));
}
/**
* Register the need for Java Serialization on the specified type.
* @param type the type that should be serializable
* @return {@code this}, to facilitate method chaining
* @since 7.0.6
*/
public ReflectionHints registerJavaSerialization(Class<?> type) {
return registerType(TypeReference.of(type),
typeHint -> typeHint.withJavaSerialization(true));
}
/**
* Register a {@link LambdaHint}.
* @param declaringClass the type declaring the lambda
* @param lambdaHint the consumer of the hint builder
* @return {@code this}, to facilitate method chaining
* @since 7.0.6
*/
public ReflectionHints registerLambda(TypeReference declaringClass, Consumer<LambdaHint.Builder> lambdaHint) {
LambdaHint.Builder builder = LambdaHint.of(declaringClass);
lambdaHint.accept(builder);
this.lambdaHints.add(builder.build());
return this;
}
/**
* Register a {@link LambdaHint}.
* @param declaringClass the type declaring the lambda
* @param lambdaHint the consumer of the hint builder
* @return {@code this}, to facilitate method chaining
* @since 7.0.6
*/
public ReflectionHints registerLambda(Class<?> declaringClass, Consumer<LambdaHint.Builder> lambdaHint) {
return this.registerLambda(TypeReference.of(declaringClass), lambdaHint);
}
private List<TypeReference> mapParameters(Executable executable) {
return TypeReference.listOf(executable.getParameterTypes());
}
@@ -17,15 +17,15 @@
package org.springframework.aot.hint;
/**
* Gather hints that can be used to optimize the application runtime.
* Hints that can be used to optimize the application runtime.
*
* <p>Use of reflection can be recorded for individual members of a type, as
* well as broader {@linkplain MemberCategory member categories}. Access to
* resources can be specified using patterns or the base name of a resource
* bundle.
* <p>The use of reflection can be recorded for individual members of a type,
* lambdas, or broader {@linkplain MemberCategory member categories}.
*
* <p>Hints that require the need for Java serialization of proxies can be
* recorded as well.
* <p>Access to resources can be specified using patterns or the base name of a
* resource bundle.
*
* <p>The need for Java serialization or proxies can be recorded as well.
*
* @author Stephane Nicoll
* @author Janne Valkealahti
@@ -37,6 +37,7 @@ public class RuntimeHints {
private final ResourceHints resources = new ResourceHints();
@SuppressWarnings("removal")
private final SerializationHints serialization = new SerializationHints();
private final ProxyHints proxies = new ProxyHints();
@@ -63,7 +64,10 @@ public class RuntimeHints {
/**
* Provide access to serialization-based hints.
* @return serialization hints
* @deprecated in favor of {@link #reflection()}
*/
@SuppressWarnings("removal")
@Deprecated(since = "7.0.6", forRemoval = true)
public SerializationHints serialization() {
return this.serialization;
}
@@ -77,8 +81,8 @@ public class RuntimeHints {
}
/**
* Provide access to jni-based hints.
* @return jni hints
* Provide access to JNI-based hints.
* @return JNI hints
*/
public ReflectionHints jni() {
return this.jni;
@@ -30,7 +30,10 @@ import org.jspecify.annotations.Nullable;
* @author Stephane Nicoll
* @since 6.0
* @see Serializable
* @deprecated in favor of {@link ReflectionHints}
*/
@Deprecated(since = "7.0.6", forRemoval = true)
@SuppressWarnings("removal")
public class SerializationHints {
private final Set<JavaSerializationHint> javaSerializationHints;
@@ -53,6 +53,8 @@ public final class TypeHint implements ConditionalHint {
private final Set<MemberCategory> memberCategories;
private final boolean javaSerialization;
private TypeHint(Builder builder) {
this.type = builder.type;
@@ -61,6 +63,7 @@ public final class TypeHint implements ConditionalHint {
this.fields = builder.fields.stream().map(FieldHint::new).collect(Collectors.toSet());
this.constructors = builder.constructors.values().stream().map(ExecutableHint.Builder::build).collect(Collectors.toSet());
this.methods = builder.methods.values().stream().map(ExecutableHint.Builder::build).collect(Collectors.toSet());
this.javaSerialization = builder.javaSerialization;
}
/**
@@ -120,6 +123,15 @@ public final class TypeHint implements ConditionalHint {
return this.memberCategories;
}
/**
* Return whether this hint registers the type for Java serialization.
* @return whether the type is registered for Java serialization
* @since 7.0.6
*/
public boolean hasJavaSerialization() {
return this.javaSerialization;
}
@Override
public String toString() {
return TypeHint.class.getSimpleName() + "[type=" + this.type + "]";
@@ -153,6 +165,8 @@ public final class TypeHint implements ConditionalHint {
private final Set<MemberCategory> memberCategories = new HashSet<>();
private boolean javaSerialization;
Builder(TypeReference type) {
this.type = type;
}
@@ -260,6 +274,17 @@ public final class TypeHint implements ConditionalHint {
return this;
}
/**
* Specify if this type should be registered for Java serialization.
* @param javaSerialization whether to register this type for Java serialization.
* @return {@code this}, to facilitate method chaining
* @since 7.0.6
*/
public Builder withJavaSerialization(boolean javaSerialization) {
this.javaSerialization = javaSerialization;
return this;
}
/**
* Create a {@link TypeHint} based on the state of this builder.
* @return a type hint
@@ -260,7 +260,7 @@ public class ReflectionHintsPredicates {
}
/**
* Return a predicate that checks whether an invocation hint is registered for the field.
* Return a predicate that checks whether a reflective field access hint is registered for the field.
* This looks up a field on the given type with the expected name, if present.
* @param className the name of the class holding the field
* @param fieldName the field name
@@ -287,7 +287,8 @@ public class ReflectionHintsPredicates {
}
/**
* Return a predicate that checks whether an invocation hint is registered for the given field.
* Return a predicate that checks whether a reflective field access hint is
* registered for the given field.
* @param field the field
* @return the {@link RuntimeHints} predicate
* @since 7.0
@@ -297,6 +298,32 @@ public class ReflectionHintsPredicates {
return new FieldHintPredicate(field);
}
/**
* Return a predicate that checks whether Java serialization is configured
* for the type according to the given flag.
* @param type the type to check
* @param javaSerialization whether the type is expected to be registered for Java serialization
* @return the {@link RuntimeHints} predicate
* @since 7.0.6
*/
public Predicate<RuntimeHints> onJavaSerialization(Class<?> type, boolean javaSerialization) {
Assert.notNull(type, "'type' must not be null");
return new JavaSerializationHintPredicate(TypeReference.of(type), javaSerialization);
}
/**
* Return a predicate that checks whether Java serialization is configured
* for the type according to the given flag.
* @param typeReference the type reference to check
* @param javaSerialization whether the type is expected to be registered for Java serialization
* @return the {@link RuntimeHints} predicate
* @since 7.0.6
*/
public Predicate<RuntimeHints> onJavaSerialization(TypeReference typeReference, boolean javaSerialization) {
Assert.notNull(typeReference, "'typeReference' must not be null");
return new JavaSerializationHintPredicate(typeReference, javaSerialization);
}
public static class TypeHintPredicate implements Predicate<RuntimeHints> {
@@ -509,4 +536,26 @@ public class ReflectionHintsPredicates {
}
}
public static class JavaSerializationHintPredicate implements Predicate<RuntimeHints> {
private final TypeReference typeReference;
private final boolean javaSerialization;
JavaSerializationHintPredicate(TypeReference typeReference, boolean javaSerialization) {
this.typeReference = typeReference;
this.javaSerialization = javaSerialization;
}
@Override
public boolean test(RuntimeHints runtimeHints) {
TypeHint typeHint = runtimeHints.reflection().getTypeHint(this.typeReference);
if (typeHint == null) {
return false;
}
return typeHint.hasJavaSerialization() == this.javaSerialization;
}
}
}
@@ -46,6 +46,7 @@ public abstract class RuntimeHintsPredicates {
private static final ResourceHintsPredicates resource = new ResourceHintsPredicates();
@SuppressWarnings("removal")
private static final SerializationHintsPredicates serialization = new SerializationHintsPredicates();
private static final ProxyHintsPredicates proxies = new ProxyHintsPredicates();
@@ -73,7 +74,10 @@ public abstract class RuntimeHintsPredicates {
/**
* Return a predicate generator for {@link SerializationHints serialization hints}.
* @return the predicate generator
* @deprecated in favor of {@link #reflection()}
*/
@Deprecated(since = "7.0.6", forRemoval = true)
@SuppressWarnings("removal")
public static SerializationHintsPredicates serialization() {
return serialization;
}
@@ -29,7 +29,10 @@ import org.springframework.util.Assert;
*
* @author Stephane Nicoll
* @since 6.0
* @deprecated in favor of {@link ReflectionHintsPredicates}
*/
@Deprecated(since = "7.0.6", forRemoval = true)
@SuppressWarnings("removal")
public class SerializationHintsPredicates {
SerializationHintsPredicates() {
@@ -44,12 +44,17 @@ public abstract class NativeConfigurationWriter {
}
private boolean hasAnyHint(RuntimeHints hints) {
return (hints.serialization().javaSerializationHints().findAny().isPresent() ||
hints.proxies().jdkProxyHints().findAny().isPresent() ||
return (hints.proxies().jdkProxyHints().findAny().isPresent() ||
hints.reflection().typeHints().findAny().isPresent() ||
hints.resources().resourcePatternHints().findAny().isPresent() ||
hints.resources().resourceBundleHints().findAny().isPresent() ||
hints.jni().typeHints().findAny().isPresent());
hints.jni().typeHints().findAny().isPresent() ||
hasAnyDeprecatedHint(hints));
}
@SuppressWarnings("removal")
private boolean hasAnyDeprecatedHint(RuntimeHints hints) {
return hints.serialization().javaSerializationHints().findAny().isPresent();
}
/**
@@ -22,6 +22,7 @@ import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -32,7 +33,9 @@ import org.springframework.aot.hint.ConditionalHint;
import org.springframework.aot.hint.ExecutableHint;
import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.FieldHint;
import org.springframework.aot.hint.JavaSerializationHint;
import org.springframework.aot.hint.JdkProxyHint;
import org.springframework.aot.hint.LambdaHint;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.ReflectionHints;
import org.springframework.aot.hint.RuntimeHints;
@@ -61,17 +64,37 @@ class ReflectionHintsAttributes {
return leftSignature.compareTo(rightSignature);
};
private static final Comparator<LambdaHint> LAMBDA_HINT_COMPARATOR =
Comparator.comparing(LambdaHint::getDeclaringClass)
.thenComparing(LambdaHint::getDeclaringMethod, Comparator.nullsFirst(
Comparator.comparing(LambdaHint.DeclaringMethod::name)));
public List<Map<String, Object>> reflection(RuntimeHints hints) {
List<Map<String, Object>> reflectionHints = new ArrayList<>();
reflectionHints.addAll(hints.reflection().typeHints()
.sorted(Comparator.comparing(TypeHint::getType))
.map(this::toAttributes).toList());
List<Map<String, Object>> reflectionHints = new ArrayList<>(reflectionHints(hints));
reflectionHints.addAll(hints.proxies().jdkProxyHints()
.sorted(JDK_PROXY_HINT_COMPARATOR)
.map(this::toAttributes).toList());
return reflectionHints;
}
@SuppressWarnings("removal")
private List<Map<String, Object>> reflectionHints(RuntimeHints hints) {
Map<TypeReference, Map<String, Object>> allTypeHints = hints.reflection().typeHints()
.map(this::toAttributes).collect(Collectors.toMap((attributes -> (TypeReference) Objects.requireNonNull(attributes.get("type"))),
attributes -> attributes));
hints.serialization().javaSerializationHints().forEach(hint -> {
allTypeHints.merge(hint.getType(), toAttributes(hint),
(currentAttributes, newAttributes) -> {
handleSerializable(currentAttributes, true);
return currentAttributes;
});
});
return Stream.concat(
allTypeHints.entrySet().stream().sorted(Map.Entry.comparingByKey()).map(Map.Entry::getValue),
hints.reflection().lambdaHints().sorted(LAMBDA_HINT_COMPARATOR).map(this::toAttributes)).toList();
}
public List<Map<String, Object>> jni(RuntimeHints hints) {
List<Map<String, Object>> jniHints = new ArrayList<>();
jniHints.addAll(hints.jni().typeHints()
@@ -88,6 +111,33 @@ class ReflectionHintsAttributes {
handleFields(attributes, hint.fields());
handleExecutables(attributes, Stream.concat(
hint.constructors(), hint.methods()).sorted().toList());
handleSerializable(attributes, hint.hasJavaSerialization());
return attributes;
}
private Map<String, Object> toAttributes(LambdaHint hint) {
Map<String, Object> attributes = new LinkedHashMap<>();
Map<String, Object> lambdaAttributes = new LinkedHashMap<>();
lambdaAttributes.put("declaringClass", hint.getDeclaringClass());
LambdaHint.DeclaringMethod declaringMethod = hint.getDeclaringMethod();
if (declaringMethod != null) {
Map<String, Object> methodAttributes = new LinkedHashMap<>();
methodAttributes.put("name", declaringMethod.name());
methodAttributes.put("parameterTypes", declaringMethod.parameterTypes());
lambdaAttributes.put("declaringMethod", methodAttributes);
}
lambdaAttributes.put("interfaces", hint.getInterfaces());
attributes.put("lambda", lambdaAttributes);
return Map.of("type", attributes);
}
@SuppressWarnings("removal")
private Map<String, Object> toAttributes(JavaSerializationHint serializationHint) {
LinkedHashMap<String, Object> attributes = new LinkedHashMap<>();
handleCondition(attributes, serializationHint);
attributes.put("type", serializationHint.getType());
handleSerializable(attributes, true);
return attributes;
}
@@ -148,7 +198,14 @@ class ReflectionHintsAttributes {
Map<String, Object> attributes = new LinkedHashMap<>();
handleCondition(attributes, hint);
attributes.put("type", Map.of("proxy", hint.getProxiedInterfaces()));
handleSerializable(attributes, hint.hasJavaSerialization());
return attributes;
}
private void handleSerializable(Map<String, Object> attributes, boolean serializable) {
if (serializable) {
attributes.put("serializable", serializable);
}
}
}
@@ -16,6 +16,7 @@
package org.springframework.aot.nativex;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
@@ -35,8 +36,9 @@ import org.springframework.aot.hint.ResourcePatternHints;
* @author Stephane Nicoll
* @author Brian Clozel
* @since 6.0
* @see <a href="https://www.graalvm.org/22.1/reference-manual/native-image/Resources/">Accessing Resources in Native Images</a>
* @see <a href="https://www.graalvm.org/22.1/reference-manual/native-image/BuildConfiguration/">Native Image Build Configuration</a>
* @see <a href="https://www.graalvm.org/jdk25/reference-manual/native-image/metadata/#resources">Accessing Resources in Native Images</a>
* @see <a href="https://www.graalvm.org/jdk25/reference-manual/native-image/metadata/#resource-bundles">Accessing Resource Bundles in Native Images</a>
* @see <a href="https://www.graalvm.org/jdk25/reference-manual/native-image/overview/BuildConfiguration">Native Image Build Configuration</a>
*/
class ResourceHintsAttributes {
@@ -48,22 +50,21 @@ class ResourceHintsAttributes {
public List<Map<String, Object>> resources(ResourceHints hint) {
return hint.resourcePatternHints()
List<Map<String, Object>> resourceHints = new ArrayList<>();
resourceHints.addAll(hint.resourcePatternHints()
.map(ResourcePatternHints::getIncludes).flatMap(List::stream).distinct()
.sorted(RESOURCE_PATTERN_HINT_COMPARATOR)
.map(this::toAttributes).toList();
}
public List<Map<String, Object>> resourceBundles(ResourceHints hint) {
return hint.resourceBundleHints()
.map(this::toAttributes).toList());
resourceHints.addAll(hint.resourceBundleHints()
.sorted(RESOURCE_BUNDLE_HINT_COMPARATOR)
.map(this::toAttributes).toList();
.map(this::toAttributes).toList());
return resourceHints;
}
private Map<String, Object> toAttributes(ResourceBundleHint hint) {
Map<String, Object> attributes = new LinkedHashMap<>();
handleCondition(attributes, hint);
attributes.put("name", hint.getBaseName());
attributes.put("bundle", hint.getBaseName());
return attributes;
}
@@ -28,8 +28,9 @@ import org.springframework.core.SpringVersion;
* GraalVM {@code native-image} compiler, typically named {@code reachability-metadata.json}.
*
* @author Brian Clozel
* @author Stephane Nicoll
* @since 7.0
* @see <a href="https://www.graalvm.org/jdk23/reference-manual/native-image/metadata/#specifying-metadata-with-json">GraalVM Reachability Metadata</a>
* @see <a href="https://www.graalvm.org/jdk25/reference-manual/native-image/metadata">GraalVM Reachability Metadata</a>
*/
class RuntimeHintsWriter {
@@ -51,14 +52,6 @@ class RuntimeHintsWriter {
if (!resourceHints.isEmpty()) {
document.put("resources", resourceHints);
}
List<Map<String, Object>> resourceBundles = new ResourceHintsAttributes().resourceBundles(hints.resources());
if (!resourceBundles.isEmpty()) {
document.put("bundles", resourceBundles);
}
List<Map<String, Object>> serialization = new SerializationHintsAttributes().toAttributes(hints.serialization());
if (!serialization.isEmpty()) {
document.put("serialization", serialization);
}
writer.writeObject(document);
}
@@ -1,63 +0,0 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aot.nativex;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.aot.hint.ConditionalHint;
import org.springframework.aot.hint.JavaSerializationHint;
import org.springframework.aot.hint.SerializationHints;
/**
* Collect {@link SerializationHints} as map attributes ready for JSON serialization for the GraalVM
* {@code native-image} compiler.
*
* @author Sebastien Deleuze
* @author Stephane Nicoll
* @author Brian Clozel
* @see <a href="https://www.graalvm.org/jdk23/reference-manual/native-image/overview/BuildConfiguration/">Native Image Build Configuration</a>
*/
class SerializationHintsAttributes {
private static final Comparator<JavaSerializationHint> JAVA_SERIALIZATION_HINT_COMPARATOR =
Comparator.comparing(JavaSerializationHint::getType);
public List<Map<String, Object>> toAttributes(SerializationHints hints) {
return hints.javaSerializationHints()
.sorted(JAVA_SERIALIZATION_HINT_COMPARATOR)
.map(this::toAttributes).toList();
}
private Map<String, Object> toAttributes(JavaSerializationHint serializationHint) {
LinkedHashMap<String, Object> attributes = new LinkedHashMap<>();
handleCondition(attributes, serializationHint);
attributes.put("type", serializationHint.getType());
return attributes;
}
private void handleCondition(Map<String, Object> attributes, ConditionalHint hint) {
if (hint.getReachableType() != null) {
Map<String, Object> conditionAttributes = new LinkedHashMap<>();
conditionAttributes.put("typeReached", hint.getReachableType());
attributes.put("condition", conditionAttributes);
}
}
}
@@ -137,14 +137,20 @@ final class SynthesizedMergedAnnotationInvocationHandler<A extends Annotation> i
String string = this.string;
if (string == null) {
StringBuilder builder = new StringBuilder("@").append(getName(this.type)).append('(');
for (int i = 0; i < this.attributes.size(); i++) {
Method attribute = this.attributes.get(i);
if (i > 0) {
builder.append(", ");
if (this.attributes.size() == 1 && this.attributes.get(0).getName().equals(MergedAnnotation.VALUE)) {
// Don't prepend "value=" for an annotation that only declares a "value" attribute.
builder.append(toString(getAttributeValue(this.attributes.get(0))));
}
else {
for (int i = 0; i < this.attributes.size(); i++) {
Method attribute = this.attributes.get(i);
if (i > 0) {
builder.append(", ");
}
builder.append(attribute.getName());
builder.append('=');
builder.append(toString(getAttributeValue(attribute)));
}
builder.append(attribute.getName());
builder.append('=');
builder.append(toString(getAttributeValue(attribute)));
}
builder.append(')');
string = builder.toString();
@@ -187,7 +193,7 @@ final class SynthesizedMergedAnnotationInvocationHandler<A extends Annotation> i
return '\'' + value.toString() + '\'';
}
if (type == Byte.class) {
return String.format("(byte) 0x%02X", value);
return String.format("(byte)0x%02x", value);
}
if (type == Long.class) {
return Long.toString((Long) value) + 'L';
@@ -196,7 +202,7 @@ final class SynthesizedMergedAnnotationInvocationHandler<A extends Annotation> i
return Float.toString((Float) value) + 'f';
}
if (type == Double.class) {
return Double.toString((Double) value) + 'd';
return Double.toString((Double) value);
}
if (value instanceof Enum<?> e) {
return e.name();
@@ -19,10 +19,12 @@ package org.springframework.core.task;
import java.io.Serializable;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicBoolean;
import org.jspecify.annotations.Nullable;
@@ -94,7 +96,9 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
private boolean rejectTasksWhenLimitReached = false;
private volatile boolean active = true;
private final AtomicBoolean closed = new AtomicBoolean();
private boolean cancelled = false; // within activeThreads synchronization
/**
@@ -269,7 +273,7 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
* @see #close()
*/
public boolean isActive() {
return this.active;
return !this.closed.get();
}
/**
@@ -307,14 +311,15 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
public void execute(Runnable task, long startTimeout) {
Assert.notNull(task, "Runnable must not be null");
if (!isActive()) {
throw new TaskRejectedException(getClass().getSimpleName() + " has been closed already");
throw new TaskRejectedException(getClass().getSimpleName() + " is not active");
}
Runnable taskToUse = (this.taskDecorator != null ? this.taskDecorator.decorate(task) : task);
Future<?> future = (task instanceof Future<?> f ? f : null);
if (isThrottleActive() && startTimeout > TIMEOUT_IMMEDIATE) {
this.concurrencyThrottle.beforeAccess();
try {
doExecute(new TaskTrackingRunnable(taskToUse));
doExecute(new TaskTrackingRunnable(taskToUse, future));
}
catch (Throwable ex) {
// Release concurrency permit if thread creation fails
@@ -324,7 +329,7 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
}
}
else if (this.activeThreads != null) {
doExecute(new TaskTrackingRunnable(taskToUse));
doExecute(new TaskTrackingRunnable(taskToUse, future));
}
else {
doExecute(taskToUse);
@@ -384,11 +389,13 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
*/
@Override
public void close() {
if (this.active) {
this.active = false;
if (this.closed.compareAndSet(false, true)) {
Set<Thread> threads = this.activeThreads;
if (threads != null) {
if (this.cancelRemainingTasksOnClose) {
synchronized (threads) {
this.cancelled = true;
}
// Early interrupt for remaining tasks on close
threads.forEach(Thread::interrupt);
}
@@ -402,6 +409,7 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
this.cancelled = true;
}
if (!this.cancelRemainingTasksOnClose) {
// Late interrupt for remaining tasks after timeout
@@ -412,6 +420,15 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
}
}
private void checkCancelled(@Nullable Future<?> future) {
if (this.cancelled) { // within synchronization from TaskTrackingRunnable
if (future != null) {
future.cancel(false);
}
throw new CancellationException(getClass().getSimpleName() + " has cancelled all remaining tasks");
}
}
/**
* Subclass of the general ConcurrencyThrottleSupport class,
@@ -453,9 +470,12 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
private final Runnable task;
public TaskTrackingRunnable(Runnable task) {
private final @Nullable Future<?> future;
public TaskTrackingRunnable(Runnable task, @Nullable Future<?> future) {
Assert.notNull(task, "Task must not be null");
this.task = task;
this.future = future;
}
@Override
@@ -464,7 +484,10 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
Thread thread = null;
if (threads != null) {
thread = Thread.currentThread();
threads.add(thread);
synchronized (threads) {
checkCancelled(this.future);
threads.add(thread);
}
}
try {
this.task.run();
@@ -472,7 +495,7 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
finally {
if (threads != null) {
threads.remove(thread);
if (!isActive()) {
if (closed.get()) {
synchronized (threads) {
if (threads.isEmpty()) {
threads.notify();
@@ -117,6 +117,7 @@ class MergedAnnotationReadingVisitor<A extends Annotation> extends AnnotationVis
return new MergedAnnotationReadingVisitor<>(this.classLoader, this.source, type, consumer);
}
@SuppressWarnings("unchecked")
static <A extends Annotation> @Nullable AnnotationVisitor get(@Nullable ClassLoader classLoader,
@Nullable Object source, String descriptor, boolean visible,
@@ -32,7 +32,6 @@ import org.springframework.core.io.ResourceLoader;
*/
public class SimpleMetadataReaderFactory extends AbstractMetadataReaderFactory {
/**
* Create a new SimpleMetadataReaderFactory for the default class loader.
*/
@@ -57,6 +56,7 @@ public class SimpleMetadataReaderFactory extends AbstractMetadataReaderFactory {
super(classLoader);
}
@Override
public MetadataReader getMetadataReader(Resource resource) throws IOException {
return new SimpleMetadataReader(resource, getResourceLoader().getClassLoader());
@@ -43,7 +43,7 @@ import java.util.Properties;
* should already apply proper decoding/encoding of characters. If you prefer
* to escape unicode characters in your properties files, do <i>not</i> specify
* an encoding for a Reader/Writer (like ReloadableResourceBundleMessageSource's
* "defaultEncoding" and "fileEncodings" properties).
* "defaultCharset" and "fileEncodings" properties).
*
* @author Juergen Hoeller
* @author Sebastien Deleuze
@@ -24,7 +24,6 @@ import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.TreeSet;
@@ -695,10 +694,4 @@ public class MimeType implements Comparable<MimeType>, Serializable {
return MimeTypeUtils.parseMimeType(value);
}
private static Map<String, String> addCharsetParameter(Charset charset, Map<String, String> parameters) {
Map<String, String> map = new LinkedHashMap<>(parameters);
map.put(PARAM_CHARSET, charset.name());
return map;
}
}
@@ -221,9 +221,13 @@ public abstract class StreamUtils {
* Return a variant of the given {@link InputStream} where calling
* {@link InputStream#close() close()} has no effect.
* @param in the InputStream to decorate
* @return a version of the InputStream that ignores calls to close
* @return a version of the InputStream that ignores calls to close,
* or the given InputStream if it is non-closing already
*/
public static InputStream nonClosing(InputStream in) {
if (in instanceof NonClosingInputStream) {
return in;
}
Assert.notNull(in, "No InputStream specified");
return new NonClosingInputStream(in);
}
@@ -232,15 +236,38 @@ public abstract class StreamUtils {
* Return a variant of the given {@link OutputStream} where calling
* {@link OutputStream#close() close()} has no effect.
* @param out the OutputStream to decorate
* @return a version of the OutputStream that ignores calls to close
* @return a version of the OutputStream that ignores calls to close,
* or the given OutputStream if it is non-closing already
* @see #nonFlushing(OutputStream)
*/
public static OutputStream nonClosing(OutputStream out) {
if (out instanceof NonClosingOutputStream) {
return out;
}
Assert.notNull(out, "No OutputStream specified");
return new NonClosingOutputStream(out);
}
/**
* Return a variant of the given {@link OutputStream} where calling
* {@link OutputStream#flush() flush()} and/or
* {@link OutputStream#close() close()} has no effect.
* @param out the OutputStream to decorate
* @return a version of the OutputStream that ignores calls to flush/close,
* or the given OutputStream if it is non-flushing already
* @since 7.0.6
* @see #nonClosing(OutputStream)
*/
public static OutputStream nonFlushing(OutputStream out) {
if (out instanceof NonFlushingOutputStream) {
return out;
}
Assert.notNull(out, "No OutputStream specified");
return new NonFlushingOutputStream(out);
}
private static final class NonClosingInputStream extends FilterInputStream {
private static class NonClosingInputStream extends FilterInputStream {
public NonClosingInputStream(InputStream in) {
super(in);
@@ -272,7 +299,7 @@ public abstract class StreamUtils {
}
private static final class NonClosingOutputStream extends FilterOutputStream {
private static class NonClosingOutputStream extends FilterOutputStream {
public NonClosingOutputStream(OutputStream out) {
super(out);
@@ -289,4 +316,16 @@ public abstract class StreamUtils {
}
}
private static class NonFlushingOutputStream extends NonClosingOutputStream {
public NonFlushingOutputStream(OutputStream out) {
super(out);
}
@Override
public void flush() throws IOException {
}
}
}
@@ -0,0 +1,177 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.type.classreading;
import java.lang.classfile.Annotation;
import java.lang.classfile.AnnotationElement;
import java.lang.classfile.AnnotationValue;
import java.lang.classfile.attribute.RuntimeVisibleAnnotationsAttribute;
import java.lang.constant.ClassDesc;
import java.lang.reflect.Array;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jspecify.annotations.Nullable;
import org.springframework.core.annotation.AnnotationFilter;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.util.ClassUtils;
/**
* Parse {@link RuntimeVisibleAnnotationsAttribute} into {@link MergedAnnotations}
* instances.
*
* @author Brian Clozel
* @author Juergen Hoeller
* @since 7.0
*/
abstract class ClassFileAnnotationDelegate {
static MergedAnnotations createMergedAnnotations(
String className, RuntimeVisibleAnnotationsAttribute annotationAttribute, @Nullable ClassLoader classLoader) {
Set<MergedAnnotation<?>> annotations = annotationAttribute.annotations()
.stream()
.map(ann -> createMergedAnnotation(className, ann, classLoader))
.filter(Objects::nonNull)
.collect(Collectors.toSet());
return MergedAnnotations.of(annotations);
}
@SuppressWarnings("unchecked")
private static <A extends java.lang.annotation.Annotation> @Nullable MergedAnnotation<A> createMergedAnnotation(
String className, Annotation annotation, @Nullable ClassLoader classLoader) {
String typeName = fromTypeDescriptor(annotation.className().stringValue());
if (AnnotationFilter.PLAIN.matches(typeName)) {
return null;
}
try {
// Fail early when annotation type is not loadable (before resolving annotation values)
Class<A> annotationType = (Class<A>) ClassUtils.forName(typeName, classLoader);
Map<String, Object> attributes = new LinkedHashMap<>(4);
for (AnnotationElement element : annotation.elements()) {
Object annotationValue = readAnnotationValue(className, element.value(), classLoader);
if (annotationValue != null) {
attributes.put(element.name().stringValue(), annotationValue);
}
}
Map<String, Object> compactedAttributes = (attributes.isEmpty() ? Collections.emptyMap() : attributes);
return MergedAnnotation.of(classLoader, new Source(annotation), annotationType, compactedAttributes);
}
catch (ClassNotFoundException | LinkageError ex) {
// Non-loadable annotation type -> ignore.
return null;
}
}
private static @Nullable Object readAnnotationValue(
String className, AnnotationValue elementValue, @Nullable ClassLoader classLoader) {
switch (elementValue) {
case AnnotationValue.OfConstant constantValue -> {
return constantValue.resolvedValue();
}
case AnnotationValue.OfAnnotation annotationValue -> {
return createMergedAnnotation(className, annotationValue.annotation(), classLoader);
}
case AnnotationValue.OfClass classValue -> {
return fromTypeDescriptor(classValue.className().stringValue());
}
case AnnotationValue.OfEnum enumValue -> {
return parseEnum(enumValue, classLoader);
}
case AnnotationValue.OfArray arrayValue -> {
return parseArrayValue(className, classLoader, arrayValue);
}
}
}
private static String fromTypeDescriptor(String descriptor) {
ClassDesc classDesc = ClassDesc.ofDescriptor(descriptor);
return (classDesc.isPrimitive() ? classDesc.displayName() :
classDesc.packageName() + "." + classDesc.displayName());
}
private static Object parseArrayValue(String className, @Nullable ClassLoader classLoader, AnnotationValue.OfArray arrayValue) {
if (arrayValue.values().isEmpty()) {
return new Object[0];
}
Stream<AnnotationValue> stream = arrayValue.values().stream();
switch (arrayValue.values().getFirst()) {
case AnnotationValue.OfInt _ -> {
return stream.map(AnnotationValue.OfInt.class::cast).mapToInt(AnnotationValue.OfInt::intValue).toArray();
}
case AnnotationValue.OfDouble _ -> {
return stream.map(AnnotationValue.OfDouble.class::cast).mapToDouble(AnnotationValue.OfDouble::doubleValue).toArray();
}
case AnnotationValue.OfLong _ -> {
return stream.map(AnnotationValue.OfLong.class::cast).mapToLong(AnnotationValue.OfLong::longValue).toArray();
}
default -> {
Class<?> arrayElementType = resolveArrayElementType(arrayValue.values(), classLoader);
return stream
.map(rawValue -> readAnnotationValue(className, rawValue, classLoader))
.toArray(length -> (Object[]) Array.newInstance(arrayElementType, length));
}
}
}
@SuppressWarnings("unchecked")
private static <E extends Enum<E>> Enum<E> parseEnum(AnnotationValue.OfEnum enumValue, @Nullable ClassLoader classLoader) {
Class<E> enumClass = (Class<E>) loadEnumClass(enumValue, classLoader);
return Enum.valueOf(enumClass, enumValue.constantName().stringValue());
}
private static Class<?> loadEnumClass(AnnotationValue.OfEnum enumValue, @Nullable ClassLoader classLoader) {
String className = fromTypeDescriptor(enumValue.className().stringValue());
return ClassUtils.resolveClassName(className, classLoader);
}
private static Class<?> resolveArrayElementType(List<AnnotationValue> values, @Nullable ClassLoader classLoader) {
AnnotationValue firstValue = values.getFirst();
switch (firstValue) {
case AnnotationValue.OfConstant constantValue -> {
return constantValue.resolvedValue().getClass();
}
case AnnotationValue.OfAnnotation _ -> {
return MergedAnnotation.class;
}
case AnnotationValue.OfClass _ -> {
return String.class;
}
case AnnotationValue.OfEnum enumValue -> {
return loadEnumClass(enumValue, classLoader);
}
default -> {
return Object.class;
}
}
}
record Source(Annotation annotation) {
}
}
@@ -16,165 +16,305 @@
package org.springframework.core.type.classreading;
import java.lang.classfile.Annotation;
import java.lang.classfile.AnnotationElement;
import java.lang.classfile.AnnotationValue;
import java.lang.classfile.AccessFlags;
import java.lang.classfile.ClassModel;
import java.lang.classfile.Interfaces;
import java.lang.classfile.MethodModel;
import java.lang.classfile.Superclass;
import java.lang.classfile.attribute.InnerClassInfo;
import java.lang.classfile.attribute.InnerClassesAttribute;
import java.lang.classfile.attribute.NestHostAttribute;
import java.lang.classfile.attribute.RuntimeVisibleAnnotationsAttribute;
import java.lang.constant.ClassDesc;
import java.lang.reflect.Array;
import java.lang.classfile.constantpool.ClassEntry;
import java.lang.reflect.AccessFlag;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jspecify.annotations.Nullable;
import org.springframework.core.annotation.AnnotationFilter;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.MethodMetadata;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* Parse {@link RuntimeVisibleAnnotationsAttribute} into {@link MergedAnnotations}
* instances.
* {@link AnnotationMetadata} implementation that leverages
* the {@link java.lang.classfile.ClassFile} API.
*
* @author Brian Clozel
* @author Juergen Hoeller
* @since 7.0
*/
abstract class ClassFileAnnotationMetadata {
final class ClassFileAnnotationMetadata implements AnnotationMetadata {
static MergedAnnotations createMergedAnnotations(
String className, RuntimeVisibleAnnotationsAttribute annotationAttribute, @Nullable ClassLoader classLoader) {
private final String className;
Set<MergedAnnotation<?>> annotations = annotationAttribute.annotations()
.stream()
.map(ann -> createMergedAnnotation(className, ann, classLoader))
.filter(Objects::nonNull)
.collect(Collectors.toSet());
return MergedAnnotations.of(annotations);
private final AccessFlags accessFlags;
private final @Nullable String enclosingClassName;
private final @Nullable String superClassName;
private final boolean independentInnerClass;
private final Set<String> interfaceNames;
private final Set<String> memberClassNames;
private final Set<MethodMetadata> declaredMethods;
private final MergedAnnotations mergedAnnotations;
private @Nullable Set<String> annotationTypes;
ClassFileAnnotationMetadata(String className, AccessFlags accessFlags, @Nullable String enclosingClassName,
@Nullable String superClassName, boolean independentInnerClass, Set<String> interfaceNames,
Set<String> memberClassNames, Set<MethodMetadata> declaredMethods, MergedAnnotations mergedAnnotations) {
this.className = className;
this.accessFlags = accessFlags;
this.enclosingClassName = enclosingClassName;
this.superClassName = (!className.endsWith(".package-info")) ? superClassName : null;
this.independentInnerClass = independentInnerClass;
this.interfaceNames = interfaceNames;
this.memberClassNames = memberClassNames;
this.declaredMethods = declaredMethods;
this.mergedAnnotations = mergedAnnotations;
}
@SuppressWarnings("unchecked")
private static <A extends java.lang.annotation.Annotation> @Nullable MergedAnnotation<A> createMergedAnnotation(
String className, Annotation annotation, @Nullable ClassLoader classLoader) {
String typeName = fromTypeDescriptor(annotation.className().stringValue());
if (AnnotationFilter.PLAIN.matches(typeName)) {
return null;
@Override
public String getClassName() {
return this.className;
}
@Override
public boolean isInterface() {
return this.accessFlags.has(AccessFlag.INTERFACE);
}
@Override
public boolean isAnnotation() {
return this.accessFlags.has(AccessFlag.ANNOTATION);
}
@Override
public boolean isAbstract() {
return this.accessFlags.has(AccessFlag.ABSTRACT);
}
@Override
public boolean isFinal() {
return this.accessFlags.has(AccessFlag.FINAL);
}
@Override
public boolean isIndependent() {
return (this.enclosingClassName == null || this.independentInnerClass);
}
@Override
public @Nullable String getEnclosingClassName() {
return this.enclosingClassName;
}
@Override
public @Nullable String getSuperClassName() {
return this.superClassName;
}
@Override
public String[] getInterfaceNames() {
return StringUtils.toStringArray(this.interfaceNames);
}
@Override
public String[] getMemberClassNames() {
return StringUtils.toStringArray(this.memberClassNames);
}
@Override
public MergedAnnotations getAnnotations() {
return this.mergedAnnotations;
}
@Override
public Set<String> getAnnotationTypes() {
Set<String> annotationTypes = this.annotationTypes;
if (annotationTypes == null) {
annotationTypes = Collections.unmodifiableSet(AnnotationMetadata.super.getAnnotationTypes());
this.annotationTypes = annotationTypes;
}
Map<String, Object> attributes = new LinkedHashMap<>(4);
try {
for (AnnotationElement element : annotation.elements()) {
Object annotationValue = readAnnotationValue(className, element.value(), classLoader);
if (annotationValue != null) {
attributes.put(element.name().stringValue(), annotationValue);
return annotationTypes;
}
@Override
public Set<MethodMetadata> getAnnotatedMethods(String annotationName) {
Set<MethodMetadata> result = new LinkedHashSet<>(4);
for (MethodMetadata annotatedMethod : this.declaredMethods) {
if (annotatedMethod.isAnnotated(annotationName)) {
result.add(annotatedMethod);
}
}
return Collections.unmodifiableSet(result);
}
@Override
public Set<MethodMetadata> getDeclaredMethods() {
return Collections.unmodifiableSet(this.declaredMethods);
}
@Override
public boolean equals(@Nullable Object other) {
return (this == other || (other instanceof ClassFileAnnotationMetadata that &&
this.className.equals(that.className)));
}
@Override
public int hashCode() {
return this.className.hashCode();
}
@Override
public String toString() {
return this.className;
}
static ClassFileAnnotationMetadata of(ClassModel classModel, @Nullable ClassLoader classLoader) {
Builder builder = new Builder(classLoader);
builder.classEntry(classModel.thisClass());
String currentClassName = classModel.thisClass().name().stringValue();
classModel.elementStream().forEach(classElement -> {
switch (classElement) {
case AccessFlags flags -> {
builder.accessFlags(flags);
}
case NestHostAttribute _ -> {
builder.enclosingClassFromNestHost(classModel.thisClass());
}
case InnerClassesAttribute innerClasses -> {
builder.nestMembers(currentClassName, innerClasses);
}
case RuntimeVisibleAnnotationsAttribute annotationsAttribute -> {
builder.mergedAnnotations(ClassFileAnnotationDelegate.createMergedAnnotations(
ClassUtils.convertResourcePathToClassName(currentClassName), annotationsAttribute, classLoader));
}
case Superclass superclass -> {
builder.superClass(superclass);
}
case Interfaces interfaces -> {
builder.interfaces(interfaces);
}
case MethodModel method -> {
builder.method(method);
}
default -> {
// ignore class element
}
}
Map<String, Object> compactedAttributes = (attributes.isEmpty() ? Collections.emptyMap() : attributes);
Class<A> annotationType = (Class<A>) ClassUtils.forName(typeName, classLoader);
return MergedAnnotation.of(classLoader, new Source(annotation), annotationType, compactedAttributes);
}
catch (ClassNotFoundException | LinkageError ex) {
return null;
}
}
private static @Nullable Object readAnnotationValue(
String className, AnnotationValue elementValue, @Nullable ClassLoader classLoader) {
switch (elementValue) {
case AnnotationValue.OfConstant constantValue -> {
return constantValue.resolvedValue();
}
case AnnotationValue.OfAnnotation annotationValue -> {
return createMergedAnnotation(className, annotationValue.annotation(), classLoader);
}
case AnnotationValue.OfClass classValue -> {
return fromTypeDescriptor(classValue.className().stringValue());
}
case AnnotationValue.OfEnum enumValue -> {
return parseEnum(enumValue, classLoader);
}
case AnnotationValue.OfArray arrayValue -> {
return parseArrayValue(className, classLoader, arrayValue);
}
}
}
private static String fromTypeDescriptor(String descriptor) {
ClassDesc classDesc = ClassDesc.ofDescriptor(descriptor);
return classDesc.isPrimitive() ? classDesc.displayName() :
classDesc.packageName() + "." + classDesc.displayName();
}
private static Class<?> loadClass(String className, @Nullable ClassLoader classLoader) {
String name = fromTypeDescriptor(className);
return ClassUtils.resolveClassName(name, classLoader);
}
private static Object parseArrayValue(String className, @Nullable ClassLoader classLoader, AnnotationValue.OfArray arrayValue) {
if (arrayValue.values().isEmpty()) {
return new Object[0];
}
Stream<AnnotationValue> stream = arrayValue.values().stream();
switch (arrayValue.values().getFirst()) {
case AnnotationValue.OfInt _ -> {
return stream.map(AnnotationValue.OfInt.class::cast).mapToInt(AnnotationValue.OfInt::intValue).toArray();
}
case AnnotationValue.OfDouble _ -> {
return stream.map(AnnotationValue.OfDouble.class::cast).mapToDouble(AnnotationValue.OfDouble::doubleValue).toArray();
}
case AnnotationValue.OfLong _ -> {
return stream.map(AnnotationValue.OfLong.class::cast).mapToLong(AnnotationValue.OfLong::longValue).toArray();
}
default -> {
Class<?> arrayElementType = resolveArrayElementType(arrayValue.values(), classLoader);
return stream
.map(rawValue -> readAnnotationValue(className, rawValue, classLoader))
.toArray(s -> (Object[]) Array.newInstance(arrayElementType, s));
}
}
}
@SuppressWarnings("unchecked")
private static @Nullable <E extends Enum<E>> Enum<E> parseEnum(AnnotationValue.OfEnum enumValue, @Nullable ClassLoader classLoader) {
String enumClassName = fromTypeDescriptor(enumValue.className().stringValue());
try {
Class<E> enumClass = (Class<E>) ClassUtils.forName(enumClassName, classLoader);
return Enum.valueOf(enumClass, enumValue.constantName().stringValue());
}
catch (ClassNotFoundException | LinkageError ex) {
return null;
}
}
private static Class<?> resolveArrayElementType(List<AnnotationValue> values, @Nullable ClassLoader classLoader) {
AnnotationValue firstValue = values.getFirst();
switch (firstValue) {
case AnnotationValue.OfConstant constantValue -> {
return constantValue.resolvedValue().getClass();
}
case AnnotationValue.OfAnnotation _ -> {
return MergedAnnotation.class;
}
case AnnotationValue.OfClass _ -> {
return String.class;
}
case AnnotationValue.OfEnum enumValue -> {
return loadClass(enumValue.className().stringValue(), classLoader);
}
default -> {
return Object.class;
}
}
});
return builder.build();
}
record Source(Annotation entryName) {
static class Builder {
private final ClassLoader classLoader;
private String className;
private AccessFlags accessFlags;
private Set<AccessFlag> innerAccessFlags;
private @Nullable String enclosingClassName;
private @Nullable String superClassName;
private Set<String> interfaceNames = new LinkedHashSet<>(4);
private Set<String> memberClassNames = new LinkedHashSet<>(4);
private Set<MethodMetadata> declaredMethods = new LinkedHashSet<>(4);
private MergedAnnotations mergedAnnotations = MergedAnnotations.of(Collections.emptySet());
public Builder(ClassLoader classLoader) {
this.classLoader = classLoader;
}
void classEntry(ClassEntry classEntry) {
this.className = ClassUtils.convertResourcePathToClassName(classEntry.name().stringValue());
}
void accessFlags(AccessFlags accessFlags) {
this.accessFlags = accessFlags;
}
void enclosingClassFromNestHost(ClassEntry thisClass) {
if (this.enclosingClassName != null) {
return;
}
String thisClassName = thisClass.name().stringValue();
int currentClassIndex = thisClassName.lastIndexOf('$');
if (currentClassIndex > 0) {
this.enclosingClassName = ClassUtils.convertResourcePathToClassName(
thisClassName.substring(0, currentClassIndex));
}
}
void superClass(Superclass superClass) {
this.superClassName = ClassUtils.convertResourcePathToClassName(
superClass.superclassEntry().name().stringValue());
}
void interfaces(Interfaces interfaces) {
for (ClassEntry entry : interfaces.interfaces()) {
this.interfaceNames.add(ClassUtils.convertResourcePathToClassName(entry.name().stringValue()));
}
}
void nestMembers(String currentClassName, InnerClassesAttribute innerClasses) {
for (InnerClassInfo classInfo : innerClasses.classes()) {
String innerClassName = classInfo.innerClass().name().stringValue();
if (currentClassName.equals(innerClassName)) {
// the current class is an inner class
this.innerAccessFlags = classInfo.flags();
classInfo.outerClass().ifPresent(outerClass ->
this.enclosingClassName = ClassUtils.convertResourcePathToClassName(outerClass.name().stringValue()));
}
classInfo.outerClass().ifPresent(outerClass -> {
if (outerClass.name().stringValue().equals(currentClassName)) {
// collecting data about actual inner classes
this.memberClassNames.add(ClassUtils.convertResourcePathToClassName(innerClassName));
}
});
}
}
void mergedAnnotations(MergedAnnotations mergedAnnotations) {
this.mergedAnnotations = mergedAnnotations;
}
void method(MethodModel method) {
ClassFileMethodMetadata classFileMethodMetadata = ClassFileMethodMetadata.of(method, this.classLoader);
if (!classFileMethodMetadata.isSynthetic() && !classFileMethodMetadata.isDefaultConstructor()) {
this.declaredMethods.add(classFileMethodMetadata);
}
}
ClassFileAnnotationMetadata build() {
boolean independentInnerClass = (this.enclosingClassName != null) &&
this.innerAccessFlags.contains(AccessFlag.STATIC);
return new ClassFileAnnotationMetadata(this.className, this.accessFlags, this.enclosingClassName,
this.superClassName, independentInnerClass, this.interfaceNames, this.memberClassNames,
this.declaredMethods, this.mergedAnnotations);
}
}
}
@@ -1,308 +0,0 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.type.classreading;
import java.lang.classfile.AccessFlags;
import java.lang.classfile.ClassModel;
import java.lang.classfile.Interfaces;
import java.lang.classfile.MethodModel;
import java.lang.classfile.Superclass;
import java.lang.classfile.attribute.InnerClassInfo;
import java.lang.classfile.attribute.InnerClassesAttribute;
import java.lang.classfile.attribute.NestHostAttribute;
import java.lang.classfile.attribute.RuntimeVisibleAnnotationsAttribute;
import java.lang.classfile.constantpool.ClassEntry;
import java.lang.reflect.AccessFlag;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.MethodMetadata;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* {@link AnnotationMetadata} implementation that leverages
* the {@link java.lang.classfile.ClassFile} API.
*
* @author Brian Clozel
* @since 7.0
*/
class ClassFileClassMetadata implements AnnotationMetadata {
private final String className;
private final AccessFlags accessFlags;
private final @Nullable String enclosingClassName;
private final @Nullable String superClassName;
private final boolean independentInnerClass;
private final Set<String> interfaceNames;
private final Set<String> memberClassNames;
private final Set<MethodMetadata> declaredMethods;
private final MergedAnnotations mergedAnnotations;
private @Nullable Set<String> annotationTypes;
ClassFileClassMetadata(String className, AccessFlags accessFlags, @Nullable String enclosingClassName,
@Nullable String superClassName, boolean independentInnerClass, Set<String> interfaceNames,
Set<String> memberClassNames, Set<MethodMetadata> declaredMethods, MergedAnnotations mergedAnnotations) {
this.className = className;
this.accessFlags = accessFlags;
this.enclosingClassName = enclosingClassName;
this.superClassName = (!className.endsWith(".package-info")) ? superClassName : null;
this.independentInnerClass = independentInnerClass;
this.interfaceNames = interfaceNames;
this.memberClassNames = memberClassNames;
this.declaredMethods = declaredMethods;
this.mergedAnnotations = mergedAnnotations;
}
@Override
public String getClassName() {
return this.className;
}
@Override
public boolean isInterface() {
return this.accessFlags.has(AccessFlag.INTERFACE);
}
@Override
public boolean isAnnotation() {
return this.accessFlags.has(AccessFlag.ANNOTATION);
}
@Override
public boolean isAbstract() {
return this.accessFlags.has(AccessFlag.ABSTRACT);
}
@Override
public boolean isFinal() {
return this.accessFlags.has(AccessFlag.FINAL);
}
@Override
public boolean isIndependent() {
return (this.enclosingClassName == null || this.independentInnerClass);
}
@Override
public @Nullable String getEnclosingClassName() {
return this.enclosingClassName;
}
@Override
public @Nullable String getSuperClassName() {
return this.superClassName;
}
@Override
public String[] getInterfaceNames() {
return StringUtils.toStringArray(this.interfaceNames);
}
@Override
public String[] getMemberClassNames() {
return StringUtils.toStringArray(this.memberClassNames);
}
@Override
public MergedAnnotations getAnnotations() {
return this.mergedAnnotations;
}
@Override
public Set<String> getAnnotationTypes() {
Set<String> annotationTypes = this.annotationTypes;
if (annotationTypes == null) {
annotationTypes = Collections.unmodifiableSet(
AnnotationMetadata.super.getAnnotationTypes());
this.annotationTypes = annotationTypes;
}
return annotationTypes;
}
@Override
public Set<MethodMetadata> getAnnotatedMethods(String annotationName) {
Set<MethodMetadata> result = new LinkedHashSet<>(4);
for (MethodMetadata annotatedMethod : this.declaredMethods) {
if (annotatedMethod.isAnnotated(annotationName)) {
result.add(annotatedMethod);
}
}
return Collections.unmodifiableSet(result);
}
@Override
public Set<MethodMetadata> getDeclaredMethods() {
return Collections.unmodifiableSet(this.declaredMethods);
}
@Override
public boolean equals(@Nullable Object other) {
return (this == other || (other instanceof ClassFileClassMetadata that && this.className.equals(that.className)));
}
@Override
public int hashCode() {
return this.className.hashCode();
}
@Override
public String toString() {
return this.className;
}
static ClassFileClassMetadata of(ClassModel classModel, @Nullable ClassLoader classLoader) {
Builder builder = new Builder(classLoader);
builder.classEntry(classModel.thisClass());
String currentClassName = classModel.thisClass().name().stringValue();
classModel.elementStream().forEach(classElement -> {
switch (classElement) {
case AccessFlags flags -> {
builder.accessFlags(flags);
}
case NestHostAttribute _ -> {
builder.enclosingClass(classModel.thisClass());
}
case InnerClassesAttribute innerClasses -> {
builder.nestMembers(currentClassName, innerClasses);
}
case RuntimeVisibleAnnotationsAttribute annotationsAttribute -> {
builder.mergedAnnotations(ClassFileAnnotationMetadata.createMergedAnnotations(
ClassUtils.convertResourcePathToClassName(currentClassName), annotationsAttribute, classLoader));
}
case Superclass superclass -> {
builder.superClass(superclass);
}
case Interfaces interfaces -> {
builder.interfaces(interfaces);
}
case MethodModel method -> {
builder.method(method);
}
default -> {
// ignore class element
}
}
});
return builder.build();
}
static class Builder {
private final ClassLoader clasLoader;
private String className;
private AccessFlags accessFlags;
private Set<AccessFlag> innerAccessFlags;
private @Nullable String enclosingClassName;
private @Nullable String superClassName;
private Set<String> interfaceNames = new LinkedHashSet<>(4);
private Set<String> memberClassNames = new LinkedHashSet<>(4);
private Set<MethodMetadata> declaredMethods = new LinkedHashSet<>(4);
private MergedAnnotations mergedAnnotations = MergedAnnotations.of(Collections.emptySet());
public Builder(ClassLoader classLoader) {
this.clasLoader = classLoader;
}
void classEntry(ClassEntry classEntry) {
this.className = ClassUtils.convertResourcePathToClassName(classEntry.name().stringValue());
}
void accessFlags(AccessFlags accessFlags) {
this.accessFlags = accessFlags;
}
void enclosingClass(ClassEntry thisClass) {
String thisClassName = thisClass.name().stringValue();
int currentClassIndex = thisClassName.lastIndexOf('$');
this.enclosingClassName = ClassUtils.convertResourcePathToClassName(thisClassName.substring(0, currentClassIndex));
}
void superClass(Superclass superClass) {
this.superClassName = ClassUtils.convertResourcePathToClassName(superClass.superclassEntry().name().stringValue());
}
void interfaces(Interfaces interfaces) {
for (ClassEntry entry : interfaces.interfaces()) {
this.interfaceNames.add(ClassUtils.convertResourcePathToClassName(entry.name().stringValue()));
}
}
void nestMembers(String currentClassName, InnerClassesAttribute innerClasses) {
for (InnerClassInfo classInfo : innerClasses.classes()) {
String innerClassName = classInfo.innerClass().name().stringValue();
if (currentClassName.equals(innerClassName)) {
// the current class is an inner class
this.innerAccessFlags = classInfo.flags();
}
classInfo.outerClass().ifPresent(outerClass -> {
if (outerClass.name().stringValue().equals(currentClassName)) {
// collecting data about actual inner classes
this.memberClassNames.add(ClassUtils.convertResourcePathToClassName(innerClassName));
}
});
}
}
void mergedAnnotations(MergedAnnotations mergedAnnotations) {
this.mergedAnnotations = mergedAnnotations;
}
void method(MethodModel method) {
ClassFileMethodMetadata classFileMethodMetadata = ClassFileMethodMetadata.of(method, this.clasLoader);
if (!classFileMethodMetadata.isSynthetic() && !classFileMethodMetadata.isDefaultConstructor()) {
this.declaredMethods.add(classFileMethodMetadata);
}
}
ClassFileClassMetadata build() {
boolean independentInnerClass = (this.enclosingClassName != null) && this.innerAccessFlags.contains(AccessFlag.STATIC);
return new ClassFileClassMetadata(this.className, this.accessFlags, this.enclosingClassName, this.superClassName,
independentInnerClass, this.interfaceNames, this.memberClassNames, this.declaredMethods, this.mergedAnnotations);
}
}
}
@@ -42,13 +42,12 @@ final class ClassFileMetadataReader implements MetadataReader {
ClassFileMetadataReader(Resource resource, @Nullable ClassLoader classLoader) throws IOException {
this.resource = resource;
this.annotationMetadata = ClassFileClassMetadata.of(parseClassModel(resource), classLoader);
this.annotationMetadata = ClassFileAnnotationMetadata.of(parseClassModel(resource), classLoader);
}
private static ClassModel parseClassModel(Resource resource) throws IOException {
try (InputStream is = resource.getInputStream()) {
byte[] bytes = is.readAllBytes();
return ClassFile.of().parse(bytes);
try (InputStream inputStream = resource.getInputStream()) {
return ClassFile.of().parse(inputStream.readAllBytes());
}
}
@@ -30,7 +30,7 @@ import org.springframework.core.io.ResourceLoader;
* @author Brian Clozel
* @since 7.0
*/
class ClassFileMetadataReaderFactory extends AbstractMetadataReaderFactory {
final class ClassFileMetadataReaderFactory extends AbstractMetadataReaderFactory {
/**
* Create a new ClassFileMetadataReaderFactory for the default class loader.
@@ -41,7 +41,7 @@ import org.springframework.util.ClassUtils;
* @author Brian Clozel
* @since 7.0
*/
class ClassFileMethodMetadata implements MethodMetadata {
final class ClassFileMethodMetadata implements MethodMetadata {
private final String methodName;
@@ -148,7 +148,7 @@ class ClassFileMethodMetadata implements MethodMetadata {
MergedAnnotations annotations = methodModel.elementStream()
.filter(element -> element instanceof RuntimeVisibleAnnotationsAttribute)
.findFirst()
.map(element -> ClassFileAnnotationMetadata.createMergedAnnotations(methodName, (RuntimeVisibleAnnotationsAttribute) element, classLoader))
.map(element -> ClassFileAnnotationDelegate.createMergedAnnotations(methodName, (RuntimeVisibleAnnotationsAttribute) element, classLoader))
.orElse(MergedAnnotations.of(Collections.emptyList()));
return new ClassFileMethodMetadata(methodName, flags, declaringClassName, returnTypeName, source, annotations);
}
@@ -25,5 +25,7 @@ import java.io.Serializable
* @author Sebastien Deleuze
* @since 6.0.5
*/
@Suppress("removal", "DEPRECATION")
@Deprecated(level = DeprecationLevel.WARNING, message = "Use ReflectionHints")
inline fun <reified T : Serializable> SerializationHints.registerType(noinline serializationHint: (JavaSerializationHint.Builder) -> Unit = {}) =
registerType(T::class.java, serializationHint::invoke)

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