Compare commits

..

156 Commits

Author SHA1 Message Date
Spring Builds 08bc1a050e Release v5.3.27 2023-04-13 08:43:55 +00:00
Sam Brannen 6bfb94a563 Disable variable assignment in SimpleEvaluationContext
This commit introduces infrastructure to differentiate between
programmatic setting of a variable in an EvaluationContext versus the
assignment of a variable within a SpEL expression using the assignment
operator (=). In addition, this commit disables variable assignment
within expressions when using the SimpleEvaluationContext.

Closes gh-30327
2023-04-13 10:20:06 +02:00
Sam Brannen ebc8265428 Limit SpEL expression length
This commit enforces a limit of the maximum size of a single SpEL
expression.

Closes gh-30329
2023-04-13 10:15:02 +02:00
Sam Brannen 86457464d7 Limit string concatenation in SpEL expressions
This commit introduces support for limiting the maximum length of a
string resulting from the concatenation operator (+) in SpEL
expressions.

Closes gh-30331
2023-04-13 10:14:18 +02:00
Sam Brannen be129dc171 Change max regex length in SpEL expressions to 1000
This commit changes the max regex length in SpEL expressions from 1024
to 1000 in order to consistently use "round" numbers for recently
introduced limits.

See gh-30265
2023-04-13 10:14:15 +02:00
Juergen Hoeller 8bb1b3eb44 Upgrade to Netty 4.1.91 and Checkstyle 10.9.3 2023-04-12 13:53:17 +02:00
Stephane Nicoll 6abd822e77 Upgrade to Reactor 2020.0.31
Closes gh-30315
2023-04-11 17:35:25 +02:00
rstoyanchev 1c43a4c7ab Fix regression in ReactorServerHttpRequest
Instead of a backport for cef916, this change simply undoes the
optimization that led to the regression.

Closes gh-30314
2023-04-11 15:12:54 +01:00
Sam Brannen 423f2215c2 Remove flaky assertion to fix build on JDK 17
Sometime between JDK 8 and JDK 17, the behavior for List::toArray()
changed. Specifically, the type returned for List<String> changed from
String[] to Object[].

This commit therefore removes an assertion against this particular
JDK-specific behavior. The affected test method retains additional
assertions along the same lines but which are not flaky.
2023-04-10 17:34:04 +02:00
Sébastien Deleuze 0bad69d5fb Fix SSE with indenting serializer in WebMvc.fn
This commit ensures that HTTP headers like "text/event-stream"
are correctly forwarded to the converter used in
SseServerResponse for proper pretty print handling.

Close gh-30302
2023-04-07 11:56:47 +02:00
Sam Brannen 6b19642256 Increase max regex length in SpEL expressions
This commit increases the max regex length in SpEL expressions from 256
to 1024 in order to support use cases where a regex may be rather long
without necessarily increasing the complexity of the regex.

Closes gh-30298
2023-04-06 18:02:36 +02:00
Sam Brannen 19bb4e96f2 Improve Javadoc for ObjectUtils.nullSafeConciseToString() 2023-04-06 17:36:32 +02:00
Sam Brannen 7a2594acda Add tests for corner cases
See gh-30290
See gh-30286
2023-04-05 15:28:45 +02:00
Sam Brannen 91c58af7af Introduce ObjectUtils.nullSafeConciseToString()
ObjectUtils.nullSafeToString(Object) exists for generating a string
representation of various objects in a "null-safe" manner, including
support for object graphs, collections, etc.

However, there are times when we would like to generate a "concise",
null-safe string representation that does not include an entire object
graph (or potentially a collection of object graphs).

This commit introduces ObjectUtils.nullSafeConciseToString(Object) to
address this need and makes use of the new feature in FieldError and
ConversionFailedException.

Closes gh-30286
2023-04-05 15:25:59 +02:00
Sam Brannen 0a1aeafe08 Introduce StringUtils.truncate()
StringUtils.truncate() serves as central, consistent way for truncating
strings used in log messages and exception failure messages, for
immediate use in LogFormatUtils and ObjectUtils.

See gh-30286
Closes gh-30290
2023-04-05 15:15:53 +02:00
Simon Baslé 0e69bac7b0 Polish 0cfbf60: fix a test for Java 8 compatibility 2023-04-04 15:25:27 +02:00
Simon Baslé 0cfbf6036c Rename MockMVC matcher methods to prevent regression in user tests
This commit changes the name of two recently introduced methods in the
`MockRestRequestMatchers` class for header and queryParam. These have
been found to cause false negatives in user tests, due to the new
overload taking precedence in some cases.

Namely, using a `Matcher` factory method which can apply to both `List`
and `String` will cause the compiler to select the newest list overload,
by instantiating a `Matcher<Object>`.

This can cause false negatives in user tests, failing tests that used
to pass because the Matcher previously applied to the first String in
the header or queryParam value list. For instance, `equalsTo("a")`.

The new overloads are recent enough and this has enough potential to
cause an arbitrary number of user tests to fail that we break the API
to eliminate the ambiguity, by renaming the methods with a `*List`
suffix.

See gh-30220
See gh-30238
Closes gh-30235
2023-04-04 15:07:41 +02:00
Sébastien Deleuze e931fdc5c2 Make HttpComponentsHeadersAdapter#getFirst nullable
Backport of gh-30267

Closes gh-30269
2023-04-03 09:06:32 +02:00
Sébastien Deleuze 3a1681357c Fix PathVariable reference documentation code snippets
Closes gh-30258
2023-03-31 15:51:45 +02:00
Juergen Hoeller a93382dbbf Propagate HttpStreamResetException itself if cause is null
Closes gh-30245

(cherry picked from commit 8fca258207)
2023-03-30 19:32:29 +02:00
Sébastien Deleuze 79c5ef88f5 Refine generic type management in AbstractMessageWriterResultHandler
This commit updates AbstractMessageWriterResultHandler#writeBody in
order to use the declared bodyParameter instead of
ResolvableType.forInstance(body) when the former has unresolvable
generics.

Closes gh-30215
2023-03-30 18:26:15 +02:00
Juergen Hoeller 0c80e5f9e6 Use JdkDynamicAopProxy class loader instead of JDK bootstrap/platform loader
Closes gh-30115

(cherry picked from commit 7e905e3e00)
2023-03-29 14:07:50 +02:00
Juergen Hoeller 7ad01a94d6 Use MethodInvocationInfo class loader in case of JDK platform loader as well
Closes gh-30210

(cherry picked from commit 491ae1e3be)
2023-03-29 13:56:23 +02:00
Juergen Hoeller 5d6d653cbd Use MethodInvocationInfo class loader in case of core JDK interface type
Closes gh-30210

(cherry picked from commit ce2689eead)
2023-03-28 13:56:02 +02:00
Johnny Lim 9868e888a5 Update versions in Javadoc
Closes gh-30191
2023-03-25 17:17:28 +01:00
ghostg00 6f6eb3d996 Fix example in Javadoc for @EnableWebSocket
The `echoWebSocketHandler()` method is not defined in the
`WebSocketConfigurer` interface and should therefore be annotated with
`@Bean` instead of `@Override`.

Closes gh-30187
2023-03-24 17:55:24 +01:00
Giuseppe a94c50e294 Handle all exceptions for stored proc out param retrieval in SharedEntityManagerCreator
Prior to this commit, the EntityManager was not closed in
SharedEntityManagerCreator.DeferredQueryInvocationHandler's
invoke(Object, Method, Object[]) method if an invocation of
getOutputParameterValue(*) threw an exception other than
IllegalArgumentException, which could lead to a connection leak.

This commit addresses this by catching RuntimeException instead of
IllegalArgumentException.

Closes gh-30164
2023-03-22 15:44:06 +01:00
Kukri b8c1255ee0 Fix anchor in link to "Web on Reactive Stack" chapter
Closes gh-30163
2023-03-22 11:35:12 +01:00
Spring Builds 129062034b Next development version (v5.3.27-SNAPSHOT) 2023-03-20 10:05:15 +00:00
rstoyanchev eafe3afe11 Polishing and minor refactoring in HandlerMappingIntrospector
Closes gh-30128
2023-03-20 08:38:11 +00:00
Sam Brannen 26e0343c16 Improve diagnostics in SpEL for matches operator
Supplying a large regular expression to the `matches` operator in a
SpEL expression can result in errors that are not very helpful to the
user.

This commit improves the diagnostics in SpEL for the `matches` operator
by throwing a SpelEvaluationException with a meaningful error message
to better assist the user.

Closes gh-30145
2023-03-20 00:07:01 +01:00
Sam Brannen 4d5e7207f2 Improve diagnostics in SpEL for repeated text
Attempting to create repeated text in a SpEL expression using the
repeat operator can result in errors that are not very helpful to the
user.

This commit improves the diagnostics in SpEL for the repeat operator by
throwing a SpelEvaluationException with a meaningful error message in
order to better assist the user.

Closes gh-30143
2023-03-20 00:06:54 +01:00
Sam Brannen 430fc25aca Increase scope of regex pattern cache for the SpEL matches operator
Prior to this commit, the pattern cache for the SpEL `matches` operator
only applied to expressions such as the following where the same
`matches` operator is invoked multiple times with different input:

  "map.keySet().?[#this matches '.+xyz']"

The pattern cache did not apply to expressions such as the following
where the same pattern ('.+xyz') is used in multiple `matches`
operations:

  "foo matches '.+xyz' AND bar matches '.+xyz'"

This commit addresses this by moving the instance of the pattern cache
map from OperatorMatches to InternalSpelExpressionParser so that the
cache can be reused for all `matches` operations for the given parser.

Closes gh-30141
2023-03-20 00:06:46 +01:00
Sam Brannen 0882ca57d4 Polishing 2023-03-20 00:06:32 +01:00
Sam Brannen 94bbf85c0e Stop printing to System.out in SpEL tests 2023-03-20 00:06:24 +01:00
Juergen Hoeller 2c2ef12f68 Upgrade to Netty 4.1.90 and Checkstyle 10.9.1 2023-03-17 18:10:02 +01:00
Juergen Hoeller 120d512ff6 Polishing (backported from main) 2023-03-17 18:09:46 +01:00
Sam Brannen 3ddf183922 Update copyright headers 2023-03-17 14:51:13 +01:00
rstoyanchev 4c69bfd32f Upgrade to Reactor 2020.0.30
Closes gh-30116
2023-03-16 08:30:16 +00:00
Sam Brannen 41d71e9a7f Revise contribution
See gh-25316
2023-03-15 14:30:53 +01:00
mrcoffee77 36682b7ad2 Ensure methods declared in Object can be invoked on a JDK proxy in SpEL
This commit ensures that methods declared in java.lang.Object (such as
toString() can be invoked on a JDK proxy instance in a SpEL expression.

Closes gh-25316
2023-03-15 14:30:53 +01:00
Sam Brannen 30601a5014 Polishing 2023-03-15 14:30:53 +01:00
Brian Clozel f2371f5e7d Ignore quality factor when filtering out "*/*"
Prior to this commit, the `RequestedContentTypeResolverBuilder` would
create a `RequestedContentTypeResolver` that internally delegates to a
list of resolvers. Each resolver would either return the list of
requested media types, or a singleton list with the "*/*" media type; in
this case this signals that the resolver cannot find a specific media
type requested and that we should continue with the next resolver in the
list.

Media Types returned by resolvers can contain parameters, such as the
quality factor. If the HTTP client requests "*/*;q=0.8", the
`HeaderContentTypeResolver` will return this as a singleton list. While
this has been resolved from the request, such a media type should not be
selected over other media types that could be returned by other
resolvers.

This commit changes the `RequestedContentTypeResolverBuilder` so that it
does not select "*/*;q=0.8" as the requested media type, but instead
continues delegating to other resolvers in the list. This means we need
to remove the quality factor before comparing it to the "*/*" for
equality check.

Fixes gh-30121
2023-03-15 10:45:31 +01:00
rstoyanchev f0da099b12 Prefer request hostName and hostPort in ReactorServerHttpRequest
Backport of 682a4d53 and 9624ea39

Closes gh-29974
2023-03-14 07:02:21 +00:00
Brian Clozel d00fd4c502 Allow runtime compatibility with SnakeYaml 2.0
This commit ensures that SnakeYaml 2.0 is compatible at runtime with
Spring Framework 5.3.x with the `YamlProcessor` support.
The baseline version for SnakeYaml remains the same.

Closes gh-30097
2023-03-10 12:41:03 +01:00
Sébastien Deleuze 44a6d13cc0 Fix minor spacings in webflux docs
Closes gh-30095
2023-03-09 11:56:55 +01:00
Juergen Hoeller 284657355a Upgrade to Log4J 2.20, Tomcat 9.0.73, Jetty 9.4.51, Undertow 2.2.23 2023-03-08 17:44:10 +01:00
Juergen Hoeller 6a81ed3a50 Polishing 2023-03-08 17:43:35 +01:00
Brian Clozel 0a053cfccb Avoid lock contention in CaffeineCacheManager
Prior to this commit, using a dynamic `CaffeineCacheManager` would rely
on `ConcurrentHashMap#computeIfAbsent` for retrieving and creating cache
instances as needed. It turns out that using this method concurrently
can cause lock contention even when all known cache instances are
instantiated.

This commit avoids using this method if the cache instance already
exists and avoid storing `null` entries in the map. This change reduces
lock contention and the overall HashMap size in the non-dynamic case.

See gh-30066
Fixes gh-30085
2023-03-08 16:27:50 +01:00
Sam Brannen 22bb76d326 Revise documentation for @AspectJ argument name resolution algorithm
Closes gh-30057
2023-03-02 17:22:17 +01:00
Sam Brannen 4dc45d551c Update documentation for @AspectJ argument name resolution algorithm
Closes gh-30057
2023-03-01 17:29:50 +01:00
Sam Brannen e5d05ddfc3 Remove obsolete MetadataAwareAspectInstanceFactory Javadoc
We no longer have any JDK 5 related limitations imposed on us as was
the case when MetadataAwareAspectInstanceFactory was introduced; however,
MetadataAwareAspectInstanceFactory will remain as a specialized
sub-interface of AspectInstanceFactory.
2023-03-01 17:13:55 +01:00
Sam Brannen 44a5f8ec06 Add missing package-info.java file for autoproxy.target package 2023-03-01 17:13:45 +01:00
Sam Brannen 0a8bda40f4 Fix .gitignore pattern for Maven "target" folders
Rationale: changes in org.springframework.aop.framework.autoproxy.target
were previously ignored since the "target" package was ignored by the
previous "eager" pattern for "target" folders.
2023-03-01 17:13:32 +01:00
Radek Kraus 8a879c6fed Protect JMS connection creation against prepareConnection errors
This commit uses a local variable for the creation of a new JMS
Connection so that a rare failure in prepareConnection(...) does not
leave the connection field in a partially initialized state.

If such a JMSException occurs, the intermediary connection is closed.
This commit further defends against close() failures at that point,
by logging the close exception at DEBUG level. As a result, the original
JMSException is always re-thrown.

See gh-29116
Closes gh-30051
2023-02-28 16:35:36 +01:00
Sam Brannen 28d11aaf64 Polish contribution
See gh-30036
2023-02-27 16:47:09 +01:00
1993heqiang 02941127e1 Fix "Configuring a Global Date and Time Format" example
Closes gh-30036
2023-02-27 16:47:03 +01:00
Sam Brannen b1b24458c9 Polishing 2023-02-27 16:46:16 +01:00
Sébastien Deleuze 854b625be2 Add missing @Nullable annotations to LogMessage methods
Closes gh-30009
2023-02-24 17:51:52 +01:00
rstoyanchev a2b7a907ec Prefer local hostAddress in ReactorServerHttpRequest
Closes gh-28601
2023-02-23 16:40:38 +00:00
Brian Clozel 4f0a8911ca Fix CI image resource in pipeline
This commit fixes the CI image resource configuration so that it can be
checked autmatically by the pipeline.
This also updates various resources.
2023-02-23 09:44:09 +01:00
Sam Brannen 40fef7b232 Fix Javadoc for MockRestRequestMatchers 2023-02-22 11:09:03 +01:00
Sam Brannen 423134f64c Revise queryParam() and header() support in MockRestRequestMatchers
See gh-29953
See gh-29964
2023-02-22 11:08:05 +01:00
Sam Brannen 574c10d219 Polishing 2023-02-22 11:08:05 +01:00
Johnny Lim 2b4b947050 Fix Javadoc since for MockRestRequestMatchers queryParam() and header() (#29986)
See gh-29953
See gh-29964
2023-02-22 11:08:05 +01:00
Juergen Hoeller 4993b1090a Polishing 2023-02-22 11:08:05 +01:00
Juergen Hoeller 3adabf391f Consistent ordering of Resource methods (backported from main) 2023-02-15 12:35:41 +01:00
Juergen Hoeller 0026338c00 Consistent @Bean method return type for equivalence with XML example
Closes gh-29970
2023-02-14 16:57:35 +01:00
Juergen Hoeller cdea667e58 Test for request attribute visibility in FreeMarker (backported from main)
See gh-29787
2023-02-14 16:57:22 +01:00
Juergen Hoeller 66b1c0b4b0 Upgrade to Netty 4.1.89 2023-02-14 11:38:32 +01:00
Juergen Hoeller 6e42d36614 ASM upgrade for JDK 20/21 support (backported from main)
Closes gh-29966
2023-02-14 11:38:12 +01:00
Simon Baslé 7f2c93fa1f Allow MockRest to match header/queryParam value list with one Matcher
This commit adds a `header` variant and a `queryParam` variant to the
`MockRestRequestMatchers` API which take a single `Matcher` over the
list of values.

Contrary to the vararg variants, the whole list is evaluated and the
caller can choose the desired semantics using readily-available iterable
matchers like `everyItem`, `hasItems`, `hasSize`, `contains` or
`containsInAnyOrder`...

The fact that the previous variants don't strictly check the size of the
actual list == the number of provided matchers or expected values is
now documented in their respective javadocs.

See gh-29953
Closes gh-29964
2023-02-13 17:53:48 +01:00
Johnny Lim 4c351e811a Polish
See gh-29928
2023-02-09 09:55:19 +01:00
rstoyanchev 5b67dea506 Unwrap session before selecting stats counter
See gh-29375
2023-02-08 18:10:11 +00:00
Sam Brannen b9fe095f60 Clearly document that DataClassRowMapper supports Java records
Closes gh-29814
2023-02-07 19:09:47 +01:00
Sam Brannen 6b17014b5a Polish RowMapper tests 2023-02-07 19:09:32 +01:00
Sam Brannen 878246b09b Avoid confusing terminology in BeanPropertyRowMapper
Prior to this commit, the term "field" was sometimes used to refer to a
database column and sometimes used to refer to a bean property, which
lead to confusion in the Javadoc as well as within the code.

This commit addresses this by avoiding use of the term "field".
2023-02-07 19:07:40 +01:00
Sam Brannen 9067ccab2d Update copyright headers 2023-02-07 16:35:52 +01:00
Manthan Bhatt 4e00aece7a Restrict forwards in MockMvcWebConnection to 100
This change restricts the maximum number of forwards in MockMvcWebConnection to 100,
in case a forward is configured in a way that causes a loop. This is necessary in HtmlUnit
backed tests, unlike in classic MockMvc tests in which the forwards are not actually resolved.

See gh-29557
Closes gh-29866

Co-authored-by: Simon Baslé <sbasle@vmware.com>
2023-02-07 16:24:57 +01:00
Sam Brannen c9841f37b6 Revise Testcontainers examples based on feedback
Closes gh-29940
2023-02-07 15:57:23 +01:00
Johnny Lim 92d513e9fb Add MockMvc.multipart() Kotlin extensions with HttpMethod
See gh-28545
See gh-28631
Closes gh-29941
2023-02-07 15:12:27 +01:00
Sam Brannen 782ee34cb7 Update @DynamicPropertySource examples regarding changes in Testcontainers
Closes gh-29939
2023-02-07 13:38:50 +01:00
Sam Brannen 80c10fad92 Polish RowMapper tests 2023-02-03 18:06:10 +01:00
Sam Brannen 498a0d286f Clarify semantics of primitivesDefaultedForNullValue in BeanPropertyRowMapper
Closes gh-29923
2023-02-03 18:06:10 +01:00
Sam Brannen e9d45d1b49 Polish Javadoc for RowMappers 2023-02-03 18:01:07 +01:00
danu 0851b0f72d Release R2DBC connection when cleanup fails in transaction
When using R2dbcTransactionManager, connection will not be released if
it encounters error while doing `afterCleanup` steps. As `afterCleanup`
can use a database connection when doing `setAutoCommit(true)`, it can
fail under some conditions where the connection is not reliable.

This leads to the Connection not being released.

This commit ensures that inner steps of the `doCleanupAfterCompletion`
are protected against errors, logging the errors and continuing the
cleanup until the last step, which releases the connection.

Backport of commit e050c37
See gh-29703
Closes gh-29925

Co-authored-by: Simon Baslé <sbasle@vmware.com>
2023-02-03 17:43:22 +01:00
Brian Clozel 91d991e86c Rely only on Docker Hub for fetching OCI images 2023-02-02 21:47:19 +01:00
Juergen Hoeller 960f6fb936 Add missing warn level check (backported from main) 2023-02-01 18:28:20 +01:00
Juergen Hoeller 4d3d34528e Upgrade to Tomcat 9.0.71, Netty 4.1.87, AssertJ 3.24.2, HtmlUnit 2.70, Checkstyle 10.7 2023-02-01 18:26:00 +01:00
Juergen Hoeller 78ba946266 Clarify postProcessBeanFactory lifecycle state
Closes gh-29064

(cherry picked from commit b8827d8e11)
2023-02-01 18:21:16 +01:00
Juergen Hoeller 42e7318cbb Polishing 2023-01-31 16:48:36 +01:00
Juergen Hoeller 000383fbff Explicit target ClassLoader for interface-based infrastructure proxies
Includes direct JDK Proxy usage instead of ProxyFactory where possible.

Closes gh-29913

(cherry picked from commit 4d6249811e)
2023-01-31 16:48:27 +01:00
Juergen Hoeller 37cbdc2cf4 Lazily load ContextLoader.properties (and lazily fail if not present)
Closes gh-29905

(cherry picked from commit a74b86e812)
2023-01-31 16:21:45 +01:00
Juergen Hoeller 6d95e7f9d9 Declare no-op close() method in order to avoid container-triggered shutdown call
Closes gh-29892

(cherry picked from commit 7c9dca3d2e)
2023-01-31 16:21:39 +01:00
rstoyanchev 45a7917a36 Backport of cf9fc69d6b
Closes gh-29911
2023-01-31 11:55:32 +00:00
rstoyanchev 25b95b8b98 Avoid ClassLoader issue in Jetty10 WebSocket upgrade
Closes gh-29256
2023-01-31 11:50:22 +00:00
Sam Brannen 6bd7a7321e Revise generated default name for @JmsListener subscription
The previous commit changed the generated default name for a JMS
subscription to <FQCN>#<method name> -- for example:

- org.example.MyListener#myListenerMethod

However, the JMS spec does not guarantee that '#' is a supported
character. This commit therefore changes '#' to '.' as the separator
between the class name and method name -- for example:

- org.example.MyListener.myListenerMethod

This commit also introduces tests and documentation for these changes.

See gh-29902
2023-01-30 19:57:28 +01:00
Sam Brannen 11aaba9877 Polishing 2023-01-30 19:52:24 +01:00
fml2 07fd7606e7 Improve generated default name for a @JmsListener subscription
Prior to this commit, when using durable subscribers with @JmsListener
methods that do not specify a custom subscription name the generated
default subscription name was always
org.springframework.jms.listener.adapter.MessagingMessageListenerAdapter.
Consequently, multiple such @JmsListener methods were assigned the
same subscription name which violates the uniqueness requirement.

To address this, MessagingMessageListenerAdapter now implements
SubscriptionNameProvider and generates the subscription name based on
the following rules.

- if the InvocableHandlerMethod is present, the subscription name will
  take the form of handlerMethod.getBeanType().getName() + "#" +
  handlerMethod.getMethod().getName().
- otherwise, getClass().getName() is used, which is analogous to the
  previous behavior.

Closes gh-29902
2023-01-30 19:52:15 +01:00
Sam Brannen 40d2466334 Fix build due to Jackson Javadoc publication changes
The Jackson project no longer publishes Javadoc at
https://fasterxml.github.io which breaks the `javadoc` and `api` build
tasks due to their dependency on that web site for external Javadoc links.

As a workaround, we now reference Jackson's Javadoc via
https://www.javadoc.io.

See https://github.com/FasterXML/jackson-databind/issues/3440
Closes gh-29895
2023-01-29 12:43:11 +01:00
Sam Brannen 3d6d853bbc Include all Hibernate methods in SharedEntityManagerCreator's queryTerminatingMethods
Prior to this commit, we included Hibernate's Query.list() method in
SharedEntityManagerCreator's queryTerminatingMethods set but did not
include all of Hibernate's query-terminating methods.

To address this, this commit additionally includes the stream(),
uniqueResult(), and uniqueResultOptional() methods from Hibernate's
Query API in SharedEntityManagerCreator's query-terminating methods set.

Closes gh-29888
2023-01-28 20:59:46 +01:00
Sam Brannen 90ea39cc6b Update copyright headers 2023-01-28 20:49:13 +01:00
Arjen Poutsma 21c3d4f4a9 Support Jetty 10 in JettyClientHttpRequest
Though Jetty 10 was previously supported in the JettyClientHttpResponse,
this commit ensures support in the JettyClientHttpRequest.

Closes gh-29867
See gh-26123
2023-01-26 17:27:57 +01:00
Simon Baslé de53d77344 DatabaseClient uses SQL Supplier more lazily
This commit modifies the `DefaultDatabaseClient` implementation in order
to ensure lazier usage of the `Supplier<String>` passed to the sql
method (`DatabaseClient#sql(Supplier)`).

Since technically `DatabaseClient` is an interface that could have 3rd
party implementations, the lazyness expectation is only hinted at in the
`DatabaseClient#sql` javadoc.

Possible caveat: some log statements attempt to reflect the now lazily
resolved SQL string. Similarly, some exceptions can capture the SQL that
caused the issue if known. We expect that these always occur after the
execution of the statement has been attempted (see `ResultFunction`).
At this point the SQL string will be accessible and logs and exceptions
should reflect it as before. Keep an eye out for such strings turning
into `null` after this change, which would indicate the opposite.

Backport of d72df5ace4
See gh-29367
Closes gh-29887
2023-01-26 15:20:38 +01:00
Sébastien Deleuze e4e90bbec0 Polish RouterFunctionDsl KDoc 2023-01-23 13:27:34 +01:00
Sébastien Deleuze d0828be0cd Remove WebClientIntegrationTests#exchangeWithRelativeUrl outdated test
This test can fail when a web server runs on port 80 and
is not relevant anymore due to the removal of related feature
via 6e936a4081.

Closes gh-29863
2023-01-20 12:24:41 +01:00
Sébastien Deleuze 4a4b332709 Refine Jackson2ObjectMapperBuilder#configureFeature exception handling
This commit changes the FatalBeanException previously thrown for
an IllegalArgumentException which seems more suitable for that
use case.

Closes gh-29860
2023-01-20 10:15:49 +01:00
Brian Clozel 33f1c9b614 Upgrade Gradle Enterprise & Conventions plugins
This is required to adapt to the repo.spring.io permission changes.
2023-01-19 11:34:05 +01:00
Arjen Poutsma 53ac812fb0 Updated sdkmanrc 2023-01-19 10:05:36 +01:00
Arjen Poutsma 60c89dd2df Fix IllegalStateException in empty ProducesRequestCondition
When comparing empty ProducesRequestCondition, compareTo would throw an
IllegalStateException if the Accept header was invalid. This commit
fixes that behavior.

See gh-29794
Closes gh-29836
2023-01-18 10:45:12 +01:00
Minsoo Cheong(Merlin) 4bd2531774 Fix R2dbcTransactionManager debug log: don't log a Mono (#29800)
When logging the current connection inside R2dbcTransactionManager
doBegin, the mono object was logged instead of the connection lambda
parameter.

Other similar debug-level logs do use the actual Connection object,
so this commit does the same.

Backport of gh-29800
Closes gh-29824
2023-01-16 11:30:35 +01:00
Spring Builds 4fd5630700 Next development version (v5.3.26-SNAPSHOT) 2023-01-11 11:19:35 +00:00
Juergen Hoeller 26cd33cb2b Upgrade to Reactor 2020.0.27
Includes HtmlUnit 2.69, AssertJ 3.24.1, Checkstyle 10.6

Closes gh-29798
2023-01-11 00:23:38 +01:00
Juergen Hoeller 2ee393ae71 Upgrade to Tomcat 9.0.70, Jetty 9.4.50, Netty 4.1.86, Undertow 2.2.22, HtmlUnit 2.67, Mockito 4.9, AssertJ 3.23.1, Checkstyle 10.5 2022-12-23 15:52:09 +01:00
Juergen Hoeller 0815d29e45 Defensive check for null returned from createConnection()
Closes gh-29706
2022-12-23 15:51:37 +01:00
Brian Clozel 777f01d786 Fix path within mapping when pattern contains ".*"
Prior to this commit, extracting the path within handler mapping would
result in "" if the matching path element would be a Regex and contain
".*". This could cause issues with resource handling if the handler
mapping pattern was similar to `"/folder/file.*.extension"`.

This commit introduces a new `isLiteral()` method in the `PathElement`
abstract class that expresses whether the path element can be compared
as a String for path matching or if it requires a more elaborate
matching process.

Using this method for extracting the path within handler mapping avoids
relying on wildcard count or other properties.

See gh-29712
Fixes gh-29716
2022-12-19 10:53:02 +01:00
Sam Brannen f8fea013fc Update Jakarta Mail info in ref docs
Closes gh-29708
2022-12-17 14:18:42 +01:00
Sam Brannen 916539178b Improve documentation for literals in SpEL expressions
Closes gh-29701
2022-12-16 14:51:48 +01:00
Sam Brannen fdf3bcc9d9 Remove obsolete AttributeMethods.hasOnlyValueAttribute() method
See gh-29685
2022-12-13 15:52:01 +01:00
Sam Brannen 5ddc984192 Support repeatable annotation containers with multiple attributes
Prior to this commit, there was a bug in the implementation of
StandardRepeatableContainers.computeRepeatedAnnotationsMethod() which
has existed since Spring Framework 5.2 (when
StandardRepeatableContainers was introduced). Specifically,
StandardRepeatableContainers ignored any repeatable container
annotation if it declared attributes other than `value()`. However,
Java permits any number of attributes in a repeatable container
annotation.

In addition, the changes made in conjunction with gh-20279 made the bug
in StandardRepeatableContainers apparent when using the
getMergedRepeatableAnnotations() or findMergedRepeatableAnnotations()
method in AnnotatedElementUtils, resulting in regressions for the
behavior of those two methods.

This commit fixes the regressions and bug by altering the logic in
StandardRepeatableContainers.computeRepeatedAnnotationsMethod() so that
it explicitly looks for the `value()` method and ignores any other
methods declared in a repeatable container annotation candidate.

See gh-29685
Closes gh-29686
2022-12-13 15:47:13 +01:00
Sam Brannen b2ce54e7f1 Revise RepeatableContainersTests 2022-12-13 15:47:13 +01:00
Juergen Hoeller 937ab5f4b2 Polishing (aligned with main) 2022-12-13 12:06:09 +01:00
Juergen Hoeller 064c618050 Drop SQLExceptionSubclassFactory and unify SQLStateSQLExceptionTranslator tests 2022-12-13 12:05:35 +01:00
Juergen Hoeller 8c80ec1138 Avoid NPE on BeanDescriptor access with SimpleBeanInfoFactory
Closes gh-29681

(cherry picked from commit d74191427e)
2022-12-13 11:43:43 +01:00
rstoyanchev 912fa7602a Improve invalid Content-Type handling in WebFlux
Closes gh-29565
2022-12-09 11:53:16 +00:00
rstoyanchev 525fc7a27e Optimize object creation PartialMatchHelper
Closes gh-29667
2022-12-09 11:27:03 +00:00
Sam Brannen 72285034ba Support arrays in AST string representations of SpEL expressions
Prior to this commit, SpEL's ConstructorReference did not provide
support for arrays when generating a string representation of the
internal AST. For example, 'new String[3]' was represented as 'new
String()' instead of 'new String[3]'.

This commit introduces support for standard array construction and array
construction with initializers in ConstructorReference's toStringAST()
implementation.

Closes gh-29666
2022-12-08 23:23:01 -05:00
Sam Brannen 933474000b Polishing 2022-12-08 18:45:02 -05:00
Sam Brannen 4e8aebcde7 Fix SpEL support for quotes within String literals
Prior to this commit, there were two bugs in the support for quotes
within String literals in SpEL expressions.

- Two double quotes ("") or two single quotes ('') were always replaced
  with one double quote or one single quote, respectively, regardless
  of which quote character was used to enclose the original String
  literal. This resulted in the loss of one of the double quotes when
  the String literal was enclosed in single quotes, and vice versa. For
  example, 'x "" y' became 'x " y'.

- A single quote which was properly escaped in a String literal
  enclosed within single quotes was not escaped in the AST string
  representation of the expression. For example, 'x '' y' became 'x ' y'.

This commit fixes both of these related issues in StringLiteral and
overhauls the structure of ParsingTests.

Closes gh-29604
Closes gh-28356
2022-12-07 16:03:49 -05:00
Sam Brannen dff5b1ff8e Polishing 2022-12-07 15:59:08 -05:00
Sam Brannen fa0a4a0a46 Improve Javadoc for SqlLobValue 2022-12-07 15:58:56 -05:00
Sam Brannen 41a6b7ec20 Update copyright headers for source code changed since August 2022
The changes in this commit were performed using the newly introduced
update_copyright_headers.sh script.
2022-12-03 17:23:21 -05:00
Sam Brannen cfaa4ba89c Apply update_copyright_headers.sh to staged files as well 2022-12-03 17:23:21 -05:00
Sam Brannen 598e9d586b Introduce update_copyright_headers.sh shell script
In order to automate maintenance of copyright headers in our source
code (especially when merging PRs from external contributors), this
commit introduces an update_copyright_headers.sh script (tested on
mac OS) that will update the copyright headers of all Java, Kotlin,
and Groovy source files that have been added or modified this year
(or at least as far back as the git log history supports it).

For example, running this script currently outputs the following.

Updating copyright headers in Java, Kotlin, and Groovy source code for year 2022
warning: log for 'main' only goes back to Tue, 16 Aug 2022 16:24:55 +0200
2022-12-03 17:23:21 -05:00
Sam Brannen 750a8b359f Stop using Mockito to spy() on JDK I/O streams
When running on JDK 16+, we are not able to spy() on JDK types. To
address this, this commit stops using Mockito to spy on JDK I/O streams
(such as ByteArrayInputStream and ByteArrayOutputStream).
2022-12-03 17:23:21 -05:00
Sam Brannen c838bcff3a Fix/Disable JMX & JNDI tests on JDK 16+ 2022-12-03 17:23:21 -05:00
Sam Brannen 109b00d24d Avoid use of deprecated Character/Double constructors in tests 2022-12-03 17:23:21 -05:00
Sam Brannen b6abf45a56 Introduce @Suite classes for individual modules 2022-12-03 16:32:27 -05:00
Juergen Hoeller 2d94b43faa Polishing 2022-12-02 11:15:03 +01:00
Sam Brannen f7f73439ab Modify MBeanTestUtils to work on JDK 17+ 2022-11-30 14:55:15 +01:00
Sam Brannen 5e55558a93 Fix SockJsServiceTests
See gh-29594
2022-11-28 17:12:08 +01:00
Sam Brannen eee50d3b8a Polishing 2022-11-28 17:06:22 +01:00
Sam Brannen 92af390ed2 Polish contribution
See gh-29594
2022-11-28 17:02:53 +01:00
Aashay Chapatwala 332d2a36bb Add title to SockJS iFrames for accessibility compliance
Closes gh-29594
2022-11-28 17:02:01 +01:00
Brian Clozel 92b2b828f5 Fix Java 17 test CI and remove Java 11 variant
This commit fixes configuration and runtime issues with the Java 17 test
CI variant and removes the Java 11 one, now that Spring Framework 5.3.x
is in maintenance mode.
2022-11-28 10:17:56 +01:00
Juergen Hoeller a11222f681 Polishing 2022-11-25 17:58:59 +01:00
Juergen Hoeller 7de8d81932 Explicit documentation notes on standard Java reflection
See gh-29531
2022-11-24 14:25:43 +01:00
Juergen Hoeller 0e33537d9d Consistent use of DefaultParameterNameResolver in tests 2022-11-23 11:17:58 +01:00
Sébastien Deleuze 00da70e26b Fix some typos in Kotlin WebClient example code
Closes gh-29542
2022-11-22 08:35:50 +01:00
Sam Brannen 72b44cebea Catch Error for SourceHttpMessageConverter in WebMvcConfigurationSupport
Prior to this commit, the addDefaultHttpMessageConverters() method in
WebMvcConfigurationSupport caught Throwable for SourceHttpMessageConverter
instantiation; whereas, the rest of the code base correctly catches Error
for SourceHttpMessageConverter instantiation (to handle errors such as
NoClassDefFoundError).

Throwable should not be caught since it can mask other categories of
failures (such as configuration errors).

This commit therefore switches to catching Error for SourceHttpMessageConverter
instantiation in WebMvcConfigurationSupport.

Closes gh-29537
2022-11-21 17:37:22 +01:00
Marten Deinum 1ee3777ac7 Fix link to Bean Utils Light Library in BeanUtils Javadoc
The URL for the BULL library has changed (not sure when, probably way back).

This updates it to the correct location.

Closes gh-29534, gh-29536
2022-11-21 17:25:50 +01:00
divcon cad6e8756d Fix link to WebFlux section in reference manual
Closes gh-29526
2022-11-19 14:51:03 +01:00
Sam Brannen c7bd685c54 Polishing 2022-11-19 14:49:26 +01:00
Sam Brannen 4f472d2ac4 Remove TODOs in WebFlux ref docs 2022-11-19 14:49:07 +01:00
Stephane Nicoll a18842b72b Fix link to WebFlux section
Closes gh-29517
2022-11-18 10:48:05 +01:00
Spring Builds a41f97bc2b Next development version (v5.3.25-SNAPSHOT) 2022-11-16 08:07:01 +00:00
4507 changed files with 108013 additions and 149520 deletions
-32
View File
@@ -1,32 +0,0 @@
name: Backport Bot
on:
issues:
types: [labeled]
pull_request:
types: [labeled]
push:
branches:
- '*.x'
permissions:
contents: read
jobs:
build:
permissions:
contents: read
issues: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
- name: Download BackportBot
run: wget https://github.com/spring-io/backport-bot/releases/download/latest/backport-bot-0.0.1-SNAPSHOT.jar
- name: Backport
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_EVENT: ${{ toJSON(github.event) }}
run: java -jar backport-bot-0.0.1-SNAPSHOT.jar --github.accessToken="$GITHUB_TOKEN" --github.event_name "$GITHUB_EVENT_NAME" --github.event "$GITHUB_EVENT"
+14 -17
View File
@@ -1,42 +1,38 @@
# Miscellaneous
*.java.hsp
*.sonarj
*.sw*
.DS_Store
.settings
.springBeans
bin
build.sh
integration-repo
ivy-cache
argfile*
activemq-data/
classes/
# Log files
jxl.log
jmx.log
derby.log
# Gradle artifacts
spring-test/test-output/
.gradle
.gradletasknamecache
argfile*
pom.xml
activemq-data/
classes/
/build
buildSrc/build
/spring-*/build
/spring-core/graalvm/build
/spring-core/kotlin-coroutines/build
/framework-bom/build
/framework-docs/build
/integration-tests/build
/src/asciidoc/build
spring-test/test-output/
# Maven artifacts
pom.xml
target/
/target/
# Eclipse artifacts, including WTP generated manifests
bin
.classpath
.project
.settings
.springBeans
spring-*/src/main/java/META-INF/MANIFEST.MF
# IDEA artifacts and output dirs
@@ -47,6 +43,7 @@ spring-*/src/main/java/META-INF/MANIFEST.MF
out
test-output
atlassian-ide-plugin.xml
.gradletasknamecache
# VS Code
.vscode/
.vscode/
+4 -7
View File
@@ -10,11 +10,10 @@ Phillip Webb <pwebb@vmware.com> <pwebb@gopivotal.com>
Chris Beams <cbeams@vmware.com>
Chris Beams <cbeams@vmware.com> <cbeams@pivotal.io>
Chris Beams <cbeams@vmware.com> <cbeams@gopivotal.com>
Arjen Poutsma <poutsmaa@vmware.com>
Arjen Poutsma <poutsmaa@vmware.com> <apoutsma@pivotal.io>
Arjen Poutsma <poutsmaa@vmware.com> <apoutsma@gopivotal.com>
Arjen Poutsma <poutsmaa@vmware.com> <poutsma@mac.com>
Arjen Poutsma <poutsmaa@vmware.com> <apoutsma@vmware.com>
Arjen Poutsma <apoutsma@vmware.com>
Arjen Poutsma <apoutsma@vmware.com> <apoutsma@pivotal.io>
Arjen Poutsma <apoutsma@vmware.com> <apoutsma@gopivotal.com>
Arjen Poutsma <apoutsma@vmware.com> <poutsma@mac.com>
Oliver Drotbohm <odrotbohm@vmware.com>
Oliver Drotbohm <odrotbohm@vmware.com> <ogierke@vmware.com>
Oliver Drotbohm <odrotbohm@vmware.com> <ogierke@pivotal.io>
@@ -30,7 +29,5 @@ Andy Clement <aclement@vmware.com> <andrew.clement@gmail.com>
Sam Brannen <sbrannen@vmware.com>
Sam Brannen <sbrannen@vmware.com> <sbrannen@pivotal.io>
Sam Brannen <sbrannen@vmware.com> <sam@sambrannen.com>
Simon Basle <sbasle@vmware.com>
Simon Baslé <sbasle@vmware.com>
<dmitry.katsubo@gmail.com> <dmitry.katsubo@gmai.com>
Nick Williams <nicholas@nicholaswilliams.net>
+1 -1
View File
@@ -1,3 +1,3 @@
# Enable auto-env through the sdkman_auto_env config
# Add key=value pairs of SDKs to use below
java=17.0.6-librca
java=8.0.362-librca
+12 -16
View File
@@ -6,7 +6,7 @@ First off, thank you for taking the time to contribute! :+1: :tada:
* [Code of Conduct](#code-of-conduct)
* [How to Contribute](#how-to-contribute)
* [Ask questions](#ask-questions)
* [Discuss](#discuss)
* [Create an Issue](#create-an-issue)
* [Issue Lifecycle](#issue-lifecycle)
* [Submit a Pull Request](#submit-a-pull-request)
@@ -22,10 +22,11 @@ Please report unacceptable behavior to spring-code-of-conduct@pivotal.io.
### How to Contribute
#### Ask questions
#### Discuss
If you have a question, check Stack Overflow using
[this list of tags](https://stackoverflow.com/questions/tagged/spring+or+spring-mvc+or+spring-aop+or+spring-jdbc+or+spring-transactions+or+spring-annotations+or+spring-jms+or+spring-el+or+spring-test+or+spring+or+spring-remoting+or+spring-orm+or+spring-jmx+or+spring-cache+or+spring-webflux?tab=Newest). Find an existing discussion, or start a new one if necessary.
[this list of tags](https://stackoverflow.com/questions/tagged/spring+or+spring-mvc+or+spring-aop+or+spring-jdbc+or+spring-transactions+or+spring-annotations+or+spring-jms+or+spring-el+or+spring-test+or+spring+or+spring-remoting+or+spring-orm+or+spring-jmx+or+spring-cache+or+spring-webflux?tab=Newest).
Find an existing discussion, or start a new one if necessary.
If you believe there is an issue, search through
[existing issues](https://github.com/spring-projects/spring-framework/issues) trying a
@@ -38,18 +39,14 @@ decision.
Reporting an issue or making a feature request is a great way to contribute. Your feedback
and the conversations that result from it provide a continuous flow of ideas. However,
before creating a ticket, please take the time to [ask and research](#ask-questions) first.
before creating a ticket, please take the time to [discuss and research](#discuss) first.
If you create an issue after a discussion on Stack Overflow, please provide a description
If creating an issue after a discussion on Stack Overflow, please provide a description
in the issue instead of simply referring to Stack Overflow. The issue tracker is an
important place of record for design discussions and should be self-sufficient.
Once you're ready, create an issue on [GitHub](https://github.com/spring-projects/spring-framework/issues).
Many issues are caused by subtle behavior, typos, and unintended configuration.
Creating a [Minimal Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example)
(starting with https://start.spring.io for example) of the problem helps the team
quickly triage your issue and get to the core of the problem.
Once you're ready, create an issue on
[GitHub](https://github.com/spring-projects/spring-framework/issues).
#### Issue Lifecycle
@@ -66,7 +63,7 @@ follow-up reports will need to be created as new issues with a fresh description
#### Submit a Pull Request
1. If you have not previously done so, please sign the
[Contributor License Agreement](https://cla.spring.io/sign/spring). You will be reminded
[Contributor License Agreement](https://cla.pivotal.io/sign/spring). You will be reminded
automatically when you submit the PR.
1. Should you create an issue first? No, just create the pull request and use the
@@ -123,13 +120,12 @@ define the source file coding standards we use along with some IDEA editor setti
### Reference Docs
The reference documentation is in the [framework-docs/src/docs/asciidoc](framework-docs/src/docs/asciidoc) directory, in
The reference documentation is in the [src/docs/asciidoc](src/docs/asciidoc) directory, in
[Asciidoctor](https://asciidoctor.org/) format. For trivial changes, you may be able to browse,
edit source files, and submit directly from GitHub.
When making changes locally, execute `./gradlew :framework-docs:asciidoctor` and then browse the result under
`framework-docs/build/docs/ref-docs/html5/index.html`.
When making changes locally, execute `./gradlew asciidoctor` and then browse the result under
`build/docs/ref-docs/html5/index.html`.
Asciidoctor also supports live editing. For more details see
[AsciiDoc Tooling](https://docs.asciidoctor.org/asciidoctor/latest/tooling/).
+2 -2
View File
@@ -1,4 +1,4 @@
# <img src="framework-docs/src/docs/spring-framework.png" width="80" height="80"> Spring Framework [![Build Status](https://ci.spring.io/api/v1/teams/spring-framework/pipelines/spring-framework-6.0.x/jobs/build/badge)](https://ci.spring.io/teams/spring-framework/pipelines/spring-framework-6.0.x?groups=Build") [![Revved up by Gradle Enterprise](https://img.shields.io/badge/Revved%20up%20by-Gradle%20Enterprise-06A0CE?logo=Gradle&labelColor=02303A)](https://ge.spring.io/scans?search.rootProjectNames=spring)
# <img src="src/docs/spring-framework.png" width="80" height="80"> Spring Framework [![Build Status](https://ci.spring.io/api/v1/teams/spring-framework/pipelines/spring-framework-5.3.x/jobs/build/badge)](https://ci.spring.io/teams/spring-framework/pipelines/spring-framework-5.3.x?groups=Build") [![Revved up by Gradle Enterprise](https://img.shields.io/badge/Revved%20up%20by-Gradle%20Enterprise-06A0CE?logo=Gradle&labelColor=02303A)](https://ge.spring.io/scans?search.rootProjectNames=spring)
This is the home of the Spring Framework: the foundation for all [Spring projects](https://spring.io/projects). Collectively the Spring Framework and the family of Spring projects are often referred to simply as "Spring".
@@ -14,7 +14,7 @@ For access to artifacts or a distribution zip, see the [Spring Framework Artifac
## Documentation
The Spring Framework maintains reference documentation ([published](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/) and [source](framework-docs/src/docs/asciidoc)), GitHub [wiki pages](https://github.com/spring-projects/spring-framework/wiki), and an
The Spring Framework maintains reference documentation ([published](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/) and [source](src/docs/asciidoc)), GitHub [wiki pages](https://github.com/spring-projects/spring-framework/wiki), and an
[API reference](https://docs.spring.io/spring-framework/docs/current/javadoc-api/). There are also [guides and tutorials](https://spring.io/guides) across Spring projects.
## Micro-Benchmarks
+327 -61
View File
@@ -1,39 +1,297 @@
plugins {
id 'io.spring.nohttp' version '0.0.11'
id 'io.freefair.aspectj' version '6.5.0.3' apply false
// kotlinVersion is managed in gradle.properties
id 'org.jetbrains.kotlin.plugin.serialization' version "${kotlinVersion}" apply false
id 'org.jetbrains.dokka' version '1.7.20'
id 'org.asciidoctor.jvm.convert' version '3.3.2' apply false
id 'org.asciidoctor.jvm.pdf' version '3.3.2' apply false
id 'io.spring.dependency-management' version '1.0.11.RELEASE' apply false
id 'io.spring.nohttp' version '0.0.10'
id 'io.freefair.aspectj' version '6.2.0' apply false
id 'org.jetbrains.dokka' version '1.6.10' apply false
id 'org.jetbrains.kotlin.jvm' version '1.5.32' apply false
id 'org.jetbrains.kotlin.plugin.serialization' version "1.5.32" apply false
id 'org.asciidoctor.jvm.convert' version '3.3.2'
id 'org.asciidoctor.jvm.pdf' version '3.3.2'
id 'org.unbroken-dome.xjc' version '2.0.0' apply false
id 'com.github.ben-manes.versions' version '0.42.0'
id 'com.github.johnrengelman.shadow' version '7.1.2' apply false
id 'de.undercouch.download' version '5.1.0'
id 'me.champeau.jmh' version '0.6.8' apply false
id 'com.github.ben-manes.versions' version '0.39.0'
id 'com.github.johnrengelman.shadow' version '7.0.0' apply false
id 'de.undercouch.download' version '4.1.2'
id 'me.champeau.jmh' version "0.6.6" apply false
}
ext {
moduleProjects = subprojects.findAll { it.name.startsWith("spring-") }
javaProjects = subprojects - project(":framework-bom") - project(":framework-platform")
javaProjects = subprojects - project(":framework-bom")
withoutJclOverSlf4j = {
exclude group: "org.slf4j", name: "jcl-over-slf4j"
}
}
configure(allprojects) { project ->
repositories {
mavenCentral()
maven {
url "https://repo.spring.io/milestone"
content {
// Netty 5 optional support
includeGroup 'io.projectreactor.netty'
apply plugin: "io.spring.dependency-management"
dependencyManagement {
imports {
mavenBom "com.fasterxml.jackson:jackson-bom:2.12.7"
mavenBom "io.netty:netty-bom:4.1.91.Final"
mavenBom "io.projectreactor:reactor-bom:2020.0.31"
mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR13"
mavenBom "io.rsocket:rsocket-bom:1.1.3"
mavenBom "org.eclipse.jetty:jetty-bom:9.4.51.v20230217"
mavenBom "org.jetbrains.kotlin:kotlin-bom:1.5.32"
mavenBom "org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.2"
mavenBom "org.jetbrains.kotlinx:kotlinx-serialization-bom:1.2.2"
mavenBom "org.junit:junit-bom:5.8.2"
}
dependencies {
dependencySet(group: 'org.apache.logging.log4j', version: '2.20.0') {
entry 'log4j-api'
entry 'log4j-core'
entry 'log4j-jul'
entry 'log4j-slf4j-impl'
}
dependency "org.slf4j:slf4j-api:1.7.36"
dependency("com.google.code.findbugs:findbugs:3.0.1") {
exclude group: "dom4j", name: "dom4j"
}
dependency "com.google.code.findbugs:jsr305:3.0.2"
dependencySet(group: 'org.aspectj', version: '1.9.7') {
entry 'aspectjrt'
entry 'aspectjtools'
entry 'aspectjweaver'
}
dependencySet(group: 'org.codehaus.groovy', version: '3.0.9') {
entry 'groovy'
entry 'groovy-jsr223'
entry 'groovy-templates' // requires findbugs for warning-free compilation
entry 'groovy-test'
entry 'groovy-xml'
}
dependency "io.reactivex:rxjava:1.3.8"
dependency "io.reactivex:rxjava-reactive-streams:1.2.1"
dependency "io.reactivex.rxjava2:rxjava:2.2.21"
dependency "io.reactivex.rxjava3:rxjava:3.1.5"
dependency "io.smallrye.reactive:mutiny:1.8.0"
dependency "io.projectreactor.tools:blockhound:1.0.6.RELEASE"
dependency "com.caucho:hessian:4.0.63"
dependency "com.fasterxml:aalto-xml:1.3.1"
dependency("com.fasterxml.woodstox:woodstox-core:6.2.8") {
exclude group: "stax", name: "stax-api"
}
dependency "com.google.code.gson:gson:2.8.9"
dependency "com.google.protobuf:protobuf-java-util:3.19.3"
dependency "com.googlecode.protobuf-java-format:protobuf-java-format:1.4"
dependency("com.thoughtworks.xstream:xstream:1.4.19") {
exclude group: "xpp3", name: "xpp3_min"
exclude group: "xmlpull", name: "xmlpull"
}
dependency "org.apache.johnzon:johnzon-jsonb:1.2.18"
dependency("org.codehaus.jettison:jettison:1.3.8") {
exclude group: "stax", name: "stax-api"
}
dependencySet(group: 'org.jibx', version: '1.3.3') {
entry 'jibx-bind'
entry 'jibx-run'
}
dependency "org.ogce:xpp3:1.1.6"
dependency "org.yaml:snakeyaml:1.33"
dependency "com.h2database:h2:2.1.214"
dependency "com.github.ben-manes.caffeine:caffeine:2.9.3"
dependency "com.github.librepdf:openpdf:1.3.30"
dependency "com.rometools:rome:1.18.0"
dependency "commons-io:commons-io:2.5"
dependency "io.vavr:vavr:0.10.4"
dependency "net.sf.jopt-simple:jopt-simple:5.0.4"
dependencySet(group: 'org.apache.activemq', version: '5.16.2') {
entry 'activemq-broker'
entry('activemq-kahadb-store') {
exclude group: "org.springframework", name: "spring-context"
}
entry 'activemq-stomp'
}
dependency "org.apache.bcel:bcel:6.0"
dependency "org.apache.commons:commons-pool2:2.9.0"
dependencySet(group: 'org.apache.derby', version: '10.14.2.0') {
entry 'derby'
entry 'derbyclient'
}
dependency "org.apache.poi:poi-ooxml:4.1.2"
dependency "org.apache-extras.beanshell:bsh:2.0b6"
dependency "org.freemarker:freemarker:2.3.31"
dependency "org.hsqldb:hsqldb:2.5.2"
dependency "org.quartz-scheduler:quartz:2.3.2"
dependency "org.codehaus.fabric3.api:commonj:1.1.0"
dependency "net.sf.ehcache:ehcache:2.10.6"
dependency "org.ehcache:jcache:1.0.1"
dependency "org.ehcache:ehcache:3.4.0"
dependency "org.hibernate:hibernate-core:5.4.33.Final"
dependency "org.hibernate:hibernate-validator:6.2.5.Final"
dependency "org.webjars:webjars-locator-core:0.48"
dependency "org.webjars:underscorejs:1.8.3"
dependencySet(group: 'org.apache.tomcat', version: '9.0.73') {
entry 'tomcat-util'
entry('tomcat-websocket') {
exclude group: "org.apache.tomcat", name: "tomcat-servlet-api"
exclude group: "org.apache.tomcat", name: "tomcat-websocket-api"
}
}
dependencySet(group: 'org.apache.tomcat.embed', version: '9.0.73') {
entry 'tomcat-embed-core'
entry 'tomcat-embed-websocket'
}
dependencySet(group: 'io.undertow', version: '2.2.23.Final') {
entry 'undertow-core'
entry('undertow-servlet') {
exclude group: "org.jboss.spec.javax.servlet", name: "jboss-servlet-api_4.0_spec"
exclude group: "org.jboss.spec.javax.annotation", name: "jboss-annotations-api_1.3_spec"
}
entry('undertow-websockets-jsr') {
exclude group: "org.jboss.spec.javax.websocket", name: "jboss-websocket-api_1.1_spec"
}
}
dependency "org.eclipse.jetty:jetty-reactive-httpclient:1.1.13"
dependency 'org.apache.httpcomponents.client5:httpclient5:5.1.3'
dependency 'org.apache.httpcomponents.core5:httpcore5-reactive:5.1.3'
dependency("org.apache.httpcomponents:httpclient:4.5.13") {
exclude group: "commons-logging", name: "commons-logging"
}
dependency("org.apache.httpcomponents:httpasyncclient:4.1.5") {
exclude group: "commons-logging", name: "commons-logging"
}
dependencySet(group: 'com.squareup.okhttp3', version: '3.14.9') {
entry 'okhttp'
entry 'mockwebserver'
}
dependency "org.jruby:jruby:9.2.20.1"
dependency "org.python:jython-standalone:2.7.1"
dependency "org.mozilla:rhino:1.7.11"
dependency "commons-fileupload:commons-fileupload:1.4"
dependency "org.synchronoss.cloud:nio-multipart-parser:1.1.0"
dependency("org.dom4j:dom4j:2.1.3") {
exclude group: "jaxen", name: "jaxen"
exclude group: "net.java.dev.msv", name: "xsdlib"
exclude group: "pull-parser", name: "pull-parser"
exclude group: "xpp3", name: "xpp3"
}
dependency("jaxen:jaxen:1.2.0") {
exclude group: "dom4j", name: "dom4j"
}
dependency("junit:junit:4.13.2") {
exclude group: "org.hamcrest", name: "hamcrest-core"
}
dependency("de.bechte.junit:junit-hierarchicalcontextrunner:4.12.1") {
exclude group: "junit", name: "junit"
}
dependency "org.testng:testng:7.4.0"
dependency "org.junit.support:testng-engine:1.0.4"
dependency "org.hamcrest:hamcrest:2.2"
dependency "org.awaitility:awaitility:3.1.6"
dependency "org.assertj:assertj-core:3.24.2"
dependencySet(group: 'org.xmlunit', version: '2.9.0') {
entry 'xmlunit-assertj'
entry('xmlunit-matchers') {
exclude group: "org.hamcrest", name: "hamcrest-core"
}
}
dependencySet(group: 'org.mockito', version: '4.9.0') { // spring-beans tests fail with 4.10+
entry('mockito-core') {
exclude group: "org.hamcrest", name: "hamcrest-core"
}
entry 'mockito-junit-jupiter'
}
dependency "io.mockk:mockk:1.12.1"
dependency("net.sourceforge.htmlunit:htmlunit:2.70.0") {
exclude group: "commons-logging", name: "commons-logging"
}
dependency("org.seleniumhq.selenium:htmlunit-driver:2.70.0") {
exclude group: "commons-logging", name: "commons-logging"
}
dependency("org.seleniumhq.selenium:selenium-java:3.141.59") {
exclude group: "commons-logging", name: "commons-logging"
exclude group: "io.netty", name: "netty"
}
dependency "org.skyscreamer:jsonassert:1.5.0"
dependency "com.jayway.jsonpath:json-path:2.7.0"
dependency "org.bouncycastle:bcpkix-jdk18on:1.71"
dependencySet(group: 'org.apache.tiles', version: '3.0.8') {
entry 'tiles-api'
entry('tiles-core', withoutJclOverSlf4j)
entry('tiles-servlet', withoutJclOverSlf4j)
entry('tiles-jsp', withoutJclOverSlf4j)
entry('tiles-el', withoutJclOverSlf4j)
entry('tiles-extras') {
exclude group: "org.springframework", name: "spring-web"
exclude group: "org.slf4j", name: "jcl-over-slf4j"
}
}
dependency("org.apache.taglibs:taglibs-standard-jstlel:1.2.5") {
exclude group: "org.apache.taglibs", name: "taglibs-standard-spec"
}
dependency "com.ibm.websphere:uow:6.0.2.17"
dependency "com.jamonapi:jamon:2.82"
dependency "joda-time:joda-time:2.10.13"
dependency "org.eclipse.persistence:org.eclipse.persistence.jpa:2.7.10"
dependency "org.javamoney:moneta:1.3"
dependency "com.sun.activation:javax.activation:1.2.0"
dependency "com.sun.mail:javax.mail:1.6.2"
dependencySet(group: 'com.sun.xml.bind', version: '2.3.0.1') {
entry 'jaxb-core'
entry 'jaxb-impl'
entry 'jaxb-xjc'
}
dependency "javax.activation:javax.activation-api:1.2.0"
dependency "javax.annotation:javax.annotation-api:1.3.2"
dependency "javax.cache:cache-api:1.1.0"
dependency "javax.ejb:javax.ejb-api:3.2"
dependency "javax.el:javax.el-api:3.0.1-b04"
dependency "javax.enterprise.concurrent:javax.enterprise.concurrent-api:1.0"
dependency "javax.faces:javax.faces-api:2.2"
dependency "javax.inject:javax.inject:1"
dependency "javax.inject:javax.inject-tck:1"
dependency "javax.interceptor:javax.interceptor-api:1.2.2"
dependency "javax.jms:javax.jms-api:2.0.1"
dependency "javax.json:javax.json-api:1.1.4"
dependency "javax.json.bind:javax.json.bind-api:1.0"
dependency "javax.mail:javax.mail-api:1.6.2"
dependency "javax.money:money-api:1.0.3"
dependency "javax.resource:javax.resource-api:1.7.1"
dependency "javax.servlet:javax.servlet-api:4.0.1"
dependency "javax.servlet.jsp:javax.servlet.jsp-api:2.3.2-b02"
dependency "javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:1.2.1"
dependency "javax.transaction:javax.transaction-api:1.3"
dependency "javax.validation:validation-api:2.0.1.Final"
dependency "javax.websocket:javax.websocket-api:1.1"
dependency "javax.xml.bind:jaxb-api:2.3.1"
dependency "javax.xml.ws:jaxws-api:2.3.1"
dependency "org.eclipse.persistence:javax.persistence:2.2.0"
// Substitute for "javax.management:jmxremote_optional:1.0.1_04" which
// is not available on Maven Central
dependency "org.glassfish.external:opendmk_jmxremote_optional_jar:1.0-b01-ea"
dependency "org.glassfish:javax.el:3.0.1-b08"
dependency "org.glassfish.main:javax.jws:4.0-b33"
dependency "org.glassfish.tyrus:tyrus-container-servlet:1.13.1"
}
maven { url "https://repo.spring.io/libs-spring-framework-build" }
if (version.contains('-')) {
maven { url "https://repo.spring.io/milestone" }
generatedPomCustomization {
enabled = false
}
if (version.endsWith('-SNAPSHOT')) {
maven { url "https://repo.spring.io/snapshot" }
resolutionStrategy {
cacheChangingModulesFor 0, "seconds"
}
repositories {
mavenCentral()
maven { url "https://repo.spring.io/libs-spring-framework-build" }
}
}
configurations.all {
@@ -50,17 +308,27 @@ configure([rootProject] + javaProjects) { project ->
apply plugin: "java"
apply plugin: "java-test-fixtures"
apply plugin: "checkstyle"
apply plugin: 'org.springframework.build.conventions'
apply plugin: 'org.springframework.build.compile'
apply from: "${rootDir}/gradle/toolchains.gradle"
apply from: "${rootDir}/gradle/ide.gradle"
configurations {
dependencyManagement {
canBeConsumed = false
canBeResolved = false
visible = false
pluginManager.withPlugin("kotlin") {
apply plugin: "org.jetbrains.dokka"
apply from: "${rootDir}/gradle/docs-dokka.gradle"
compileKotlin {
kotlinOptions {
languageVersion = "1.3"
apiVersion = "1.3"
freeCompilerArgs = ["-Xjsr305=strict", "-Xsuppress-version-warnings", "-Xopt-in=kotlin.RequiresOptIn"]
allWarningsAsErrors = true
}
}
compileTestKotlin {
kotlinOptions {
freeCompilerArgs = ["-Xjsr305=strict"]
}
}
matching { it.name.endsWith("Classpath") }.all { it.extendsFrom(dependencyManagement) }
}
test {
@@ -69,29 +337,14 @@ configure([rootProject] + javaProjects) { project ->
systemProperty("java.awt.headless", "true")
systemProperty("testGroups", project.properties.get("testGroups"))
systemProperty("io.netty.leakDetection.level", "paranoid")
systemProperty("io.netty5.leakDetectionLevel", "paranoid")
systemProperty("io.netty5.leakDetection.targetRecords", "32")
systemProperty("io.netty5.buffer.lifecycleTracingEnabled", "true")
systemProperty("io.netty5.buffer.leakDetectionEnabled", "true")
jvmArgs(["--add-opens=java.base/java.lang=ALL-UNNAMED",
"--add-opens=java.base/java.util=ALL-UNNAMED"])
}
checkstyle {
toolVersion = "10.7.0"
toolVersion = "10.9.3"
configDirectory.set(rootProject.file("src/checkstyle"))
}
tasks.named("checkstyleMain").configure {
maxHeapSize = "1g"
}
tasks.named("checkstyleTest").configure {
maxHeapSize = "1g"
}
dependencies {
dependencyManagement(enforcedPlatform(dependencies.project(path: ":framework-platform")))
testImplementation("org.junit.jupiter:junit-jupiter-api")
testImplementation("org.junit.jupiter:junit-jupiter-params")
testImplementation("org.junit.platform:junit-platform-suite-api")
@@ -113,29 +366,29 @@ configure([rootProject] + javaProjects) { project ->
}
ext.javadocLinks = [
"https://docs.oracle.com/en/java/javase/17/docs/api/",
"https://jakarta.ee/specifications/platform/9/apidocs/",
"https://docs.oracle.com/javase/8/docs/api/",
"https://docs.oracle.com/javaee/7/api/",
"https://docs.oracle.com/cd/E13222_01/wls/docs90/javadocs/", // CommonJ
"https://www.ibm.com/docs/api/v1/content/SSEQTP_8.5.5/com.ibm.websphere.javadoc.doc/web/apidocs/",
"https://docs.jboss.org/jbossas/javadoc/4.0.5/connector/",
"https://docs.jboss.org/jbossas/javadoc/7.1.2.Final/",
"https://tiles.apache.org/tiles-request/apidocs/",
"https://tiles.apache.org/framework/apidocs/",
"https://www.eclipse.org/aspectj/doc/released/aspectj5rt-api/",
"https://www.ehcache.org/apidocs/2.10.4/",
"https://www.quartz-scheduler.org/api/2.3.0/",
"https://www.javadoc.io/doc/com.fasterxml.jackson.core/jackson-core/2.14.1/",
"https://www.javadoc.io/doc/com.fasterxml.jackson.core/jackson-databind/2.14.1/",
"https://www.javadoc.io/doc/com.fasterxml.jackson.dataformat/jackson-dataformat-xml/2.14.1/",
"https://hc.apache.org/httpcomponents-client-5.2.x/current/httpclient5/apidocs/",
"https://www.javadoc.io/doc/com.fasterxml.jackson.core/jackson-core/2.12.7/",
"https://www.javadoc.io/doc/com.fasterxml.jackson.core/jackson-databind/2.12.7/",
"https://www.javadoc.io/doc/com.fasterxml.jackson.dataformat/jackson-dataformat-xml/2.12.7/",
"https://hc.apache.org/httpcomponents-client-5.1.x/current/httpclient5/apidocs/",
"https://projectreactor.io/docs/test/release/api/",
"https://junit.org/junit4/javadoc/4.13.2/",
// TODO Uncomment link to JUnit 5 docs once we have sorted out
// the following warning in the build.
//
// warning: The code being documented uses packages in the unnamed module, but the packages defined in https://junit.org/junit5/docs/5.9.2/api/ are in named modules.
//
// "https://junit.org/junit5/docs/5.9.2/api/",
// Disabling linking to JUnit 5.8.2, since the `package-list` file no longer exists due to
// https://github.com/junit-team/junit5/commit/67ad4e545518b0ce2b0e7c96df31a669866d5003.
// "https://junit.org/junit5/docs/5.8.2/api/",
"https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/",
"https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/",
"https://r2dbc.io/spec/1.0.0.RELEASE/api/",
"https://r2dbc.io/spec/0.8.5.RELEASE/api/",
// The external Javadoc link for JSR 305 must come last to ensure that types from
// JSR 250 (such as @PostConstruct) are still supported. This is due to the fact
// that JSR 250 and JSR 305 both define types in javax.annotation, which results
@@ -152,14 +405,17 @@ configure(moduleProjects) { project ->
configure(rootProject) {
description = "Spring Framework"
apply plugin: "kotlin"
apply plugin: "io.spring.nohttp"
apply plugin: 'org.springframework.build.api-diff'
apply from: "${rootDir}/gradle/publications.gradle"
apply from: "${rootDir}/gradle/docs.gradle"
nohttp {
source.exclude "**/test-output/**"
allowlistFile = project.file("src/nohttp/allowlist.lines")
def rootPath = file(rootDir).toPath()
def projectDirs = allprojects.collect { it.projectDir } + "${rootDir}/buildSrc"
def projectDirs = allprojects.collect { it.projectDir } + "${rootDir}/spring-core-test" + "${rootDir}/buildSrc" + "${rootDir}/framework-docs"
projectDirs.forEach { dir ->
[ 'bin', 'build', 'out', '.settings' ]
.collect { rootPath.relativize(new File(dir, it).toPath()) }
@@ -174,4 +430,14 @@ configure(rootProject) {
maxHeapSize = "1g"
}
publishing {
publications {
mavenJava(MavenPublication) {
artifact docsZip
artifact schemaZip
artifact distZip
}
}
}
}
+2 -42
View File
@@ -5,12 +5,9 @@ They are declared in the `build.gradle` file in this folder.
## Build Conventions
The `org.springframework.build.conventions` plugin applies all conventions to the Framework build:
* Configuring the Java compiler, see `JavaConventions`
* Configuring the Kotlin compiler, see `KotlinConventions`
* Configuring testing in the build with `TestConventions`
### Compiler conventions
The `org.springframework.build.compile` plugin applies the Java compiler conventions to the build.
## Build Plugins
@@ -36,40 +33,3 @@ current working version with. You can generate the reports for all modules or a
```
The reports are located under `build/reports/api-diff/$OLDVERSION_to_$NEWVERSION/`.
### RuntimeHints Java Agent
The `spring-core-test` project module contributes the `RuntimeHintsAgent` Java agent.
The `RuntimeHintsAgentPlugin` Gradle plugin creates a dedicated `"runtimeHintsTest"` test task for each project.
This task will detect and execute [tests tagged](https://junit.org/junit5/docs/current/user-guide/#running-tests-build-gradle)
with the `"RuntimeHintsTests"` [JUnit tag](https://junit.org/junit5/docs/current/user-guide/#running-tests-tags).
In the Spring Framework test suite, those are usually annotated with the `@EnabledIfRuntimeHintsAgent` annotation.
By default, the agent will instrument all classes located in the `"org.springframework"` package, as they are loaded.
The `RuntimeHintsAgentExtension` allows to customize this using a DSL:
```groovy
// this applies the `RuntimeHintsAgentPlugin` to the project
plugins {
id 'org.springframework.build.runtimehints-agent'
}
// You can configure the agent to include and exclude packages from the instrumentation process.
runtimeHintsAgent {
includedPackages = ["org.springframework", "io.spring"]
excludedPackages = ["org.example"]
}
dependencies {
// to use the test infrastructure, the project should also depend on the "spring-core-test" module
testImplementation(project(":spring-core-test"))
}
```
With this configuration, `./gradlew runtimeHintsTest` will run all tests instrumented by this java agent.
The global `./gradlew check` task depends on `runtimeHintsTest`.
NOTE: the "spring-core-test" module doesn't shade "spring-core" by design, so the agent should never instrument
code that doesn't have "spring-core" on its classpath.
+3 -19
View File
@@ -7,20 +7,8 @@ repositories {
gradlePluginPortal()
}
ext {
def propertiesFile = new File(new File("$projectDir").parentFile, "gradle.properties")
propertiesFile.withInputStream {
def properties = new Properties()
properties.load(it)
set("kotlinVersion", properties["kotlinVersion"])
}
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
implementation("org.jetbrains.kotlin:kotlin-compiler-embeddable:${kotlinVersion}")
implementation "me.champeau.gradle:japicmp-gradle-plugin:0.3.0"
implementation "org.gradle:test-retry-gradle-plugin:1.4.1"
}
gradlePlugin {
@@ -29,17 +17,13 @@ gradlePlugin {
id = "org.springframework.build.api-diff"
implementationClass = "org.springframework.build.api.ApiDiffPlugin"
}
conventionsPlugin {
id = "org.springframework.build.conventions"
implementationClass = "org.springframework.build.ConventionsPlugin"
compileConventionsPlugin {
id = "org.springframework.build.compile"
implementationClass = "org.springframework.build.compile.CompilerConventionsPlugin"
}
optionalDependenciesPlugin {
id = "org.springframework.build.optional-dependencies"
implementationClass = "org.springframework.build.optional.OptionalDependenciesPlugin"
}
runtimeHintsAgentPlugin {
id = "org.springframework.build.runtimehints-agent"
implementationClass = "org.springframework.build.hint.RuntimeHintsAgentPlugin"
}
}
}
@@ -1,45 +0,0 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.build;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.plugins.JavaBasePlugin;
import org.jetbrains.kotlin.gradle.plugin.KotlinBasePlugin;
/**
* Plugin to apply conventions to projects that are part of Spring Framework's build.
* Conventions are applied in response to various plugins being applied.
*
* When the {@link JavaBasePlugin} is applied, the conventions in {@link TestConventions}
* are applied.
* When the {@link JavaBasePlugin} is applied, the conventions in {@link JavaConventions}
* are applied.
* When the {@link KotlinBasePlugin} is applied, the conventions in {@link KotlinConventions}
* are applied.
*
* @author Brian Clozel
*/
public class ConventionsPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
new JavaConventions().apply(project);
new KotlinConventions().apply(project);
new TestConventions().apply(project);
}
}
@@ -1,48 +0,0 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.build;
import java.util.ArrayList;
import java.util.List;
import org.gradle.api.Project;
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions;
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile;
/**
* @author Brian Clozel
*/
public class KotlinConventions {
void apply(Project project) {
project.getPlugins().withId("org.jetbrains.kotlin.jvm",
(plugin) -> project.getTasks().withType(KotlinCompile.class, this::configure));
}
private void configure(KotlinCompile compile) {
KotlinJvmOptions kotlinOptions = compile.getKotlinOptions();
kotlinOptions.setApiVersion("1.7");
kotlinOptions.setLanguageVersion("1.7");
kotlinOptions.setJvmTarget("17");
kotlinOptions.setJavaParameters(true);
kotlinOptions.setAllWarningsAsErrors(true);
List<String> freeCompilerArgs = new ArrayList<>(compile.getKotlinOptions().getFreeCompilerArgs());
freeCompilerArgs.addAll(List.of("-Xsuppress-version-warnings", "-Xjsr305=strict", "-opt-in=kotlin.RequiresOptIn"));
compile.getKotlinOptions().setFreeCompilerArgs(freeCompilerArgs);
}
}
@@ -1,55 +0,0 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.build;
import org.gradle.api.Project;
import org.gradle.api.plugins.JavaBasePlugin;
import org.gradle.api.tasks.testing.Test;
import org.gradle.testretry.TestRetryPlugin;
import org.gradle.testretry.TestRetryTaskExtension;
/**
* Conventions that are applied in the presence of the {@link JavaBasePlugin}. When the
* plugin is applied:
* <ul>
* <li>The {@link TestRetryPlugin Test Retry} plugins is applied so that flaky tests
* are retried 3 times when running on the CI.
* </ul>
*
* @author Brian Clozel
* @author Andy Wilkinson
*/
class TestConventions {
void apply(Project project) {
project.getPlugins().withType(JavaBasePlugin.class, (java) -> configureTestConventions(project));
}
private void configureTestConventions(Project project) {
project.getTasks().withType(Test.class,
(test) -> project.getPlugins().withType(TestRetryPlugin.class, (testRetryPlugin) -> {
TestRetryTaskExtension testRetry = test.getExtensions().getByType(TestRetryTaskExtension.class);
testRetry.getFailOnPassedAfterRetry().set(true);
testRetry.getMaxRetries().set(isCi() ? 3 : 0);
}));
}
private boolean isCi() {
return Boolean.parseBoolean(System.getenv("CI"));
}
}
@@ -16,7 +16,6 @@
package org.springframework.build.api;
import java.io.File;
import java.net.URI;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
@@ -24,7 +23,6 @@ import java.util.List;
import me.champeau.gradle.japicmp.JapicmpPlugin;
import me.champeau.gradle.japicmp.JapicmpTask;
import org.gradle.api.GradleException;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
@@ -34,12 +32,10 @@ import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.publish.maven.plugins.MavenPublishPlugin;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.jvm.tasks.Jar;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link Plugin} that applies the {@code "japicmp-gradle-plugin"}
* and create tasks for all subprojects named {@code "spring-*"}, diffing the public API one by one
* and create tasks for all subprojects, diffing the public API one by one
* and creating the reports in {@code "build/reports/api-diff/$OLDVERSION_to_$NEWVERSION/"}.
* <p>{@code "./gradlew apiDiff -PbaselineVersion=5.1.0.RELEASE"} will output the
* reports for the API diff between the baseline version and the current one for all modules.
@@ -50,16 +46,12 @@ import org.slf4j.LoggerFactory;
*/
public class ApiDiffPlugin implements Plugin<Project> {
private static final Logger logger = LoggerFactory.getLogger(ApiDiffPlugin.class);
public static final String TASK_NAME = "apiDiff";
private static final String BASELINE_VERSION_PROPERTY = "baselineVersion";
private static final List<String> PACKAGE_INCLUDES = Collections.singletonList("org.springframework.*");
private static final URI SPRING_MILESTONE_REPOSITORY = URI.create("https://repo.spring.io/milestone");
@Override
public void apply(Project project) {
if (project.hasProperty(BASELINE_VERSION_PROPERTY) && project.equals(project.getRootProject())) {
@@ -71,24 +63,16 @@ public class ApiDiffPlugin implements Plugin<Project> {
private void applyApiDiffConventions(Project project) {
String baselineVersion = project.property(BASELINE_VERSION_PROPERTY).toString();
project.subprojects(subProject -> {
if (subProject.getName().startsWith("spring-")) {
createApiDiffTask(baselineVersion, subProject);
}
});
project.subprojects(subProject -> createApiDiffTask(baselineVersion, subProject));
}
private void createApiDiffTask(String baselineVersion, Project project) {
if (isProjectEligible(project)) {
// Add Spring Milestone repository for generating diffs against previous milestones
project.getRootProject()
.getRepositories()
.maven(mavenArtifactRepository -> mavenArtifactRepository.setUrl(SPRING_MILESTONE_REPOSITORY));
JapicmpTask apiDiff = project.getTasks().create(TASK_NAME, JapicmpTask.class);
apiDiff.setDescription("Generates an API diff report with japicmp");
apiDiff.setGroup(JavaBasePlugin.DOCUMENTATION_GROUP);
apiDiff.setOldClasspath(createBaselineConfiguration(baselineVersion, project));
apiDiff.setOldClasspath(project.files(createBaselineConfiguration(baselineVersion, project)));
TaskProvider<Jar> jar = project.getTasks().withType(Jar.class).named("jar");
apiDiff.setNewArchives(project.getLayout().files(jar.get().getArchiveFile().get().getAsFile()));
apiDiff.setNewClasspath(getRuntimeClassPath(project));
@@ -114,16 +98,7 @@ public class ApiDiffPlugin implements Plugin<Project> {
String baseline = String.join(":",
project.getGroup().toString(), project.getName(), baselineVersion);
Dependency baselineDependency = project.getDependencies().create(baseline + "@jar");
Configuration baselineConfiguration = project.getRootProject().getConfigurations().detachedConfiguration(baselineDependency);
try {
// eagerly resolve the baseline configuration to check whether this is a new Spring module
baselineConfiguration.resolve();
return baselineConfiguration;
}
catch (GradleException exception) {
logger.warn("Could not resolve {} - assuming this is a new Spring module.", baseline);
}
return project.getRootProject().getConfigurations().detachedConfiguration();
return project.getRootProject().getConfigurations().detachedConfiguration(baselineDependency);
}
private Configuration getRuntimeClassPath(Project project) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2021 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.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.build;
package org.springframework.build.compile;
import java.util.ArrayList;
import java.util.Arrays;
@@ -22,8 +22,9 @@ import java.util.List;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.plugins.JavaBasePlugin;
import org.gradle.api.plugins.JavaLibraryPlugin;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.JavaPluginConvention;
import org.gradle.api.tasks.compile.JavaCompile;
/**
@@ -33,7 +34,7 @@ import org.gradle.api.tasks.compile.JavaCompile;
* @author Sam Brannen
* @author Sebastien Deleuze
*/
public class JavaConventions {
public class CompilerConventionsPlugin implements Plugin<Project> {
private static final List<String> COMPILER_ARGS;
@@ -58,8 +59,9 @@ public class JavaConventions {
"-Xlint:-deprecation", "-Xlint:-unchecked"));
}
@Override
public void apply(Project project) {
project.getPlugins().withType(JavaBasePlugin.class, javaPlugin -> applyJavaCompileConventions(project));
project.getPlugins().withType(JavaLibraryPlugin.class, javaPlugin -> applyJavaCompileConventions(project));
}
/**
@@ -68,6 +70,7 @@ public class JavaConventions {
* @param project the current project
*/
private void applyJavaCompileConventions(Project project) {
JavaPluginConvention java = project.getConvention().getPlugin(JavaPluginConvention.class);
project.getTasks().withType(JavaCompile.class)
.matching(compileTask -> compileTask.getName().equals(JavaPlugin.COMPILE_JAVA_TASK_NAME))
.forEach(compileTask -> {
@@ -1,53 +0,0 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.build.hint;
import java.util.Collections;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.provider.SetProperty;
/**
* Entry point to the DSL extension for the {@link RuntimeHintsAgentPlugin} Gradle plugin.
* @author Brian Clozel
*/
public class RuntimeHintsAgentExtension {
private final SetProperty<String> includedPackages;
private final SetProperty<String> excludedPackages;
public RuntimeHintsAgentExtension(ObjectFactory objectFactory) {
this.includedPackages = objectFactory.setProperty(String.class).convention(Collections.singleton("org.springframework"));
this.excludedPackages = objectFactory.setProperty(String.class).convention(Collections.emptySet());
}
public SetProperty<String> getIncludedPackages() {
return this.includedPackages;
}
public SetProperty<String> getExcludedPackages() {
return this.excludedPackages;
}
String asJavaAgentArgument() {
StringBuilder builder = new StringBuilder();
this.includedPackages.get().forEach(packageName -> builder.append('+').append(packageName).append(','));
this.excludedPackages.get().forEach(packageName -> builder.append('-').append(packageName).append(','));
return builder.toString();
}
}
@@ -1,56 +0,0 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.build.hint;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.tasks.bundling.Jar;
import org.gradle.api.tasks.testing.Test;
/**
* {@link Plugin} that configures the {@code RuntimeHints} Java agent to test tasks.
*
* @author Brian Clozel
*/
public class RuntimeHintsAgentPlugin implements Plugin<Project> {
public static final String RUNTIMEHINTS_TEST_TASK = "runtimeHintsTest";
private static final String EXTENSION_NAME = "runtimeHintsAgent";
@Override
public void apply(Project project) {
project.getPlugins().withType(JavaPlugin.class, javaPlugin -> {
RuntimeHintsAgentExtension agentExtension = project.getExtensions().create(EXTENSION_NAME,
RuntimeHintsAgentExtension.class, project.getObjects());
Test agentTest = project.getTasks().create(RUNTIMEHINTS_TEST_TASK, Test.class, test -> {
test.useJUnitPlatform(options -> {
options.includeTags("RuntimeHintsTests");
});
test.include("**/*Tests.class", "**/*Test.class");
test.systemProperty("java.awt.headless", "true");
});
project.afterEvaluate(p -> {
Jar jar = project.getRootProject().project("spring-core-test").getTasks().withType(Jar.class).named("jar").get();
agentTest.jvmArgs("-javaagent:" + jar.getArchiveFile().get().getAsFile() + "=" + agentExtension.asJavaAgentArgument());
});
project.getTasks().getByName("check", task -> task.dependsOn(agentTest));
});
}
}
@@ -19,9 +19,12 @@ package org.springframework.build.optional;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.attributes.Usage;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.JavaPluginExtension;
import org.gradle.api.plugins.JavaPluginConvention;
import org.gradle.api.tasks.SourceSetContainer;
import org.gradle.plugins.ide.eclipse.EclipsePlugin;
import org.gradle.plugins.ide.eclipse.model.EclipseModel;
/**
* A {@code Plugin} that adds support for Maven-style optional dependencies. Creates a new
@@ -44,7 +47,7 @@ public class OptionalDependenciesPlugin implements Plugin<Project> {
optional.setCanBeConsumed(false);
optional.setCanBeResolved(false);
project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> {
SourceSetContainer sourceSets = project.getExtensions().getByType(JavaPluginExtension.class)
SourceSetContainer sourceSets = project.getConvention().getPlugin(JavaPluginConvention.class)
.getSourceSets();
sourceSets.all((sourceSet) -> {
project.getConfigurations().getByName(sourceSet.getCompileClasspathConfigurationName()).extendsFrom(optional);
@@ -1,175 +0,0 @@
package org.springframework.build.shadow;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.gradle.api.DefaultTask;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.component.ModuleComponentSelector;
import org.gradle.api.artifacts.query.ArtifactResolutionQuery;
import org.gradle.api.artifacts.result.ArtifactResolutionResult;
import org.gradle.api.artifacts.result.ComponentArtifactsResult;
import org.gradle.api.artifacts.result.DependencyResult;
import org.gradle.api.artifacts.result.ResolutionResult;
import org.gradle.api.artifacts.result.ResolvedArtifactResult;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.FileCopyDetails;
import org.gradle.api.file.FileTree;
import org.gradle.api.tasks.Classpath;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Nested;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.TaskAction;
import org.gradle.jvm.JvmLibrary;
import org.gradle.language.base.artifact.SourcesArtifact;
/**
* Gradle task to add source from shadowed jars into our own source jars.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
public class ShadowSource extends DefaultTask {
private final DirectoryProperty outputDirectory = getProject().getObjects().directoryProperty();
private List<Configuration> configurations = new ArrayList<>();
private final List<Relocation> relocations = new ArrayList<>();
@Classpath
@Optional
public List<Configuration> getConfigurations() {
return this.configurations;
}
public void setConfigurations(List<Configuration> configurations) {
this.configurations = configurations;
}
@Nested
public List<Relocation> getRelocations() {
return this.relocations;
}
public void relocate(String pattern, String destination) {
this.relocations.add(new Relocation(pattern, destination));
}
@OutputDirectory
DirectoryProperty getOutputDirectory() {
return this.outputDirectory;
}
@TaskAction
void syncSourceJarFiles() {
sync(getSourceJarFiles());
}
private List<File> getSourceJarFiles() {
List<File> sourceJarFiles = new ArrayList<>();
for (Configuration configuration : this.configurations) {
ResolutionResult resolutionResult = configuration.getIncoming().getResolutionResult();
resolutionResult.getRootComponent().get().getDependencies().forEach(dependency -> {
Set<ComponentArtifactsResult> artifactsResults = resolveSourceArtifacts(dependency);
for (ComponentArtifactsResult artifactResult : artifactsResults) {
artifactResult.getArtifacts(SourcesArtifact.class).forEach(sourceArtifact -> {
sourceJarFiles.add(((ResolvedArtifactResult) sourceArtifact).getFile());
});
}
});
}
return Collections.unmodifiableList(sourceJarFiles);
}
private Set<ComponentArtifactsResult> resolveSourceArtifacts(DependencyResult dependency) {
ModuleComponentSelector componentSelector = (ModuleComponentSelector) dependency.getRequested();
ArtifactResolutionQuery query = getProject().getDependencies().createArtifactResolutionQuery()
.forModule(componentSelector.getGroup(), componentSelector.getModule(), componentSelector.getVersion());
return executeQuery(query).getResolvedComponents();
}
@SuppressWarnings("unchecked")
private ArtifactResolutionResult executeQuery(ArtifactResolutionQuery query) {
return query.withArtifacts(JvmLibrary.class, SourcesArtifact.class).execute();
}
private void sync(List<File> sourceJarFiles) {
getProject().sync(spec -> {
spec.into(this.outputDirectory);
spec.eachFile(this::relocateFile);
spec.filter(this::transformContent);
spec.exclude("META-INF/**");
spec.setIncludeEmptyDirs(false);
sourceJarFiles.forEach(sourceJar -> spec.from(zipTree(sourceJar)));
});
}
private void relocateFile(FileCopyDetails details) {
String path = details.getPath();
for (Relocation relocation : this.relocations) {
path = relocation.relocatePath(path);
}
details.setPath(path);
}
private String transformContent(String content) {
for (Relocation relocation : this.relocations) {
content = relocation.transformContent(content);
}
return content;
}
private FileTree zipTree(File sourceJar) {
return getProject().zipTree(sourceJar);
}
/**
* A single relocation.
*/
static class Relocation {
private final String pattern;
private final String pathPattern;
private final String destination;
private final String pathDestination;
Relocation(String pattern, String destination) {
this.pattern = pattern;
this.pathPattern = pattern.replace('.', '/');
this.destination = destination;
this.pathDestination = destination.replace('.', '/');
}
@Input
public String getPattern() {
return this.pattern;
}
@Input
public String getDestination() {
return this.destination;
}
String relocatePath(String path) {
return path.replace(this.pathPattern, this.pathDestination);
}
public String transformContent(String content) {
return content.replaceAll("\\b" + this.pattern, this.destination);
}
}
}
+2 -2
View File
@@ -2,7 +2,7 @@
The Spring Framework uses https://concourse-ci.org/[Concourse] for its CI build and other automated tasks.
The Spring team has a dedicated Concourse instance available at https://ci.spring.io with a build pipeline
for https://ci.spring.io/teams/spring-framework/pipelines/spring-framework-6.0.x[Spring Framework 6.0.x].
for https://ci.spring.io/teams/spring-framework/pipelines/spring-framework-5.3.x[Spring Framework 5.3.x].
=== Setting up your development environment
@@ -51,7 +51,7 @@ The pipeline can be deployed using the following command:
[source]
----
$ fly -t spring set-pipeline -p spring-framework-6.0.x -c ci/pipeline.yml -l ci/parameters.yml
$ fly -t spring set-pipeline -p spring-framework-5.3.x -c ci/pipeline.yml -l ci/parameters.yml
----
NOTE: This assumes that you have credhub integration configured with the appropriate secrets.
+1 -1
View File
@@ -17,4 +17,4 @@ changelog:
- "type: dependency-upgrade"
contributors:
exclude:
names: ["bclozel", "jhoeller", "poutsma", "rstoyanchev", "sbrannen", "sdeleuze", "snicoll", "simonbasle"]
names: ["bclozel", "jhoeller", "poutsma", "rstoyanchev", "sbrannen", "sdeleuze", "snicoll"]
+2 -3
View File
@@ -2,10 +2,9 @@ FROM ubuntu:focal-20220922
ADD setup.sh /setup.sh
ADD get-jdk-url.sh /get-jdk-url.sh
RUN ./setup.sh
RUN ./setup.sh java8
ENV JAVA_HOME /opt/openjdk/java17
ENV JAVA_HOME /opt/openjdk/java8
ENV JDK17 /opt/openjdk/java17
ENV JDK19 /opt/openjdk/java19
ENV PATH $JAVA_HOME/bin:$PATH
+4 -4
View File
@@ -2,11 +2,11 @@
set -e
case "$1" in
java17)
echo "https://github.com/bell-sw/Liberica/releases/download/17.0.6+10/bellsoft-jdk17.0.6+10-linux-amd64.tar.gz"
java8)
echo "https://github.com/bell-sw/Liberica/releases/download/8u345+1/bellsoft-jdk8u345+1-linux-amd64.tar.gz"
;;
java19)
echo "https://github.com/bell-sw/Liberica/releases/download/19.0.2+9/bellsoft-jdk19.0.2+9-linux-amd64.tar.gz"
java17)
echo "https://github.com/bell-sw/Liberica/releases/download/17.0.4+8/bellsoft-jdk17.0.4+8-linux-amd64.tar.gz"
;;
*)
echo $"Unknown java version"
+1 -1
View File
@@ -20,7 +20,7 @@ curl https://raw.githubusercontent.com/spring-io/concourse-java-scripts/v0.0.4/c
mkdir -p /opt/openjdk
pushd /opt/openjdk > /dev/null
for jdk in java17 java19
for jdk in java8 java17
do
JDK_URL=$( /get-jdk-url.sh $jdk )
mkdir $jdk
+2 -2
View File
@@ -3,8 +3,8 @@ github-repo-name: "spring-projects/spring-framework"
sonatype-staging-profile: "org.springframework"
docker-hub-organization: "springci"
artifactory-server: "https://repo.spring.io"
branch: "main"
milestone: "6.0.x"
branch: "5.3.x"
milestone: "5.3.x"
build-name: "spring-framework"
pipeline-name: "spring-framework"
concourse-url: "https://ci.spring.io"
+28 -32
View File
@@ -45,7 +45,7 @@ resource_types:
source:
<<: *docker-resource-source
repository: concourse/registry-image-resource
tag: 1.5.0
tag: 1.7.1
- name: artifactory-resource
type: registry-image
source:
@@ -57,7 +57,7 @@ resource_types:
source:
<<: *docker-resource-source
repository: concourse/github-release-resource
tag: 1.5.5
tag: 1.8.0
- name: github-status-resource
type: registry-image
source:
@@ -82,6 +82,13 @@ resources:
icon: github
source:
<<: *git-repo-resource-source
- name: every-morning
type: time
icon: alarm
source:
start: 8:00 AM
stop: 9:00 AM
location: Europe/Vienna
- name: ci-images-git-repo
type: git
icon: github
@@ -96,13 +103,6 @@ resources:
<<: *docker-resource-source
repository: ((docker-hub-organization))/spring-framework-ci
tag: ((milestone))
- name: every-morning
type: time
icon: alarm
source:
start: 8:00 AM
stop: 9:00 AM
location: Europe/Vienna
- name: artifactory-repo
type: artifactory-resource
icon: package-variant
@@ -127,14 +127,14 @@ resources:
access_token: ((github-ci-status-token))
branch: ((branch))
context: build
- name: repo-status-jdk19-build
- name: repo-status-jdk17-build
type: github-status-resource
icon: eye-check-outline
source:
repository: ((github-repo-name))
access_token: ((github-ci-status-token))
branch: ((branch))
context: jdk19-build
context: jdk17-build
- name: slack-alert
type: slack-notification
icon: slack
@@ -212,26 +212,26 @@ jobs:
threads: 8
artifact_set:
- include:
- "/**/framework-docs-*.zip"
- "/**/spring-*.zip"
properties:
"zip.name": "spring-framework"
"zip.displayname": "Spring Framework"
"zip.deployed": "false"
- include:
- "/**/framework-docs-*-docs.zip"
- "/**/spring-*-docs.zip"
properties:
"zip.type": "docs"
- include:
- "/**/framework-docs-*-dist.zip"
- "/**/spring-*-dist.zip"
properties:
"zip.type": "dist"
- include:
- "/**/framework-docs-*-schema.zip"
- "/**/spring-*-schema.zip"
properties:
"zip.type": "schema"
get_params:
threads: 8
- name: jdk19-build
- name: jdk17-build
serial: true
public: true
plan:
@@ -239,25 +239,25 @@ jobs:
- get: git-repo
- get: every-morning
trigger: true
- put: repo-status-jdk19-build
- put: repo-status-jdk17-build
params: { state: "pending", commit: "git-repo" }
- do:
- task: check-project
image: ci-image
file: git-repo/ci/tasks/check-project.yml
privileged: true
timeout: ((task-timeout))
params:
TEST_TOOLCHAIN: 19
<<: *build-project-task-params
- task: check-project
image: ci-image
file: git-repo/ci/tasks/check-project.yml
privileged: true
timeout: ((task-timeout))
params:
TEST_TOOLCHAIN: 17
<<: *build-project-task-params
on_failure:
do:
- put: repo-status-jdk19-build
- put: repo-status-jdk17-build
params: { state: "failure", commit: "git-repo" }
- put: slack-alert
params:
<<: *slack-fail-params
- put: repo-status-jdk19-build
- put: repo-status-jdk17-build
params: { state: "success", commit: "git-repo" }
- name: build-pull-requests
serial: true
@@ -331,7 +331,6 @@ jobs:
params:
RELEASE_TYPE: M
<<: *github-task-params
<<: *docker-resource-source
- put: github-pre-release
params:
<<: *changelog-task-params
@@ -370,7 +369,6 @@ jobs:
file: git-repo/ci/tasks/promote-version.yml
params:
RELEASE_TYPE: RC
<<: *docker-resource-source
<<: *artifactory-task-params
- task: generate-changelog
file: git-repo/ci/tasks/generate-changelog.yml
@@ -415,7 +413,6 @@ jobs:
file: git-repo/ci/tasks/promote-version.yml
params:
RELEASE_TYPE: RELEASE
<<: *docker-resource-source
<<: *artifactory-task-params
<<: *sonatype-task-params
- name: create-github-release
@@ -433,7 +430,6 @@ jobs:
file: git-repo/ci/tasks/generate-changelog.yml
params:
RELEASE_TYPE: RELEASE
<<: *docker-resource-source
<<: *github-task-params
- put: github-release
params:
@@ -441,7 +437,7 @@ jobs:
groups:
- name: "builds"
jobs: ["build", "jdk19-build"]
jobs: ["build", "jdk17-build"]
- name: "releases"
jobs: ["stage-milestone", "stage-rc", "stage-release", "promote-milestone", "promote-rc", "promote-release", "create-github-release"]
- name: "ci-images"
+1 -1
View File
@@ -4,6 +4,6 @@ set -e
source $(dirname $0)/common.sh
pushd git-repo > /dev/null
./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false -Porg.gradle.java.installations.fromEnv=JDK17,JDK19 \
./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false -Porg.gradle.java.installations.fromEnv=JDK17 \
-PmainToolchain=${MAIN_TOOLCHAIN} -PtestToolchain=${TEST_TOOLCHAIN} --no-daemon --max-workers=4 check
popd > /dev/null
+1
View File
@@ -1,5 +1,6 @@
#!/bin/bash
source $(dirname $0)/common.sh
CONFIG_DIR=git-repo/ci/config
version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' )
+1 -2
View File
@@ -26,5 +26,4 @@ run:
cat > /root/.docker/config.json <<EOF
{ "auths": { "https://index.docker.io/v1/": { "auth": "$DOCKER_HUB_AUTH" }}}
EOF
build
build
+1 -3
View File
@@ -4,9 +4,7 @@ image_resource:
type: registry-image
source:
repository: springio/github-changelog-generator
tag: '0.0.8'
username: ((docker-hub-username))
password: ((docker-hub-password))
tag: '0.0.7'
inputs:
- name: git-repo
- name: artifactory-repo
-2
View File
@@ -5,8 +5,6 @@ image_resource:
source:
repository: springio/concourse-release-scripts
tag: '0.3.4'
username: ((docker-hub-username))
password: ((docker-hub-password))
inputs:
- name: git-repo
- name: artifactory-repo
@@ -1,20 +0,0 @@
// Spring Portfolio
:docs-site: https://docs.spring.io
:docs-spring-boot: {docs-site}/spring-boot/docs/current/reference
:docs-spring-gemfire: {docs-site}/spring-gemfire/docs/current/reference
:docs-spring-security: {docs-site}/spring-security/reference
// spring-asciidoctor-backends Settings
:chomp: default headers packages
:fold: all
// Spring Framework
:docs-spring-framework: {docs-site}/spring-framework/docs/{spring-version}
:api-spring-framework: {docs-spring-framework}/javadoc-api/org/springframework
:docs-java: {docdir}/../../main/java/org/springframework/docs
:docs-kotlin: {docdir}/../../main/kotlin/org/springframework/docs
:docs-resources: {docdir}/../../main/resources
:spring-framework-main-code: https://github.com/spring-projects/spring-framework/tree/main
// Third-party Links
:docs-graalvm: https://www.graalvm.org/22.3/reference-manual
:gh-rsocket: https://github.com/rsocket
:gh-rsocket-extensions: {gh-rsocket}/rsocket/blob/master/Extensions
:gh-rsocket-java: {gh-rsocket}/rsocket-java
@@ -1,309 +0,0 @@
[[core.aot]]
= Ahead of Time Optimizations
This chapter covers Spring's Ahead of Time (AOT) optimizations.
For AOT support specific to integration tests, see <<testing.adoc#testcontext-aot, Ahead of Time Support for Tests>>.
[[core.aot.introduction]]
== Introduction to Ahead of Time Optimizations
Spring's support for AOT optimizations is meant to inspect an `ApplicationContext` at build time and apply decisions and discovery logic that usually happens at runtime.
Doing so allows building an application startup arrangement that is more straightforward and focused on a fixed set of features based mainly on the classpath and the `Environment`.
Applying such optimizations early implies the following restrictions:
* The classpath is fixed and fully defined at build time.
* The beans defined in your application cannot change at runtime, meaning:
** `@Profile`, in particular profile-specific configuration needs to be chosen at build time.
** `Environment` properties that impact the presence of a bean (`@Conditional`) are only considered at build time.
* Bean definitions with instance suppliers (lambdas or method references) cannot be transformed ahead-of-time (see related https://github.com/spring-projects/spring-framework/issues/29555[spring-framework#29555] issue).
* The return type of methods annotated with `@Bean` should be the most specific type possible (typically the concrete class, not an interface) in order to support proper type inference without invoking the corresponding `@Bean` method at build time.
When these restrictions are in place, it becomes possible to perform ahead-of-time processing at build time and generate additional assets.
A Spring AOT processed application typically generates:
* Java source code
* Bytecode (usually for dynamic proxies)
* {api-spring-framework}/aot/hint/RuntimeHints.html[`RuntimeHints`] for the use of reflection, resource loading, serialization, and JDK proxies.
NOTE: At the moment, AOT is focused on allowing Spring applications to be deployed as native images using GraalVM.
We intend to support more JVM-based use cases in future generations.
[[core.aot.basics]]
== AOT engine overview
The entry point of the AOT engine for processing an `ApplicationContext` arrangement is `ApplicationContextAotGenerator`. It takes care of the following steps, based on a `GenericApplicationContext` that represents the application to optimize and a {api-spring-framework}/aot/generate/GenerationContext.html[`GenerationContext`]:
* Refresh an `ApplicationContext` for AOT processing. Contrary to a traditional refresh, this version only creates bean definitions, not bean instances.
* Invoke the available `BeanFactoryInitializationAotProcessor` implementations and apply their contributions against the `GenerationContext`.
For instance, a core implementation iterates over all candidate bean definitions and generates the necessary code to restore the state of the `BeanFactory`.
Once this process completes, the `GenerationContext` will have been updated with the generated code, resources, and classes that are necessary for the application to run.
The `RuntimeHints` instance can also be used to generate the relevant GraalVM native image configuration files.
`ApplicationContextAotGenerator#processAheadOfTime` returns the class name of the `ApplicationContextInitializer` entry point that allows the context to be started with AOT optimizations.
Those steps are covered in greater detail in the sections below.
[[core.aot.refresh]]
== Refresh for AOT Processing
Refresh for AOT processing is supported on all `GenericApplicationContext` implementations.
An application context is created with any number of entry points, usually in the form of `@Configuration`-annotated classes.
Let's look at a basic example:
include::code:AotProcessingSample[tag=myapplication]
Starting this application with the regular runtime involves a number of steps including classpath scanning, configuration class parsing, bean instantiation, and lifecycle callback handling.
Refresh for AOT processing only applies a subset of what happens with a <<beans-introduction,regular `refresh`>>.
AOT processing can be triggered as follows:
include::code:AotProcessingSample[tag=aotcontext]
In this mode, <<beans-factory-extension-factory-postprocessors,`BeanFactoryPostProcessor` implementations>> are invoked as usual.
This includes configuration class parsing, import selectors, classpath scanning, etc.
Such steps make sure that the `BeanRegistry` contains the relevant bean definitions for the application.
If bean definitions are guarded by conditions (such as `@Profile`), these are discarded at this stage.
Because this mode does not actually create bean instances, `BeanPostProcessor` implementations are not invoked, except for specific variants that are relevant for AOT processing.
These are:
* `MergedBeanDefinitionPostProcessor` implementations post-process bean definitions to extract additional settings, such as `init` and `destroy` methods.
* `SmartInstantiationAwareBeanPostProcessor` implementations determine a more precise bean type if necessary.
This makes sure to create any proxy that will be required at runtime.
Once this part completes, the `BeanFactory` contains the bean definitions that are necessary for the application to run. It does not trigger bean instantiation but allows the AOT engine to inspect the beans that will be created at runtime.
[[core.aot.bean-factory-initialization-contributions]]
== Bean Factory Initialization AOT Contributions
Components that want to participate in this step can implement the {api-spring-framework}/beans/factory/aot/BeanFactoryInitializationAotProcessor.html[`BeanFactoryInitializationAotProcessor`] interface.
Each implementation can return an AOT contribution, based on the state of the bean factory.
An AOT contribution is a component that contributes generated code that reproduces a particular behavior.
It can also contribute `RuntimeHints` to indicate the need for reflection, resource loading, serialization, or JDK proxies.
A `BeanFactoryInitializationAotProcessor` implementation can be registered in `META-INF/spring/aot.factories` with a key equal to the fully qualified name of the interface.
A `BeanFactoryInitializationAotProcessor` can also be implemented directly by a bean.
In this mode, the bean provides an AOT contribution equivalent to the feature it provides with a regular runtime.
Consequently, such a bean is automatically excluded from the AOT-optimized context.
[NOTE]
====
If a bean implements the `BeanFactoryInitializationAotProcessor` interface, the bean and **all** of its dependencies will be initialized during AOT processing.
We generally recommend that this interface is only implemented by infrastructure beans such as `BeanFactoryPostProcessor` which have limited dependencies and are already initialized early in the bean factory lifecycle.
If such a bean is registered using an `@Bean` factory method, ensure the method is `static` so that its enclosing `@Configuration` class does not have to be initialized.
====
[[core.aot.bean-registration-contributions]]
=== Bean Registration AOT Contributions
A core `BeanFactoryInitializationAotProcessor` implementation is responsible for collecting the necessary contributions for each candidate `BeanDefinition`.
It does so using a dedicated `BeanRegistrationAotProcessor`.
This interface is used as follows:
* Implemented by a `BeanPostProcessor` bean, to replace its runtime behavior.
For instance <<beans-factory-extension-bpp-examples-aabpp,`AutowiredAnnotationBeanPostProcessor`>> implements this interface to generate code that injects members annotated with `@Autowired`.
* Implemented by a type registered in `META-INF/spring/aot.factories` with a key equal to the fully qualified name of the interface.
Typically used when the bean definition needs to be tuned for specific features of the core framework.
[NOTE]
====
If a bean implements the `BeanRegistrationAotProcessor` interface, the bean and **all** of its dependencies will be initialized during AOT processing.
We generally recommend that this interface is only implemented by infrastructure beans such as `BeanFactoryPostProcessor` which have limited dependencies and are already initialized early in the bean factory lifecycle.
If such a bean is registered using an `@Bean` factory method, ensure the method is `static` so that its enclosing `@Configuration` class does not have to be initialized.
====
If no `BeanRegistrationAotProcessor` handles a particular registered bean, a default implementation processes it.
This is the default behavior, since tuning the generated code for a bean definition should be restricted to corner cases.
Taking our previous example, let's assume that `DataSourceConfiguration` is as follows:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
@Configuration(proxyBeanMethods = false)
public class DataSourceConfiguration {
@Bean
public SimpleDataSource dataSource() {
return new SimpleDataSource();
}
}
----
Since there isn't any particular condition on this class, `dataSourceConfiguration` and `dataSource` are identified as candidates.
The AOT engine will convert the configuration class above to code similar to the following:
[source,java,indent=0,role="primary"]
.Java
----
/**
* Bean definitions for {@link DataSourceConfiguration}
*/
public class DataSourceConfiguration__BeanDefinitions {
/**
* Get the bean definition for 'dataSourceConfiguration'
*/
public static BeanDefinition getDataSourceConfigurationBeanDefinition() {
Class<?> beanType = DataSourceConfiguration.class;
RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType);
beanDefinition.setInstanceSupplier(DataSourceConfiguration::new);
return beanDefinition;
}
/**
* Get the bean instance supplier for 'dataSource'.
*/
private static BeanInstanceSupplier<SimpleDataSource> getDataSourceInstanceSupplier() {
return BeanInstanceSupplier.<SimpleDataSource>forFactoryMethod(DataSourceConfiguration.class, "dataSource")
.withGenerator((registeredBean) -> registeredBean.getBeanFactory().getBean(DataSourceConfiguration.class).dataSource());
}
/**
* Get the bean definition for 'dataSource'
*/
public static BeanDefinition getDataSourceBeanDefinition() {
Class<?> beanType = SimpleDataSource.class;
RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType);
beanDefinition.setInstanceSupplier(getDataSourceInstanceSupplier());
return beanDefinition;
}
}
----
NOTE: The exact code generated may differ depending on the exact nature of your bean definitions.
The generated code above creates bean definitions equivalent to the `@Configuration` class, but in a direct way and without the use of reflection if at all possible.
There is a bean definition for `dataSourceConfiguration` and one for `dataSourceBean`.
When a `datasource` instance is required, a `BeanInstanceSupplier` is called.
This supplier invokes the `dataSource()` method on the `dataSourceConfiguration` bean.
[[core.aot.hints]]
== Runtime Hints
Running an application as a native image requires additional information compared to a regular JVM runtime.
For instance, GraalVM needs to know ahead of time if a component uses reflection.
Similarly, classpath resources are not shipped in a native image unless specified explicitly.
Consequently, if the application needs to load a resource, it must be referenced from the corresponding GraalVM native image configuration file.
The {api-spring-framework}/aot/hint/RuntimeHints.html[`RuntimeHints`] API collects the need for reflection, resource loading, serialization, and JDK proxies at runtime.
The following example makes sure that `config/app.properties` can be loaded from the classpath at runtime within a native image:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
runtimeHints.resources().registerPattern("config/app.properties");
----
A number of contracts are handled automatically during AOT processing.
For instance, the return type of a `@Controller` method is inspected, and relevant reflection hints are added if Spring detects that the type should be serialized (typically to JSON).
For cases that the core container cannot infer, you can register such hints programmatically.
A number of convenient annotations are also provided for common use cases.
[[core.aot.hints.import-runtime-hints]]
=== `@ImportRuntimeHints`
`RuntimeHintsRegistrar` implementations allow you to get a callback to the `RuntimeHints` instance managed by the AOT engine.
Implementations of this interface can be registered using `@ImportRuntimeHints` on any Spring bean or `@Bean` factory method.
`RuntimeHintsRegistrar` implementations are detected and invoked at build time.
include::code:SpellCheckService[]
If at all possible, `@ImportRuntimeHints` should be used as close as possible to the component that requires the hints.
This way, if the component is not contributed to the `BeanFactory`, the hints won't be contributed either.
It is also possible to register an implementation statically by adding an entry in `META-INF/spring/aot.factories` with a key equal to the fully qualified name of the `RuntimeHintsRegistrar` interface.
[[core.aot.hints.reflective]]
=== `@Reflective`
{api-spring-framework}/aot/hint/annotation/Reflective.html[`@Reflective`] provides an idiomatic way to flag the need for reflection on an annotated element.
For instance, `@EventListener` is meta-annotated with `@Reflective` since the underlying implementation invokes the annotated method using reflection.
By default, only Spring beans are considered and an invocation hint is registered for the annotated element.
This can be tuned by specifying a custom `ReflectiveProcessor` implementation via the
`@Reflective` annotation.
Library authors can reuse this annotation for their own purposes.
If components other than Spring beans need to be processed, a `BeanFactoryInitializationAotProcessor` can detect the relevant types and use `ReflectiveRuntimeHintsRegistrar` to process them.
[[core.aot.hints.register-reflection-for-binding]]
=== `@RegisterReflectionForBinding`
{api-spring-framework}/aot/hint/annotation/RegisterReflectionForBinding.html[`@RegisterReflectionForBinding`] is a specialization of `@Reflective` that registers the need for serializing arbitrary types.
A typical use case is the use of DTOs that the container cannot infer, such as using a web client within a method body.
`@RegisterReflectionForBinding` can be applied to any Spring bean at the class level, but it can also be applied directly to a method, field, or constructor to better indicate where the hints are actually required.
The following example registers `Account` for serialization.
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
@Component
public class OrderService {
@RegisterReflectionForBinding(Account.class)
public void process(Order order) {
// ...
}
}
----
[[core.aot.hints.testing]]
=== Testing Runtime Hints
Spring Core also ships `RuntimeHintsPredicates`, a utility for checking that existing hints match a particular use case.
This can be used in your own tests to validate that a `RuntimeHintsRegistrar` contains the expected results.
We can write a test for our `SpellCheckService` and ensure that we will be able to load a dictionary at runtime:
include::code:SpellCheckServiceTests[tag=hintspredicates]
With `RuntimeHintsPredicates`, we can check for reflection, resource, serialization, or proxy generation hints.
This approach works well for unit tests but implies that the runtime behavior of a component is well known.
You can learn more about the global runtime behavior of an application by running its test suite (or the app itself) with the {docs-graalvm}/native-image/metadata/AutomaticMetadataCollection/[GraalVM tracing agent].
This agent will record all relevant calls requiring GraalVM hints at runtime and write them out as JSON configuration files.
For more targeted discovery and testing, Spring Framework ships a dedicated module with core AOT testing utilities, `"org.springframework:spring-core-test"`.
This module contains the RuntimeHints Agent, a Java agent that records all method invocations that are related to runtime hints and helps you to assert that a given `RuntimeHints` instance covers all recorded invocations.
Let's consider a piece of infrastructure for which we'd like to test the hints we're contributing during the AOT processing phase.
include::code:SampleReflection[]
We can then write a unit test (no native compilation required) that checks our contributed hints:
include::code:SampleReflectionRuntimeHintsTests[]
If you forgot to contribute a hint, the test will fail and provide some details about the invocation:
[source,txt,indent=0,subs="verbatim,quotes"]
----
org.springframework.docs.core.aot.hints.testing.SampleReflection performReflection
INFO: Spring version:6.0.0-SNAPSHOT
Missing <"ReflectionHints"> for invocation <java.lang.Class#forName>
with arguments ["org.springframework.core.SpringVersion",
false,
jdk.internal.loader.ClassLoaders$AppClassLoader@251a69d7].
Stacktrace:
<"org.springframework.util.ClassUtils#forName, Line 284
io.spring.runtimehintstesting.SampleReflection#performReflection, Line 19
io.spring.runtimehintstesting.SampleReflectionRuntimeHintsTests#lambda$shouldRegisterReflectionHints$0, Line 25
----
There are various ways to configure this Java agent in your build, so please refer to the documentation of your build tool and test execution plugin.
The agent itself can be configured to instrument specific packages (by default, only `org.springframework` is instrumented).
You'll find more details in the {spring-framework-main-code}/buildSrc/README.md[Spring Framework `buildSrc` README] file.
@@ -1,23 +0,0 @@
[[spring-integration]]
= Integration
include::attributes.adoc[]
include::page-layout.adoc[]
This part of the reference documentation covers Spring Framework's integration with
a number of technologies.
include::integration/rest-clients.adoc[leveloffset=+1]
include::integration/jms.adoc[leveloffset=+1]
include::integration/jmx.adoc[leveloffset=+1]
include::integration/email.adoc[leveloffset=+1]
include::integration/scheduling.adoc[leveloffset=+1]
include::integration/cache.adoc[leveloffset=+1]
include::integration/observability.adoc[leveloffset=+1]
include::integration/integration-appendix.adoc[leveloffset=+1]
File diff suppressed because it is too large Load Diff
@@ -1,303 +0,0 @@
[[mail]]
= Email
This section describes how to send email with the Spring Framework.
.Library dependencies
****
The following JAR needs to be on the classpath of your application in order to use the
Spring Framework's email support:
* The https://jakartaee.github.io/mail-api/[Jakarta Mail] library
This library is freely available on the web -- for example, in Maven Central as
`com.sun.mail:jakarta.mail`. Please make sure to use the latest 2.x version (which uses
the `jakarta.mail` package namespace) rather than Jakarta Mail 1.6.x (which uses the
`javax.mail` package namespace).
****
The Spring Framework provides a helpful utility library for sending email that shields
you from the specifics of the underlying mailing system and is responsible for
low-level resource handling on behalf of the client.
The `org.springframework.mail` package is the root level package for the Spring
Framework's email support. The central interface for sending emails is the `MailSender`
interface. A simple value object that encapsulates the properties of a simple mail such
as `from` and `to` (plus many others) is the `SimpleMailMessage` class. This package
also contains a hierarchy of checked exceptions that provide a higher level of
abstraction over the lower level mail system exceptions, with the root exception being
`MailException`. See the {api-spring-framework}/mail/MailException.html[javadoc]
for more information on the rich mail exception hierarchy.
The `org.springframework.mail.javamail.JavaMailSender` interface adds specialized
JavaMail features, such as MIME message support to the `MailSender` interface
(from which it inherits). `JavaMailSender` also provides a callback interface called
`org.springframework.mail.javamail.MimeMessagePreparator` for preparing a `MimeMessage`.
[[mail-usage]]
== Usage
Assume that we have a business interface called `OrderManager`, as the following example shows:
[source,java,indent=0,subs="verbatim,quotes"]
----
public interface OrderManager {
void placeOrder(Order order);
}
----
Further assume that we have a requirement stating that an email message with an
order number needs to be generated and sent to a customer who placed the relevant order.
[[mail-usage-simple]]
=== Basic `MailSender` and `SimpleMailMessage` Usage
The following example shows how to use `MailSender` and `SimpleMailMessage` to send an
email when someone places an order:
[source,java,indent=0,subs="verbatim,quotes"]
----
import org.springframework.mail.MailException;
import org.springframework.mail.MailSender;
import org.springframework.mail.SimpleMailMessage;
public class SimpleOrderManager implements OrderManager {
private MailSender mailSender;
private SimpleMailMessage templateMessage;
public void setMailSender(MailSender mailSender) {
this.mailSender = mailSender;
}
public void setTemplateMessage(SimpleMailMessage templateMessage) {
this.templateMessage = templateMessage;
}
public void placeOrder(Order order) {
// Do the business calculations...
// Call the collaborators to persist the order...
// Create a thread safe "copy" of the template message and customize it
SimpleMailMessage msg = new SimpleMailMessage(this.templateMessage);
msg.setTo(order.getCustomer().getEmailAddress());
msg.setText(
"Dear " + order.getCustomer().getFirstName()
+ order.getCustomer().getLastName()
+ ", thank you for placing order. Your order number is "
+ order.getOrderNumber());
try {
this.mailSender.send(msg);
}
catch (MailException ex) {
// simply log it and go on...
System.err.println(ex.getMessage());
}
}
}
----
The following example shows the bean definitions for the preceding code:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
<property name="host" value="mail.mycompany.example"/>
</bean>
<!-- this is a template message that we can pre-load with default state -->
<bean id="templateMessage" class="org.springframework.mail.SimpleMailMessage">
<property name="from" value="customerservice@mycompany.example"/>
<property name="subject" value="Your order"/>
</bean>
<bean id="orderManager" class="com.mycompany.businessapp.support.SimpleOrderManager">
<property name="mailSender" ref="mailSender"/>
<property name="templateMessage" ref="templateMessage"/>
</bean>
----
[[mail-usage-mime]]
=== Using `JavaMailSender` and `MimeMessagePreparator`
This section describes another implementation of `OrderManager` that uses the `MimeMessagePreparator`
callback interface. In the following example, the `mailSender` property is of type
`JavaMailSender` so that we are able to use the JavaMail `MimeMessage` class:
[source,java,indent=0,subs="verbatim,quotes"]
----
import jakarta.mail.Message;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeMessage;
import jakarta.mail.internet.MimeMessage;
import org.springframework.mail.MailException;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessagePreparator;
public class SimpleOrderManager implements OrderManager {
private JavaMailSender mailSender;
public void setMailSender(JavaMailSender mailSender) {
this.mailSender = mailSender;
}
public void placeOrder(final Order order) {
// Do the business calculations...
// Call the collaborators to persist the order...
MimeMessagePreparator preparator = new MimeMessagePreparator() {
public void prepare(MimeMessage mimeMessage) throws Exception {
mimeMessage.setRecipient(Message.RecipientType.TO,
new InternetAddress(order.getCustomer().getEmailAddress()));
mimeMessage.setFrom(new InternetAddress("mail@mycompany.example"));
mimeMessage.setText("Dear " + order.getCustomer().getFirstName() + " " +
order.getCustomer().getLastName() + ", thanks for your order. " +
"Your order number is " + order.getOrderNumber() + ".");
}
};
try {
this.mailSender.send(preparator);
}
catch (MailException ex) {
// simply log it and go on...
System.err.println(ex.getMessage());
}
}
}
----
NOTE: The mail code is a crosscutting concern and could well be a candidate for
refactoring into a <<core.adoc#aop, custom Spring AOP aspect>>, which could then
be run at appropriate joinpoints on the `OrderManager` target.
The Spring Framework's mail support ships with the standard JavaMail implementation.
See the relevant javadoc for more information.
[[mail-javamail-mime]]
== Using the JavaMail `MimeMessageHelper`
A class that comes in pretty handy when dealing with JavaMail messages is
`org.springframework.mail.javamail.MimeMessageHelper`, which shields you from
having to use the verbose JavaMail API. Using the `MimeMessageHelper`, it is
pretty easy to create a `MimeMessage`, as the following example shows:
[source,java,indent=0,subs="verbatim,quotes"]
----
// of course you would use DI in any real-world cases
JavaMailSenderImpl sender = new JavaMailSenderImpl();
sender.setHost("mail.host.com");
MimeMessage message = sender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message);
helper.setTo("test@host.com");
helper.setText("Thank you for ordering!");
sender.send(message);
----
[[mail-javamail-mime-attachments]]
=== Sending Attachments and Inline Resources
Multipart email messages allow for both attachments and inline resources. Examples of
inline resources include an image or a stylesheet that you want to use in your message but
that you do not want displayed as an attachment.
[[mail-javamail-mime-attachments-attachment]]
==== Attachments
The following example shows you how to use the `MimeMessageHelper` to send an email
with a single JPEG image attachment:
[source,java,indent=0,subs="verbatim,quotes"]
----
JavaMailSenderImpl sender = new JavaMailSenderImpl();
sender.setHost("mail.host.com");
MimeMessage message = sender.createMimeMessage();
// use the true flag to indicate you need a multipart message
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setTo("test@host.com");
helper.setText("Check out this image!");
// let's attach the infamous windows Sample file (this time copied to c:/)
FileSystemResource file = new FileSystemResource(new File("c:/Sample.jpg"));
helper.addAttachment("CoolImage.jpg", file);
sender.send(message);
----
[[mail-javamail-mime-attachments-inline]]
==== Inline Resources
The following example shows you how to use the `MimeMessageHelper` to send an email
with an inline image:
[source,java,indent=0,subs="verbatim,quotes"]
----
JavaMailSenderImpl sender = new JavaMailSenderImpl();
sender.setHost("mail.host.com");
MimeMessage message = sender.createMimeMessage();
// use the true flag to indicate you need a multipart message
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setTo("test@host.com");
// use the true flag to indicate the text included is HTML
helper.setText("<html><body><img src='cid:identifier1234'></body></html>", true);
// let's include the infamous windows Sample file (this time copied to c:/)
FileSystemResource res = new FileSystemResource(new File("c:/Sample.jpg"));
helper.addInline("identifier1234", res);
sender.send(message);
----
WARNING: Inline resources are added to the `MimeMessage` by using the specified `Content-ID`
(`identifier1234` in the above example). The order in which you add the text
and the resource are very important. Be sure to first add the text and then
the resources. If you are doing it the other way around, it does not work.
[[mail-templates]]
=== Creating Email Content by Using a Templating Library
The code in the examples shown in the previous sections explicitly created the content of the email message,
by using methods calls such as `message.setText(..)`. This is fine for simple cases, and it
is okay in the context of the aforementioned examples, where the intent was to show you
the very basics of the API.
In your typical enterprise application, though, developers often do not create the content
of email messages by using the previously shown approach for a number of reasons:
* Creating HTML-based email content in Java code is tedious and error prone.
* There is no clear separation between display logic and business logic.
* Changing the display structure of the email content requires writing Java code,
recompiling, redeploying, and so on.
Typically, the approach taken to address these issues is to use a template library (such
as FreeMarker) to define the display structure of email content. This leaves your code
tasked only with creating the data that is to be rendered in the email template and
sending the email. It is definitely a best practice when the content of your email messages
becomes even moderately complex, and, with the Spring Framework's support classes for
FreeMarker, it becomes quite easy to do.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,193 +0,0 @@
[[integration.observability]]
= Observability Support
Micrometer defines an https://micrometer.io/docs/observation[Observation concept that enables both Metrics and Traces] in applications.
Metrics support offers a way to create timers, gauges or counters for collecting statistics about the runtime behavior of your application.
Metrics can help you to track error rates, usage patterns, performance and more.
Traces provide a holistic view of an entire system, crossing application boundaries; you can zoom in on particular user requests and follow their entire completion across applications.
Spring Framework instruments various parts of its own codebase to publish observations if an `ObservationRegistry` is configured.
You can learn more about {docs-spring-boot}/html/actuator.html#actuator.metrics[configuring the observability infrastructure in Spring Boot].
[[integration.observability.concepts]]
== Micrometer Observation concepts
If you are not familiar with Micrometer Observation, here's a quick summary of the new concepts you should know about.
* `Observation` is the actual recording of something happening in your application. This is processed by `ObservationHandler` implementations to produce metrics or traces.
* Each observation has a corresponding `ObservationContext` implementation; this type holds all the relevant information for extracting metadata for it.
In the case of an HTTP server observation, the context implementation could hold the HTTP request, the HTTP response, any Exception thrown during processing...
* Each `Observation` holds `KeyValues` metadata. In the case of a server HTTP observation, this could be the HTTP request method, the HTTP response status...
This metadata is contributed by `ObservationConvention` implementations which should declare the type of `ObservationContext` they support.
* `KeyValues` are said to be "low cardinality" if there is a low, bounded number of possible values for the `KeyValue` tuple (HTTP method is a good example).
Low cardinality values are contributed to metrics only.
High cardinality values are on the other hand unbounded (for example, HTTP request URIs) and are only contributed to Traces.
* An `ObservationDocumentation` documents all observations in a particular domain, listing the expected key names and their meaning.
[[integration.observability.config]]
== Configuring Observations
Global configuration options are available at the `ObservationRegistry#observationConfig()` level.
Each instrumented component will provide two extension points:
* setting the `ObservationRegistry`; if not set, observations will not be recorded and will be no-ops
* providing a custom `ObservationConvention` to change the default observation name and extracted `KeyValues`
[[integration.observability.config.conventions]]
=== Using custom Observation conventions
Let's take the example of the Spring MVC "http.server.requests" metrics instrumentation with the `ServerHttpObservationFilter`.
This observation is using a `ServerRequestObservationConvention` with a `ServerRequestObservationContext`; custom conventions can be configured on the Servlet filter.
If you would like to customize the metadata produced with the observation, you can extend the `DefaultServerRequestObservationConvention` for your requirements:
include::code:ExtendedServerRequestObservationConvention[]
If you want full control, you can then implement the entire convention contract for the observation you're interested in:
include::code:CustomServerRequestObservationConvention[]
You can also achieve similar goals using a custom `ObservationFilter` - adding or removing key values for an observation.
Filters do not replace the default convention and are used as a post-processing component.
include::code:ServerRequestObservationFilter[]
You can configure `ObservationFilter` instances on the `ObservationRegistry`.
[[integration.observability.http-server]]
== HTTP Server instrumentation
HTTP server exchanges observations are created with the name `"http.server.requests"` for Servlet and Reactive applications.
[[integration.observability.http-server.servlet]]
=== Servlet applications
Applications need to configure the `org.springframework.web.filter.ServerHttpObservationFilter` Servlet filter in their application.
It is using the `org.springframework.http.server.observation.DefaultServerRequestObservationConvention` by default, backed by the `ServerRequestObservationContext`.
This will only record an observation as an error if the `Exception` has not been handled by the web Framework and has bubbled up to the Servlet filter.
Typically, all exceptions handled by Spring MVC's `@ExceptionHandler` and <<web.adoc#mvc-ann-rest-exceptions,`ProblemDetail` support>> will not be recorded with the observation.
You can, at any point during request processing, set the error field on the `ObservationContext` yourself:
include::code:UserController[]
By default, the following `KeyValues` are created:
.Low cardinality Keys
[cols="a,a"]
|===
|Name | Description
|`exception` _(required)_|Name of the exception thrown during the exchange, or `KeyValue#NONE_VALUE`} if no exception happened.
|`method` _(required)_|Name of HTTP request method or `"none"` if the request was not received properly.
|`outcome` _(required)_|Outcome of the HTTP server exchange.
|`status` _(required)_|HTTP response raw status code, or `"UNKNOWN"` if no response was created.
|`uri` _(required)_|URI pattern for the matching handler if available, falling back to `REDIRECTION` for 3xx responses, `NOT_FOUND` for 404 responses, `root` for requests with no path info, and `UNKNOWN` for all other requests.
|===
.High cardinality Keys
[cols="a,a"]
|===
|Name | Description
|`http.url` _(required)_|HTTP request URI.
|===
[[integration.observability.http-server.reactive]]
=== Reactive applications
Applications need to configure the `org.springframework.web.filter.reactive.ServerHttpObservationFilter` reactive `WebFilter` in their application.
It is using the `org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention` by default, backed by the `ServerRequestObservationContext`.
This will only record an observation as an error if the `Exception` has not been handled by the web Framework and has bubbled up to the `WebFilter`.
Typically, all exceptions handled by Spring WebFlux's `@ExceptionHandler` and <<web.adoc#webflux-ann-rest-exceptions,`ProblemDetail` support>> will not be recorded with the observation.
You can, at any point during request processing, set the error field on the `ObservationContext` yourself:
include::code:UserController[]
By default, the following `KeyValues` are created:
.Low cardinality Keys
[cols="a,a"]
|===
|Name | Description
|`exception` _(required)_|Name of the exception thrown during the exchange, or `"none"` if no exception happened.
|`method` _(required)_|Name of HTTP request method or `"none"` if the request was not received properly.
|`outcome` _(required)_|Outcome of the HTTP server exchange.
|`status` _(required)_|HTTP response raw status code, or `"UNKNOWN"` if no response was created.
|`uri` _(required)_|URI pattern for the matching handler if available, falling back to `REDIRECTION` for 3xx responses, `NOT_FOUND` for 404 responses, `root` for requests with no path info, and `UNKNOWN` for all other requests.
|===
.High cardinality Keys
[cols="a,a"]
|===
|Name | Description
|`http.url` _(required)_|HTTP request URI.
|===
[[integration.observability.http-client]]
== HTTP Client instrumentation
HTTP client exchanges observations are created with the name `"http.client.requests"` for blocking and reactive clients.
Unlike their server counterparts, the instrumentation is implemented directly in the client so the only required step is to configure an `ObservationRegistry` on the client.
[[integration.observability.http-client.resttemplate]]
=== RestTemplate
Applications must configure an `ObservationRegistry` on `RestTemplate` instances to enable the instrumentation; without that, observations are "no-ops".
Spring Boot will auto-configure `RestTemplateBuilder` beans with the observation registry already set.
Instrumentation is using the `org.springframework.http.client.observation.ClientRequestObservationConvention` by default, backed by the `ClientRequestObservationContext`.
.Low cardinality Keys
[cols="a,a"]
|===
|Name | Description
|`method` _(required)_|Name of HTTP request method or `"none"` if the request could not be created.
|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided. Only the path part of the URI is considered.
|`client.name` _(required)_|Client name derived from the request URI host.
|`status` _(required)_|HTTP response raw status code, or `"IO_ERROR"` in case of `IOException`, or `"CLIENT_ERROR"` if no response was received.
|`outcome` _(required)_|Outcome of the HTTP client exchange.
|`exception` _(required)_|Name of the exception thrown during the exchange, or `"none"` if no exception happened.
|===
.High cardinality Keys
[cols="a,a"]
|===
|Name | Description
|`http.url` _(required)_|HTTP request URI.
|===
[[integration.observability.http-client.webclient]]
=== WebClient
Applications must configure an `ObservationRegistry` on the `WebClient` builder to enable the instrumentation; without that, observations are "no-ops".
Spring Boot will auto-configure `WebClient.Builder` beans with the observation registry already set.
Instrumentation is using the `org.springframework.web.reactive.function.client.ClientRequestObservationConvention` by default, backed by the `ClientRequestObservationContext`.
.Low cardinality Keys
[cols="a,a"]
|===
|Name | Description
|`method` _(required)_|Name of HTTP request method or `"none"` if the request could not be created.
|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided. Only the path part of the URI is considered.
|`client.name` _(required)_|Client name derived from the request URI host.
|`status` _(required)_|HTTP response raw status code, or `"IO_ERROR"` in case of `IOException`, or `"CLIENT_ERROR"` if no response was received.
|`outcome` _(required)_|Outcome of the HTTP client exchange.
|`exception` _(required)_|Name of the exception thrown during the exchange, or `"none"` if no exception happened.
|===
.High cardinality Keys
[cols="a,a"]
|===
|Name | Description
|`http.url` _(required)_|HTTP request URI.
|===
@@ -1,520 +0,0 @@
[[rest-client-access]]
= REST Clients
The Spring Framework provides the following choices for making calls to REST endpoints:
* <<rest-webclient>> - non-blocking, reactive client w fluent API.
* <<rest-resttemplate>> - synchronous client with template method API.
* <<rest-http-interface>> - annotated interface with generated, dynamic proxy implementation.
[[rest-webclient]]
== `WebClient`
`WebClient` is a non-blocking, reactive client to perform HTTP requests. It was
introduced in 5.0 and offers an alternative to the `RestTemplate`, with support for
synchronous, asynchronous, and streaming scenarios.
`WebClient` supports the following:
* Non-blocking I/O.
* Reactive Streams back pressure.
* High concurrency with fewer hardware resources.
* Functional-style, fluent API that takes advantage of Java 8 lambdas.
* Synchronous and asynchronous interactions.
* Streaming up to or streaming down from a server.
See <<web-reactive.adoc#webflux-client, WebClient>> for more details.
[[rest-resttemplate]]
== `RestTemplate`
The `RestTemplate` provides a higher level API over HTTP client libraries. It makes it
easy to invoke REST endpoints in a single line. It exposes the following groups of
overloaded methods:
NOTE: `RestTemplate` is in maintenance mode, with only requests for minor
changes and bugs to be accepted. Please, consider using the
<<web-reactive.adoc#webflux-client, WebClient>> instead.
[[rest-overview-of-resttemplate-methods-tbl]]
.RestTemplate methods
[cols="1,3"]
|===
| Method group | Description
| `getForObject`
| Retrieves a representation via GET.
| `getForEntity`
| Retrieves a `ResponseEntity` (that is, status, headers, and body) by using GET.
| `headForHeaders`
| Retrieves all headers for a resource by using HEAD.
| `postForLocation`
| Creates a new resource by using POST and returns the `Location` header from the response.
| `postForObject`
| Creates a new resource by using POST and returns the representation from the response.
| `postForEntity`
| Creates a new resource by using POST and returns the representation from the response.
| `put`
| Creates or updates a resource by using PUT.
| `patchForObject`
| Updates a resource by using PATCH and returns the representation from the response.
Note that the JDK `HttpURLConnection` does not support `PATCH`, but Apache
HttpComponents and others do.
| `delete`
| Deletes the resources at the specified URI by using DELETE.
| `optionsForAllow`
| Retrieves allowed HTTP methods for a resource by using ALLOW.
| `exchange`
| More generalized (and less opinionated) version of the preceding methods that provides extra
flexibility when needed. It accepts a `RequestEntity` (including HTTP method, URL, headers,
and body as input) and returns a `ResponseEntity`.
These methods allow the use of `ParameterizedTypeReference` instead of `Class` to specify
a response type with generics.
| `execute`
| The most generalized way to perform a request, with full control over request
preparation and response extraction through callback interfaces.
|===
[[rest-resttemplate-create]]
=== Initialization
The default constructor uses `java.net.HttpURLConnection` to perform requests. You can
switch to a different HTTP library with an implementation of `ClientHttpRequestFactory`.
There is built-in support for the following:
* Apache HttpComponents
* Netty
* OkHttp
For example, to switch to Apache HttpComponents, you can use the following:
[source,java,indent=0,subs="verbatim,quotes"]
----
RestTemplate template = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
----
Each `ClientHttpRequestFactory` exposes configuration options specific to the underlying
HTTP client library -- for example, for credentials, connection pooling, and other details.
TIP: Note that the `java.net` implementation for HTTP requests can raise an exception when
accessing the status of a response that represents an error (such as 401). If this is an
issue, switch to another HTTP client library.
NOTE: `RestTemplate` can be instrumented for observability, in order to produce metrics and traces.
See the <<integration.adoc#integration.observability.http-client.resttemplate,RestTemplate Observability support>> section.
[[rest-resttemplate-uri]]
==== URIs
Many of the `RestTemplate` methods accept a URI template and URI template variables,
either as a `String` variable argument, or as `Map<String,String>`.
The following example uses a `String` variable argument:
[source,java,indent=0,subs="verbatim,quotes"]
----
String result = restTemplate.getForObject(
"https://example.com/hotels/{hotel}/bookings/{booking}", String.class, "42", "21");
----
The following example uses a `Map<String, String>`:
[source,java,indent=0,subs="verbatim,quotes"]
----
Map<String, String> vars = Collections.singletonMap("hotel", "42");
String result = restTemplate.getForObject(
"https://example.com/hotels/{hotel}/rooms/{hotel}", String.class, vars);
----
Keep in mind URI templates are automatically encoded, as the following example shows:
[source,java,indent=0,subs="verbatim,quotes"]
----
restTemplate.getForObject("https://example.com/hotel list", String.class);
// Results in request to "https://example.com/hotel%20list"
----
You can use the `uriTemplateHandler` property of `RestTemplate` to customize how URIs
are encoded. Alternatively, you can prepare a `java.net.URI` and pass it into one of
the `RestTemplate` methods that accepts a `URI`.
For more details on working with and encoding URIs, see <<web.adoc#mvc-uri-building, URI Links>>.
[[rest-template-headers]]
==== Headers
You can use the `exchange()` methods to specify request headers, as the following example shows:
[source,java,indent=0,subs="verbatim,quotes"]
----
String uriTemplate = "https://example.com/hotels/{hotel}";
URI uri = UriComponentsBuilder.fromUriString(uriTemplate).build(42);
RequestEntity<Void> requestEntity = RequestEntity.get(uri)
.header("MyRequestHeader", "MyValue")
.build();
ResponseEntity<String> response = template.exchange(requestEntity, String.class);
String responseHeader = response.getHeaders().getFirst("MyResponseHeader");
String body = response.getBody();
----
You can obtain response headers through many `RestTemplate` method variants that return
`ResponseEntity`.
[[rest-template-body]]
=== Body
Objects passed into and returned from `RestTemplate` methods are converted to and from raw
content with the help of an `HttpMessageConverter`.
On a POST, an input object is serialized to the request body, as the following example shows:
----
URI location = template.postForLocation("https://example.com/people", person);
----
You need not explicitly set the Content-Type header of the request. In most cases,
you can find a compatible message converter based on the source `Object` type, and the chosen
message converter sets the content type accordingly. If necessary, you can use the
`exchange` methods to explicitly provide the `Content-Type` request header, and that, in
turn, influences what message converter is selected.
On a GET, the body of the response is deserialized to an output `Object`, as the following example shows:
----
Person person = restTemplate.getForObject("https://example.com/people/{id}", Person.class, 42);
----
The `Accept` header of the request does not need to be explicitly set. In most cases,
a compatible message converter can be found based on the expected response type, which
then helps to populate the `Accept` header. If necessary, you can use the `exchange`
methods to provide the `Accept` header explicitly.
By default, `RestTemplate` registers all built-in
<<rest-message-conversion, message converters>>, depending on classpath checks that help
to determine what optional conversion libraries are present. You can also set the message
converters to use explicitly.
[[rest-message-conversion]]
==== Message Conversion
[.small]#<<web-reactive.adoc#webflux-codecs, See equivalent in the Reactive stack>>#
The `spring-web` module contains the `HttpMessageConverter` contract for reading and
writing the body of HTTP requests and responses through `InputStream` and `OutputStream`.
`HttpMessageConverter` instances are used on the client side (for example, in the `RestTemplate`) and
on the server side (for example, in Spring MVC REST controllers).
Concrete implementations for the main media (MIME) types are provided in the framework
and are, by default, registered with the `RestTemplate` on the client side and with
`RequestMappingHandlerAdapter` on the server side (see
<<web.adoc#mvc-config-message-converters, Configuring Message Converters>>).
The implementations of `HttpMessageConverter` are described in the following sections.
For all converters, a default media type is used, but you can override it by setting the
`supportedMediaTypes` bean property. The following table describes each implementation:
[[rest-message-converters-tbl]]
.HttpMessageConverter Implementations
[cols="1,3"]
|===
| MessageConverter | Description
| `StringHttpMessageConverter`
| An `HttpMessageConverter` implementation that can read and write `String` instances from the HTTP
request and response. By default, this converter supports all text media types
(`text/{asterisk}`) and writes with a `Content-Type` of `text/plain`.
| `FormHttpMessageConverter`
| An `HttpMessageConverter` implementation that can read and write form data from the HTTP
request and response. By default, this converter reads and writes the
`application/x-www-form-urlencoded` media type. Form data is read from and written into a
`MultiValueMap<String, String>`. The converter can also write (but not read) multipart
data read from a `MultiValueMap<String, Object>`. By default, `multipart/form-data` is
supported. As of Spring Framework 5.2, additional multipart subtypes can be supported for
writing form data. Consult the javadoc for `FormHttpMessageConverter` for further details.
| `ByteArrayHttpMessageConverter`
| An `HttpMessageConverter` implementation that can read and write byte arrays from the
HTTP request and response. By default, this converter supports all media types (`{asterisk}/{asterisk}`)
and writes with a `Content-Type` of `application/octet-stream`. You can override this
by setting the `supportedMediaTypes` property and overriding `getContentType(byte[])`.
| `MarshallingHttpMessageConverter`
| An `HttpMessageConverter` implementation that can read and write XML by using Spring's
`Marshaller` and `Unmarshaller` abstractions from the `org.springframework.oxm` package.
This converter requires a `Marshaller` and `Unmarshaller` before it can be used. You can inject these
through constructor or bean properties. By default, this converter supports
`text/xml` and `application/xml`.
| `MappingJackson2HttpMessageConverter`
| An `HttpMessageConverter` implementation that can read and write JSON by using Jackson's
`ObjectMapper`. You can customize JSON mapping as needed through the use of Jackson's
provided annotations. When you need further control (for cases where custom JSON
serializers/deserializers need to be provided for specific types), you can inject a custom `ObjectMapper`
through the `ObjectMapper` property. By default, this
converter supports `application/json`.
| `MappingJackson2XmlHttpMessageConverter`
| An `HttpMessageConverter` implementation that can read and write XML by using
https://github.com/FasterXML/jackson-dataformat-xml[Jackson XML] extension's
`XmlMapper`. You can customize XML mapping as needed through the use of JAXB
or Jackson's provided annotations. When you need further control (for cases where custom XML
serializers/deserializers need to be provided for specific types), you can inject a custom `XmlMapper`
through the `ObjectMapper` property. By default, this
converter supports `application/xml`.
| `SourceHttpMessageConverter`
| An `HttpMessageConverter` implementation that can read and write
`javax.xml.transform.Source` from the HTTP request and response. Only `DOMSource`,
`SAXSource`, and `StreamSource` are supported. By default, this converter supports
`text/xml` and `application/xml`.
| `BufferedImageHttpMessageConverter`
| An `HttpMessageConverter` implementation that can read and write
`java.awt.image.BufferedImage` from the HTTP request and response. This converter reads
and writes the media type supported by the Java I/O API.
|===
[[rest-template-jsonview]]
=== Jackson JSON Views
You can specify a https://www.baeldung.com/jackson-json-view-annotation[Jackson JSON View]
to serialize only a subset of the object properties, as the following example shows:
[source,java,indent=0,subs="verbatim,quotes"]
----
MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23"));
value.setSerializationView(User.WithoutPasswordView.class);
RequestEntity<MappingJacksonValue> requestEntity =
RequestEntity.post(new URI("https://example.com/user")).body(value);
ResponseEntity<String> response = template.exchange(requestEntity, String.class);
----
[[rest-template-multipart]]
=== Multipart
To send multipart data, you need to provide a `MultiValueMap<String, Object>` whose values
may be an `Object` for part content, a `Resource` for a file part, or an `HttpEntity` for
part content with headers. For example:
[source,java,indent=0,subs="verbatim,quotes"]
----
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
parts.add("fieldPart", "fieldValue");
parts.add("filePart", new FileSystemResource("...logo.png"));
parts.add("jsonPart", new Person("Jason"));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_XML);
parts.add("xmlPart", new HttpEntity<>(myBean, headers));
----
In most cases, you do not have to specify the `Content-Type` for each part. The content
type is determined automatically based on the `HttpMessageConverter` chosen to serialize
it or, in the case of a `Resource` based on the file extension. If necessary, you can
explicitly provide the `MediaType` with an `HttpEntity` wrapper.
Once the `MultiValueMap` is ready, you can pass it to the `RestTemplate`, as show below:
[source,java,indent=0,subs="verbatim,quotes"]
----
MultiValueMap<String, Object> parts = ...;
template.postForObject("https://example.com/upload", parts, Void.class);
----
If the `MultiValueMap` contains at least one non-`String` value, the `Content-Type` is set
to `multipart/form-data` by the `FormHttpMessageConverter`. If the `MultiValueMap` has
`String` values the `Content-Type` is defaulted to `application/x-www-form-urlencoded`.
If necessary the `Content-Type` may also be set explicitly.
[[rest-http-interface]]
== HTTP Interface
The Spring Framework lets you define an HTTP service as a Java interface with annotated
methods for HTTP exchanges. You can then generate a proxy that implements this interface
and performs the exchanges. This helps to simplify HTTP remote access which often
involves a facade that wraps the details of using the underlying HTTP client.
One, declare an interface with `@HttpExchange` methods:
[source,java,indent=0,subs="verbatim,quotes"]
----
interface RepositoryService {
@GetExchange("/repos/{owner}/{repo}")
Repository getRepository(@PathVariable String owner, @PathVariable String repo);
// more HTTP exchange methods...
}
----
Two, create a proxy that will perform the declared HTTP exchanges:
[source,java,indent=0,subs="verbatim,quotes"]
----
WebClient client = WebClient.builder().baseUrl("https://api.github.com/").build();
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder(WebClientAdapter.forClient(client)).build();
RepositoryService service = factory.createClient(RepositoryService.class);
----
`@HttpExchange` is supported at the type level where it applies to all methods:
[source,java,indent=0,subs="verbatim,quotes"]
----
@HttpExchange(url = "/repos/{owner}/{repo}", accept = "application/vnd.github.v3+json")
interface RepositoryService {
@GetExchange
Repository getRepository(@PathVariable String owner, @PathVariable String repo);
@PatchExchange(contentType = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
void updateRepository(@PathVariable String owner, @PathVariable String repo,
@RequestParam String name, @RequestParam String description, @RequestParam String homepage);
}
----
[[rest-http-interface-method-parameters]]
=== Method Parameters
Annotated, HTTP exchange methods support flexible method signatures with the following
method parameters:
[cols="1,2", options="header"]
|===
| Method argument | Description
| `URI`
| Dynamically set the URL for the request, overriding the annotation's `url` attribute.
| `HttpMethod`
| Dynamically set the HTTP method for the request, overriding the annotation's `method` attribute
| `@RequestHeader`
| Add a request header or multiple headers. The argument may be a `Map<String, ?>` or
`MultiValueMap<String, ?>` with multiple headers, a `Collection<?>` of values, or an
individual value. Type conversion is supported for non-String values.
| `@PathVariable`
| Add a variable for expand a placeholder in the request URL. The argument may be a
`Map<String, ?>` with multiple variables, or an individual value. Type conversion
is supported for non-String values.
| `@RequestBody`
| Provide the body of the request either as an Object to be serialized, or a
Reactive Streams `Publisher` such as `Mono`, `Flux`, or any other async type supported
through the configured `ReactiveAdapterRegistry`.
| `@RequestParam`
| Add a request parameter or multiple parameters. The argument may be a `Map<String, ?>`
or `MultiValueMap<String, ?>` with multiple parameters, a `Collection<?>` of values, or
an individual value. Type conversion is supported for non-String values.
When `"content-type"` is set to `"application/x-www-form-urlencoded"`, request
parameters are encoded in the request body. Otherwise, they are added as URL query
parameters.
| `@RequestPart`
| Add a request part, which may be a String (form field), `Resource` (file part),
Object (entity to be encoded, e.g. as JSON), `HttpEntity` (part content and headers),
a Spring `Part`, or Reactive Streams `Publisher` of any of the above.
| `@CookieValue`
| Add a cookie or multiple cookies. The argument may be a `Map<String, ?>` or
`MultiValueMap<String, ?>` with multiple cookies, a `Collection<?>` of values, or an
individual value. Type conversion is supported for non-String values.
|===
[[rest-http-interface-return-values]]
=== Return Values
Annotated, HTTP exchange methods support the following return values:
[cols="1,2", options="header"]
|===
| Method return value | Description
| `void`, `Mono<Void>`
| Perform the given request, and release the response content, if any.
| `HttpHeaders`, `Mono<HttpHeaders>`
| Perform the given request, release the response content, if any, and return the
response headers.
| `<T>`, `Mono<T>`
| Perform the given request and decode the response content to the declared return type.
| `<T>`, `Flux<T>`
| Perform the given request and decode the response content to a stream of the declared
element type.
| `ResponseEntity<Void>`, `Mono<ResponseEntity<Void>>`
| Perform the given request, and release the response content, if any, and return a
`ResponseEntity` with the status and headers.
| `ResponseEntity<T>`, `Mono<ResponseEntity<T>>`
| Perform the given request, decode the response content to the declared return type, and
return a `ResponseEntity` with the status, headers, and the decoded body.
| `Mono<ResponseEntity<Flux<T>>`
| Perform the given request, decode the response content to a stream of the declared
element type, and return a `ResponseEntity` with the status, headers, and the decoded
response body stream.
|===
TIP: You can also use any other async or reactive types registered in the
`ReactiveAdapterRegistry`.
[[rest-http-interface-exceptions]]
=== Exception Handling
By default, `WebClient` raises `WebClientResponseException` for 4xx and 5xx HTTP status
codes. To customize this, you can register a response status handler that applies to all
responses performed through the client:
[source,java,indent=0,subs="verbatim,quotes"]
----
WebClient webClient = WebClient.builder()
.defaultStatusHandler(HttpStatusCode::isError, resp -> ...)
.build();
WebClientAdapter clientAdapter = WebClientAdapter.forClient(webClient);
HttpServiceProxyFactory factory = HttpServiceProxyFactory
.builder(clientAdapter).build();
----
For more details and options, such as suppressing error status codes, see the Javadoc of
`defaultStatusHandler` in `WebClient.Builder`.
@@ -1,970 +0,0 @@
[[scheduling]]
= Task Execution and Scheduling
The Spring Framework provides abstractions for the asynchronous execution and scheduling of
tasks with the `TaskExecutor` and `TaskScheduler` interfaces, respectively. Spring also
features implementations of those interfaces that support thread pools or delegation to
CommonJ within an application server environment. Ultimately, the use of these
implementations behind the common interfaces abstracts away the differences between Java
SE 5, Java SE 6, and Jakarta EE environments.
Spring also features integration classes to support scheduling with the `Timer`
(part of the JDK since 1.3) and the https://www.quartz-scheduler.org/[Quartz Scheduler].
You can set up both of those schedulers by using a `FactoryBean` with optional references to
`Timer` or `Trigger` instances, respectively. Furthermore, a convenience class for both
the Quartz Scheduler and the `Timer` is available that lets you invoke a method of
an existing target object (analogous to the normal `MethodInvokingFactoryBean`
operation).
[[scheduling-task-executor]]
== The Spring `TaskExecutor` Abstraction
Executors are the JDK name for the concept of thread pools. The "`executor`" naming is
due to the fact that there is no guarantee that the underlying implementation is
actually a pool. An executor may be single-threaded or even synchronous. Spring's
abstraction hides implementation details between the Java SE and Jakarta EE environments.
Spring's `TaskExecutor` interface is identical to the `java.util.concurrent.Executor`
interface. In fact, originally, its primary reason for existence was to abstract away
the need for Java 5 when using thread pools. The interface has a single method
(`execute(Runnable task)`) that accepts a task for execution based on the semantics
and configuration of the thread pool.
The `TaskExecutor` was originally created to give other Spring components an abstraction
for thread pooling where needed. Components such as the `ApplicationEventMulticaster`,
JMS's `AbstractMessageListenerContainer`, and Quartz integration all use the
`TaskExecutor` abstraction to pool threads. However, if your beans need thread pooling
behavior, you can also use this abstraction for your own needs.
[[scheduling-task-executor-types]]
=== `TaskExecutor` Types
Spring includes a number of pre-built implementations of `TaskExecutor`.
In all likelihood, you should never need to implement your own.
The variants that Spring provides are as follows:
* `SyncTaskExecutor`:
This implementation does not run invocations asynchronously. Instead, each
invocation takes place in the calling thread. It is primarily used in situations
where multi-threading is not necessary, such as in simple test cases.
* `SimpleAsyncTaskExecutor`:
This implementation does not reuse any threads. Rather, it starts up a new thread
for each invocation. However, it does support a concurrency limit that blocks
any invocations that are over the limit until a slot has been freed up. If you
are looking for true pooling, see `ThreadPoolTaskExecutor`, later in this list.
* `ConcurrentTaskExecutor`:
This implementation is an adapter for a `java.util.concurrent.Executor` instance.
There is an alternative (`ThreadPoolTaskExecutor`) that exposes the `Executor`
configuration parameters as bean properties. There is rarely a need to use
`ConcurrentTaskExecutor` directly. However, if the `ThreadPoolTaskExecutor` is not
flexible enough for your needs, `ConcurrentTaskExecutor` is an alternative.
* `ThreadPoolTaskExecutor`:
This implementation is most commonly used. It exposes bean properties for
configuring a `java.util.concurrent.ThreadPoolExecutor` and wraps it in a `TaskExecutor`.
If you need to adapt to a different kind of `java.util.concurrent.Executor`, we
recommend that you use a `ConcurrentTaskExecutor` instead.
* `DefaultManagedTaskExecutor`:
This implementation uses a JNDI-obtained `ManagedExecutorService` in a JSR-236
compatible runtime environment (such as a Jakarta EE application server),
replacing a CommonJ WorkManager for that purpose.
[[scheduling-task-executor-usage]]
=== Using a `TaskExecutor`
Spring's `TaskExecutor` implementations are used as simple JavaBeans. In the following example,
we define a bean that uses the `ThreadPoolTaskExecutor` to asynchronously print
out a set of messages:
[source,java,indent=0,subs="verbatim,quotes"]
----
import org.springframework.core.task.TaskExecutor;
public class TaskExecutorExample {
private class MessagePrinterTask implements Runnable {
private String message;
public MessagePrinterTask(String message) {
this.message = message;
}
public void run() {
System.out.println(message);
}
}
private TaskExecutor taskExecutor;
public TaskExecutorExample(TaskExecutor taskExecutor) {
this.taskExecutor = taskExecutor;
}
public void printMessages() {
for(int i = 0; i < 25; i++) {
taskExecutor.execute(new MessagePrinterTask("Message" + i));
}
}
}
----
As you can see, rather than retrieving a thread from the pool and executing it yourself,
you add your `Runnable` to the queue. Then the `TaskExecutor` uses its internal rules to
decide when the task gets run.
To configure the rules that the `TaskExecutor` uses, we expose simple bean properties:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<property name="corePoolSize" value="5"/>
<property name="maxPoolSize" value="10"/>
<property name="queueCapacity" value="25"/>
</bean>
<bean id="taskExecutorExample" class="TaskExecutorExample">
<constructor-arg ref="taskExecutor"/>
</bean>
----
[[scheduling-task-scheduler]]
== The Spring `TaskScheduler` Abstraction
In addition to the `TaskExecutor` abstraction, Spring has a `TaskScheduler` SPI with a
variety of methods for scheduling tasks to run at some point in the future. The following
listing shows the `TaskScheduler` interface definition:
[source,java,indent=0,subs="verbatim,quotes"]
----
public interface TaskScheduler {
Clock getClock();
ScheduledFuture schedule(Runnable task, Trigger trigger);
ScheduledFuture schedule(Runnable task, Instant startTime);
ScheduledFuture scheduleAtFixedRate(Runnable task, Instant startTime, Duration period);
ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period);
ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay);
ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay);
----
The simplest method is the one named `schedule` that takes only a `Runnable` and an `Instant`.
That causes the task to run once after the specified time. All of the other methods
are capable of scheduling tasks to run repeatedly. The fixed-rate and fixed-delay
methods are for simple, periodic execution, but the method that accepts a `Trigger` is
much more flexible.
[[scheduling-trigger-interface]]
=== `Trigger` Interface
The `Trigger` interface is essentially inspired by JSR-236. The basic idea of the
`Trigger` is that execution times may be determined based on past execution outcomes or
even arbitrary conditions. If these determinations take into account the outcome of the
preceding execution, that information is available within a `TriggerContext`. The
`Trigger` interface itself is quite simple, as the following listing shows:
[source,java,indent=0,subs="verbatim,quotes"]
----
public interface Trigger {
Instant nextExecution(TriggerContext triggerContext);
}
----
The `TriggerContext` is the most important part. It encapsulates all of
the relevant data and is open for extension in the future, if necessary. The
`TriggerContext` is an interface (a `SimpleTriggerContext` implementation is used by
default). The following listing shows the available methods for `Trigger` implementations.
[source,java,indent=0,subs="verbatim,quotes"]
----
public interface TriggerContext {
Clock getClock();
Instant lastScheduledExecution();
Instant lastActualExecution();
Instant lastCompletion();
}
----
[[scheduling-trigger-implementations]]
=== `Trigger` Implementations
Spring provides two implementations of the `Trigger` interface. The most interesting one
is the `CronTrigger`. It enables the scheduling of tasks based on
<<scheduling-cron-expression,cron expressions>>.
For example, the following task is scheduled to run 15 minutes past each hour but only
during the 9-to-5 "business hours" on weekdays:
[source,java,indent=0]
[subs="verbatim"]
----
scheduler.schedule(task, new CronTrigger("0 15 9-17 * * MON-FRI"));
----
The other implementation is a `PeriodicTrigger` that accepts a fixed
period, an optional initial delay value, and a boolean to indicate whether the period
should be interpreted as a fixed-rate or a fixed-delay. Since the `TaskScheduler`
interface already defines methods for scheduling tasks at a fixed rate or with a
fixed delay, those methods should be used directly whenever possible. The value of the
`PeriodicTrigger` implementation is that you can use it within components that rely on
the `Trigger` abstraction. For example, it may be convenient to allow periodic triggers,
cron-based triggers, and even custom trigger implementations to be used interchangeably.
Such a component could take advantage of dependency injection so that you can configure such `Triggers`
externally and, therefore, easily modify or extend them.
[[scheduling-task-scheduler-implementations]]
=== `TaskScheduler` implementations
As with Spring's `TaskExecutor` abstraction, the primary benefit of the `TaskScheduler`
arrangement is that an application's scheduling needs are decoupled from the deployment
environment. This abstraction level is particularly relevant when deploying to an
application server environment where threads should not be created directly by the
application itself. For such scenarios, Spring provides a `TimerManagerTaskScheduler`
that delegates to a CommonJ `TimerManager` on WebLogic or WebSphere as well as a more recent
`DefaultManagedTaskScheduler` that delegates to a JSR-236 `ManagedScheduledExecutorService`
in a Jakarta EE environment. Both are typically configured with a JNDI lookup.
Whenever external thread management is not a requirement, a simpler alternative is
a local `ScheduledExecutorService` setup within the application, which can be adapted
through Spring's `ConcurrentTaskScheduler`. As a convenience, Spring also provides a
`ThreadPoolTaskScheduler`, which internally delegates to a `ScheduledExecutorService`
to provide common bean-style configuration along the lines of `ThreadPoolTaskExecutor`.
These variants work perfectly fine for locally embedded thread pool setups in lenient
application server environments, as well -- in particular on Tomcat and Jetty.
[[scheduling-annotation-support]]
== Annotation Support for Scheduling and Asynchronous Execution
Spring provides annotation support for both task scheduling and asynchronous method
execution.
[[scheduling-enable-annotation-support]]
=== Enable Scheduling Annotations
To enable support for `@Scheduled` and `@Async` annotations, you can add `@EnableScheduling` and
`@EnableAsync` to one of your `@Configuration` classes, as the following example shows:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableAsync
@EnableScheduling
public class AppConfig {
}
----
You can pick and choose the relevant annotations for your application. For example,
if you need only support for `@Scheduled`, you can omit `@EnableAsync`. For more
fine-grained control, you can additionally implement the `SchedulingConfigurer`
interface, the `AsyncConfigurer` interface, or both. See the
{api-spring-framework}/scheduling/annotation/SchedulingConfigurer.html[`SchedulingConfigurer`]
and {api-spring-framework}/scheduling/annotation/AsyncConfigurer.html[`AsyncConfigurer`]
javadoc for full details.
If you prefer XML configuration, you can use the `<task:annotation-driven>` element,
as the following example shows:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<task:annotation-driven executor="myExecutor" scheduler="myScheduler"/>
<task:executor id="myExecutor" pool-size="5"/>
<task:scheduler id="myScheduler" pool-size="10"/>
----
Note that, with the preceding XML, an executor reference is provided for handling those
tasks that correspond to methods with the `@Async` annotation, and the scheduler
reference is provided for managing those methods annotated with `@Scheduled`.
NOTE: The default advice mode for processing `@Async` annotations is `proxy` which allows
for interception of calls through the proxy only. Local calls within the same class
cannot get intercepted that way. For a more advanced mode of interception, consider
switching to `aspectj` mode in combination with compile-time or load-time weaving.
[[scheduling-annotation-support-scheduled]]
=== The `@Scheduled` annotation
You can add the `@Scheduled` annotation to a method, along with trigger metadata. For
example, the following method is invoked every five seconds (5000 milliseconds) with a
fixed delay, meaning that the period is measured from the completion time of each
preceding invocation.
[source,java,indent=0,subs="verbatim,quotes"]
----
@Scheduled(fixedDelay = 5000)
public void doSomething() {
// something that should run periodically
}
----
[NOTE]
====
By default, milliseconds will be used as the time unit for fixed delay, fixed rate, and
initial delay values. If you would like to use a different time unit such as seconds or
minutes, you can configure this via the `timeUnit` attribute in `@Scheduled`.
For example, the previous example can also be written as follows.
[source,java,indent=0,subs="verbatim,quotes"]
----
@Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS)
public void doSomething() {
// something that should run periodically
}
----
====
If you need a fixed-rate execution, you can use the `fixedRate` attribute within the
annotation. The following method is invoked every five seconds (measured between the
successive start times of each invocation).
[source,java,indent=0,subs="verbatim,quotes"]
----
@Scheduled(fixedRate = 5, timeUnit = TimeUnit.SECONDS)
public void doSomething() {
// something that should run periodically
}
----
For fixed-delay and fixed-rate tasks, you can specify an initial delay by indicating the
amount of time to wait before the first execution of the method, as the following
`fixedRate` example shows.
[source,java,indent=0,subs="verbatim,quotes"]
----
@Scheduled(initialDelay = 1000, fixedRate = 5000)
public void doSomething() {
// something that should run periodically
}
----
If simple periodic scheduling is not expressive enough, you can provide a
<<scheduling-cron-expression,cron expression>>.
The following example runs only on weekdays:
[source,java,indent=0]
[subs="verbatim"]
----
@Scheduled(cron="*/5 * * * * MON-FRI")
public void doSomething() {
// something that should run on weekdays only
}
----
TIP: You can also use the `zone` attribute to specify the time zone in which the cron
expression is resolved.
Notice that the methods to be scheduled must have void returns and must not accept any
arguments. If the method needs to interact with other objects from the application
context, those would typically have been provided through dependency injection.
[NOTE]
====
As of Spring Framework 4.3, `@Scheduled` methods are supported on beans of any scope.
Make sure that you are not initializing multiple instances of the same `@Scheduled`
annotation class at runtime, unless you do want to schedule callbacks to each such
instance. Related to this, make sure that you do not use `@Configurable` on bean
classes that are annotated with `@Scheduled` and registered as regular Spring beans
with the container. Otherwise, you would get double initialization (once through the
container and once through the `@Configurable` aspect), with the consequence of each
`@Scheduled` method being invoked twice.
====
[[scheduling-annotation-support-async]]
=== The `@Async` annotation
You can provide the `@Async` annotation on a method so that invocation of that method
occurs asynchronously. In other words, the caller returns immediately upon
invocation, while the actual execution of the method occurs in a task that has been
submitted to a Spring `TaskExecutor`. In the simplest case, you can apply the annotation
to a method that returns `void`, as the following example shows:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Async
void doSomething() {
// this will be run asynchronously
}
----
Unlike the methods annotated with the `@Scheduled` annotation, these methods can expect
arguments, because they are invoked in the "`normal`" way by callers at runtime rather
than from a scheduled task being managed by the container. For example, the following code is
a legitimate application of the `@Async` annotation:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Async
void doSomething(String s) {
// this will be run asynchronously
}
----
Even methods that return a value can be invoked asynchronously. However, such methods
are required to have a `Future`-typed return value. This still provides the benefit of
asynchronous execution so that the caller can perform other tasks prior to calling
`get()` on that `Future`. The following example shows how to use `@Async` on a method
that returns a value:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Async
Future<String> returnSomething(int i) {
// this will be run asynchronously
}
----
TIP: `@Async` methods may not only declare a regular `java.util.concurrent.Future` return type
but also Spring's `org.springframework.util.concurrent.ListenableFuture` or, as of Spring
4.2, JDK 8's `java.util.concurrent.CompletableFuture`, for richer interaction with the
asynchronous task and for immediate composition with further processing steps.
You can not use `@Async` in conjunction with lifecycle callbacks such as
`@PostConstruct`. To asynchronously initialize Spring beans, you currently have to use
a separate initializing Spring bean that then invokes the `@Async` annotated method on the
target, as the following example shows:
[source,java,indent=0,subs="verbatim,quotes"]
----
public class SampleBeanImpl implements SampleBean {
@Async
void doSomething() {
// ...
}
}
public class SampleBeanInitializer {
private final SampleBean bean;
public SampleBeanInitializer(SampleBean bean) {
this.bean = bean;
}
@PostConstruct
public void initialize() {
bean.doSomething();
}
}
----
NOTE: There is no direct XML equivalent for `@Async`, since such methods should be designed
for asynchronous execution in the first place, not externally re-declared to be asynchronous.
However, you can manually set up Spring's `AsyncExecutionInterceptor` with Spring AOP,
in combination with a custom pointcut.
[[scheduling-annotation-support-qualification]]
=== Executor Qualification with `@Async`
By default, when specifying `@Async` on a method, the executor that is used is the
one <<scheduling-enable-annotation-support, configured when enabling async support>>,
i.e. the "`annotation-driven`" element if you are using XML or your `AsyncConfigurer`
implementation, if any. However, you can use the `value` attribute of the `@Async`
annotation when you need to indicate that an executor other than the default should be
used when executing a given method. The following example shows how to do so:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Async("otherExecutor")
void doSomething(String s) {
// this will be run asynchronously by "otherExecutor"
}
----
In this case, `"otherExecutor"` can be the name of any `Executor` bean in the Spring
container, or it may be the name of a qualifier associated with any `Executor` (for example, as
specified with the `<qualifier>` element or Spring's `@Qualifier` annotation).
[[scheduling-annotation-support-exception]]
=== Exception Management with `@Async`
When an `@Async` method has a `Future`-typed return value, it is easy to manage
an exception that was thrown during the method execution, as this exception is
thrown when calling `get` on the `Future` result. With a `void` return type,
however, the exception is uncaught and cannot be transmitted. You can provide an
`AsyncUncaughtExceptionHandler` to handle such exceptions. The following example shows
how to do so:
[source,java,indent=0,subs="verbatim,quotes"]
----
public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
// handle exception
}
}
----
By default, the exception is merely logged. You can define a custom `AsyncUncaughtExceptionHandler`
by using `AsyncConfigurer` or the `<task:annotation-driven/>` XML element.
[[scheduling-task-namespace]]
== The `task` Namespace
As of version 3.0, Spring includes an XML namespace for configuring `TaskExecutor` and
`TaskScheduler` instances. It also provides a convenient way to configure tasks to be
scheduled with a trigger.
[[scheduling-task-namespace-scheduler]]
=== The 'scheduler' Element
The following element creates a `ThreadPoolTaskScheduler` instance with the
specified thread pool size:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<task:scheduler id="scheduler" pool-size="10"/>
----
The value provided for the `id` attribute is used as the prefix for thread names
within the pool. The `scheduler` element is relatively straightforward. If you do not
provide a `pool-size` attribute, the default thread pool has only a single thread.
There are no other configuration options for the scheduler.
[[scheduling-task-namespace-executor]]
=== The `executor` Element
The following creates a `ThreadPoolTaskExecutor` instance:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<task:executor id="executor" pool-size="10"/>
----
As with the scheduler shown in the <<scheduling-task-namespace-scheduler, previous section>>,
the value provided for the `id` attribute is used as the prefix for thread names within
the pool. As far as the pool size is concerned, the `executor` element supports more
configuration options than the `scheduler` element. For one thing, the thread pool for
a `ThreadPoolTaskExecutor` is itself more configurable. Rather than only a single size,
an executor's thread pool can have different values for the core and the max size.
If you provide a single value, the executor has a fixed-size thread pool (the core and
max sizes are the same). However, the `executor` element's `pool-size` attribute also
accepts a range in the form of `min-max`. The following example sets a minimum value of
`5` and a maximum value of `25`:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<task:executor
id="executorWithPoolSizeRange"
pool-size="5-25"
queue-capacity="100"/>
----
In the preceding configuration, a `queue-capacity` value has also been provided.
The configuration of the thread pool should also be considered in light of the
executor's queue capacity. For the full description of the relationship between pool
size and queue capacity, see the documentation for
https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ThreadPoolExecutor.html[`ThreadPoolExecutor`].
The main idea is that, when a task is submitted, the executor first tries to use a
free thread if the number of active threads is currently less than the core size.
If the core size has been reached, the task is added to the queue, as long as its
capacity has not yet been reached. Only then, if the queue's capacity has been
reached, does the executor create a new thread beyond the core size. If the max size
has also been reached, then the executor rejects the task.
By default, the queue is unbounded, but this is rarely the desired configuration,
because it can lead to `OutOfMemoryErrors` if enough tasks are added to that queue while
all pool threads are busy. Furthermore, if the queue is unbounded, the max size has
no effect at all. Since the executor always tries the queue before creating a new
thread beyond the core size, a queue must have a finite capacity for the thread pool to
grow beyond the core size (this is why a fixed-size pool is the only sensible case
when using an unbounded queue).
Consider the case, as mentioned above, when a task is rejected. By default, when a
task is rejected, a thread pool executor throws a `TaskRejectedException`. However,
the rejection policy is actually configurable. The exception is thrown when using
the default rejection policy, which is the `AbortPolicy` implementation.
For applications where some tasks can be skipped under heavy load, you can instead
configure either `DiscardPolicy` or `DiscardOldestPolicy`. Another option that works
well for applications that need to throttle the submitted tasks under heavy load is
the `CallerRunsPolicy`. Instead of throwing an exception or discarding tasks,
that policy forces the thread that is calling the submit method to run the task itself.
The idea is that such a caller is busy while running that task and not able to submit
other tasks immediately. Therefore, it provides a simple way to throttle the incoming
load while maintaining the limits of the thread pool and queue. Typically, this allows
the executor to "`catch up`" on the tasks it is handling and thereby frees up some
capacity on the queue, in the pool, or both. You can choose any of these options from an
enumeration of values available for the `rejection-policy` attribute on the `executor`
element.
The following example shows an `executor` element with a number of attributes to specify
various behaviors:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<task:executor
id="executorWithCallerRunsPolicy"
pool-size="5-25"
queue-capacity="100"
rejection-policy="CALLER_RUNS"/>
----
Finally, the `keep-alive` setting determines the time limit (in seconds) for which threads
may remain idle before being stopped. If there are more than the core number of threads
currently in the pool, after waiting this amount of time without processing a task, excess
threads get stopped. A time value of zero causes excess threads to stop
immediately after executing a task without remaining follow-up work in the task queue.
The following example sets the `keep-alive` value to two minutes:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<task:executor
id="executorWithKeepAlive"
pool-size="5-25"
keep-alive="120"/>
----
[[scheduling-task-namespace-scheduled-tasks]]
=== The 'scheduled-tasks' Element
The most powerful feature of Spring's task namespace is the support for configuring
tasks to be scheduled within a Spring Application Context. This follows an approach
similar to other "`method-invokers`" in Spring, such as that provided by the JMS namespace
for configuring message-driven POJOs. Basically, a `ref` attribute can point to any
Spring-managed object, and the `method` attribute provides the name of a method to be
invoked on that object. The following listing shows a simple example:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<task:scheduled-tasks scheduler="myScheduler">
<task:scheduled ref="beanA" method="methodA" fixed-delay="5000"/>
</task:scheduled-tasks>
<task:scheduler id="myScheduler" pool-size="10"/>
----
The scheduler is referenced by the outer element, and each individual
task includes the configuration of its trigger metadata. In the preceding example, that
metadata defines a periodic trigger with a fixed delay indicating the number of
milliseconds to wait after each task execution has completed. Another option is
`fixed-rate`, indicating how often the method should be run regardless of how long
any previous execution takes. Additionally, for both `fixed-delay` and `fixed-rate` tasks, you can specify an
'initial-delay' parameter, indicating the number of milliseconds to wait
before the first execution of the method. For more control, you can instead provide a `cron` attribute
to provide a <<scheduling-cron-expression,cron expression>>.
The following example shows these other options:
[source,xml,indent=0]
[subs="verbatim"]
----
<task:scheduled-tasks scheduler="myScheduler">
<task:scheduled ref="beanA" method="methodA" fixed-delay="5000" initial-delay="1000"/>
<task:scheduled ref="beanB" method="methodB" fixed-rate="5000"/>
<task:scheduled ref="beanC" method="methodC" cron="*/5 * * * * MON-FRI"/>
</task:scheduled-tasks>
<task:scheduler id="myScheduler" pool-size="10"/>
----
[[scheduling-cron-expression]]
== Cron Expressions
All Spring cron expressions have to conform to the same format, whether you are using them in
<<scheduling-annotation-support-scheduled,`@Scheduled` annotations>>,
<<scheduling-task-namespace-scheduled-tasks,`task:scheduled-tasks` elements>>,
or someplace else.
A well-formed cron expression, such as `* * * * * *`, consists of six space-separated time and date
fields, each with its own range of valid values:
....
┌───────────── second (0-59)
│ ┌───────────── minute (0 - 59)
│ │ ┌───────────── hour (0 - 23)
│ │ │ ┌───────────── day of the month (1 - 31)
│ │ │ │ ┌───────────── month (1 - 12) (or JAN-DEC)
│ │ │ │ │ ┌───────────── day of the week (0 - 7)
│ │ │ │ │ │ (0 or 7 is Sunday, or MON-SUN)
│ │ │ │ │ │
* * * * * *
....
There are some rules that apply:
* A field may be an asterisk (`*`), which always stands for "`first-last`".
For the day-of-the-month or day-of-the-week fields, a question mark (`?`) may be used instead of an
asterisk.
* Commas (`,`) are used to separate items of a list.
* Two numbers separated with a hyphen (`-`) express a range of numbers.
The specified range is inclusive.
* Following a range (or `*`) with `/` specifies the interval of the number's value through the range.
* English names can also be used for the month and day-of-week fields.
Use the first three letters of the particular day or month (case does not matter).
* The day-of-month and day-of-week fields can contain an `L` character, which has a different meaning.
** In the day-of-month field, `L` stands for _the last day of the month_.
If followed by a negative offset (that is, `L-n`), it means _``n``th-to-last day of the month_.
** In the day-of-week field, `L` stands for _the last day of the week_.
If prefixed by a number or three-letter name (`dL` or `DDDL`), it means _the last day of week (`d`
or `DDD`) in the month_.
* The day-of-month field can be `nW`, which stands for _the nearest weekday to day of the month ``n``_.
If `n` falls on Saturday, this yields the Friday before it.
If `n` falls on Sunday, this yields the Monday after, which also happens if `n` is `1` and falls on
a Saturday (that is: `1W` stands for _the first weekday of the month_).
* If the day-of-month field is `LW`, it means _the last weekday of the month_.
* The day-of-week field can be `d#n` (or `DDD#n`), which stands for _the ``n``th day of week `d`
(or ``DDD``) in the month_.
Here are some examples:
|===
| Cron Expression | Meaning
|`0 0 * * * *` | top of every hour of every day
|`*/10 * * * * *` | every ten seconds
| `0 0 8-10 * * *` | 8, 9 and 10 o'clock of every day
| `0 0 6,19 * * *` | 6:00 AM and 7:00 PM every day
| `0 0/30 8-10 * * *` | 8:00, 8:30, 9:00, 9:30, 10:00 and 10:30 every day
| `0 0 9-17 * * MON-FRI`| on the hour nine-to-five weekdays
| `0 0 0 25 DEC ?` | every Christmas Day at midnight
| `0 0 0 L * *` | last day of the month at midnight
| `0 0 0 L-3 * *` | third-to-last day of the month at midnight
| `0 0 0 * * 5L` | last Friday of the month at midnight
| `0 0 0 * * THUL` | last Thursday of the month at midnight
| `0 0 0 1W * *` | first weekday of the month at midnight
| `0 0 0 LW * *` | last weekday of the month at midnight
| `0 0 0 ? * 5#2` | the second Friday in the month at midnight
| `0 0 0 ? * MON#1` | the first Monday in the month at midnight
|===
=== Macros
Expressions such as `0 0 * * * *` are hard for humans to parse and are, therefore, hard to fix in case of bugs.
To improve readability, Spring supports the following macros, which represent commonly used sequences.
You can use these macros instead of the six-digit value, thus: `@Scheduled(cron = "@hourly")`.
|===
|Macro | Meaning
| `@yearly` (or `@annually`) | once a year (`0 0 0 1 1 *`)
| `@monthly` | once a month (`0 0 0 1 * *`)
| `@weekly` | once a week (`0 0 0 * * 0`)
| `@daily` (or `@midnight`) | once a day (`0 0 0 * * *`), or
| `@hourly` | once an hour, (`0 0 * * * *`)
|===
[[scheduling-quartz]]
== Using the Quartz Scheduler
Quartz uses `Trigger`, `Job`, and `JobDetail` objects to realize scheduling of all kinds
of jobs. For the basic concepts behind Quartz, see the
https://www.quartz-scheduler.org/[Quartz Web site]. For convenience purposes, Spring
offers a couple of classes that simplify using Quartz within Spring-based applications.
[[scheduling-quartz-jobdetail]]
=== Using the `JobDetailFactoryBean`
Quartz `JobDetail` objects contain all the information needed to run a job. Spring provides a
`JobDetailFactoryBean`, which provides bean-style properties for XML configuration purposes.
Consider the following example:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<bean name="exampleJob" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
<property name="jobClass" value="example.ExampleJob"/>
<property name="jobDataAsMap">
<map>
<entry key="timeout" value="5"/>
</map>
</property>
</bean>
----
The job detail configuration has all the information it needs to run the job (`ExampleJob`).
The timeout is specified in the job data map. The job data map is available through the
`JobExecutionContext` (passed to you at execution time), but the `JobDetail` also gets
its properties from the job data mapped to properties of the job instance. So, in the following example,
the `ExampleJob` contains a bean property named `timeout`, and the `JobDetail`
has it applied automatically:
[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"]
----
package example;
public class ExampleJob extends QuartzJobBean {
private int timeout;
/**
* Setter called after the ExampleJob is instantiated
* with the value from the JobDetailFactoryBean.
*/
public void setTimeout(int timeout) {
this.timeout = timeout;
}
protected void executeInternal(JobExecutionContext ctx) throws JobExecutionException {
// do the actual work
}
}
----
All additional properties from the job data map are available to you as well.
NOTE: By using the `name` and `group` properties, you can modify the name and the group
of the job, respectively. By default, the name of the job matches the bean name
of the `JobDetailFactoryBean` (`exampleJob` in the preceding example above).
[[scheduling-quartz-method-invoking-job]]
=== Using the `MethodInvokingJobDetailFactoryBean`
Often you merely need to invoke a method on a specific object. By using the
`MethodInvokingJobDetailFactoryBean`, you can do exactly this, as the following example shows:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="exampleBusinessObject"/>
<property name="targetMethod" value="doIt"/>
</bean>
----
The preceding example results in the `doIt` method being called on the
`exampleBusinessObject` method, as the following example shows:
[source,java,indent=0,subs="verbatim,quotes"]
----
public class ExampleBusinessObject {
// properties and collaborators
public void doIt() {
// do the actual work
}
}
----
[source,xml,indent=0,subs="verbatim,quotes"]
----
<bean id="exampleBusinessObject" class="examples.ExampleBusinessObject"/>
----
By using the `MethodInvokingJobDetailFactoryBean`, you need not create one-line jobs
that merely invoke a method. You need only create the actual business object and
wire up the detail object.
By default, Quartz Jobs are stateless, resulting in the possibility of jobs interfering
with each other. If you specify two triggers for the same `JobDetail`, it is possible
that the second one starts before the first job has finished. If `JobDetail` classes
implement the `Stateful` interface, this does not happen: the second job does not start
before the first one has finished.
To make jobs resulting from the `MethodInvokingJobDetailFactoryBean` be non-concurrent,
set the `concurrent` flag to `false`, as the following example shows:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="exampleBusinessObject"/>
<property name="targetMethod" value="doIt"/>
<property name="concurrent" value="false"/>
</bean>
----
NOTE: By default, jobs will run in a concurrent fashion.
[[scheduling-quartz-cron]]
=== Wiring up Jobs by Using Triggers and `SchedulerFactoryBean`
We have created job details and jobs. We have also reviewed the convenience bean that lets
you invoke a method on a specific object. Of course, we still need to schedule the
jobs themselves. This is done by using triggers and a `SchedulerFactoryBean`. Several
triggers are available within Quartz, and Spring offers two Quartz `FactoryBean`
implementations with convenient defaults: `CronTriggerFactoryBean` and
`SimpleTriggerFactoryBean`.
Triggers need to be scheduled. Spring offers a `SchedulerFactoryBean` that exposes
triggers to be set as properties. `SchedulerFactoryBean` schedules the actual jobs with
those triggers.
The following listing uses both a `SimpleTriggerFactoryBean` and a `CronTriggerFactoryBean`:
[source,xml,indent=0]
[subs="verbatim"]
----
<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
<!-- see the example of method invoking job above -->
<property name="jobDetail" ref="jobDetail"/>
<!-- 10 seconds -->
<property name="startDelay" value="10000"/>
<!-- repeat every 50 seconds -->
<property name="repeatInterval" value="50000"/>
</bean>
<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="exampleJob"/>
<!-- run every morning at 6 AM -->
<property name="cronExpression" value="0 0 6 * * ?"/>
</bean>
----
The preceding example sets up two triggers, one running every 50 seconds with a starting delay of 10
seconds and one running every morning at 6 AM. To finalize everything, we need to set up the
`SchedulerFactoryBean`, as the following example shows:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="cronTrigger"/>
<ref bean="simpleTrigger"/>
</list>
</property>
</bean>
----
More properties are available for the `SchedulerFactoryBean`, such as the calendars used by the
job details, properties to customize Quartz with, and a Spring-provided JDBC DataSource. See
the {api-spring-framework}/scheduling/quartz/SchedulerFactoryBean.html[`SchedulerFactoryBean`]
javadoc for more information.
NOTE: `SchedulerFactoryBean` also recognizes a `quartz.properties` file in the classpath,
based on Quartz property keys, as with regular Quartz configuration. Please note that many
`SchedulerFactoryBean` settings interact with common Quartz settings in the properties file;
it is therefore not recommended to specify values at both levels. For example, do not set
an "org.quartz.jobStore.class" property if you mean to rely on a Spring-provided DataSource,
or specify an `org.springframework.scheduling.quartz.LocalDataSourceJobStore` variant which
is a full-fledged replacement for the standard `org.quartz.impl.jdbcjobstore.JobStoreTX`.
@@ -1,4 +0,0 @@
:toc: left
:toclevels: 4
:tabsize: 4
:docinfo1:
@@ -1,26 +0,0 @@
:noheader:
:toc:
:toclevels: 4
:tabsize: 4
include::attributes.adoc[]
= Spring Framework Documentation
Rod Johnson; Juergen Hoeller; Keith Donald; Colin Sampaleanu; Rob Harrop; Thomas Risberg; Alef Arendsen; Darren Davison; Dmitriy Kopylenko; Mark Pollack; Thierry Templier; Erwin Vervaet; Portia Tung; Ben Hale; Adrian Colyer; John Lewis; Costin Leau; Mark Fisher; Sam Brannen; Ramnivas Laddad; Arjen Poutsma; Chris Beams; Tareq Abedrabbo; Andy Clement; Dave Syer; Oliver Gierke; Rossen Stoyanchev; Phillip Webb; Rob Winch; Brian Clozel; Stephane Nicoll; Sebastien Deleuze; Jay Bryant; Mark Paluch
NOTE: This documentation is also available in {docs-spring-framework}/reference/html/index.html[HTML] format.
[[legal]]
== Legal
Copyright © 2002 - 2023 VMware, Inc. All Rights Reserved.
Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically.
include::overview.adoc[leveloffset=+1]
include::core.adoc[leveloffset=+1]
include::testing.adoc[leveloffset=+1]
include::data-access.adoc[leveloffset=+1]
include::web.adoc[leveloffset=+1]
include::web-reactive.adoc[leveloffset=+1]
include::integration.adoc[leveloffset=+1]
include::languages.adoc[leveloffset=+1]
include::appendix.adoc[leveloffset=+1]
@@ -1,30 +0,0 @@
[[testing]]
= Testing
include::attributes.adoc[]
include::page-layout.adoc[]
This chapter covers Spring's support for integration testing and best practices for unit
testing. The Spring team advocates test-driven development (TDD). The Spring team has
found that the correct use of inversion of control (IoC) certainly does make both unit
and integration testing easier (in that the presence of setter methods and appropriate
constructors on classes makes them easier to wire together in a test without having to
set up service locator registries and similar structures).
include::testing/testing-introduction.adoc[leveloffset=+1]
include::testing/unit-testing.adoc[leveloffset=+1]
include::testing/integration-testing.adoc[leveloffset=+1]
include::testing/testing-support-jdbc.adoc[leveloffset=+1]
include::testing/testcontext-framework.adoc[leveloffset=+1]
include::testing/testing-webtestclient.adoc[leveloffset=+1]
include::testing/spring-mvc-test-framework.adoc[leveloffset=+1]
include::testing/spring-mvc-test-client.adoc[leveloffset=+1]
include::testing/testing-appendix.adoc[leveloffset=+1]
@@ -1,155 +0,0 @@
[[integration-testing]]
= Integration Testing
It is important to be able to perform some integration testing without requiring
deployment to your application server or connecting to other enterprise infrastructure.
Doing so lets you test things such as:
* The correct wiring of your Spring IoC container contexts.
* Data access using JDBC or an ORM tool. This can include such things as the correctness
of SQL statements, Hibernate queries, JPA entity mappings, and so forth.
The Spring Framework provides first-class support for integration testing in the
`spring-test` module. The name of the actual JAR file might include the release version
and might also be in the long `org.springframework.test` form, depending on where you get
it from (see the <<core.adoc#beans-dependencies, section on Dependency Management>>
for an explanation). This library includes the `org.springframework.test` package, which
contains valuable classes for integration testing with a Spring container. This testing
does not rely on an application server or other deployment environment. Such tests are
slower to run than unit tests but much faster than the equivalent Selenium tests or
remote tests that rely on deployment to an application server.
Unit and integration testing support is provided in the form of the annotation-driven
<<testcontext-framework, Spring TestContext Framework>>. The TestContext framework is
agnostic of the actual testing framework in use, which allows instrumentation of tests
in various environments, including JUnit, TestNG, and others.
The following section provides an overview of the high-level goals of Spring's
integration support, and the rest of this chapter then focuses on dedicated topics:
* <<integration-testing-support-jdbc>>
* <<testcontext-framework>>
* <<webtestclient>>
* <<spring-mvc-test-framework>>
* <<spring-mvc-test-client>>
* <<integration-testing-annotations>>
[[integration-testing-goals]]
== Goals of Integration Testing
Spring's integration testing support has the following primary goals:
* To manage <<testing-ctx-management, Spring IoC container caching>> between tests.
* To provide <<testing-fixture-di, Dependency Injection of test fixture instances>>.
* To provide <<testing-tx, transaction management>> appropriate to integration testing.
* To supply <<testing-support-classes, Spring-specific base classes>> that assist
developers in writing integration tests.
The next few sections describe each goal and provide links to implementation and
configuration details.
[[testing-ctx-management]]
=== Context Management and Caching
The Spring TestContext Framework provides consistent loading of Spring
`ApplicationContext` instances and `WebApplicationContext` instances as well as caching
of those contexts. Support for the caching of loaded contexts is important, because
startup time can become an issue -- not because of the overhead of Spring itself, but
because the objects instantiated by the Spring container take time to instantiate. For
example, a project with 50 to 100 Hibernate mapping files might take 10 to 20 seconds to
load the mapping files, and incurring that cost before running every test in every test
fixture leads to slower overall test runs that reduce developer productivity.
Test classes typically declare either an array of resource locations for XML or Groovy
configuration metadata -- often in the classpath -- or an array of component classes that
is used to configure the application. These locations or classes are the same as or
similar to those specified in `web.xml` or other configuration files for production
deployments.
By default, once loaded, the configured `ApplicationContext` is reused for each test.
Thus, the setup cost is incurred only once per test suite, and subsequent test execution
is much faster. In this context, the term "`test suite`" means all tests run in the same
JVM -- for example, all tests run from an Ant, Maven, or Gradle build for a given project
or module. In the unlikely case that a test corrupts the application context and requires
reloading (for example, by modifying a bean definition or the state of an application
object) the TestContext framework can be configured to reload the configuration and
rebuild the application context before executing the next test.
See <<testcontext-ctx-management>> and <<testcontext-ctx-management-caching>> with the
TestContext framework.
[[testing-fixture-di]]
=== Dependency Injection of Test Fixtures
When the TestContext framework loads your application context, it can optionally
configure instances of your test classes by using Dependency Injection. This provides a
convenient mechanism for setting up test fixtures by using preconfigured beans from your
application context. A strong benefit here is that you can reuse application contexts
across various testing scenarios (for example, for configuring Spring-managed object
graphs, transactional proxies, `DataSource` instances, and others), thus avoiding the
need to duplicate complex test fixture setup for individual test cases.
As an example, consider a scenario where we have a class (`HibernateTitleRepository`)
that implements data access logic for a `Title` domain entity. We want to write
integration tests that test the following areas:
* The Spring configuration: Basically, is everything related to the configuration of the
`HibernateTitleRepository` bean correct and present?
* The Hibernate mapping file configuration: Is everything mapped correctly and are the
correct lazy-loading settings in place?
* The logic of the `HibernateTitleRepository`: Does the configured instance of this class
perform as anticipated?
See dependency injection of test fixtures with the
<<testcontext-fixture-di, TestContext framework>>.
[[testing-tx]]
=== Transaction Management
One common issue in tests that access a real database is their effect on the state of the
persistence store. Even when you use a development database, changes to the state may
affect future tests. Also, many operations -- such as inserting or modifying persistent
data -- cannot be performed (or verified) outside of a transaction.
The TestContext framework addresses this issue. By default, the framework creates and
rolls back a transaction for each test. You can write code that can assume the existence
of a transaction. If you call transactionally proxied objects in your tests, they behave
correctly, according to their configured transactional semantics. In addition, if a test
method deletes the contents of selected tables while running within the transaction
managed for the test, the transaction rolls back by default, and the database returns to
its state prior to execution of the test. Transactional support is provided to a test by
using a `PlatformTransactionManager` bean defined in the test's application context.
If you want a transaction to commit (unusual, but occasionally useful when you want a
particular test to populate or modify the database), you can tell the TestContext
framework to cause the transaction to commit instead of roll back by using the
<<integration-testing-annotations, `@Commit`>> annotation.
See transaction management with the <<testcontext-tx, TestContext framework>>.
[[testing-support-classes]]
=== Support Classes for Integration Testing
The Spring TestContext Framework provides several `abstract` support classes that
simplify the writing of integration tests. These base test classes provide well-defined
hooks into the testing framework as well as convenient instance variables and methods,
which let you access:
* The `ApplicationContext`, for performing explicit bean lookups or testing the state of
the context as a whole.
* A `JdbcTemplate`, for executing SQL statements to query the database. You can use such
queries to confirm database state both before and after execution of database-related
application code, and Spring ensures that such queries run in the scope of the same
transaction as the application code. When used in conjunction with an ORM tool, be sure
to avoid <<testcontext-tx-false-positives, false positives>>.
In addition, you may want to create your own custom, application-wide superclass with
instance variables and methods specific to your project.
See support classes for the <<testcontext-support-classes, TestContext framework>>.
@@ -1,185 +0,0 @@
[[spring-mvc-test-client]]
= Testing Client Applications
You can use client-side tests to test code that internally uses the `RestTemplate`. The
idea is to declare expected requests and to provide "`stub`" responses so that you can
focus on testing the code in isolation (that is, without running a server). The following
example shows how to do so:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
RestTemplate restTemplate = new RestTemplate();
MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
mockServer.expect(requestTo("/greeting")).andRespond(withSuccess());
// Test code that uses the above RestTemplate ...
mockServer.verify();
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val restTemplate = RestTemplate()
val mockServer = MockRestServiceServer.bindTo(restTemplate).build()
mockServer.expect(requestTo("/greeting")).andRespond(withSuccess())
// Test code that uses the above RestTemplate ...
mockServer.verify()
----
In the preceding example, `MockRestServiceServer` (the central class for client-side REST
tests) configures the `RestTemplate` with a custom `ClientHttpRequestFactory` that
asserts actual requests against expectations and returns "`stub`" responses. In this
case, we expect a request to `/greeting` and want to return a 200 response with
`text/plain` content. We can define additional expected requests and stub responses as
needed. When we define expected requests and stub responses, the `RestTemplate` can be
used in client-side code as usual. At the end of testing, `mockServer.verify()` can be
used to verify that all expectations have been satisfied.
By default, requests are expected in the order in which expectations were declared. You
can set the `ignoreExpectOrder` option when building the server, in which case all
expectations are checked (in order) to find a match for a given request. That means
requests are allowed to come in any order. The following example uses `ignoreExpectOrder`:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
server = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build();
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
server = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build()
----
Even with unordered requests by default, each request is allowed to run once only.
The `expect` method provides an overloaded variant that accepts an `ExpectedCount`
argument that specifies a count range (for example, `once`, `manyTimes`, `max`, `min`,
`between`, and so on). The following example uses `times`:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
RestTemplate restTemplate = new RestTemplate();
MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
mockServer.expect(times(2), requestTo("/something")).andRespond(withSuccess());
mockServer.expect(times(3), requestTo("/somewhere")).andRespond(withSuccess());
// ...
mockServer.verify();
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val restTemplate = RestTemplate()
val mockServer = MockRestServiceServer.bindTo(restTemplate).build()
mockServer.expect(times(2), requestTo("/something")).andRespond(withSuccess())
mockServer.expect(times(3), requestTo("/somewhere")).andRespond(withSuccess())
// ...
mockServer.verify()
----
Note that, when `ignoreExpectOrder` is not set (the default), and, therefore, requests
are expected in order of declaration, then that order applies only to the first of any
expected request. For example if "/something" is expected two times followed by
"/somewhere" three times, then there should be a request to "/something" before there is
a request to "/somewhere", but, aside from that subsequent "/something" and "/somewhere",
requests can come at any time.
As an alternative to all of the above, the client-side test support also provides a
`ClientHttpRequestFactory` implementation that you can configure into a `RestTemplate` to
bind it to a `MockMvc` instance. That allows processing requests using actual server-side
logic but without running a server. The following example shows how to do so:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
this.restTemplate = new RestTemplate(new MockMvcClientHttpRequestFactory(mockMvc));
// Test code that uses the above RestTemplate ...
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build()
restTemplate = RestTemplate(MockMvcClientHttpRequestFactory(mockMvc))
// Test code that uses the above RestTemplate ...
----
In some cases it may be necessary to perform an actual call to a remote service instead
of mocking the response. The following example shows how to do that through
`ExecutingResponseCreator`:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
RestTemplate restTemplate = new RestTemplate();
// Create ExecutingResponseCreator with the original request factory
ExecutingResponseCreator withActualResponse = new ExecutingResponseCreator(restTemplate.getRequestFactory());
MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
mockServer.expect(requestTo("/profile")).andRespond(withSuccess());
mockServer.expect(requestTo("/quoteOfTheDay")).andRespond(withActualResponse);
// Test code that uses the above RestTemplate ...
mockServer.verify();
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val restTemplate = RestTemplate()
// Create ExecutingResponseCreator with the original request factory
val withActualResponse = new ExecutingResponseCreator(restTemplate.getRequestFactory())
val mockServer = MockRestServiceServer.bindTo(restTemplate).build()
mockServer.expect(requestTo("/profile")).andRespond(withSuccess())
mockServer.expect(requestTo("/quoteOfTheDay")).andRespond(withActualResponse)
// Test code that uses the above RestTemplate ...
mockServer.verify()
----
In the preceding example, we create the `ExecutingResponseCreator` using the
`ClientHttpRequestFactory` from the `RestTemplate` _before_ `MockRestServiceServer` replaces
it with a different one that mocks responses.
Then we define expectations with two kinds of responses:
* a stub `200` response for the `/profile` endpoint (no actual request will be executed)
* a response obtained through a call to the `/quoteOfTheDay` endpoint
In the second case, the request is executed through the `ClientHttpRequestFactory` that was
captured earlier. This generates a response that could e.g. come from an actual remote server,
depending on how the `RestTemplate` was originally configured.
[[spring-mvc-test-client-static-imports]]
== Static Imports
As with server-side tests, the fluent API for client-side tests requires a few static
imports. Those are easy to find by searching for `MockRest*`. Eclipse users should add
`MockRestRequestMatchers.{asterisk}` and `MockRestResponseCreators.{asterisk}` as
"`favorite static members`" in the Eclipse preferences under Java -> Editor -> Content
Assist -> Favorites. That allows using content assist after typing the first character of
the static method name. Other IDEs (such IntelliJ) may not require any additional
configuration. Check for the support for code completion on static members.
[[spring-mvc-test-client-resources]]
== Further Examples of Client-side REST Tests
Spring MVC Test's own tests include
{spring-framework-main-code}/spring-test/src/test/java/org/springframework/test/web/client/samples[example
tests] of client-side REST tests.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,6 +0,0 @@
[[testing.appendix]]
= Appendix
include::testing-annotations.adoc[leveloffset=+1]
include::testing-resources.adoc[leveloffset=+1]
@@ -1,8 +0,0 @@
[[testing-introduction]]
= Introduction to Spring Testing
Testing is an integral part of enterprise software development. This chapter focuses on
the value added by the IoC principle to <<unit-testing, unit testing>> and on the benefits
of the Spring Framework's support for <<integration-testing, integration testing>>. (A
thorough treatment of testing in the enterprise is beyond the scope of this reference
manual.)
@@ -1,32 +0,0 @@
[[testing-resources]]
= Further Resources
See the following resources for more information about testing:
* https://www.junit.org/[JUnit]: "A programmer-friendly testing framework for Java and the JVM".
Used by the Spring Framework in its test suite and supported in the
<<testcontext-framework, Spring TestContext Framework>>.
* https://testng.org/[TestNG]: A testing framework inspired by JUnit with added support
for test groups, data-driven testing, distributed testing, and other features. Supported
in the <<testcontext-framework, Spring TestContext Framework>>
* https://assertj.github.io/doc/[AssertJ]: "Fluent assertions for Java",
including support for Java 8 lambdas, streams, and numerous other features.
* https://en.wikipedia.org/wiki/Mock_Object[Mock Objects]: Article in Wikipedia.
* http://www.mockobjects.com/[MockObjects.com]: Web site dedicated to mock objects, a
technique for improving the design of code within test-driven development.
* https://mockito.github.io[Mockito]: Java mock library based on the
http://xunitpatterns.com/Test%20Spy.html[Test Spy] pattern. Used by the Spring Framework
in its test suite.
* https://easymock.org/[EasyMock]: Java library "that provides Mock Objects for
interfaces (and objects through the class extension) by generating them on the fly using
Java's proxy mechanism."
* https://jmock.org/[JMock]: Library that supports test-driven development of Java code
with mock objects.
* https://www.dbunit.org/[DbUnit]: JUnit extension (also usable with Ant and Maven) that
is targeted at database-driven projects and, among other things, puts your database into
a known state between test runs.
* https://www.testcontainers.org/[Testcontainers]: Java library that supports JUnit
tests, providing lightweight, throwaway instances of common databases, Selenium web
browsers, or anything else that can run in a Docker container.
* https://sourceforge.net/projects/grinder/[The Grinder]: Java load testing framework.
* https://github.com/Ninja-Squad/springmockk[SpringMockK]: Support for Spring Boot
integration tests written in Kotlin using https://mockk.io/[MockK] instead of Mockito.
@@ -1,35 +0,0 @@
[[integration-testing-support-jdbc]]
= JDBC Testing Support
[[integration-testing-support-jdbc-test-utils]]
== JdbcTestUtils
The `org.springframework.test.jdbc` package contains `JdbcTestUtils`, which is a
collection of JDBC-related utility functions intended to simplify standard database
testing scenarios. Specifically, `JdbcTestUtils` provides the following static utility
methods.
* `countRowsInTable(..)`: Counts the number of rows in the given table.
* `countRowsInTableWhere(..)`: Counts the number of rows in the given table by using the
provided `WHERE` clause.
* `deleteFromTables(..)`: Deletes all rows from the specified tables.
* `deleteFromTableWhere(..)`: Deletes rows from the given table by using the provided
`WHERE` clause.
* `dropTables(..)`: Drops the specified tables.
[TIP]
====
<<testcontext-support-classes-junit4, `AbstractTransactionalJUnit4SpringContextTests`>>
and <<testcontext-support-classes-testng, `AbstractTransactionalTestNGSpringContextTests`>>
provide convenience methods that delegate to the aforementioned methods in
`JdbcTestUtils`.
====
[[integration-testing-support-jdbc-embedded-database]]
== Embedded Databases
The `spring-jdbc` module provides support for configuring and launching an embedded
database, which you can use in integration tests that interact with a database.
For details, see <<data-access.adoc#jdbc-embedded-database-support, Embedded Database
Support>> and <<data-access.adoc#jdbc-embedded-database-dao-testing, Testing Data Access
Logic with an Embedded Database>>.
@@ -1,168 +0,0 @@
[[unit-testing]]
= Unit Testing
Dependency injection should make your code less dependent on the container than it would
be with traditional J2EE / Java EE development. The POJOs that make up your application
should be testable in JUnit or TestNG tests, with objects instantiated by using the `new`
operator, without Spring or any other container. You can use <<mock-objects, mock objects>>
(in conjunction with other valuable testing techniques) to test your code in isolation.
If you follow the architecture recommendations for Spring, the resulting clean layering
and componentization of your codebase facilitate easier unit testing. For example,
you can test service layer objects by stubbing or mocking DAO or repository interfaces,
without needing to access persistent data while running unit tests.
True unit tests typically run extremely quickly, as there is no runtime infrastructure to
set up. Emphasizing true unit tests as part of your development methodology can boost
your productivity. You may not need this section of the testing chapter to help you write
effective unit tests for your IoC-based applications. For certain unit testing scenarios,
however, the Spring Framework provides mock objects and testing support classes, which
are described in this chapter.
[[mock-objects]]
== Mock Objects
Spring includes a number of packages dedicated to mocking:
* <<mock-objects-env>>
* <<mock-objects-jndi>>
* <<mock-objects-servlet>>
* <<mock-objects-web-reactive>>
[[mock-objects-env]]
=== Environment
The `org.springframework.mock.env` package contains mock implementations of the
`Environment` and `PropertySource` abstractions (see
<<core.adoc#beans-definition-profiles, Bean Definition Profiles>>
and <<core.adoc#beans-property-source-abstraction, `PropertySource` Abstraction>>).
`MockEnvironment` and `MockPropertySource` are useful for developing
out-of-container tests for code that depends on environment-specific properties.
[[mock-objects-jndi]]
=== JNDI
The `org.springframework.mock.jndi` package contains a partial implementation of the JNDI
SPI, which you can use to set up a simple JNDI environment for test suites or stand-alone
applications. If, for example, JDBC `DataSource` instances get bound to the same JNDI
names in test code as they do in a Jakarta EE container, you can reuse both application code
and configuration in testing scenarios without modification.
WARNING: The mock JNDI support in the `org.springframework.mock.jndi` package is
officially deprecated as of Spring Framework 5.2 in favor of complete solutions from third
parties such as https://github.com/h-thurow/Simple-JNDI[Simple-JNDI].
[[mock-objects-servlet]]
=== Servlet API
The `org.springframework.mock.web` package contains a comprehensive set of Servlet API
mock objects that are useful for testing web contexts, controllers, and filters. These
mock objects are targeted at usage with Spring's Web MVC framework and are generally more
convenient to use than dynamic mock objects (such as https://easymock.org/[EasyMock])
or alternative Servlet API mock objects (such as http://www.mockobjects.com[MockObjects]).
TIP: Since Spring Framework 6.0, the mock objects in `org.springframework.mock.web` are
based on the Servlet 6.0 API.
The Spring MVC Test framework builds on the mock Servlet API objects to provide an
integration testing framework for Spring MVC. See <<spring-mvc-test-framework>>.
[[mock-objects-web-reactive]]
=== Spring Web Reactive
The `org.springframework.mock.http.server.reactive` package contains mock implementations
of `ServerHttpRequest` and `ServerHttpResponse` for use in WebFlux applications. The
`org.springframework.mock.web.server` package contains a mock `ServerWebExchange` that
depends on those mock request and response objects.
Both `MockServerHttpRequest` and `MockServerHttpResponse` extend from the same abstract
base classes as server-specific implementations and share behavior with them. For
example, a mock request is immutable once created, but you can use the `mutate()` method
from `ServerHttpRequest` to create a modified instance.
In order for the mock response to properly implement the write contract and return a
write completion handle (that is, `Mono<Void>`), it by default uses a `Flux` with
`cache().then()`, which buffers the data and makes it available for assertions in tests.
Applications can set a custom write function (for example, to test an infinite stream).
The <<webtestclient>> builds on the mock request and response to provide support for
testing WebFlux applications without an HTTP server. The client can also be used for
end-to-end tests with a running server.
[[unit-testing-support-classes]]
== Unit Testing Support Classes
Spring includes a number of classes that can help with unit testing. They fall into two
categories:
* <<unit-testing-utilities>>
* <<unit-testing-spring-mvc>>
[[unit-testing-utilities]]
=== General Testing Utilities
The `org.springframework.test.util` package contains several general purpose utilities
for use in unit and integration testing.
{api-spring-framework}/test/util/AopTestUtils.html[`AopTestUtils`] is a collection of
AOP-related utility methods. You can use these methods to obtain a reference to the
underlying target object hidden behind one or more Spring proxies. For example, if you
have configured a bean as a dynamic mock by using a library such as EasyMock or Mockito,
and the mock is wrapped in a Spring proxy, you may need direct access to the underlying
mock to configure expectations on it and perform verifications. For Spring's core AOP
utilities, see {api-spring-framework}/aop/support/AopUtils.html[`AopUtils`] and
{api-spring-framework}/aop/framework/AopProxyUtils.html[`AopProxyUtils`].
{api-spring-framework}/test/util/ReflectionTestUtils.html[`ReflectionTestUtils`] is a
collection of reflection-based utility methods. You can use these methods in testing
scenarios where you need to change the value of a constant, set a non-`public` field,
invoke a non-`public` setter method, or invoke a non-`public` configuration or lifecycle
callback method when testing application code for use cases such as the following:
* ORM frameworks (such as JPA and Hibernate) that condone `private` or `protected` field
access as opposed to `public` setter methods for properties in a domain entity.
* Spring's support for annotations (such as `@Autowired`, `@Inject`, and `@Resource`),
that provide dependency injection for `private` or `protected` fields, setter methods,
and configuration methods.
* Use of annotations such as `@PostConstruct` and `@PreDestroy` for lifecycle callback
methods.
{api-spring-framework}/test/util/TestSocketUtils.html[`TestSocketUtils`] is a simple
utility for finding available TCP ports on `localhost` for use in integration testing
scenarios.
[NOTE]
====
`TestSocketUtils` can be used in integration tests which start an external server on an
available random port. However, these utilities make no guarantee about the subsequent
availability of a given port and are therefore unreliable. Instead of using
`TestSocketUtils` to find an available local port for a server, it is recommended that
you rely on a server's ability to start on a random ephemeral port that it selects or is
assigned by the operating system. To interact with that server, you should query the
server for the port it is currently using.
====
[[unit-testing-spring-mvc]]
=== Spring MVC Testing Utilities
The `org.springframework.test.web` package contains
{api-spring-framework}/test/web/ModelAndViewAssert.html[`ModelAndViewAssert`], which you
can use in combination with JUnit, TestNG, or any other testing framework for unit tests
that deal with Spring MVC `ModelAndView` objects.
.Unit testing Spring MVC Controllers
TIP: To unit test your Spring MVC `Controller` classes as POJOs, use `ModelAndViewAssert`
combined with `MockHttpServletRequest`, `MockHttpSession`, and so on from Spring's
<<mock-objects-servlet, Servlet API mocks>>. For thorough integration testing of your
Spring MVC and REST `Controller` classes in conjunction with your `WebApplicationContext`
configuration for Spring MVC, use the
<<spring-mvc-test-framework, Spring MVC Test Framework>> instead.
@@ -1,44 +0,0 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.docs.core.aot.hints.importruntimehints;
import java.util.Locale;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
@Component
@ImportRuntimeHints(SpellCheckService.SpellCheckServiceRuntimeHints.class)
public class SpellCheckService {
public void loadDictionary(Locale locale) {
ClassPathResource resource = new ClassPathResource("dicts/" + locale.getLanguage() + ".txt");
//...
}
static class SpellCheckServiceRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
hints.resources().registerPattern("dicts/*");
}
}
}
@@ -1,42 +0,0 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.docs.core.aot.hints.testing;
import java.lang.reflect.Method;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.ClassUtils;
public class SampleReflection {
private final Log logger = LogFactory.getLog(SampleReflection.class);
public void performReflection() {
try {
Class<?> springVersion = ClassUtils.forName("org.springframework.core.SpringVersion", null);
Method getVersion = ClassUtils.getMethod(springVersion, "getVersion");
String version = (String) getVersion.invoke(null);
logger.info("Spring version:" + version);
}
catch (Exception exc) {
logger.error("reflection failed", exc);
}
}
}
@@ -1,54 +0,0 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.docs.core.aot.hints.testing;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.test.agent.EnabledIfRuntimeHintsAgent;
import org.springframework.aot.test.agent.RuntimeHintsInvocations;
import org.springframework.aot.test.agent.RuntimeHintsRecorder;
import org.springframework.core.SpringVersion;
import static org.assertj.core.api.Assertions.assertThat;
// @EnabledIfRuntimeHintsAgent signals that the annotated test class or test
// method is only enabled if the RuntimeHintsAgent is loaded on the current JVM.
// It also tags tests with the "RuntimeHints" JUnit tag.
@EnabledIfRuntimeHintsAgent
class SampleReflectionRuntimeHintsTests {
@Test
void shouldRegisterReflectionHints() {
RuntimeHints runtimeHints = new RuntimeHints();
// Call a RuntimeHintsRegistrar that contributes hints like:
runtimeHints.reflection().registerType(SpringVersion.class, typeHint ->
typeHint.withMethod("getVersion", List.of(), ExecutableMode.INVOKE));
// Invoke the relevant piece of code we want to test within a recording lambda
RuntimeHintsInvocations invocations = RuntimeHintsRecorder.record(() -> {
SampleReflection sample = new SampleReflection();
sample.performReflection();
});
// assert that the recorded invocations are covered by the contributed hints
assertThat(invocations).match(runtimeHints);
}
}
@@ -1,48 +0,0 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.docs.core.aot.hints.testing;
import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
import static org.assertj.core.api.Assertions.assertThat;
public class SpellCheckServiceTests {
// tag::hintspredicates[]
@Test
void shouldRegisterResourceHints() {
RuntimeHints hints = new RuntimeHints();
new SpellCheckServiceRuntimeHints().registerHints(hints, getClass().getClassLoader());
assertThat(RuntimeHintsPredicates.resource().forResource("dicts/en.txt"))
.accepts(hints);
}
// end::hintspredicates[]
// Copied here because it is package private in SpellCheckService
static class SpellCheckServiceRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
hints.resources().registerPattern("dicts/*");
}
}
}
@@ -1,52 +0,0 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.docs.core.aot.refresh;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
public class AotProcessingSample {
public void createAotContext() {
// tag::aotcontext[]
RuntimeHints hints = new RuntimeHints();
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(MyApplication.class);
context.refreshForAotProcessing(hints);
// ...
context.close();
// end::aotcontext[]
}
// tag::myapplication[]
@Configuration(proxyBeanMethods=false)
@ComponentScan
@Import({DataSourceConfiguration.class, ContainerConfiguration.class})
public class MyApplication {
}
// end::myapplication[]
class DataSourceConfiguration {
}
class ContainerConfiguration {
}
}
@@ -1,71 +0,0 @@
/*
* Copyright 2002-2022 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.integration.observability.config.conventions;
import io.micrometer.common.KeyValue;
import io.micrometer.common.KeyValues;
import org.springframework.http.server.observation.ServerHttpObservationDocumentation;
import org.springframework.http.server.observation.ServerRequestObservationContext;
import org.springframework.http.server.observation.ServerRequestObservationConvention;
public class CustomServerRequestObservationConvention implements ServerRequestObservationConvention {
@Override
public String getName() {
// will be used as the metric name
return "http.server.requests";
}
@Override
public String getContextualName(ServerRequestObservationContext context) {
// will be used for the trace name
return "http " + context.getCarrier().getMethod().toLowerCase();
}
@Override
public KeyValues getLowCardinalityKeyValues(ServerRequestObservationContext context) {
return KeyValues.of(method(context), status(context), exception(context));
}
@Override
public KeyValues getHighCardinalityKeyValues(ServerRequestObservationContext context) {
return KeyValues.of(httpUrl(context));
}
private KeyValue method(ServerRequestObservationContext context) {
// You should reuse as much as possible the corresponding ObservationDocumentation for key names
return KeyValue.of(ServerHttpObservationDocumentation.LowCardinalityKeyNames.METHOD, context.getCarrier().getMethod());
}
// @fold:on // status(), exception(), httpUrl()...
private KeyValue status(ServerRequestObservationContext context) {
return KeyValue.of(ServerHttpObservationDocumentation.LowCardinalityKeyNames.STATUS, String.valueOf(context.getResponse().getStatus()));
}
private KeyValue exception(ServerRequestObservationContext context) {
String exception = (context.getError() != null) ? context.getError().getClass().getSimpleName() : KeyValue.NONE_VALUE;
return KeyValue.of(ServerHttpObservationDocumentation.LowCardinalityKeyNames.EXCEPTION, exception);
}
private KeyValue httpUrl(ServerRequestObservationContext context) {
return KeyValue.of(ServerHttpObservationDocumentation.HighCardinalityKeyNames.HTTP_URL, context.getCarrier().getRequestURI());
}
// @fold:off
}
@@ -1,37 +0,0 @@
/*
* Copyright 2002-2022 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.integration.observability.config.conventions;
import io.micrometer.common.KeyValue;
import io.micrometer.common.KeyValues;
import org.springframework.http.server.observation.DefaultServerRequestObservationConvention;
import org.springframework.http.server.observation.ServerRequestObservationContext;
public class ExtendedServerRequestObservationConvention extends DefaultServerRequestObservationConvention {
@Override
public KeyValues getLowCardinalityKeyValues(ServerRequestObservationContext context) {
// here, we just want to have an additional KeyValue to the observation, keeping the default values
return super.getLowCardinalityKeyValues(context).and(custom(context));
}
private KeyValue custom(ServerRequestObservationContext context) {
return KeyValue.of("custom.method", context.getCarrier().getMethod());
}
}
@@ -1,38 +0,0 @@
/*
* Copyright 2002-2022 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.integration.observability.config.conventions;
import io.micrometer.common.KeyValue;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationFilter;
import org.springframework.http.server.observation.ServerRequestObservationContext;
public class ServerRequestObservationFilter implements ObservationFilter {
@Override
public Observation.Context map(Observation.Context context) {
if (context instanceof ServerRequestObservationContext serverContext) {
context.setName("custom.observation.name");
context.addLowCardinalityKeyValue(KeyValue.of("project", "spring"));
String customAttribute = (String) serverContext.getCarrier().getAttribute("customAttribute");
context.addLowCardinalityKeyValue(KeyValue.of("custom.attribute", customAttribute));
}
return context;
}
}
@@ -1,42 +0,0 @@
/*
* Copyright 2002-2023 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.integration.observability.httpserver.reactive;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.filter.reactive.ServerHttpObservationFilter;
import org.springframework.web.server.ServerWebExchange;
@Controller
public class UserController {
@ExceptionHandler(MissingUserException.class)
ResponseEntity<Void> handleMissingUser(ServerWebExchange exchange, MissingUserException exception) {
// We want to record this exception with the observation
ServerHttpObservationFilter.findObservationContext(exchange)
.ifPresent(context -> context.setError(exception));
return ResponseEntity.notFound().build();
}
// @fold:on
@SuppressWarnings("serial")
static class MissingUserException extends RuntimeException {
}
// @fold:off
}
@@ -1,43 +0,0 @@
/*
* Copyright 2002-2023 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.integration.observability.httpserver.servlet;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.filter.ServerHttpObservationFilter;
@Controller
public class UserController {
@ExceptionHandler(MissingUserException.class)
ResponseEntity<Void> handleMissingUser(HttpServletRequest request, MissingUserException exception) {
// We want to record this exception with the observation
ServerHttpObservationFilter.findObservationContext(request)
.ifPresent(context -> context.setError(exception));
return ResponseEntity.notFound().build();
}
// @fold:on
@SuppressWarnings("serial")
static class MissingUserException extends RuntimeException {
}
// @fold:off
}
@@ -1,148 +0,0 @@
plugins {
id 'java-platform'
}
javaPlatform {
allowDependencies()
}
dependencies {
api(platform("com.fasterxml.jackson:jackson-bom:2.14.2"))
api(platform("io.micrometer:micrometer-bom:1.10.4"))
api(platform("io.netty:netty-bom:4.1.89.Final"))
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
api(platform("io.projectreactor:reactor-bom:2022.0.4"))
api(platform("io.rsocket:rsocket-bom:1.1.3"))
api(platform("org.apache.groovy:groovy-bom:4.0.8"))
api(platform("org.apache.logging.log4j:log4j-bom:2.19.0"))
api(platform("org.eclipse.jetty:jetty-bom:11.0.13"))
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4"))
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.4.0"))
api(platform("org.junit:junit-bom:5.9.2"))
api(platform("org.mockito:mockito-bom:5.1.1"))
constraints {
api("com.fasterxml:aalto-xml:1.3.1")
api("com.fasterxml.woodstox:woodstox-core:6.5.0")
api("com.github.ben-manes.caffeine:caffeine:3.1.2")
api("com.github.librepdf:openpdf:1.3.30")
api("com.google.code.findbugs:findbugs:3.0.1")
api("com.google.code.findbugs:jsr305:3.0.2")
api("com.google.code.gson:gson:2.10")
api("com.google.protobuf:protobuf-java-util:3.21.12")
api("com.googlecode.protobuf-java-format:protobuf-java-format:1.4")
api("com.h2database:h2:2.1.214")
api("com.jayway.jsonpath:json-path:2.7.0")
api("com.rometools:rome:1.18.0")
api("com.squareup.okhttp3:mockwebserver:3.14.9")
api("com.squareup.okhttp3:okhttp:3.14.9")
api("com.sun.activation:jakarta.activation:2.0.1")
api("com.sun.mail:jakarta.mail:2.0.1")
api("com.sun.xml.bind:jaxb-core:3.0.2")
api("com.sun.xml.bind:jaxb-impl:3.0.2")
api("com.sun.xml.bind:jaxb-xjc:3.0.2")
api("com.thoughtworks.qdox:qdox:2.0.2")
api("com.thoughtworks.xstream:xstream:1.4.19")
api("commons-io:commons-io:2.11.0")
api("de.bechte.junit:junit-hierarchicalcontextrunner:4.12.1")
api("info.picocli:picocli:4.7.0")
api("io.micrometer:context-propagation:1.0.0")
api("io.mockk:mockk:1.12.1")
api("io.projectreactor.netty:reactor-netty5-http:2.0.0-M3")
api("io.projectreactor.tools:blockhound:1.0.6.RELEASE")
api("io.r2dbc:r2dbc-h2:1.0.0.RELEASE")
api("io.r2dbc:r2dbc-spi-test:1.0.0.RELEASE")
api("io.r2dbc:r2dbc-spi:1.0.0.RELEASE")
api("io.reactivex.rxjava3:rxjava:3.1.5")
api("io.smallrye.reactive:mutiny:1.8.0")
api("io.undertow:undertow-core:2.3.3.Final")
api("io.undertow:undertow-servlet:2.3.3.Final")
api("io.undertow:undertow-websockets-jsr:2.3.3.Final")
api("io.vavr:vavr:0.10.4")
api("jakarta.activation:jakarta.activation-api:2.0.1")
api("jakarta.annotation:jakarta.annotation-api:2.0.0")
api("jakarta.ejb:jakarta.ejb-api:4.0.0")
api("jakarta.el:jakarta.el-api:4.0.0")
api("jakarta.enterprise.concurrent:jakarta.enterprise.concurrent-api:2.0.0")
api("jakarta.faces:jakarta.faces-api:3.0.0")
api("jakarta.inject:jakarta.inject-api:2.0.0")
api("jakarta.inject:jakarta.inject-tck:2.0.1")
api("jakarta.interceptor:jakarta.interceptor-api:2.0.0")
api("jakarta.jms:jakarta.jms-api:3.0.0")
api("jakarta.json.bind:jakarta.json.bind-api:2.0.0")
api("jakarta.json:jakarta.json-api:2.0.1")
api("jakarta.mail:jakarta.mail-api:2.0.1")
api("jakarta.persistence:jakarta.persistence-api:3.0.0")
api("jakarta.resource:jakarta.resource-api:2.0.0")
api("jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:3.0.0")
api("jakarta.servlet.jsp:jakarta.servlet.jsp-api:3.1.0")
api("jakarta.servlet:jakarta.servlet-api:6.0.0")
api("jakarta.transaction:jakarta.transaction-api:2.0.1")
api("jakarta.validation:jakarta.validation-api:3.0.2")
api("jakarta.websocket:jakarta.websocket-api:2.1.0")
api("jakarta.websocket:jakarta.websocket-client-api:2.1.0")
api("jakarta.xml.bind:jakarta.xml.bind-api:3.0.1")
api("javax.cache:cache-api:1.1.1")
api("javax.money:money-api:1.1")
api("jaxen:jaxen:1.2.0")
api("junit:junit:4.13.2")
api("net.sf.jopt-simple:jopt-simple:5.0.4")
api("net.sourceforge.htmlunit:htmlunit:2.70.0")
api("org.apache-extras.beanshell:bsh:2.0b6")
api("org.apache.activemq:activemq-broker:5.16.2")
api("org.apache.activemq:activemq-kahadb-store:5.16.2")
api("org.apache.activemq:activemq-stomp:5.16.2")
api("org.apache.commons:commons-pool2:2.9.0")
api("org.apache.derby:derby:10.16.1.1")
api("org.apache.derby:derbyclient:10.16.1.1")
api("org.apache.derby:derbytools:10.16.1.1")
api("org.apache.httpcomponents.client5:httpclient5:5.2.1")
api("org.apache.httpcomponents.core5:httpcore5-reactive:5.2.1")
api("org.apache.poi:poi-ooxml:5.2.3")
api("org.apache.tomcat.embed:tomcat-embed-core:10.1.5")
api("org.apache.tomcat.embed:tomcat-embed-websocket:10.1.5")
api("org.apache.tomcat:tomcat-util:10.1.5")
api("org.apache.tomcat:tomcat-websocket:10.1.5")
api("org.aspectj:aspectjrt:1.9.9.1")
api("org.aspectj:aspectjtools:1.9.9.1")
api("org.aspectj:aspectjweaver:1.9.9.1")
api("org.assertj:assertj-core:3.24.2")
api("org.awaitility:awaitility:3.1.6")
api("org.bouncycastle:bcpkix-jdk18on:1.71")
api("org.codehaus.jettison:jettison:1.3.8")
api("org.dom4j:dom4j:2.1.3")
api("org.eclipse.jetty:jetty-reactive-httpclient:3.0.7")
api("org.eclipse.persistence:org.eclipse.persistence.jpa:3.0.3")
api("org.eclipse:yasson:2.0.4")
api("org.ehcache:ehcache:3.4.0")
api("org.ehcache:jcache:1.0.1")
api("org.freemarker:freemarker:2.3.32")
// Substitute for "javax.management:jmxremote_optional:1.0.1_04" which
// is not available on Maven Central
api("org.glassfish.external:opendmk_jmxremote_optional_jar:1.0-b01-ea")
api("org.glassfish.tyrus:tyrus-container-servlet:2.0.1")
api("org.glassfish:jakarta.el:4.0.2")
api("org.graalvm.sdk:graal-sdk:22.3.0")
api("org.hamcrest:hamcrest:2.2")
api("org.hibernate:hibernate-core-jakarta:5.6.15.Final")
api("org.hibernate:hibernate-validator:7.0.5.Final")
api("org.hsqldb:hsqldb:2.7.1")
api("org.javamoney:moneta:1.4.2")
api("org.jruby:jruby:9.4.0.0")
api("org.junit.support:testng-engine:1.0.4")
api("org.mozilla:rhino:1.7.11")
api("org.ogce:xpp3:1.1.6")
api("org.python:jython-standalone:2.7.1")
api("org.quartz-scheduler:quartz:2.3.2")
api("org.seleniumhq.selenium:htmlunit-driver:2.70.0")
api("org.seleniumhq.selenium:selenium-java:3.141.59")
api("org.skyscreamer:jsonassert:1.5.0")
api("org.slf4j:slf4j-api:2.0.6")
api("org.testng:testng:7.7.1")
api("org.webjars:underscorejs:1.8.3")
api("org.webjars:webjars-locator-core:0.52")
api("org.xmlunit:xmlunit-assertj:2.9.1")
api("org.xmlunit:xmlunit-matchers:2.9.1")
api("org.yaml:snakeyaml:1.33")
}
}
+2 -6
View File
@@ -1,9 +1,5 @@
version=6.0.6
org.gradle.caching=true
version=5.3.27
org.gradle.jvmargs=-Xmx2048m
org.gradle.caching=true
org.gradle.parallel=true
kotlinVersion=1.7.21
kotlin.stdlib.default.dependency=false
+1 -1
View File
@@ -20,7 +20,7 @@ tasks.findByName("dokkaHtmlPartial")?.configure {
url.set(new URL("https://javadoc.io/doc/org.hamcrest/hamcrest/2.1/"))
}
externalDocumentationLink {
url.set(new URL("https://javadoc.io/doc/jakarta.servlet/jakarta.servlet-api/latest/"))
url.set(new URL("https://javadoc.io/doc/javax.servlet/javax.servlet-api/latest/"))
}
externalDocumentationLink {
url.set(new URL("https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/"))
@@ -1,34 +1,18 @@
description = "Spring Framework Docs"
apply plugin: 'kotlin'
apply plugin: 'org.asciidoctor.jvm.convert'
apply plugin: 'org.asciidoctor.jvm.pdf'
apply from: "${rootDir}/gradle/publications.gradle"
configurations {
asciidoctorExtensions
asciidoctorExt
}
dependencies {
api(project(":spring-context"))
api(project(":spring-web"))
api("jakarta.servlet:jakarta.servlet-api")
implementation(project(":spring-core-test"))
implementation("org.assertj:assertj-core")
asciidoctorExt("io.spring.asciidoctor:spring-asciidoctor-extensions-block-switch:0.5.0")
}
jar {
enabled = false
}
javadoc {
enabled = false
}
dependencies {
asciidoctorExtensions "io.spring.asciidoctor.backends:spring-asciidoctor-backends:0.0.5"
repositories {
maven {
url "https://repo.spring.io/release"
mavenContent {
includeGroup "io.spring.asciidoctor"
}
}
}
/**
@@ -58,11 +42,15 @@ task api(type: Javadoc) {
author = true
header = rootProject.description
use = true
overview = "framework-docs/src/docs/api/overview.html"
overview = "src/docs/api/overview.html"
stylesheetFile = file("src/docs/api/stylesheet.css")
splitIndex = true
links(project.ext.javadocLinks)
addBooleanOption('Xdoclint:syntax', true) // only check syntax with doclint
addBooleanOption('Werror', true) // fail build on Javadoc warnings
addBooleanOption('Xwerror', true) // fail build on Javadoc warnings
if (JavaVersion.current().isJava9Compatible()) {
addBooleanOption('html5', true)
}
}
source moduleProjects.collect { project ->
project.sourceSets.main.allJava
@@ -74,77 +62,95 @@ task api(type: Javadoc) {
/**
* Produce KDoc for all Spring Framework modules in "build/docs/kdoc"
*/
rootProject.tasks.dokkaHtmlMultiModule.configure {
dependsOn {
tasks.getByName("api")
pluginManager.withPlugin("kotlin") {
tasks.dokkaHtmlMultiModule.configure {
dependsOn {
tasks.getByName("api")
}
moduleName.set("spring-framework")
outputDirectory.set(project.file("$buildDir/docs/kdoc"))
}
moduleName.set("spring-framework")
outputDirectory.set(project.file("$buildDir/docs/kdoc"))
}
task downloadResources(type: Download) {
def version = "0.2.5"
src "https://repo.spring.io/release/io/spring/docresources/" +
"spring-doc-resources/$version/spring-doc-resources-${version}.zip"
dest project.file("$buildDir/docs/spring-doc-resources.zip")
onlyIfModified true
useETag "all"
}
task extractDocResources(type: Copy, dependsOn: downloadResources) {
from project.zipTree(downloadResources.dest);
into "$buildDir/docs/spring-docs-resources/"
}
asciidoctorj {
version = '2.4.3'
def docRoot = 'https://docs.spring.io'
def docsSpringFramework = "${docRoot}/spring-framework/docs/${project.version}"
version = '2.4.1'
fatalWarnings ".*"
options doctype: 'book', eruby: 'erubis'
attributes([
icons: 'font',
idprefix: '',
idseparator: '-',
docinfo: 'shared,private-header', // https://docs.asciidoctor.org/asciidoctor/latest/docinfo/
revnumber: project.version,
sectanchors: '',
sectnums: '',
'spring-version': project.version
'source-highlighter': 'highlight.js',
highlightjsdir: 'js/highlight',
'highlightjs-theme': 'googlecode',
stylesdir: 'css/',
stylesheet: 'stylesheet.css',
'spring-version': project.version,
'spring-framework-main-code': 'https://github.com/spring-projects/spring-framework/tree/main',
'doc-root': docRoot,
'docs-spring-framework': docsSpringFramework,
'api-spring-framework': "${docsSpringFramework}/javadoc-api/org/springframework"
])
}
/**
* Generate the Spring Framework Reference documentation from
* "src/docs/asciidoc" in "build/docs/ref-docs/html5".
* Generate the Spring Framework Reference documentation from "src/docs/asciidoc"
* in "build/docs/ref-docs/html5".
*/
asciidoctor {
baseDirFollowsSourceDir()
configurations "asciidoctorExtensions"
configurations 'asciidoctorExt'
sources {
include '*.adoc'
}
outputDir "$buildDir/docs/ref-docs/html5"
logDocuments = true
resources {
from(sourceDir) {
include 'images/*.png'
include 'images/*.png', 'css/**', 'js/**'
}
from extractDocResources
}
outputDir "$buildDir/docs/ref-docs/html5"
outputOptions {
backends "spring-html"
}
forkOptions {
jvmArgs += ["--add-opens", "java.base/sun.nio.ch=ALL-UNNAMED", "--add-opens", "java.base/java.io=ALL-UNNAMED"]
}
logDocuments = true
}
asciidoctor.mustRunAfter "check"
/**
* Generate the Spring Framework Reference documentation from "src/docs/asciidoc"
* in "build/docs/ref-docs/pdf".
*/
asciidoctorPdf {
baseDirFollowsSourceDir()
configurations 'asciidoctorExtensions'
configurations 'asciidoctorExt'
sources {
include 'spring-framework.adocbook'
include '*.adoc'
}
outputDir "$buildDir/docs/ref-docs/pdf"
forkOptions {
jvmArgs += ["--add-opens", "java.base/sun.nio.ch=ALL-UNNAMED", "--add-opens", "java.base/java.io=ALL-UNNAMED"]
}
logDocuments = true
}
/**
* Zip all docs (API and reference) into a single archive
*/
task docsZip(type: Zip, dependsOn: ['api', 'asciidoctor', 'asciidoctorPdf', rootProject.tasks.dokkaHtmlMultiModule]) {
task docsZip(type: Zip, dependsOn: ['api', 'asciidoctor', 'asciidoctorPdf', 'dokkaHtmlMultiModule']) {
group = "Distribution"
description = "Builds -${archiveClassifier} archive containing api and reference " +
"for deployment at https://docs.spring.io/spring-framework/docs/."
@@ -163,7 +169,7 @@ task docsZip(type: Zip, dependsOn: ['api', 'asciidoctor', 'asciidoctorPdf', root
from ("$asciidoctorPdf.outputDir") {
into "reference/pdf"
}
from (rootProject.tasks.dokkaHtmlMultiModule.outputDirectory) {
from (dokkaHtmlMultiModule.outputDirectory) {
into "kdoc-api"
}
}
@@ -242,13 +248,3 @@ task distZip(type: Zip, dependsOn: [docsZip, schemaZip]) {
}
distZip.mustRunAfter moduleProjects.check
publishing {
publications {
mavenJava(MavenPublication) {
artifact docsZip
artifact schemaZip
artifact distZip
}
}
}
+4 -4
View File
@@ -4,8 +4,8 @@ import org.gradle.plugins.ide.eclipse.model.SourceFolder
apply plugin: 'eclipse'
eclipse.jdt {
sourceCompatibility = 17
targetCompatibility = 17
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
// Replace classpath entries with project dependencies (GRADLE-1116)
@@ -30,7 +30,7 @@ eclipse.classpath.file.whenMerged { classpath ->
}
// Remove any remaining direct depencencies on JARs in the build/libs folder
// except the repack JARs.
// except Spring's spring-cglib-repack and spring-objenesis-repack JARs.
classpath.entries.removeAll { entry -> (entry.path =~ /(?!.*?repack.*\.jar).*?\/([^\/]+)\/build\/libs\/[^\/]+\.jar/) }
}
@@ -64,7 +64,7 @@ eclipse.classpath.file.whenMerged {
// so that they can see test fixtures.
// https://github.com/melix/jmh-gradle-plugin/issues/157
eclipse.classpath.file.whenMerged {
entries.findAll { it.path =~ /src\/jmh\/(java|kotlin|resources)/ }.each {
entries.findAll { it.path =~ /src\/jmh\/(java|resources)/ }.each {
it.entryAttributes['test'] = 'true'
}
}
+4 -9
View File
@@ -1,5 +1,5 @@
apply plugin: 'java-library'
apply plugin: 'org.springframework.build.conventions'
apply plugin: 'org.springframework.build.compile'
apply plugin: 'org.springframework.build.optional-dependencies'
// Uncomment the following for Shadow support in the jmhJar block.
// Currently commented out due to ZipException: archive is not a ZIP archive
@@ -8,16 +8,11 @@ apply plugin: 'me.champeau.jmh'
apply from: "$rootDir/gradle/publications.gradle"
dependencies {
jmh 'org.openjdk.jmh:jmh-core:1.36'
jmh 'org.openjdk.jmh:jmh-generator-annprocess:1.36'
jmh 'org.openjdk.jmh:jmh-core:1.32'
jmh 'org.openjdk.jmh:jmh-generator-annprocess:1.32'
jmh 'net.sf.jopt-simple:jopt-simple'
}
pluginManager.withPlugin("kotlin") {
apply plugin: "org.jetbrains.dokka"
apply from: "${rootDir}/gradle/docs-dokka.gradle"
}
jmh {
duplicateClassesStrategy = DuplicatesStrategy.EXCLUDE
}
@@ -49,7 +44,7 @@ jar {
manifest.attributes["Created-By"] =
"${System.getProperty("java.version")} (${System.getProperty("java.specification.vendor")})"
from("${rootDir}/framework-docs/src/docs/dist") {
from("${rootDir}/src/docs/dist") {
include "license.txt"
include "notice.txt"
into "META-INF"
+49 -21
View File
@@ -5,13 +5,13 @@
* One can choose the toolchain to use for compiling the MAIN sources and/or compiling
* and running the TEST sources. These options apply to Java, Kotlin and Groovy sources
* when available.
* {@code "./gradlew check -PmainToolchain=17 -PtestToolchain=19"} will use:
* {@code "./gradlew check -PmainToolchain=8 -PtestToolchain=11"} will use:
* <ul>
* <li>a JDK17 toolchain for compiling the main SourceSet
* <li>a JDK19 toolchain for compiling and running the test SourceSet
* <li>a JDK8 toolchain for compiling the main SourceSet
* <li>a JDK11 toolchain for compiling and running the test SourceSet
* </ul>
*
* By default, the build will fall back to using the current JDK and 17 language level for all sourceSets.
* By default, the build will fall back to using the current JDK and 1.8 language level for all sourceSets.
*
* Gradle will automatically detect JDK distributions in well-known locations.
* The following command will list the detected JDKs on the host.
@@ -21,11 +21,11 @@
*
* We can also configure ENV variables and let Gradle know about them:
* {@code
* $ echo JDK17
* /opt/openjdk/java17
* $ echo JDK19
* /opt/openjdk/java18
* $ ./gradlew -Porg.gradle.java.installations.fromEnv=JDK17,JDK19 check
* $ echo JDK11
* /opt/openjdk/java11
* $ echo JDK15
* /opt/openjdk/java15
* $ ./gradlew -Porg.gradle.java.installations.fromEnv=JDK11,JDK15 check
* }
*
* @author Brian Clozel
@@ -44,7 +44,7 @@ def mainToolchainLanguageVersion() {
if (mainToolchainConfigured()) {
return JavaLanguageVersion.of(project.mainToolchain.toString())
}
return JavaLanguageVersion.of(17)
return JavaLanguageVersion.of(8)
}
def testToolchainLanguageVersion() {
@@ -64,9 +64,9 @@ plugins.withType(JavaPlugin) {
}
}
else {
// Fallback to JDK17
// Fallback to JDK8
java {
sourceCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_1_8
}
}
// Configure a specific Java Toolchain for compiling and running tests if the 'testToolchain' property is defined
@@ -81,29 +81,57 @@ plugins.withType(JavaPlugin) {
javaLauncher = javaToolchains.launcherFor {
languageVersion = testLanguageVersion
}
if(testLanguageVersion == JavaLanguageVersion.of(17)) {
jvmArgs(["--add-opens=java.base/java.lang=ALL-UNNAMED",
"--add-opens=java.base/java.util=ALL-UNNAMED"])
}
}
}
}
plugins.withType(GroovyPlugin) {
// Fallback to JDK17
// Fallback to JDK8
if (!mainToolchainConfigured()) {
compileGroovy {
sourceCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_1_8
}
}
}
pluginManager.withPlugin("kotlin") {
// Fallback to JDK17
compileKotlin {
kotlinOptions {
jvmTarget = '17'
// Configure the Kotlin compiler if the 'mainToolchain' property is defined
if (mainToolchainConfigured()) {
def mainLanguageVersion = mainToolchainLanguageVersion()
def compiler = javaToolchains.compilerFor {
languageVersion = mainLanguageVersion
}
// See https://kotlinlang.org/docs/gradle.html#attributes-specific-for-jvm
def javaVersion = mainLanguageVersion.toString() == '8' ? '1.8' : mainLanguageVersion.toString()
compileKotlin {
kotlinOptions {
jvmTarget = javaVersion
jdkHome = compiler.get().metadata.installationPath.asFile.absolutePath
}
}
// Compile the test classes with the same version, 'testToolchain' will override if defined
compileTestKotlin {
kotlinOptions {
jvmTarget = javaVersion
jdkHome = compiler.get().metadata.installationPath.asFile.absolutePath
}
}
}
compileTestKotlin {
kotlinOptions {
jvmTarget = '17'
else {
// Fallback to JDK8
compileKotlin {
kotlinOptions {
jvmTarget = '1.8'
}
}
compileTestKotlin {
kotlinOptions {
jvmTarget = '1.8'
}
}
}
}
Binary file not shown.
+1 -2
View File
@@ -1,6 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
networkTimeout=10000
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+4 -14
View File
@@ -55,7 +55,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -80,11 +80,11 @@ do
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
@@ -143,16 +143,12 @@ fi
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -209,12 +205,6 @@ set -- \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
Vendored
+6 -9
View File
@@ -14,7 +14,7 @@
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -25,8 +25,7 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -41,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -76,15 +75,13 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
+4 -5
View File
@@ -3,7 +3,7 @@
This document will guide you through the process of importing the Spring Framework
projects into Eclipse or the Spring Tool Suite (_STS_). It is recommended that you
have a recent version of Eclipse. As a bare minimum you will need Eclipse with full Java
17 support and Eclipse Buildship.
8 support, Eclipse Buildship, and the Groovy plugin.
The following instructions have been tested against [STS](https://spring.io/tools) 4.12.0
([download](https://github.com/spring-projects/sts4/wiki/Previous-Versions#spring-tools-4120-changelog))
@@ -12,14 +12,13 @@ The instructions should work with the latest Eclipse distribution as long as you
[Buildship](https://marketplace.eclipse.org/content/buildship-gradle-integration). Note
that STS 4 comes with Buildship preinstalled.
If you are using Eclipse 4.21, you will need to install
[Java 17 Support for Eclipse 2021-09 (4.21)](https://marketplace.eclipse.org/content/java-17-support-eclipse-2021-09-421)
from the Eclipse Marketplace.
## Steps
_When instructed to execute `./gradlew` from the command line, be sure to execute it within your locally cloned `spring-framework` working directory._
1. Install the [Groovy Development Tools](https://marketplace.eclipse.org/content/groovy-development-tools).
1. Switch to Groovy 3.0 in Eclipse (Preferences &#8594; Groovy &#8594; Compiler &#8594; Switch to 3.0...).
- If you encounter build errors stating something similar to _"Groovy: compiler mismatch: project level is 2.5, workspace level is 3.0"_, change the Groovy compiler version to 3.0 for each affected project.
1. Ensure that the _Forbidden reference (access rule)_ in Eclipse is set to `Info`
(Preferences &#8594; Java &#8594; Compiler &#8594; Errors/Warnings &#8594; Deprecated and restricted API &#8594; Forbidden reference (access rule)).
1. Optionally install the [Kotlin Plugin for Eclipse](https://marketplace.eclipse.org/content/kotlin-plugin-eclipse) if you need to execute Kotlin-based tests or develop Kotlin extensions.
+4 -9
View File
@@ -1,7 +1,3 @@
plugins {
id 'org.springframework.build.runtimehints-agent'
}
description = "Spring Integration Tests"
dependencies {
@@ -9,7 +5,6 @@ dependencies {
testImplementation(project(":spring-beans"))
testImplementation(project(":spring-context"))
testImplementation(project(":spring-core"))
testImplementation(project(":spring-core-test"))
testImplementation(testFixtures(project(":spring-aop")))
testImplementation(testFixtures(project(":spring-beans")))
testImplementation(testFixtures(project(":spring-core")))
@@ -20,12 +15,12 @@ dependencies {
testImplementation(project(":spring-test"))
testImplementation(project(":spring-tx"))
testImplementation(project(":spring-web"))
testImplementation("jakarta.inject:jakarta.inject-api")
testImplementation("jakarta.resource:jakarta.resource-api")
testImplementation("jakarta.servlet:jakarta.servlet-api")
testImplementation("javax.inject:javax.inject")
testImplementation("javax.resource:javax.resource-api")
testImplementation("javax.servlet:javax.servlet-api")
testImplementation("org.aspectj:aspectjweaver")
testImplementation("org.hsqldb:hsqldb")
testImplementation("org.hibernate:hibernate-core-jakarta")
testImplementation("org.hibernate:hibernate-core")
}
normalization {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2019 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.
@@ -34,7 +34,7 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for scoped proxy use in conjunction with aop: namespace.
* Deemed an integration test because web mocks and application contexts are required.
* Deemed an integration test because .web mocks and application contexts are required.
*
* @author Rob Harrop
* @author Juergen Hoeller
@@ -20,7 +20,8 @@ import java.io.IOException;
import java.lang.reflect.Method;
import java.util.List;
import jakarta.servlet.ServletException;
import javax.servlet.ServletException;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
@@ -1,297 +0,0 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aot;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.ResourceBundle;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.aot.agent.HintType;
import org.springframework.aot.agent.MethodReference;
import org.springframework.aot.agent.RecordedInvocation;
import org.springframework.aot.agent.RecordedInvocationsListener;
import org.springframework.aot.agent.RecordedInvocationsPublisher;
import org.springframework.aot.agent.RuntimeHintsAgent;
import org.springframework.aot.test.agent.EnabledIfRuntimeHintsAgent;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link RuntimeHintsAgent}.
*
* @author Brian Clozel
*/
@EnabledIfRuntimeHintsAgent
public class RuntimeHintsAgentTests {
private static final ClassLoader classLoader = ClassLoader.getSystemClassLoader();
private static Constructor<String> defaultConstructor;
private static Method toStringMethod;
private static Method privateGreetMethod;
@BeforeAll
public static void classSetup() throws NoSuchMethodException {
defaultConstructor = String.class.getConstructor();
toStringMethod = String.class.getMethod("toString");
privateGreetMethod = PrivateClass.class.getDeclaredMethod("greet");
}
@ParameterizedTest
@MethodSource("instrumentedReflectionMethods")
void shouldInstrumentReflectionMethods(Runnable runnable, MethodReference methodReference) {
RecordingSession session = RecordingSession.record(runnable);
assertThat(session.recordedInvocations()).hasSize(1);
RecordedInvocation invocation = session.recordedInvocations().findFirst().get();
assertThat(invocation.getMethodReference()).isEqualTo(methodReference);
assertThat(invocation.getStackFrames()).first().matches(frame -> frame.getClassName().equals(RuntimeHintsAgentTests.class.getName()));
}
private static Stream<Arguments> instrumentedReflectionMethods() {
return Stream.of(
Arguments.of((Runnable) () -> {
try {
Class.forName("java.lang.String");
}
catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}, MethodReference.of(Class.class, "forName")),
Arguments.of((Runnable) () -> String.class.getClasses(), MethodReference.of(Class.class, "getClasses")),
Arguments.of((Runnable) () -> {
try {
String.class.getConstructor();
}
catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}, MethodReference.of(Class.class, "getConstructor")),
Arguments.of((Runnable) () -> String.class.getConstructors(), MethodReference.of(Class.class, "getConstructors")),
Arguments.of((Runnable) () -> String.class.getDeclaredClasses(), MethodReference.of(Class.class, "getDeclaredClasses")),
Arguments.of((Runnable) () -> {
try {
String.class.getDeclaredConstructor();
}
catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}, MethodReference.of(Class.class, "getDeclaredConstructor")),
Arguments.of((Runnable) () -> String.class.getDeclaredConstructors(), MethodReference.of(Class.class, "getDeclaredConstructors")),
Arguments.of((Runnable) () -> {
try {
String.class.getDeclaredField("value");
}
catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}, MethodReference.of(Class.class, "getDeclaredField")),
Arguments.of((Runnable) () -> String.class.getDeclaredFields(), MethodReference.of(Class.class, "getDeclaredFields")),
Arguments.of((Runnable) () -> {
try {
String.class.getDeclaredMethod("toString");
}
catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}, MethodReference.of(Class.class, "getDeclaredMethod")),
Arguments.of((Runnable) () -> String.class.getDeclaredMethods(), MethodReference.of(Class.class, "getDeclaredMethods")),
Arguments.of((Runnable) () -> {
try {
String.class.getField("value");
}
catch (NoSuchFieldException e) {
}
}, MethodReference.of(Class.class, "getField")),
Arguments.of((Runnable) () -> String.class.getFields(), MethodReference.of(Class.class, "getFields")),
Arguments.of((Runnable) () -> {
try {
String.class.getMethod("toString");
}
catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}, MethodReference.of(Class.class, "getMethod")),
Arguments.of((Runnable) () -> String.class.getMethods(), MethodReference.of(Class.class, "getMethods")),
Arguments.of((Runnable) () -> {
try {
classLoader.loadClass("java.lang.String");
}
catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}, MethodReference.of(ClassLoader.class, "loadClass")),
Arguments.of((Runnable) () -> {
try {
defaultConstructor.newInstance();
}
catch (Exception e) {
throw new RuntimeException(e);
}
}, MethodReference.of(Constructor.class, "newInstance")),
Arguments.of((Runnable) () -> {
try {
toStringMethod.invoke("");
}
catch (Exception e) {
throw new RuntimeException(e);
}
}, MethodReference.of(Method.class, "invoke")),
Arguments.of((Runnable) () -> {
try {
privateGreetMethod.invoke(new PrivateClass());
}
catch (Exception e) {
throw new RuntimeException(e);
}
}, MethodReference.of(Method.class, "invoke"))
);
}
@ParameterizedTest
@MethodSource("instrumentedResourceBundleMethods")
void shouldInstrumentResourceBundleMethods(Runnable runnable, MethodReference methodReference) {
RecordingSession session = RecordingSession.record(runnable);
assertThat(session.recordedInvocations(HintType.RESOURCE_BUNDLE)).hasSize(1);
RecordedInvocation resolution = session.recordedInvocations(HintType.RESOURCE_BUNDLE).findFirst().get();
assertThat(resolution.getMethodReference()).isEqualTo(methodReference);
assertThat(resolution.getStackFrames()).first().matches(frame -> frame.getClassName().equals(RuntimeHintsAgentTests.class.getName()));
}
private static Stream<Arguments> instrumentedResourceBundleMethods() {
return Stream.of(
Arguments.of((Runnable) () -> {
try {
ResourceBundle.getBundle("testBundle");
}
catch (Throwable exc) {
}
},
MethodReference.of(ResourceBundle.class, "getBundle"))
);
}
@ParameterizedTest
@MethodSource("instrumentedResourcePatternMethods")
void shouldInstrumentResourcePatternMethods(Runnable runnable, MethodReference methodReference) {
RecordingSession session = RecordingSession.record(runnable);
assertThat(session.recordedInvocations(HintType.RESOURCE_PATTERN)).hasSize(1);
RecordedInvocation resolution = session.recordedInvocations(HintType.RESOURCE_PATTERN).findFirst().get();
assertThat(resolution.getMethodReference()).isEqualTo(methodReference);
assertThat(resolution.getStackFrames()).first().matches(frame -> frame.getClassName().equals(RuntimeHintsAgentTests.class.getName()));
}
private static Stream<Arguments> instrumentedResourcePatternMethods() {
return Stream.of(
Arguments.of((Runnable) () -> RuntimeHintsAgentTests.class.getResource("sample.txt"),
MethodReference.of(Class.class, "getResource")),
Arguments.of((Runnable) () -> RuntimeHintsAgentTests.class.getResourceAsStream("sample.txt"),
MethodReference.of(Class.class, "getResourceAsStream")),
Arguments.of((Runnable) () -> classLoader.getResource("sample.txt"),
MethodReference.of(ClassLoader.class, "getResource")),
Arguments.of((Runnable) () -> classLoader.getResourceAsStream("sample.txt"),
MethodReference.of(ClassLoader.class, "getResourceAsStream")),
Arguments.of((Runnable) () -> {
try {
classLoader.getResources("sample.txt");
}
catch (IOException e) {
}
},
MethodReference.of(ClassLoader.class, "getResources")),
Arguments.of((Runnable) () -> {
try {
RuntimeHintsAgentTests.class.getModule().getResourceAsStream("sample.txt");
}
catch (IOException e) {
}
},
MethodReference.of(Module.class, "getResourceAsStream")),
Arguments.of((Runnable) () -> classLoader.resources("sample.txt"),
MethodReference.of(ClassLoader.class, "resources"))
);
}
@Test
void shouldInstrumentStaticMethodHandle() {
RecordingSession session = RecordingSession.record(ClassLoader.class::getClasses);
assertThat(session.recordedInvocations(HintType.REFLECTION)).hasSize(1);
RecordedInvocation resolution = session.recordedInvocations(HintType.REFLECTION).findFirst().get();
assertThat(resolution.getMethodReference()).isEqualTo(MethodReference.of(Class.class, "getClasses"));
assertThat(resolution.getStackFrames()).first().extracting(StackWalker.StackFrame::getClassName)
.isEqualTo(RuntimeHintsAgentTests.class.getName() + "$RecordingSession");
}
static class RecordingSession implements RecordedInvocationsListener {
final Deque<RecordedInvocation> recordedInvocations = new ArrayDeque<>();
static RecordingSession record(Runnable action) {
RecordingSession session = new RecordingSession();
RecordedInvocationsPublisher.addListener(session);
try {
action.run();
}
finally {
RecordedInvocationsPublisher.removeListener(session);
}
return session;
}
@Override
public void onInvocation(RecordedInvocation invocation) {
this.recordedInvocations.addLast(invocation);
}
Stream<RecordedInvocation> recordedInvocations() {
return this.recordedInvocations.stream();
}
Stream<RecordedInvocation> recordedInvocations(HintType hintType) {
return recordedInvocations().filter(invocation -> invocation.getHintType() == hintType);
}
}
private static class PrivateClass {
@SuppressWarnings("unused")
private String greet() {
return "hello";
}
}
}
@@ -1,57 +0,0 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aot.test;
import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.test.agent.EnabledIfRuntimeHintsAgent;
import org.springframework.aot.test.agent.RuntimeHintsInvocations;
import org.springframework.aot.test.agent.RuntimeHintsRecorder;
import static org.assertj.core.api.Assertions.assertThat;
@EnabledIfRuntimeHintsAgent
class ReflectionInvocationsTests {
@Test
void sampleTest() {
RuntimeHints hints = new RuntimeHints();
hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_PUBLIC_METHODS);
RuntimeHintsInvocations invocations = RuntimeHintsRecorder.record(() -> {
SampleReflection sample = new SampleReflection();
sample.sample(); // does Method[] methods = String.class.getMethods();
});
assertThat(invocations).match(hints);
}
@Test
void multipleCallsTest() {
RuntimeHints hints = new RuntimeHints();
hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_PUBLIC_METHODS);
hints.reflection().registerType(Integer.class,MemberCategory.INTROSPECT_PUBLIC_METHODS);
RuntimeHintsInvocations invocations = RuntimeHintsRecorder.record(() -> {
SampleReflection sample = new SampleReflection();
sample.multipleCalls(); // does Method[] methods = String.class.getMethods(); methods = Integer.class.getMethods();
});
assertThat(invocations).match(hints);
}
}
@@ -1,40 +0,0 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aot.test;
import java.lang.reflect.Method;
/**
* @author Brian Clozel
* @since 6.0
*/
public class SampleReflection {
@SuppressWarnings("unused")
public void sample() {
String value = "Sample";
Method[] methods = String.class.getMethods();
}
@SuppressWarnings("unused")
public void multipleCalls() {
String value = "Sample";
Method[] methods = String.class.getMethods();
methods = Integer.class.getMethods();
}
}
@@ -61,7 +61,7 @@ class EnableCachingIntegrationTests {
ctx.register(Config.class, AspectJCacheConfig.class);
// this test is a bit fragile, but gets the job done, proving that an
// attempt was made to look up the AJ aspect. It's due to classpath issues
// in integration-tests that it's not found.
// in .integration-tests that it's not found.
assertThatException().isThrownBy(ctx::refresh)
.withMessageContaining("AspectJCachingConfiguration");
}
@@ -21,8 +21,9 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import jakarta.inject.Named;
import jakarta.inject.Singleton;
import javax.inject.Named;
import javax.inject.Singleton;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -307,18 +308,19 @@ class ClassPathBeanDefinitionScannerJsr330ScopeIntegrationTests {
scanner.setIncludeAnnotationConfig(false);
scanner.setScopeMetadataResolver(definition -> {
ScopeMetadata metadata = new ScopeMetadata();
if (definition instanceof AnnotatedBeanDefinition annDef) {
if (definition instanceof AnnotatedBeanDefinition) {
AnnotatedBeanDefinition annDef = (AnnotatedBeanDefinition) definition;
for (String type : annDef.getMetadata().getAnnotationTypes()) {
if (type.equals(jakarta.inject.Singleton.class.getName())) {
if (type.equals(javax.inject.Singleton.class.getName())) {
metadata.setScopeName(BeanDefinition.SCOPE_SINGLETON);
break;
}
else if (annDef.getMetadata().getMetaAnnotationTypes(type).contains(jakarta.inject.Scope.class.getName())) {
else if (annDef.getMetadata().getMetaAnnotationTypes(type).contains(javax.inject.Scope.class.getName())) {
metadata.setScopeName(type.substring(type.length() - 13, type.length() - 6).toLowerCase());
metadata.setScopedProxyMode(scopedProxyMode);
break;
}
else if (type.startsWith("jakarta.inject")) {
else if (type.startsWith("javax.inject")) {
metadata.setScopeName(BeanDefinition.SCOPE_PROTOTYPE);
}
}
@@ -385,14 +387,14 @@ class ClassPathBeanDefinitionScannerJsr330ScopeIntegrationTests {
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@jakarta.inject.Scope
@javax.inject.Scope
public @interface RequestScoped {
}
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@jakarta.inject.Scope
@javax.inject.Scope
public @interface SessionScoped {
}
@@ -41,6 +41,9 @@ import org.springframework.context.support.GenericApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jca.context.ResourceAdapterApplicationContext;
import org.springframework.jca.support.SimpleBootstrapContext;
import org.springframework.jca.work.SimpleTaskWorkManager;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.mock.env.MockPropertySource;
import org.springframework.mock.web.MockServletConfig;
@@ -532,6 +535,22 @@ public class EnvironmentSystemIntegrationTests {
assertThat(environment.getProperty("pSysProps1")).isEqualTo("pSysProps1Value");
}
@Test
void resourceAdapterApplicationContext() {
ResourceAdapterApplicationContext ctx = new ResourceAdapterApplicationContext(new SimpleBootstrapContext(new SimpleTaskWorkManager()));
assertHasStandardEnvironment(ctx);
registerEnvironmentBeanDefinition(ctx);
ctx.setEnvironment(prodEnv);
ctx.refresh();
assertHasEnvironment(ctx, prodEnv);
assertEnvironmentBeanRegistered(ctx);
assertEnvironmentAwareInvoked(ctx, prodEnv);
}
@Test
void abstractApplicationContextValidatesRequiredPropertiesOnRefresh() {
{
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2022 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.
@@ -151,7 +151,7 @@ class ScheduledAndTransactionalAnnotationIntegrationTests {
@Bean
PersistenceExceptionTranslator peTranslator() {
return mock();
return mock(PersistenceExceptionTranslator.class);
}
@Bean
@@ -97,7 +97,7 @@ class EnableTransactionManagementIntegrationTests {
ctx.register(Config.class, AspectJTxConfig.class);
// this test is a bit fragile, but gets the job done, proving that an
// attempt was made to look up the AJ aspect. It's due to classpath issues
// in integration-tests that it's not found.
// in .integration-tests that it's not found.
assertThatException()
.isThrownBy(ctx::refresh)
.withMessageContaining("AspectJJtaTransactionManagementConfiguration");
@@ -41,7 +41,7 @@
<props>
<prop key="setA*">PROPAGATION_REQUIRED</prop>
<prop key="rollbackOnly">PROPAGATION_REQUIRED</prop>
<prop key="echoException">PROPAGATION_REQUIRED,+jakarta.servlet.ServletException,-java.lang.Exception</prop>
<prop key="echoException">PROPAGATION_REQUIRED,+javax.servlet.ServletException,-java.lang.Exception</prop>
</props>
</property>
</bean>
+1 -4
View File
@@ -7,7 +7,7 @@ pluginManagement {
}
plugins {
id "com.gradle.enterprise" version "3.12.3"
id "com.gradle.enterprise" version "3.12.1"
id "io.spring.ge.conventions" version "0.0.13"
}
@@ -18,7 +18,6 @@ include "spring-context"
include "spring-context-indexer"
include "spring-context-support"
include "spring-core"
include "spring-core-test"
include "spring-expression"
include "spring-instrument"
include "spring-jcl"
@@ -35,8 +34,6 @@ include "spring-webflux"
include "spring-webmvc"
include "spring-websocket"
include "framework-bom"
include "framework-docs"
include "framework-platform"
include "integration-tests"
rootProject.name = "spring"
+1 -1
View File
@@ -5,7 +5,7 @@ dependencies {
api(project(":spring-core"))
optional("org.aspectj:aspectjweaver")
optional("org.apache.commons:commons-pool2")
testImplementation(project(":spring-core-test"))
optional("com.jamonapi:jamon")
testImplementation(testFixtures(project(":spring-beans")))
testImplementation(testFixtures(project(":spring-core")))
testFixturesImplementation(testFixtures(project(":spring-beans")))
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2022 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.
@@ -78,9 +78,10 @@ public abstract class AbstractAspectJAdvice implements Advice, AspectJPrecedence
*/
public static JoinPoint currentJoinPoint() {
MethodInvocation mi = ExposeInvocationInterceptor.currentInvocation();
if (!(mi instanceof ProxyMethodInvocation pmi)) {
if (!(mi instanceof ProxyMethodInvocation)) {
throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi);
}
ProxyMethodInvocation pmi = (ProxyMethodInvocation) mi;
JoinPoint jp = (JoinPoint) pmi.getUserAttribute(JOIN_POINT_KEY);
if (jp == null) {
jp = new MethodInvocationProceedingJoinPoint(pmi);
@@ -261,23 +262,25 @@ public abstract class AbstractAspectJAdvice implements Advice, AspectJPrecedence
public void setArgumentNamesFromStringArray(String... args) {
this.argumentNames = new String[args.length];
for (int i = 0; i < args.length; i++) {
this.argumentNames[i] = args[i].strip();
this.argumentNames[i] = StringUtils.trimWhitespace(args[i]);
if (!isVariableName(this.argumentNames[i])) {
throw new IllegalArgumentException(
"'argumentNames' property of AbstractAspectJAdvice contains an argument name '" +
this.argumentNames[i] + "' that is not a valid Java identifier");
}
}
if (this.aspectJAdviceMethod.getParameterCount() == this.argumentNames.length + 1) {
// May need to add implicit join point arg name...
Class<?> firstArgType = this.aspectJAdviceMethod.getParameterTypes()[0];
if (firstArgType == JoinPoint.class ||
firstArgType == ProceedingJoinPoint.class ||
firstArgType == JoinPoint.StaticPart.class) {
String[] oldNames = this.argumentNames;
this.argumentNames = new String[oldNames.length + 1];
this.argumentNames[0] = "THIS_JOIN_POINT";
System.arraycopy(oldNames, 0, this.argumentNames, 1, oldNames.length);
if (this.argumentNames != null) {
if (this.aspectJAdviceMethod.getParameterCount() == this.argumentNames.length + 1) {
// May need to add implicit join point arg name...
Class<?> firstArgType = this.aspectJAdviceMethod.getParameterTypes()[0];
if (firstArgType == JoinPoint.class ||
firstArgType == ProceedingJoinPoint.class ||
firstArgType == JoinPoint.StaticPart.class) {
String[] oldNames = this.argumentNames;
this.argumentNames = new String[oldNames.length + 1];
this.argumentNames[0] = "THIS_JOIN_POINT";
System.arraycopy(oldNames, 0, this.argumentNames, 1, oldNames.length);
}
}
}
}
@@ -653,10 +656,10 @@ public abstract class AbstractAspectJAdvice implements Advice, AspectJPrecedence
@Nullable
protected JoinPointMatch getJoinPointMatch() {
MethodInvocation mi = ExposeInvocationInterceptor.currentInvocation();
if (!(mi instanceof ProxyMethodInvocation pmi)) {
if (!(mi instanceof ProxyMethodInvocation)) {
throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi);
}
return getJoinPointMatch(pmi);
return getJoinPointMatch((ProxyMethodInvocation) mi);
}
// Note: We can't use JoinPointMatch.getClass().getName() as the key, since
@@ -711,9 +714,10 @@ public abstract class AbstractAspectJAdvice implements Advice, AspectJPrecedence
if (this == other) {
return true;
}
if (!(other instanceof AdviceExcludingMethodMatcher otherMm)) {
if (!(other instanceof AdviceExcludingMethodMatcher)) {
return false;
}
AdviceExcludingMethodMatcher otherMm = (AdviceExcludingMethodMatcher) other;
return this.adviceMethod.equals(otherMm.adviceMethod);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2021 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.
@@ -38,14 +38,6 @@ import org.springframework.util.StringUtils;
* for an advice method from the pointcut expression, returning, and throwing clauses.
* If an unambiguous interpretation is not available, it returns {@code null}.
*
* <h3>Algorithm Summary</h3>
* <p>If an unambiguous binding can be deduced, then it is.
* If the advice requirements cannot possibly be satisfied, then {@code null}
* is returned. By setting the {@link #setRaiseExceptions(boolean) raiseExceptions}
* property to {@code true}, descriptive exceptions will be thrown instead of
* returning {@code null} in the case that the parameter names cannot be discovered.
*
* <h3>Algorithm Details</h3>
* <p>This class interprets arguments in the following way:
* <ol>
* <li>If the first parameter of the method is of type {@link JoinPoint}
@@ -73,15 +65,15 @@ import org.springframework.util.StringUtils;
* zero we proceed to the next stage. If {@code a} &gt; 1 then an
* {@code AmbiguousBindingException} is raised. If {@code a} == 1,
* and there are no unbound arguments of type {@code Annotation+},
* then an {@code IllegalArgumentException} is raised. If there is
* then an {@code IllegalArgumentException} is raised. if there is
* exactly one such argument, then the corresponding parameter name is
* assigned the value from the pointcut expression.</li>
* <li>If a {@code returningName} has been set, and there are no unbound arguments
* <li>If a returningName has been set, and there are no unbound arguments
* then an {@code IllegalArgumentException} is raised. If there is
* more than one unbound argument then an
* {@code AmbiguousBindingException} is raised. If there is exactly
* one unbound argument then the corresponding parameter name is assigned
* the value of the {@code returningName}.</li>
* the value &lt;returningName&gt;.</li>
* <li>If there remain unbound arguments, then the pointcut expression is
* examined once more for {@code this}, {@code target}, and
* {@code args} pointcut expressions used in the binding form (binding
@@ -107,12 +99,20 @@ import org.springframework.util.StringUtils;
* <p>The behavior on raising an {@code IllegalArgumentException} or
* {@code AmbiguousBindingException} is configurable to allow this discoverer
* to be used as part of a chain-of-responsibility. By default the condition will
* be logged and the {@link #getParameterNames(Method)} method will simply return
* be logged and the {@code getParameterNames(..)} method will simply return
* {@code null}. If the {@link #setRaiseExceptions(boolean) raiseExceptions}
* property is set to {@code true}, the conditions will be thrown as
* {@code IllegalArgumentException} and {@code AmbiguousBindingException},
* respectively.
*
* <p>Was that perfectly clear? ;)
*
* <p>Short version: If an unambiguous binding can be deduced, then it is.
* If the advice requirements cannot possibly be satisfied, then {@code null}
* is returned. By setting the {@link #setRaiseExceptions(boolean) raiseExceptions}
* property to {@code true}, descriptive exceptions will be thrown instead of
* returning {@code null} in the case that the parameter names cannot be discovered.
*
* @author Adrian Colyer
* @author Juergen Hoeller
* @since 2.0
@@ -132,17 +132,17 @@ public class AspectJAdviceParameterNameDiscoverer implements ParameterNameDiscov
private static final int STEP_REFERENCE_PCUT_BINDING = 7;
private static final int STEP_FINISHED = 8;
private static final Set<String> singleValuedAnnotationPcds = Set.of(
"@this",
"@target",
"@within",
"@withincode",
"@annotation");
private static final Set<String> singleValuedAnnotationPcds = new HashSet<>();
private static final Set<String> nonReferencePointcutTokens = new HashSet<>();
static {
singleValuedAnnotationPcds.add("@this");
singleValuedAnnotationPcds.add("@target");
singleValuedAnnotationPcds.add("@within");
singleValuedAnnotationPcds.add("@withincode");
singleValuedAnnotationPcds.add("@annotation");
Set<PointcutPrimitive> pointcutPrimitives = PointcutParser.getAllSupportedPointcutPrimitives();
for (PointcutPrimitive primitive : pointcutPrimitives) {
nonReferencePointcutTokens.add(primitive.getName());
@@ -158,7 +158,7 @@ public class AspectJAdviceParameterNameDiscoverer implements ParameterNameDiscov
/** The pointcut expression associated with the advice, as a simple String. */
@Nullable
private final String pointcutExpression;
private String pointcutExpression;
private boolean raiseExceptions;
@@ -197,7 +197,7 @@ public class AspectJAdviceParameterNameDiscoverer implements ParameterNameDiscov
/**
* If {@code afterReturning} advice binds the return value, the
* {@code returning} variable name must be specified.
* returning variable name must be specified.
* @param returningName the name of the returning variable
*/
public void setReturningName(@Nullable String returningName) {
@@ -206,17 +206,18 @@ public class AspectJAdviceParameterNameDiscoverer implements ParameterNameDiscov
/**
* If {@code afterThrowing} advice binds the thrown value, the
* {@code throwing} variable name must be specified.
* throwing variable name must be specified.
* @param throwingName the name of the throwing variable
*/
public void setThrowingName(@Nullable String throwingName) {
this.throwingName = throwingName;
}
/**
* Deduce the parameter names for an advice method.
* <p>See the {@link AspectJAdviceParameterNameDiscoverer class-level javadoc}
* for this class for details on the algorithm used.
* <p>See the {@link AspectJAdviceParameterNameDiscoverer class level javadoc}
* for this class for details of the algorithm used.
* @param method the target {@link Method}
* @return the parameter names
*/
@@ -243,18 +244,31 @@ public class AspectJAdviceParameterNameDiscoverer implements ParameterNameDiscov
int algorithmicStep = STEP_JOIN_POINT_BINDING;
while ((this.numberOfRemainingUnboundArguments > 0) && algorithmicStep < STEP_FINISHED) {
switch (algorithmicStep++) {
case STEP_JOIN_POINT_BINDING -> {
case STEP_JOIN_POINT_BINDING:
if (!maybeBindThisJoinPoint()) {
maybeBindThisJoinPointStaticPart();
}
}
case STEP_THROWING_BINDING -> maybeBindThrowingVariable();
case STEP_ANNOTATION_BINDING -> maybeBindAnnotationsFromPointcutExpression();
case STEP_RETURNING_BINDING -> maybeBindReturningVariable();
case STEP_PRIMITIVE_ARGS_BINDING -> maybeBindPrimitiveArgsFromPointcutExpression();
case STEP_THIS_TARGET_ARGS_BINDING -> maybeBindThisOrTargetOrArgsFromPointcutExpression();
case STEP_REFERENCE_PCUT_BINDING -> maybeBindReferencePointcutParameter();
default -> throw new IllegalStateException("Unknown algorithmic step: " + (algorithmicStep - 1));
break;
case STEP_THROWING_BINDING:
maybeBindThrowingVariable();
break;
case STEP_ANNOTATION_BINDING:
maybeBindAnnotationsFromPointcutExpression();
break;
case STEP_RETURNING_BINDING:
maybeBindReturningVariable();
break;
case STEP_PRIMITIVE_ARGS_BINDING:
maybeBindPrimitiveArgsFromPointcutExpression();
break;
case STEP_THIS_TARGET_ARGS_BINDING:
maybeBindThisOrTargetOrArgsFromPointcutExpression();
break;
case STEP_REFERENCE_PCUT_BINDING:
maybeBindReferencePointcutParameter();
break;
default:
throw new IllegalStateException("Unknown algorithmic step: " + (algorithmicStep - 1));
}
}
}
@@ -308,7 +322,7 @@ public class AspectJAdviceParameterNameDiscoverer implements ParameterNameDiscov
}
/**
* If the first parameter is of type JoinPoint or ProceedingJoinPoint, bind "thisJoinPoint" as
* If the first parameter is of type JoinPoint or ProceedingJoinPoint,bind "thisJoinPoint" as
* parameter name and return true, else return false.
*/
private boolean maybeBindThisJoinPoint() {
@@ -347,14 +361,14 @@ public class AspectJAdviceParameterNameDiscoverer implements ParameterNameDiscov
// Second candidate we've found - ambiguous binding
throw new AmbiguousBindingException("Binding of throwing parameter '" +
this.throwingName + "' is ambiguous: could be bound to argument " +
throwableIndex + " or " + i);
throwableIndex + " or argument " + i);
}
}
}
if (throwableIndex == -1) {
throw new IllegalStateException("Binding of throwing parameter '" + this.throwingName +
"' could not be completed as no available arguments are a subtype of Throwable");
throw new IllegalStateException("Binding of throwing parameter '" + this.throwingName
+ "' could not be completed as no available arguments are a subtype of Throwable");
}
else {
bindParameterName(throwableIndex, this.throwingName);
@@ -373,7 +387,7 @@ public class AspectJAdviceParameterNameDiscoverer implements ParameterNameDiscov
if (this.returningName != null) {
if (this.numberOfRemainingUnboundArguments > 1) {
throw new AmbiguousBindingException("Binding of returning parameter '" + this.returningName +
"' is ambiguous: there are " + this.numberOfRemainingUnboundArguments + " candidates.");
"' is ambiguous, there are " + this.numberOfRemainingUnboundArguments + " candidates.");
}
// We're all set... find the unbound parameter, and bind it.
@@ -386,6 +400,7 @@ public class AspectJAdviceParameterNameDiscoverer implements ParameterNameDiscov
}
}
/**
* Parse the string pointcut expression looking for:
* &#64;this, &#64;target, &#64;args, &#64;within, &#64;withincode, &#64;annotation.
@@ -429,7 +444,7 @@ public class AspectJAdviceParameterNameDiscoverer implements ParameterNameDiscov
int numAnnotationSlots = countNumberOfUnboundAnnotationArguments();
if (numAnnotationSlots > 1) {
throw new AmbiguousBindingException("Found " + varNames.size() +
" potential annotation variable(s) and " +
" potential annotation variable(s), and " +
numAnnotationSlots + " potential argument slots");
}
else if (numAnnotationSlots == 1) {
@@ -450,7 +465,7 @@ public class AspectJAdviceParameterNameDiscoverer implements ParameterNameDiscov
}
}
/**
/*
* If the token starts meets Java identifier conventions, it's in.
*/
@Nullable
@@ -471,7 +486,7 @@ public class AspectJAdviceParameterNameDiscoverer implements ParameterNameDiscov
}
String[] tokens = StringUtils.tokenizeToStringArray(argsSpec, ",");
for (int i = 0; i < tokens.length; i++) {
tokens[i] = tokens[i].strip();
tokens[i] = StringUtils.trimWhitespace(tokens[i]);
String varName = maybeExtractVariableName(tokens[i]);
if (varName != null) {
varNames.add(varName);
@@ -486,7 +501,7 @@ public class AspectJAdviceParameterNameDiscoverer implements ParameterNameDiscov
private void maybeBindThisOrTargetOrArgsFromPointcutExpression() {
if (this.numberOfRemainingUnboundArguments > 1) {
throw new AmbiguousBindingException("Still " + this.numberOfRemainingUnboundArguments
+ " unbound args at this()/target()/args() binding stage, with no way to determine between them");
+ " unbound args at this(),target(),args() binding stage, with no way to determine between them");
}
List<String> varNames = new ArrayList<>();
@@ -518,9 +533,10 @@ public class AspectJAdviceParameterNameDiscoverer implements ParameterNameDiscov
}
}
if (varNames.size() > 1) {
throw new AmbiguousBindingException("Found " + varNames.size() +
" candidate this(), target(), or args() variables but only one unbound argument slot");
" candidate this(), target() or args() variables but only one unbound argument slot");
}
else if (varNames.size() == 1) {
for (int j = 0; j < this.parameterNameBindings.length; j++) {
@@ -593,7 +609,7 @@ public class AspectJAdviceParameterNameDiscoverer implements ParameterNameDiscov
// else varNames.size must be 0 and we have nothing to bind.
}
/**
/*
* We've found the start of a binding pointcut at the given index into the
* token array. Now we need to extract the pointcut body and return it.
*/
@@ -646,8 +662,8 @@ public class AspectJAdviceParameterNameDiscoverer implements ParameterNameDiscov
private void maybeBindPrimitiveArgsFromPointcutExpression() {
int numUnboundPrimitives = countNumberOfUnboundPrimitiveArguments();
if (numUnboundPrimitives > 1) {
throw new AmbiguousBindingException("Found " + numUnboundPrimitives +
" unbound primitive arguments with no way to distinguish between them.");
throw new AmbiguousBindingException("Found '" + numUnboundPrimitives +
"' unbound primitive arguments with no way to distinguish between them.");
}
if (numUnboundPrimitives == 1) {
// Look for arg variable and bind it if we find exactly one...
@@ -693,7 +709,7 @@ public class AspectJAdviceParameterNameDiscoverer implements ParameterNameDiscov
return false;
}
/**
/*
* Return {@code true} if the given argument type is a subclass
* of the given supertype.
*/
@@ -721,7 +737,7 @@ public class AspectJAdviceParameterNameDiscoverer implements ParameterNameDiscov
return count;
}
/**
/*
* Find the argument index with the given type, and bind the given
* {@code varName} in that position.
*/
@@ -738,10 +754,22 @@ public class AspectJAdviceParameterNameDiscoverer implements ParameterNameDiscov
/**
* Simple record to hold the extracted text from a pointcut body, together
* Simple struct to hold the extracted text from a pointcut body, together
* with the number of tokens consumed in extracting it.
*/
private record PointcutBody(int numTokensConsumed, @Nullable String text) {}
private static class PointcutBody {
private int numTokensConsumed;
@Nullable
private String text;
public PointcutBody(int tokens, @Nullable String text) {
this.numTokensConsumed = tokens;
this.text = text;
}
}
/**
* Thrown in response to an ambiguous binding being detected when
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2012 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.
@@ -61,12 +61,12 @@ public abstract class AspectJAopUtils {
*/
@Nullable
public static AspectJPrecedenceInformation getAspectJPrecedenceInformationFor(Advisor anAdvisor) {
if (anAdvisor instanceof AspectJPrecedenceInformation ajpi) {
return ajpi;
if (anAdvisor instanceof AspectJPrecedenceInformation) {
return (AspectJPrecedenceInformation) anAdvisor;
}
Advice advice = anAdvisor.getAdvice();
if (advice instanceof AspectJPrecedenceInformation ajpi) {
return ajpi;
if (advice instanceof AspectJPrecedenceInformation) {
return (AspectJPrecedenceInformation) advice;
}
return null;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2020 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.
@@ -63,9 +63,10 @@ public class AspectJAroundAdvice extends AbstractAspectJAdvice implements Method
@Override
@Nullable
public Object invoke(MethodInvocation mi) throws Throwable {
if (!(mi instanceof ProxyMethodInvocation pmi)) {
if (!(mi instanceof ProxyMethodInvocation)) {
throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi);
}
ProxyMethodInvocation pmi = (ProxyMethodInvocation) mi;
ProceedingJoinPoint pjp = lazyGetProceedingJoinPoint(pmi);
JoinPointMatch jpm = getJoinPointMatch(pmi);
return invokeAdviceMethod(pjp, jpm, null, null);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2022 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.
@@ -21,6 +21,7 @@ import java.io.ObjectInputStream;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -84,17 +85,21 @@ import org.springframework.util.StringUtils;
public class AspectJExpressionPointcut extends AbstractExpressionPointcut
implements ClassFilter, IntroductionAwareMethodMatcher, BeanFactoryAware {
private static final Set<PointcutPrimitive> SUPPORTED_PRIMITIVES = Set.of(
PointcutPrimitive.EXECUTION,
PointcutPrimitive.ARGS,
PointcutPrimitive.REFERENCE,
PointcutPrimitive.THIS,
PointcutPrimitive.TARGET,
PointcutPrimitive.WITHIN,
PointcutPrimitive.AT_ANNOTATION,
PointcutPrimitive.AT_WITHIN,
PointcutPrimitive.AT_ARGS,
PointcutPrimitive.AT_TARGET);
private static final Set<PointcutPrimitive> SUPPORTED_PRIMITIVES = new HashSet<>();
static {
SUPPORTED_PRIMITIVES.add(PointcutPrimitive.EXECUTION);
SUPPORTED_PRIMITIVES.add(PointcutPrimitive.ARGS);
SUPPORTED_PRIMITIVES.add(PointcutPrimitive.REFERENCE);
SUPPORTED_PRIMITIVES.add(PointcutPrimitive.THIS);
SUPPORTED_PRIMITIVES.add(PointcutPrimitive.TARGET);
SUPPORTED_PRIMITIVES.add(PointcutPrimitive.WITHIN);
SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_ANNOTATION);
SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_WITHIN);
SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_ARGS);
SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_TARGET);
}
private static final Log logger = LogFactory.getLog(AspectJExpressionPointcut.class);
@@ -200,8 +205,8 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
*/
@Nullable
private ClassLoader determinePointcutClassLoader() {
if (this.beanFactory instanceof ConfigurableBeanFactory cbf) {
return cbf.getBeanClassLoader();
if (this.beanFactory instanceof ConfigurableBeanFactory) {
return ((ConfigurableBeanFactory) this.beanFactory).getBeanClassLoader();
}
if (this.pointcutDeclarationScope != null) {
return this.pointcutDeclarationScope.getClassLoader();
@@ -335,10 +340,10 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
try {
MethodInvocation mi = ExposeInvocationInterceptor.currentInvocation();
targetObject = mi.getThis();
if (!(mi instanceof ProxyMethodInvocation _pmi)) {
if (!(mi instanceof ProxyMethodInvocation)) {
throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi);
}
pmi = _pmi;
pmi = (ProxyMethodInvocation) mi;
thisObject = pmi.getProxy();
}
catch (IllegalStateException ex) {
@@ -404,8 +409,8 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
}
private RuntimeTestWalker getRuntimeTestWalker(ShadowMatch shadowMatch) {
if (shadowMatch instanceof DefensiveShadowMatch defensiveShadowMatch) {
return new RuntimeTestWalker(defensiveShadowMatch.primary);
if (shadowMatch instanceof DefensiveShadowMatch) {
return new RuntimeTestWalker(((DefensiveShadowMatch) shadowMatch).primary);
}
return new RuntimeTestWalker(shadowMatch);
}
@@ -422,8 +427,7 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
private ShadowMatch getTargetShadowMatch(Method method, Class<?> targetClass) {
Method targetMethod = AopUtils.getMostSpecificMethod(method, targetClass);
if (targetMethod.getDeclaringClass().isInterface() && targetMethod.getDeclaringClass() != targetClass &&
obtainPointcutExpression().getPointcutExpression().contains("." + targetMethod.getName() + "(")) {
if (targetMethod.getDeclaringClass().isInterface()) {
// Try to build the most specific interface possible for inherited methods to be
// considered for sub-interface matches as well, in particular for proxy classes.
// Note: AspectJ is only going to take Method.getDeclaringClass() into account.
@@ -519,9 +523,10 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
if (this == other) {
return true;
}
if (!(other instanceof AspectJExpressionPointcut otherPc)) {
if (!(other instanceof AspectJExpressionPointcut)) {
return false;
}
AspectJExpressionPointcut otherPc = (AspectJExpressionPointcut) other;
return ObjectUtils.nullSafeEquals(this.getExpression(), otherPc.getExpression()) &&
ObjectUtils.nullSafeEquals(this.pointcutDeclarationScope, otherPc.pointcutDeclarationScope) &&
ObjectUtils.nullSafeEquals(this.pointcutParameterNames, otherPc.pointcutParameterNames) &&
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2018 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.
@@ -97,9 +97,10 @@ public class AspectJPointcutAdvisor implements PointcutAdvisor, Ordered {
if (this == other) {
return true;
}
if (!(other instanceof AspectJPointcutAdvisor otherAdvisor)) {
if (!(other instanceof AspectJPointcutAdvisor)) {
return false;
}
AspectJPointcutAdvisor otherAdvisor = (AspectJPointcutAdvisor) other;
return this.advice.equals(otherAdvisor.advice);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2020 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.
@@ -71,8 +71,8 @@ public abstract class AspectJProxyUtils {
private static boolean isAspectJAdvice(Advisor advisor) {
return (advisor instanceof InstantiationModelAwarePointcutAdvisor ||
advisor.getAdvice() instanceof AbstractAspectJAdvice ||
(advisor instanceof PointcutAdvisor pointcutAdvisor &&
pointcutAdvisor.getPointcut() instanceof AspectJExpressionPointcut));
(advisor instanceof PointcutAdvisor &&
((PointcutAdvisor) advisor).getPointcut() instanceof AspectJExpressionPointcut));
}
static boolean isVariableName(@Nullable String name) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2018 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.
@@ -202,8 +202,8 @@ class RuntimeTestWalker {
}
Class<?> typeClass = null;
ResolvedType type = (ResolvedType) i.getType();
if (type instanceof ReferenceType referenceType) {
ReferenceTypeDelegate delegate = referenceType.getDelegate();
if (type instanceof ReferenceType) {
ReferenceTypeDelegate delegate = ((ReferenceType) type).getDelegate();
if (delegate instanceof ReflectionBasedReferenceTypeDelegate) {
try {
ReflectionUtils.makeAccessible(myClassField);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2015 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.
@@ -69,8 +69,8 @@ public class SingletonAspectInstanceFactory implements AspectInstanceFactory, Se
*/
@Override
public int getOrder() {
if (this.aspectInstance instanceof Ordered ordered) {
return ordered.getOrder();
if (this.aspectInstance instanceof Ordered) {
return ((Ordered) this.aspectInstance).getOrder();
}
return getOrderForAspectClass(this.aspectInstance.getClass());
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2019 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.
@@ -117,9 +117,9 @@ public class TypePatternClassFilter implements ClassFilter {
}
@Override
public boolean equals(Object obj) {
return (this == obj || (obj instanceof TypePatternClassFilter that &&
ObjectUtils.nullSafeEquals(this.typePattern, that.typePattern)));
public boolean equals(Object other) {
return (this == other || (other instanceof TypePatternClassFilter &&
ObjectUtils.nullSafeEquals(this.typePattern, ((TypePatternClassFilter) other).typePattern)));
}
@Override
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2021 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.
@@ -20,6 +20,8 @@ import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;
@@ -51,7 +53,6 @@ import org.springframework.lang.Nullable;
* @author Rod Johnson
* @author Adrian Colyer
* @author Juergen Hoeller
* @author Sam Brannen
* @since 2.0
*/
public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFactory {
@@ -101,6 +102,14 @@ public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFac
@Override
public void validate(Class<?> aspectClass) throws AopConfigException {
// If the parent has the annotation and isn't abstract it's an error
Class<?> superclass = aspectClass.getSuperclass();
if (superclass.getAnnotation(Aspect.class) != null &&
!Modifier.isAbstract(superclass.getModifiers())) {
throw new AopConfigException("[" + aspectClass.getName() + "] cannot extend concrete aspect [" +
superclass.getName() + "]");
}
AjType<?> ajType = AjTypeSystem.getAjType(aspectClass);
if (!ajType.isAspect()) {
throw new NotAnAtAspectException(aspectClass);
@@ -121,21 +130,21 @@ public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFac
*/
@SuppressWarnings("unchecked")
@Nullable
protected static AspectJAnnotation findAspectJAnnotationOnMethod(Method method) {
for (Class<?> annotationType : ASPECTJ_ANNOTATION_CLASSES) {
AspectJAnnotation annotation = findAnnotation(method, (Class<Annotation>) annotationType);
if (annotation != null) {
return annotation;
protected static AspectJAnnotation<?> findAspectJAnnotationOnMethod(Method method) {
for (Class<?> clazz : ASPECTJ_ANNOTATION_CLASSES) {
AspectJAnnotation<?> foundAnnotation = findAnnotation(method, (Class<Annotation>) clazz);
if (foundAnnotation != null) {
return foundAnnotation;
}
}
return null;
}
@Nullable
private static AspectJAnnotation findAnnotation(Method method, Class<? extends Annotation> annotationType) {
Annotation annotation = AnnotationUtils.findAnnotation(method, annotationType);
if (annotation != null) {
return new AspectJAnnotation(annotation);
private static <A extends Annotation> AspectJAnnotation<A> findAnnotation(Method method, Class<A> toLookFor) {
A result = AnnotationUtils.findAnnotation(method, toLookFor);
if (result != null) {
return new AspectJAnnotation<>(result);
}
else {
return null;
@@ -154,23 +163,26 @@ public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFac
/**
* Class modeling an AspectJ annotation, exposing its type enumeration and
* Class modelling an AspectJ annotation, exposing its type enumeration and
* pointcut String.
* @param <A> the annotation type
*/
protected static class AspectJAnnotation {
protected static class AspectJAnnotation<A extends Annotation> {
private static final String[] EXPRESSION_ATTRIBUTES = {"pointcut", "value"};
private static final String[] EXPRESSION_ATTRIBUTES = new String[] {"pointcut", "value"};
private static final Map<Class<?>, AspectJAnnotationType> annotationTypeMap = Map.of(
Pointcut.class, AspectJAnnotationType.AtPointcut, //
Around.class, AspectJAnnotationType.AtAround, //
Before.class, AspectJAnnotationType.AtBefore, //
After.class, AspectJAnnotationType.AtAfter, //
AfterReturning.class, AspectJAnnotationType.AtAfterReturning, //
AfterThrowing.class, AspectJAnnotationType.AtAfterThrowing //
);
private static Map<Class<?>, AspectJAnnotationType> annotationTypeMap = new HashMap<>(8);
private final Annotation annotation;
static {
annotationTypeMap.put(Pointcut.class, AspectJAnnotationType.AtPointcut);
annotationTypeMap.put(Around.class, AspectJAnnotationType.AtAround);
annotationTypeMap.put(Before.class, AspectJAnnotationType.AtBefore);
annotationTypeMap.put(After.class, AspectJAnnotationType.AtAfter);
annotationTypeMap.put(AfterReturning.class, AspectJAnnotationType.AtAfterReturning);
annotationTypeMap.put(AfterThrowing.class, AspectJAnnotationType.AtAfterThrowing);
}
private final A annotation;
private final AspectJAnnotationType annotationType;
@@ -178,20 +190,20 @@ public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFac
private final String argumentNames;
public AspectJAnnotation(Annotation annotation) {
public AspectJAnnotation(A annotation) {
this.annotation = annotation;
this.annotationType = determineAnnotationType(annotation);
try {
this.pointcutExpression = resolvePointcutExpression(annotation);
this.pointcutExpression = resolveExpression(annotation);
Object argNames = AnnotationUtils.getValue(annotation, "argNames");
this.argumentNames = (argNames instanceof String names ? names : "");
this.argumentNames = (argNames instanceof String ? (String) argNames : "");
}
catch (Exception ex) {
throw new IllegalArgumentException(annotation + " is not a valid AspectJ annotation", ex);
}
}
private AspectJAnnotationType determineAnnotationType(Annotation annotation) {
private AspectJAnnotationType determineAnnotationType(A annotation) {
AspectJAnnotationType type = annotationTypeMap.get(annotation.annotationType());
if (type != null) {
return type;
@@ -199,21 +211,24 @@ public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFac
throw new IllegalStateException("Unknown annotation type: " + annotation);
}
private String resolvePointcutExpression(Annotation annotation) {
private String resolveExpression(A annotation) {
for (String attributeName : EXPRESSION_ATTRIBUTES) {
Object val = AnnotationUtils.getValue(annotation, attributeName);
if (val instanceof String str && !str.isEmpty()) {
return str;
if (val instanceof String) {
String str = (String) val;
if (!str.isEmpty()) {
return str;
}
}
}
throw new IllegalStateException("Failed to resolve pointcut expression in: " + annotation);
throw new IllegalStateException("Failed to resolve expression: " + annotation);
}
public AspectJAnnotationType getAnnotationType() {
return this.annotationType;
}
public Annotation getAnnotation() {
public A getAnnotation() {
return this.annotation;
}
@@ -238,22 +253,19 @@ public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFac
*/
private static class AspectJAnnotationParameterNameDiscoverer implements ParameterNameDiscoverer {
private static final String[] EMPTY_ARRAY = new String[0];
@Override
@Nullable
public String[] getParameterNames(Method method) {
if (method.getParameterCount() == 0) {
return EMPTY_ARRAY;
return new String[0];
}
AspectJAnnotation annotation = findAspectJAnnotationOnMethod(method);
AspectJAnnotation<?> annotation = findAspectJAnnotationOnMethod(method);
if (annotation == null) {
return null;
}
StringTokenizer nameTokens = new StringTokenizer(annotation.getArgumentNames(), ",");
int numTokens = nameTokens.countTokens();
if (numTokens > 0) {
String[] names = new String[numTokens];
if (nameTokens.countTokens() > 0) {
String[] names = new String[nameTokens.countTokens()];
for (int i = 0; i < names.length; i++) {
names[i] = nameTokens.nextToken();
}
@@ -101,22 +101,24 @@ public class AspectMetadata implements Serializable {
this.ajType = ajType;
switch (this.ajType.getPerClause().getKind()) {
case SINGLETON -> {
case SINGLETON:
this.perClausePointcut = Pointcut.TRUE;
}
case PERTARGET, PERTHIS -> {
return;
case PERTARGET:
case PERTHIS:
AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut();
ajexp.setLocation(aspectClass.getName());
ajexp.setExpression(findPerClause(aspectClass));
ajexp.setPointcutDeclarationScope(aspectClass);
this.perClausePointcut = ajexp;
}
case PERTYPEWITHIN -> {
return;
case PERTYPEWITHIN:
// Works with a type pattern
this.perClausePointcut = new ComposablePointcut(new TypePatternClassFilter(findPerClause(aspectClass)));
}
default -> throw new AopConfigException(
"PerClause " + ajType.getPerClause().getKind() + " not supported by Spring AOP for " + aspectClass);
return;
default:
throw new AopConfigException(
"PerClause " + ajType.getPerClause().getKind() + " not supported by Spring AOP for " + aspectClass);
}
}

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