Compare commits

...

278 Commits

Author SHA1 Message Date
Brian Clozel 006c0bc418 Release v7.0.4 2026-02-12 10:06:55 +01:00
Juergen Hoeller 4734837fe4 Upgrade to Netty 4.2.10, Hibernate ORM 7.2.4, Checkstyle 13.2 2026-02-11 22:41:52 +01:00
Juergen Hoeller 4a13655aa5 Polishing 2026-02-11 19:22:27 +01:00
Brian Clozel 5ef9972528 Allow to disable defaults in HttpMessageConverters
The builder for `HttpMessageConverters` allows for auto-detection of
message converters on the classpath and their default registration when
`registerDefaults()` is called. Once called, there is no way to undo
this.

This commit adds a new `disableDefaults()` method to disable the default
registration and take full control over the list of message converters.

Closes gh-36303
2026-02-11 18:37:12 +01:00
Christian Schuster a9b1d6335e avoid unnecessary locking in ConcurrentReferenceHashMap's implementation of computeIfAbsent and computeIfPresent
Signed-off-by: Christian Schuster <christian@dnup.de>
2026-02-11 17:53:26 +01:00
Sam Brannen 98d4046e4b Document that @⁠Validated & @⁠Lazy may be used as meta-annotations
See gh-36305
See gh-36306
2026-02-11 17:37:06 +01:00
Sam Brannen 282fee5c95 Consistently find @⁠Lazy as a meta-annotation at arbitrary depths
Prior to this commit, AnnotationConfigUtils looked up @⁠Lazy as a
meta-annotation at arbitrary depths (e.g., when used as a
meta-meta-annotation); however,
ContextAnnotationAutowireCandidateResolver only found @Lazy as a
"directly present" meta-annotation.

For consistency, this commit revises
ContextAnnotationAutowireCandidateResolver so that it also finds @⁠Lazy
as a meta-annotation at arbitrary depths.

Closes gh-36306
2026-02-11 17:33:15 +01:00
Sam Brannen 0d4737aeec Polishing 2026-02-11 17:26:15 +01:00
Juergen Hoeller 1ef8734881 Cache method annotations in AnnotatedMethod as well
See gh-36307
2026-02-11 17:23:05 +01:00
Juergen Hoeller 6dfa54b8ea Cache method annotations in MethodParameter
Closes gh-36307
2026-02-11 16:41:55 +01:00
Sam Brannen 2d3a2c5cbd Consistently find @⁠Validated as a meta-annotation at arbitrary depths
Prior to this commit, ValidationAnnotationUtils looked up @⁠Validated
as a meta-annotation at arbitrary depths (e.g., when used as a
meta-meta-annotation) in determineValidationGroups() but only as a
"directly present" meta-annotation in determineValidationHints().

For consistency, this commit revises determineValidationHints() so that
it finds @⁠Validated as a meta-annotation at arbitrary depths as well.

Closes gh-36274
2026-02-11 16:02:13 +01:00
Brian Clozel 62e590a437 Use updated message in HttpEntityMethodProcessor
Prior to this commit, the `HttpEntityMethodProcessor` would create a new
`ServletServerHttpRequest` input message to parse the native Servlet
request, but would not reuse it for reading the request body using the
message converters.

In gh-32471, we applied a change that updates HTTP headers accordingly
when request parameters are read. But not reusing the input message
means that we are losing this update when instantiating the resulting
`HttpEntity`.

This commit ensures that `HttpEntityMethodProcessor` uses the input
message it just created when decoding the request body.

Fixes gh-36298
2026-02-11 12:32:15 +01:00
Sam Brannen 7f29df4d79 Make StompBrokerRelayMessageHandler's isPauseable() logic dynamic
Prior to this commit, an attempt to restart the
StompBrokerRelayMessageHandler (SBRMH) resulted in an
IllegalStateException stating that the ReactorNettyTcpClient was still
in the process of "Shutting down." The reason is that a
ReactorNettyTcpClient cannot be restarted after it has been closed, and
that is by design.

To address that issue, this commit introduces an
`internallyManagedTcpClient` flag in SBRMH that is used to track
whether SBRMH is in charge of managing the TcpClient internally or if
the TcpClient was supplied by the user and is therefore managed
externally.

If SBRMH manages the TcpClient internally, isPauseable() returns
`true`, and the `tcpClient` field is set to null (in stopInternal())
when the handler is stopped. Consequently, a new internally managed
TcpClient will be created when the handler is restarted.

If the TcpClient is managed externally, the handler is not considered
to be "pauseable", and a reference to the externally managed TcpClient
is retained after the handler has been stopped. If the
ApplicationContext is subsequently restarted, the externally managed
TcpClient will be reused -- which may or may not work, depending on the
implementation of the TcpClient. Note, however, that this has always
been the behavior of SBRMH with regard to stop/start scenarios for
externally managed TcpClients.

Closes gh-36266
2026-02-11 11:19:36 +01:00
rstoyanchev 8fb119df1e Determine validation groups in HandlerMethod
Closes gh-36274
2026-02-11 09:00:52 +00:00
rstoyanchev 1bb0fdda6b Move validation group determination to ValidationAnnotationUtils
See gh-36274
2026-02-10 16:56:33 +00:00
rstoyanchev 6507367d10 Polishing contribution
Closes gh-36179
2026-02-10 15:28:54 +00:00
Junhwan Kim fee7e6b7e2 Refine SendTo/Subscription header propagation
See gh-36179

Signed-off-by: Junhwan Kim <musoyou1085@gmail.com>
2026-02-10 15:28:54 +00:00
Brian Clozel 35c4973504 Upgrade to Reactor 2025.0.3
Closes gh-36288
2026-02-10 14:06:02 +01:00
Sam Brannen 77b54c7409 Stop setting the relayPort, since we explicitly set the TCP client 2026-02-10 13:21:33 +01:00
Sam Brannen ff9ba15762 Extract test logging into extension 2026-02-10 13:20:43 +01:00
Łukasz Jernaś c33b82f2a1 Log which HandlerInterceptor threw the exception
Closes gh-36281

Signed-off-by: Łukasz Jernaś <lukasz.jernas@allegro.com>
2026-02-10 11:44:26 +01:00
Brian Clozel ec459aefbf Upgrade to Micrometer 1.6.3
Closes gh-36287
2026-02-10 11:42:30 +01:00
rstoyanchev b227701473 Optimize RequestMappingInfo hashcode calculation
Precalculated hashcode makes sense for infos in the registry, but
matched infos created on the fly don't need it.

Closes gh-36279
2026-02-10 10:14:54 +00:00
Tran Ngoc Nhan 7a85ce959b Remove obsolete code in Redirecting to a resource section
See gh-34103
Closes gh-36384

Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-02-10 11:13:39 +01:00
Brian Clozel e93e55b456 Fix wildcard MIME type support in messaging converters
Prior to this commit, the "application/*+json" wildcard MIME type was
added to the list of supported MIME types in the JSON messaging
converter. This change wasn't fully reflected in the
`AbstractMessageConverter`, because only strict matching of type and
subtybe were considered.

This commit updates the `AbstractMessageConverter` to not only check the
type and subtype, but also check whether the supported MIME type
includes the one given as a parameter.

Fixes gh-36285
2026-02-10 10:41:17 +01:00
rstoyanchev e22b1648bf Cache HandlerMethod with resolved bean if singleton
See gh-36278
2026-02-09 17:16:05 +00:00
rstoyanchev 78967d4bd6 Optimize version match
Closes gh-36276
2026-02-09 16:36:26 +00:00
rstoyanchev 1d2351c87b Optimize single PathPattern match
Closes gh-36275
2026-02-09 16:33:22 +00:00
rstoyanchev 02dc4b6342 Avoid determineValidationGroups not necessary
There is no need to call determineValidationGroups if the method
itself doesn't require method validation.

See gh-36274
2026-02-09 15:52:58 +00:00
Sam Brannen 143828e27c Introduce "Attention Required" section in changelog
Closes gh-36277
2026-02-09 12:39:39 +01:00
Sam Brannen 7998bc9de2 Fix grammar in Javadoc 2026-02-09 12:06:06 +01:00
Manu Sridharan 8a1a9d2ab2 Fix nullability annotation in JdbcTemplate
Signed-off-by: Manu Sridharan <msridhar@gmail.com>
2026-02-08 18:13:57 +01:00
Juergen Hoeller 394a97d7e0 Add setPackagesToScan to LocalEntityManagerFactoryBean (and make setDataSource work)
Includes fix for consistent PersistenceException in case of no unit found for name.
Includes proper tests for Local(Container)EntityManagerFactoryBean with scan setup.

Closes gh-36270
Closes gh-36271
2026-02-08 17:54:02 +01:00
Juergen Hoeller 05ffe8249f Adapt to getAllClassNames SPI (post JPA 4.0 M1)
See gh-35705
2026-02-07 21:01:25 +01:00
Juergen Hoeller 20970a4a37 Repeatedly check status while trying to lock for shutdown
Closes gh-36260
2026-02-06 19:03:54 +01:00
rstoyanchev cb4b87cce2 Use headerFilter on SimpAnnotationMethodMessageHandler
See gh-36179
2026-02-06 17:13:11 +00:00
rstoyanchev 1d470fc375 Exposes headerFilter on SimpAnnotationMethodMessageHandler
Closes gh-36179
2026-02-06 17:08:50 +00:00
rstoyanchev 7fc619acdc Polishing contribution
See gh-36179
2026-02-06 17:08:50 +00:00
김준환 e5f8c5b7ae Add header propagation predicate support to message return value handlers
See gh-36179

Signed-off-by: Junhwan Kim <musoyou1085@gmail.com>
2026-02-06 17:08:50 +00:00
Juergen Hoeller 3666ad9d7f Derive StatelessSession from primary Session in HibernateJpaDialect
This is analogous to LocalSessionFactoryBean's SpringSessionContext.

See gh-36025
2026-02-06 00:18:00 +01:00
Juergen Hoeller 652d509cfc Avoid direct javadoc reference to JPA 4.0 EntityAgent class
See gh-36025
2026-02-05 20:13:20 +01:00
Juergen Hoeller 153c378bb2 Polishing 2026-02-05 20:01:47 +01:00
Juergen Hoeller cdbaa7f3a7 Early support for JPA 4.0 EntityAgent (autowiring a shared proxy)
Includes skipping of isOpen check on close where easily possible.

Closes gh-36025
2026-02-05 20:01:26 +01:00
Brian Clozel d1b0bdb3d5 Revert Servlet TRACE error dispatch workaround
Closes gh-36258
2026-02-04 14:53:07 +01:00
Brian Clozel 59a9d0fcb9 Remove outdated GraalVM substitutions
Closes gh-36257
2026-02-04 14:52:40 +01:00
rstoyanchev db9045ae28 Polishing in CharBufferDecoder 2026-02-04 11:37:52 +00:00
rstoyanchev 7c2159fbe9 Polishing contribution
Closes gh-36225
2026-02-04 11:36:45 +00:00
Đặng Minh Dũng f2d3da3f32 Add ProxyFactoryCustomizer
Allows manipulating the `ProxyFactory` before the proxy is created

See gh-36225

Signed-off-by: Đặng Minh Dũng <dungdm93@live.com>
2026-02-04 11:35:48 +00:00
Yurii Platunov f1c86b1bbc Fix formatting in Web MVC HTTP Message Conversion documentation
Closes gh-36250

Signed-off-by: Yurii Platunov <furachannel16@gmail.com>
2026-02-04 11:30:32 +01:00
Johnny Lim 7fde8a6fa3 Remove obsolete doc for spring.context.annotated-bean-reader.create
See gh-35570
Closes gh-36241

Signed-off-by: Johnny Lim <izeye@naver.com>
2026-02-04 11:06:16 +01:00
Ivars Silamiķelis 033c309d39 Fix JSON syntax in STOMP overview documentation
Closes gh-36229

Signed-off-by: Ivars Silamiķelis <ivars.silamikelis@gmail.com>
2026-02-04 11:04:13 +01:00
Juergen Hoeller ac243fae65 Runtime compatibility with JPA 4.0.0-M1 and Hibernate ORM 8.0.0.Alpha1
Closes gh-35705
2026-02-03 19:13:48 +01:00
Juergen Hoeller ad19c1ac82 Introduce beforeRetry callback with RetryState argument
Closes gh-36245
2026-02-02 18:34:00 +01:00
Sébastien Deleuze 29d125e659 Add resource hint for BeanRegistrar implementing ImportAware
Closes gh-36242
2026-02-02 15:44:22 +01:00
Juergen Hoeller d31bd8139d Remove duplicated Jakarta Validation API declaration 2026-02-02 14:45:22 +01:00
Juergen Hoeller fb10455d3c Upgrade to Groovy 5.0.4, Tomcat 11.0.18, Jetty 12.1.6, Jackson 3.0.4/2.20.2, Hibernate ORM 7.2.3, Checkstyle 13.1 2026-02-02 12:14:40 +01:00
Juergen Hoeller 40350653e1 Upgrade to ASM 9.9.1 and Objenesis 3.5
Includes JavaPoet 0.10.0

Closes gh-36243
2026-02-02 12:13:41 +01:00
Sam Brannen a08a4d6068 Document @⁠SpringExtensionConfig in the reference manual
This commit also updates the Javadoc for the SpringExtension and
@⁠SpringExtensionConfig to point out that the SpringExtension always
uses a test-class scoped ExtensionContext if
@⁠TestInstance(Lifecycle.PER_CLASS) semantics are in effect.

Closes gh-36240
2026-01-31 16:24:38 +01:00
dependabot[bot] 6fd84e4c2f Upgrade fast-xml-parser from 4.5.2 to 5.3.4 in /framework-docs
Closes gh-36234
2026-01-31 15:31:24 +01:00
Juergen Hoeller 516bf1c606 Consistently detect resilience annotations on interfaces
Closes gh-36233
2026-01-30 17:12:22 +01:00
Sam Brannen 32b55e68fc Upgrade to Gradle 9.3.1 2026-01-30 15:48:15 +01:00
Sam Brannen b1f427f137 Fix typo 2026-01-30 13:41:20 +01:00
Sébastien Deleuze f52481493c Use @Nullable Void in TaskCallback#from
See gh-36208
2026-01-30 13:36:29 +01:00
qwding 149397ed10 Optimize NamedParameterUtils#buildValueArray by lazily fetching SqlParameter
This PR optimizes the performance of NamedParameterUtils#buildValueArray by deferring the call to findParameter(declaredParams, paramName, i).

Changes: In the original implementation, findParameter was called for every parameter in the loop, regardless of whether the paramValue retrieved from paramSource was already an instance of SqlParameterValue.

Since findParameter involves iterating through the declaredParams list (or performing lookups), skipping this call when paramValue instanceof SqlParameterValue is true reduces unnecessary CPU cycles and memory access, especially for queries with a large number of parameters or long declaredParams lists.

Signed-off-by: qwding <761945125@qq.com>
2026-01-30 11:59:43 +01:00
Brian Clozel e1d3076570 Fix "remove" implementation in netty headers adapter
Prior to this commit, the `Netty4HeadersAdapter` `MultiValueMapi#remove`
implementation would return an empty list if no value was present. This
is not consistent with other implementations.

This change ensures that `null` is returned for those cases.

Fixes gh-36226
2026-01-29 11:06:46 +01:00
Juergen Hoeller 16f4b23c32 Consistently close streams through try-with-resources
Closes gh-36223
2026-01-28 18:08:06 +01:00
Sam Brannen 54c3d01270 Revise documentation on JSR-330 support in the reference manual
Closes gh-36222
2026-01-28 18:06:39 +01:00
Sam Brannen 799a520c35 Consistently refer to "an SQL" statement, type, etc.
Aside from the three classes I modified, we already do this
consistently across the code base and in the reference manual.
2026-01-28 18:02:34 +01:00
Patrick Strawderman bf5386712c Use Java Files utilities in FileCopyUtils
Use helpers from java.nio.file.Files for some methods in FileCopyUtils.

Additionally, add unit tests for the various file-related methods.

Signed-off-by: Patrick Strawderman <pstrawderman@netflix.com>
2026-01-28 17:46:09 +01:00
Juergen Hoeller 5d33de943b Polishing 2026-01-28 17:09:02 +01:00
Juergen Hoeller 7da65fe4fc Support -1 for undetermined length in SqlBinaryValue/SqlCharacterValue
Closes gh-36219
2026-01-28 17:08:40 +01:00
Sam Brannen 0d612b3b1d Make TaskCallback a @⁠FunctionalInterface 2026-01-28 14:54:52 +01:00
Sam Brannen 4dacc64a30 Fix formatting and spelling 2026-01-28 14:27:27 +01:00
Sam Brannen ba47dd0714 Fix links to JUnit User Guide
Closes gh-36217
2026-01-28 14:16:32 +01:00
rstoyanchev 79386b0e83 Fix unfinished edit from previous commit
See gh-36198
2026-01-28 12:27:09 +00:00
rstoyanchev d1bfe03358 Update docs on trailing slash handling
Closes gh-36198
2026-01-28 12:21:33 +00:00
rstoyanchev 4bc9f68451 Update docs content types for ProblemDetail
Closes gh-36192
2026-01-28 12:21:33 +00:00
Sam Brannen ce0e37536b Fix Javadoc formatting
See gh-36207
2026-01-28 11:00:42 +01:00
Juergen Hoeller b0ac8df13e Consistent delegation to target ValidatorFactory 2026-01-27 20:55:01 +01:00
Juergen Hoeller 80a57c2468 Consistent references to Hibernate ORM 7.x 2026-01-27 20:46:30 +01:00
Juergen Hoeller 02e05b40b0 Polishing 2026-01-27 20:30:46 +01:00
Juergen Hoeller c8c99d0464 Support JtaTransactionManager/SpringBeanContainer on HibernateJpaVendorAdapter
Makes ConfigurableJtaPlatform public and removes outdated WebSphere UOW check.

Closes gh-36216
2026-01-27 20:30:21 +01:00
Juergen Hoeller 867dd0e972 Upgrade to Hibernate ORM 7.2.2 2026-01-27 20:28:18 +01:00
Sébastien Deleuze 2c96f18439 Upgrade nullability plugin to 0.0.11
Closes gh-36188
2026-01-27 14:10:18 +01:00
Patrick Strawderman 96fb5d72eb Use Reader.transferTo in FileCopyUtils
Use the transferTo method in FileCopyUtils#copy(Reader, Writer).

Closes gh-36196

Signed-off-by: Patrick Strawderman <pstrawderman@netflix.com>
2026-01-27 09:38:58 +01:00
Sébastien Deleuze bc01eeb433 Leverage JUnit 6 suspending function support
Closes gh-36215
2026-01-27 08:45:04 +01:00
rstoyanchev 77c24b28b6 Polishing in ReactorClientHttpConnector 2026-01-26 10:51:00 +00:00
Kuyho Chung e809395635 Clarify Kotlin Coroutine definition
Closes gh-36209
Signed-off-by: Kuyho Chung <kchung1995@gmail.com>
2026-01-26 09:54:15 +01:00
Sam Brannen 801035bea7 Upgrade to AssertJ 3.27.7 2026-01-25 17:51:00 +01:00
Sam Brannen 9e4a54bb99 Ensure RetryTaskTests compiles in Eclipse IDE
Prior to this commit, "new RetryTask<>" declarations whose
TaskCallbacks (implemented as lambda expressions) did not actually
throw an exception resulted in a compilation error in Eclipse:

  "Unhandled exception type Exception".

This is due to the fact that RetryTask is declared with "E extends
Exception", and the Eclipse compiler therefore infers that the lambda
expression potentially throws a checked Exception.

To address that, this commit explicitly declares that the affected
lambda expressions throw unchecked RuntimeExceptions:

  "new RetryTask<String, RuntimeException>"
2026-01-25 17:46:14 +01:00
Sam Brannen e56f7c1e53 Polishing 2026-01-25 17:35:17 +01:00
Padraic Slattery 65bdc78a1e Update GitHub upload-artifact action to version 6
Closes gh-36199

Signed-off-by: Padraic Slattery <pgoslatara@gmail.com>
2026-01-25 17:07:09 +01:00
Juergen Hoeller fd3bbb057e Polishing 2026-01-25 10:42:13 +01:00
Juergen Hoeller c47da1247a Add TaskCallback/Callable/Runnable wrapper for retryable tasks
Closes gh-36208
2026-01-25 10:41:25 +01:00
Juergen Hoeller 22cf7958a5 Revise setPersistenceUnitName javadoc
Closes gh-36205
2026-01-25 10:38:24 +01:00
Phillip Webb ab314aadd9 Refine YamlProcessor to allow different empty values
Update `YamlProcessor` to allow any value to be inserted for
empty entries. This update will allow blank strings to be used
instead of `null` which better aligns with the way that
`.properties` files are loaded.

This update also allows allows the specified `emptyValue` to be
inserted instead of a blank String when the YAML contains `null`
or `~` values.

See gh-36197
See gh-19986

Signed-off-by: Phillip Webb <phil.webb@broadcom.com>
2026-01-24 20:23:48 +01:00
Sébastien Deleuze 611811d8fc Anticipate fixing Kotlin 2.3 compilation warning
Closes gh-36203
2026-01-23 18:34:45 +01:00
Phillip Webb 0c9f40b171 Allow YAML processors to create a flattened map with nulls included
Add an additional `getFlattenedMap` method to `YamlProcessor` to allow
the resulting flattened map to include nulls.

This update will allow processor subclasses to tell the difference
between YAML that is defined with an empty object vs missing the key
entirely:

e.g.:

  application:
    name: test
    optional: {}

vs

  application:
    name: test
    optional: {}

Closes gh-36197

Signed-off-by: Phillip Webb <phil.webb@broadcom.com>
2026-01-23 18:07:06 +01:00
Sam Brannen 5b4eda3d20 Clean up warnings in Gradle build 2026-01-22 17:27:24 +01:00
Sam Brannen af8fd8f07d Upgrade to TestNG 7.12 2026-01-22 17:27:00 +01:00
rstoyanchev 345fa51e77 Handle early exception from AsynchronousFileChannel#write
Closes gh-36184
2026-01-21 15:43:03 +01:00
Juergen Hoeller c05ea841bc Declare Callable parameter with nullable type variable
Closes gh-36191
2026-01-21 19:45:18 +01:00
Juergen Hoeller dab28b5dce Upgrade to Selenium 4.40 2026-01-21 15:09:24 +01:00
Juergen Hoeller 1c4b20287f Use correlation id for explicit sendAndReceive response queue
See gh-36162
See gh-36177
2026-01-21 14:46:36 +01:00
Sébastien Deleuze 8fbd147f0b Upgrade nullability plugin to 0.10.0
Closes gh-36188
2026-01-21 10:39:41 +01:00
Juergen Hoeller 1c56ec3f7e Polishing 2026-01-20 19:24:03 +01:00
Juergen Hoeller 65565de1a8 Prevent accidental printStackTrace() usage in main codebase
Closes gh-36185
2026-01-20 19:23:37 +01:00
Juergen Hoeller b3c26255b1 Upgrade to Hibernate ORM 7.2.1 and Protobuf 4.33.4 2026-01-20 19:23:29 +01:00
Sam Brannen 66962875f9 Reinstate test for Jetty in CookieIntegrationTests 2026-01-20 17:11:03 +01:00
Sam Brannen 5490ba94ae Simplify tests 2026-01-20 17:10:07 +01:00
Sam Brannen 17e02804ba Revert changes to FailingBeforeAndAfterMethodsTestNGTests 2026-01-20 15:47:45 +01:00
Sam Brannen 357205723f Simplify tests 2026-01-20 13:54:19 +01:00
Sam Brannen c4c167c0e1 Convert various test classes to @⁠ParameterizedClass
See gh-35833
2026-01-20 13:53:56 +01:00
rstoyanchev 4119ecc25b Refine solution to clear Netty channel attribute
Closes gh-36158
2026-01-20 11:34:18 +00:00
Sam Brannen 9ebfdb8b6a Fix test parameterization in CaffeineReactiveCachingTests
Prior to this commit, test methods in CaffeineReactiveCachingTests
were parameterized twice with the same configuration class.

See gh-31637
See gh-35833
2026-01-20 11:43:22 +01:00
Sébastien Deleuze c4cb55fbde Modernize DispatcherServlet context hierarchy diagram
See gh-36175
2026-01-20 09:41:28 +01:00
Sam Brannen 7917ae53e8 Polishing 2026-01-19 19:16:28 +01:00
Sébastien Deleuze 693ec8219f Introduce RestClient.ResponseSpec#requiredBody
Closes gh-36173
2026-01-19 18:15:34 +01:00
rstoyanchev 9b95482557 ContentRequestMatchers supports HttpEntity multipart
Closes gh-36154
2026-01-19 17:05:16 +00:00
Sam Brannen a029dfc6a8 Improve display names for parameterized invocations
See gh-35833
2026-01-19 17:59:49 +01:00
Sébastien Deleuze a921c487b4 Fix snippet imports
See gh-36175
2026-01-19 17:38:30 +01:00
Sébastien Deleuze 3f01830d11 Refine CORS documentation to match latest changes
Closes gh-36156
2026-01-19 17:27:19 +01:00
Sébastien Deleuze 64d95d7f3e Extract code snippets from mvc-servlet.adoc
Closes gh-36175
2026-01-19 17:24:38 +01:00
Sébastien Deleuze 5a4b1bd1d1 Extract code snippets from logging.adoc
See gh-36175
2026-01-19 17:24:38 +01:00
Sébastien Deleuze a91402dcc6 Extract code snippets from multipart.adoc
See gh-36175
2026-01-19 17:24:38 +01:00
Sébastien Deleuze 0014e0b3c9 Extract code snippets from context-hierarchy.adoc
See gh-36175
2026-01-19 17:24:38 +01:00
Sébastien Deleuze 49da766551 Extract code snippets from exceceptionhandlers.adoc
See gh-36175
2026-01-19 17:24:38 +01:00
Sébastien Deleuze 50bbe6191a Extract code snippets from container-config.adoc
See gh-36175
2026-01-19 17:24:37 +01:00
Shubham Kalloli a8c33c13e2 Upgraded Apache POI and Commons IO to remediate CVEs
Signed-off-by: Shubham Kalloli <shubham.kalloli@est.tech>
2026-01-19 17:06:02 +01:00
Sam Brannen b164db35c1 Revise contribution
See gh-36170
2026-01-18 16:59:30 +01:00
Tran Ngoc Nhan 385e62dbf0 Replace getErrors() with getBindingResult() in examples
DataBinder#getErrors was removed in v4.0.0.M1.

Closes gh-36170

Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-01-18 16:55:32 +01:00
Sam Brannen 4d4adeb2c6 Polish Javadoc 2026-01-18 16:52:04 +01:00
Juergen Hoeller 7aa862f5ec Make scope fully configurable
Closes gh-36171
2026-01-18 15:55:36 +01:00
Juergen Hoeller 2d2809f45c Consistently throw Task/InvocationRejectedException on access rejected
Closes gh-36167
2026-01-18 15:39:58 +01:00
Michał Pisarski 3f390d55f0 Add overloaded sendAndReceive methods to JmsOperations and JmsTemplate to use explicit response queue
Signed-off-by: Michał Pisarski <pisekfm@o2.pl>
2026-01-17 00:16:15 +01:00
Juergen Hoeller 2b96a61063 Apply transactionIsolationLock in EclipseLinkConnectionHandle as well
Closes gh-36165
2026-01-16 23:12:07 +01:00
Juergen Hoeller c55351aa4a Remove javadoc references to WebLogic API
See gh-36151
2026-01-16 22:50:46 +01:00
Juergen Hoeller 48bedd0af1 Revise DefaultMessageListenerContainer connection recovery
Includes listener validation for consistent back-off behavior.

Closes gh-36143
2026-01-16 22:38:01 +01:00
Juergen Hoeller c1b0e68d6c Bring back WebLogicJtaTransactionManager for WebLogic 15.1.1
Closes gh-36151
2026-01-16 22:37:20 +01:00
rstoyanchev fe2113f5a3 Clear Netty channel attribute
Closes gh-36158
2026-01-16 17:21:27 +00:00
Sam Brannen 2ff2a89f22 Upgrade to Gradle 9.3
Closes gh-36161
2026-01-16 15:41:31 +01:00
Brian Clozel 14641c4299 Next development version (v7.0.4-SNAPSHOT) 2026-01-15 09:26:50 +01:00
Juergen Hoeller 62fd09dfa5 Polishing 2026-01-14 22:05:06 +01:00
Sam Brannen 9df19dedaf Revise wording for PauseMode documentation
See gh-36044
2026-01-14 17:02:22 +01:00
Sam Brannen 01a57a7219 Simplify DefaultContextCache implementation by using entrySet().removeIf() 2026-01-14 13:47:44 +01:00
Sam Brannen b5c2003c51 Fix variable name 2026-01-14 13:44:04 +01:00
Sam Brannen 5f5da06467 Upgrade to JUnit 6.0.2
Closes gh-36149
2026-01-14 13:00:31 +01:00
rstoyanchev 9f19b4074d Exclude DataAccessException and MessagingException in DisconnectedClientHelper
Closes gh-36134
2026-01-14 11:44:43 +00:00
Yanming Zhou a784eb0ab4 Improve DisconnectedClientHelper to better guard ClassNotFoundException
Before this commit, WebClientException is ignored if RestClientException is not present.

Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>
2026-01-14 11:03:09 +00:00
Sam Brannen fa404063ab Avoid unnecessary pausing of application contexts for tests
In commit 9711db787e, we introduced support for disabling test
application context pausing via a Spring property or JVM system
property, as follows.

-Dspring.test.context.cache.pause=never

However, users may actually be interested in keeping the pausing
feature enabled if contexts are not paused unnecessarily.

To address that, this commit introduces a new
PauseMode.ON_CONTEXT_SWITCH enum constant which is now used by default
in the DefaultContextCache.

With this new pause mode, an unused application context will no longer
be paused immediately. Instead, an unused application context will be
paused lazily the first time a different context is retrieved from or
stored in the ContextCache. This effectively means that an unused
context will not be paused at all if the next test class uses the same
context.

Although ON_CONTEXT_SWITCH is the now the default pause mode, users
still have the option to enable context pausing for all usage scenarios
(not only context switches) by setting the Spring property or JVM
system property to ALWAYS (case insensitive) — for example:

-Dspring.test.context.cache.pause=always

This commit also introduces a dedicated "Context Pausing" section in
the reference manual.

See gh-36117
Closes gh-36044
2026-01-14 11:37:37 +01:00
Sam Brannen 948af8bcc6 Fix typo in Javadoc 2026-01-14 11:37:37 +01:00
Juergen Hoeller e3997e3060 Upgrade to Reactor 2025.0.2 and Micrometer 1.16.2
Includes Vavr 0.11, HtmlUnit 4.21, Checkstyle 13.0

Closes gh-36144
Closes gh-36145
2026-01-14 11:29:29 +01:00
Juergen Hoeller 0323218129 Deprecate PagedListHolder and PropertyComparator for removal
Closes gh-36139
2026-01-13 21:20:38 +01:00
Brian Clozel a62699ccfc Further Gradle deprecation fixes
Closes gh-36132
2026-01-13 09:32:08 +01:00
Yejeong, Ham dd6faf62bb Build: Fix deprecated Groovy syntax in spring-aspects
Replace legacy Groovy DSL property assignment (space-separated) with
the standard assignment operator (=) in spring-aspects.gradle.

The legacy syntax is deprecated in Gradle 9 and will be removed in
Gradle 10. This change eliminates deprecation warnings during the
build and ensures future compatibility.

See gh-36132

Signed-off-by: Yejeong, Ham <dev@thelightway.kr>
2026-01-13 09:31:48 +01:00
rstoyanchev cbc2c63abc Polishing contribution
Closes gh-36125
2026-01-12 12:48:49 +00:00
hojeong 2f83e6b4b5 Update reference for API version without a value
See gh-36125

Signed-off-by: hojeong park <parkhj062@gmail.com>
Signed-off-by: hojeong <parkhj062@gmail.com>
2026-01-12 12:24:13 +00:00
Sam Brannen 19c2506476 Do not restart contexts if pausing is disabled
If the PauseMode is set to NEVER, we never pause contexts and therefore
do not need to restart them.

See gh-36117
2026-01-12 12:24:18 +01:00
Brian Clozel ee4780fab6 Build javadoc against HttpClient 5.6.x 2026-01-12 12:11:26 +01:00
Sam Brannen 8e6d44349d Polishing 2026-01-12 11:50:03 +01:00
Sam Brannen ceb6ab3c74 Introduce ContextCacheTestSuite 2026-01-12 11:48:31 +01:00
Sébastien Deleuze 7140ab5bca Simplify TransactionalOperator.executeAndAwait
This commit simplifies TransactionalOperator.executeAndAwait by removing
Optional. It is based on a proposal by @vlsi refined to handle properly
reactive transaction commits (see related commit
217b6e37a6).

Closes gh-36039
2026-01-12 10:52:39 +01:00
rstoyanchev 615c25d4fa Polishing contribution
Closes gh-36126
2026-01-12 09:51:17 +00:00
Juhwan Lee 3df66580f3 Support property placeholders in HTTP service registry
Implement EmbeddedValueResolverAware to resolve ${...} placeholders
in @HttpExchange URL attributes.

See gh-36126
Signed-off-by: Juhwan Lee <jhan0121@gmail.com>
2026-01-12 08:56:35 +00:00
Sam Brannen 9711db787e Introduce Spring property to disable context pausing for tests
Spring Framework 7.0 introduced support for pausing inactive
application contexts between test classes and restarting them once they
are needed again. If pausing and restarting are fast, this feature does
not have a negative impact on test suites.

However, if the pausing or restarting of certain Lifecycle components
in the application context is slow, that can have a negative impact on
the duration of the overall test suite.

In gh-36044, we hope to find a way to avoid unnecessarily pausing an
application context after a test class if the same context is used by
the next test class that is run. That should help reduce the risk of a
negative impact caused by the pause/restart feature; however, for
certain scenarios that may not be enough. In light of that, this commit
introduces a mechanism for completely disabling the pausing feature via
a Spring property or JVM system property, as follows.

-Dspring.test.context.cache.pause=never

See gh-35168
See gh-36044
Closes gh-36117
2026-01-11 11:42:14 +01:00
Yanming Zhou d3a385d222 Retain original requested bean class for SpringContainedBean
Closes GH-36115

Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>
2026-01-09 19:52:22 +01:00
Juergen Hoeller 38a397873e Polishing 2026-01-09 19:33:52 +01:00
Juergen Hoeller 73fa708905 Fix SmartFactoryBean type matching for ResolvableType.NONE
Closes gh-36123
2026-01-09 19:33:32 +01:00
rstoyanchev 23a532ed82 Polishing contribution
Closes gh-36097
2026-01-09 15:06:07 +00:00
Nabil Fawwaz Elqayyim e0db67aa3b Fix double encoding in DefaultApiVersionInserter
Ensure that the DefaultApiVersionInserter does not re-encode existing parts
of the input URI by using the 'encoded' flag in UriComponentsBuilder.

This prevents percent-encoded characters (like %20) from being incorrectly
double-encoded to %2520 during the version insertion process.

See gh-36097

Signed-off-by: Nabil Fawwaz Elqayyim <master@nabilfawwaz.com>
2026-01-09 15:06:07 +00:00
Sam Brannen 1370a833ed Further clarify the meaning of maxAttempts in FixedBackOff and ExponentialBackOff
See gh-36119
2026-01-09 15:37:32 +01:00
Sam Brannen 8d4320e9ef Honor headers in AbstractMessageSendingTemplate.convertAndSend() variant
Prior to this commit, the following method in AbstractMessageSendingTemplate
simply ignored the supplied headers map.

convertAndSend(Object, Map<String, Object>, MessagePostProcessor)

Closes gh-36120
2026-01-09 15:04:47 +01:00
Sam Brannen c53a00a6c4 Polish Javadoc 2026-01-09 15:04:47 +01:00
Sam Brannen 00606d2171 Use default destination name with JmsClient.sendAndReceive() and headers
Prior to this commit, if the JmsClient was configured with a default
destination name (instead of a default Destination), invoking the
sendAndReceive() variant which accepts a map of headers resulted in an
exception stating that a default destination is required.

To address that, this commit overrides the
convertSendAndReceive(Object, Map<String, Object>, Class<T>, MessagePostProcessor)
method in JmsMessagingTemplate to add support for both a default
Destination and a default destination name.

Closes gh-36118
2026-01-09 15:04:47 +01:00
Juergen Hoeller 3e5d8d184a Clarify back-off attempts versus retries for BackOff maxAttempts setting
Closes gh-36119
2026-01-09 14:32:59 +01:00
rstoyanchev b69cbad38d Fall back on the value type in BodyInserters if necessary
Closes gh-36078
2026-01-09 11:52:18 +00:00
rstoyanchev 39106b5811 Restore ApiVersionResolver contract
Instead of making it async and having a sync subinterface variant,
this restores ApiVersionResolver to be as it was with an async
subinterface variant.

ApiVersionStrategy, and the infrastructure invoking it, remains
async first, but also accommodates sync resolvers.

This should provide a better balance with backwards compatibility
while also accommodating async version resolution as the less
common scenario.

See gh-36084
2026-01-09 10:25:44 +00:00
rstoyanchev 2f91baa71c Polishing in ApiVersionResolver 2026-01-09 09:38:53 +00:00
rstoyanchev d8e303f95b Merge branch for PR 36084
Resolve conflicts and refactor merged code in AbstractHandlerMapping

See gh-36084
2026-01-08 17:43:35 +00:00
rstoyanchev 8ff89ffda2 Update contribution
- deprecate sync method on ApiVersionResolver
- add SyncApiVersionResolver
- refactor resolverParseAndValidateApiVersion method

See gh-36084
2026-01-08 17:41:27 +00:00
rstoyanchev dcf5d69d94 Rename async methods
See gh-36084
2026-01-08 17:41:19 +00:00
rstoyanchev 7d33a87278 Polishing contribution
See gh-36084
2026-01-08 17:41:11 +00:00
Jonathan Kaplan b878771dca Update ApiVersionResolver to return Mono String
See gh-36084

Signed-off-by: Jonathan Kaplan <jkaplan@empty-nes.com>
2026-01-08 17:41:01 +00:00
Sam Brannen 06f0f2f317 Polishing 2026-01-08 18:32:57 +01:00
Sam Brannen f16474d585 Reliably resolve overloaded write methods in PropertyDescriptorUtils
Prior to this commit, the algorithm behind determineBasicProperties()
in PropertyDescriptorUtils did not reliably resolve the correct write
method when one candidate write method had a parameter type that was a
subtype of another candidate write method whose parameter type was an
exact match for the resolved read method's return type.

In other words, the algorithm always resolved the candidate write
method with the most specific parameter type (similar to covariant
return types) which is not necessarily the resolved read method's
return type.

To address that, this commit ensures that determineBasicProperties()
always selects an exact match for the write method whenever possible.

As an added bonus, determineBasicProperties() no longer invokes
BasicPropertyDescriptor.getWriteMethod(), which avoids triggering the
resolution algorithm multiple times (when multiple write method
candidates exist), resulting in lazy resolution of the write method the
first time client code invokes getWriteMethod().

Closes gh-36113
2026-01-08 18:13:46 +01:00
Sam Brannen dfb17d84d6 Reintroduce deleted text in Javadoc 2026-01-08 18:11:55 +01:00
Sébastien Deleuze eea9130ea6 Fix context class resolution for nested types
This commit introduces ContextClassRequestBodyAdvice which adds a
"contextClass" hint allowing to resolve generics for Optional,
HttpEntity or ServerSentEvent container types.

Closes gh-36111
2026-01-08 16:53:28 +01:00
Sébastien Deleuze d85ac300bb Update JsonView advices Javadoc
Make the documentation relevant for all Jackson converters.
2026-01-08 16:53:19 +01:00
Sébastien Deleuze d4d10dc75d Enable JacksonSmileEncoderTests#annotEncodeServerSentEvent
This test has been fixed at some (unidentified) point and should be
enabled.
2026-01-08 16:49:11 +01:00
Juergen Hoeller 169465cce1 Align InvocationRejectedException javadoc with TaskRejectedException
See gh-36109
See gh-36114
2026-01-08 16:18:14 +01:00
Juergen Hoeller c4d5e3c57d Add task rejection support to SyncTaskExecutor's concurrency throttle
Closes gh-36114
2026-01-08 15:51:17 +01:00
Juergen Hoeller 38f5f4de8e Add configurable throttle policy to @ConcurrencyLimit annotation
Closes gh-36109
2026-01-08 15:50:57 +01:00
Sébastien Deleuze 644072f7ea Refine the AOT Cache creation section
Put Java 25+ snippet first and refine the note.

Closes gh-36108
2026-01-08 14:50:14 +01:00
Mikhail Polivakha 859940b824 Polishing of the AOT cache documentation
See gh-36108
Signed-off-by: Mikhail Polivakha <mikhailpolivakha@email.com>
2026-01-08 14:49:21 +01:00
Sam Brannen 992ae2ea6b Precompute PropertyDescriptor array in SimpleBeanInfoFactory
Closes gh-36112
2026-01-08 13:34:03 +01:00
Sam Brannen 589a10e6e6 Polish Javadoc formatting 2026-01-08 13:25:15 +01:00
Sam Brannen 79bd2b11da Polishing 2026-01-08 13:23:23 +01:00
Sam Brannen 0624c3a597 Introduce tests for non-generic types for PropertyDescriptorUtils
This commit introduces tests for proper support for non-generic types
in PropertyDescriptorUtils.determineBasicProperties(), effectively to
test the status quo and serve as regression tests.
2026-01-08 13:15:53 +01:00
Sébastien Deleuze bf695c058d Configure Jackson XML codecs when present
Closes gh-35752
2026-01-08 11:52:38 +01:00
Sébastien Deleuze a1204a405a Introduce Jackson XML codecs
See gh-35752
2026-01-08 11:52:38 +01:00
rstoyanchev e0aa116217 Re-initialize API version in each HandlerMapping
Each HandlerMapping may have its own ApiVersionStrategy rules, or may not
have one at all. This change ensures independent decisions.

See gh-36059
2026-01-08 09:32:34 +00:00
Philip Bolting 7fdda1cf0f Fix typo in MVC REST Error Responses documentation
Signed-off-by: Philip Bolting <pb@philipbolting.de>
2026-01-08 08:40:31 +01:00
rstoyanchev 05b98b0a32 Suppress API version errors on ERROR dispatch
While an API version may be important for mapping in an ERROR dispatch,
it is more important to allow the original exception to be handled.

Closes gh-36058
2026-01-07 16:00:40 +00:00
rstoyanchev 46efd930f0 Update docs
On the order of a ControllerAdvice that overrides the handling of
built-in exceptions in a scenario with Boot autoconfig.

Closes gh-35982
2026-01-07 16:00:39 +00:00
rstoyanchev a9027ddf76 Refine handling of API version errors
Among HandlerMapping's some may not expect an API version. This is why
those that do must be careful not to raise API validation errors if
they don't match the request.

Closes gh-36059
2026-01-07 16:00:39 +00:00
rstoyanchev 5551292639 Polishing contribution
Closes gh-35989
2026-01-07 16:00:39 +00:00
Minkyu Park 1240962d68 Optimize single-char wildcard path matching performance
Use lazy evaluation in SingleCharWildcardedPathElement to avoid
unnecessary Character.toLowerCase() calls.
Resolves performance TODO from 2017.

Closes gh-36095

Signed-off-by: Minkyu Park <rb6609@naver.com>
2026-01-07 14:37:54 +01:00
Brian Clozel 9ef4ceb047 Polishing contribution
Closes gh-36083
2026-01-07 14:29:13 +01:00
hayden.rear c7413cbdfc Configure HttpMessageConverters as a list
This commit adds a new `configureMessageConvertersList` method on the
builder to add/remove/move converters in the resulting list before they
are individually post-processed.

This allows to re-introduce a behavior that was missing with the new
contract: the ability to append a converter at the end of the list.

See gh-36083

Signed-off-by: hayden.rear <hayden.rear@gmail.com>
2026-01-07 14:29:05 +01:00
Sam Brannen e94aaab288 Improve exception message for JPA entity scanning 2026-01-07 13:13:27 +01:00
Sébastien Deleuze c5044bfdbc Refine multipart-forms.adoc
See gh-36094
2026-01-07 12:49:25 +01:00
Tran Ngoc Nhan 9abe4b46ca Fix Kotlin example in Multipart Content
Closes gh-36094
Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-01-07 12:20:59 +01:00
Sébastien Deleuze 192da6229c Fix locale interceptor code snippet
See gh-36105
See gh-36099
2026-01-07 09:52:33 +01:00
Sébastien Deleuze e1402fd252 Upgrade Antora dependencies
Closes gh-36105
2026-01-07 09:51:04 +01:00
Sébastien Deleuze 253eb28458 Update CBOR Jackson codec Javadoc
See gh-20513
2026-01-06 18:26:26 +01:00
Brian Clozel 97cd9dd03b Support HttpComponents 5.6
This commit updates the HttpComponents HttpClient to refer to the parsed
`HttpEntity` for the content-related HTTP response headers such as
encoding and body length.

Closes gh-36100
2026-01-06 16:56:45 +01:00
Sébastien Deleuze 3027d78f40 Modernize the DispatcherServlet Locale documentation
The Java sample for "Locale Interceptor" shows a
`urlHandlerMapping.setUrlMap(Map.of("...` line due the inability to
disable the code chomping Asciidoctor extension with the code include
one. It will be fixed by a subsequent commit or a bug fix in
https://github.com/spring-io/asciidoctor-extensions.

Closes gh-36099
2026-01-06 11:04:05 +01:00
Sébastien Deleuze d752e80356 Add missing line to MVC config interceptors snippet
To ensure consistency with Kotlin and XML snippets.
2026-01-06 11:04:05 +01:00
Sam Brannen e7f019bd3f Introduce tests for PropertyDescriptorUtils with bounded generics
As a follow up to commit 4b07edbaeb, this commit introduces tests for
PropertyDescriptorUtils.determineBasicProperties() using types with
bounded generics.

Note, however, that the following test effectively fails, since
PropertyDescriptorUtils.determineBasicProperties() does not match the
behavior of java.beans.Introspector. Consequently, this test method
currently changes the expected write method type conditionally.

resolvePropertiesWithUnresolvedGenericsInSubclassWithOverloadedSetter()

See gh-36019
2026-01-05 17:12:59 +01:00
Sébastien Deleuze 6939cb90df Add ServerRequest.bindAndAwait Kotlin extension
Closes gh-36092
2026-01-02 18:06:27 +01:00
Sébastien Deleuze 3ddaf51f5d Remove @EnableWebFlux from documentation code snippets
Like what we did for `@EnableWebMvc`, we want to remove `@EnableWebFlux`
from typical code snippets extending WebFluxConfigurer to make them more
Spring Boot friendly.

Closes gh-36091
2026-01-02 18:06:27 +01:00
Sébastien Deleuze 6a28f06b2e Extract some WebFlux functional snippets
See gh-36089
2026-01-02 18:06:27 +01:00
Sébastien Deleuze 5ecece91dc Polishing
See gh-36089
2026-01-02 18:06:26 +01:00
Tran Ngoc Nhan 13d7a6c480 Fix Kotlin snippets in "CORS" and "Functional Endpoints" sections
Closes gh-36089
Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-01-02 18:05:34 +01:00
Sébastien Deleuze 6e66af15a6 Extract remaining WebMVC configuration snippets
Closes gh-36088
2025-12-31 12:15:12 +01:00
Sébastien Deleuze cd8e38448c Remove the "Other Web Frameworks" documentation
Closes gh-36086
2025-12-31 10:58:44 +01:00
Sébastien Deleuze 93ce99ea06 Polishing 2025-12-31 10:58:44 +01:00
Sébastien Deleuze bf8a1600e5 Add missing override annotations to PropagationContextElement
Closes gh-36087
2025-12-31 10:58:43 +01:00
Sébastien Deleuze 96eee8c8b1 Make nullability flexible in ConverterFactory#getConverter
This commits udpates ConverterFactory#getConverter to accept both
nullable and non-null T in the Converter return value.

Flexible nullability at ConverterFactory type level would have been
ideal, but is not possible due to how Kotlin deals with Class<T> when
T is nullable.

See gh-36063
2025-12-30 16:07:39 +01:00
Tran Ngoc Nhan b8ed4f2ba8 Correct encode Javadoc for UriComponents and UriComponentsBuilder
Closes gh-36040
Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2025-12-30 15:26:08 +01:00
Tran Ngoc Nhan 0048f01074 Fix links in client-builder.adoc
Closes gh-36079
Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2025-12-30 15:11:52 +01:00
Sébastien Deleuze 0b904a878d Fix indentation in framework-docs.gradle 2025-12-30 15:04:14 +01:00
Sébastien Deleuze 9c78f84915 Modernize the view technologies section
Closes gh-35450
2025-12-30 15:03:34 +01:00
Sébastien Deleuze a55309d478 Null-mark org.springframework.test.web.support package
See gh-36054
2025-12-29 17:09:53 +01:00
Sébastien Deleuze 46fb7c026c Upgrade to nullability plugin 0.0.9
This commit also replaces Arch Unit packageInfoShouldBeNullMarked() rule
by either configuring requireExplicitNullMarking = false when the whole
module does not have JSpecify annotations, or explicit @NullUnmarked
when some have and some don't.

See gh-36054
2025-12-29 17:09:53 +01:00
Manu Sridharan d36244ee56 Update to NullAway 0.12.15 and fix new warnings
Closes gh-36054
Signed-off-by: Manu Sridharan <msridhar@gmail.com>
2025-12-29 17:09:52 +01:00
Sam Brannen 4b07edbaeb Reliably resolve generic read/write methods in PropertyDescriptorUtils
Prior to this commit, the determineBasicProperties() method in
PropertyDescriptorUtils did not reliably resolve read/write methods in
type hierarchies with generics. This utility method is used by
SimpleBeanInfoFactory which is used by BeanUtils and BeanWrapperImpl.
Thus, failure to reliably resolve read/write JavaBeans methods resulted
in bugs in certain scenarios.

For example, BeanUtils.copyProperties() randomly failed to copy certain
properties if the write method for the property could not be resolved.

To address such issues, this commit revises the implementation of
PropertyDescriptorUtils as follows.

1) Read methods with covariant return types are now consistently
   resolved correctly.

2) If multiple ambiguous write methods are discovered, the algorithm now
   checks for an exact match against the resolved generic parameter type
   as a fallback.

Closes gh-36019
2025-12-29 12:18:23 +02:00
Brian Clozel eb3bfc0d65 Document HttpHeaders#toSingleValueMap() case-sensitive behavior
This commit updates the `HttpHeaders` javadoc to better reflect that
`asSingleValueMap()`, `asMultiValueMap()`, and `toSingleValueMap()` all
return case-sentitive map implementations.

Closes gh-36070
2025-12-29 11:02:28 +01:00
木葉 Scarlet d077e043d6 Consistently declare @⁠Nullable on parameter in equals() implementations
Closes gh-36075

Signed-off-by: 木葉 Scarlet <93977077+mukjepscarlet@users.noreply.github.com>
2025-12-29 11:43:49 +02:00
Sam Brannen 7212fbe36a Polishing 2025-12-29 11:43:23 +02:00
Juergen Hoeller 5b6c231788 Use dedicated ApplicationEventFactory interface in EventPublicationInterceptor
See gh-36072
2025-12-25 11:25:54 +01:00
Juergen Hoeller aeb0115605 Introduce MethodRollbackEvent for @Transactional rollbacks
Closes gh-36073
2025-12-24 18:54:39 +01:00
Juergen Hoeller 08104100d3 Introduce generalized MethodFailureEvent for use in EventPublicationInterceptor
Closes gh-36072
2025-12-24 18:50:04 +01:00
Brian Clozel 214cb2c1e1 Polishing contribution
Closes gh-36056
2025-12-23 15:33:20 +01:00
Terry Tao 26e0f550b0 Fix LinkedCaseInsensitiveMap entrySet case-insensitivity
See gh-36056

Signed-off-by: Terry Tao <yueyang.tao@gmail.com>
2025-12-23 15:33:14 +01:00
Brian Clozel 012fb29097 Fix nullness mismatch for Converter/ConverterFactory
In gh-35947, the `Converter` contract was refined to allow for nullable
return values. This created a mismatch with the `ConverterFactory`
contract.

This commit fixes this mismatch by allowing nullable return values in
`Converter` instances created by `ConverterFactory`.

Fixes gh-36063
2025-12-23 11:37:44 +01:00
Juergen Hoeller 92a43c007a Introduce MethodRetryEvent for @Retryable execution
Closes gh-35382
2025-12-23 09:28:23 +01:00
Brian Clozel fc1ff88ede Avoid duplicate flushes in String Http converter
Prior to this commit, the `StringHttpMessageConverter` would perform a
flush after writing the body, via `StreamUtils#copy`. This operation is
not needed as a flush is already performed by the abstract class.

Flush calls by HTTP message converters is being reconsidered altogether
in gh-35427, but we should first remove this extra operation.

Closes gh-36065
2025-12-22 15:34:41 +01:00
Juergen Hoeller fc2e1dd2e5 Avoid redirection for JUnit javadoc link 2025-12-21 22:07:14 +01:00
Juergen Hoeller ffd2e03a9f Upgrade to Log4J 2.25.3, Protobuf 4.33.2, Selenium 4.39 2025-12-21 22:01:11 +01:00
Juergen Hoeller fd6e15bb4d Align execute(TaskCallback) declaration with return value nullability
See gh-36057
2025-12-21 21:44:10 +01:00
Juergen Hoeller 85c6fb0fd0 Add invoke(Runnable) as variant of invoke(Supplier)
See gh-36052
2025-12-21 21:40:50 +01:00
Juergen Hoeller 0b2bb7e751 Declare TaskCallback return value as potentially nullable
Closes gh-36057
2025-12-20 15:21:14 +01:00
Juergen Hoeller faa17abdae Introduce invoke(Supplier) with last original RuntimeException propagated
Closes gh-36052
2025-12-20 15:20:58 +01:00
Brian Clozel 89ca8e6976 Document how to discard response body with WebTestClient
This commit better documents the ways to discard the response body with
`WebTestClient`.

Closes gh-35953
2025-12-19 16:48:57 +01:00
Brian Clozel 7353ab41d2 Fix memory leak in WiretapConnector
Prior to this commit, we found in gh-35953 that using the `WebTestClient`
the following way leaks data buffers:

```
var body = client.get().uri("download")
  .exchange()
  .expectStatus().isOk()
  .returnResult()
  .getResponseBodyContent();
```

Here, the test performs expectations on the response status and headers,
but not on the response body. The WiretapConnector already supports this
case by subscribing to the Flux response body in those cases and
accumulating the entire content as a single byte[].

Here, the `DataBuffer` instances are not decoded by any `Decoder` and
are not released. This results in a memory leak.

This commit ensures that the automatic subscription in
`WiretapConnector` also releases the buffers automatically as the DSL
does not allow at that point to go back to performing body expectations.

Fixes gh-36050
2025-12-19 16:10:33 +01:00
Stéphane Nicoll f1db0ef036 Do not display error location when unknown
Closes gh-36041
2025-12-17 14:33:14 +01:00
Stéphane Nicoll e9a4b93477 Expose compiler warnings in CompilationException
This commit improves TestCompiler to expose both errors and warnings
instead of an opaque message. When compilation fails, both errors and
warnings are displayed.

This is particularly useful when combined with the `-Werror` option
that turns the presence of a warning into an error.

Closes gh-36037
2025-12-17 14:33:14 +01:00
Juergen Hoeller 86e89d53a9 Do not attempt nested PropertyHandler resolution for argument conversion
Includes fix for return type declaration in PropertyAccessor subclasses.

See gh-36024
2025-12-17 14:30:44 +01:00
Juergen Hoeller c813577908 Consistently use DefaultParameterNameDiscoverer.getSharedInstance()
This includes MethodParameter resolving getParameterName() by default now.
initParameterNameDiscovery(null) can be used to suppress such resolution.

Closes gh-36024
2025-12-17 13:39:02 +01:00
Brian Clozel ec6b7730a2 Polishing contribution
Closes gh-36032
2025-12-17 12:35:30 +01:00
jher235 fc29d88778 Use StringBuilder in JdbcTemplate for batch updates
See gh-36032

Signed-off-by: jher235 <tim668666@gmail.com>
2025-12-17 12:35:30 +01:00
Brian Clozel 033df68da6 Fix Javadoc links 2025-12-17 10:21:30 +01:00
Brian Clozel df5b06dabc Fix RfcUriParser parsing for single char fragments
Prior to this commit, the `RfcUriParser` would ignore URI fragments if
their length is < 2. This commit fixes the length check to allow for
single char fragments when parsing URIs.

Fixes gh-36029
2025-12-17 09:51:53 +01:00
Juergen Hoeller 3d8d7ffa1e Upgrade to AspectJ 1.9.25, Tomcat 11.0.15, Netty 4.2.9, Jackson 3.0.3, EclipseLink 5.0.0-B13, Mockito 5.21, Checkstyle 12.3 2025-12-16 13:55:36 +01:00
Juergen Hoeller 6d5a512d90 Suppress serial warning 2025-12-16 13:54:40 +01:00
Sébastien Deleuze d4e4250bb1 Remove JavaScript/Kotlin from ScriptEngine documentation
Closes gh-27919
2025-12-15 18:43:09 +01:00
Sam Brannen 93bc022a46 Extract CopyPropertiesTests as nested test class in BeanUtilsTests 2025-12-15 18:11:35 +01:00
Sam Brannen a4c72c8fcf Polish integration tests 2025-12-15 15:05:05 +01:00
Juergen Hoeller 3d7862abb6 Upgrade to Hibernate ORM 7.2.0.Final 2025-12-15 14:27:39 +01:00
Juergen Hoeller 7fd0c5041d Use default ConstraintValidatorFactory for provider-internal validators
Closes gh-36012
2025-12-15 14:26:58 +01:00
Juergen Hoeller d99ace81c1 Enforce publicly accessible method for externalClassLoader(ClassLoader)
Closes gh-36012
2025-12-14 14:48:33 +01:00
Sam Brannen ed451c107f Polishing 2025-12-13 17:11:05 +01:00
Sam Brannen 452257eb96 Revise contribution
See gh-36022
2025-12-13 17:10:06 +01:00
Tran Ngoc Nhan a7863a0877 Fix typos and grammar in reference manual
Closes gh-36022

Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2025-12-13 16:50:39 +01:00
Sam Brannen 1da0e13a2b Polishing 2025-12-12 15:41:23 +01:00
Stéphane Nicoll ee6a156275 Refresh GitHub Actions 2025-12-12 11:51:11 +01:00
Brian Clozel 0b6fe8dcc5 Next development version (v7.0.3-SNAPSHOT) 2025-12-11 17:34:57 +01:00
614 changed files with 15161 additions and 5109 deletions
@@ -1,6 +1,13 @@
changelog:
repository: spring-projects/spring-framework
sections:
- title: ":warning: Attention Required"
labels:
- "for: upgrade-attention"
summary:
mode: "member-comment"
config:
prefix: "Attention Required:"
- title: ":star: New Features"
labels:
- "type: enhancement"
@@ -24,7 +24,7 @@ runs:
using: composite
steps:
- name: Set Up Java
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: ${{ inputs.java-early-access == 'true' && 'temurin' || (inputs.java-distribution || 'liberica') }}
java-version: |
@@ -32,7 +32,7 @@ runs:
${{ inputs.java-toolchain == 'true' && '17' || '' }}
25
- name: Set Up Gradle
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
cache-read-only: false
develocity-access-key: ${{ inputs.develocity-access-key }}
@@ -17,14 +17,14 @@ runs:
using: composite
steps:
- name: Set Up JFrog CLI
uses: jfrog/setup-jfrog-cli@f748a0599171a192a2668afee8d0497f7c1069df # v4.5.6
uses: jfrog/setup-jfrog-cli@5b06f730cc5a6f55d78b30753f8583454b08c0aa # v4.8.1
env:
JF_ENV_SPRING: ${{ inputs.jfrog-cli-config-token }}
- name: Download Release Artifacts
shell: bash
run: jf rt download --spec ${{ format('{0}/artifacts.spec', github.action_path) }} --spec-vars 'buildName=${{ format('spring-framework-{0}', inputs.spring-framework-version) }};buildNumber=${{ github.run_number }}'
- name: Sync
uses: spring-io/central-publish-action@0cdd90d12e6876341e82860d951e1bcddc1e51b6 # v0.2.0
uses: spring-io/central-publish-action@0c03960e9b16fdfe70e2443e1d5393cbc3a35622 # v0.3.0
with:
token: ${{ inputs.central-token-password }}
token-name: ${{ inputs.central-token-username }}
+2 -2
View File
@@ -18,9 +18,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up Java
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: 'liberica'
java-version: 17
@@ -13,7 +13,7 @@ jobs:
timeout-minutes: 60
steps:
- name: Check Out Code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Build and Publish
id: build-and-publish
uses: ./.github/actions/build
@@ -21,7 +21,7 @@ jobs:
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
publish: true
- name: Deploy
uses: spring-io/artifactory-deploy-action@dc1913008c0599f0c4b1fdafb6ff3c502b3565ea # v0.0.2
uses: spring-io/artifactory-deploy-action@926d7f7cc810569395346bf3a4d91b380b3e355b # v0.0.4
with:
artifact-properties: |
/**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false
+2 -2
View File
@@ -10,7 +10,7 @@ jobs:
timeout-minutes: 60
steps:
- name: Check Out Code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Build
id: build
uses: ./.github/actions/build
@@ -19,7 +19,7 @@ jobs:
uses: ./.github/actions/print-jvm-thread-dumps
- name: Upload Build Reports
if: failure()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: build-reports
path: '**/build/reports/'
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
git config --global core.longPaths true
Stop-Service -name Docker
- name: Check Out Code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Build
id: build
uses: ./.github/actions/build
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 1
ref: docs-build
+5 -5
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Build and Publish
id: build-and-publish
uses: ./.github/actions/build
@@ -21,7 +21,7 @@ jobs:
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
publish: true
- name: Stage Release
uses: spring-io/artifactory-deploy-action@26bbe925a75f4f863e1e529e85be2d0093cac116 # v0.0.1
uses: spring-io/artifactory-deploy-action@926d7f7cc810569395346bf3a4d91b380b3e355b # v0.0.4
with:
artifact-properties: |
/**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false
@@ -57,7 +57,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
uses: actions/checkout@v6
- name: Sync to Maven Central
uses: ./.github/actions/sync-to-maven-central
with:
@@ -73,7 +73,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Set up JFrog CLI
uses: jfrog/setup-jfrog-cli@dff217c085c17666e8849ebdbf29c8fe5e3995e6 # v4.5.2
uses: jfrog/setup-jfrog-cli@5b06f730cc5a6f55d78b30753f8583454b08c0aa # v4.8.1
env:
JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }}
- name: Promote build
@@ -86,7 +86,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
uses: actions/checkout@v6
- name: Create GitHub Release
uses: ./.github/actions/create-github-release
with:
+5 -5
View File
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Build and Publish
id: build-and-publish
uses: ./.github/actions/build
@@ -20,7 +20,7 @@ jobs:
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
publish: true
- name: Stage Release
uses: spring-io/artifactory-deploy-action@dc1913008c0599f0c4b1fdafb6ff3c502b3565ea # v0.0.2
uses: spring-io/artifactory-deploy-action@926d7f7cc810569395346bf3a4d91b380b3e355b # v0.0.4
with:
artifact-properties: |
/**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false
@@ -56,7 +56,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
uses: actions/checkout@v6
- name: Sync to Maven Central
uses: ./.github/actions/sync-to-maven-central
with:
@@ -72,7 +72,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Set up JFrog CLI
uses: jfrog/setup-jfrog-cli@dff217c085c17666e8849ebdbf29c8fe5e3995e6 # v4.5.2
uses: jfrog/setup-jfrog-cli@5b06f730cc5a6f55d78b30753f8583454b08c0aa # v4.8.1
env:
JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }}
- name: Promote build
@@ -85,7 +85,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
uses: actions/checkout@v6
- name: Create GitHub Release
uses: ./.github/actions/create-github-release
with:
+5 -5
View File
@@ -30,23 +30,23 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check Out Release Verification Tests
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
ref: 'v0.0.2'
repository: spring-projects/spring-framework-release-verification
token: ${{ secrets.token }}
- name: Check Out Send Notification Action
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
path: send-notification
sparse-checkout: .github/actions/send-notification
- name: Set Up Java
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: 'liberica'
java-version: 17
- name: Set Up Gradle
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
cache-read-only: false
- name: Configure Gradle Properties
@@ -64,7 +64,7 @@ jobs:
run: ./gradlew spring-framework-release-verification-tests:test
- name: Upload Build Reports on Failure
if: failure()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: build-reports
path: '**/build/reports/'
+6 -5
View File
@@ -6,7 +6,7 @@ plugins {
id 'com.github.bjornvester.xjc' version '1.8.2' apply false
id 'com.gradleup.shadow' version "9.2.2" apply false
id 'me.champeau.jmh' version '0.7.2' apply false
id 'io.spring.nullability' version '0.0.8' apply false
id 'io.spring.nullability' version '0.0.11' apply false
}
ext {
@@ -70,13 +70,14 @@ 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/5.6/javadocs/",
"https://docs.hibernate.org/orm/7.2/javadocs/",
"https://www.quartz-scheduler.org/api/2.3.0/",
"https://hc.apache.org/httpcomponents-client-5.5.x/current/httpclient5/apidocs/",
"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.1/api/",
"https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/",
"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/"
] 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("12.2.0");
checkstyle.setToolVersion("13.2.0");
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
@@ -49,7 +49,6 @@ import static org.springframework.build.architecture.ArchitectureRules.classesSh
import static org.springframework.build.architecture.ArchitectureRules.javaClassesShouldNotImportKotlinAnnotations;
import static org.springframework.build.architecture.ArchitectureRules.noClassesShouldCallStringToLowerCaseWithoutLocale;
import static org.springframework.build.architecture.ArchitectureRules.noClassesShouldCallStringToUpperCaseWithoutLocale;
import static org.springframework.build.architecture.ArchitectureRules.packageInfoShouldBeNullMarked;
/**
* {@link Task} that checks for architecture problems.
@@ -64,8 +63,7 @@ public abstract class ArchitectureCheck extends DefaultTask {
public ArchitectureCheck() {
getOutputDirectory().convention(getProject().getLayout().getBuildDirectory().dir(getName()));
getProhibitObjectsRequireNonNull().convention(true);
getRules().addAll(packageInfoShouldBeNullMarked(),
classesShouldNotImportForbiddenTypes(),
getRules().addAll(classesShouldNotImportForbiddenTypes(),
javaClassesShouldNotImportKotlinAnnotations(),
allPackagesShouldBeFreeOfTangles(),
noClassesShouldCallStringToLowerCaseWithoutLocale(),
@@ -46,13 +46,6 @@ abstract class ArchitectureRules {
.because("String.toUpperCase(Locale.ROOT) should be used instead");
}
static ArchRule packageInfoShouldBeNullMarked() {
return ArchRuleDefinition.classes()
.that().haveSimpleName("package-info")
.should().beAnnotatedWith("org.jspecify.annotations.NullMarked")
.allowEmptyShould(true);
}
static ArchRule classesShouldNotImportForbiddenTypes() {
return ArchRuleDefinition.noClasses()
.should().dependOnClassesThat()
@@ -78,7 +78,7 @@ public class ShadowSource extends DefaultTask {
}
@OutputDirectory
DirectoryProperty getOutputDirectory() {
public DirectoryProperty getOutputDirectory() {
return this.outputDirectory;
}
+2 -1
View File
@@ -20,6 +20,7 @@ dependencies {
}
}
def springAspectsOutput = project(":spring-aspects").sourceSets.main.output
javadoc {
javadocTool.set(javaToolchains.javadocToolFor({
languageVersion = JavaLanguageVersion.of(25)
@@ -49,7 +50,7 @@ javadoc {
doFirst {
classpath += files(
// ensure the javadoc process can resolve types compiled from .aj sources
project(":spring-aspects").sourceSets.main.output
springAspectsOutput
)
classpath += files(moduleProjects.collect { it.sourceSets.main.compileClasspath })
}
+5 -3
View File
@@ -70,7 +70,7 @@ dependencies {
implementation("com.github.ben-manes.caffeine:caffeine")
implementation("com.mchange:c3p0:0.9.5.5")
implementation("com.oracle.database.jdbc:ojdbc11")
implementation("io.micrometer:context-propagation")
implementation("io.micrometer:context-propagation")
implementation("io.projectreactor.netty:reactor-netty-http")
implementation("jakarta.jms:jakarta.jms-api")
implementation("jakarta.servlet:jakarta.servlet-api")
@@ -80,12 +80,14 @@ dependencies {
implementation("javax.cache:cache-api")
implementation("org.apache.activemq:activemq-ra:6.1.2")
implementation("org.apache.commons:commons-dbcp2:2.11.0")
implementation("org.apache.groovy:groovy-templates")
implementation("org.aspectj:aspectjweaver")
implementation("org.assertj:assertj-core")
implementation("org.eclipse.jetty.websocket:jetty-websocket-jetty-api")
implementation("org.freemarker:freemarker")
implementation("org.jetbrains.kotlin:kotlin-stdlib")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
implementation("org.junit.jupiter:junit-jupiter-api")
implementation("tools.jackson.core:jackson-databind")
implementation("tools.jackson.dataformat:jackson-dataformat-xml")
Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 158 KiB

@@ -17,6 +17,7 @@
class="st5"
id="svg5499"
version="1.1"
font-family="Helvetica, Arial, sans-serif"
inkscape:version="0.91 r13725"
sodipodi:docname="mvc-splitted-contexts.svg"
style="font-size:12px;overflow:visible;color-interpolation-filters:sRGB;fill:none;fill-rule:evenodd;stroke-linecap:square;stroke-miterlimit:3"
@@ -36,7 +37,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
id="path9164" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -47,7 +48,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
id="path8836" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -58,7 +59,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
id="path8520" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -69,7 +70,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
id="path8216" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -80,7 +81,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
id="path7924" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -91,7 +92,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
id="path7644" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -102,7 +103,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
id="path7375" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -113,7 +114,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
id="path7119" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -124,7 +125,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
id="path6875" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -135,7 +136,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
id="path6643" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -146,7 +147,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
id="path6423" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -157,7 +158,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
id="path6215" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -168,7 +169,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
id="path6019" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -179,7 +180,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
id="path5835" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -190,7 +191,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
id="path5663" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -201,7 +202,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
id="path5503" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -212,7 +213,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
id="path5355" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -223,7 +224,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
id="path5219" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -234,7 +235,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
id="path5095" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -245,7 +246,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
id="path4983" /></marker><marker
inkscape:stockid="Arrow2Mend"
orient="auto"
@@ -256,7 +257,7 @@
inkscape:isstock="true"
inkscape:collect="always"><path
id="path7394"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
transform="scale(0.6) rotate(180) translate(0,0)" /></marker><marker
inkscape:stockid="Arrow2Lend"
@@ -267,7 +268,7 @@
style="overflow:visible;"
inkscape:isstock="true"><path
id="path8123"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
transform="scale(1.1) rotate(180) translate(1,0)" /></marker><marker
inkscape:stockid="Arrow1Mend"
@@ -279,7 +280,7 @@
inkscape:isstock="true"><path
id="path8035"
d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "
style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1"
style="fill-rule:evenodd;stroke:#333333;stroke-width:1pt;stroke-opacity:1;fill:#333333;fill-opacity:1"
transform="scale(0.4) rotate(180) translate(10,0)" /></marker><marker
inkscape:stockid="Arrow1Mend"
orient="auto"
@@ -290,7 +291,7 @@
inkscape:isstock="true"><path
id="path7959"
d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "
style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1"
style="fill-rule:evenodd;stroke:#333333;stroke-width:1pt;stroke-opacity:1;fill:#333333;fill-opacity:1"
transform="scale(0.4) rotate(180) translate(10,0)" /></marker><marker
inkscape:stockid="Arrow1Mend"
orient="auto"
@@ -301,7 +302,7 @@
inkscape:isstock="true"><path
id="path7376"
d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "
style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1"
style="fill-rule:evenodd;stroke:#333333;stroke-width:1pt;stroke-opacity:1;fill:#333333;fill-opacity:1"
transform="scale(0.4) rotate(180) translate(10,0)" /></marker><marker
inkscape:stockid="Arrow2Lend"
orient="auto"
@@ -311,7 +312,7 @@
style="overflow:visible;"
inkscape:isstock="true"><path
id="path7388"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
transform="scale(1.1) rotate(180) translate(1,0)" /></marker><marker
inkscape:stockid="Arrow1Lend"
@@ -323,7 +324,7 @@
inkscape:isstock="true"><path
id="path7370"
d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "
style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1"
style="fill-rule:evenodd;stroke:#333333;stroke-width:1pt;stroke-opacity:1;fill:#333333;fill-opacity:1"
transform="scale(0.8) rotate(180) translate(12.5,0)" /></marker></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
@@ -352,9 +353,9 @@
v:viewMarkup="false" /><style
type="text/css"
id="style5501"><![CDATA[
.st1 {fill:#969696;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-width:0.24}
.st2 {fill:#dde2cd;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-width:0.24}
.st3 {fill:#000000;font-family:Arial;font-size:2.50001em;font-weight:bold}
.st1 {fill:#969696;stroke:#333333;stroke-linecap:round;stroke-linejoin:round;stroke-width:0.24}
.st2 {fill:#F8F9FA;stroke:#333333;stroke-linecap:round;stroke-linejoin:round;stroke-width:0.24}
.st3 {fill:#333333;font-family:Helvetica, Arial, sans-serif;font-size:2.50001em;font-weight:bold}
.st4 {font-size:0.333333em;font-weight:normal}
.st5 {fill:none;fill-rule:evenodd;font-size:12;overflow:visible;stroke-linecap:square;stroke-miterlimit:3}
]]></style><g
@@ -363,20 +364,22 @@
v:groupContext="foregroundPage"
id="g5503"
transform="matrix(0.99998201,0,0,1.0824094,-40.812382,-98.908648)"><rect
style="fill:#dde2cd;fill-opacity:1;stroke:#000000;stroke-width:1.53790233;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
style="fill:#F8F9FA;fill-opacity:1;stroke:#6db33f;stroke-width:2;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
id="rect6599"
width="382.68423"
height="146.09897"
x="87.884865"
y="148.26482" /><v:userDefs><v:ud
y="148.26482"
rx="8" /><v:userDefs><v:ud
v:nameU="SchemeName"
v:val="VT4(Default)" /></v:userDefs><rect
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1.53790233;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
style="fill:none;fill-opacity:1;stroke:#6db33f;stroke-width:2;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
id="rect5725"
width="464.31128"
height="374.11411"
x="41.684383"
y="112.3262" /><title
y="112.3262"
rx="8" /><title
id="title5505">Page-1</title><v:pageProperties
v:drawingScale="0.0393701"
v:pageScale="0.0393701"
@@ -385,15 +388,16 @@
v:shadowOffsetY="-8.50394" /><v:layer
v:name="Connector"
v:index="0" /><rect
style="fill:#dde2cd;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.53790233;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
style="fill:#F8F9FA;fill-opacity:1;fill-rule:evenodd;stroke:#6db33f;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
id="rect5715"
width="322.8194"
height="43.63184"
x="119.95335"
y="-135.66222"
transform="scale(1,-1)" /><text
transform="scale(1,-1)"
rx="8" /><text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:30.7580471px;line-height:125%;font-family:sans-serif;-inkscape-font-specification:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:30.7580471px;line-height:125%;font-family:Helvetica, Arial, sans-serif;-inkscape-font-specification:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#333333;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="168.843"
y="124.32391"
id="text5717"
@@ -401,12 +405,12 @@
transform="scale(1.0403984,0.96117025)"><tspan
sodipodi:role="line"
id="tspan5719"
x="168.843"
x="172.843"
y="124.32391"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:23.06853676px;font-family:sans-serif;-inkscape-font-specification:sans-serif">DispatcherServlet</tspan></text>
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:23.06853676px;font-family:Helvetica, Arial, sans-serif;-inkscape-font-specification:sans-serif;fill:#333333">DispatcherServlet</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Helvetica, Arial, sans-serif;letter-spacing:0px;word-spacing:0px;fill:#333333;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="89.770851"
y="181.20923"
id="text6589"
@@ -414,12 +418,12 @@
transform="scale(1.0403984,0.96117025)"><tspan
sodipodi:role="line"
id="tspan6591"
x="89.770851"
x="106.770851"
y="181.20923"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:23.06853676px;font-family:sans-serif;-inkscape-font-specification:sans-serif">Servlet WebApplicationContext</tspan></text>
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:23.06853676px;font-family:Helvetica, Arial, sans-serif;-inkscape-font-specification:sans-serif;fill:#333333">Servlet WebApplicationContext</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Helvetica, Arial, sans-serif;letter-spacing:0px;word-spacing:0px;fill:#333333;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="260.00443"
y="198.41273"
id="text6593"
@@ -429,36 +433,39 @@
id="tspan6595"
x="260.00443"
y="198.41273"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:11.53426838px;font-family:sans-serif;-inkscape-font-specification:sans-serif;text-align:center;text-anchor:middle">(containing controllers, view resolvers,</tspan><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:11.53426838px;font-family:Helvetica, Arial, sans-serif;-inkscape-font-specification:sans-serif;text-align:center;text-anchor:middle;fill:#555555">(containing controllers, view resolvers,</tspan><tspan
sodipodi:role="line"
x="260.00443"
y="212.83057"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:11.53426838px;font-family:sans-serif;-inkscape-font-specification:sans-serif;text-align:center;text-anchor:middle"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:11.53426838px;font-family:Helvetica, Arial, sans-serif;-inkscape-font-specification:sans-serif;text-align:center;text-anchor:middle;fill:#555555"
id="tspan6597">and other web-related beans)</tspan></text>
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.86203903;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
style="fill:#ffffff;fill-opacity:1;stroke:#6db33f;stroke-width:1.5;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
id="rect6620"
width="82.040657"
height="36.72575"
x="114.52653"
y="-259.43161"
transform="scale(1,-1)" /><rect
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.89166164;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
transform="scale(1,-1)"
rx="4" /><rect
style="fill:#ffffff;fill-opacity:1;stroke:#6db33f;stroke-width:1.5;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
id="rect6622"
width="87.843979"
height="36.697304"
x="223.39864"
y="-287.19809"
transform="scale(1,-1)" /><rect
transform="scale(1,-1)"
rx="4" /><rect
transform="scale(1,-1)"
y="-264.81918"
x="117.92834"
height="36.72575"
width="82.040657"
id="rect6614"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.86203903;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1" /><text
style="fill:#ffffff;fill-opacity:1;stroke:#6db33f;stroke-width:1.5;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
rx="4" /><text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Helvetica, Arial, sans-serif;letter-spacing:0px;word-spacing:0px;fill:#333333;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="121.24728"
y="260.14957"
id="text6616"
@@ -466,20 +473,20 @@
transform="scale(1.0403984,0.96117025)"><tspan
sodipodi:role="line"
id="tspan6618"
x="121.24728"
x="124.24728"
y="260.14957"
style="font-size:11.53426838px">Controllers</tspan></text>
style="font-size:11.53426838px;fill:#333333">Controllers</tspan></text>
<text
transform="scale(1.0403984,0.96117025)"
sodipodi:linespacing="125%"
id="text6624"
y="282.70709"
x="219.61203"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Helvetica, Arial, sans-serif;letter-spacing:0px;word-spacing:0px;fill:#333333;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-size:11.53426838px"
style="font-size:11.53426838px;fill:#333333"
y="282.70709"
x="219.61203"
x="221.61203"
id="tspan6626"
sodipodi:role="line">ViewResolver</tspan></text>
<rect
@@ -489,9 +496,10 @@
height="36.577778"
width="114.4539"
id="rect6628"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.0161339;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1" /><text
style="fill:#ffffff;fill-opacity:1;stroke:#6db33f;stroke-width:1.5;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
rx="4" /><text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Helvetica, Arial, sans-serif;letter-spacing:0px;word-spacing:0px;fill:#333333;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="327.51276"
y="255.28464"
id="text6630"
@@ -499,26 +507,27 @@
transform="scale(1.0403984,0.96117025)"><tspan
sodipodi:role="line"
id="tspan6632"
x="327.51276"
x="332.51276"
y="255.28464"
style="font-size:11.53426838px">HandlerMapping</tspan></text>
style="font-size:11.53426838px;fill:#333333">HandlerMapping</tspan></text>
<rect
y="338.69724"
x="87.803261"
height="121.5683"
width="382.84744"
id="rect6634"
style="fill:#dde2cd;fill-opacity:1;stroke:#000000;stroke-width:1.53790233;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1" /><text
style="fill:#F8F9FA;fill-opacity:1;stroke:#6db33f;stroke-width:2;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
rx="8" /><text
transform="scale(1.0403984,0.96117025)"
sodipodi:linespacing="125%"
id="text6636"
y="376.61673"
x="108.61351"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Helvetica, Arial, sans-serif;letter-spacing:0px;word-spacing:0px;fill:#333333;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:23.06853676px;font-family:sans-serif;-inkscape-font-specification:sans-serif"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:23.06853676px;font-family:Helvetica, Arial, sans-serif;-inkscape-font-specification:sans-serif;fill:#333333"
y="376.61673"
x="108.61351"
x="113.61351"
id="tspan6638"
sodipodi:role="line">Root WebApplicationContext</tspan></text>
<text
@@ -527,10 +536,10 @@
id="text6640"
y="395.35812"
x="260.93863"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Helvetica, Arial, sans-serif;letter-spacing:0px;word-spacing:0px;fill:#333333;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
id="tspan6644"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:11.53426838px;font-family:sans-serif;-inkscape-font-specification:sans-serif;text-align:center;text-anchor:middle"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:11.53426838px;font-family:Helvetica, Arial, sans-serif;-inkscape-font-specification:sans-serif;text-align:center;text-anchor:middle;fill:#555555"
y="395.35812"
x="260.93863"
sodipodi:role="line">(containing middle-tier services, datasources, etc.)</tspan></text>
@@ -541,43 +550,47 @@
height="36.72575"
width="82.040657"
id="rect6648"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.86203903;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1" /><rect
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.86203903;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
style="fill:#ffffff;fill-opacity:1;stroke:#6db33f;stroke-width:1.5;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
rx="4" /><rect
style="fill:#ffffff;fill-opacity:1;stroke:#6db33f;stroke-width:1.5;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
id="rect6650"
width="82.040657"
height="36.72575"
x="167.73116"
y="-445.45563"
transform="scale(1,-1)" /><text
transform="scale(1,-1)"
rx="4" /><text
transform="scale(1.0403984,0.96117025)"
sodipodi:linespacing="125%"
id="text6652"
y="448.55054"
x="175.87148"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Helvetica, Arial, sans-serif;letter-spacing:0px;word-spacing:0px;fill:#333333;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-size:11.53426838px"
style="font-size:11.53426838px;fill:#333333"
y="448.55054"
x="175.87148"
x="178.87148"
id="tspan6654"
sodipodi:role="line">Services</tspan></text>
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.88435173;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
style="fill:#ffffff;fill-opacity:1;stroke:#6db33f;stroke-width:1.5;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
id="rect6656"
width="86.393044"
height="36.704323"
x="306.86328"
y="-439.60837"
transform="scale(1,-1)" /><rect
transform="scale(1,-1)"
rx="4" /><rect
transform="scale(1,-1)"
y="-444.99475"
x="310.26624"
height="36.701977"
width="86.876686"
id="rect6658"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.88679528;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1" /><text
style="fill:#ffffff;fill-opacity:1;stroke:#6db33f;stroke-width:1.5;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
rx="4" /><text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Helvetica, Arial, sans-serif;letter-spacing:0px;word-spacing:0px;fill:#333333;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="305.30771"
y="448.55054"
id="text6660"
@@ -585,20 +598,20 @@
transform="scale(1.0403984,0.96117025)"><tspan
sodipodi:role="line"
id="tspan6662"
x="305.30771"
x="308.30771"
y="448.55054"
style="font-size:11.53426838px">Repositories</tspan></text>
style="font-size:11.53426838px;fill:#333333">Repositories</tspan></text>
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.76895118px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="fill:none;fill-rule:evenodd;stroke:#333333;stroke-width:0.76895118px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 265.33234,295.60379 c 0,42.65169 0,42.65169 0,0 z"
id="path7643"
inkscape:connector-curvature="0" /><path
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:3.46028023;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow2Mend)"
style="fill:#333333;fill-opacity:1;fill-rule:evenodd;stroke:#333333;stroke-width:3.46028023;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow2Mend)"
d="m 270.43505,294.70585 c 0,39.4721 0,39.87903 0,39.87903"
id="path7645"
inkscape:connector-curvature="0" /><text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Helvetica, Arial, sans-serif;letter-spacing:0px;word-spacing:0px;fill:#333333;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="352.55331"
y="333.03622"
id="text4963"
@@ -607,6 +620,6 @@
sodipodi:role="line"
x="352.55331"
y="333.03622"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:11.53426838px;font-family:sans-serif;-inkscape-font-specification:sans-serif;text-align:center;text-anchor:middle"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:11.53426838px;font-family:Helvetica, Arial, sans-serif;-inkscape-font-specification:sans-serif;text-align:center;text-anchor:middle;fill:#555555"
id="tspan4965">Delegates if no bean found</tspan></text>
</g></svg>

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 31 KiB

+1 -1
View File
@@ -261,7 +261,6 @@
**** xref:web/websocket/stomp/configuration-performance.adoc[]
**** xref:web/websocket/stomp/stats.adoc[]
**** xref:web/websocket/stomp/testing.adoc[]
** xref:web/integration.adoc[]
* xref:web-reactive.adoc[]
** xref:web/webflux.adoc[]
*** xref:web/webflux/new-framework.adoc[]
@@ -344,6 +343,7 @@
**** xref:testing/testcontext-framework/ctx-management/web.adoc[]
**** xref:testing/testcontext-framework/ctx-management/web-mocks.adoc[]
**** xref:testing/testcontext-framework/ctx-management/caching.adoc[]
**** xref:testing/testcontext-framework/ctx-management/context-pausing.adoc[]
**** xref:testing/testcontext-framework/ctx-management/failure-threshold.adoc[]
**** xref:testing/testcontext-framework/ctx-management/hierarchies.adoc[]
*** xref:testing/testcontext-framework/fixture-di.adoc[]
@@ -124,6 +124,10 @@ on a test class. See xref:testing/annotations/integration-junit-jupiter.adoc#int
| The maximum size of the context cache in the _Spring TestContext Framework_. See
xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching].
| `spring.test.context.cache.pause`
| The pause mode for the context cache in the _Spring TestContext Framework_. See
xref:testing/testcontext-framework/ctx-management/context-pausing.adoc[Context Pausing].
| `spring.test.context.failure.threshold`
| The failure threshold for errors encountered while attempting to load an `ApplicationContext`
in the _Spring TestContext Framework_. See
@@ -104,7 +104,7 @@ Note that pointcut definitions are generally matched against any intercepted met
If a pointcut is strictly meant to be public-only, even in a CGLIB proxy scenario with
potential non-public interactions through proxies, it needs to be defined accordingly.
If your interception needs include method calls or even constructors within the target
If your interception needs to include method calls or even constructors within the target
class, consider the use of Spring-driven xref:core/aop/using-aspectj.adoc#aop-aj-ltw[native AspectJ weaving] instead
of Spring's proxy-based AOP framework. This constitutes a different mode of AOP usage
with different characteristics, so be sure to make yourself familiar with weaving
@@ -19,10 +19,6 @@ its behavior changes.
| Initialization of `SmartInitializingSingleton` beans.
| `beanName` the name of the bean.
| `spring.context.annotated-bean-reader.create`
| Creation of the `AnnotatedBeanDefinitionReader`.
|
| `spring.context.base-packages.scan`
| Scanning of base packages.
| `packages` array of base packages for scanning.
@@ -1,16 +1,17 @@
[[beans-standard-annotations]]
= Using JSR 330 Standard Annotations
= Using JSR-330 Standard Annotations
Spring offers support for JSR-330 standard annotations (Dependency Injection). Those
annotations are scanned in the same way as the Spring annotations. To use them, you need
to have the relevant jars in your classpath.
Spring offers support for JSR-330 standard _Dependency Injection_ annotations which are
available in the `jakarta.inject` package. These annotations may optionally be used as
alternatives to Spring annotations.
To use them, you need to have the relevant jar in your classpath. For example, the
`jakarta.inject` artifact is available in the standard Maven repository
(`https://repo.maven.apache.org/maven2/jakarta/inject/jakarta.inject-api/2.0.0/`),
[NOTE]
=====
If you use Maven, the `jakarta.inject` artifact is available in the standard Maven
repository (
https://repo.maven.apache.org/maven2/jakarta/inject/jakarta.inject-api/2.0.0/[https://repo.maven.apache.org/maven2/jakarta/inject/jakarta.inject-api/2.0.0/]).
You can add the following dependency to your file pom.xml:
If you use Maven, you can add the following dependency to your `pom.xml` file.
[source,xml,indent=0,subs="verbatim,quotes"]
----
@@ -26,13 +27,14 @@ You can add the following dependency to your file pom.xml:
[[beans-inject-named]]
== Dependency Injection with `@Inject` and `@Named`
Instead of `@Autowired`, you can use `@jakarta.inject.Inject` as follows:
Instead of using `@Autowired` for dependency injection, you may optionally choose to use
`@jakarta.inject.Inject` as follows.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",fold="-imports"]
----
import jakarta.inject.Inject;
@@ -54,7 +56,7 @@ Java::
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
[source,kotlin,indent=0,subs="verbatim,quotes",fold="-imports"]
----
import jakarta.inject.Inject
@@ -72,17 +74,19 @@ Kotlin::
----
======
As with `@Autowired`, you can use `@Inject` at the field level, method level
and constructor-argument level. Furthermore, you may declare your injection point as a
`Provider`, allowing for on-demand access to beans of shorter scopes or lazy access to
other beans through a `Provider.get()` call. The following example offers a variant of the
preceding example:
As with `@Autowired`, you can use `@Inject` at the field level, method level, and
constructor-argument level.
Furthermore, as an alternative to Spring's `ObjectProvider` mechanism, you may choose to
declare your injection point as a `jakarta.inject.Provider`, allowing for on-demand
access to beans of shorter scopes or lazy access to other beans through a
`Provider.get()` call. The following example offers a variant of the preceding example.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",fold="-imports"]
----
import jakarta.inject.Inject;
import jakarta.inject.Provider;
@@ -105,9 +109,10 @@ Java::
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
[source,kotlin,indent=0,subs="verbatim,quotes",fold="-imports"]
----
import jakarta.inject.Inject
import jakarta.inject.Provider
class SimpleMovieLister {
@@ -123,14 +128,15 @@ Kotlin::
----
======
If you would like to use a qualified name for the dependency that should be injected,
you should use the `@Named` annotation, as the following example shows:
If you would like to use a qualified name for the dependency that should be injected, you
may choose to use the `@Named` annotation as an alternative to Spring's `@Qualifier`
support, as the following example shows.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",fold="-imports"]
----
import jakarta.inject.Inject;
import jakarta.inject.Named;
@@ -150,7 +156,7 @@ Java::
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
[source,kotlin,indent=0,subs="verbatim,quotes",fold="-imports"]
----
import jakarta.inject.Inject
import jakarta.inject.Named
@@ -170,12 +176,15 @@ Kotlin::
======
As with `@Autowired`, `@Inject` can also be used with `java.util.Optional` or
`@Nullable`. This is even more applicable here, since `@Inject` does not have
a `required` attribute. The following pair of examples show how to use `@Inject` and
`@Nullable`:
`@Nullable`. This is even more applicable here, since `@Inject` does not have a
`required` attribute. The following examples show how to use `@Inject` with `Optional`,
`@Nullable`, and Kotlin's built-in support for nullable types.
[source,java,indent=0,subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",fold="-imports"]
----
import jakarta.inject.Inject;
import java.util.Optional;
public class SimpleMovieLister {
@Inject
@@ -189,8 +198,11 @@ a `required` attribute. The following pair of examples show how to use `@Inject`
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",fold="-imports"]
----
import jakarta.inject.Inject;
import org.jspecify.annotations.Nullable;
public class SimpleMovieLister {
@Inject
@@ -202,8 +214,10 @@ Java::
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
[source,kotlin,indent=0,subs="verbatim,quotes",fold="-imports"]
----
import jakarta.inject.Inject
class SimpleMovieLister {
@Inject
@@ -214,21 +228,21 @@ Kotlin::
[[beans-named]]
== `@Named` and `@ManagedBean`: Standard Equivalents to the `@Component` Annotation
== `@Named`: Standard Equivalent to the `@Component` Annotation
Instead of `@Component`, you can use `@jakarta.inject.Named` or `jakarta.annotation.ManagedBean`,
as the following example shows:
Instead of `@Component` or other Spring stereotype annotations, you may optionally choose
to use `@jakarta.inject.Named`, as the following example shows.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",fold="-imports"]
----
import jakarta.inject.Inject;
import jakarta.inject.Named;
@Named("movieListener") // @ManagedBean("movieListener") could be used as well
@Named("movieListener")
public class SimpleMovieLister {
private MovieFinder movieFinder;
@@ -244,12 +258,12 @@ Java::
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
[source,kotlin,indent=0,subs="verbatim,quotes",fold="-imports"]
----
import jakarta.inject.Inject
import jakarta.inject.Named
@Named("movieListener") // @ManagedBean("movieListener") could be used as well
@Named("movieListener")
class SimpleMovieLister {
@Inject
@@ -260,14 +274,15 @@ Kotlin::
----
======
It is very common to use `@Component` without specifying a name for the component.
`@Named` can be used in a similar fashion, as the following example shows:
It is very common to use `@Component` or other Spring stereotype annotations without
specifying an explicit name for the component, and `@Named` can be used in a similar
fashion, as the following example shows.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",fold="-imports"]
----
import jakarta.inject.Inject;
import jakarta.inject.Named;
@@ -288,7 +303,7 @@ Java::
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
[source,kotlin,indent=0,subs="verbatim,quotes",fold="-imports"]
----
import jakarta.inject.Inject
import jakarta.inject.Named
@@ -304,14 +319,14 @@ Kotlin::
----
======
When you use `@Named` or `@ManagedBean`, you can use component scanning in the
exact same way as when you use Spring annotations, as the following example shows:
When you use `@Named`, you can use component scanning in the exact same way as when you
use Spring annotations, as the following example shows.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
[source,java,indent=0,subs="verbatim,quotes",fold="-imports"]
----
@Configuration
@ComponentScan(basePackages = "org.example")
@@ -322,7 +337,7 @@ Java::
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
[source,kotlin,indent=0,subs="verbatim,quotes",fold="-imports"]
----
@Configuration
@ComponentScan(basePackages = ["org.example"])
@@ -332,55 +347,106 @@ Kotlin::
----
======
NOTE: In contrast to `@Component`, the JSR-330 `@Named` and the JSR-250 `@ManagedBean`
annotations are not composable. You should use Spring's stereotype model for building
custom component annotations.
NOTE: In contrast to `@Component`, the JSR-330 `@Named` annotation is not composable. You
should use Spring's stereotype model for building custom component annotations.
[TIP]
====
If you work with legacy systems that still use `@javax.inject.Named` or
`@javax.annotation.ManagedBean` for components (note the `javax` package namespace), you
can explicitly configure component scanning to include those annotation types, as shown
in the following example.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",fold="-imports"]
----
@Configuration
@ComponentScan(
basePackages = "org.example",
includeFilters = @Filter({
javax.inject.Named.class,
javax.annotation.ManagedBean.class
})
)
public class AppConfig {
// ...
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",fold="-imports"]
----
@Configuration
@ComponentScan(
basePackages = ["org.example"],
includeFilters = [Filter([
javax.inject.Named::class,
javax.annotation.ManagedBean::class
])]
)
class AppConfig {
// ...
}
----
======
In addition, if you would like for the `value` attributes in `@javax.inject.Named` and
`@javax.annotation.ManagedBean` to be used as component names, you need to override the
`isStereotypeWithNameValue(...)` method in `AnnotationBeanNameGenerator` to add explicit
support for `javax.annotation.ManagedBean` and `javax.inject.Named` and register your
custom `AnnotationBeanNameGenerator` via the `nameGenerator` attribute in
`@ComponentScan`.
====
[[beans-standard-annotations-limitations]]
== Limitations of JSR-330 Standard Annotations
When you work with standard annotations, you should know that some significant
features are not available, as the following table shows:
When you work with JSR-330 standard annotations, you should know that some significant
features are not available, as the following table shows.
[[annotations-comparison]]
.Spring component model elements versus JSR-330 variants
.Spring component model versus JSR-330 variants
|===
| Spring| jakarta.inject.*| jakarta.inject restrictions / comments
| Spring | JSR-330 | JSR-330 restrictions / comments
| @Autowired
| @Inject
| `@Inject` has no 'required' attribute. Can be used with Java 8's `Optional` instead.
| `@Autowired`
| `@Inject`
| `@Inject` has no `required` attribute. Can be used with Java's `Optional` instead.
| @Component
| @Named / @ManagedBean
| `@Component`
| `@Named`
| JSR-330 does not provide a composable model, only a way to identify named components.
| @Scope("singleton")
| @Singleton
| `@Scope("singleton")`
| `@Singleton`
| The JSR-330 default scope is like Spring's `prototype`. However, in order to keep it
consistent with Spring's general defaults, a JSR-330 bean declared in the Spring
container is a `singleton` by default. In order to use a scope other than `singleton`,
you should use Spring's `@Scope` annotation. `jakarta.inject` also provides a
`jakarta.inject.Scope` annotation: however, this one is only intended to be used
`jakarta.inject.Scope` annotation; however, this one is only intended to be used
for creating custom annotations.
| @Qualifier
| @Qualifier / @Named
| `@Qualifier`
| `@Qualifier` / `@Named`
| `jakarta.inject.Qualifier` is just a meta-annotation for building custom qualifiers.
Concrete `String` qualifiers (like Spring's `@Qualifier` with a value) can be associated
through `jakarta.inject.Named`.
| @Value
| `@Value`
| -
| no equivalent
| @Lazy
| `@Lazy`
| -
| no equivalent
| ObjectFactory
| Provider
| `ObjectFactory`
| `Provider`
| `jakarta.inject.Provider` is a direct alternative to Spring's `ObjectFactory`,
only with a shorter `get()` method name. It can also be used in combination with
Spring's `@Autowired` or with non-annotated constructors and setter methods.
@@ -6,7 +6,7 @@ are set by using the `setVariable()` method in `EvaluationContext` implementatio
[NOTE]
====
Variable names must be begin with a letter (as defined below), an underscore, or a dollar
Variable names must begin with a letter (as defined below), an underscore, or a dollar
sign.
Variable names must be composed of one or more of the following supported types of
@@ -15,7 +15,7 @@ configuration options. You should instantiate the `SimpleJdbcInsert` in the data
layer's initialization method. For this example, the initializing method is the
`setDataSource` method. You do not need to subclass the `SimpleJdbcInsert` class. Instead,
you can create a new instance and set the table name by using the `withTableName` method.
Configuration methods for this class follow the `fluid` style that returns the instance
Configuration methods for this class follow the `fluent` style that returns the instance
of the `SimpleJdbcInsert`, which lets you chain all configuration methods. The following
example uses only one configuration method (we show examples of multiple methods later):
@@ -349,11 +349,11 @@ parameters return the data read from the table.
You can declare `SimpleJdbcCall` in a manner similar to declaring `SimpleJdbcInsert`. You
should instantiate and configure the class in the initialization method of your data-access
layer. Compared to the `StoredProcedure` class, you need not create a subclass
and you need not to declare parameters that can be looked up in the database metadata.
The following example of a `SimpleJdbcCall` configuration uses the preceding stored
procedure (the only configuration option, in addition to the `DataSource`, is the name
of the stored procedure):
layer. In contrast to the `StoredProcedure` class, you do not need to create a subclass,
and you do not need to declare parameters that can be looked up in the database metadata.
The following `SimpleJdbcCall` configuration example uses the preceding stored procedure.
The only configuration option (other than the `DataSource`) is the name of the stored
procedure.
[tabs]
======
@@ -14,7 +14,7 @@ As of Spring Framework 7.0, Spring requires Hibernate ORM 7.x for Spring's
`HibernateJpaVendorAdapter`.
The `org.springframework.orm.jpa.hibernate` package supersedes the former `orm.hibernate5`:
now for use with Hibernate ORM 7.1+, tightly integrated with `HibernateJpaVendorAdapter`
now for use with Hibernate ORM 7.x, tightly integrated with `HibernateJpaVendorAdapter`
as well as supporting Hibernate's native `SessionFactory.getCurrentSession()` style.
====
@@ -3,21 +3,22 @@
:page-aliases: integration/class-data-sharing.adoc
:page-aliases: integration/cds.adoc
The ahead-of-time cache is a JVM feature introduced in Java 24 via the
The ahead-of-time cache is a JVM feature introduced in Java 24 via
https://openjdk.org/jeps/483[JEP 483] that can help reduce the startup time and memory
footprint of Java applications. AOT cache is a natural evolution of https://docs.oracle.com/en/java/javase/17/vm/class-data-sharing.html[Class Data Sharing (CDS)].
footprint of Java applications. AOT cache is a natural evolution of
https://docs.oracle.com/en/java/javase/17/vm/class-data-sharing.html[Class Data Sharing (CDS)].
Spring Framework supports both CDS and AOT cache, and it is recommended that you use the
later if available in the JVM version your are using (Java 24+).
latter if available in the JVM version you are using (Java 24+).
To use this feature, an AOT cache should be created for the particular classpath of the
application. It is possible to create this cache on the deployed instance, or during a
training run performed for example when packaging the application thanks to an hook-point
training run performed for example when packaging the application thanks to a hook-point
provided by the Spring Framework to ease such use case. Once the cache is available, users
should opt in to use it via a JVM flag.
NOTE: If you are using Spring Boot, it is highly recommended to leverage its
{spring-boot-docs-ref}/packaging/efficient.html#packaging.efficient.unpacking[executable JAR unpacking support]
which is designed to fulfill the class loading requirements of both AOT cache and CDS.
which is designed to fulfill the class loading requirements of both the AOT cache and CDS.
== Creating the cache
@@ -29,14 +30,22 @@ invoked; but the lifecycle has not started, and the `ContextRefreshedEvent` has
been published.
To create the cache during the training run, it is possible to specify the `-Dspring.context.exit=onRefresh`
JVM flag to start then exit your Spring application once the
JVM flag to start and then exit your Spring application once the
`ApplicationContext` has refreshed:
--
[tabs]
======
AOT cache::
AOT cache (Java 25+)::
+
[source,bash,subs="verbatim,quotes"]
----
java -XX:AOTCacheOutput=app.aot -Dspring.context.exit=onRefresh -jar application.jar ...
----
AOT cache (Java 24)::
+
[source,bash,subs="verbatim,quotes"]
----
@@ -55,6 +64,12 @@ java -XX:ArchiveClassesAtExit=app.jsa -Dspring.context.exit=onRefresh ...
======
--
NOTE: With Java 25+, AOT cache stores, among other things, the
https://openjdk.org/jeps/515[method profiling information]. Therefore, to benefit of this capability,
it is recommended to create an AOT cache for an application that experienced a portion of a
production-like workflow instead of using the `-Dspring.context.exit=onRefresh` flag which designed to
optimize only the startup of your application.
== Using the cache
Once the cache file has been created, you can use it to start your application faster:
@@ -82,7 +97,7 @@ java -XX:SharedArchiveFile=app.jsa ...
Pay attention to the logs and the startup time to check if the AOT cache is used successfully.
To figure out how effective the cache is, you can enable class loading logs by adding
an extra attribute: `-Xlog:class+load:file=aot-cache.log`. This creates a `aot-cache.log` with
an extra attribute: `-Xlog:class+load:file=aot-cache.log`. This creates an `aot-cache.log` with
every attempt to load a class and its source. Classes that are loaded from the cache should have
a "shared objects file" source, as shown in the following example:
@@ -95,11 +110,11 @@ a "shared objects file" source, as shown in the following example:
[0.151s][info][class,load] org.springframework.context.MessageSource source: shared objects file
----
If the AOT cache can't be enabled or if you have a large number of classes that are not loaded from
If the AOT cache cannot be enabled or if you have a large number of classes that are not loaded from
the cache, make sure that the following conditions are fulfilled when creating and using the cache:
- The very same JVM must be used.
- The classpath must be specified as a JAR or a list of JARs, and avoid the usage of directories and `*` wildcard characters.
- The timestamps of the JARs must be preserved.
- When using the cache, the classpath must be the same than the one used to create it, in the same order.
Additional JARs or directories can be specified *at the end* (but won't be cached).
- When using the cache, the classpath must be the same as the one used to create it, in the same order.
Additional JARs or directories can be specified *at the end* (but will not be cached).
@@ -1,16 +1,11 @@
[[appendix]]
= Appendix
[[appendix.xsd-schemas]]
== XML Schemas
This part of the appendix lists XML schemas related to integration technologies.
[[appendix.xsd-schemas-jee]]
=== The `jee` Schema
@@ -172,7 +167,7 @@ different properties with `jee`:
The `<jee:local-slsb/>` element configures a reference to a local EJB Stateless Session Bean.
The following example shows how to configures a reference to a local EJB Stateless Session Bean
The following example shows how to configure a reference to a local EJB Stateless Session Bean
without `jee`:
[source,xml,indent=0,subs="verbatim,quotes"]
@@ -184,7 +179,7 @@ without `jee`:
</bean>
----
The following example shows how to configures a reference to a local EJB Stateless Session Bean
The following example shows how to configure a reference to a local EJB Stateless Session Bean
with `jee`:
[source,xml,indent=0,subs="verbatim,quotes"]
@@ -200,7 +195,7 @@ with `jee`:
The `<jee:local-slsb/>` element configures a reference to a local EJB Stateless Session Bean.
The following example shows how to configures a reference to a local EJB Stateless Session Bean
The following example shows how to configure a reference to a local EJB Stateless Session Bean
and a number of properties without `jee`:
[source,xml,indent=0,subs="verbatim,quotes"]
@@ -215,7 +210,7 @@ and a number of properties without `jee`:
</bean>
----
The following example shows how to configures a reference to a local EJB Stateless Session Bean
The following example shows how to configure a reference to a local EJB Stateless Session Bean
and a number of properties with `jee`:
[source,xml,indent=0,subs="verbatim,quotes"]
@@ -234,7 +229,7 @@ and a number of properties with `jee`:
The `<jee:remote-slsb/>` element configures a reference to a `remote` EJB Stateless Session Bean.
The following example shows how to configures a reference to a remote EJB Stateless Session Bean
The following example shows how to configure a reference to a remote EJB Stateless Session Bean
without `jee`:
[source,xml,indent=0,subs="verbatim,quotes"]
@@ -251,7 +246,7 @@ without `jee`:
</bean>
----
The following example shows how to configures a reference to a remote EJB Stateless Session Bean
The following example shows how to configure a reference to a remote EJB Stateless Session Bean
with `jee`:
[source,xml,indent=0,subs="verbatim,quotes"]
@@ -313,7 +308,7 @@ xref:integration/jmx/naming.adoc#jmx-context-mbeanexport[Configuring Annotation-
=== The `cache` Schema
You can use the `cache` elements to enable support for Spring's `@CacheEvict`, `@CachePut`,
and `@Caching` annotations. It it also supports declarative XML-based caching. See
and `@Caching` annotations. The `cache` schema also supports declarative XML-based caching. See
xref:integration/cache/annotations.adoc#cache-annotation-enable[Enabling Caching Annotations] and
xref:integration/cache/declarative-xml.adoc[Declarative XML-based Caching] for details.
@@ -13,4 +13,4 @@ running the Kotlin compiler with its `-java-parameters` flag for standard Java p
You can declare configuration classes as
{kotlin-docs}/nested-classes.html[top level or nested but not inner],
since the later requires a reference to the outer class.
since the latter requires a reference to the outer class.
@@ -1,8 +1,8 @@
[[coroutines]]
= Coroutines
Kotlin {kotlin-docs}/coroutines-overview.html[Coroutines] are Kotlin
lightweight threads allowing to write non-blocking code in an imperative way. On language side,
Kotlin {kotlin-docs}/coroutines-overview.html[Coroutines] are instances of
suspendable computations allowing to write non-blocking code in an imperative way. On language side,
suspending functions provides an abstraction for asynchronous operations while on library side
{kotlin-github-org}/kotlinx.coroutines[kotlinx.coroutines] provides functions like
{kotlin-coroutines-api}/kotlinx-coroutines-core/kotlinx.coroutines/async.html[`async { }`]
@@ -5,6 +5,7 @@ The following annotations are supported when used in conjunction with the
xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[`SpringExtension`]
and JUnit Jupiter (that is, the programming model in JUnit):
* xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-springextensionconfig[`@SpringExtensionConfig`]
* xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-junit-jupiter-springjunitconfig[`@SpringJUnitConfig`]
* xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-junit-jupiter-springjunitwebconfig[`@SpringJUnitWebConfig`]
* xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[`@TestConstructor`]
@@ -14,6 +15,47 @@ and JUnit Jupiter (that is, the programming model in JUnit):
* xref:testing/annotations/integration-spring/annotation-disabledinaotmode.adoc[`@DisabledInAotMode`]
[[integration-testing-annotations-springextensionconfig]]
== `@SpringExtensionConfig`
`@SpringExtensionConfig` is a type-level annotation that can be used to configure the
behavior of the `SpringExtension`.
As of Spring Framework 7.0, the `SpringExtension` is configured to use a test-method
scoped `ExtensionContext`, which enables consistent dependency injection into fields and
constructors from the `ApplicationContext` for the current test method in a `@Nested`
test class hierarchy. However, if a third-party `TestExecutionListener` is not compatible
with the semantics associated with a test-method scoped extension context — or if a
developer wishes to switch to test-class scoped semantics — the `SpringExtension` can be
configured to use a test-class scoped `ExtensionContext` by annotating a top-level test
class with `@SpringExtensionConfig(useTestClassScopedExtensionContext = true)`.
[TIP]
====
If your top-level test class is configured to use JUnit Jupiter's
`@TestInstance(Lifecycle.PER_CLASS)` semantics, the `SpringExtension` will always use a
test-class scoped `ExtensionContext`, and there is no need to declare
`@SpringExtensionConfig(useTestClassScopedExtensionContext = true)`.
====
[NOTE]
====
This annotation is currently only applicable to `@Nested` test class hierarchies and
should be applied to the top-level enclosing class of a `@Nested` test class hierarchy.
Consequently, there is no need to declare this annotation on a test class that does not
contain `@Nested` test classes.
====
[NOTE]
====
xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration[`@NestedTestConfiguration`]
does not apply to this annotation.
`@SpringExtensionConfig` will always be detected within a `@Nested` test class hierarchy,
effectively disregarding any `@NestedTestConfiguration(OVERRIDE)` declarations.
====
[[integration-testing-annotations-junit-jupiter-springjunitconfig]]
== `@SpringJUnitConfig`
@@ -187,7 +229,7 @@ default mode may be set via the
xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism.
The default mode may also be configured as a
https://docs.junit.org/current/user-guide/#running-tests-config-params[JUnit Platform configuration parameter].
https://docs.junit.org/current/running-tests/configuration-parameters.html[JUnit Platform configuration parameter].
If the `spring.test.constructor.autowire.mode` property is not set, test class
constructors will not be automatically autowired.
@@ -9,7 +9,6 @@ and can be used anywhere in the Spring Framework.
* `@Qualifier`
* `@Value`
* `@Resource` (jakarta.annotation) if JSR-250 is present
* `@ManagedBean` (jakarta.annotation) if JSR-250 is present
* `@Inject` (jakarta.inject) if JSR-330 is present
* `@Named` (jakarta.inject) if JSR-330 is present
* `@PersistenceContext` (jakarta.persistence) if JPA is present
@@ -10,7 +10,7 @@ applications without a running server via MockMvc.
[[resttestclient-setup]]
[[resttestclient.setup]]
== Setup
To set up a `RestTestClient` you need to choose a server setup to bind to. This can be one
@@ -18,7 +18,7 @@ of several MockMvc setup choices, or a connection to a live server.
[[resttestclient-controller-config]]
[[resttestclient.controller-config]]
=== Bind to Controller
This setup allows you to test specific controller(s) via mock request and response objects,
@@ -42,55 +42,17 @@ Kotlin::
----
======
[[resttestclient-context-config]]
[[resttestclient.context-config]]
=== Bind to `ApplicationContext`
This setup allows you to load Spring configuration with Spring MVC
infrastructure and controller declarations and use it to handle requests via mock request
and response objects, without a running server.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@SpringJUnitConfig(WebConfig.class) // <1>
class MyTests {
include-code::./RestClientContextTests[indent=0]
RestTestClient client;
@BeforeEach
void setUp(ApplicationContext context) { // <2>
client = RestTestClient.bindToApplicationContext(context).build(); // <3>
}
}
----
<1> Specify the configuration to load
<2> Inject the configuration
<3> Create the `RestTestClient`
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@SpringJUnitConfig(WebConfig::class) // <1>
class MyTests {
lateinit var client: RestTestClient
@BeforeEach
fun setUp(context: ApplicationContext) { // <2>
client = RestTestClient.bindToApplicationContext(context).build() // <3>
}
}
----
<1> Specify the configuration to load
<2> Inject the configuration
<3> Create the `RestTestClient`
======
[[resttestclient-fn-config]]
[[resttestclient.fn-config]]
=== Bind to Router Function
This setup allows you to test xref:web/webmvc-functional.adoc[functional endpoints] via
@@ -115,7 +77,7 @@ Kotlin::
----
======
[[resttestclient-server-config]]
[[resttestclient.server-config]]
=== Bind to Server
This setup connects to a running server to perform full, end-to-end HTTP tests:
@@ -139,7 +101,7 @@ Kotlin::
[[resttestclient-client-config]]
[[resttestclient.client-config]]
=== Client Config
In addition to the server setup options described earlier, you can also configure client
@@ -170,7 +132,7 @@ Kotlin::
[[resttestclient-tests]]
[[resttestclient.tests]]
== Writing Tests
xref:integration/rest-clients.adoc#rest-restclient[`RestClient`] and `RestTestClient` have
@@ -182,70 +144,22 @@ provides two alternative ways to verify the response:
[[resttestclient-workflow]]
[[resttestclient.workflow]]
=== Built-in Assertions
To use the built-in assertions, remain in the workflow after the call to `exchange()`, and
use one of the expectation methods. For example:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON);
----
include-code::./RestClientWorkflowTests[tag=test,indent=0]
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
----
======
If you would like for all expectations to be asserted even if one of them fails, you can
use `expectAll(..)` instead of multiple chained `expect*(..)` calls. This feature is
similar to the _soft assertions_ support in AssertJ and the `assertAll()` support in
JUnit Jupiter.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectAll(
spec -> spec.expectStatus().isOk(),
spec -> spec.expectHeader().contentType(MediaType.APPLICATION_JSON)
);
----
include-code::./RestClientWorkflowTests[tag=soft-assertions,indent=0]
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectAll(
{ spec -> spec.expectStatus().isOk() },
{ spec -> spec.expectHeader().contentType(MediaType.APPLICATION_JSON) }
)
----
======
You can then choose to decode the response body through one of the following:
@@ -256,124 +170,34 @@ You can then choose to decode the response body through one of the following:
If the built-in assertions are insufficient, you can consume the object instead and
perform any other assertions:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody(Person.class)
.consumeWith(result -> {
// custom assertions (for example, AssertJ)...
});
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody<Person>()
.consumeWith {
// custom assertions (for example, AssertJ)...
}
----
======
include-code::./RestClientWorkflowTests[tag=consume,indent=0]
Or you can exit the workflow and obtain a `EntityExchangeResult`:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
EntityExchangeResult<Person> result = client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody(Person.class)
.returnResult();
----
include-code::./RestClientWorkflowTests[tag=result,indent=0]
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val result = client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk
.expectBody<Person>()
.returnResult()
----
======
TIP: When you need to decode to a target type with generics, look for the overloaded methods
that accept {spring-framework-api}/core/ParameterizedTypeReference.html[`ParameterizedTypeReference`]
instead of `Class<T>`.
[[resttestclient-no-content]]
[[resttestclient.no-content]]
==== No Content
If the response is not expected to have content, you can assert that as follows:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
client.post().uri("/persons")
.body(person)
.exchange()
.expectStatus().isCreated()
.expectBody().isEmpty();
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
client.post().uri("/persons")
.body(person)
.exchange()
.expectStatus().isCreated()
.expectBody().isEmpty()
----
======
include-code::./NoContentTests[tag=emptyBody,indent=0]
If you want to ignore the response content, the following releases the content without any assertions:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons/123")
.exchange()
.expectStatus().isNotFound()
.expectBody(Void.class);
----
include-code::./NoContentTests[tag=ignoreBody,indent=0]
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons/123")
.exchange()
.expectStatus().isNotFound
.expectBody<Unit>()
----
======
NOTE: Consuming the response body (for example, with `expectBody`) is required if your tests are running with
leak detection for pooled buffers. Without that, the tool will report buffers being leaked.
[[resttestclient-json]]
[[resttestclient.json]]
==== JSON Content
You can use `expectBody()` without a target type to perform assertions on the raw
@@ -381,126 +205,25 @@ content rather than through higher level Object(s).
To verify the full JSON content with https://jsonassert.skyscreamer.org[JSONAssert]:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody()
.json("{\"name\":\"Jane\"}")
----
include-code::./JsonTests[tag=jsonBody,indent=0]
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody()
.json("{\"name\":\"Jane\"}")
----
======
To verify JSON content with https://github.com/jayway/JsonPath[JSONPath]:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$[0].name").isEqualTo("Jane")
.jsonPath("$[1].name").isEqualTo("Jason");
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$[0].name").isEqualTo("Jane")
.jsonPath("$[1].name").isEqualTo("Jason")
----
======
include-code::./JsonTests[tag=jsonPath,indent=0]
[[resttestclient-assertj]]
[[resttestclient.assertj]]
=== AssertJ Integration
`RestTestClientResponse` is the main entry point for the AssertJ integration.
It is an `AssertProvider` that wraps the `ResponseSpec` of an exchange in order to enable
use of `assertThat()` statements. For example:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
ResponseSpec spec = client.get().uri("/persons").exchange();
include-code::./AssertJTests[tag=withSpec,indent=0]
RestTestClientResponse response = RestTestClientResponse.from(spec);
assertThat(response).hasStatusOk();
assertThat(response).hasContentTypeCompatibleWith(MediaType.TEXT_PLAIN);
// ...
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val spec = client.get().uri("/persons").exchange()
val response = RestTestClientResponse.from(spec)
assertThat(response).hasStatusOk()
assertThat(response).hasContentTypeCompatibleWith(MediaType.TEXT_PLAIN)
// ...
----
======
You can also use the built-in workflow first, and then obtain an `ExchangeResult` to wrap
and continue with AssertJ. For example:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
ExchangeResult result = client.get().uri("/persons").exchange()
. // ...
.returnResult();
RestTestClientResponse response = RestTestClientResponse.from(result);
assertThat(response).hasStatusOk();
assertThat(response).hasContentTypeCompatibleWith(MediaType.TEXT_PLAIN);
// ...
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val result = client.get().uri("/persons").exchange()
. // ...
.returnResult()
val response = RestTestClientResponse.from(spec)
assertThat(response).hasStatusOk()
assertThat(response).hasContentTypeCompatibleWith(MediaType.TEXT_PLAIN)
// ...
----
======
include-code::./AssertJTests[tag=withResult,indent=0]
@@ -62,17 +62,6 @@ script by setting a JVM system property named `spring.test.context.cache.maxSize
alternative, you can set the same property via the
xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism.
As of Spring Framework 7.0, an application context stored in the context cache will be
_paused_ when it is no longer actively in use and automatically _restarted_ the next time
the context is retrieved from the cache. Specifically, the latter will restart all
auto-startup beans in the application context, effectively restoring the lifecycle state.
This ensures that background processes within the context are not actively running while
the context is not used by tests. For example, JMS listener containers, scheduled tasks,
and any other components in the context that implement `Lifecycle` or `SmartLifecycle`
will be in a "stopped" state until the context is used again by a test. Note, however,
that `SmartLifecycle` components can opt out of pausing by returning `false` from
`SmartLifecycle#isPauseable()`.
Since having a large number of application contexts loaded within a given test suite can
cause the suite to take an unnecessarily long time to run, it is often beneficial to
know exactly how many contexts have been loaded and cached. To view the statistics for
@@ -0,0 +1,45 @@
[[testcontext-ctx-management-pausing]]
= Context Pausing
As of Spring Framework 7.0, an `ApplicationContext` stored in the context cache (see
xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching]) may be
_paused_ when it is no longer actively in use and automatically _restarted_ the next time
the context is retrieved from the cache. Specifically, the latter will restart all
auto-startup beans in the application context, effectively restoring the lifecycle state.
This ensures that background processes within the context are not actively running while
the context is not used by tests. For example, JMS listener containers, scheduled tasks,
and any other components in the context that implement `Lifecycle` or `SmartLifecycle`
will be in a "stopped" state until the context is used again by a test. Note, however,
that `SmartLifecycle` components can opt out of pausing by returning `false` from
`SmartLifecycle#isPauseable()`.
You can control whether inactive application contexts should be paused by setting the
`PauseMode` to one of the following supported values.
`ALWAYS` :: Always pause inactive application contexts.
`ON_CONTEXT_SWITCH` :: Only pause inactive application contexts if the next context
retrieved from the context cache is a different context.
`NEVER` :: Never pause inactive application contexts, effectively disabling the pausing
feature of the context cache.
The `PauseMode` defaults to `ON_CONTEXT_SWITCH`, but it can be changed from the command
line or a build script by setting a JVM system property named
`spring.test.context.cache.pause` to one of the supported values (case insensitive). As
an alternative, you can set the property via the
xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism.
For example, if you want inactive application contexts to always be paused, you can
switch from the default `ON_CONTEXT_SWITCH` mode to `ALWAYS` by setting the
`spring.test.context.cache.pause` system property to `always`.
```shell
-Dspring.test.context.cache.pause=always
```
Similarly, if you encounter issues with `Lifecycle` components that cannot or should not
opt out of pausing, or if you discover that your test suite runs more slowly due to the
pausing and restarting of application contexts, you can disable the pausing feature by
setting the `spring.test.context.cache.pause` system property to `never`.
```shell
-Dspring.test.context.cache.pause=never
```
@@ -25,7 +25,7 @@ TestNG:
* Dependency injection for test constructors, test methods, and test lifecycle callback
methods. See xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-di[Dependency
Injection with the `SpringExtension`] for further details.
* Powerful support for link:https://docs.junit.org/current/user-guide/#extensions-conditions[conditional
* Powerful support for link:https://docs.junit.org/current/extensions/conditional-test-execution.html[conditional
test execution] based on SpEL expressions, environment variables, system properties,
and so on. See the documentation for `@EnabledIf` and `@DisabledIf` in
xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations]
@@ -163,7 +163,7 @@ for further details.
=== Dependency Injection with the `SpringExtension`
The `SpringExtension` implements the
link:https://docs.junit.org/current/user-guide/#extensions-parameter-resolution[`ParameterResolver`]
link:https://docs.junit.org/current/extensions/parameter-resolution.html[`ParameterResolver`]
extension API from JUnit Jupiter, which lets Spring provide dependency injection for test
constructors, test methods, and test lifecycle callback methods.
@@ -392,6 +392,17 @@ any of its subclasses and nested classes. Thus, you may annotate a top-level tes
with `@NestedTestConfiguration`, and that will apply to all of its nested test classes
recursively.
[NOTE]
====
As of Spring Framework 7.0, the `SpringExtension` uses a test-method scoped
`ExtensionContext` within `@Nested` test class hierarchies by default. However, the
`SpringExtension` can be configured to use a test-class scoped `ExtensionContext`.
See the documentation for
xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-springextensionconfig[`@SpringExtensionConfig`]
for details.
====
[TIP]
====
If you are developing a component that integrates with the Spring TestContext Framework
@@ -1,186 +0,0 @@
[[web-integration]]
= Other Web Frameworks
This chapter details Spring's integration with third-party web frameworks.
One of the core value propositions of the Spring Framework is that of enabling
_choice_. In a general sense, Spring does not force you to use or buy into any
particular architecture, technology, or methodology (although it certainly recommends
some over others). This freedom to pick and choose the architecture, technology, or
methodology that is most relevant to a developer and their development team is
arguably most evident in the web area, where Spring provides its own web frameworks
(xref:web/webmvc.adoc#mvc[Spring MVC] and xref:web/webflux.adoc#webflux[Spring WebFlux])
while, at the same time, supporting integration with a number of popular third-party
web frameworks.
[[web-integration-common]]
== Common Configuration
Before diving into the integration specifics of each supported web framework, let us
first take a look at common Spring configuration that is not specific to any one web
framework. (This section is equally applicable to Spring's own web framework variants.)
One of the concepts (for want of a better word) espoused by Spring's lightweight
application model is that of a layered architecture. Remember that in a "classic"
layered architecture, the web layer is but one of many layers. It serves as one of the
entry points into a server-side application, and it delegates to service objects
(facades) that are defined in a service layer to satisfy business-specific (and
presentation-technology agnostic) use cases. In Spring, these service objects, any other
business-specific objects, data-access objects, and others exist in a distinct "business
context", which contains no web or presentation layer objects (presentation objects,
such as Spring MVC controllers, are typically configured in a distinct "presentation
context"). This section details how you can configure a Spring container (a
`WebApplicationContext`) that contains all of the 'business beans' in your application.
Moving on to specifics, all you need to do is declare a
{spring-framework-api}/web/context/ContextLoaderListener.html[`ContextLoaderListener`]
in the standard Jakarta EE servlet `web.xml` file of your web application and add a
`contextConfigLocation` `<context-param/>` section (in the same file) that defines which
set of Spring XML configuration files to load.
Consider the following `<listener/>` configuration:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
----
Further consider the following `<context-param/>` configuration:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext*.xml</param-value>
</context-param>
----
If you do not specify the `contextConfigLocation` context parameter, the
`ContextLoaderListener` looks for a file called `/WEB-INF/applicationContext.xml` to
load. Once the context files are loaded, Spring creates a
{spring-framework-api}/web/context/WebApplicationContext.html[`WebApplicationContext`]
object based on the bean definitions and stores it in the `ServletContext` of the web
application.
All Java web frameworks are built on top of the Servlet API, so you can use the
following code snippet to get access to this "business context" `ApplicationContext`
created by the `ContextLoaderListener`.
The following example shows how to get the `WebApplicationContext`:
[source,java,indent=0,subs="verbatim,quotes"]
----
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);
----
The
{spring-framework-api}/web/context/support/WebApplicationContextUtils.html[`WebApplicationContextUtils`]
class is for convenience, so you need not remember the name of the `ServletContext`
attribute. Its `getWebApplicationContext()` method returns `null` if an object
does not exist under the `WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE`
key. Rather than risk getting `NullPointerExceptions` in your application, it is better
to use the `getRequiredWebApplicationContext()` method. This method throws an exception
when the `ApplicationContext` is missing.
Once you have a reference to the `WebApplicationContext`, you can retrieve beans by their
name or type. Most developers retrieve beans by name and then cast them to one of their
implemented interfaces.
Fortunately, most of the frameworks in this section have simpler ways of looking up beans.
Not only do they make it easy to get beans from a Spring container, but they also let you
use dependency injection on their controllers. Each web framework section has more detail
on its specific integration strategies.
[[jsf]]
== JSF
JavaServer Faces (JSF) is the JCP's standard component-based, event-driven web
user interface framework. It is an official part of the Jakarta EE umbrella but also
individually usable, for example, through embedding Mojarra or MyFaces within Tomcat.
Please note that recent versions of JSF became closely tied to CDI infrastructure
in application servers, with some new JSF functionality only working in such an
environment. Spring's JSF support is not actively evolved anymore and primarily
exists for migration purposes when modernizing older JSF-based applications.
The key element in Spring's JSF integration is the JSF `ELResolver` mechanism.
[[jsf-springbeanfaceselresolver]]
=== Spring Bean Resolver
`SpringBeanFacesELResolver` is a JSF compliant `ELResolver` implementation,
integrating with the standard Unified EL as used by JSF and JSP. It delegates to
Spring's "business context" `WebApplicationContext` first and then to the
default resolver of the underlying JSF implementation.
Configuration-wise, you can define `SpringBeanFacesELResolver` in your JSF
`faces-context.xml` file, as the following example shows:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<faces-config>
<application>
<el-resolver>org.springframework.web.jsf.el.SpringBeanFacesELResolver</el-resolver>
...
</application>
</faces-config>
----
[[jsf-facescontextutils]]
=== Using `FacesContextUtils`
A custom `ELResolver` works well when mapping your properties to beans in
`faces-config.xml`, but, at times, you may need to explicitly grab a bean.
The {spring-framework-api}/web/jsf/FacesContextUtils.html[`FacesContextUtils`]
class makes this easy. It is similar to `WebApplicationContextUtils`, except that
it takes a `FacesContext` parameter rather than a `ServletContext` parameter.
The following example shows how to use `FacesContextUtils`:
[source,java,indent=0,subs="verbatim,quotes"]
----
ApplicationContext ctx = FacesContextUtils.getWebApplicationContext(FacesContext.getCurrentInstance());
----
[[struts]]
== Apache Struts
Invented by Craig McClanahan, https://struts.apache.org[Struts] is an open-source project
hosted by the Apache Software Foundation. Struts 1.x greatly simplified the
JSP/Servlet programming paradigm and won over many developers who were using proprietary
frameworks. It simplified the programming model; it was open source; and it had a large
community, which let the project grow and become popular among Java web developers.
As a successor to the original Struts 1.x, check out Struts 2.x or more recent versions
as well as the Struts-provided
https://struts.apache.org/plugins/spring/[Spring Plugin] for built-in Spring integration.
[[tapestry]]
== Apache Tapestry
https://tapestry.apache.org/[Tapestry] is a "Component oriented framework for creating
dynamic, robust, highly scalable web applications in Java."
While Spring has its own xref:web/webmvc.adoc#mvc[powerful web layer], there are a number of unique
advantages to building an enterprise Java application by using a combination of Tapestry
for the web user interface and the Spring container for the lower layers.
For more information, see Tapestry's dedicated
https://tapestry.apache.org/integrating-with-spring-framework.html[integration module for Spring].
[[web-integration-resources]]
== Further Resources
The following links go to further resources about the various web frameworks described in
this chapter.
* The https://www.oracle.com/java/technologies/javaserverfaces.html[JSF] homepage
* The https://struts.apache.org/[Struts] homepage
* The https://tapestry.apache.org/[Tapestry] homepage
@@ -39,9 +39,9 @@ required CORS response headers set.
In order to enable cross-origin requests (that is, the `Origin` header is present and
differs from the host of the request), you need to have some explicitly declared CORS
configuration. If no matching CORS configuration is found, preflight requests are
rejected. No CORS headers are added to the responses of simple and actual CORS requests
and, consequently, browsers reject them.
configuration. If no matching CORS configuration is found, no CORS headers are added to
the responses to preflight, simple and actual CORS requests and, consequently, browsers
reject them.
Each `HandlerMapping` can be
{spring-framework-api}/web/reactive/handler/AbstractHandlerMapping.html#setCorsConfigurations(java.util.Map)[configured]
@@ -305,7 +305,6 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
@@ -328,7 +327,6 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun addCorsMappings(registry: CorsRegistry) {
@@ -235,88 +235,14 @@ val map = request.awaitMultipartData()
The following example shows how to access multipart data, one at a time, in streaming fashion:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
Flux<PartEvent> allPartEvents = request.bodyToFlux(PartEvent.class);
allPartsEvents.windowUntil(PartEvent::isLast)
.concatMap(p -> p.switchOnFirst((signal, partEvents) -> {
if (signal.hasValue()) {
PartEvent event = signal.get();
if (event instanceof FormPartEvent formEvent) {
String value = formEvent.value();
// handle form field
}
else if (event instanceof FilePartEvent fileEvent) {
String filename = fileEvent.filename();
Flux<DataBuffer> contents = partEvents.map(PartEvent::content);
// handle file upload
}
else {
return Mono.error(new RuntimeException("Unexpected event: " + event));
}
}
else {
return partEvents; // either complete or error signal
}
}));
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val parts = request.bodyToFlux<PartEvent>()
allPartsEvents.windowUntil(PartEvent::isLast)
.concatMap {
it.switchOnFirst { signal, partEvents ->
if (signal.hasValue()) {
val event = signal.get()
if (event is FormPartEvent) {
val value: String = event.value();
// handle form field
} else if (event is FilePartEvent) {
val filename: String = event.filename();
val contents: Flux<DataBuffer> = partEvents.map(PartEvent::content);
// handle file upload
} else {
return Mono.error(RuntimeException("Unexpected event: " + event));
}
} else {
return partEvents; // either complete or error signal
}
}
}
}
----
======
include-code::./PartEventHandler[tag=snippet,indent=0]
NOTE: The body contents of the `PartEvent` objects must be completely consumed, relayed, or released to avoid memory leaks.
The following shows how to bind request parameters, URI variables, or headers via `DataBinder`,
and also shows how to customize the `DataBinder`:
[tabs]
======
Java::
+
[source,java]
----
Pet pet = request.bind(Pet.class, dataBinder -> dataBinder.setAllowedFields("name"));
----
Kotlin::
+
[source,kotlin]
----
val pet = request.bind(Pet::class.java, {dataBinder -> dataBinder.setAllowedFields("name")})
----
======
include-code::./RequestHandler[tag=snippet,indent=0]
[[webflux-fn-response]]
=== ServerResponse
@@ -326,24 +252,7 @@ a `build` method to create it. You can use the builder to set the response statu
headers, or to provide a body. The following example creates a 200 (OK) response with JSON
content:
[tabs]
======
Java::
+
[source,java]
----
Mono<Person> person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class);
----
Kotlin::
+
[source,kotlin]
----
val person: Person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue(person)
----
======
include-code::./ResponseHandler[tag=snippet,indent=0]
The following example shows how to build a 201 (CREATED) response with a `Location` header and no body:
@@ -354,7 +263,7 @@ Java::
[source,java]
----
URI location = ...
ServerResponse.created(location).build();
return ServerResponse.created(location).build();
----
Kotlin::
@@ -362,7 +271,7 @@ Kotlin::
[source,kotlin]
----
val location: URI = ...
ServerResponse.created(location).build()
return ServerResponse.created(location).build()
----
======
@@ -375,14 +284,14 @@ Java::
+
[source,java]
----
ServerResponse.ok().hint(JacksonCodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
return ServerResponse.ok().hint(JacksonCodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
----
Kotlin::
+
[source,kotlin]
----
ServerResponse.ok().hint(JacksonCodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).body(...)
return ServerResponse.ok().hint(JacksonCodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).body(...)
----
======
@@ -417,87 +326,7 @@ Therefore, it is useful to group related handler functions together into a handl
has a similar role as `@Controller` in an annotation-based application.
For example, the following class exposes a reactive `Person` repository:
--
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
public class PersonHandler {
private final PersonRepository repository;
public PersonHandler(PersonRepository repository) {
this.repository = repository;
}
public Mono<ServerResponse> listPeople(ServerRequest request) { // <1>
Flux<Person> people = repository.allPeople();
return ok().contentType(APPLICATION_JSON).body(people, Person.class);
}
public Mono<ServerResponse> createPerson(ServerRequest request) { // <2>
Mono<Person> person = request.bodyToMono(Person.class);
return ok().build(repository.savePerson(person));
}
public Mono<ServerResponse> getPerson(ServerRequest request) { // <3>
int personId = Integer.valueOf(request.pathVariable("id"));
return repository.getPerson(personId)
.flatMap(person -> ok().contentType(APPLICATION_JSON).bodyValue(person))
.switchIfEmpty(ServerResponse.notFound().build());
}
}
----
<1> `listPeople` is a handler function that returns all `Person` objects found in the repository as
JSON.
<2> `createPerson` is a handler function that stores a new `Person` contained in the request body.
Note that `PersonRepository.savePerson(Person)` returns `Mono<Void>`: an empty `Mono` that emits
a completion signal when the person has been read from the request and stored. So we use the
`build(Publisher<Void>)` method to send a response when that completion signal is received (that is,
when the `Person` has been saved).
<3> `getPerson` is a handler function that returns a single person, identified by the `id` path
variable. We retrieve that `Person` from the repository and create a JSON response, if it is
found. If it is not found, we use `switchIfEmpty(Mono<T>)` to return a 404 Not Found response.
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
class PersonHandler(private val repository: PersonRepository) {
suspend fun listPeople(request: ServerRequest): ServerResponse { // <1>
val people: Flow<Person> = repository.allPeople()
return ok().contentType(APPLICATION_JSON).bodyAndAwait(people);
}
suspend fun createPerson(request: ServerRequest): ServerResponse { // <2>
val person = request.awaitBody<Person>()
repository.savePerson(person)
return ok().buildAndAwait()
}
suspend fun getPerson(request: ServerRequest): ServerResponse { // <3>
val personId = request.pathVariable("id").toInt()
return repository.getPerson(personId)?.let { ok().contentType(APPLICATION_JSON).bodyValueAndAwait(it) }
?: ServerResponse.notFound().buildAndAwait()
}
}
----
<1> `listPeople` is a handler function that returns all `Person` objects found in the repository as
JSON.
<2> `createPerson` is a handler function that stores a new `Person` contained in the request body.
Note that `PersonRepository.savePerson(Person)` is a suspending function with no return type.
<3> `getPerson` is a handler function that returns a single person, identified by the `id` path
variable. We retrieve that `Person` from the repository and create a JSON response, if it is
found. If it is not found, we return a 404 Not Found response.
======
--
include-code::./PersonHandler[tag=snippet,indent=0]
[[webflux-fn-handler-validation]]
=== Validation
@@ -506,66 +335,7 @@ A functional endpoint can use Spring's xref:web/webmvc/mvc-config/validation.ado
apply validation to the request body. For example, given a custom Spring
xref:web/webmvc/mvc-config/validation.adoc[Validator] implementation for a `Person`:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
public class PersonHandler {
private final Validator validator = new PersonValidator(); // <1>
// ...
public Mono<ServerResponse> createPerson(ServerRequest request) {
Mono<Person> person = request.bodyToMono(Person.class).doOnNext(this::validate); // <2>
return ok().build(repository.savePerson(person));
}
private void validate(Person person) {
Errors errors = new BeanPropertyBindingResult(person, "person");
validator.validate(person, errors);
if (errors.hasErrors()) {
throw new ServerWebInputException(errors.toString()); // <3>
}
}
}
----
<1> Create `Validator` instance.
<2> Apply validation.
<3> Raise exception for a 400 response.
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
class PersonHandler(private val repository: PersonRepository) {
private val validator = PersonValidator() // <1>
// ...
suspend fun createPerson(request: ServerRequest): ServerResponse {
val person = request.awaitBody<Person>()
validate(person) // <2>
repository.savePerson(person)
return ok().buildAndAwait()
}
private fun validate(person: Person) {
val errors: Errors = BeanPropertyBindingResult(person, "person");
validator.validate(person, errors);
if (errors.hasErrors()) {
throw ServerWebInputException(errors.toString()) // <3>
}
}
}
----
<1> Create `Validator` instance.
<2> Apply validation.
<3> Raise exception for a 400 response.
======
include-code::./PersonHandler[tag=snippet,indent=0]
Handlers can also use the standard bean validation API (JSR-303) by creating and injecting
a global `Validator` instance based on `LocalValidatorFactoryBean`.
@@ -603,28 +373,7 @@ path, headers, xref:#api-version[API version], and more.
The following example uses an `Accept` header, request predicate:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
request -> ServerResponse.ok().bodyValue("Hello World")).build();
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val route = coRouter {
GET("/hello-world", accept(TEXT_PLAIN)) {
ServerResponse.ok().bodyValueAndAwait("Hello World")
}
}
----
======
include-code::./RouterConfiguration[tag=snippet,indent=0]
You can compose multiple request predicates together by using:
@@ -659,61 +408,7 @@ There are also other ways to compose multiple router functions together:
The following example shows the composition of four routes:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> otherRoute = ...
RouterFunction<ServerResponse> route = route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) // <1>
.GET("/person", accept(APPLICATION_JSON), handler::listPeople) // <2>
.POST("/person", handler::createPerson) // <3>
.add(otherRoute) // <4>
.build();
----
<1> pass:q[`GET /person/{id}`] with an `Accept` header that matches JSON is routed to
`PersonHandler.getPerson`
<2> `GET /person` with an `Accept` header that matches JSON is routed to
`PersonHandler.listPeople`
<3> `POST /person` with no additional predicates is mapped to
`PersonHandler.createPerson`, and
<4> `otherRoute` is a router function that is created elsewhere, and added to the route built.
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
import org.springframework.http.MediaType.APPLICATION_JSON
val repository: PersonRepository = ...
val handler = PersonHandler(repository);
val otherRoute: RouterFunction<ServerResponse> = coRouter { }
val route = coRouter {
GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) // <1>
GET("/person", accept(APPLICATION_JSON), handler::listPeople) // <2>
POST("/person", handler::createPerson) // <3>
}.and(otherRoute) // <4>
----
<1> pass:q[`GET /person/{id}`] with an `Accept` header that matches JSON is routed to
`PersonHandler.getPerson`
<2> `GET /person` with an `Accept` header that matches JSON is routed to
`PersonHandler.listPeople`
<3> `POST /person` with no additional predicates is mapped to
`PersonHandler.createPerson`, and
<4> `otherRoute` is a router function that is created elsewhere, and added to the route built.
======
include-code::./RouterConfiguration[tag=snippet,indent=0]
[[nested-routes]]
=== Nested Routes
@@ -811,7 +506,7 @@ Java::
----
RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/hello-world", version("1.2"),
request -> ServerResponse.ok().body("Hello World")).build();
request -> ServerResponse.ok().bodyValue("Hello World")).build();
----
Kotlin::
@@ -944,7 +639,6 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Bean
@@ -981,7 +675,6 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
@Bean
@@ -1049,9 +742,9 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val route = router {
"/person".nest {
("/person" and accept(APPLICATION_JSON)).nest {
GET("/{id}", handler::getPerson)
GET("", handler::listPeople)
GET(handler::listPeople)
before { // <1>
ServerRequest.from(it)
.header("X-RequestHeader", "Value").build()
@@ -1078,54 +771,7 @@ Now we can add a simple security filter to our route, assuming that we have a `S
can determine whether a particular path is allowed.
The following example shows how to do so:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
SecurityManager securityManager = ...
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople))
.POST(handler::createPerson))
.filter((request, next) -> {
if (securityManager.allowAccessTo(request.path())) {
return next.handle(request);
}
else {
return ServerResponse.status(UNAUTHORIZED).build();
}
})
.build();
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val securityManager: SecurityManager = ...
val route = router {
("/person" and accept(APPLICATION_JSON)).nest {
GET("/{id}", handler::getPerson)
GET("", handler::listPeople)
POST(handler::createPerson)
filter { request, next ->
if (securityManager.allowAccessTo(request.path())) {
next(request)
}
else {
status(UNAUTHORIZED).build();
}
}
}
}
----
======
include-code::./RouterConfiguration[tag=snippet,indent=0]
The preceding example demonstrates that invoking the `next.handle(ServerRequest)` is optional.
We only let the handler function be run when access is allowed.
@@ -56,8 +56,7 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
public class WebConfiguration implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
@@ -80,8 +79,7 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
class WebConfiguration : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.freeMarker()
@@ -119,8 +117,7 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
public class WebConfiguration implements WebFluxConfigurer {
// ...
@@ -142,8 +139,7 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
class WebConfiguration : WebFluxConfigurer {
// ...
@@ -210,13 +206,8 @@ The following table shows the templating libraries that we have tested on differ
[%header]
|===
|Scripting Library |Scripting Engine
|https://handlebarsjs.com/[Handlebars] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|https://mustache.github.io/[Mustache] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|https://react.dev/[React] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|https://ejs.co/[EJS] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|https://docs.ruby-lang.org/en/master/ERB.html[ERB] |https://www.jruby.org[JRuby]
|https://docs.python.org/2/library/string.html#template-strings[String templates] |https://www.jython.org/[Jython]
|https://github.com/sdeleuze/kotlin-script-templating[Kotlin Script templating] |{kotlin-site}[Kotlin]
|===
TIP: The basic rule for integrating any other script engine is that it must implement the
@@ -228,17 +219,8 @@ TIP: The basic rule for integrating any other script engine is that it must impl
You need to have the script engine on your classpath, the details of which vary by script engine:
* The https://openjdk.java.net/projects/nashorn/[Nashorn] JavaScript engine is provided with
Java 8+. Using the latest update release available is highly recommended.
* https://www.jruby.org[JRuby] should be added as a dependency for Ruby support.
* https://www.jython.org[Jython] should be added as a dependency for Python support.
* `org.jetbrains.kotlin:kotlin-script-util` dependency and a `META-INF/services/javax.script.ScriptEngineFactory`
file containing a `org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory`
line should be added for Kotlin script support. See
https://github.com/sdeleuze/kotlin-script-templating[this example] for more detail.
You need to have the script templating library. One way to do that for JavaScript is
through https://www.webjars.org/[WebJars].
[[webflux-view-script-integrate]]
=== Script Templates
@@ -246,7 +228,7 @@ through https://www.webjars.org/[WebJars].
You can declare a `ScriptTemplateConfigurer` bean to specify the script engine to use,
the script files to load, what function to call to render templates, and so on.
The following example uses Mustache templates and the Nashorn JavaScript engine:
The following example uses the Jython Python engine:
[tabs]
======
@@ -255,8 +237,7 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
public class WebConfiguration implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
@@ -266,9 +247,8 @@ Java::
@Bean
public ScriptTemplateConfigurer configurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("nashorn");
configurer.setScripts("mustache.js");
configurer.setRenderObject("Mustache");
configurer.setEngineName("jython");
configurer.setScripts("render.py");
configurer.setRenderFunction("render");
return configurer;
}
@@ -280,8 +260,7 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
class WebConfiguration : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.scriptTemplate()
@@ -289,9 +268,8 @@ Kotlin::
@Bean
fun configurer() = ScriptTemplateConfigurer().apply {
engineName = "nashorn"
setScripts("mustache.js")
renderObject = "Mustache"
engineName = "jython"
setScripts("render.py")
renderFunction = "render"
}
}
@@ -305,94 +283,7 @@ The `render` function is called with the following parameters:
* `RenderingContext renderingContext`: The
{spring-framework-api}/web/servlet/view/script/RenderingContext.html[`RenderingContext`]
that gives access to the application context, the locale, the template loader, and the
URL (since 5.0)
`Mustache.render()` is natively compatible with this signature, so you can call it directly.
If your templating technology requires some customization, you can provide a script that
implements a custom render function. For example, https://handlebarsjs.com[Handlerbars]
needs to compile templates before using them and requires a
https://en.wikipedia.org/wiki/Polyfill[polyfill] in order to emulate some
browser facilities not available in the server-side script engine.
The following example shows how to set a custom render function:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.scriptTemplate();
}
@Bean
public ScriptTemplateConfigurer configurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("nashorn");
configurer.setScripts("polyfill.js", "handlebars.js", "render.js");
configurer.setRenderFunction("render");
configurer.setSharedEngine(false);
return configurer;
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.scriptTemplate()
}
@Bean
fun configurer() = ScriptTemplateConfigurer().apply {
engineName = "nashorn"
setScripts("polyfill.js", "handlebars.js", "render.js")
renderFunction = "render"
isSharedEngine = false
}
}
----
======
NOTE: Setting the `sharedEngine` property to `false` is required when using non-thread-safe
script engines with templating libraries not designed for concurrency, such as Handlebars or
React running on Nashorn. In that case, Java SE 8 update 60 is required, due to
https://bugs.openjdk.java.net/browse/JDK-8076099[this bug], but it is generally
recommended to use a recent Java SE patch release in any case.
`polyfill.js` defines only the `window` object needed by Handlebars to run properly,
as the following snippet shows:
[source,javascript,indent=0,subs="verbatim,quotes"]
----
var window = {};
----
This basic `render.js` implementation compiles the template before using it. A production
ready implementation should also store and reused cached templates or pre-compiled templates.
This can be done on the script side, as well as any customization you need (managing
template engine configuration for example).
The following example shows how compile a template:
[source,javascript,indent=0,subs="verbatim,quotes"]
----
function render(template, model) {
var compiledTemplate = Handlebars.compile(template);
return compiledTemplate(model);
}
----
URL
Check out the Spring Framework unit tests,
{spring-framework-code}/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/script[Java], and
@@ -18,8 +18,8 @@ You can also use `WebClient.builder()` with further options:
* `exchangeStrategies`: HTTP message reader/writer customizations.
* `clientConnector`: HTTP client library settings.
* `apiVersionInserter`: to insert API version values in the request
* `observationRegistry`: the registry to use for enabling xref:integration/observability.adoc#http-client.webclient[Observability support].
* `observationConvention`: xref:integration/observability.adoc#config[an optional, custom convention to extract metadata] for recorded observations.
* `observationRegistry`: the registry to use for enabling xref:integration/observability.adoc#observability.http-client.webclient[Observability support].
* `observationConvention`: xref:integration/observability.adoc#observability.config[an optional, custom convention to extract metadata] for recorded observations.
For example:
@@ -32,9 +32,9 @@ any `@RequestMapping` method to render an RFC 9457 response. This is processed a
- The `status` property of `ProblemDetail` determines the HTTP status.
- The `instance` property of `ProblemDetail` is set from the current URL path, if not
already set.
- For content negotiation, the Jackson `HttpMessageConverter` prefers
"application/problem+json" over "application/json" when rendering a `ProblemDetail`,
and also falls back on it if no compatible media type is found.
- The Jackson JSON and XML message converters use "application/problem+json" or
"application/problem+xml" respectively as the producible media types for `ProblemDetail`
to ensure they are favored for content negotiation.
To enable RFC 9457 responses for Spring WebFlux exceptions and for any
`ErrorResponseException`, extend `ResponseEntityExceptionHandler` and declare it as an
@@ -66,6 +66,13 @@ from an existing `ProblemDetail`. This could be done centrally, for example, fro
`@ControllerAdvice` such as `ResponseEntityExceptionHandler` that re-creates the
`ProblemDetail` of an exception into a subclass with the additional non-standard fields.
TIP: In Spring Boot, the `spring.webflux.problemdetails.enabled` property autoconfigures
a `ResponseEntityExceptionHandler` that handles built-in exceptions with problem details.
In that case, you may prefer to create another `@ControllerAdvice` instead of extending
`ResponseEntityExceptionHandler` if you want to take over the handling of a specific
built-in exception. You'll need to ensure your handler is ordered ahead of the one
configured by Spring Boot whose order is 0.
[[webflux-ann-rest-exceptions-i18n]]
== Customization and i18n
@@ -41,8 +41,8 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
class MyForm(
val name: String,
val file: FilePart)
private val name: String,
private val file: FilePart)
@Controller
class FileUploadController {
@@ -103,8 +103,8 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@PostMapping("/")
fun handle(@RequestPart("meta-data") Part metadata, // <1>
@RequestPart("file-data") FilePart file): String { // <2>
fun handle(@RequestPart("meta-data") metadata: Part, // <1>
@RequestPart("file-data") file: FilePart): String { // <2>
// ...
}
----
@@ -169,8 +169,8 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@PostMapping("/")
fun handle(@Valid @RequestPart("meta-data") metadata: MetaData): String {
// ...
fun handle(@Valid @RequestPart("meta-data") metadata: Mono<MetaData>): String {
// use one of the onError* operators...
}
----
======
@@ -202,7 +202,7 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@PostMapping("/")
fun handle(@RequestBody parts: MultiValueMap<String, Part>): String { // <1>
fun handle(@RequestBody parts: Mono<MultiValueMap<String, Part>>): String { // <1>
// ...
}
----
@@ -227,87 +227,7 @@ when uploading. If the file is large enough to be split across multiple buffers,
For example:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@PostMapping("/")
public void handle(@RequestBody Flux<PartEvent> allPartsEvents) { <1>
allPartsEvents.windowUntil(PartEvent::isLast) <2>
.concatMap(p -> p.switchOnFirst((signal, partEvents) -> { <3>
if (signal.hasValue()) {
PartEvent event = signal.get();
if (event instanceof FormPartEvent formEvent) { <4>
String value = formEvent.value();
// handle form field
}
else if (event instanceof FilePartEvent fileEvent) { <5>
String filename = fileEvent.filename();
Flux<DataBuffer> contents = partEvents.map(PartEvent::content); <6>
// handle file upload
}
else {
return Mono.error(new RuntimeException("Unexpected event: " + event));
}
}
else {
return partEvents; // either complete or error signal
}
}));
}
----
<1> Using `@RequestBody`.
<2> The final `PartEvent` for a particular part will have `isLast()` set to `true`, and can be
followed by additional events belonging to subsequent parts.
This makes the `isLast` property suitable as a predicate for the `Flux::windowUntil` operator, to
split events from all parts into windows that each belong to a single part.
<3> The `Flux::switchOnFirst` operator allows you to see whether you are handling a form field or
file upload.
<4> Handling the form field.
<5> Handling the file upload.
<6> The body contents must be completely consumed, relayed, or released to avoid memory leaks.
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@PostMapping("/")
fun handle(@RequestBody allPartsEvents: Flux<PartEvent>) = { // <1>
allPartsEvents.windowUntil(PartEvent::isLast) <2>
.concatMap {
it.switchOnFirst { signal, partEvents -> <3>
if (signal.hasValue()) {
val event = signal.get()
if (event is FormPartEvent) { <4>
val value: String = event.value();
// handle form field
} else if (event is FilePartEvent) { <5>
val filename: String = event.filename();
val contents: Flux<DataBuffer> = partEvents.map(PartEvent::content); <6>
// handle file upload
} else {
return Mono.error(RuntimeException("Unexpected event: " + event));
}
} else {
return partEvents; // either complete or error signal
}
}
}
}
----
<1> Using `@RequestBody`.
<2> The final `PartEvent` for a particular part will have `isLast()` set to `true`, and can be
followed by additional events belonging to subsequent parts.
This makes the `isLast` property suitable as a predicate for the `Flux::windowUntil` operator, to
split events from all parts into windows that each belong to a single part.
<3> The `Flux::switchOnFirst` operator allows you to see whether you are handling a form field or
file upload.
<4> Handling the form field.
<5> Handling the file upload.
<6> The body contents must be completely consumed, relayed, or released to avoid memory leaks.
======
include-code::./PartEventController[tag=snippet,indent=0]
Received part events can also be relayed to another service by using the `WebClient`.
See xref:web/webflux-webclient/client-body.adoc#webflux-client-body-multipart[Multipart Data].
@@ -393,9 +393,9 @@ is used to map requests.
Once API versioning is enabled, you can begin to map requests with versions.
The `@RequestMapping` `version` attribute supports the following:
- No value -- matches any version
- Fixed version ("1.2") -- matches the given version only
- Baseline version ("1.2+") -- matches the given version and above
- No value -- matches any version, but is superseded by a more specific version match
If multiple controller methods have a version less than or equal to the request version,
the highest of those, and closest to the request version, is the one considered,
@@ -454,6 +454,11 @@ A request with version `"1.6"` does not have a match. (1) and (3) do match, but
superseded by (4), which allows only a strict match, and therefore does not match.
In this scenario, a `NotAcceptableApiVersionException` results in a 400 response.
Controller methods without a version are intended to support clients created before a
versioned alternative was introduced. Therefore, even though an unversioned controller
method is considered a match for any version, it is in fact given the lowest priority,
and is effectively superseded by any alternative controller method with a version.
NOTE: The above assumes the request version is a
xref:web/webflux/config.adoc#webflux-config-api-version["supported" version],
or otherwise it would fail.
@@ -381,16 +381,23 @@ See the section on xref:web/webflux-cors.adoc[CORS] and the xref:web/webflux-cor
You may want your controller endpoints to match routes with or without a trailing slash in the URL path.
For example, both "GET /home" and "GET /home/" should be handled by a controller method annotated with `@GetMapping("/home")`.
Adding trailing slash variants to all mapping declarations is not the best way to handle this use case.
The `UrlHandlerFilter` web filter has been designed for this purpose. It can be configured to:
Spring provides `UrlHandlerFilter` that removes the trailing slash from URL paths to ensure a consistent view of paths with or without a trailing slash.
This is important to avoid a mismatch between URL-based authorization decisions and web framework request mappings.
The filter can remove the trailing slash in one of a couple of ways:
* respond with an HTTP redirect status when receiving URLs with trailing slashes, sending browsers to the non-trailing slash URL variant.
* mutate the request to act as if the request was sent without a trailing slash and continue the processing of the request.
* respond with an HTTP redirect status that sends clients to the same path without a trailing slash.
* mutate the request to remove the trailing slash.
Here is how you can instantiate and configure a `UrlHandlerFilter` for a blog application:
include-code::./UrlHandlerFilterConfiguration[tag=config,indent=0]
Keep in mind the following:
- the root path `"/"` is excluded from trailing slash handling.
- `@RequestMapping("/")` adds a trailing slash to a type-level mapping, and therefore will
not map when trailing slash handling applies; use `@RequestMapping` (no path attribute) instead.
[[webflux-exception-handler]]
== Exceptions
@@ -658,7 +665,6 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebFlux
class MyConfig implements WebFluxConfigurer {
@Override
@@ -673,7 +679,6 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebFlux
class MyConfig : WebFluxConfigurer {
override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
@@ -66,9 +66,9 @@ required CORS response headers set.
In order to enable cross-origin requests (that is, the `Origin` header is present and
differs from the host of the request), you need to have some explicitly declared CORS
configuration. If no matching CORS configuration is found, preflight requests are
rejected. No CORS headers are added to the responses of simple and actual CORS requests
and, consequently, browsers reject them.
configuration. If no matching CORS configuration is found, no CORS headers are added to
the responses to preflight, simple and actual CORS requests and, consequently, browsers
reject them.
Each `HandlerMapping` can be
{spring-framework-api}/web/servlet/handler/AbstractHandlerMapping.html#setCorsConfigurations(java.util.Map)[configured]
@@ -208,6 +208,7 @@ Kotlin::
fun remove(@PathVariable id: Long) {
// ...
}
}
----
======
@@ -286,84 +287,9 @@ the `allowOriginPatterns` property may be used to match to a dynamic set of orig
`maxAge` is set to 30 minutes.
[[mvc-cors-global-java]]
=== Java Configuration
[.small]#xref:web/webflux-cors.adoc#webflux-cors-global[See equivalent in the Reactive stack]#
To enable CORS in the MVC Java config, you can use the `CorsRegistry` callback,
as the following example shows:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true).maxAge(3600);
// Add more mappings...
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/api/**")
.allowedOrigins("https://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true).maxAge(3600)
// Add more mappings...
}
}
----
======
[[mvc-cors-global-xml]]
=== XML Configuration
To enable CORS in the XML namespace, you can use the `<mvc:cors>` element,
as the following example shows:
[source,xml,indent=0,subs="verbatim"]
----
<mvc:cors>
<mvc:mapping path="/api/**"
allowed-origins="https://domain1.com, https://domain2.com"
allowed-methods="GET, PUT"
allowed-headers="header1, header2, header3"
exposed-headers="header1, header2" allow-credentials="true"
max-age="123" />
<mvc:mapping path="/resources/**"
allowed-origins="https://domain1.com" />
</mvc:cors>
----
You can enable CORS in the Spring MVC configuration as the following example shows:
include-code::./WebConfiguration[tag=snippet,indent=0]
[[mvc-cors-filter]]
== CORS Filter
@@ -836,7 +836,6 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"]
----
ClassPathResource index = new ClassPathResource("static/index.html");
List<String> extensions = List.of("js", "css", "ico", "png", "jpg", "gif");
RequestPredicate spaPredicate = path("/api/**").or(path("/error")).negate();
RouterFunction<ServerResponse> redirectToIndex = route()
.resource(spaPredicate, index)
@@ -900,81 +899,9 @@ processing lifecycle and also (potentially) run side by side with annotated cont
any are declared. It is also how functional endpoints are enabled by the Spring Boot Web
starter.
The following example shows a WebMvc Java configuration:
The following example shows a related Spring MVC configuration:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableMvc
public class WebConfig implements WebMvcConfigurer {
@Bean
public RouterFunction<?> routerFunctionA() {
// ...
}
@Bean
public RouterFunction<?> routerFunctionB() {
// ...
}
// ...
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// configure message conversion...
}
@Override
public void addCorsMappings(CorsRegistry registry) {
// configure CORS...
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
// configure view resolution for HTML rendering...
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableMvc
class WebConfig : WebMvcConfigurer {
@Bean
fun routerFunctionA(): RouterFunction<*> {
// ...
}
@Bean
fun routerFunctionB(): RouterFunction<*> {
// ...
}
// ...
override fun configureMessageConverters(converters: List<HttpMessageConverter<*>>) {
// configure message conversion...
}
override fun addCorsMappings(registry: CorsRegistry) {
// configure CORS...
}
override fun configureViewResolvers(registry: ViewResolverRegistry) {
// configure view resolution for HTML rendering...
}
}
----
======
include-code::./WebConfiguration[tag=snippet,indent=0]
[[webmvc-fn-handler-filter-function]]
@@ -14,82 +14,7 @@ integration for using Spring MVC with FreeMarker templates.
The following example shows how to configure FreeMarker as a view technology:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.freeMarker();
}
// Configure FreeMarker...
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("/WEB-INF/freemarker");
configurer.setDefaultCharset(StandardCharsets.UTF_8);
return configurer;
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.freeMarker()
}
// Configure FreeMarker...
@Bean
fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
setTemplateLoaderPath("/WEB-INF/freemarker")
setDefaultCharset(StandardCharsets.UTF_8)
}
}
----
======
The following example shows how to configure the same in XML:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<mvc:annotation-driven/>
<mvc:view-resolvers>
<mvc:freemarker/>
</mvc:view-resolvers>
<!-- Configure FreeMarker... -->
<mvc:freemarker-configurer>
<mvc:template-loader-path location="/WEB-INF/freemarker"/>
</mvc:freemarker-configurer>
----
Alternatively, you can also declare the `FreeMarkerConfigurer` bean for full control over all
properties, as the following example shows:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
<property name="templateLoaderPath" value="/WEB-INF/freemarker/"/>
<property name="defaultEncoding" value="UTF-8"/>
</bean>
----
include-code::./WebConfiguration[tag=snippet,indent=0]
Your templates need to be stored in the directory specified by the `FreeMarkerConfigurer`
shown in the preceding example. Given the preceding configuration, if your controller
@@ -107,19 +32,7 @@ properties on the `FreeMarkerConfigurer` bean. The `freemarkerSettings` property
a `java.util.Properties` object, and the `freemarkerVariables` property requires a
`java.util.Map`. The following example shows how to use a `FreeMarkerConfigurer`:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
<property name="templateLoaderPath" value="/WEB-INF/freemarker/"/>
<property name="freemarkerVariables">
<map>
<entry key="xml_escape" value-ref="fmXmlEscape"/>
</map>
</property>
</bean>
<bean id="fmXmlEscape" class="freemarker.template.utility.XmlEscape"/>
----
include-code::./WebConfiguration[tag=snippet,indent=0]
See the FreeMarker documentation for details of settings and variables as they apply to
the `Configuration` object.
@@ -14,68 +14,7 @@ NOTE: The Groovy Markup Template engine requires Groovy 2.3.1+.
The following example shows how to configure the Groovy Markup Template Engine:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.groovy();
}
// Configure the Groovy Markup Template Engine...
@Bean
public GroovyMarkupConfigurer groovyMarkupConfigurer() {
GroovyMarkupConfigurer configurer = new GroovyMarkupConfigurer();
configurer.setResourceLoaderPath("/WEB-INF/");
return configurer;
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.groovy()
}
// Configure the Groovy Markup Template Engine...
@Bean
fun groovyMarkupConfigurer() = GroovyMarkupConfigurer().apply {
resourceLoaderPath = "/WEB-INF/"
}
}
----
======
The following example shows how to configure the same in XML:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<mvc:annotation-driven/>
<mvc:view-resolvers>
<mvc:groovy/>
</mvc:view-resolvers>
<!-- Configure the Groovy Markup Template Engine... -->
<mvc:groovy-configurer resource-loader-path="/WEB-INF/"/>
----
include-code::./WebConfiguration[tag=snippet,indent=0]
[[mvc-view-groovymarkup-example]]
== Example
@@ -16,39 +16,11 @@ a directory under the `WEB-INF` directory so there can be no direct access by cl
This is what is done by the configuration below which registers a JSP view resolver using
a default view name prefix of `"/WEB-INF/"` and a default suffix of `".jsp"`.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@EnableWebMvc
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.jsp();
}
}
----
XML::
+
[source,xml,indent=0,subs="verbatim,quotes"]
----
<mvc:annotation-driven/>
<mvc:view-resolvers>
<mvc:jsp/>
</mvc:view-resolvers>
----
======
include-code::./WebConfiguration[tag=snippet,indent=0]
[NOTE]
You can specify custom prefix and suffix.
[[mvc-view-jsp-jstl]]
== JSPs versus JSTL
@@ -11,13 +11,8 @@ templating libraries on different script engines:
[%header]
|===
|Scripting Library |Scripting Engine
|https://handlebarsjs.com/[Handlebars] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|https://mustache.github.io/[Mustache] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|https://react.dev/[React] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|https://ejs.co/[EJS] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|https://docs.ruby-lang.org/en/master/ERB.html[ERB] |https://www.jruby.org[JRuby]
|https://docs.python.org/2/library/string.html#template-strings[String templates] |https://www.jython.org/[Jython]
|https://github.com/sdeleuze/kotlin-script-templating[Kotlin Script templating] |{kotlin-site}[Kotlin]
|===
TIP: The basic rule for integrating any other script engine is that it must implement the
@@ -30,18 +25,8 @@ TIP: The basic rule for integrating any other script engine is that it must impl
You need to have the script engine on your classpath, the details of which vary by script engine:
* The https://openjdk.java.net/projects/nashorn/[Nashorn] JavaScript engine is provided with
Java 8+. Using the latest update release available is highly recommended.
* https://www.jruby.org[JRuby] should be added as a dependency for Ruby support.
* https://www.jython.org[Jython] should be added as a dependency for Python support.
* `org.jetbrains.kotlin:kotlin-script-util` dependency and a `META-INF/services/javax.script.ScriptEngineFactory`
file containing a `org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory`
line should be added for Kotlin script support. See
https://github.com/sdeleuze/kotlin-script-templating[this example] for more details.
You need to have the script templating library. One way to do that for JavaScript is
through https://www.webjars.org/[WebJars].
[[mvc-view-script-integrate]]
== Script Templates
@@ -49,74 +34,20 @@ through https://www.webjars.org/[WebJars].
You can declare a `ScriptTemplateConfigurer` bean to specify the script engine to use,
the script files to load, what function to call to render templates, and so on.
The following example uses Mustache templates and the Nashorn JavaScript engine:
The following example uses the Jython Python engine:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
include-code::./WebConfiguration[tag=snippet,indent=0]
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.scriptTemplate();
}
The render function is called with the following parameters:
@Bean
public ScriptTemplateConfigurer configurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("nashorn");
configurer.setScripts("mustache.js");
configurer.setRenderObject("Mustache");
configurer.setRenderFunction("render");
return configurer;
}
}
----
* `String template`: The template content
* `Map model`: The view model
* `RenderingContext renderingContext`: The
{spring-framework-api}/web/servlet/view/script/RenderingContext.html[`RenderingContext`]
that gives access to the application context, the locale, the template loader, and the
URL
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.scriptTemplate()
}
@Bean
fun configurer() = ScriptTemplateConfigurer().apply {
engineName = "nashorn"
setScripts("mustache.js")
renderObject = "Mustache"
renderFunction = "render"
}
}
----
======
The following example shows the same arrangement in XML:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<mvc:annotation-driven/>
<mvc:view-resolvers>
<mvc:script-template/>
</mvc:view-resolvers>
<mvc:script-template-configurer engine-name="nashorn" render-object="Mustache" render-function="render">
<mvc:script location="mustache.js"/>
</mvc:script-template-configurer>
----
The controller would look no different for the Java and XML configurations, as the following example shows:
The controller is used to populate the model attributes and specify the view name, as the following example shows:
[tabs]
======
@@ -153,115 +84,6 @@ Kotlin::
----
======
The following example shows the Mustache template:
[source,html,indent=0,subs="verbatim,quotes"]
----
<html>
<head>
<title>{{title}}</title>
</head>
<body>
<p>{{body}}</p>
</body>
</html>
----
The render function is called with the following parameters:
* `String template`: The template content
* `Map model`: The view model
* `RenderingContext renderingContext`: The
{spring-framework-api}/web/servlet/view/script/RenderingContext.html[`RenderingContext`]
that gives access to the application context, the locale, the template loader, and the
URL (since 5.0)
`Mustache.render()` is natively compatible with this signature, so you can call it directly.
If your templating technology requires some customization, you can provide a script that
implements a custom render function. For example, https://handlebarsjs.com[Handlerbars]
needs to compile templates before using them and requires a
https://en.wikipedia.org/wiki/Polyfill[polyfill] to emulate some
browser facilities that are not available in the server-side script engine.
The following example shows how to do so:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.scriptTemplate();
}
@Bean
public ScriptTemplateConfigurer configurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("nashorn");
configurer.setScripts("polyfill.js", "handlebars.js", "render.js");
configurer.setRenderFunction("render");
configurer.setSharedEngine(false);
return configurer;
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.scriptTemplate()
}
@Bean
fun configurer() = ScriptTemplateConfigurer().apply {
engineName = "nashorn"
setScripts("polyfill.js", "handlebars.js", "render.js")
renderFunction = "render"
isSharedEngine = false
}
}
----
======
NOTE: Setting the `sharedEngine` property to `false` is required when using non-thread-safe
script engines with templating libraries not designed for concurrency, such as Handlebars or
React running on Nashorn. In that case, Java SE 8 update 60 is required, due to
https://bugs.openjdk.java.net/browse/JDK-8076099[this bug], but it is generally
recommended to use a recent Java SE patch release in any case.
`polyfill.js` defines only the `window` object needed by Handlebars to run properly, as follows:
[source,javascript,indent=0,subs="verbatim,quotes"]
----
var window = {};
----
This basic `render.js` implementation compiles the template before using it. A production-ready
implementation should also store any reused cached templates or pre-compiled templates.
You can do so on the script side (and handle any customization you need -- managing
template engine configuration, for example). The following example shows how to do so:
[source,javascript,indent=0,subs="verbatim,quotes"]
----
function render(template, model) {
var compiledTemplate = Handlebars.compile(template);
return compiledTemplate(model);
}
----
Check out the Spring Framework unit tests,
{spring-framework-code}/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script[Java], and
{spring-framework-code}/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script[resources],
@@ -21,45 +21,7 @@ Configuration is standard for a simple Spring web application: The MVC configura
has to define an `XsltViewResolver` bean and regular MVC annotation configuration.
The following example shows how to do so:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@EnableWebMvc
@ComponentScan
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public XsltViewResolver xsltViewResolver() {
XsltViewResolver viewResolver = new XsltViewResolver();
viewResolver.setPrefix("/WEB-INF/xsl/");
viewResolver.setSuffix(".xslt");
return viewResolver;
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@EnableWebMvc
@ComponentScan
@Configuration
class WebConfig : WebMvcConfigurer {
@Bean
fun xsltViewResolver() = XsltViewResolver().apply {
setPrefix("/WEB-INF/xsl/")
setSuffix(".xslt")
}
}
----
======
include-code::./WebConfiguration[tag=snippet,indent=0]
[[mvc-view-xslt-controllercode]]
== Controller
@@ -120,17 +120,26 @@ See the sections on xref:web/webmvc-cors.adoc[CORS] and the xref:web/webmvc-cors
== URL Handler
[.small]#xref:web/webflux/reactive-spring.adoc#filters.url-handler[See equivalent in the Reactive stack]#
In previous Spring Framework versions, Spring MVC could be configured to ignore trailing slashes in URL paths
when mapping incoming requests on controller methods. This means that sending a "GET /home/" request would be
handled by a controller method annotated with `@GetMapping("/home")`.
You may want your controller endpoints to match routes with or without a trailing slash in the URL path.
For example, both "GET /home" and "GET /home/" should be handled by a controller method annotated with `@GetMapping("/home")`.
This option was deprecated in 6.0 and removed in 7.0, but applications are still expected to handle such
requests in a safe way. The `UrlHandlerFilter` Servlet filter has been designed for this purpose.
It can be configured to:
Spring provides `UrlHandlerFilter` that removes the trailing slash from URL paths to ensure a consistent view of paths with or without a trailing slash.
This is important to avoid a mismatch between URL-based authorization decisions and web framework request mappings.
The filter can remove the trailing slash in one of a couple of ways:
* respond with an HTTP redirect status when receiving URLs with trailing slashes, sending browsers to the non-trailing slash URL variant.
* wrap the request to act as if the request was sent without a trailing slash and continue the processing of the request.
* respond with an HTTP redirect status that sends clients to the same path without a trailing slash.
* wrap the request to remove the trailing slash.
NOTE: Historically Spring MVC supported trailing slash matching of URL paths.
This capability was deprecated in 6.0 for security reasons and removed in 7.0 with
`UrlHandlerFilter` providing a safer alternative.
Here is how you can instantiate and configure a `UrlHandlerFilter` for a blog application:
include-code::./UrlHandlerFilterConfiguration[tag=config,indent=0]
Keep in mind the following:
- the root path `"/"` is excluded from trailing slash handling.
- `@RequestMapping("/")` adds a trailing slash to a type-level mapping, and therefore will
not map when trailing slash handling applies; use `@RequestMapping` (no path attribute) instead.
@@ -45,7 +45,7 @@ By default, this converter supports `text/xml` and `application/xml`.
| `JacksonJsonHttpMessageConverter`
| An `HttpMessageConverter` implementation that can read and write JSON by using Jackson's `JsonMapper`.
You can customize JSON mapping as needed through the use of Jackson's provided annotations.
When you need further control (for cases where custom JSON serializers/deserializers need to be provided for specific types), you can inject a custom `JsonMapper` through the `JsonMapper` or `JsonMapper.Builder ` constructor parameters.
When you need further control (for cases where custom JSON serializers/deserializers need to be provided for specific types), you can inject a custom `JsonMapper` through the `JsonMapper` or `JsonMapper.Builder` constructor parameters.
By default, this converter supports `application/json`. This requires the `tools.jackson.core:jackson-databind` dependency.
| `JacksonXmlHttpMessageConverter`
@@ -32,9 +32,9 @@ any `@RequestMapping` method to render an RFC 9457 response. This is processed a
- The `status` property of `ProblemDetail` determines the HTTP status.
- The `instance` property of `ProblemDetail` is set from the current URL path, if not
already set.
- For content negotiation, the Jackson `HttpMessageConverter` prefers
"application/problem+json" over "application/json" when rendering a `ProblemDetail`,
and also falls back on it if no compatible media type is found.
- The Jackson JSON and XML codecs use "application/problem+json" or
"application/problem+xml" respectively as the producible media types for `ProblemDetail`
to ensure they are favored for content negotiation.
To enable RFC 9457 responses for Spring MVC exceptions and for any
`ErrorResponseException`, extend `ResponseEntityExceptionHandler` and declare it as an
@@ -66,6 +66,13 @@ from an existing `ProblemDetail`. This could be done centrally, for example, fro
`@ControllerAdvice` such as `ResponseEntityExceptionHandler` that re-creates the
`ProblemDetail` of an exception into a subclass with the additional non-standard fields.
TIP: In Spring Boot, the `spring.mvc.problemdetails.enabled` property autoconfigures
a `ResponseEntityExceptionHandler` that handles built-in exceptions with problem details.
In that case, you may prefer to create another `@ControllerAdvice` instead of extending
`ResponseEntityExceptionHandler` if you want to take over the handling of a specific
built-in exception. You'll need to ensure your handler is ordered ahead of the one
configured by Spring Boot whose order is 0.
[[mvc-ann-rest-exceptions-i18n]]
== Customization and i18n
@@ -184,7 +191,7 @@ Message codes and arguments for each error are also resolved via `MessageSource`
|===
NOTE: Unlike other exceptions, the message arguments for
`MethodArgumentValidException` and `HandlerMethodValidationException` are based on a list of
`MethodArgumentNotValidException` and `HandlerMethodValidationException` are based on a list of
`MessageSourceResolvable` errors that can also be customized through a
xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource]
resource bundle. See
@@ -424,9 +424,9 @@ is used to map requests.
Once API versioning is enabled, you can begin to map requests with versions.
The `@RequestMapping` `version` attribute supports the following:
- No value -- matches any version
- Fixed version ("1.2") -- matches the given version only
- Baseline version ("1.2+") -- matches the given version and above
- No value -- matches any version, but is superseded by a more specific version match
If multiple controller methods have a version less than or equal to the request version,
the highest of those, and closest to the request version, is the one considered,
@@ -485,6 +485,11 @@ A request with version `"1.6"` does not have a match. (1) and (3) do match, but
superseded by (4), which allows only a strict match, and therefore does not match.
In this scenario, a `NotAcceptableApiVersionException` results in a 400 response.
Controller methods without a version are intended to support clients created before a
versioned alternative was introduced. Therefore, even though an unversioned controller
method is considered a match for any version, it is in fact given the lowest priority,
and is effectively superseded by any alternative controller method with a version.
NOTE: The above assumes the request version is a
xref:web/webmvc/mvc-config/api-version.adoc["supported" version], or otherwise it
would fail.
@@ -550,53 +555,7 @@ You can programmatically register handler methods, which you can use for dynamic
registrations or for advanced cases, such as different instances of the same handler
under different URLs. The following example registers a handler method:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
public class MyConfig {
@Autowired
public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler) // <1>
throws NoSuchMethodException {
RequestMappingInfo info = RequestMappingInfo
.paths("/user/{id}").methods(RequestMethod.GET).build(); // <2>
Method method = UserHandler.class.getMethod("getUser", Long.class); // <3>
mapping.registerMapping(info, handler, method); // <4>
}
}
----
<1> Inject the target handler and the handler mapping for controllers.
<2> Prepare the request mapping meta data.
<3> Get the handler method.
<4> Add the registration.
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@Configuration
class MyConfig {
@Autowired
fun setHandlerMapping(mapping: RequestMappingHandlerMapping, handler: UserHandler) { // <1>
val info = RequestMappingInfo.paths("/user/{id}").methods(RequestMethod.GET).build() // <2>
val method = UserHandler::class.java.getMethod("getUser", Long::class.java) // <3>
mapping.registerMapping(info, handler, method) // <4>
}
}
----
<1> Inject the target handler and the handler mapping for controllers.
<2> Prepare the request mapping meta data.
<3> Get the handler method.
<4> Add the registration.
======
include-code::./MyConfiguration[tag=snippet,indent=0]
@@ -14,55 +14,12 @@ In turn, the `DispatcherServlet` uses Spring configuration to discover
the delegate components it needs for request mapping, view resolution, exception
handling, xref:web/webmvc/mvc-servlet/special-bean-types.adoc[and more].
The following example of the Java configuration registers and initializes
The following example shows the programmatic registration and initialization of
the `DispatcherServlet`, which is auto-detected by the Servlet container
(see xref:web/webmvc/mvc-servlet/container-config.adoc[Servlet Config]):
(see xref:web/webmvc/mvc-servlet/container-config.adoc[Servlet Config]), and the
equivalent `web.xml`:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) {
// Load Spring web application configuration
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(AppConfig.class);
// Create and register the DispatcherServlet
DispatcherServlet servlet = new DispatcherServlet(context);
ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);
registration.setLoadOnStartup(1);
registration.addMapping("/app/*");
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
class MyWebApplicationInitializer : WebApplicationInitializer {
override fun onStartup(servletContext: ServletContext) {
// Load Spring web application configuration
val context = AnnotationConfigWebApplicationContext()
context.register(AppConfig::class.java)
// Create and register the DispatcherServlet
val servlet = DispatcherServlet(context)
val registration = servletContext.addServlet("app", servlet)
registration.setLoadOnStartup(1)
registration.addMapping("/app/*")
}
}
----
======
include-code::./MyWebApplicationInitializer[tag=snippet,indent=0]
NOTE: In addition to using the ServletContext API directly, you can also extend
`AbstractAnnotationConfigDispatcherServletInitializer` and override specific methods
@@ -73,39 +30,6 @@ alternative to `AnnotationConfigWebApplicationContext`. See the
{spring-framework-api}/web/context/support/GenericWebApplicationContext.html[`GenericWebApplicationContext`]
javadoc for details.
The following example of `web.xml` configuration registers and initializes the `DispatcherServlet`:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<web-app>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/app-context.xml</param-value>
</context-param>
<servlet>
<servlet-name>app</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value></param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>app</servlet-name>
<url-pattern>/app/*</url-pattern>
</servlet-mapping>
</web-app>
----
NOTE: Spring Boot follows a different initialization sequence. Rather than hooking into
the lifecycle of the Servlet container, Spring Boot uses Spring configuration to
bootstrap itself and the embedded Servlet container. `Filter` and `Servlet` declarations
@@ -5,48 +5,7 @@ In a Servlet environment, you have the option of configuring the Servlet contain
programmatically as an alternative or in combination with a `web.xml` file.
The following example registers a `DispatcherServlet`:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
import org.springframework.web.WebApplicationInitializer;
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext container) {
XmlWebApplicationContext appContext = new XmlWebApplicationContext();
appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
ServletRegistration.Dynamic registration = container.addServlet("dispatcher", new DispatcherServlet(appContext));
registration.setLoadOnStartup(1);
registration.addMapping("/");
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
import org.springframework.web.WebApplicationInitializer
class MyWebApplicationInitializer : WebApplicationInitializer {
override fun onStartup(container: ServletContext) {
val appContext = XmlWebApplicationContext()
appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml")
val registration = container.addServlet("dispatcher", DispatcherServlet(appContext))
registration.setLoadOnStartup(1)
registration.addMapping("/")
}
}
----
======
include-code::./MyWebApplicationInitializer[tag=snippet,indent=0]
`WebApplicationInitializer` is an interface provided by Spring MVC that ensures your
implementation is detected and automatically used to initialize any Servlet 3 container.
@@ -55,144 +14,21 @@ An abstract base class implementation of `WebApplicationInitializer` named
`DispatcherServlet` by overriding methods to specify the servlet mapping and the
location of the `DispatcherServlet` configuration.
This is recommended for applications that use Java-based Spring configuration, as the
This is recommended for applications that use programmatic Spring configuration, as the
following example shows:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return null;
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { MyWebConfig.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
class MyWebAppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() {
override fun getRootConfigClasses(): Array<Class<*>>? {
return null
}
override fun getServletConfigClasses(): Array<Class<*>>? {
return arrayOf(MyWebConfig::class.java)
}
override fun getServletMappings(): Array<String> {
return arrayOf("/")
}
}
----
======
include-code::./MyWebAppInitializer[tag=snippet,indent=0]
If you use XML-based Spring configuration, you should extend directly from
`AbstractDispatcherServletInitializer`, as the following example shows:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {
@Override
protected WebApplicationContext createRootApplicationContext() {
return null;
}
@Override
protected WebApplicationContext createServletApplicationContext() {
XmlWebApplicationContext cxt = new XmlWebApplicationContext();
cxt.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
return cxt;
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
class MyWebAppInitializer : AbstractDispatcherServletInitializer() {
override fun createRootApplicationContext(): WebApplicationContext? {
return null
}
override fun createServletApplicationContext(): WebApplicationContext {
return XmlWebApplicationContext().apply {
setConfigLocation("/WEB-INF/spring/dispatcher-config.xml")
}
}
override fun getServletMappings(): Array<String> {
return arrayOf("/")
}
}
----
======
include-code::./MyXmlDispatcherServletInitializer[tag=snippet,indent=0]
`AbstractDispatcherServletInitializer` also provides a convenient way to add `Filter`
instances and have them be automatically mapped to the `DispatcherServlet`, as the
following example shows:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {
// ...
@Override
protected Filter[] getServletFilters() {
return new Filter[] {
new HiddenHttpMethodFilter(), new CharacterEncodingFilter() };
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
class MyWebAppInitializer : AbstractDispatcherServletInitializer() {
// ...
override fun getServletFilters(): Array<Filter> {
return arrayOf(HiddenHttpMethodFilter(), CharacterEncodingFilter())
}
}
----
======
include-code::./MyFilterDispatcherServletInitializer[tag=snippet,indent=0]
Each filter is added with a default name based on its concrete type and automatically
mapped to the `DispatcherServlet`.
@@ -20,91 +20,15 @@ are effectively inherited and can be overridden (that is, re-declared) in the Se
child `WebApplicationContext`, which typically contains beans local to the given `Servlet`.
The following image shows this relationship:
image::mvc-context-hierarchy.png[]
image::mvc-context-hierarchy.png[width=60%,align="center"]
The following example configures a `WebApplicationContext` hierarchy:
The following example configures a `WebApplicationContext` hierarchy, and the equivalent `web.xml`:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] { RootConfig.class };
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { App1Config.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/app1/*" };
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
class MyWebAppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() {
override fun getRootConfigClasses(): Array<Class<*>> {
return arrayOf(RootConfig::class.java)
}
override fun getServletConfigClasses(): Array<Class<*>> {
return arrayOf(App1Config::class.java)
}
override fun getServletMappings(): Array<String> {
return arrayOf("/app1/*")
}
}
----
======
include-code::./MyWebAppInitializer[tag=snippet,indent=0]
TIP: If an application context hierarchy is not required, applications can return all
configuration through `getRootConfigClasses()` and `null` from `getServletConfigClasses()`.
The following example shows the `web.xml` equivalent:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<web-app>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/root-context.xml</param-value>
</context-param>
<servlet>
<servlet-name>app1</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/app1-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>app1</servlet-name>
<url-pattern>/app1/*</url-pattern>
</servlet-mapping>
</web-app>
----
TIP: If an application context hierarchy is not required, applications may configure a
"`root`" context only and leave the `contextConfigLocation` Servlet parameter empty.
@@ -74,42 +74,7 @@ Servlet container makes an ERROR dispatch within the container to the configured
to a `@Controller`, which could be implemented to return an error view name with a model
or to render a JSON response, as the following example shows:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@RestController
public class ErrorController {
@RequestMapping(path = "/error")
public Map<String, Object> handle(HttpServletRequest request) {
Map<String, Object> map = new HashMap<>();
map.put("status", request.getAttribute("jakarta.servlet.error.status_code"));
map.put("reason", request.getAttribute("jakarta.servlet.error.message"));
return map;
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@RestController
class ErrorController {
@RequestMapping(path = ["/error"])
fun handle(request: HttpServletRequest): Map<String, Any> {
val map = HashMap<String, Any>()
map["status"] = request.getAttribute("jakarta.servlet.error.status_code")
map["reason"] = request.getAttribute("jakarta.servlet.error.message")
return map
}
}
----
======
include-code::./ErrorController[tag=snippet,indent=0]
TIP: The Servlet API does not provide a way to create error page mappings in Java. You can,
however, use both a `WebApplicationInitializer` and a minimal `web.xml`.
@@ -54,44 +54,9 @@ information.
This locale resolver inspects a `Cookie` that might exist on the client to see if a
`Locale` or `TimeZone` is specified. If so, it uses the specified details. By using the
properties of this locale resolver, you can specify the name of the cookie as well as the
maximum age. The following example defines a `CookieLocaleResolver`:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver">
<property name="cookieName" value="clientlanguage"/>
<!-- in seconds. If set to -1, the cookie is not persisted (deleted when browser shuts down) -->
<property name="cookieMaxAge" value="100000"/>
</bean>
----
The following table describes the properties `CookieLocaleResolver`:
[[mvc-cookie-locale-resolver-props-tbl]]
.CookieLocaleResolver properties
[cols="1,1,4"]
|===
| Property | Default | Description
| `cookieName`
| class name + LOCALE
| The name of the cookie
| `cookieMaxAge`
| Servlet container default
| The maximum time a cookie persists on the client. If `-1` is specified, the
cookie will not be persisted. It is available only until the client shuts down
the browser.
| `cookiePath`
| /
| Limits the visibility of the cookie to a certain part of your site. When `cookiePath` is
specified, the cookie is visible only to that path and the paths below it.
|===
maximum age. The following example defines a `CookieLocaleResolver` bean:
include-code::./WebConfiguration[tag=snippet,indent=0]
[[mvc-localeresolver-session]]
== Session Resolver
@@ -115,31 +80,7 @@ You can enable changing of locales by adding the `LocaleChangeInterceptor` to on
accordingly, calling the `setLocale` method on the `LocaleResolver` in the dispatcher's
application context. The next example shows that calls to all `{asterisk}.view` resources
that contain a parameter named `siteLanguage` now changes the locale. So, for example,
a request for the URL, `https://www.sf.net/home.view?siteLanguage=nl`, changes the site
a request for the URL `https://domain.com/home.view?siteLanguage=nl` changes the site
language to Dutch. The following example shows how to intercept the locale:
[source,xml,indent=0,subs="verbatim"]
----
<bean id="localeChangeInterceptor"
class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
<property name="paramName" value="siteLanguage"/>
</bean>
<bean id="localeResolver"
class="org.springframework.web.servlet.i18n.CookieLocaleResolver"/>
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="interceptors">
<list>
<ref bean="localeChangeInterceptor"/>
</list>
</property>
<property name="mappings">
<value>/**/*.view=someController</value>
</property>
</bean>
----
include-code::./WebConfiguration[tag=snippet,indent=0,chomp=-tags]
@@ -25,62 +25,7 @@ through the `enableLoggingRequestDetails` property on `DispatcherServlet`.
The following example shows how to do so by using Java configuration:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
public class MyInitializer
extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return ... ;
}
@Override
protected Class<?>[] getServletConfigClasses() {
return ... ;
}
@Override
protected String[] getServletMappings() {
return ... ;
}
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
registration.setInitParameter("enableLoggingRequestDetails", "true");
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
class MyInitializer : AbstractAnnotationConfigDispatcherServletInitializer() {
override fun getRootConfigClasses(): Array<Class<*>>? {
return ...
}
override fun getServletConfigClasses(): Array<Class<*>>? {
return ...
}
override fun getServletMappings(): Array<String> {
return ...
}
override fun customizeRegistration(registration: ServletRegistration.Dynamic) {
registration.setInitParameter("enableLoggingRequestDetails", "true")
}
}
----
======
include-code::./MyInitializer[tag=snippet,indent=0]
@@ -26,43 +26,7 @@ To do so:
The following example shows how to set a `MultipartConfigElement` on the Servlet registration:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
// ...
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
// Optionally also set maxFileSize, maxRequestSize, fileSizeThreshold
registration.setMultipartConfig(new MultipartConfigElement("/tmp"));
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
class AppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() {
// ...
override fun customizeRegistration(registration: ServletRegistration.Dynamic) {
// Optionally also set maxFileSize, maxRequestSize, fileSizeThreshold
registration.setMultipartConfig(MultipartConfigElement("/tmp"))
}
}
----
======
include-code::./AppInitializer[tag=snippet,indent=0]
Once the Servlet multipart configuration is in place, you can add a bean of type
`StandardServletMultipartResolver` with a name of `multipartResolver`.
@@ -152,26 +152,9 @@ from the iframe. By default, the iframe is set to download the SockJS client
from a CDN location. It is a good idea to configure this option to use
a URL from the same origin as the application.
The following example shows how to do so in Java configuration:
The following example shows how to configure it:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").withSockJS()
.setClientLibraryUrl("http://localhost:8080/myapp/js/sockjs-client.js");
}
// ...
}
----
The XML namespace provides a similar option through the `<websocket:sockjs>` element.
include-code::./WebSocketConfiguration[tag=snippet,indent=0]
NOTE: During initial development, do enable the SockJS client `devel` mode that prevents
the browser from caching SockJS requests (like the iframe) that would otherwise
@@ -307,23 +290,4 @@ jettyHttpClient.setExecutor(new QueuedThreadPool(1000));
The following example shows the server-side SockJS-related properties (see javadoc for details)
that you should also consider customizing:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/sockjs").withSockJS()
.setStreamBytesLimit(512 * 1024) <1>
.setHttpMessageCacheSize(1000) <2>
.setDisconnectDelay(30 * 1000); <3>
}
// ...
}
----
<1> Set the `streamBytesLimit` property to 512KB (the default is 128KB -- `128 * 1024`).
<2> Set the `httpMessageCacheSize` property to 1,000 (the default is `100`).
<3> Set the `disconnectDelay` property to 30 property seconds (the default is five seconds
-- `5 * 1000`).
include-code::./WebSocketConfiguration[tag=snippet,indent=0]
@@ -59,7 +59,7 @@ destination:/queue/trade
content-type:application/json
content-length:44
{"action":"BUY","ticker":"MMM","shares",44}^@
{"action":"BUY","ticker":"MMM","shares":44}^@
----
After the execution, the server can
@@ -268,9 +268,9 @@ response to a browser.
`UriComponentsBuilder` exposes encoding options at two levels:
* {spring-framework-api}/web/util/UriComponentsBuilder.html#encode--[UriComponentsBuilder#encode()]:
* {spring-framework-api}/web/util/UriComponentsBuilder.html#encode()[UriComponentsBuilder#encode()]:
Pre-encodes the URI template first and then strictly encodes URI variables when expanded.
* {spring-framework-api}/web/util/UriComponents.html#encode--[UriComponents#encode()]:
* {spring-framework-api}/web/util/UriComponents.html#encode()[UriComponents#encode()]:
Encodes URI components _after_ URI variables are expanded.
Both options replace non-ASCII and illegal characters with escaped octets. However, the first option
+5 -5
View File
@@ -1,11 +1,11 @@
{
"dependencies": {
"antora": "3.2.0-alpha.4",
"@antora/atlas-extension": "1.0.0-alpha.2",
"@antora/collector-extension": "1.0.0-alpha.3",
"antora": "3.2.0-alpha.11",
"@antora/atlas-extension": "1.0.0-alpha.5",
"@antora/collector-extension": "1.0.2",
"@asciidoctor/tabs": "1.0.0-beta.6",
"@springio/antora-extensions": "1.14.7",
"fast-xml-parser": "4.5.2",
"@springio/asciidoctor-extensions": "1.0.0-alpha.10"
"fast-xml-parser": "5.3.4",
"@springio/asciidoctor-extensions": "1.0.0-alpha.17"
}
}
+12 -15
View File
@@ -200,6 +200,7 @@
See the License for the specific language governing permissions and
limitations under the License.
=======================================================================
SPRING FRAMEWORK ${version} SUBCOMPONENTS:
@@ -212,7 +213,7 @@ code for these subcomponents is subject to the terms and
conditions of the following licenses.
>>> ASM 9.1 (org.ow2.asm:asm:9.1, org.ow2.asm:asm-commons:9.1):
>>> ASM 9.9.1 (org.ow2.asm:asm:9.9.1):
Copyright (c) 2000-2011 INRIA, France Telecom
All rights reserved.
@@ -249,32 +250,28 @@ Copyright (c) 1999-2009, OW2 Consortium <https://www.ow2.org/>
>>> CGLIB 3.3 (cglib:cglib:3.3):
Per the LICENSE file in the CGLIB JAR distribution downloaded from
https://github.com/cglib/cglib/releases/download/RELEASE_3_3_0/cglib-3.3.0.jar,
CGLIB 3.3 is licensed under the Apache License, version 2.0, the text of which
is included above.
Per the LICENSE file in the CGLIB distribution, CGLIB 3.3 is licensed
under the Apache License, version 2.0, the text of which is included above.
>>> JavaPoet 1.13.0 (com.squareup:javapoet:1.13.0):
>>> JavaPoet 0.10.0 (com.palantir.javapoet:javapoet:0.10.0):
Per the LICENSE file in the JavaPoet JAR distribution downloaded from
https://github.com/square/javapoet/archive/refs/tags/javapoet-1.13.0.zip,
JavaPoet 1.13.0 is licensed under the Apache License, version 2.0, the text of
which is included above.
Per the LICENSE file in the JavaPoet distribution, JavaPoet 0.10.0 is licensed
under the Apache License, version 2.0, the text of which is included above.
>>> Objenesis 3.4 (org.objenesis:objenesis:3.4):
>>> Objenesis 3.5 (org.objenesis:objenesis:3.5):
Per the LICENSE file in the Objenesis ZIP distribution downloaded from
http://objenesis.org/download.html, Objenesis 3.4 is licensed under the
Per the LICENSE file in the Objenesis distribution downloaded from
http://objenesis.org/download.html, Objenesis 3.5 is licensed under the
Apache License, version 2.0, the text of which is included above.
Per the NOTICE file in the Objenesis ZIP distribution downloaded from
Per the NOTICE file in the Objenesis distribution downloaded from
http://objenesis.org/download.html and corresponding to section 4d of the
Apache License, Version 2.0, in this case for Objenesis:
Objenesis
Copyright 2006-2019 Joe Walnes, Henri Tremblay, Leonardo Mesquita
Copyright 2006-2026 Joe Walnes, Henri Tremblay, Leonardo Mesquita
===============================================================================
@@ -0,0 +1,54 @@
/*
* Copyright 2025-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.testing.resttestclient.assertj;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.client.ExchangeResult;
import org.springframework.test.web.servlet.client.RestTestClient;
import org.springframework.test.web.servlet.client.assertj.RestTestClientResponse;
import static org.assertj.core.api.Assertions.assertThat;
public class AssertJTests {
RestTestClient client;
@Test
void withSpec() {
// tag::withSpec[]
RestTestClient.ResponseSpec spec = client.get().uri("/persons").exchange();
RestTestClientResponse response = RestTestClientResponse.from(spec);
assertThat(response).hasStatusOk();
assertThat(response).hasContentTypeCompatibleWith(MediaType.TEXT_PLAIN);
// end::withSpec[]
}
@Test
void withResult() {
// tag::withResult[]
ExchangeResult result = client.get().uri("/persons").exchange().returnResult();
RestTestClientResponse response = RestTestClientResponse.from(result);
assertThat(response).hasStatusOk();
assertThat(response).hasContentTypeCompatibleWith(MediaType.TEXT_PLAIN);
// end::withResult[]
}
}
@@ -0,0 +1,37 @@
/*
* Copyright 2025-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.testing.resttestclient.contextconfig;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.web.servlet.client.RestTestClient;
import org.springframework.web.context.WebApplicationContext;
@SpringJUnitConfig(WebConfig.class) // Specify the configuration to load
public class RestClientContextTests {
RestTestClient client;
@BeforeEach
void setUp(WebApplicationContext context) { // Inject the configuration
// Create the `RestTestClient`
client = RestTestClient.bindToApplicationContext(context).build();
}
}
@@ -0,0 +1,20 @@
/*
* Copyright 2025-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.testing.resttestclient.contextconfig;
public class WebConfig {
}
@@ -0,0 +1,50 @@
/*
* Copyright 2025-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.testing.resttestclient.json;
import org.junit.jupiter.api.Test;
import org.springframework.test.web.servlet.client.RestTestClient;
public class JsonTests {
RestTestClient client;
@Test
void jsonBody() {
// tag::jsonBody[]
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody()
.json("{\"name\":\"Jane\"}");
// end::jsonBody[]
}
@Test
void jsonPath() {
// tag::jsonPath[]
client.get().uri("/persons")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$[0].name").isEqualTo("Jane")
.jsonPath("$[1].name").isEqualTo("Jason");
// end::jsonPath[]
}
}
@@ -0,0 +1,56 @@
/*
* Copyright 2025-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.testing.resttestclient.nocontent;
import org.junit.jupiter.api.Test;
import org.springframework.test.web.servlet.client.RestTestClient;
public class NoContentTests {
RestTestClient client;
@Test
void emptyBody() {
Person person = new Person("Jane");
// tag::emptyBody[]
client.post().uri("/persons")
.body(person)
.exchange()
.expectStatus().isCreated()
.expectBody().isEmpty();
// end::emptyBody[]
}
@Test
void ignoreBody() {
Person person = new Person("Jane");
// tag::ignoreBody[]
client.post().uri("/persons")
.body(person)
.exchange()
.expectStatus().isCreated()
.expectBody(Void.class);
// end::ignoreBody[]
}
record Person(String name) {
}
}
@@ -0,0 +1,85 @@
/*
* Copyright 2025-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.testing.resttestclient.workflow;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.client.EntityExchangeResult;
import org.springframework.test.web.servlet.client.RestTestClient;
public class RestClientWorkflowTests {
RestTestClient client;
@Test
void workflowTest() {
// tag::test[]
client.get().uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBody();
// end::test[]
}
@Test
void softAssertions() {
// tag::soft-assertions[]
client.get().uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectAll(
spec -> spec.expectStatus().isOk(),
spec -> spec.expectHeader().contentType(MediaType.APPLICATION_JSON)
);
// end::soft-assertions[]
}
@Test
void consumeWith() {
// tag::consume[]
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody(Person.class)
.consumeWith(result -> {
// custom assertions (for example, AssertJ)...
});
// end::consume[]
}
@Test
void returnResult() {
// tag::result[]
EntityExchangeResult<Person> result = client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody(Person.class)
.returnResult();
Person person = result.getResponseBody();
HttpHeaders requestHeaders = result.getRequestHeaders();
// end::result[]
}
record Person(String name) {
}
}
@@ -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.web.mvccorsglobal;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
// tag::snippet[]
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://domain1.com", "https://domain2.com")
.allowedMethods("GET", "PUT")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true)
.maxAge(3600);
// Add more mappings...
}
}
// end::snippet[]
@@ -0,0 +1,69 @@
/*
* Copyright 2026-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.web.webflux.controller.annmethods.partevent;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.codec.multipart.FilePartEvent;
import org.springframework.http.codec.multipart.FormPartEvent;
import org.springframework.http.codec.multipart.PartEvent;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
public class PartEventController {
// tag::snippet[]
@PostMapping("/")
public void handle(@RequestBody Flux<PartEvent> allPartEvents) {
// The final PartEvent for a particular part will have isLast() set to true, and can be
// followed by additional events belonging to subsequent parts.
// This makes the isLast property suitable as a predicate for the Flux::windowUntil operator, to
// split events from all parts into windows that each belong to a single part.
allPartEvents.windowUntil(PartEvent::isLast)
// The Flux::switchOnFirst operator allows you to see whether you are handling
// a form field or file upload
.concatMap(p -> p.switchOnFirst((signal, partEvents) -> {
if (signal.hasValue()) {
PartEvent event = signal.get();
if (event instanceof FormPartEvent formEvent) {
String value = formEvent.value();
// Handling of the form field
}
else if (event instanceof FilePartEvent fileEvent) {
String filename = fileEvent.filename();
// The body contents must be completely consumed, relayed, or released to avoid memory leaks
Flux<DataBuffer> contents = partEvents.map(PartEvent::content);
// Handling of the file upload
}
else {
return Mono.error(new RuntimeException("Unexpected event: " + event));
}
}
else {
return partEvents; // either complete or error signal
}
return Mono.empty();
}));
}
// end::snippet[]
}
@@ -0,0 +1,19 @@
/*
* 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.web.webfluxfnhandlerclasses;
public record Person(String name) { }
@@ -0,0 +1,67 @@
/*
* 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.web.webfluxfnhandlerclasses;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
// tag::snippet[]
public class PersonHandler {
private final PersonRepository repository;
public PersonHandler(PersonRepository repository) {
this.repository = repository;
}
// listPeople is a handler function that returns all Person objects found
// in the repository as JSON
public Mono<ServerResponse> listPeople(ServerRequest request) {
Flux<Person> people = repository.allPeople();
return ok().contentType(APPLICATION_JSON).body(people, Person.class);
}
// createPerson is a handler function that stores a new Person contained
// in the request body.
// Note that PersonRepository.savePerson(Person) returns Mono<Void>: an empty
// Mono that emits a completion signal when the person has been read from the
// request and stored. So we use the build(Publisher<Void>) method to send a
// response when that completion signal is received (that is, when the Person
// has been saved)
public Mono<ServerResponse> createPerson(ServerRequest request) {
Mono<Person> person = request.bodyToMono(Person.class);
return ok().build(repository.savePerson(person));
}
// getPerson is a handler function that returns a single person, identified by
// the id path variable. We retrieve that Person from the repository and create
// a JSON response, if it is found. If it is not found, we use switchIfEmpty(Mono<T>)
// to return a 404 Not Found response.
public Mono<ServerResponse> getPerson(ServerRequest request) {
int personId = Integer.valueOf(request.pathVariable("id"));
return repository.getPerson(personId)
.flatMap(person -> ok().contentType(APPLICATION_JSON).bodyValue(person))
.switchIfEmpty(ServerResponse.notFound().build());
}
}
// end::snippet[]
@@ -0,0 +1,29 @@
/*
* 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.web.webfluxfnhandlerclasses;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface PersonRepository {
Flux<Person> allPeople();
Mono<Void> savePerson(Mono<Person> person);
Mono<Person> getPerson(int id);
}
@@ -0,0 +1,55 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.docs.web.webfluxfnhandlerfilterfunction;
import org.springframework.docs.web.webfluxfnhandlerclasses.PersonHandler;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.http.HttpStatus.UNAUTHORIZED;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
public class RouterConfiguration {
public RouterFunction<ServerResponse> route(PersonHandler handler) {
// tag::snippet[]
SecurityManager securityManager = getSecurityManager();
RouterFunction<ServerResponse> route = RouterFunctions.route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople))
.POST(handler::createPerson))
.filter((request, next) -> {
if (securityManager.allowAccessTo(request.path())) {
return next.handle(request);
}
else {
return ServerResponse.status(UNAUTHORIZED).build();
}
}).build();
// end::snippet[]
return route;
}
SecurityManager getSecurityManager() {
return path -> false;
}
}
@@ -0,0 +1,22 @@
/*
* 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.web.webfluxfnhandlerfilterfunction;
public interface SecurityManager {
boolean allowAccessTo(String path);
}
@@ -0,0 +1,58 @@
/*
* 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.web.webfluxfnhandlervalidation;
import org.springframework.docs.web.webfluxfnhandlerclasses.Person;
import org.springframework.docs.web.webfluxfnhandlerclasses.PersonRepository;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebInputException;
import reactor.core.publisher.Mono;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
// tag::snippet[]
public class PersonHandler {
// Create Validator instance
private final Validator validator = new PersonValidator();
private final PersonRepository repository;
public PersonHandler(PersonRepository repository) {
this.repository = repository;
}
public Mono<ServerResponse> createPerson(ServerRequest request) {
// Apply validation
Mono<Person> person = request.bodyToMono(Person.class).doOnNext(this::validate);
return ok().build(repository.savePerson(person));
}
private void validate(Person person) {
Errors errors = new BeanPropertyBindingResult(person, "person");
validator.validate(person, errors);
if (errors.hasErrors()) {
// Raise exception for a 400 response
throw new ServerWebInputException(errors.toString());
}
}
}
// end::snippet[]
@@ -0,0 +1,34 @@
/*
* 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.web.webfluxfnhandlervalidation;
import org.springframework.docs.web.webfluxfnhandlerclasses.Person;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
public class PersonValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Person.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
// Validation logic
}
}
@@ -0,0 +1,36 @@
/*
* 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.web.webfluxfnpredicates;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
public class RouterConfiguration {
public RouterFunction<ServerResponse> route() {
// tag::snippet[]
RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
request -> ServerResponse.ok().bodyValue("Hello World")).build();
// end::snippet[]
return route;
}
}
@@ -0,0 +1,55 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.docs.web.webfluxfnrequest;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.codec.multipart.FilePartEvent;
import org.springframework.http.codec.multipart.FormPartEvent;
import org.springframework.http.codec.multipart.PartEvent;
import org.springframework.web.reactive.function.server.ServerRequest;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public class PartEventHandler {
public void handle(ServerRequest request) {
// tag::snippet[]
request.bodyToFlux(PartEvent.class).windowUntil(PartEvent::isLast)
.concatMap(p -> p.switchOnFirst((signal, partEvents) -> {
if (signal.hasValue()) {
PartEvent event = signal.get();
if (event instanceof FormPartEvent formEvent) {
String value = formEvent.value();
// handle form field
}
else if (event instanceof FilePartEvent fileEvent) {
String filename = fileEvent.filename();
Flux<DataBuffer> contents = partEvents.map(PartEvent::content);
// handle file upload
}
else {
return Mono.error(new RuntimeException("Unexpected event: " + event));
}
}
else {
return partEvents; // either complete or error signal
}
return Mono.empty();
}));
// end::snippet[]
}
}
@@ -0,0 +1,33 @@
/*
* 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.web.webfluxfnrequest;
import reactor.core.publisher.Mono;
import org.springframework.web.reactive.function.server.ServerRequest;
public class RequestHandler {
public void bind(ServerRequest request) {
// tag::snippet[]
Mono<Pet> pet = request.bind(Pet.class, dataBinder -> dataBinder.setAllowedFields("name"));
// end::snippet[]
}
record Pet(String name) { }
}
@@ -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.web.webfluxfnresponse;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
public class ResponseHandler {
public Mono<ServerResponse> createResponse() {
// tag::snippet[]
Mono<Person> person = getPerson();
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class);
// end::snippet[]
}
private Mono<Person> getPerson() {
return Mono.just(new Person("foo"));
}
record Person(String name) { }
}
@@ -0,0 +1,78 @@
/*
* 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.web.webfluxfnroutes;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.docs.web.webfluxfnhandlerclasses.Person;
import org.springframework.docs.web.webfluxfnhandlerclasses.PersonHandler;
import org.springframework.docs.web.webfluxfnhandlerclasses.PersonRepository;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
public class RouterConfiguration {
public RouterFunction<ServerResponse> routes() {
// tag::snippet[]
PersonRepository repository = getPersonRepository();
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> otherRoute = getOtherRoute();
RouterFunction<ServerResponse> route = route()
// GET /person/{id} with an Accept header that matches JSON is routed to PersonHandler.getPerson
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
// GET /person with an Accept header that matches JSON is routed to PersonHandler.listPeople
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
// POST /person with no additional predicates is mapped to PersonHandler.createPerson
.POST("/person", handler::createPerson)
// otherRoute is a router function that is created elsewhere and added to the route built
.add(otherRoute)
.build();
// end::snippet[]
return route;
}
PersonRepository getPersonRepository() {
return new PersonRepository() {
@Override
public Flux<Person> allPeople() {
return null;
}
@Override
public Mono<Void> savePerson(Mono<Person> person) {
return null;
}
@Override
public Mono<Person> getPerson(int id) {
return null;
}
};
}
RouterFunction<ServerResponse> getOtherRoute() {
return RouterFunctions.route().build();
}
}
@@ -19,6 +19,7 @@ package org.springframework.docs.web.webmvc.mvcconfig.mvcconfiginterceptors;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.handler.UserRoleAuthorizationInterceptor;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
// tag::snippet[]
@@ -28,6 +29,7 @@ public class WebConfiguration implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LocaleChangeInterceptor());
registry.addInterceptor(new UserRoleAuthorizationInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**");
}
}
// end::snippet[]
@@ -0,0 +1,48 @@
/*
* 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.web.webmvc.mvccontroller.mvcannrequestmappingregistration;
import java.lang.reflect.Method;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
// tag::snippet[]
@Configuration
public class MyConfiguration {
// Inject the target handler and the handler mapping for controllers
@Autowired
public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler)
throws NoSuchMethodException {
// Prepare the request mapping meta data
RequestMappingInfo info = RequestMappingInfo
.paths("/user/{id}").methods(RequestMethod.GET).build();
// Get the handler method
Method method = UserHandler.class.getMethod("getUser", Long.class);
// Add the registration
mapping.registerMapping(info, handler, method);
}
}
// end::snippet[]
@@ -0,0 +1,25 @@
/*
* 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.web.webmvc.mvccontroller.mvcannrequestmappingregistration;
public class UserHandler {
public void getUser(Long id) {
// ...
}
}
@@ -0,0 +1,23 @@
/*
* Copyright 2002-2025 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.web.webmvc.mvcservlet;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
}
@@ -0,0 +1,43 @@
/*
* Copyright 2002-2025 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.web.webmvc.mvcservlet;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletRegistration;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
// tag::snippet[]
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) {
// Load Spring web application configuration
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(AppConfig.class);
// Create and register the DispatcherServlet
DispatcherServlet servlet = new DispatcherServlet(context);
ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);
registration.setLoadOnStartup(1);
registration.addMapping("/app/*");
}
}
// end::snippet[]
@@ -0,0 +1,38 @@
/*
* Copyright 2002-2025 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.web.webmvc.mvcservlet.mvcanncustomerservletcontainererrorpage;
import java.util.HashMap;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
// tag::snippet[]
@RestController
public class ErrorController {
@RequestMapping(path = "/error")
public Map<String, Object> handle(HttpServletRequest request) {
Map<String, Object> map = new HashMap<>();
map.put("status", request.getAttribute("jakarta.servlet.error.status_code"));
map.put("reason", request.getAttribute("jakarta.servlet.error.message"));
return map;
}
}
// end::snippet[]
@@ -0,0 +1,51 @@
/*
* Copyright 2002-2025 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.web.webmvc.mvcservlet.mvccontainerconfig;
import jakarta.servlet.Filter;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.filter.HiddenHttpMethodFilter;
import org.springframework.web.servlet.support.AbstractDispatcherServletInitializer;
// tag::snippet[]
public class MyFilterDispatcherServletInitializer extends AbstractDispatcherServletInitializer {
@Override
protected Filter[] getServletFilters() {
return new Filter[] {
new HiddenHttpMethodFilter(), new CharacterEncodingFilter() };
}
// @fold:on
@Override
protected WebApplicationContext createServletApplicationContext() {
/**/return null;
}
@Override
protected String[] getServletMappings() {
/**/return new String[] { "/" };
}
@Override
protected WebApplicationContext createRootApplicationContext() {
/**/return null;
}
// @fold:off
}
// end::snippet[]
@@ -0,0 +1,41 @@
/*
* Copyright 2002-2025 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.web.webmvc.mvcservlet.mvccontainerconfig;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
// tag::snippet[]
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return null;
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { MyWebConfig.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
// end::snippet[]
class MyWebConfig {}
@@ -0,0 +1,38 @@
/*
* Copyright 2002-2025 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.web.webmvc.mvcservlet.mvccontainerconfig;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletRegistration;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.support.XmlWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
// tag::snippet[]
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext container) {
XmlWebApplicationContext appContext = new XmlWebApplicationContext();
appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
ServletRegistration.Dynamic registration = container.addServlet("dispatcher", new DispatcherServlet(appContext));
registration.setLoadOnStartup(1);
registration.addMapping("/");
}
}
// end::snippet[]
@@ -0,0 +1,43 @@
/*
* Copyright 2002-2025 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.web.webmvc.mvcservlet.mvccontainerconfig;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.XmlWebApplicationContext;
import org.springframework.web.servlet.support.AbstractDispatcherServletInitializer;
// tag::snippet[]
public class MyXmlDispatcherServletInitializer extends AbstractDispatcherServletInitializer {
@Override
protected WebApplicationContext createRootApplicationContext() {
return null;
}
@Override
protected WebApplicationContext createServletApplicationContext() {
XmlWebApplicationContext cxt = new XmlWebApplicationContext();
cxt.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
return cxt;
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
// end::snippet[]

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