Compare commits

..

188 Commits

Author SHA1 Message Date
Spring Buildmaster 967637d763 Release v5.3.1 2020-11-10 08:59:43 +00:00
Rossen Stoyanchev 2899652d8e Fix checkstyle violation 2020-11-09 22:30:35 +00:00
Rossen Stoyanchev 560a504a83 Polishing in DefaultWebClient 2020-11-09 22:07:31 +00:00
Rossen Stoyanchev 79f79e9306 WebClient method to populate the Reactor Context
The alternative is to use a filter but this makes it a little easier
and also guarantees that it will be downstream from all filters
regardless of their order, and therefore the Context will be visible
to all of them.

Closes gh-25710
2020-11-09 22:07:31 +00:00
Juergen Hoeller bd2640a9d6 Strict nullability for field assignment 2020-11-09 21:33:32 +01:00
Juergen Hoeller 6825287360 Polishing 2020-11-09 18:18:33 +01:00
Sébastien Deleuze 737d77a739 Fix @Transactional support on functions returning Flow
Closes gh-26052
2020-11-09 17:45:28 +01:00
Sébastien Deleuze 1f13516528 Fix @Transactional support on suspending function
With this commit, @Transactional on suspending functions
returning a value now commits the transaction properly.

Closes gh-25998
2020-11-09 17:43:31 +01:00
Rossen Stoyanchev 6bb3ad793e Add isOpen to WebSocketSession in WebFlux
Closes gh-26043
2020-11-09 14:06:43 +00:00
Sébastien Deleuze c73cff8bad Use RxJava 2/3 fromPublisher() when possible in ReactiveAdapterRegistry
Closes gh-26051
2020-11-09 14:49:45 +01:00
Juergen Hoeller e592634290 Polishing 2020-11-09 13:32:41 +01:00
Juergen Hoeller b25637e8ad Make setTaskScheduler and setTcpClient chaining-friendly
Closes gh-26049
2020-11-09 13:32:34 +01:00
izeye 0c347769a2 Fix wrong reference in UrlPathHelper.removeSemicolonContentInternal()
This commit also changes to short-circuit when `slashIndex` is -1.
2020-11-09 13:08:44 +01:00
Juergen Hoeller 2497d4285f Detect existing DispatcherServlet strategy beans in parent context as well
Closes gh-25290
2020-11-09 12:39:09 +01:00
Sébastien Deleuze 32238cc996 Add CoroutinesAnnotationTransactionInterceptorTests
suspendingValueSuccess() currently fails due to an
unexpected rollback on transactional suspending functions
returning a value.

See gh-25998
2020-11-09 10:19:24 +01:00
Sébastien Deleuze d80dd50c9d Add CoroutinesAnnotationTransactionInterceptorTests
suspendingValueSuccess() currently fails due to an
unexpected rollback on transactional suspending functions
returning a value.

See gh-25998
2020-11-09 10:11:39 +01:00
Sébastien Deleuze bdf120f1ef Add CoroutinesAnnotationTransactionInterceptorTests
suspendingValueSuccess() currently fails due to an
unexpected rollback on transactional suspending functions
returning a value.

See gh-25998
2020-11-09 10:10:25 +01:00
Sébastien Deleuze e781d21097 Add CoroutinesAnnotationTransactionInterceptorTests
suspendingValueSuccess() currently fails due to an
unexpected rollback on transactional suspending functions
returning a value.

See gh-25998
2020-11-09 10:10:13 +01:00
Sébastien Deleuze 8cef13cc30 Upgrade to Kotlin Coroutines 1.4.1 2020-11-09 09:34:46 +01:00
Rüdiger Schulz 2214ff8ce0 Fix Kotlin example code which does not compile
This kotlin code does not compile:
Overload resolution ambiguity. All these functions match.
default <R> RowsFetchSpec<R> map(Function<Row, R> mappingFunction)
<R> RowsFetchSpec<R> map(BiFunction<Row, RowMetadata, R> mappingFunction);

Closes gh-26016
2020-11-08 15:43:30 +01:00
Sam Brannen 411539ffef Update copyright date
See gh-26044
2020-11-08 15:41:31 +01:00
NaNRailgun 11aa901634 Delete explicit type arguments that can be inferred
Closes gh-26044
2020-11-08 15:40:01 +01:00
Rossen Stoyanchev 6e51370490 AbstractJackson2Encoder support for MappingJacksonValue
Closes gh-26035
2020-11-06 20:46:35 +00:00
Rossen Stoyanchev bcd2b9a8a7 Further refine logging in StompErrorHandler
In 5.3.x and forward, completely avoid logging unsent CONNECT messages
that are most likely authentication issues before the session is even
established.

Closes gh-26026
2020-11-05 21:50:12 +00:00
Rossen Stoyanchev 17dd7785f6 Refine logging in StompErrorHandler
Avoid a full stacktrace at ERROR level for a client message that could
not be sent to a MessageChannel.

See gh-26026
2020-11-05 21:34:56 +00:00
Juergen Hoeller 21f9d05bd9 Upgrade to Jetty 9.4.34, AssertJ 3.18, Mockito 3.6 2020-11-05 19:21:54 +01:00
Juergen Hoeller 19911af30a Polishing 2020-11-05 19:09:58 +01:00
Juergen Hoeller 6acb091c4e Upgrade SpelCompiler bytecode level to 1.8 and optimize for concurrent access
Closes gh-26033
2020-11-05 17:44:40 +01:00
Juergen Hoeller 99ed01e3f7 Optimize locking in AspectJProxyFactory for concurrent aspect instantiation
Closes gh-26034
2020-11-05 13:11:16 +01:00
Juergen Hoeller 2657757566 Separate assignment statements for local variable and instance field
See gh-25899
2020-11-05 13:10:45 +01:00
Сергей Цыпанов e1f51b4bf8 Avoid multiple volatile reads/writes in a row where only one is enough 2020-11-05 12:25:24 +01:00
Juergen Hoeller f2f84bfa7a Polishing 2020-11-05 00:04:33 +01:00
Juergen Hoeller 6d67bdf04f Polishing 2020-11-04 23:43:43 +01:00
Juergen Hoeller 97c8628bd6 Suppress NotWritablePropertyException in case of ignoreUnknown=true
Closes gh-25986
2020-11-04 23:42:37 +01:00
Juergen Hoeller 990a9c74b9 Restore removal of trailing semicolon content
See gh-26012
2020-11-04 23:42:28 +01:00
Juergen Hoeller b815accca9 Upgrade to Hibernate ORM 5.4.23 2020-11-04 16:52:40 +01:00
Juergen Hoeller 4e8d6bea4d Fix test code formatting 2020-11-04 16:52:11 +01:00
Juergen Hoeller 079ca80854 SpEL supports record-style accessor methods as well
Closes gh-26029
2020-11-04 16:51:54 +01:00
Juergen Hoeller 412aa06d86 Reliably refresh metadata for dynamically changing prototype bean class
Closes gh-26019
2020-11-04 16:48:54 +01:00
Juergen Hoeller d5b3e65718 Add since tag
See gh-26025
2020-11-04 16:46:44 +01:00
Juergen Hoeller a4679b87b9 Fix Checkstyle violation and rearrange getAdvisorCount declaration
See gh-26017
2020-11-04 16:43:31 +01:00
Erik van Paassen fbf8e37f94 Add FullyQualifiedAnnotationBeanNameGenerator.INSTANCE
Adds a static INSTANCE to FullyQualifiedAnnotationBeanNameGenerator,
just like its parent class, AnnotationBeanNameGenerator, does.

The class doesn't have any state and overriding/hiding the INSTANCE of
the superclass, this prevents unintended use of a regular
AnnotationBeanNameGenerator through
FullyQualifiedAnnotationBeanNameGenerator.INSTANCE.
2020-11-04 16:29:31 +01:00
Сергей Цыпанов 8c3cab7ead Improve usage of AdvisedSupport.getAdvisors() 2020-11-04 16:29:09 +01:00
Сергей Цыпанов 0015fd6734 Improve URI/query strings sanitization 2020-11-04 16:28:51 +01:00
Sam Brannen b077e4cd85 Use MergedAnnotationCollectors again in TestContextAnnotationUtils
See gh-26031
2020-11-04 14:42:19 +01:00
Sam Brannen 0a0299693b Do not create intermediate list in MergedAnnotationCollectors.toAnnotationSet()
Prior to this commit, MergedAnnotationCollectors.toAnnotationSet()
created an intermediate ArrayList for storing the results prior to
creating a LinkedHashSet in the finishing step.

Since the creation of the intermediate list is unnecessary, this commit
simplifies the implementation of toAnnotationSet() by using the
Collector.of() factory method that does not accept a `finisher` argument.
The resulting Collector internally uses a `castingIdentity()` function
as the `finisher`.

Closes gh-26031
2020-11-04 14:19:58 +01:00
Rossen Stoyanchev 41bdde5d6b Polishing 2020-11-03 19:31:02 +00:00
Rossen Stoyanchev 2e2d662158 Add toEntityFlux methods
Closes gh-26023
2020-11-03 19:31:02 +00:00
Sam Brannen 68934f1b79 Use TYPE_HIERARCHY strategy in AnnoDescr.findAllLocalMergedAnnotations()
Prior to this commit, the findAllLocalMergedAnnotations() method in
AnnotationDescriptor altered between the use of TYPE_HIERARCHY and
TYPE_HIERARCHY_AND_ENCLOSING_CLASSES for the SearchStrategy, depending
on @NestedTestConfiguration semantics; however, when searching for
"local" annotations, there is no need to search the enclosing class
hierarchy since AnnotationDescriptor#next() handles that use case.

This commit therefore switches to using only the TYPE_HIERARCHY
strategy.

This commit also discontinues the use of
MergedAnnotationCollectors.toAnnotationSet() in order to avoid the
unnecessary creation of a temporary List when collecting synthesized
annotations in a LinkedHashSet.

Closes gh-25985
2020-11-03 16:17:15 +01:00
hzmpay f781c459b4 Simplify AbstractBeanFactory.registerCustomEditors()
This commit avoids an unnecessary non-null check in
registerCustomEditors().

Closes gh-26022
2020-11-03 14:52:23 +01:00
Sam Brannen 5628c8cd63 Polish ActiveProfilesUtils 2020-11-03 14:46:17 +01:00
Rossen Stoyanchev 0f6038af70 Log can[Se]Deserialize error in Jackson codecs
Closes gh-25892
2020-11-02 17:25:57 +00:00
Rossen Stoyanchev 3eb2c5e22f Use static accessors in DefaultSimpUserRegistry
Closes gh-26010
2020-11-02 17:25:57 +00:00
Brian Clozel 3f722a9c7f Set DEBUG logs for release tasks in CI pipeline
This commit configures the logging level to DEBUG for the concourse
release scripts tasks.

This is useful to get a clue on where those long-running tasks are at.
2020-11-02 17:17:02 +01:00
Sam Brannen acbbf61be8 Preserve registration order in @ActiveProfiles
With this commit, bean definition profiles declared via @ActiveProfiles
are once again stored in registration order, in order to support use
cases in Spring Boot and other frameworks that depend on the
registration order.

This effectively reverts the changes made in conjunction with gh-25973.

Closes gh-26004
2020-11-02 16:39:59 +01:00
Sam Brannen 154f0c71c9 Polishing 2020-11-02 15:47:15 +01:00
Rossen Stoyanchev 7be7e5beb4 Ensure response not closed by MappingJackson2HttpMessageConverter
Closes gh-25987
2020-10-30 18:31:30 +00:00
Stephane Nicoll 37504e75e9 Merge pull request #25999 from izeye
* pr/25999:
  Fix a broken Asciidoctor syntax in core-resources.adoc

Closes gh-25999
2020-10-30 09:33:00 +01:00
izeye b89b37729c Fix a broken Asciidoctor syntax in core-resources.adoc
See gh-25999
2020-10-30 09:32:42 +01:00
Rossen Stoyanchev 141c79bc8f Default resolution of non-annotated Principal argument
Closes gh-25981
2020-10-29 21:29:35 +00:00
Arjen Poutsma 885a5048c1 Copy default headers, cookies in WebClient builder
This commit makes copies of the default headers and cookies when a
WebClient is built, so that subsequent changes to these do not affect
previously built clients.

Closes: gh-25992
2020-10-29 13:37:25 +01:00
Arjen Poutsma e981bf34b3 Removed unused sample code 2020-10-29 13:37:25 +01:00
Aakarshit Uppal 44239b2d7b Remove rogue semicolon 2020-10-29 12:32:20 +00:00
Stephane Nicoll a00c483927 Merge branch '5.2.x' 2020-10-27 16:56:22 +01:00
Spring Buildmaster 96ffa14141 Next development version (v5.3.1-SNAPSHOT) 2020-10-27 14:52:47 +00:00
Rossen Stoyanchev 9f632cf0c1 Upgrade to RSocket 1.1.0
Closes gh-25956
2020-10-27 14:05:05 +00:00
Spring Buildmaster a9dec6a4af Next Development Version 2020-10-27 13:48:52 +00:00
Sam Brannen 9c2fd0bd05 Document @DynamicPropertySource use cases that require @DirtiesContext
Closes gh-25850
2020-10-27 14:26:40 +01:00
Nick Hitchan 6e936a4081 Remove absolute URI check from ReactorClientHttpConnector
This allows maknig use of the Unix Domain Socket support in Reactor 1.0.

Closes gh-25929
2020-10-27 12:54:18 +00:00
Sébastien Deleuze b2d337c527 Fix Coroutines minimal version in the refdoc
See gh-25969
2020-10-27 12:48:29 +01:00
Rossen Stoyanchev 24cef14683 Merge branch '5.2.x' into master 2020-10-27 11:21:41 +00:00
Rossen Stoyanchev bb4e802af5 Full header support HTTP HEAD Resource requests
Allow the body to be written in order for all headers to be set
as they would be on HTTP GET. The body content is ignored as a
lower level.

See gh-25976
2020-10-27 11:14:37 +00:00
Sam Brannen b01adadf60 Improve documentation for SpEL property reference syntax
See gh-25538
2020-10-27 11:48:44 +01:00
Yanming Zhou 9c11887fe8 Use lowercase property names in SpEL examples
Closes gh-25538
2020-10-27 11:48:44 +01:00
Sébastien Deleuze 4503260282 Fix Dokka warnings 2020-10-27 11:21:55 +01:00
Rossen Stoyanchev d91b66a04c Allow Resource to add headers for range requests
Closes gh-25976
2020-10-27 10:15:56 +00:00
Sam Brannen d1e6883d5d Fix issues in Javadoc 2020-10-27 11:05:31 +01:00
Sébastien Deleuze 5170f3f594 Fix a Kotlin test compile warning 2020-10-27 09:41:59 +01:00
Stephane Nicoll a4d546a9d6 Upgrade to Reactor 2020.0.0
Closes gh-25947
2020-10-27 03:08:54 +01:00
Sam Brannen fc9650a9a6 Finalize API for TestContextAnnotationUtils 2020-10-27 00:22:03 +01:00
Sam Brannen 82fa3f3fc1 Reinstate tests for deprecated MetaAnnotationUtils 2020-10-27 00:18:25 +01:00
Juergen Hoeller 1fb33fbd3c Merge branch '5.2.x'
# Conflicts:
#	build.gradle
2020-10-26 23:23:59 +01:00
Juergen Hoeller b6dae6acba Upgrade to JUnit 5.6.3, Woodstox 6.2.3, BlockHound 1.0.4 2020-10-26 23:20:14 +01:00
Rossen Stoyanchev a3cd8e11b0 Merge branch '5.2.x' into master 2020-10-26 21:49:14 +00:00
Rossen Stoyanchev f50c2caf59 Upgrade to RSocket 1.0.3
Closes gh-25957
2020-10-26 21:42:54 +00:00
Juergen Hoeller 0aa3205e38 Fix nullability warnings 2020-10-26 22:19:26 +01:00
Sébastien Deleuze 5b910a87c3 Polishing 2020-10-26 20:10:54 +01:00
Sébastien Deleuze 9ebd3ee1fe Upgrade to Kotlin Coroutines 1.4.0
Closes gh-25969
2020-10-26 20:10:54 +01:00
Sébastien Deleuze 3f01af6f7c Add kotlinx.serialization JSON support to Spring Messaging
Closes gh-25883
2020-10-26 20:10:54 +01:00
Juergen Hoeller f329748657 Merge branch '5.2.x' 2020-10-26 18:32:16 +01:00
Juergen Hoeller d12996a6a8 Polishing 2020-10-26 18:18:33 +01:00
Sébastien Deleuze d04c5f8b2c Support multiple matchers in MockMvc Kotlin DSL
Previous incarnation of MockMvc Kotlin DSL tried to reuse directly
Java APIs like ModelResultMatchers or StatusResultMatchers, but
when using multiple matchers in DSL blocks like model { } or
status { }, only the last statement was taken in account which
was very confusing.

This refactoring provides dedicated Kotlin DSLs for matchers.

The main API breaking changes is that functions like isOk() need to be
invoked with the parenthesis, isOk is not supported anymore (on purpose).

Closes gh-24103
2020-10-26 18:13:55 +01:00
Juergen Hoeller 41247d49ba R2DBC javadoc and code style revision
See gh-25065
2020-10-26 17:47:20 +01:00
Juergen Hoeller 16c8676e5b Use ConcurrentLruCache (aligned with NamedParameterJdbcTemplate)
See gh-24197
2020-10-26 17:37:20 +01:00
Juergen Hoeller d7f1813cd7 R2DBC and Hibernate documentation revision
Closes gh-25860
Closes gh-25533
2020-10-26 17:33:00 +01:00
Sam Brannen ada255d584 Ignore duplicate config metadata for cache key in TestContext framework
Prior to this commit, if a developer accidentally copied and pasted the
same @ContextConfiguration or @TestPropertySource declaration from a
test class to one of its subclasses or nested test classes, the Spring
TestContext Framework (TCF) would merge the inherited configuration
with the local configuration, resulting in different sets of
configuration metadata which in turn resulted in a different
ApplicationContext instance being loaded for the test classes. This
behavior led to unnecessary creation of identical application contexts
in the context cache for the TCF stored under different keys.

This commit ignores duplicate configuration metadata when generating
the ApplicationContext cache key (i.e., MergedContextConfiguration) in
the TCF. This is performed for the following annotations.

- @ContextConfiguration
- @ActiveProfiles (support already existed prior to this commit)
- @TestPropertySource

Specifically, if @ContextConfiguration or @TestPropertySource is
declared on a test class and its subclass or nested test class with the
exact same attributes, only one instance of the annotation will be used
to generate the cache key for the resulting ApplicationContext. The
exception to this rule is an "empty" annotation declaration. An empty
@ContextConfiguration or @TestPropertySource declaration signals that
Spring (or a third-party SmartContextLoader) should detect default
configuration specific to the annotated class. Thus, multiple empty
@ContextConfiguration or @TestPropertySource declarations within a test
class hierarchy are not considered to be duplicate configuration and
are therefore not ignored.

Since @TestPropertySource is a @Repeatable annotation, the same
duplicate configuration detection logic is applied for multiple
@TestPropertySource declarations on a single test class or test
interface.

In addition, this commit reinstates validation of the rules for
repeated @TestPropertySource annotations that was removed when support
for @NestedTestConfiguration was introduced.

Closes gh-25800
2020-10-26 17:17:53 +01:00
Stephane Nicoll 7aef0c78a0 Merge branch '5.2.x' 2020-10-26 17:02:11 +01:00
Stephane Nicoll 188a5a086b Upgrade to Reactor Dysprosium-SR13
Closes gh-25946
2020-10-26 17:00:46 +01:00
Sam Brannen 705cf09ad7 Merge branch '5.2.x' 2020-10-26 15:05:35 +01:00
Sam Brannen 449377908f Fix JUnit 4 to AssertJ migration bugs
The migration from JUnit 4 assertions to AssertJ assertions resulted in
several unnecessary casts from int to long that actually cause
assertions to pass when they should otherwise fail.

This commit fixes all such bugs for the pattern `.isNotEqualTo((long)`.
2020-10-26 14:53:09 +01:00
Sam Brannen b1fef925af Cache DefaultActiveProfilesResolver instance since it is stateless 2020-10-26 13:58:45 +01:00
Sam Brannen 9287c15583 Simplify implementation of DefaultActiveProfilesResolver 2020-10-26 13:53:23 +01:00
Sam Brannen 9a4a593c7f Merge branch '5.2.x' 2020-10-26 12:53:53 +01:00
Sam Brannen c840ba9989 Avoid cache miss for @ActiveProfiles w/ same profiles but different order
Prior to this commit, two @ActiveProfiles declarations with the same
profiles but different order resulted in an identical, duplicate
ApplicationContext in the context cache in the Spring TestContext
Framework.

This commit uses a TreeSet to ensure that registered active profiles
are both unique and sorted, thereby avoiding cache misses for
semantically identical active profiles configuration on different test
classes.

Closes gh-25973
2020-10-26 11:42:28 +01:00
Juergen Hoeller 3924947773 Merge branch '5.2.x'
# Conflicts:
#	build.gradle
#	spring-core/src/main/java/org/springframework/util/LinkedMultiValueMap.java
2020-10-26 11:27:05 +01:00
Juergen Hoeller 9d70d2023c Upgrade to SnakeYAML 1.27 2020-10-26 11:16:52 +01:00
Juergen Hoeller 82835b99ec Restore independent LinkedMultiValueMap implementation (without base class)
Closes gh-25960
2020-10-26 11:16:33 +01:00
Juergen Hoeller a3f3a136c4 Make MultiValueMapAdapter public (as base class for LinkedMultiValueMap)
Closes gh-25960
2020-10-26 10:41:59 +01:00
Sébastien Deleuze a2ff03074f Polishing 2020-10-26 09:57:30 +01:00
Sébastien Deleuze a3fcfc5d2a Add SAM conversion example for MockMvcResultHandlersDsl.handle()
See gh-23757
2020-10-26 09:57:30 +01:00
Sam Brannen 1247e4a0e8 Use new ConcurrentLruCache#clear() method in TestContextAnnotationUtils 2020-10-26 09:48:33 +01:00
Rossen Stoyanchev 1676470729 Merge branch '5.2.x' into master 2020-10-26 07:49:51 +00:00
Rossen Stoyanchev 4726663f81 Correct Javadoc link core-resources.adoc
Closes gh-25970
2020-10-26 07:43:30 +00:00
Rossen Stoyanchev 86af93a504 Correct sample in webmvc.adoc
Closes gh-25965
2020-10-26 07:40:33 +00:00
Sébastien Deleuze efe933ad83 Polish Coroutines detection 2020-10-25 18:33:57 +01:00
Sébastien Deleuze 5429c7afeb Support suspending functions annotated with @Transactional
This commit makes TransactionInterceptor and TransactionAspectSupport
Coroutines aware, adapting Reactive transaction support to Coroutines.

Suspending functions returning a Flow are handled like Flux, for other
return types, they are handled like Mono.

Closes gh-23575
2020-10-25 18:33:42 +01:00
Juergen Hoeller 73eefeac5f Expose contains, remove and clear methods on ConcurrentLruCache
Closes gh-25963
2020-10-24 23:48:13 +02:00
Sam Brannen 1a50732daa Attempt to fix failing CI builds 2020-10-24 23:27:18 +02:00
Sam Brannen 1c82ba425b Reduce surface area of public API for AnnotationDescriptor types 2020-10-24 22:45:17 +02:00
Sam Brannen 946e5c9bbf Delete trailing whitespace 2020-10-24 22:29:38 +02:00
Sam Brannen af4d68b1a5 Cache default EnclosingConfiguration mode in TestContextAnnotationUtils 2020-10-24 22:26:29 +02:00
Rossen Stoyanchev a40cc8bbe0 Polishing contribution
Closes gh-25647
2020-10-23 18:27:49 +01:00
Riley Park 6e640c806f Improve support for Content-Disposition types
See gh-25647
2020-10-23 18:16:05 +01:00
Rossen Stoyanchev e66e34766e Support Optional in UriComponentsBuilder#queryParam
Closes gh-25951
2020-10-23 18:09:22 +01:00
Rossen Stoyanchev 5644a7aebb Polishing contribution
Closes gh-25951
2020-10-23 17:43:17 +01:00
Robin Roos 7af726480f Add queryParamIfPresent to UriComponentsBuilder
See gh-25951
2020-10-23 17:07:39 +01:00
Rossen Stoyanchev 51fb49be34 Correct outdated error message 2020-10-23 17:05:00 +01:00
Sam Brannen c524849774 Deleted unused field in DefaultListableBeanFactoryTests 2020-10-23 17:06:26 +02:00
Sam Brannen f2eb67373a Polish TestContextAnnotationUtils 2020-10-23 17:00:39 +02:00
Rossen Stoyanchev cf988f81b5 Merge branch '5.2.x' into master 2020-10-23 15:14:05 +01:00
Arjen Poutsma 417e7e03d4 Introduce RouterFunction attributes
This commit introduces support for router function attributes, a
way to associate meta-data with a route.

Closes: gh-25938
2020-10-23 14:58:49 +02:00
Sam Brannen 8d86d61f9f Introduce TestContextAnnotationUtils to avoid package cycles
This commit introduces TestContextAnnotationUtils as a replacement for
MetaAnnotationUtils, with dedicated support for honoring the new
@NestedTestConfiguration annotation and related annotation search
semantics.

MetaAnnotationUtils has been reverted to its previous scope and is now
deprecated.

See gh-19930
2020-10-23 14:24:33 +02:00
Rossen Stoyanchev fb4363e4e0 Refactor DataBufferUtils Matcher implementation
The existing implementation was exposed to very poor performance when matching
with multiple delimiters against a large buffer with many delimiters. In that
case all matchers are invoked many times (as many as the number of delimiters)
even though some of them found no match at all on the first pass.

The revised implementation uses a single index and advances all matchers
together, checking one byte a time, and not letting any one of them search to
the end of the entire buffer on a single pass.

Closes gh-25915
2020-10-23 13:23:51 +01:00
Juergen Hoeller 1ec6843913 Upgrade CGLIB to ASM9 level as well
See gh-24872
2020-10-23 11:47:03 +02:00
Juergen Hoeller cff57ca1fe Upgrade to R2DBC Arabba-SR8, Jetty 9.4.33, Woodstox 6.2.3, Mockito 3.5.15 2020-10-23 09:30:43 +02:00
Rossen Stoyanchev 784d72cc56 Fix checkstyle violations 2020-10-23 08:22:23 +01:00
Rossen Stoyanchev 4480269432 Upgrade to RSocket 1.1 snapshots
See gh-25956
2020-10-22 21:55:44 +01:00
Rossen Stoyanchev 9a1323aad5 Update StringDecoderBenchmark
See gh-25915
2020-10-22 21:50:27 +01:00
Rossen Stoyanchev eec6ec8f44 Merge branch '5.2.x' into master 2020-10-22 21:48:45 +01:00
Rossen Stoyanchev 6946fe2f74 Upgrade to RSocket 1.0.3 snapshots
See gh-25957
2020-10-22 21:46:17 +01:00
Rossen Stoyanchev db9e0b0ccb Refactor StringDecoder
Simplify and optimize the processing of the input stream.

The existing implementation was using bufferUntil and creating a List
for every line along with an EndFrameBuffer inserted for the
bufferUntil predicate. So the larger the input buffer and the more
lines it contained, the greater the overhead.

The new implementation avoids bufferUntil for all lines and
instead uses concatMapIterable to aggregate the lines from a buffer
into a single list. So the larger the input buffer and the more
lines it contains, the better the throughput. The only buffering
used then is for partial chunks and those are accumulated in a list.

See gh-25915
2020-10-22 21:29:54 +01:00
Sam Brannen aaa2d21058 Polish TestPropertySourceUtils 2020-10-22 17:31:59 +02:00
Sam Brannen b8b854db8c Discover @Sql annotations on enclosing class for nested test class
This commit introduces support for discovering @Sql, @SqlGroup,
@SqlConfig, and @SqlMergeMode on enclosing classes for @Nested test
classes in JUnit Jupiter.

Closes gh-25913
2020-10-22 17:08:10 +02:00
Juergen Hoeller 95110d8257 Introduce TransactionalApplicationListener interface (with callback support)
Includes forPayload methods and common adapter classes for programmatic usage.
Aligns default order values for event handling delegates to LOWEST_PRECEDENCE.

Closes gh-24163
2020-10-22 15:19:32 +02:00
Rossen Stoyanchev cfc3522641 Add jmh benchmark for StringDecoder
See gh-25915
2020-10-21 22:06:55 +01:00
Juergen Hoeller 621295dbd8 Deprecate StringUtils.isEmpty(Object) and replace remaining usage
Closes gh-25945
2020-10-21 16:09:26 +02:00
Stephane Nicoll 07769ddcc6 Start building against Reactor 2020.0.0 snapshots
See gh-25947
2020-10-21 14:21:49 +02:00
Stephane Nicoll c748870853 Merge branch '5.2.x' 2020-10-21 14:20:58 +02:00
Stephane Nicoll f7ec92c647 Start building against Reactor Dysprosium-SR13 snpshots
See gh-25946
2020-10-21 14:19:59 +02:00
Brian Clozel bd91dc70d5 Merge branch '5.2.x' 2020-10-20 15:41:02 +02:00
Brian Clozel 7bee3d1574 Optimize Jackson resource management in codecs
Prior to this commit, references to `JsonGenerator` and
`ByteArrayBuilder` were not closed/released within codecs calls.
This prevents Jackson from reusing more efficiently shared memory
resources.

This commit properly closes/releases Jackson resources in Spring MVC,
Spring WebFlux and Spring Messaging codecs.
A benchmark on WebFlux codecs (in both single value/streaming mode)
shows significant throughput and allocation improvements for small
payloads.

Closes gh-25910
2020-10-20 15:36:23 +02:00
jacopo-salamina dff9924a0c WebSocketTransportRegistration: javadoc typo
The documentation written for the method WebSocketTransportRegistration.setSendTimeLimit(int timeLimit) mentions that, if no time limit is specified, a default value of 10 seconds is used; however, later on the exact default value employed is incorrectly typed as 10 * 10000 ms, which would result in 100 seconds instead.
The javadoc has been updated in order to show the correct value in milliseconds (10 * 1000).
2020-10-20 12:52:46 +01:00
Rossen Stoyanchev bfb2ce6e2a SimpUser exposes Principal
Closes gh-25191
2020-10-19 21:40:09 +01:00
Rossen Stoyanchev 76eb5e6e2c Use of TcpClient extensible per connection
Closes gh-25889
2020-10-19 21:40:09 +01:00
Jupiter 43f595f80e Wording changes. Fixed typo 'CorsConfiguraiton' 2020-10-19 14:17:23 +01:00
Rossen Stoyanchev 6f131ba1c4 Polishing 2020-10-19 13:10:56 +01:00
Rossen Stoyanchev d18fbab7a8 Heartbeat in lieu of non-broker destination message
Closes gh-22822
2020-10-19 13:06:30 +01:00
Rossen Stoyanchev a4d0af802a Merge branch '5.2.x' into master 2020-10-16 19:16:11 +01:00
Rossen Stoyanchev db3d537e72 Protect SseEmitter against early I/O errors
Closes gh-25442
2020-10-16 19:08:30 +01:00
Rossen Stoyanchev a0727191e1 Polishing 2020-10-16 19:08:13 +01:00
Juergen Hoeller 702a05e926 Merge branch '5.2.x' 2020-10-16 15:33:37 +02:00
Brian Clozel 0df6d60fea Fix GitHub icon in build pipeline 2020-10-16 15:29:50 +02:00
Juergen Hoeller 5ab1097b43 Consistent dependency declaration order 2020-10-16 15:19:25 +02:00
Juergen Hoeller d77ecb26a9 Fail on warnings with compileGroovy (since it compiles Java code as well) 2020-10-16 15:18:48 +02:00
Brian Clozel 6d4b286b1a Generate release notes in release pipeline
This commit adds new tasks to the release pipeline, generating release
notes automatically using the issues in the current milestone and
pushing that as the content of the GitHub release.

Closes gh-25922
2020-10-16 14:51:44 +02:00
Brian Clozel 970afbc614 Remove Slack "here" mentions for failed builds
This was introduced by mistake in 6b978cb5dc
2020-10-16 14:51:44 +02:00
Juergen Hoeller f13c43bafe Consistent dependency declaration order 2020-10-16 12:22:04 +02:00
Juergen Hoeller 23211c784e Upgrade to Netty 4.1.53 and Tomcat 9.0.39
Correct version declaration for Apache HttpClient 5 and its reactive module.
2020-10-16 12:20:15 +02:00
Rossen Stoyanchev a2ff139edd Resolve UriComponentsBuilder argument with contextPath
Closes gh-25822
2020-10-15 22:08:32 +01:00
Rossen Stoyanchev 424e28bc35 Merge branch '5.2.x' into master 2020-10-15 17:18:33 +01:00
Rossen Stoyanchev 4952336419 Polishing contribution
See gh-25856
2020-10-15 17:14:27 +01:00
Rostislav Svoboda babcbd0d2b Simplify code for SimpleClientHttpRequestFactory.prepareConnection 2020-10-15 17:14:27 +01:00
Rossen Stoyanchev c45b106d95 Fix race issue and improve readAsynchronousFileChannel
Before this commit, ReadCompletionHandler delayed closing the channel
to allow an ongoing read to complete/fail so we can get a hold of the
associated DataBuffer and release it. This can be problematic since the
read take time to complete but even more importantly there was a race
condition where we didn't check if we've been disposed concurrently
while releasing the read flag.

This commit removes the delay and closes the channel immediately from
cancel() and that should in turn fail any ongoing read operation,
according to AsynchronousChannel, and the DataBuffer is released.

Further improvements include:
- combining the "reading" and "disposed" AtomicBoolean's into a
single "state" AtomicReference.
- an optimistic check to remain in READING mode and avoid state
switches when there is demand to continue reading.

Closes gh-25831
2020-10-15 17:10:22 +01:00
Brian Clozel b7d1bbb991 Configure Concourse release pipeline
Closes gh-25911
2020-10-15 18:06:37 +02:00
Brian Clozel 603a1dd319 Upgrade to Concourse Release Scripts 0.2.1
This release fixes a bug where Spring Boot specifics were baked in the
application itself.
2020-10-15 18:02:32 +02:00
Brian Clozel 6b978cb5dc Refactor Concourse pipeline
Extract duplicate snippets as document anchors.
2020-10-15 18:02:00 +02:00
Sam Brannen b79548d12e Add notes regarding enclosing test configuration inheritance
See gh-25912
2020-10-15 16:23:29 +02:00
Sam Brannen f9e11593a6 Document @NestedTestConfiguration support in reference manual
See gh-25912
2020-10-15 16:04:59 +02:00
Sam Brannen 57ac32b808 Upgrade to Gradle 6.7
Closes gh-25918
2020-10-15 13:48:50 +02:00
Sam Brannen 2eee56d0e2 Polish TestPropertySourceUtils 2020-10-14 15:03:22 +02:00
Sam Brannen a271a0a2bf Ignore @NestedTestConfiguration on enclosing class for nested interface
Closes gh-25917
2020-10-14 14:21:25 +02:00
Sam Brannen 69af56c4e9 Polish MetaAnnotationUtils 2020-10-14 14:21:13 +02:00
Sam Brannen a924677b49 Document @NestedTestConfiguration support in reference manual
See gh-25912
2020-10-14 11:04:46 +02:00
Rossen Stoyanchev 7e647ab1d8 HttpHeadResponseDecorator sets Content-Length for Mono only
See gh-25908
2020-10-13 23:45:51 +01:00
Rossen Stoyanchev abd79d43af Merge branch '5.2.x' into master 2020-10-13 23:42:43 +01:00
Rossen Stoyanchev 431ec906c4 Ensure buffers released in HttpHeadResponseDecorator
See gh-25908
2020-10-13 23:38:59 +01:00
Rossen Stoyanchev 1d96f6a266 HttpHeaderResponseDecorator checks for "Transfer-Encoding"
This commit extends the fix from b86c11cc9b
by checking for both existing Content-Length and Transfer-Encoding.

Closes gh-25908
2020-10-13 23:06:06 +01:00
Sam Brannen e5ae2cb0fe Support Propagation.NEVER for disabling test-managed transactions
Prior to this commit only Propagation.NOT_SUPPORTED was supported for
disabling test-managed transactions via the `propagation` attribute of
`@Transactional`.

This commit allows users to specify Propagation.NOT_SUPPORTED or
Propagation.NEVER to disable test-managed transactions.

Closes gh-25909
2020-10-13 18:30:50 +02:00
304 changed files with 10393 additions and 3102 deletions
+17 -17
View File
@@ -26,21 +26,21 @@ configure(allprojects) { project ->
dependencyManagement {
imports {
mavenBom "com.fasterxml.jackson:jackson-bom:2.11.3"
mavenBom "io.netty:netty-bom:4.1.52.Final"
mavenBom "io.projectreactor:reactor-bom:2020.0.0-RC2"
mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR7"
mavenBom "io.rsocket:rsocket-bom:1.1.0-RC1"
mavenBom "org.eclipse.jetty:jetty-bom:9.4.32.v20200930"
mavenBom "io.netty:netty-bom:4.1.53.Final"
mavenBom "io.projectreactor:reactor-bom:2020.0.0"
mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR8"
mavenBom "io.rsocket:rsocket-bom:1.1.0"
mavenBom "org.eclipse.jetty:jetty-bom:9.4.34.v20201102"
mavenBom "org.jetbrains.kotlin:kotlin-bom:1.4.10"
mavenBom "org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.4.0-M1"
mavenBom "org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.4.1"
mavenBom "org.junit:junit-bom:5.7.0"
}
dependencies {
dependencySet(group: 'org.apache.logging.log4j', version: '2.13.3') {
entry 'log4j-api'
entry 'log4j-core'
entry 'log4j-slf4j-impl'
entry 'log4j-jul'
entry 'log4j-slf4j-impl'
}
dependency "org.slf4j:slf4j-api:1.7.30"
dependency("com.google.code.findbugs:findbugs:3.0.1") {
@@ -65,11 +65,11 @@ configure(allprojects) { project ->
dependency "io.reactivex:rxjava-reactive-streams:1.2.1"
dependency "io.reactivex.rxjava2:rxjava:2.2.19"
dependency "io.reactivex.rxjava3:rxjava:3.0.7"
dependency "io.projectreactor.tools:blockhound:1.0.2.RELEASE"
dependency "io.projectreactor.tools:blockhound:1.0.4.RELEASE"
dependency "com.caucho:hessian:4.0.63"
dependency "com.fasterxml:aalto-xml:1.2.2"
dependency("com.fasterxml.woodstox:woodstox-core:6.2.1") {
dependency("com.fasterxml.woodstox:woodstox-core:6.2.3") {
exclude group: "stax", name: "stax-api"
}
dependency "com.google.code.gson:gson:2.8.6"
@@ -88,7 +88,7 @@ configure(allprojects) { project ->
entry 'jibx-run'
}
dependency "org.ogce:xpp3:1.1.6"
dependency "org.yaml:snakeyaml:1.26"
dependency "org.yaml:snakeyaml:1.27"
dependencySet(group: 'org.jetbrains.kotlinx', version: '1.0.0') {
entry 'kotlinx-serialization-core'
entry 'kotlinx-serialization-json'
@@ -123,19 +123,19 @@ configure(allprojects) { project ->
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.22.Final"
dependency "org.hibernate:hibernate-core:5.4.23.Final"
dependency "org.hibernate:hibernate-validator:6.1.6.Final"
dependency "org.webjars:webjars-locator-core:0.46"
dependency "org.webjars:underscorejs:1.8.3"
dependencySet(group: 'org.apache.tomcat', version: '9.0.38') {
dependencySet(group: 'org.apache.tomcat', version: '9.0.39') {
entry 'tomcat-util'
entry('tomcat-websocket') {
exclude group: "org.apache.tomcat", name: "tomcat-websocket-api"
exclude group: "org.apache.tomcat", name: "tomcat-servlet-api"
}
}
dependencySet(group: 'org.apache.tomcat.embed', version: '9.0.38') {
dependencySet(group: 'org.apache.tomcat.embed', version: '9.0.39') {
entry 'tomcat-embed-core'
entry 'tomcat-embed-websocket'
}
@@ -161,7 +161,7 @@ configure(allprojects) { project ->
exclude group: "commons-logging", name: "commons-logging"
}
dependency 'org.apache.httpcomponents.client5:httpclient5:5.0.3'
dependency 'org.apache.httpcomponents.core5:httpcore5-reactive:5.0.3'
dependency 'org.apache.httpcomponents.core5:httpcore5-reactive:5.0.2'
dependency "org.eclipse.jetty:jetty-reactive-httpclient:1.1.4"
dependency "org.jruby:jruby:9.2.13.0"
@@ -190,14 +190,14 @@ configure(allprojects) { project ->
dependency "org.testng:testng:7.3.0"
dependency "org.hamcrest:hamcrest:2.1"
dependency "org.awaitility:awaitility:3.1.6"
dependency "org.assertj:assertj-core:3.17.2"
dependency "org.assertj:assertj-core:3.18.0"
dependencySet(group: 'org.xmlunit', version: '2.6.2') {
entry 'xmlunit-assertj'
entry('xmlunit-matchers') {
exclude group: "org.hamcrest", name: "hamcrest-core"
}
}
dependencySet(group: 'org.mockito', version: '3.5.13') {
dependencySet(group: 'org.mockito', version: '3.6.0') {
entry('mockito-core') {
exclude group: "org.hamcrest", name: "hamcrest-core"
}
@@ -291,7 +291,6 @@ configure(allprojects) { project ->
repositories {
mavenCentral()
maven { url "https://repo.spring.io/libs-spring-framework-build" }
maven { url "https://repo.spring.io/milestone" } // Reactor
}
}
configurations.all {
@@ -380,6 +379,7 @@ configure([rootProject] + javaProjects) { project ->
"https://fasterxml.github.io/jackson-databind/javadoc/2.10/",
"https://fasterxml.github.io/jackson-dataformat-xml/javadoc/2.10/",
"https://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/",
"https://projectreactor.io/docs/test/release/api/",
"https://junit.org/junit4/javadoc/4.13.1/",
"https://junit.org/junit5/docs/5.7.0/api/"
] as String[]
+17
View File
@@ -0,0 +1,17 @@
changelog:
repository: spring-projects/spring-framework
sections:
- title: ":star: New Features"
labels:
- "type: enhancement"
- title: ":beetle: Bug Fixes"
labels:
- "type: bug"
- "type: regression"
- title: ":notebook_with_decorative_cover: Documentation"
labels:
- "type: documentation"
- title: ":hammer: Dependency Upgrades"
sort: "title"
labels:
- "type: dependency-upgrade"
+9
View File
@@ -0,0 +1,9 @@
logging:
level:
io.spring.concourse: DEBUG
distribute:
optional-deployments:
- '.*\\.zip'
spring:
main:
banner-mode: off
+1 -1
View File
@@ -11,7 +11,7 @@ rm -rf /var/lib/apt/lists/*
curl https://raw.githubusercontent.com/spring-io/concourse-java-scripts/v0.0.3/concourse-java.sh > /opt/concourse-java.sh
curl --output /opt/concourse-release-scripts.jar https://repo.spring.io/release/io/spring/concourse/releasescripts/concourse-release-scripts/0.2.0/concourse-release-scripts-0.2.0.jar
curl --output /opt/concourse-release-scripts.jar https://repo.spring.io/release/io/spring/concourse/releasescripts/concourse-release-scripts/0.2.1/concourse-release-scripts-0.2.1.jar
###########################################################
# JAVA
+159 -80
View File
@@ -1,28 +1,61 @@
anchors:
artifactory-task-params: &artifactory-task-params
ARTIFACTORY_SERVER: ((artifactory-server))
ARTIFACTORY_USERNAME: ((artifactory-username))
ARTIFACTORY_PASSWORD: ((artifactory-password))
bintray-task-params: &bintray-task-params
BINTRAY_SUBJECT: ((bintray-subject))
BINTRAY_REPO: ((bintray-repo))
BINTRAY_USERNAME: ((bintray-username))
BINTRAY_API_KEY: ((bintray-api-key))
docker-resource-source: &docker-resource-source
username: ((docker-hub-username))
password: ((docker-hub-password))
tag: 5.3.x
gradle-enterprise-task-params: &gradle-enterprise-task-params
GRADLE_ENTERPRISE_ACCESS_KEY: ((gradle_enterprise_secret_access_key))
GRADLE_ENTERPRISE_CACHE_USERNAME: ((gradle_enterprise_cache_user.username))
GRADLE_ENTERPRISE_CACHE_PASSWORD: ((gradle_enterprise_cache_user.password))
slack-fail-params: &slack-fail-params
text: >
:concourse-failed: <https://ci.spring.io/teams/${BUILD_TEAM_NAME}/pipelines/${BUILD_PIPELINE_NAME}/jobs/${BUILD_JOB_NAME}/builds/${BUILD_NAME}|${BUILD_PIPELINE_NAME} ${BUILD_JOB_NAME} failed!>
[$TEXT_FILE_CONTENT]
text_file: git-repo/build/build-scan-uri.txt
silent: true
icon_emoji: ":concourse:"
username: concourse-ci
sonatype-task-params: &sonatype-task-params
SONATYPE_USER_TOKEN: ((sonatype-user-token))
SONATYPE_PASSWORD_TOKEN: ((sonatype-user-token-password))
changelog-task-params: &changelog-task-params
name: generated-changelog/tag
tag: generated-changelog/tag
body: generated-changelog/changelog.md
github-task-params: &github-task-params
GITHUB_USERNAME: ((github-username))
GITHUB_TOKEN: ((github-ci-release-token))
resource_types:
- name: artifactory-resource
type: docker-image
source:
repository: springio/artifactory-resource
tag: 0.0.12-SNAPSHOT
tag: 0.0.12
- name: github-status-resource
type: docker-image
source:
repository: dpb587/github-status-resource
tag: master
- name: email
type: docker-image
source:
repository: pcfseceng/email-resource
tag: 1.0.19
- name: slack-notification
type: docker-image
source:
repository: cfcommunity/slack-notification-resource
tag: latest
resources:
- name: git-repo
type: git
icon: github-circle
icon: github
source:
uri: ((github-repo))
username: ((github-username))
@@ -37,7 +70,7 @@ resources:
location: Europe/Vienna
- name: ci-images-git-repo
type: git
icon: github-circle
icon: github
source:
uri: ((github-repo))
branch: ((branch))
@@ -46,34 +79,26 @@ resources:
type: docker-image
icon: docker
source:
<<: *docker-resource-source
repository: ((docker-hub-organization))/spring-framework-ci-image
username: ((docker-hub-username))
password: ((docker-hub-password))
tag: 5.3.x
- name: spring-framework-jdk11-ci-image
type: docker-image
icon: docker
source:
<<: *docker-resource-source
repository: ((docker-hub-organization))/spring-framework-jdk11-ci-image
username: ((docker-hub-username))
password: ((docker-hub-password))
tag: 5.3.x
- name: spring-framework-jdk14-ci-image
type: docker-image
icon: docker
source:
<<: *docker-resource-source
repository: ((docker-hub-organization))/spring-framework-jdk14-ci-image
username: ((docker-hub-username))
password: ((docker-hub-password))
tag: 5.3.x
- name: spring-framework-jdk15-ci-image
type: docker-image
icon: docker
source:
<<: *docker-resource-source
repository: ((docker-hub-organization))/spring-framework-jdk15-ci-image
username: ((docker-hub-username))
password: ((docker-hub-password))
tag: 5.3.x
- name: artifactory-repo
type: artifactory-resource
icon: package-variant
@@ -119,6 +144,24 @@ resources:
icon: slack
source:
url: ((slack-webhook-url))
- name: github-pre-release
type: github-release
icon: briefcase-download-outline
source:
owner: spring-projects
repository: spring-framework
access_token: ((github-ci-release-token))
pre_release: true
release: false
- name: github-release
type: github-release
icon: briefcase-download
source:
owner: spring-projects
repository: spring-framework
access_token: ((github-ci-release-token))
pre_release: false
jobs:
- name: build-spring-framework-ci-images
plan:
@@ -158,23 +201,14 @@ jobs:
file: git-repo/ci/tasks/build-project.yml
params:
BRANCH: ((branch))
GRADLE_ENTERPRISE_ACCESS_KEY: ((gradle_enterprise_secret_access_key))
GRADLE_ENTERPRISE_CACHE_USERNAME: ((gradle_enterprise_cache_user.username))
GRADLE_ENTERPRISE_CACHE_PASSWORD: ((gradle_enterprise_cache_user.password))
<<: *gradle-enterprise-task-params
on_failure:
do:
- put: repo-status-build
params: { state: "failure", commit: "git-repo" }
- put: slack-alert
params:
text: >
:concourse-failed: ${BUILD_PIPELINE_NAME} ${BUILD_JOB_NAME} failed!
[<https://ci.spring.io/teams/${BUILD_TEAM_NAME}/pipelines/${BUILD_PIPELINE_NAME}/jobs/${BUILD_JOB_NAME}/builds/${BUILD_NAME}|build log>]
[<$TEXT_FILE_CONTENT|build scan>]
text_file: git-repo/build/build-scan-uri.txt
silent: true
icon_emoji: ":concourse:"
username: concourse-ci
<<: *slack-fail-params
- put: repo-status-build
params: { state: "success", commit: "git-repo" }
- put: artifactory-repo
@@ -224,23 +258,14 @@ jobs:
file: git-repo/ci/tasks/check-project.yml
params:
BRANCH: ((branch))
GRADLE_ENTERPRISE_ACCESS_KEY: ((gradle_enterprise_secret_access_key))
GRADLE_ENTERPRISE_CACHE_USERNAME: ((gradle_enterprise_cache_user.username))
GRADLE_ENTERPRISE_CACHE_PASSWORD: ((gradle_enterprise_cache_user.password))
<<: *gradle-enterprise-task-params
on_failure:
do:
- put: repo-status-jdk11-build
params: { state: "failure", commit: "git-repo" }
- put: slack-alert
params:
text: >
:concourse-failed: ${BUILD_PIPELINE_NAME} ${BUILD_JOB_NAME} failed!
[<https://ci.spring.io/teams/${BUILD_TEAM_NAME}/pipelines/${BUILD_PIPELINE_NAME}/jobs/${BUILD_JOB_NAME}/builds/${BUILD_NAME}|build log>]
[<$TEXT_FILE_CONTENT|build scan>]
text_file: git-repo/build/build-scan-uri.txt
silent: true
icon_emoji: ":concourse:"
username: concourse-ci
<<: *slack-fail-params
- put: repo-status-jdk11-build
params: { state: "success", commit: "git-repo" }
- name: jdk14-build
@@ -261,23 +286,14 @@ jobs:
file: git-repo/ci/tasks/check-project.yml
params:
BRANCH: ((branch))
GRADLE_ENTERPRISE_ACCESS_KEY: ((gradle_enterprise_secret_access_key))
GRADLE_ENTERPRISE_CACHE_USERNAME: ((gradle_enterprise_cache_user.username))
GRADLE_ENTERPRISE_CACHE_PASSWORD: ((gradle_enterprise_cache_user.password))
<<: *gradle-enterprise-task-params
on_failure:
do:
- put: repo-status-jdk14-build
params: { state: "failure", commit: "git-repo" }
- put: slack-alert
params:
text: >
:concourse-failed: ${BUILD_PIPELINE_NAME} ${BUILD_JOB_NAME} failed!
[<https://ci.spring.io/teams/${BUILD_TEAM_NAME}/pipelines/${BUILD_PIPELINE_NAME}/jobs/${BUILD_JOB_NAME}/builds/${BUILD_NAME}|build log>]
[<$TEXT_FILE_CONTENT|build scan>]
text_file: git-repo/build/build-scan-uri.txt
silent: true
icon_emoji: ":concourse:"
username: concourse-ci
<<: *slack-fail-params
- put: repo-status-jdk14-build
params: { state: "success", commit: "git-repo" }
- name: jdk15-build
@@ -298,23 +314,14 @@ jobs:
file: git-repo/ci/tasks/check-project.yml
params:
BRANCH: ((branch))
GRADLE_ENTERPRISE_ACCESS_KEY: ((gradle_enterprise_secret_access_key))
GRADLE_ENTERPRISE_CACHE_USERNAME: ((gradle_enterprise_cache_user.username))
GRADLE_ENTERPRISE_CACHE_PASSWORD: ((gradle_enterprise_cache_user.password))
<<: *gradle-enterprise-task-params
on_failure:
do:
- put: repo-status-jdk15-build
params: { state: "failure", commit: "git-repo" }
- put: slack-alert
params:
text: >
:concourse-failed: ${BUILD_PIPELINE_NAME} ${BUILD_JOB_NAME} failed!
[<https://ci.spring.io/teams/${BUILD_TEAM_NAME}/pipelines/${BUILD_PIPELINE_NAME}/jobs/${BUILD_JOB_NAME}/builds/${BUILD_NAME}|build log>]
[<$TEXT_FILE_CONTENT|build scan>]
text_file: git-repo/build/build-scan-uri.txt
silent: true
icon_emoji: ":concourse:"
username: concourse-ci
<<: *slack-fail-params
- put: repo-status-jdk15-build
params: { state: "success", commit: "git-repo" }
- name: stage-milestone
@@ -328,9 +335,7 @@ jobs:
file: git-repo/ci/tasks/stage-version.yml
params:
RELEASE_TYPE: M
GRADLE_ENTERPRISE_ACCESS_KEY: ((gradle_enterprise_secret_access_key))
GRADLE_ENTERPRISE_CACHE_USERNAME: ((gradle_enterprise_cache_user.username))
GRADLE_ENTERPRISE_CACHE_PASSWORD: ((gradle_enterprise_cache_user.password))
<<: *gradle-enterprise-task-params
- put: artifactory-repo
params:
<<: *artifactory-params
@@ -355,9 +360,15 @@ jobs:
file: git-repo/ci/tasks/promote-version.yml
params:
RELEASE_TYPE: M
ARTIFACTORY_SERVER: ((artifactory-server))
ARTIFACTORY_USERNAME: ((artifactory-username))
ARTIFACTORY_PASSWORD: ((artifactory-password))
<<: *artifactory-task-params
- task: generate-changelog
file: git-repo/ci/tasks/generate-changelog.yml
params:
RELEASE_TYPE: M
<<: *github-task-params
- put: github-pre-release
params:
<<: *changelog-task-params
- name: stage-rc
serial: true
plan:
@@ -369,9 +380,7 @@ jobs:
file: git-repo/ci/tasks/stage-version.yml
params:
RELEASE_TYPE: RC
GRADLE_ENTERPRISE_ACCESS_KEY: ((gradle_enterprise_secret_access_key))
GRADLE_ENTERPRISE_CACHE_USERNAME: ((gradle_enterprise_cache_user.username))
GRADLE_ENTERPRISE_CACHE_PASSWORD: ((gradle_enterprise_cache_user.password))
<<: *gradle-enterprise-task-params
- put: artifactory-repo
params:
<<: *artifactory-params
@@ -396,13 +405,83 @@ jobs:
file: git-repo/ci/tasks/promote-version.yml
params:
RELEASE_TYPE: RC
ARTIFACTORY_SERVER: ((artifactory-server))
ARTIFACTORY_USERNAME: ((artifactory-username))
ARTIFACTORY_PASSWORD: ((artifactory-password))
<<: *artifactory-task-params
- task: generate-changelog
file: git-repo/ci/tasks/generate-changelog.yml
params:
RELEASE_TYPE: RC
<<: *github-task-params
- put: github-pre-release
params:
<<: *changelog-task-params
- name: stage-release
serial: true
plan:
- get: spring-framework-ci-image
- get: git-repo
trigger: false
- task: stage
image: spring-framework-ci-image
file: git-repo/ci/tasks/stage-version.yml
params:
RELEASE_TYPE: RELEASE
<<: *gradle-enterprise-task-params
- put: artifactory-repo
params:
<<: *artifactory-params
repo: libs-staging-local
- put: git-repo
params:
repository: stage-git-repo
- name: promote-release
serial: true
plan:
- get: spring-framework-ci-image
- get: git-repo
trigger: false
- get: artifactory-repo
trigger: false
passed: [stage-release]
params:
download_artifacts: false
save_build_info: true
- task: promote
image: spring-framework-ci-image
file: git-repo/ci/tasks/promote-version.yml
params:
RELEASE_TYPE: RELEASE
<<: *artifactory-task-params
<<: *bintray-task-params
- name: sync-to-maven-central
serial: true
plan:
- get: spring-framework-ci-image
- get: git-repo
- get: artifactory-repo
trigger: true
passed: [promote-release]
params:
download_artifacts: false
save_build_info: true
- task: sync-to-maven-central
image: spring-framework-ci-image
file: git-repo/ci/tasks/sync-to-maven-central.yml
params:
<<: *bintray-task-params
<<: *sonatype-task-params
- task: generate-changelog
file: git-repo/ci/tasks/generate-changelog.yml
params:
RELEASE_TYPE: RELEASE
<<: *github-task-params
- put: github-release
params:
<<: *changelog-task-params
groups:
- name: "Build"
- name: "builds"
jobs: ["build", "jdk11-build", "jdk14-build", "jdk15-build"]
- name: "Release"
jobs: ["stage-milestone","stage-rc", "promote-milestone","promote-rc"]
- name: "CI Images"
- name: "releases"
jobs: ["stage-milestone", "stage-rc", "stage-release", "promote-milestone","promote-rc", "promote-release", "sync-to-maven-central"]
- name: "ci-images"
jobs: ["build-spring-framework-ci-images"]
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
set -e
CONFIG_DIR=git-repo/ci/config
version=$( cat version/version )
java -jar /github-changelog-generator.jar \
--spring.config.location=${CONFIG_DIR}/changelog-generator.yml \
${version} generated-changelog/changelog.md
echo ${version} > generated-changelog/version
echo v${version} > generated-changelog/tag
+4 -1
View File
@@ -1,13 +1,16 @@
#!/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/' )
export BUILD_INFO_LOCATION=$(pwd)/artifactory-repo/build-info.json
java -jar /opt/concourse-release-scripts.jar promote $RELEASE_TYPE $BUILD_INFO_LOCATION > /dev/null || { exit 1; }
java -jar /opt/concourse-release-scripts.jar distribute $RELEASE_TYPE $BUILD_INFO_LOCATION > /dev/null || { exit 1; }
java -jar /opt/concourse-release-scripts.jar \
--spring.config.location=${CONFIG_DIR}/release-scripts.yml \
distribute $RELEASE_TYPE $BUILD_INFO_LOCATION > /dev/null || { exit 1; }
echo "Promotion complete"
echo $version > version/version
+8
View File
@@ -0,0 +1,8 @@
#!/bin/bash
export BUILD_INFO_LOCATION=$(pwd)/artifactory-repo/build-info.json
version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' )
java -jar /opt/concourse-release-scripts.jar syncToCentral "RELEASE" $BUILD_INFO_LOCATION || { exit 1; }
echo "Sync complete"
echo $version > version/version
+20
View File
@@ -0,0 +1,20 @@
---
platform: linux
image_resource:
type: docker-image
source:
repository: springio/github-changelog-generator
tag: '0.0.4'
inputs:
- name: git-repo
- name: version
outputs:
- name: generated-changelog
params:
GITHUB_ORGANIZATION:
GITHUB_REPO:
GITHUB_USERNAME:
GITHUB_TOKEN:
RELEASE_TYPE:
run:
path: git-repo/ci/scripts/generate-changelog.sh
+16
View File
@@ -0,0 +1,16 @@
---
platform: linux
inputs:
- name: git-repo
- name: artifactory-repo
outputs:
- name: version
params:
BINTRAY_REPO:
BINTRAY_SUBJECT:
BINTRAY_USERNAME:
BINTRAY_API_KEY:
SONATYPE_USER_TOKEN:
SONATYPE_PASSWORD_TOKEN:
run:
path: git-repo/ci/scripts/sync-to-maven-central.sh
+1 -1
View File
@@ -1,4 +1,4 @@
version=5.3.0-RC2
version=5.3.1
org.gradle.jvmargs=-Xmx1536M
org.gradle.caching=true
org.gradle.parallel=true
+3
View File
@@ -106,6 +106,9 @@ dokka {
externalDocumentationLink {
url = new URL("https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/")
}
externalDocumentationLink {
url = new URL("https://r2dbc.io/spec/0.8.3.RELEASE/api/")
}
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
@@ -221,10 +221,12 @@ public class MethodInvocationProceedingJoinPoint implements ProceedingJoinPoint,
@Override
@Nullable
public String[] getParameterNames() {
if (this.parameterNames == null) {
this.parameterNames = parameterNameDiscoverer.getParameterNames(getMethod());
String[] parameterNames = this.parameterNames;
if (parameterNames == null) {
parameterNames = parameterNameDiscoverer.getParameterNames(getMethod());
this.parameterNames = parameterNames;
}
return this.parameterNames;
return parameterNames;
}
@Override
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 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.
@@ -160,23 +160,12 @@ public class AspectJProxyFactory extends ProxyCreatorSupport {
}
/**
* Get the singleton aspect instance for the supplied aspect type. An instance
* is created if one cannot be found in the instance cache.
* Get the singleton aspect instance for the supplied aspect type.
* An instance is created if one cannot be found in the instance cache.
*/
private Object getSingletonAspectInstance(Class<?> aspectClass) {
// Quick check without a lock...
Object instance = aspectCache.get(aspectClass);
if (instance == null) {
synchronized (aspectCache) {
// To be safe, check within full lock now...
instance = aspectCache.get(aspectClass);
if (instance == null) {
instance = new SimpleAspectInstanceFactory(aspectClass).getAspectInstance();
aspectCache.put(aspectClass, instance);
}
}
}
return instance;
return aspectCache.computeIfAbsent(aspectClass,
clazz -> new SimpleAspectInstanceFactory(clazz).getAspectInstance());
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 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.
@@ -114,6 +114,15 @@ public interface Advised extends TargetClassAware {
*/
Advisor[] getAdvisors();
/**
* Return the number of advisors applying to this proxy.
* <p>The default implementation delegates to {@code getAdvisors().length}.
* @since 5.3.1
*/
default int getAdvisorCount() {
return getAdvisors().length;
}
/**
* Add an advisor at the end of the advisor chain.
* <p>The Advisor may be an {@link org.springframework.aop.IntroductionAdvisor},
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 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.
@@ -95,12 +95,6 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
*/
private List<Advisor> advisors = new ArrayList<>();
/**
* Array updated on changes to the advisors list, which is easier
* to manipulate internally.
*/
private Advisor[] advisorArray = new Advisor[0];
/**
* No-arg constructor for use as a JavaBean.
@@ -244,7 +238,12 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
@Override
public final Advisor[] getAdvisors() {
return this.advisorArray;
return this.advisors.toArray(new Advisor[0]);
}
@Override
public int getAdvisorCount() {
return this.advisors.size();
}
@Override
@@ -292,7 +291,6 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
}
}
updateAdvisorArray();
adviceChanged();
}
@@ -339,7 +337,6 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
Assert.notNull(advisor, "Advisor must not be null");
this.advisors.add(advisor);
}
updateAdvisorArray();
adviceChanged();
}
}
@@ -363,27 +360,18 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
"Illegal position " + pos + " in advisor list with size " + this.advisors.size());
}
this.advisors.add(pos, advisor);
updateAdvisorArray();
adviceChanged();
}
/**
* Bring the array up to date with the list.
*/
protected final void updateAdvisorArray() {
this.advisorArray = this.advisors.toArray(new Advisor[0]);
}
/**
* Allows uncontrolled access to the {@link List} of {@link Advisor Advisors}.
* <p>Use with care, and remember to {@link #updateAdvisorArray() refresh the advisor array}
* and {@link #adviceChanged() fire advice changed events} when making any modifications.
* <p>Use with care, and remember to {@link #adviceChanged() fire advice changed events}
* when making any modifications.
*/
protected final List<Advisor> getAdvisorsInternal() {
return this.advisors;
}
@Override
public void addAdvice(Advice advice) throws AopConfigException {
int pos = this.advisors.size();
@@ -521,7 +509,6 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
Assert.notNull(advisor, "Advisor must not be null");
this.advisors.add(advisor);
}
updateAdvisorArray();
adviceChanged();
}
@@ -536,7 +523,6 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
copy.advisorChainFactory = this.advisorChainFactory;
copy.interfaces = this.interfaces;
copy.advisors = this.advisors;
copy.updateAdvisorArray();
return copy;
}
@@ -553,7 +539,6 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
this.methodCache = new ConcurrentHashMap<>(32);
}
@Override
public String toProxyConfigString() {
return toString();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 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.
@@ -206,7 +206,7 @@ public abstract class AopProxyUtils {
* Check equality of the advisors behind the given AdvisedSupport objects.
*/
public static boolean equalsAdvisors(AdvisedSupport a, AdvisedSupport b) {
return Arrays.equals(a.getAdvisors(), b.getAdvisors());
return a.getAdvisorCount() == b.getAdvisorCount() && Arrays.equals(a.getAdvisors(), b.getAdvisors());
}
@@ -125,7 +125,7 @@ class CglibAopProxy implements AopProxy, Serializable {
*/
public CglibAopProxy(AdvisedSupport config) throws AopConfigException {
Assert.notNull(config, "AdvisedSupport must not be null");
if (config.getAdvisors().length == 0 && config.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE) {
if (config.getAdvisorCount() == 0 && config.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE) {
throw new AopConfigException("No advisors and no TargetSource specified");
}
this.advised = config;
@@ -942,11 +942,11 @@ class CglibAopProxy implements AopProxy, Serializable {
}
// Advice instance identity is unimportant to the proxy class:
// All that matters is type and ordering.
Advisor[] thisAdvisors = this.advised.getAdvisors();
Advisor[] thatAdvisors = otherAdvised.getAdvisors();
if (thisAdvisors.length != thatAdvisors.length) {
if (this.advised.getAdvisorCount() != otherAdvised.getAdvisorCount()) {
return false;
}
Advisor[] thisAdvisors = this.advised.getAdvisors();
Advisor[] thatAdvisors = otherAdvised.getAdvisors();
for (int i = 0; i < thisAdvisors.length; i++) {
Advisor thisAdvisor = thisAdvisors[i];
Advisor thatAdvisor = thatAdvisors[i];
@@ -104,7 +104,7 @@ final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializa
*/
public JdkDynamicAopProxy(AdvisedSupport config) throws AopConfigException {
Assert.notNull(config, "AdvisedSupport must not be null");
if (config.getAdvisors().length == 0 && config.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE) {
if (config.getAdvisorCount() == 0 && config.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE) {
throw new AopConfigException("No advisors and no TargetSource specified");
}
this.advised = config;
+1
View File
@@ -25,6 +25,7 @@ sourceSets {
compileGroovy {
sourceCompatibility = 1.8
targetCompatibility = 1.8
options.compilerArgs += "-Werror"
}
// This module also builds Kotlin code and the compileKotlin task naturally depends on
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 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.
@@ -422,9 +422,12 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
}
return;
}
else {
throw createNotWritablePropertyException(tokens.canonicalName);
if (this.suppressNotWritablePropertyException) {
// Optimization for common ignoreUnknown=true scenario since the
// exception would be caught and swallowed higher up anyway...
return;
}
throw createNotWritablePropertyException(tokens.canonicalName);
}
Object oldValue = null;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 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.
@@ -40,6 +40,8 @@ public abstract class AbstractPropertyAccessor extends TypeConverterSupport impl
private boolean autoGrowNestedPaths = false;
boolean suppressNotWritablePropertyException = false;
@Override
public void setExtractOldValueForEditor(boolean extractOldValueForEditor) {
@@ -89,30 +91,41 @@ public abstract class AbstractPropertyAccessor extends TypeConverterSupport impl
List<PropertyAccessException> propertyAccessExceptions = null;
List<PropertyValue> propertyValues = (pvs instanceof MutablePropertyValues ?
((MutablePropertyValues) pvs).getPropertyValueList() : Arrays.asList(pvs.getPropertyValues()));
for (PropertyValue pv : propertyValues) {
try {
// This method may throw any BeansException, which won't be caught
if (ignoreUnknown) {
this.suppressNotWritablePropertyException = true;
}
try {
for (PropertyValue pv : propertyValues) {
// setPropertyValue may throw any BeansException, which won't be caught
// here, if there is a critical failure such as no matching field.
// We can attempt to deal only with less serious exceptions.
setPropertyValue(pv);
}
catch (NotWritablePropertyException ex) {
if (!ignoreUnknown) {
throw ex;
try {
setPropertyValue(pv);
}
// Otherwise, just ignore it and continue...
}
catch (NullValueInNestedPathException ex) {
if (!ignoreInvalid) {
throw ex;
catch (NotWritablePropertyException ex) {
if (!ignoreUnknown) {
throw ex;
}
// Otherwise, just ignore it and continue...
}
// Otherwise, just ignore it and continue...
}
catch (PropertyAccessException ex) {
if (propertyAccessExceptions == null) {
propertyAccessExceptions = new ArrayList<>();
catch (NullValueInNestedPathException ex) {
if (!ignoreInvalid) {
throw ex;
}
// Otherwise, just ignore it and continue...
}
propertyAccessExceptions.add(ex);
catch (PropertyAccessException ex) {
if (propertyAccessExceptions == null) {
propertyAccessExceptions = new ArrayList<>();
}
propertyAccessExceptions.add(ex);
}
}
}
finally {
if (ignoreUnknown) {
this.suppressNotWritablePropertyException = false;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 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.
@@ -92,8 +92,7 @@ public class DirectFieldAccessor extends AbstractNestablePropertyAccessor {
@Override
protected NotWritablePropertyException createNotWritablePropertyException(String propertyName) {
PropertyMatches matches = PropertyMatches.forField(propertyName, getRootClass());
throw new NotWritablePropertyException(
getRootClass(), getNestedPath() + propertyName,
throw new NotWritablePropertyException(getRootClass(), getNestedPath() + propertyName,
matches.buildErrorMessage(), matches.getPossibleMatches());
}
@@ -644,21 +644,20 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
}
synchronized (this) {
if (!this.cached) {
Object cachedFieldValue = null;
if (value != null || this.required) {
this.cachedFieldValue = desc;
cachedFieldValue = desc;
registerDependentBeans(beanName, autowiredBeanNames);
if (autowiredBeanNames.size() == 1) {
String autowiredBeanName = autowiredBeanNames.iterator().next();
if (beanFactory.containsBean(autowiredBeanName) &&
beanFactory.isTypeMatch(autowiredBeanName, field.getType())) {
this.cachedFieldValue = new ShortcutDependencyDescriptor(
cachedFieldValue = new ShortcutDependencyDescriptor(
desc, autowiredBeanName, field.getType());
}
}
}
else {
this.cachedFieldValue = null;
}
this.cachedFieldValue = cachedFieldValue;
this.cached = true;
}
}
@@ -26,9 +26,6 @@ import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.factory.support.RootBeanDefinition;
@@ -69,8 +66,6 @@ public class InjectionMetadata {
};
private static final Log logger = LogFactory.getLog(InjectionMetadata.class);
private final Class<?> targetClass;
private final Collection<InjectedElement> injectedElements;
@@ -110,9 +105,6 @@ public class InjectionMetadata {
if (!beanDefinition.isExternallyManagedConfigMember(member)) {
beanDefinition.registerExternallyManagedConfigMember(member);
checkedElements.add(element);
if (logger.isTraceEnabled()) {
logger.trace("Registered injected element on class [" + this.targetClass.getName() + "]: " + element);
}
}
}
this.checkedElements = checkedElements;
@@ -124,9 +116,6 @@ public class InjectionMetadata {
(checkedElements != null ? checkedElements : this.injectedElements);
if (!elementsToIterate.isEmpty()) {
for (InjectedElement element : elementsToIterate) {
if (logger.isTraceEnabled()) {
logger.trace("Processing injected element of bean '" + beanName + "': " + element);
}
element.inject(target, beanName, pvs);
}
}
@@ -157,7 +146,8 @@ public class InjectionMetadata {
* @since 5.2
*/
public static InjectionMetadata forElements(Collection<InjectedElement> elements, Class<?> clazz) {
return (elements.isEmpty() ? InjectionMetadata.EMPTY : new InjectionMetadata(clazz, elements));
return (elements.isEmpty() ? new InjectionMetadata(clazz, Collections.emptyList()) :
new InjectionMetadata(clazz, elements));
}
/**
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 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.
@@ -40,7 +40,6 @@ import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* {@link AutowireCandidateResolver} implementation that matches bean definition qualifiers
@@ -188,7 +187,7 @@ public class QualifierAnnotationAutowireCandidateResolver extends GenericTypeAwa
foundMeta = true;
// Only accept fallback match if @Qualifier annotation has a value...
// Otherwise it is just a marker for a custom qualifier annotation.
if ((fallbackToMeta && StringUtils.isEmpty(AnnotationUtils.getValue(metaAnn))) ||
if ((fallbackToMeta && ObjectUtils.isEmpty(AnnotationUtils.getValue(metaAnn))) ||
!checkQualifier(bdHolder, metaAnn, typeConverter)) {
return false;
}
@@ -1288,10 +1288,8 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
* @param registry the PropertyEditorRegistry to initialize
*/
protected void registerCustomEditors(PropertyEditorRegistry registry) {
PropertyEditorRegistrySupport registrySupport =
(registry instanceof PropertyEditorRegistrySupport ? (PropertyEditorRegistrySupport) registry : null);
if (registrySupport != null) {
registrySupport.useConfigValueEditors();
if (registry instanceof PropertyEditorRegistrySupport) {
((PropertyEditorRegistrySupport) registry).useConfigValueEditors();
}
if (!this.propertyEditorRegistrars.isEmpty()) {
for (PropertyEditorRegistrar registrar : this.propertyEditorRegistrars) {
@@ -43,8 +43,6 @@ import java.util.stream.IntStream;
import javax.annotation.Priority;
import javax.security.auth.Subject;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeansException;
@@ -121,8 +119,6 @@ import static org.mockito.Mockito.verify;
*/
class DefaultListableBeanFactoryTests {
private static final Log factoryLog = LogFactory.getLog(DefaultListableBeanFactory.class);
private DefaultListableBeanFactory lbf = new DefaultListableBeanFactory();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 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.
@@ -126,7 +126,7 @@ public class DefinitionMetadataEqualsHashCodeTests {
assertThat(equal.hashCode()).as("Hash code for equal instances should match").isEqualTo(master.hashCode());
assertThat(notEqual).as("Should not be equal").isNotEqualTo(master);
assertThat(notEqual.hashCode()).as("Hash code for non-equal instances should not match").isNotEqualTo((long) master.hashCode());
assertThat(notEqual.hashCode()).as("Hash code for non-equal instances should not match").isNotEqualTo(master.hashCode());
assertThat(subclass).as("Subclass should be equal").isEqualTo(master);
assertThat(subclass.hashCode()).as("Hash code for subclass should match").isEqualTo(master.hashCode());
@@ -155,8 +155,7 @@ public abstract class AbstractCacheManager implements CacheManager, Initializing
* @param name the name of the cache to be added
*/
private void updateCacheNames(String name) {
Set<String> cacheNames = new LinkedHashSet<>(this.cacheNames.size() + 1);
cacheNames.addAll(this.cacheNames);
Set<String> cacheNames = new LinkedHashSet<>(this.cacheNames);
cacheNames.add(name);
this.cacheNames = Collections.unmodifiableSet(cacheNames);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 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.
@@ -17,6 +17,7 @@
package org.springframework.context;
import java.util.EventListener;
import java.util.function.Consumer;
/**
* Interface to be implemented by application event listeners.
@@ -45,4 +46,17 @@ public interface ApplicationListener<E extends ApplicationEvent> extends EventLi
*/
void onApplicationEvent(E event);
/**
* Create a new {@code ApplicationListener} for the given payload consumer.
* @param consumer the event payload consumer
* @param <T> the type of the event payload
* @return a corresponding {@code ApplicationListener} instance
* @since 5.3
* @see PayloadApplicationEvent
*/
static <T> ApplicationListener<PayloadApplicationEvent<T>> forPayload(Consumer<T> consumer) {
return event -> consumer.accept(event.getPayload());
}
}
@@ -99,7 +99,7 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
* @see #setBeanNameGenerator
*/
public static final AnnotationBeanNameGenerator IMPORT_BEAN_NAME_GENERATOR =
new FullyQualifiedAnnotationBeanNameGenerator();
FullyQualifiedAnnotationBeanNameGenerator.INSTANCE;
private static final String IMPORT_REGISTRY_BEAN_NAME =
ConfigurationClassPostProcessor.class.getName() + ".importRegistry";
@@ -43,6 +43,15 @@ import org.springframework.util.Assert;
*/
public class FullyQualifiedAnnotationBeanNameGenerator extends AnnotationBeanNameGenerator {
/**
* A convenient constant for a default {@code FullyQualifiedAnnotationBeanNameGenerator}
* instance, as used for configuration-level import purposes.
* @since 5.2.11
*/
public static final FullyQualifiedAnnotationBeanNameGenerator INSTANCE =
new FullyQualifiedAnnotationBeanNameGenerator();
@Override
protected String buildDefaultBeanName(BeanDefinition definition) {
String beanClassName = definition.getBeanClassName();
@@ -37,6 +37,7 @@ import org.springframework.context.ApplicationEvent;
import org.springframework.context.PayloadApplicationEvent;
import org.springframework.context.expression.AnnotatedElementKey;
import org.springframework.core.BridgeMethodResolver;
import org.springframework.core.Ordered;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.ResolvableType;
@@ -95,6 +96,12 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe
private EventExpressionEvaluator evaluator;
/**
* Construct a new ApplicationListenerMethodAdapter.
* @param beanName the name of the bean to invoke the listener method on
* @param targetClass the target class that the method is declared on
* @param method the listener method to invoke
*/
public ApplicationListenerMethodAdapter(String beanName, Class<?> targetClass, Method method) {
this.beanName = beanName;
this.method = BridgeMethodResolver.findBridgedMethod(method);
@@ -135,7 +142,7 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe
private static int resolveOrder(Method method) {
Order ann = AnnotatedElementUtils.findMergedAnnotation(method, Order.class);
return (ann != null ? ann.value() : 0);
return (ann != null ? ann.value() : Ordered.LOWEST_PRECEDENCE);
}
@@ -332,6 +339,14 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe
return this.applicationContext.getBean(this.beanName);
}
/**
* Return the target listener method.
* @since 5.3
*/
protected Method getTargetMethod() {
return this.targetMethod;
}
/**
* Return the condition to use.
* <p>Matches the {@code condition} attribute of the {@link EventListener}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 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.
@@ -29,6 +29,7 @@ import org.springframework.core.Ordered;
*
* @author Stephane Nicoll
* @since 4.2
* @see ApplicationListenerMethodAdapter
*/
public class DefaultEventListenerFactory implements EventListenerFactory, Ordered {
@@ -307,6 +307,23 @@ public class ConfigurationClassProcessingTests {
assertThat(tb.getLawyer()).isEqualTo(ctx.getBean(NestedTestBean.class));
}
@Test // gh-26019
public void autowiringWithDynamicPrototypeBeanClass() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
ConfigWithDynamicPrototype.class, PrototypeDependency.class);
PrototypeInterface p1 = ctx.getBean(PrototypeInterface.class, 1);
assertThat(p1).isInstanceOf(PrototypeOne.class);
assertThat(((PrototypeOne) p1).prototypeDependency).isNotNull();
PrototypeInterface p2 = ctx.getBean(PrototypeInterface.class, 2);
assertThat(p2).isInstanceOf(PrototypeTwo.class);
PrototypeInterface p3 = ctx.getBean(PrototypeInterface.class, 1);
assertThat(p3).isInstanceOf(PrototypeOne.class);
assertThat(((PrototypeOne) p3).prototypeDependency).isNotNull();
}
/**
* Creates a new {@link BeanFactory}, populates it with a {@link BeanDefinition}
@@ -632,4 +649,42 @@ public class ConfigurationClassProcessingTests {
}
}
static class PrototypeDependency {
}
interface PrototypeInterface {
}
static class PrototypeOne extends AbstractPrototype {
@Autowired
PrototypeDependency prototypeDependency;
}
static class PrototypeTwo extends AbstractPrototype {
// no autowired dependency here, in contrast to above
}
static class AbstractPrototype implements PrototypeInterface {
}
@Configuration
static class ConfigWithDynamicPrototype {
@Bean
@Scope(value = "prototype")
public PrototypeInterface getDemoBean( int i) {
switch ( i) {
case 1: return new PrototypeOne();
case 2:
default:
return new PrototypeTwo();
}
}
}
}
@@ -26,6 +26,7 @@ import org.springframework.aop.framework.ProxyFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.PayloadApplicationEvent;
import org.springframework.core.Ordered;
import org.springframework.core.ResolvableType;
import org.springframework.core.ResolvableTypeProvider;
import org.springframework.core.annotation.Order;
@@ -161,7 +162,7 @@ public class ApplicationListenerMethodAdapterTests extends AbstractApplicationEv
Method method = ReflectionUtils.findMethod(
SampleEvents.class, "handleGenericString", GenericTestEvent.class);
ApplicationListenerMethodAdapter adapter = createTestInstance(method);
assertThat(adapter.getOrder()).isEqualTo(0);
assertThat(adapter.getOrder()).isEqualTo(Ordered.LOWEST_PRECEDENCE);
}
@Test
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 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.
@@ -22,8 +22,11 @@ import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.PayloadApplicationEvent;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.stereotype.Component;
import static org.assertj.core.api.Assertions.assertThat;
@@ -34,14 +37,42 @@ import static org.assertj.core.api.Assertions.assertThat;
public class PayloadApplicationEventTests {
@Test
@SuppressWarnings({ "rawtypes", "resource" })
public void testEventClassWithInterface() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AuditableListener.class);
AuditablePayloadEvent event = new AuditablePayloadEvent<>(this, "xyz");
AuditablePayloadEvent<String> event = new AuditablePayloadEvent<>(this, "xyz");
ac.publishEvent(event);
assertThat(ac.getBean(AuditableListener.class).events.contains(event)).isTrue();
}
@Test
public void testProgrammaticEventListener() {
List<Auditable> events = new ArrayList<>();
ApplicationListener<AuditablePayloadEvent<String>> listener = events::add;
ConfigurableApplicationContext ac = new GenericApplicationContext();
ac.addApplicationListener(listener);
ac.refresh();
AuditablePayloadEvent<String> event = new AuditablePayloadEvent<>(this, "xyz");
ac.publishEvent(event);
assertThat(events.contains(event)).isTrue();
}
@Test
public void testProgrammaticPayloadListener() {
List<String> events = new ArrayList<>();
ApplicationListener<PayloadApplicationEvent<String>> listener = ApplicationListener.forPayload(events::add);
ConfigurableApplicationContext ac = new GenericApplicationContext();
ac.addApplicationListener(listener);
ac.refresh();
AuditablePayloadEvent<String> event = new AuditablePayloadEvent<>(this, "xyz");
ac.publishEvent(event);
assertThat(events.contains(event.getPayload())).isTrue();
}
public interface Auditable {
}
+7 -7
View File
@@ -55,20 +55,20 @@ dependencies {
optional("io.reactivex.rxjava3:rxjava")
optional("io.netty:netty-buffer")
testCompile("io.projectreactor:reactor-test")
testCompile("javax.annotation:javax.annotation-api")
testCompile("com.google.code.findbugs:jsr305")
testCompile("org.xmlunit:xmlunit-assertj")
testCompile("org.xmlunit:xmlunit-matchers")
testCompile("javax.annotation:javax.annotation-api")
testCompile("javax.xml.bind:jaxb-api")
testCompile("com.fasterxml.woodstox:woodstox-core")
testCompile("org.xmlunit:xmlunit-assertj")
testCompile("org.xmlunit:xmlunit-matchers")
testCompile(project(":kotlin-coroutines"))
testCompile("io.projectreactor.tools:blockhound")
testFixturesImplementation("com.google.code.findbugs:jsr305")
testFixturesImplementation("io.projectreactor:reactor-test")
testFixturesImplementation("org.assertj:assertj-core")
testFixturesImplementation("com.google.code.findbugs:jsr305")
testFixturesImplementation("org.junit.platform:junit-platform-launcher")
testFixturesImplementation("org.junit.jupiter:junit-jupiter-api")
testFixturesImplementation("org.junit.jupiter:junit-jupiter-params")
testFixturesImplementation("org.assertj:assertj-core")
testFixturesImplementation("org.xmlunit:xmlunit-assertj")
}
@@ -99,7 +99,7 @@ jar {
}
test {
// make sure the classes dir is used on the test classpath (required by ResourceTests)
// Make sure the classes dir is used on the test classpath (required by ResourceTests)
// When test fixtures are involved, the JAR is used by default
classpath = sourceSets.main.output.classesDirs + classpath - files(jar.archiveFile)
}
}
@@ -0,0 +1,110 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.codec;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.infra.Blackhole;
import reactor.core.publisher.Flux;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.util.MimeType;
/**
* Benchmarks for {@link DataBufferUtils}.
*
* @author Rossen Stoyanchev
*/
@BenchmarkMode(Mode.Throughput)
public class StringDecoderBenchmark {
@Benchmark
public void parseSseLines(SseLinesState state, Blackhole blackhole) {
blackhole.consume(state.parseLines().blockLast());
}
@State(Scope.Benchmark)
@SuppressWarnings({"NotNullFieldNotInitialized", "ConstantConditions"})
public static class SseLinesState {
private static final Charset CHARSET = StandardCharsets.UTF_8;
private static final ResolvableType ELEMENT_TYPE = ResolvableType.forClass(String.class);
@Param("10240")
int totalSize;
@Param("2000")
int chunkSize;
List<DataBuffer> chunks;
StringDecoder decoder = StringDecoder.textPlainOnly(Arrays.asList("\r\n", "\n"), false);
MimeType mimeType = new MimeType("text", "plain", CHARSET);
@Setup(Level.Trial)
public void setup() {
String eventTemplate = "id:$1\n" +
"event:some-event\n" +
":some-comment-$1-aa\n" +
":some-comment-$1-bb\n" +
"data:abcdefg-$1-hijklmnop-$1-qrstuvw-$1-xyz-$1\n\n";
int eventLength = String.format(eventTemplate, String.format("%05d", 1)).length();
int eventCount = this.totalSize / eventLength;
DataBufferFactory bufferFactory = new DefaultDataBufferFactory();
this.chunks = Flux.range(1, eventCount)
.map(index -> String.format(eventTemplate, String.format("%05d", index)))
.buffer(this.chunkSize > eventLength ? this.chunkSize / eventLength : 1)
.map(strings -> String.join("", strings))
.map(chunk -> {
byte[] bytes = chunk.getBytes(CHARSET);
DataBuffer buffer = bufferFactory.allocateBuffer(bytes.length);
buffer.write(bytes);
return buffer;
})
.collectList()
.block();
}
public Flux<String> parseLines() {
Flux<DataBuffer> input = Flux.fromIterable(this.chunks).doOnNext(DataBufferUtils::retain);
return this.decoder.decode(input, ELEMENT_TYPE, this.mimeType, Collections.emptyMap());
}
}
}
@@ -53,7 +53,7 @@ public interface Opcodes {
/**
* <i>Experimental, use at your own risk. This field will be renamed when it becomes stable, this
* will break existing code using it. Only code compiled with --enable-preview can use this.</i>
* <p>SPRING PATCH: no preview mode check for ASM 9 experimental, enabling it by default.
* <p>SPRING PATCH: no preview mode check for ASM 10 experimental, enabling it by default.
*/
int ASM10_EXPERIMENTAL = 1 << 24 | 10 << 16 | 0 << 8;
@@ -21,10 +21,10 @@ import org.springframework.asm.Opcodes;
final class AsmApi {
/**
* SPRING PATCH: always returns ASM7.
* SPRING PATCH: always returns ASM9.
*/
static int value() {
return Opcodes.ASM7;
return Opcodes.ASM9;
}
private AsmApi() {
@@ -77,7 +77,6 @@ public abstract class KotlinDetector {
/**
* Return {@code true} if the method is a suspending function.
* @author Sebastien Deleuze
* @since 5.3
*/
public static boolean isSuspendingFunction(Method method) {
@@ -264,14 +264,12 @@ public class ReactiveAdapterRegistry {
registry.registerReactiveType(
ReactiveTypeDescriptor.multiValue(io.reactivex.Observable.class, io.reactivex.Observable::empty),
source -> ((io.reactivex.Observable<?>) source).toFlowable(io.reactivex.BackpressureStrategy.BUFFER),
source -> io.reactivex.Flowable.fromPublisher(source)
.toObservable()
io.reactivex.Observable::fromPublisher
);
registry.registerReactiveType(
ReactiveTypeDescriptor.singleRequiredValue(io.reactivex.Single.class),
source -> ((io.reactivex.Single<?>) source).toFlowable(),
source -> io.reactivex.Flowable.fromPublisher(source)
.toObservable().singleElement().toSingle()
io.reactivex.Single::fromPublisher
);
registry.registerReactiveType(
ReactiveTypeDescriptor.singleOptionalValue(io.reactivex.Maybe.class, io.reactivex.Maybe::empty),
@@ -282,8 +280,7 @@ public class ReactiveAdapterRegistry {
registry.registerReactiveType(
ReactiveTypeDescriptor.noValue(io.reactivex.Completable.class, io.reactivex.Completable::complete),
source -> ((io.reactivex.Completable) source).toFlowable(),
source -> io.reactivex.Flowable.fromPublisher(source)
.toObservable().ignoreElements()
io.reactivex.Completable::fromPublisher
);
}
}
@@ -304,30 +301,26 @@ public class ReactiveAdapterRegistry {
io.reactivex.rxjava3.core.Observable::empty),
source -> ((io.reactivex.rxjava3.core.Observable<?>) source).toFlowable(
io.reactivex.rxjava3.core.BackpressureStrategy.BUFFER),
source -> io.reactivex.rxjava3.core.Flowable.fromPublisher(source)
.toObservable()
io.reactivex.rxjava3.core.Observable::fromPublisher
);
registry.registerReactiveType(
ReactiveTypeDescriptor.singleRequiredValue(io.reactivex.rxjava3.core.Single.class),
source -> ((io.reactivex.rxjava3.core.Single<?>) source).toFlowable(),
source -> io.reactivex.rxjava3.core.Flowable.fromPublisher(source)
.toObservable().singleElement().toSingle()
io.reactivex.rxjava3.core.Single::fromPublisher
);
registry.registerReactiveType(
ReactiveTypeDescriptor.singleOptionalValue(
io.reactivex.rxjava3.core.Maybe.class,
io.reactivex.rxjava3.core.Maybe::empty),
source -> ((io.reactivex.rxjava3.core.Maybe<?>) source).toFlowable(),
source -> io.reactivex.rxjava3.core.Flowable.fromPublisher(source)
.toObservable().singleElement()
io.reactivex.rxjava3.core.Maybe::fromPublisher
);
registry.registerReactiveType(
ReactiveTypeDescriptor.noValue(
io.reactivex.rxjava3.core.Completable.class,
io.reactivex.rxjava3.core.Completable::complete),
source -> ((io.reactivex.rxjava3.core.Completable) source).toFlowable(),
source -> io.reactivex.rxjava3.core.Flowable.fromPublisher(source)
.toObservable().ignoreElements()
io.reactivex.rxjava3.core.Completable::fromPublisher
);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 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.
@@ -18,8 +18,8 @@ package org.springframework.core.annotation;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.function.IntFunction;
@@ -31,10 +31,11 @@ import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* Collector implementations that provide various reduction operations for
* {@link Collector} implementations that provide various reduction operations for
* {@link MergedAnnotation} instances.
*
* @author Phillip Webb
* @author Sam Brannen
* @since 5.2
*/
public abstract class MergedAnnotationCollectors {
@@ -52,13 +53,16 @@ public abstract class MergedAnnotationCollectors {
* Create a new {@link Collector} that accumulates merged annotations to a
* {@link LinkedHashSet} containing {@linkplain MergedAnnotation#synthesize()
* synthesized} versions.
* <p>The collector returned by this method is effectively equivalent to
* {@code Collectors.mapping(MergedAnnotation::synthesize, Collectors.toCollection(LinkedHashSet::new))}
* but avoids the creation of a composite collector.
* @param <A> the annotation type
* @return a {@link Collector} which collects and synthesizes the
* annotations into a {@link Set}
*/
public static <A extends Annotation> Collector<MergedAnnotation<A>, ?, Set<A>> toAnnotationSet() {
return Collector.of(ArrayList<A>::new, (list, annotation) -> list.add(annotation.synthesize()),
MergedAnnotationCollectors::addAll, LinkedHashSet::new);
return Collector.of(LinkedHashSet::new, (set, annotation) -> set.add(annotation.synthesize()),
MergedAnnotationCollectors::combiner);
}
/**
@@ -90,14 +94,14 @@ public abstract class MergedAnnotationCollectors {
IntFunction<R[]> generator) {
return Collector.of(ArrayList::new, (list, annotation) -> list.add(annotation.synthesize()),
MergedAnnotationCollectors::addAll, list -> list.toArray(generator.apply(list.size())));
MergedAnnotationCollectors::combiner, list -> list.toArray(generator.apply(list.size())));
}
/**
* Create a new {@link Collector} that accumulates merged annotations to an
* Create a new {@link Collector} that accumulates merged annotations to a
* {@link MultiValueMap} with items {@linkplain MultiValueMap#add(Object, Object)
* added} from each merged annotation
* {@link MergedAnnotation#asMap(Adapt...) as a map}.
* {@linkplain MergedAnnotation#asMap(Adapt...) as a map}.
* @param <A> the annotation type
* @param adaptations the adaptations that should be applied to the annotation values
* @return a {@link Collector} which collects and synthesizes the
@@ -111,13 +115,13 @@ public abstract class MergedAnnotationCollectors {
}
/**
* Create a new {@link Collector} that accumulates merged annotations to an
* Create a new {@link Collector} that accumulates merged annotations to a
* {@link MultiValueMap} with items {@linkplain MultiValueMap#add(Object, Object)
* added} from each merged annotation
* {@link MergedAnnotation#asMap(Adapt...) as a map}.
* {@linkplain MergedAnnotation#asMap(Adapt...) as a map}.
* @param <A> the annotation type
* @param adaptations the adaptations that should be applied to the annotation values
* @param finisher the finisher function for the new {@link MultiValueMap}
* @param adaptations the adaptations that should be applied to the annotation values
* @return a {@link Collector} which collects and synthesizes the
* annotations into a {@link LinkedMultiValueMap}
* @see #toMultiValueMap(MergedAnnotation.Adapt...)
@@ -130,7 +134,7 @@ public abstract class MergedAnnotationCollectors {
IDENTITY_FINISH_CHARACTERISTICS : NO_CHARACTERISTICS);
return Collector.of(LinkedMultiValueMap::new,
(map, annotation) -> annotation.asMap(adaptations).forEach(map::add),
MergedAnnotationCollectors::merge, finisher, characteristics);
MergedAnnotationCollectors::combiner, finisher, characteristics);
}
@@ -138,13 +142,22 @@ public abstract class MergedAnnotationCollectors {
return instance == candidate;
}
private static <E, L extends List<E>> L addAll(L list, L additions) {
list.addAll(additions);
return list;
/**
* {@link Collector#combiner() Combiner} for collections.
* <p>This method is only invoked if the {@link java.util.stream.Stream} is
* processed in {@linkplain java.util.stream.Stream#parallel() parallel}.
*/
private static <E, C extends Collection<E>> C combiner(C collection, C additions) {
collection.addAll(additions);
return collection;
}
private static <K, V> MultiValueMap<K, V> merge(MultiValueMap<K, V> map,
MultiValueMap<K, V> additions) {
/**
* {@link Collector#combiner() Combiner} for multi-value maps.
* <p>This method is only invoked if the {@link java.util.stream.Stream} is
* processed in {@linkplain java.util.stream.Stream#parallel() parallel}.
*/
private static <K, V> MultiValueMap<K, V> combiner(MultiValueMap<K, V> map, MultiValueMap<K, V> additions) {
map.addAll(additions);
return map;
}
@@ -21,21 +21,20 @@ import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Consumer;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferLimitException;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DataBufferWrapper;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.core.io.buffer.LimitedDataBufferList;
import org.springframework.core.io.buffer.PooledDataBuffer;
import org.springframework.core.log.LogFormatUtils;
@@ -45,12 +44,12 @@ import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
/**
* Decode from a data buffer stream to a {@code String} stream. Before decoding, this decoder
* realigns the incoming data buffers so that each buffer ends with a newline.
* This is to make sure that multibyte characters are decoded properly, and do not cross buffer
* boundaries. The default delimiters ({@code \n}, {@code \r\n})can be customized.
*
* <p>Partially inspired by Netty's {@code DelimiterBasedFrameDecoder}.
* Decode from a data buffer stream to a {@code String} stream, either splitting
* or aggregating incoming data chunks to realign along newlines delimiters
* and produce a stream of strings. This is useful for streaming but is also
* necessary to ensure that that multibyte characters can be decoded correctly,
* avoiding split-character issues. The default delimiters used by default are
* {@code \n} and {@code \r\n} but that can be customized.
*
* @author Sebastien Deleuze
* @author Brian Clozel
@@ -115,21 +114,22 @@ public final class StringDecoder extends AbstractDataBufferDecoder<String> {
byte[][] delimiterBytes = getDelimiterBytes(mimeType);
Flux<DataBuffer> inputFlux = Flux.defer(() -> {
DataBufferUtils.Matcher matcher = DataBufferUtils.matcher(delimiterBytes);
LimitedDataBufferList chunks = new LimitedDataBufferList(getMaxInMemorySize());
DataBufferUtils.Matcher matcher = DataBufferUtils.matcher(delimiterBytes);
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
LimitChecker limiter = new LimitChecker(getMaxInMemorySize());
return Flux.from(input)
.concatMapIterable(buffer -> endFrameAfterDelimiter(buffer, matcher))
.doOnNext(limiter)
.bufferUntil(buffer -> buffer instanceof EndFrameBuffer)
.map(list -> joinAndStrip(list, this.stripDelimiter))
.doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release);
});
return super.decode(inputFlux, elementType, mimeType, hints);
return Flux.from(input)
.concatMapIterable(buffer -> processDataBuffer(buffer, matcher, chunks))
.concatWith(Mono.defer(() -> {
if (chunks.isEmpty()) {
return Mono.empty();
}
DataBuffer lastBuffer = chunks.get(0).factory().join(chunks);
chunks.clear();
return Mono.just(lastBuffer);
}))
.doOnTerminate(chunks::releaseAndClear)
.doOnDiscard(PooledDataBuffer.class, PooledDataBuffer::release)
.map(buffer -> decode(buffer, elementType, mimeType, hints));
}
private byte[][] getDelimiterBytes(@Nullable MimeType mimeType) {
@@ -142,6 +142,43 @@ public final class StringDecoder extends AbstractDataBufferDecoder<String> {
});
}
private Collection<DataBuffer> processDataBuffer(
DataBuffer buffer, DataBufferUtils.Matcher matcher, LimitedDataBufferList chunks) {
try {
List<DataBuffer> result = null;
do {
int endIndex = matcher.match(buffer);
if (endIndex == -1) {
chunks.add(buffer);
DataBufferUtils.retain(buffer); // retain after add (may raise DataBufferLimitException)
break;
}
int startIndex = buffer.readPosition();
int length = (endIndex - startIndex + 1);
DataBuffer slice = buffer.retainedSlice(startIndex, length);
if (this.stripDelimiter) {
slice.writePosition(slice.writePosition() - matcher.delimiter().length);
}
result = (result != null ? result : new ArrayList<>());
if (chunks.isEmpty()) {
result.add(slice);
}
else {
chunks.add(slice);
result.add(buffer.factory().join(chunks));
chunks.clear();
}
buffer.readPosition(endIndex + 1);
}
while (buffer.readableByteCount() > 0);
return (result != null ? result : Collections.emptyList());
}
finally {
DataBufferUtils.release(buffer);
}
}
@Override
public String decode(DataBuffer dataBuffer, ResolvableType elementType,
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
@@ -166,68 +203,6 @@ public final class StringDecoder extends AbstractDataBufferDecoder<String> {
}
}
/**
* Finds the first match and longest delimiter, {@link EndFrameBuffer} just after it.
* @param dataBuffer the buffer to find delimiters in
* @param matcher used to find the first delimiters
* @return a flux of buffers, containing {@link EndFrameBuffer} after each delimiter that was
* found in {@code dataBuffer}. Returns Flux, because returning List (w/ flatMapIterable)
* results in memory leaks due to pre-fetching.
*/
private static List<DataBuffer> endFrameAfterDelimiter(DataBuffer dataBuffer, DataBufferUtils.Matcher matcher) {
List<DataBuffer> result = new ArrayList<>();
try {
do {
int endIdx = matcher.match(dataBuffer);
if (endIdx != -1) {
int readPosition = dataBuffer.readPosition();
int length = (endIdx - readPosition + 1);
DataBuffer slice = dataBuffer.retainedSlice(readPosition, length);
result.add(slice);
result.add(new EndFrameBuffer(matcher.delimiter()));
dataBuffer.readPosition(endIdx + 1);
}
else {
result.add(DataBufferUtils.retain(dataBuffer));
break;
}
}
while (dataBuffer.readableByteCount() > 0);
}
finally {
DataBufferUtils.release(dataBuffer);
}
return result;
}
/**
* Joins the given list of buffers. If the list ends with a {@link EndFrameBuffer}, it is
* removed. If {@code stripDelimiter} is {@code true} and the resulting buffer ends with
* a delimiter, it is removed.
* @param dataBuffers the data buffers to join
* @param stripDelimiter whether to strip the delimiter
* @return the joined buffer
*/
private static DataBuffer joinAndStrip(List<DataBuffer> dataBuffers, boolean stripDelimiter) {
Assert.state(!dataBuffers.isEmpty(), "DataBuffers should not be empty");
byte[] matchingDelimiter = null;
int lastIdx = dataBuffers.size() - 1;
DataBuffer lastBuffer = dataBuffers.get(lastIdx);
if (lastBuffer instanceof EndFrameBuffer) {
matchingDelimiter = ((EndFrameBuffer) lastBuffer).delimiter();
dataBuffers.remove(lastIdx);
}
DataBuffer result = dataBuffers.get(0).factory().join(dataBuffers);
if (stripDelimiter && matchingDelimiter != null) {
result.writePosition(result.writePosition() - matchingDelimiter.length);
}
return result;
}
/**
* Create a {@code StringDecoder} for {@code "text/plain"}.
* @param stripDelimiter this flag is ignored
@@ -285,46 +260,4 @@ public final class StringDecoder extends AbstractDataBufferDecoder<String> {
new MimeType("text", "plain", DEFAULT_CHARSET), MimeTypeUtils.ALL);
}
private static class EndFrameBuffer extends DataBufferWrapper {
private static final DataBuffer BUFFER = DefaultDataBufferFactory.sharedInstance.wrap(new byte[0]);
private final byte[] delimiter;
public EndFrameBuffer(byte[] delimiter) {
super(BUFFER);
this.delimiter = delimiter;
}
public byte[] delimiter() {
return this.delimiter;
}
}
private static class LimitChecker implements Consumer<DataBuffer> {
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
private final LimitedDataBufferList list;
LimitChecker(int maxInMemorySize) {
this.list = new LimitedDataBufferList(maxInMemorySize);
}
@Override
public void accept(DataBuffer buffer) {
if (buffer instanceof EndFrameBuffer) {
this.list.clear();
}
try {
this.list.add(buffer);
}
catch (DataBufferLimitException ex) {
DataBufferUtils.release(buffer);
throw ex;
}
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 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.
@@ -118,7 +118,7 @@ public final class Property {
}
// package private
// Package private
MethodParameter getMethodParameter() {
return this.methodParameter;
@@ -132,7 +132,7 @@ public final class Property {
}
// internal helpers
// Internal helpers
private String resolveName() {
if (this.readMethod != null) {
@@ -142,10 +142,13 @@ public final class Property {
}
else {
index = this.readMethod.getName().indexOf("is");
if (index == -1) {
throw new IllegalArgumentException("Not a getter method");
if (index != -1) {
index += 2;
}
else {
// Record-style plain accessor method, e.g. name()
index = 0;
}
index += 2;
}
return StringUtils.uncapitalize(this.readMethod.getName().substring(index));
}
@@ -30,7 +30,6 @@ import java.nio.channels.WritableByteChannel;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.Callable;
@@ -568,65 +567,290 @@ public abstract class DataBufferUtils {
/**
* Return a {@link Matcher} for the given delimiter.
* The matcher can be used to find the delimiters in data buffers.
* The matcher can be used to find the delimiters in a stream of data buffers.
* @param delimiter the delimiter bytes to find
* @return the matcher
* @since 5.2
*/
public static Matcher matcher(byte[] delimiter) {
Assert.isTrue(delimiter.length > 0, "Delimiter must not be empty");
return new KnuthMorrisPrattMatcher(delimiter);
return createMatcher(delimiter);
}
/** Return a {@link Matcher} for the given delimiters.
* The matcher can be used to find the delimiters in data buffers.
/**
* Return a {@link Matcher} for the given delimiters.
* The matcher can be used to find the delimiters in a stream of data buffers.
* @param delimiters the delimiters bytes to find
* @return the matcher
* @since 5.2
*/
public static Matcher matcher(byte[]... delimiters) {
Assert.isTrue(delimiters.length > 0, "Delimiters must not be empty");
if (delimiters.length == 1) {
return matcher(delimiters[0]);
}
else {
Matcher[] matchers = new Matcher[delimiters.length];
for (int i = 0; i < delimiters.length; i++) {
matchers[i] = matcher(delimiters[i]);
}
return new CompositeMatcher(matchers);
return (delimiters.length == 1 ? createMatcher(delimiters[0]) : new CompositeMatcher(delimiters));
}
private static NestedMatcher createMatcher(byte[] delimiter) {
Assert.isTrue(delimiter.length > 0, "Delimiter must not be empty");
switch (delimiter.length) {
case 1:
return (delimiter[0] == 10 ? SingleByteMatcher.NEWLINE_MATCHER : new SingleByteMatcher(delimiter));
case 2:
return new TwoByteMatcher(delimiter);
default:
return new KnuthMorrisPrattMatcher(delimiter);
}
}
/**
* Defines an object that matches a data buffer against a delimiter.
* Contract to find delimiter(s) against one or more data buffers that can
* be passed one at a time to the {@link #match(DataBuffer)} method.
*
* @since 5.2
* @see #match(DataBuffer)
*/
public interface Matcher {
/**
* Returns the position of the final matching delimiter byte that matches the given buffer,
* or {@code -1} if not found.
* @param dataBuffer the buffer in which to search for the delimiter
* @return the position of the final matching delimiter, or {@code -1} if not found.
* Find the first matching delimiter and return the index of the last
* byte of the delimiter, or {@code -1} if not found.
*/
int match(DataBuffer dataBuffer);
/**
* Return the delimiter used for this matcher.
* @return the delimiter
* Return the delimiter from the last invocation of {@link #match(DataBuffer)}.
*/
byte[] delimiter();
/**
* Resets the state of this matcher.
* Reset the state of this matcher.
*/
void reset();
}
/**
* Matcher that supports searching for multiple delimiters.
*/
private static class CompositeMatcher implements Matcher {
private static final byte[] NO_DELIMITER = new byte[0];
private final NestedMatcher[] matchers;
byte[] longestDelimiter = NO_DELIMITER;
CompositeMatcher(byte[][] delimiters) {
this.matchers = initMatchers(delimiters);
}
private static NestedMatcher[] initMatchers(byte[][] delimiters) {
NestedMatcher[] matchers = new NestedMatcher[delimiters.length];
for (int i = 0; i < delimiters.length; i++) {
matchers[i] = createMatcher(delimiters[i]);
}
return matchers;
}
@Override
public int match(DataBuffer dataBuffer) {
this.longestDelimiter = NO_DELIMITER;
for (int pos = dataBuffer.readPosition(); pos < dataBuffer.writePosition(); pos++) {
byte b = dataBuffer.getByte(pos);
for (NestedMatcher matcher : this.matchers) {
if (matcher.match(b) && matcher.delimiter().length > this.longestDelimiter.length) {
this.longestDelimiter = matcher.delimiter();
}
}
if (this.longestDelimiter != NO_DELIMITER) {
reset();
return pos;
}
}
return -1;
}
@Override
public byte[] delimiter() {
Assert.state(this.longestDelimiter != NO_DELIMITER, "Illegal state!");
return this.longestDelimiter;
}
@Override
public void reset() {
for (NestedMatcher matcher : this.matchers) {
matcher.reset();
}
}
}
/**
* Matcher that can be nested within {@link CompositeMatcher} where multiple
* matchers advance together using the same index, one byte at a time.
*/
private interface NestedMatcher extends Matcher {
/**
* Perform a match against the next byte of the stream and return true
* if the delimiter is fully matched.
*/
boolean match(byte b);
}
/**
* Matcher for a single byte delimiter.
*/
private static class SingleByteMatcher implements NestedMatcher {
static SingleByteMatcher NEWLINE_MATCHER = new SingleByteMatcher(new byte[] {10});
private final byte[] delimiter;
SingleByteMatcher(byte[] delimiter) {
Assert.isTrue(delimiter.length == 1, "Expected a 1 byte delimiter");
this.delimiter = delimiter;
}
@Override
public int match(DataBuffer dataBuffer) {
for (int pos = dataBuffer.readPosition(); pos < dataBuffer.writePosition(); pos++) {
byte b = dataBuffer.getByte(pos);
if (match(b)) {
return pos;
}
}
return -1;
}
@Override
public boolean match(byte b) {
return this.delimiter[0] == b;
}
@Override
public byte[] delimiter() {
return this.delimiter;
}
@Override
public void reset() {
}
}
/**
* Base class for a {@link NestedMatcher}.
*/
private static abstract class AbstractNestedMatcher implements NestedMatcher {
private final byte[] delimiter;
private int matches = 0;
protected AbstractNestedMatcher(byte[] delimiter) {
this.delimiter = delimiter;
}
protected void setMatches(int index) {
this.matches = index;
}
protected int getMatches() {
return this.matches;
}
@Override
public int match(DataBuffer dataBuffer) {
for (int pos = dataBuffer.readPosition(); pos < dataBuffer.writePosition(); pos++) {
byte b = dataBuffer.getByte(pos);
if (match(b)) {
reset();
return pos;
}
}
return -1;
}
@Override
public boolean match(byte b) {
if (b == this.delimiter[this.matches]) {
this.matches++;
return (this.matches == delimiter().length);
}
return false;
}
@Override
public byte[] delimiter() {
return this.delimiter;
}
@Override
public void reset() {
this.matches = 0;
}
}
/**
* Matcher with a 2 byte delimiter that does not benefit from a
* Knuth-Morris-Pratt suffix-prefix table.
*/
private static class TwoByteMatcher extends AbstractNestedMatcher {
protected TwoByteMatcher(byte[] delimiter) {
super(delimiter);
Assert.isTrue(delimiter.length == 2, "Expected a 2-byte delimiter");
}
}
/**
* Implementation of {@link Matcher} that uses the Knuth-Morris-Pratt algorithm.
* @see <a href="https://www.nayuki.io/page/knuth-morris-pratt-string-matching">Knuth-Morris-Pratt string matching</a>
*/
private static class KnuthMorrisPrattMatcher extends AbstractNestedMatcher {
private final int[] table;
public KnuthMorrisPrattMatcher(byte[] delimiter) {
super(delimiter);
this.table = longestSuffixPrefixTable(delimiter);
}
private static int[] longestSuffixPrefixTable(byte[] delimiter) {
int[] result = new int[delimiter.length];
result[0] = 0;
for (int i = 1; i < delimiter.length; i++) {
int j = result[i - 1];
while (j > 0 && delimiter[i] != delimiter[j]) {
j = result[j - 1];
}
if (delimiter[i] == delimiter[j]) {
j++;
}
result[i] = j;
}
return result;
}
@Override
public boolean match(byte b) {
while (getMatches() > 0 && b != delimiter()[getMatches()]) {
setMatches(this.table[getMatches() - 1]);
}
return super.match(b);
}
}
private static class ReadableByteChannelGenerator implements Consumer<SynchronousSink<DataBuffer>> {
private final ReadableByteChannel channel;
@@ -683,9 +907,7 @@ public abstract class DataBufferUtils {
private final AtomicLong position;
private final AtomicBoolean reading = new AtomicBoolean();
private final AtomicBoolean disposed = new AtomicBoolean();
private final AtomicReference<State> state = new AtomicReference<>(State.IDLE);
public ReadCompletionHandler(AsynchronousFileChannel channel,
FluxSink<DataBuffer> sink, long position, DataBufferFactory dataBufferFactory, int bufferSize) {
@@ -697,39 +919,68 @@ public abstract class DataBufferUtils {
this.bufferSize = bufferSize;
}
public void read() {
if (this.sink.requestedFromDownstream() > 0 &&
isNotDisposed() &&
this.reading.compareAndSet(false, true)) {
DataBuffer dataBuffer = this.dataBufferFactory.allocateBuffer(this.bufferSize);
ByteBuffer byteBuffer = dataBuffer.asByteBuffer(0, this.bufferSize);
this.channel.read(byteBuffer, this.position.get(), dataBuffer, this);
/**
* Invoked when Reactive Streams consumer signals demand.
*/
public void request(long n) {
tryRead();
}
/**
* Invoked when Reactive Streams consumer cancels.
*/
public void cancel() {
this.state.getAndSet(State.DISPOSED);
// According java.nio.channels.AsynchronousChannel "if an I/O operation is outstanding
// on the channel and the channel's close method is invoked, then the I/O operation
// fails with the exception AsynchronousCloseException". That should invoke the failed
// callback below and the current DataBuffer should be released.
closeChannel(this.channel);
}
private void tryRead() {
if (this.sink.requestedFromDownstream() > 0 && this.state.compareAndSet(State.IDLE, State.READING)) {
read();
}
}
private void read() {
DataBuffer dataBuffer = this.dataBufferFactory.allocateBuffer(this.bufferSize);
ByteBuffer byteBuffer = dataBuffer.asByteBuffer(0, this.bufferSize);
this.channel.read(byteBuffer, this.position.get(), dataBuffer, this);
}
@Override
public void completed(Integer read, DataBuffer dataBuffer) {
if (isNotDisposed()) {
if (read != -1) {
this.position.addAndGet(read);
dataBuffer.writePosition(read);
this.sink.next(dataBuffer);
this.reading.set(false);
read();
}
else {
release(dataBuffer);
closeChannel(this.channel);
if (this.disposed.compareAndSet(false, true)) {
this.sink.complete();
}
this.reading.set(false);
}
}
else {
if (this.state.get().equals(State.DISPOSED)) {
release(dataBuffer);
closeChannel(this.channel);
this.reading.set(false);
return;
}
if (read == -1) {
release(dataBuffer);
closeChannel(this.channel);
this.state.set(State.DISPOSED);
this.sink.complete();
return;
}
this.position.addAndGet(read);
dataBuffer.writePosition(read);
this.sink.next(dataBuffer);
// Stay in READING mode if there is demand
if (this.sink.requestedFromDownstream() > 0) {
read();
return;
}
// Release READING mode and then try again in case of concurrent "request"
if (this.state.compareAndSet(State.READING, State.IDLE)) {
tryRead();
}
}
@@ -737,26 +988,12 @@ public abstract class DataBufferUtils {
public void failed(Throwable exc, DataBuffer dataBuffer) {
release(dataBuffer);
closeChannel(this.channel);
if (this.disposed.compareAndSet(false, true)) {
this.sink.error(exc);
}
this.reading.set(false);
this.state.set(State.DISPOSED);
this.sink.error(exc);
}
public void request(long n) {
read();
}
public void cancel() {
if (this.disposed.compareAndSet(false, true)) {
if (!this.reading.get()) {
closeChannel(this.channel);
}
}
}
private boolean isNotDisposed() {
return !this.disposed.get();
private enum State {
IDLE, READING, DISPOSED
}
}
@@ -895,124 +1132,4 @@ public abstract class DataBufferUtils {
}
}
/**
* Implementation of {@link Matcher} that uses the Knuth-Morris-Pratt algorithm.
* @see <a href="https://www.nayuki.io/page/knuth-morris-pratt-string-matching">Knuth-Morris-Pratt string matching</a>
*/
private static class KnuthMorrisPrattMatcher implements Matcher {
private final byte[] delimiter;
private final int[] table;
private int matches = 0;
public KnuthMorrisPrattMatcher(byte[] delimiter) {
this.delimiter = Arrays.copyOf(delimiter, delimiter.length);
this.table = longestSuffixPrefixTable(delimiter);
}
private static int[] longestSuffixPrefixTable(byte[] delimiter) {
int[] result = new int[delimiter.length];
result[0] = 0;
for (int i = 1; i < delimiter.length; i++) {
int j = result[i - 1];
while (j > 0 && delimiter[i] != delimiter[j]) {
j = result[j - 1];
}
if (delimiter[i] == delimiter[j]) {
j++;
}
result[i] = j;
}
return result;
}
@Override
public int match(DataBuffer dataBuffer) {
for (int i = dataBuffer.readPosition(); i < dataBuffer.writePosition(); i++) {
byte b = dataBuffer.getByte(i);
while (this.matches > 0 && b != this.delimiter[this.matches]) {
this.matches = this.table[this.matches - 1];
}
if (b == this.delimiter[this.matches]) {
this.matches++;
if (this.matches == this.delimiter.length) {
reset();
return i;
}
}
}
return -1;
}
@Override
public byte[] delimiter() {
return Arrays.copyOf(this.delimiter, this.delimiter.length);
}
@Override
public void reset() {
this.matches = 0;
}
}
/**
* Implementation of {@link Matcher} that wraps several other matchers.
*/
private static class CompositeMatcher implements Matcher {
private static final byte[] NO_DELIMITER = new byte[0];
private final Matcher[] matchers;
byte[] longestDelimiter = NO_DELIMITER;
public CompositeMatcher(Matcher[] matchers) {
this.matchers = matchers;
}
@Override
public int match(DataBuffer dataBuffer) {
this.longestDelimiter = NO_DELIMITER;
int bestEndIdx = Integer.MAX_VALUE;
for (Matcher matcher : this.matchers) {
int endIdx = matcher.match(dataBuffer);
if (endIdx != -1 &&
endIdx <= bestEndIdx &&
matcher.delimiter().length > this.longestDelimiter.length) {
bestEndIdx = endIdx;
this.longestDelimiter = matcher.delimiter();
}
}
if (bestEndIdx == Integer.MAX_VALUE) {
this.longestDelimiter = NO_DELIMITER;
return -1;
}
else {
reset();
return bestEndIdx;
}
}
@Override
public byte[] delimiter() {
Assert.state(this.longestDelimiter != NO_DELIMITER, "Illegal state!");
return this.longestDelimiter;
}
@Override
public void reset() {
for (Matcher matcher : this.matchers) {
matcher.reset();
}
}
}
}
@@ -24,10 +24,13 @@ import org.springframework.core.metrics.StartupStep;
/**
* {@link ApplicationStartup} implementation for the Java Flight Recorder.
* <p>This variant records {@link StartupStep} as Flight Recorder events; because such events
* only support base types, the {@link StartupStep.Tags} are serialized as a single String attribute.
* <p>Once this is configured on the application context, you can record data by launching the application
* with recording enabled: {@code java -XX:StartFlightRecording:filename=recording.jfr,duration=10s -jar app.jar}.
* <p>This variant records {@link StartupStep} as Flight Recorder events. Because
* such events only support base types, the
* {@link org.springframework.core.metrics.StartupStep.Tags} are serialized as a
* single String attribute.
* <p>Once this is configured on the application context, you can record data by
* launching the application with recording enabled:
* {@code java -XX:StartFlightRecording:filename=recording.jfr,duration=10s -jar app.jar}.
*
* @author Brian Clozel
* @since 5.3
@@ -457,7 +457,6 @@ public abstract class CollectionUtils {
* @since 3.1
*/
public static <K, V> MultiValueMap<K, V> toMultiValueMap(Map<K, List<V>> targetMap) {
Assert.notNull(targetMap, "'targetMap' must not be null");
return new MultiValueMapAdapter<>(targetMap);
}
@@ -105,17 +105,15 @@ public class ConcurrentLruCache<K, V> {
}
// Generate value first, to prevent size inconsistency
V value = this.generator.apply(key);
int cacheSize = this.size;
if (cacheSize == this.sizeLimit) {
if (this.size == this.sizeLimit) {
K leastUsed = this.queue.poll();
if (leastUsed != null) {
this.cache.remove(leastUsed);
cacheSize--;
}
}
this.queue.offer(key);
this.cache.put(key, value);
this.size = cacheSize + 1;
this.size = this.cache.size();
return value;
}
finally {
@@ -123,6 +121,50 @@ public class ConcurrentLruCache<K, V> {
}
}
/**
* Determine whether the given key is present in this cache.
* @param key the key to check for
* @return {@code true} if the key is present,
* {@code false} if there was no matching key
*/
public boolean contains(K key) {
return this.cache.containsKey(key);
}
/**
* Immediately remove the given key and any associated value.
* @param key the key to evict the entry for
* @return {@code true} if the key was present before,
* {@code false} if there was no matching key
*/
public boolean remove(K key) {
this.lock.writeLock().lock();
try {
boolean wasPresent = (this.cache.remove(key) != null);
this.queue.remove(key);
this.size = this.cache.size();
return wasPresent;
}
finally {
this.lock.writeLock().unlock();
}
}
/**
* Immediately remove all entries from this cache.
*/
public void clear() {
this.lock.writeLock().lock();
try {
this.cache.clear();
this.queue.clear();
this.size = 0;
}
finally {
this.lock.writeLock().unlock();
}
}
/**
* Return the current size of the cache.
* @see #sizeLimit()
@@ -35,7 +35,8 @@ import java.util.Map;
* @param <K> the key type
* @param <V> the value element type
*/
public class LinkedMultiValueMap<K, V> extends MultiValueMapAdapter<K, V> implements Serializable, Cloneable {
public class LinkedMultiValueMap<K, V> extends MultiValueMapAdapter<K, V> // new public base class in 5.3
implements Serializable, Cloneable {
private static final long serialVersionUID = 3801124242820219131L;
@@ -30,23 +30,30 @@ import org.springframework.lang.Nullable;
*
* @author Arjen Poutsma
* @author Juergen Hoeller
* @since 3.1
* @since 5.3
* @param <K> the key type
* @param <V> the value element type
* @see CollectionUtils#toMultiValueMap
* @see LinkedMultiValueMap
*/
@SuppressWarnings("serial")
class MultiValueMapAdapter<K, V> implements MultiValueMap<K, V>, Serializable {
public class MultiValueMapAdapter<K, V> implements MultiValueMap<K, V>, Serializable {
private final Map<K, List<V>> targetMap;
MultiValueMapAdapter(Map<K, List<V>> targetMap) {
/**
* Wrap the given target {@link Map} as a {@link MultiValueMap} adapter.
* @param targetMap the plain target {@code Map}
*/
public MultiValueMapAdapter(Map<K, List<V>> targetMap) {
Assert.notNull(targetMap, "'targetMap' must not be null");
this.targetMap = targetMap;
}
// MultiValueMap implementation
@Override
@Nullable
public V getFirst(K key) {
@@ -96,6 +103,9 @@ class MultiValueMapAdapter<K, V> implements MultiValueMap<K, V>, Serializable {
return singleValueMap;
}
// Map implementation
@Override
public int size() {
return this.targetMap.size();
@@ -128,7 +128,6 @@ public abstract class ObjectUtils {
* @see Optional#isPresent()
* @see ObjectUtils#isEmpty(Object[])
* @see StringUtils#hasLength(CharSequence)
* @see StringUtils#isEmpty(Object)
* @see CollectionUtils#isEmpty(java.util.Collection)
* @see CollectionUtils#isEmpty(java.util.Map)
*/
@@ -91,9 +91,10 @@ public abstract class StringUtils {
* {@link #hasLength(String)} or {@link #hasText(String)} instead.</b>
* @param str the candidate object (possibly a {@code String})
* @since 3.2.1
* @see #hasLength(String)
* @see #hasText(String)
* @deprecated as of 5.3, in favor of {@link #hasLength(String)} and
* {@link #hasText(String)} (or {@link ObjectUtils#isEmpty(Object)})
*/
@Deprecated
public static boolean isEmpty(@Nullable Object str) {
return (str == null || "".equals(str));
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 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.
@@ -89,7 +89,7 @@ class MethodParameterTests {
Method method = getClass().getMethod("method", String.class, Long.TYPE);
MethodParameter methodParameter = new MethodParameter(method, 0);
assertThat(methodParameter.hashCode()).isEqualTo(stringParameter.hashCode());
assertThat(methodParameter.hashCode()).isNotEqualTo((long) longParameter.hashCode());
assertThat(methodParameter.hashCode()).isNotEqualTo(longParameter.hashCode());
}
@Test
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 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.
@@ -86,7 +86,7 @@ class SynthesizingMethodParameterTests {
Method method = getClass().getMethod("method", String.class, Long.TYPE);
SynthesizingMethodParameter methodParameter = new SynthesizingMethodParameter(method, 0);
assertThat(methodParameter.hashCode()).isEqualTo(stringParameter.hashCode());
assertThat(methodParameter.hashCode()).isNotEqualTo((long) longParameter.hashCode());
assertThat(methodParameter.hashCode()).isNotEqualTo(longParameter.hashCode());
}
@Test
@@ -139,20 +139,28 @@ class StringDecoderTests extends AbstractDecoderTests<StringDecoder> {
@Test
void maxInMemoryLimit() {
Flux<DataBuffer> input = Flux.just(
stringBuffer("abc\n"), stringBuffer("defg\n"), stringBuffer("hijkl\n"));
stringBuffer("abc\n"), stringBuffer("defg\n"),
stringBuffer("hi"), stringBuffer("jkl"), stringBuffer("mnop"));
this.decoder.setMaxInMemorySize(5);
testDecode(input, String.class, step ->
step.expectNext("abc", "defg").verifyError(DataBufferLimitException.class));
}
@Test // gh-24312
void maxInMemoryLimitReleaseUnprocessedLinesFromCurrentBuffer() {
@Test
void maxInMemoryLimitDoesNotApplyToParsedItemsThatDontRequireBuffering() {
Flux<DataBuffer> input = Flux.just(
stringBuffer("TOO MUCH DATA\nanother line\n\nand another\n"));
this.decoder.setMaxInMemorySize(5);
testDecode(input, String.class, step -> step.verifyError(DataBufferLimitException.class));
testDecode(input, String.class, step -> step
.expectNext("TOO MUCH DATA")
.expectNext("another line")
.expectNext("")
.expectNext("and another")
.expectComplete()
.verify());
}
@Test // gh-24339
@@ -886,18 +886,38 @@ class DataBufferUtilsTests extends AbstractDataBufferAllocatingTests {
void matcher2(String displayName, DataBufferFactory bufferFactory) {
super.bufferFactory = bufferFactory;
DataBuffer foo = stringBuffer("fooobar");
DataBuffer foo = stringBuffer("foooobar");
byte[] delims = "oo".getBytes(StandardCharsets.UTF_8);
DataBufferUtils.Matcher matcher = DataBufferUtils.matcher(delims);
int result = matcher.match(foo);
assertThat(result).isEqualTo(2);
foo.readPosition(2);
result = matcher.match(foo);
assertThat(result).isEqualTo(3);
foo.readPosition(3);
result = matcher.match(foo);
assertThat(result).isEqualTo(-1);
int endIndex = matcher.match(foo);
assertThat(endIndex).isEqualTo(2);
foo.readPosition(endIndex + 1);
endIndex = matcher.match(foo);
assertThat(endIndex).isEqualTo(4);
foo.readPosition(endIndex + 1);
endIndex = matcher.match(foo);
assertThat(endIndex).isEqualTo(-1);
release(foo);
}
@ParameterizedDataBufferAllocatingTest
void matcher3(String displayName, DataBufferFactory bufferFactory) {
super.bufferFactory = bufferFactory;
DataBuffer foo = stringBuffer("foooobar");
byte[] delims = "oo".getBytes(StandardCharsets.UTF_8);
DataBufferUtils.Matcher matcher = DataBufferUtils.matcher(delims);
int endIndex = matcher.match(foo);
assertThat(endIndex).isEqualTo(2);
foo.readPosition(endIndex + 1);
endIndex = matcher.match(foo);
assertThat(endIndex).isEqualTo(4);
foo.readPosition(endIndex + 1);
endIndex = matcher.match(foo);
assertThat(endIndex).isEqualTo(-1);
release(foo);
}
@@ -0,0 +1,85 @@
/*
* 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.
* 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.util;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Juergen Hoeller
*/
class ConcurrentLruCacheTests {
private final ConcurrentLruCache<String, String> cache = new ConcurrentLruCache<>(2, key -> key + "value");
@Test
void getAndSize() {
assertThat(this.cache.sizeLimit()).isEqualTo(2);
assertThat(this.cache.size()).isEqualTo(0);
assertThat(this.cache.get("k1")).isEqualTo("k1value");
assertThat(this.cache.size()).isEqualTo(1);
assertThat(this.cache.contains("k1")).isTrue();
assertThat(this.cache.get("k2")).isEqualTo("k2value");
assertThat(this.cache.size()).isEqualTo(2);
assertThat(this.cache.contains("k1")).isTrue();
assertThat(this.cache.contains("k2")).isTrue();
assertThat(this.cache.get("k3")).isEqualTo("k3value");
assertThat(this.cache.size()).isEqualTo(2);
assertThat(this.cache.contains("k1")).isFalse();
assertThat(this.cache.contains("k2")).isTrue();
assertThat(this.cache.contains("k3")).isTrue();
}
@Test
void removeAndSize() {
assertThat(this.cache.get("k1")).isEqualTo("k1value");
assertThat(this.cache.get("k2")).isEqualTo("k2value");
assertThat(this.cache.size()).isEqualTo(2);
assertThat(this.cache.contains("k1")).isTrue();
assertThat(this.cache.contains("k2")).isTrue();
this.cache.remove("k2");
assertThat(this.cache.size()).isEqualTo(1);
assertThat(this.cache.contains("k1")).isTrue();
assertThat(this.cache.contains("k2")).isFalse();
assertThat(this.cache.get("k3")).isEqualTo("k3value");
assertThat(this.cache.size()).isEqualTo(2);
assertThat(this.cache.contains("k1")).isTrue();
assertThat(this.cache.contains("k2")).isFalse();
assertThat(this.cache.contains("k3")).isTrue();
}
@Test
void clearAndSize() {
assertThat(this.cache.get("k1")).isEqualTo("k1value");
assertThat(this.cache.get("k2")).isEqualTo("k2value");
assertThat(this.cache.size()).isEqualTo(2);
assertThat(this.cache.contains("k1")).isTrue();
assertThat(this.cache.contains("k2")).isTrue();
this.cache.clear();
assertThat(this.cache.size()).isEqualTo(0);
assertThat(this.cache.contains("k1")).isFalse();
assertThat(this.cache.contains("k2")).isFalse();
assertThat(this.cache.get("k3")).isEqualTo("k3value");
assertThat(this.cache.size()).isEqualTo(1);
assertThat(this.cache.contains("k1")).isFalse();
assertThat(this.cache.contains("k2")).isFalse();
assertThat(this.cache.contains("k3")).isTrue();
}
}
@@ -24,7 +24,6 @@ import org.springframework.expression.spel.CodeFlow;
import org.springframework.expression.spel.ExpressionState;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Represents the elvis operator ?:. For an expression "a?:b" if a is not null, the value
@@ -52,7 +51,7 @@ public class Elvis extends SpelNodeImpl {
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
TypedValue value = this.children[0].getValueInternal(state);
// If this check is changed, the generateCode method will need changing too
if (!StringUtils.isEmpty(value.getValue())) {
if (value.getValue() != null && !"".equals(value.getValue())) {
return value;
}
else {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 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,34 +63,36 @@ import org.springframework.util.StringUtils;
* <p>Individual expressions can be compiled by calling {@code SpelCompiler.compile(expression)}.
*
* @author Andy Clement
* @author Juergen Hoeller
* @since 4.1
*/
public final class SpelCompiler implements Opcodes {
private static final Log logger = LogFactory.getLog(SpelCompiler.class);
private static final int CLASSES_DEFINED_LIMIT = 100;
private static final Log logger = LogFactory.getLog(SpelCompiler.class);
// A compiler is created for each classloader, it manages a child class loader of that
// classloader and the child is used to load the compiled expressions.
private static final Map<ClassLoader, SpelCompiler> compilers = new ConcurrentReferenceHashMap<>();
// The child ClassLoader used to load the compiled expression classes
private ChildClassLoader ccl;
private volatile ChildClassLoader childClassLoader;
// Counter suffix for generated classes within this SpelCompiler instance
private final AtomicInteger suffixId = new AtomicInteger(1);
private SpelCompiler(@Nullable ClassLoader classloader) {
this.ccl = new ChildClassLoader(classloader);
this.childClassLoader = new ChildClassLoader(classloader);
}
/**
* Attempt compilation of the supplied expression. A check is made to see
* if it is compilable before compilation proceeds. The check involves
* visiting all the nodes in the expression Ast and ensuring enough state
* visiting all the nodes in the expression AST and ensuring enough state
* is known about them that bytecode can be generated for them.
* @param expression the expression to compile
* @return an instance of the class implementing the compiled expression,
@@ -125,7 +127,7 @@ public final class SpelCompiler implements Opcodes {
/**
* Generate the class that encapsulates the compiled expression and define it.
* The generated class will be a subtype of CompiledExpression.
* The generated class will be a subtype of CompiledExpression.
* @param expressionToCompile the expression to be compiled
* @return the expression call, or {@code null} if the decision was to opt out of
* compilation during code generation
@@ -135,7 +137,7 @@ public final class SpelCompiler implements Opcodes {
// Create class outline 'spel/ExNNN extends org.springframework.expression.spel.CompiledExpression'
String className = "spel/Ex" + getNextSuffix();
ClassWriter cw = new ExpressionClassWriter();
cw.visit(V1_5, ACC_PUBLIC, className, null, "org/springframework/expression/spel/CompiledExpression", null);
cw.visit(V1_8, ACC_PUBLIC, className, null, "org/springframework/expression/spel/CompiledExpression", null);
// Create default constructor
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
@@ -150,7 +152,7 @@ public final class SpelCompiler implements Opcodes {
// Create getValue() method
mv = cw.visitMethod(ACC_PUBLIC, "getValue",
"(Ljava/lang/Object;Lorg/springframework/expression/EvaluationContext;)Ljava/lang/Object;", null,
new String[ ]{"org/springframework/expression/EvaluationException"});
new String[] {"org/springframework/expression/EvaluationException"});
mv.visitCode();
CodeFlow cf = new CodeFlow(className, cw);
@@ -187,7 +189,7 @@ public final class SpelCompiler implements Opcodes {
/**
* Load a compiled expression class. Makes sure the classloaders aren't used too much
* because they anchor compiled classes in memory and prevent GC. If you have expressions
* because they anchor compiled classes in memory and prevent GC. If you have expressions
* continually recompiling over time then by replacing the classloader periodically
* at least some of the older variants can be garbage collected.
* @param name the name of the class
@@ -196,12 +198,25 @@ public final class SpelCompiler implements Opcodes {
*/
@SuppressWarnings("unchecked")
private Class<? extends CompiledExpression> loadClass(String name, byte[] bytes) {
if (this.ccl.getClassesDefinedCount() > CLASSES_DEFINED_LIMIT) {
this.ccl = new ChildClassLoader(this.ccl.getParent());
ChildClassLoader ccl = this.childClassLoader;
if (ccl.getClassesDefinedCount() >= CLASSES_DEFINED_LIMIT) {
synchronized (this) {
ChildClassLoader currentCcl = this.childClassLoader;
if (ccl == currentCcl) {
// Still the same ClassLoader that needs to be replaced...
ccl = new ChildClassLoader(ccl.getParent());
this.childClassLoader = ccl;
}
else {
// Already replaced by some other thread, let's pick it up.
ccl = currentCcl;
}
}
}
return (Class<? extends CompiledExpression>) this.ccl.defineClass(name, bytes);
return (Class<? extends CompiledExpression>) ccl.defineClass(name, bytes);
}
/**
* Factory method for compiler instances. The returned SpelCompiler will
* attach a class loader as the child of the given class loader and this
@@ -211,21 +226,28 @@ public final class SpelCompiler implements Opcodes {
*/
public static SpelCompiler getCompiler(@Nullable ClassLoader classLoader) {
ClassLoader clToUse = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
synchronized (compilers) {
SpelCompiler compiler = compilers.get(clToUse);
if (compiler == null) {
compiler = new SpelCompiler(clToUse);
compilers.put(clToUse, compiler);
// Quick check for existing compiler without lock contention
SpelCompiler compiler = compilers.get(clToUse);
if (compiler == null) {
// Full lock now since we're creating a child ClassLoader
synchronized (compilers) {
compiler = compilers.get(clToUse);
if (compiler == null) {
compiler = new SpelCompiler(clToUse);
compilers.put(clToUse, compiler);
}
}
return compiler;
}
return compiler;
}
/**
* Request that an attempt is made to compile the specified expression. It may fail if
* components of the expression are not suitable for compilation or the data types
* involved are not suitable for compilation. Used for testing.
* @return true if the expression was successfully compiled
* Request that an attempt is made to compile the specified expression.
* It may fail if components of the expression are not suitable for compilation
* or the data types involved are not suitable for compilation. Used for testing.
* @param expression the expression to compile
* @return {@code true} if the expression was successfully compiled,
* {@code false} otherwise
*/
public static boolean compile(Expression expression) {
return (expression instanceof SpelExpression && ((SpelExpression) expression).compileExpression());
@@ -250,24 +272,27 @@ public final class SpelCompiler implements Opcodes {
private static final URL[] NO_URLS = new URL[0];
private int classesDefinedCount = 0;
private final AtomicInteger classesDefinedCount = new AtomicInteger(0);
public ChildClassLoader(@Nullable ClassLoader classLoader) {
super(NO_URLS, classLoader);
}
int getClassesDefinedCount() {
return this.classesDefinedCount;
}
public Class<?> defineClass(String name, byte[] bytes) {
Class<?> clazz = super.defineClass(name, bytes, 0, bytes.length);
this.classesDefinedCount++;
this.classesDefinedCount.incrementAndGet();
return clazz;
}
public int getClassesDefinedCount() {
return this.classesDefinedCount.get();
}
}
/**
* An ASM ClassWriter extension bound to the SpelCompiler's ClassLoader.
*/
private class ExpressionClassWriter extends ClassWriter {
public ExpressionClassWriter() {
@@ -276,7 +301,7 @@ public final class SpelCompiler implements Opcodes {
@Override
protected ClassLoader getClassLoader() {
return ccl;
return childClassLoader;
}
}
@@ -395,6 +395,11 @@ public class ReflectivePropertyAccessor implements PropertyAccessor {
if (method == null) {
method = findMethodForProperty(getPropertyMethodSuffixes(propertyName),
"is", clazz, mustBeStatic, 0, BOOLEAN_TYPES);
if (method == null) {
// Record-style plain accessor method, e.g. name()
method = findMethodForProperty(new String[] {propertyName},
"", clazz, mustBeStatic, 0, ANY_TYPES);
}
}
return method;
}
@@ -683,12 +688,11 @@ public class ReflectivePropertyAccessor implements PropertyAccessor {
return true;
}
getterName = "is" + StringUtils.capitalize(name);
return getterName.equals(method.getName());
}
else {
Field field = (Field) this.member;
return field.getName().equals(name);
if (getterName.equals(method.getName())) {
return true;
}
}
return this.member.getName().equals(name);
}
@Override
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 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.
@@ -36,6 +36,7 @@ import org.springframework.expression.spel.support.SimpleEvaluationContext;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.expression.spel.testresources.Inventor;
import org.springframework.expression.spel.testresources.Person;
import org.springframework.expression.spel.testresources.RecordPerson;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -191,6 +192,20 @@ public class PropertyAccessTests extends AbstractExpressionTests {
parser.parseExpression("name='p3'").getValue(context, target));
}
@Test
public void propertyReadOnlyWithRecordStyle() {
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
Expression expr = parser.parseExpression("name");
RecordPerson target1 = new RecordPerson("p1");
assertThat(expr.getValue(context, target1)).isEqualTo("p1");
RecordPerson target2 = new RecordPerson("p2");
assertThat(expr.getValue(context, target2)).isEqualTo("p2");
assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() ->
parser.parseExpression("name='p3'").getValue(context, target2));
}
@Test
public void propertyReadWrite() {
EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
@@ -4180,6 +4180,13 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
assertThat(expression.getValue(tc)).isEqualTo("value4");
assertCanCompile(expression);
assertThat(expression.getValue(tc)).isEqualTo("value4");
// record-style accessor
expression = parser.parseExpression("strawberry");
assertCantCompile(expression);
assertThat(expression.getValue(tc)).isEqualTo("value5");
assertCanCompile(expression);
assertThat(expression.getValue(tc)).isEqualTo("value5");
}
@Test
@@ -4553,23 +4560,9 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
Object v = expression.getValue(ctx,holder);
assertThat(v).isEqualTo("abc");
// // time it interpreted
// long stime = System.currentTimeMillis();
// for (int i = 0; i < 100000; i++) {
// v = expression.getValue(ctx,holder);
// }
// System.out.println((System.currentTimeMillis() - stime));
assertCanCompile(expression);
v = expression.getValue(ctx,holder);
assertThat(v).isEqualTo("abc");
// // time it compiled
// stime = System.currentTimeMillis();
// for (int i = 0; i < 100000; i++) {
// v = expression.getValue(ctx,holder);
// }
// System.out.println((System.currentTimeMillis() - stime));
}
@Test
@@ -4985,13 +4978,12 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
assertThat(fast.compileExpression()).isTrue();
r.setValue2(null);
// try the numbers 0,1,2,null
for (int i=0;i<4;i++) {
r.setValue(i<3?i:null);
for (int i = 0; i < 4; i++) {
r.setValue(i < 3 ? i : null);
boolean slowResult = (Boolean)slow.getValue(ctx);
boolean fastResult = (Boolean)fast.getValue(ctx);
// System.out.println("Trying "+expressionText+" with value="+r.getValue()+" result is "+slowResult);
assertThat(fastResult).as(" Differing results: expression="+expressionText+
" value="+r.getValue()+" slow="+slowResult+" fast="+fastResult).isEqualTo(slowResult);
assertThat(fastResult).as("Differing results: expression=" + expressionText +
" value=" + r.getValue() + " slow=" + slowResult + " fast="+fastResult).isEqualTo(slowResult);
}
}
@@ -5002,13 +4994,12 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
assertThat(fast.compileExpression()).isTrue();
Reg r = (Reg)ctx.getRootObject().getValue();
// try the numbers 0,1,2,null
for (int i=0;i<4;i++) {
r.setValue(i<3?i:null);
for (int i = 0; i < 4; i++) {
r.setValue(i < 3 ? i : null);
boolean slowResult = (Boolean)slow.getValue(ctx);
boolean fastResult = (Boolean)fast.getValue(ctx);
// System.out.println("Trying "+expressionText+" with value="+r.getValue()+" result is "+slowResult);
assertThat(fastResult).as(" Differing results: expression="+expressionText+
" value="+r.getValue()+" slow="+slowResult+" fast="+fastResult).isEqualTo(slowResult);
assertThat(fastResult).as("Differing results: expression=" + expressionText +
" value=" + r.getValue() + " slow=" + slowResult + " fast="+fastResult).isEqualTo(slowResult);
}
}
@@ -5839,7 +5830,6 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
public String orange = "value1";
public static String apple = "value2";
public long peach = 34L;
public String getBanana() {
@@ -5849,6 +5839,10 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
public static String getPlum() {
return "value4";
}
public String strawberry() {
return "value5";
}
}
@@ -76,7 +76,6 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
*/
public class SpelReproTests extends AbstractExpressionTests {
@Test
public void NPE_SPR5661() {
evaluate("joinThreeStrings('a',null,'c')", "anullc", String.class);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 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.
@@ -89,8 +89,8 @@ public class ReflectionHelperTests extends AbstractExpressionTests {
assertThat(tv1).isNotEqualTo(tv3);
assertThat(tv2).isNotEqualTo(tv3);
assertThat(tv2.hashCode()).isEqualTo(tv1.hashCode());
assertThat(tv3.hashCode()).isNotEqualTo((long) tv1.hashCode());
assertThat(tv3.hashCode()).isNotEqualTo((long) tv2.hashCode());
assertThat(tv3.hashCode()).isNotEqualTo(tv1.hashCode());
assertThat(tv3.hashCode()).isNotEqualTo(tv2.hashCode());
}
@Test
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 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.
@@ -16,9 +16,10 @@
package org.springframework.expression.spel.testresources;
///CLOVER:OFF
public class Person {
private String privateName;
Company company;
public Person(String name) {
@@ -41,4 +42,5 @@ public class Person {
public Company getCompany() {
return company;
}
}
@@ -0,0 +1,42 @@
/*
* 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.
* 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.expression.spel.testresources;
public class RecordPerson {
private String name;
private Company company;
public RecordPerson(String name) {
this.name = name;
}
public RecordPerson(String name, Company company) {
this.name = name;
this.company = company;
}
public String name() {
return name;
}
public Company company() {
return company;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 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.
@@ -19,19 +19,25 @@ package org.springframework.expression.spel.testresources;
import java.util.List;
public class TestAddress{
private String street;
private List<String> crossStreets;
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public List<String> getCrossStreets() {
return crossStreets;
}
public void setCrossStreets(List<String> crossStreets) {
this.crossStreets = crossStreets;
}
private String street;
private List<String> crossStreets;
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public List<String> getCrossStreets() {
return crossStreets;
}
public void setCrossStreets(List<String> crossStreets) {
this.crossStreets = crossStreets;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 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.
@@ -17,19 +17,25 @@
package org.springframework.expression.spel.testresources;
public class TestPerson {
private String name;
private TestAddress address;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public TestAddress getAddress() {
return address;
}
public void setAddress(TestAddress address) {
this.address = address;
}
private String name;
private TestAddress address;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public TestAddress getAddress() {
return address;
}
public void setAddress(TestAddress address) {
this.address = address;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 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.
@@ -36,7 +36,7 @@ public interface SqlProvider {
/**
* Return the SQL string for this object, i.e.
* typically the SQL used for creating statements.
* @return the SQL string, or {@code null}
* @return the SQL string, or {@code null} if not available
*/
@Nullable
String getSql();
@@ -637,21 +637,23 @@ public class CallMetaDataContext {
schemaNameToUse = this.metaDataProvider.schemaNameToUse(getSchemaName());
}
String procedureNameToUse = this.metaDataProvider.procedureNameToUse(getProcedureName());
if (isFunction() || isReturnValueRequired()) {
callString = new StringBuilder().append("{? = call ").
append(StringUtils.hasLength(catalogNameToUse) ? catalogNameToUse + "." : "").
append(StringUtils.hasLength(schemaNameToUse) ? schemaNameToUse + "." : "").
append(procedureNameToUse).append("(");
callString = new StringBuilder("{? = call ");
parameterCount = -1;
}
else {
callString = new StringBuilder().append("{call ").
append(StringUtils.hasLength(catalogNameToUse) ? catalogNameToUse + "." : "").
append(StringUtils.hasLength(schemaNameToUse) ? schemaNameToUse + "." : "").
append(procedureNameToUse).append("(");
callString = new StringBuilder("{call ");
}
if (StringUtils.hasLength(catalogNameToUse)) {
callString.append(catalogNameToUse).append(".");
}
if (StringUtils.hasLength(schemaNameToUse)) {
callString.append(schemaNameToUse).append(".");
}
callString.append(this.metaDataProvider.procedureNameToUse(getProcedureName()));
callString.append("(");
for (SqlParameter parameter : this.callParameters) {
if (!parameter.isResultsParameter()) {
if (parameterCount > 0) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 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.
@@ -83,7 +83,7 @@ public abstract class NamedParameterUtils {
Assert.notNull(sql, "SQL must not be null");
Set<String> namedParameters = new HashSet<>();
String sqlToUse = sql;
StringBuilder sqlToUse = new StringBuilder(sql);
List<ParameterHolder> parameterList = new ArrayList<>();
char[] statement = sql.toCharArray();
@@ -155,7 +155,7 @@ public abstract class NamedParameterUtils {
int j = i + 1;
if (j < statement.length && statement[j] == ':') {
// escaped ":" should be skipped
sqlToUse = sqlToUse.substring(0, i - escapes) + sqlToUse.substring(i - escapes + 1);
sqlToUse.deleteCharAt(i - escapes);
escapes++;
i = i + 2;
continue;
@@ -174,7 +174,7 @@ public abstract class NamedParameterUtils {
}
i++;
}
ParsedSql parsedSql = new ParsedSql(sqlToUse);
ParsedSql parsedSql = new ParsedSql(sqlToUse.toString());
for (ParameterHolder ph : parameterList) {
parsedSql.addNamedParameter(ph.getParameterName(), ph.getStartIndex(), ph.getEndIndex());
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 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.
@@ -28,11 +28,11 @@ import java.util.List;
*/
public class ParsedSql {
private String originalSql;
private final String originalSql;
private List<String> parameterNames = new ArrayList<>();
private final List<String> parameterNames = new ArrayList<>();
private List<int[]> parameterIndexes = new ArrayList<>();
private final List<int[]> parameterIndexes = new ArrayList<>();
private int namedParameterCount;
+2
View File
@@ -1,6 +1,7 @@
description = "Spring Messaging"
apply plugin: "kotlin"
apply plugin: "kotlinx-serialization"
dependencies {
compile(project(":spring-beans"))
@@ -17,6 +18,7 @@ dependencies {
optional("javax.xml.bind:jaxb-api")
optional("com.google.protobuf:protobuf-java-util")
optional("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
optional("org.jetbrains.kotlinx:kotlinx-serialization-json")
testCompile(project(":kotlin-coroutines"))
testCompile(testFixtures(project(":spring-core")))
testCompile("javax.inject:javax.inject-tck")
@@ -0,0 +1,109 @@
/*
* 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.
* 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.messaging.converter;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.Type;
import java.util.Map;
import kotlinx.serialization.KSerializer;
import kotlinx.serialization.SerializersKt;
import kotlinx.serialization.json.Json;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.FileCopyUtils;
/**
* Implementation of {@link MessageConverter} that can read and write JSON
* using <a href="https://github.com/Kotlin/kotlinx.serialization">kotlinx.serialization</a>.
*
* <p>This converter can be used to bind {@code @Serializable} Kotlin classes.
*
* @author Sebastien Deleuze
* @since 5.3
*/
public class KotlinSerializationJsonMessageConverter extends AbstractJsonMessageConverter {
private static final Map<Type, KSerializer<Object>> serializerCache = new ConcurrentReferenceHashMap<>();
private final Json json;
/**
* Construct a new {@code KotlinSerializationJsonMessageConverter} with default configuration.
*/
public KotlinSerializationJsonMessageConverter() {
this(Json.Default);
}
/**
* Construct a new {@code KotlinSerializationJsonMessageConverter} with the given delegate.
* @param json the Json instance to use
*/
public KotlinSerializationJsonMessageConverter(Json json) {
this.json = json;
}
@Override
protected Object fromJson(Reader reader, Type resolvedType) {
try {
return fromJson(FileCopyUtils.copyToString(reader), resolvedType);
}
catch (IOException ex) {
throw new MessageConversionException("Could not read JSON: " + ex.getMessage(), ex);
}
}
@Override
protected Object fromJson(String payload, Type resolvedType) {
return this.json.decodeFromString(serializer(resolvedType), payload);
}
@Override
protected void toJson(Object payload, Type resolvedType, Writer writer) {
try {
writer.write(toJson(payload, resolvedType).toCharArray());
}
catch (IOException ex) {
throw new MessageConversionException("Could not write JSON: " + ex.getMessage(), ex);
}
}
@Override
protected String toJson(Object payload, Type resolvedType) {
return this.json.encodeToString(serializer(resolvedType), payload);
}
/**
* Tries to find a serializer that can marshall or unmarshall instances of the given type
* using kotlinx.serialization. If no serializer can be found, an exception is thrown.
* <p>Resolved serializers are cached and cached results are returned on successive calls.
* @param type the type to find a serializer for
* @return a resolved serializer for the given type
* @throws RuntimeException if no serializer supporting the given type can be found
*/
private KSerializer<Object> serializer(Type type) {
KSerializer<Object> serializer = serializerCache.get(type);
if (serializer == null) {
serializer = SerializersKt.serializer(type);
serializerCache.put(type, serializer);
}
return serializer;
}
}
@@ -247,14 +247,15 @@ public class MappingJackson2MessageConverter extends AbstractMessageConverter {
if (byte[].class == getSerializedPayloadClass()) {
ByteArrayOutputStream out = new ByteArrayOutputStream(1024);
JsonEncoding encoding = getJsonEncoding(getMimeType(headers));
JsonGenerator generator = this.objectMapper.getFactory().createGenerator(out, encoding);
if (view != null) {
this.objectMapper.writerWithView(view).writeValue(generator, payload);
try (JsonGenerator generator = this.objectMapper.getFactory().createGenerator(out, encoding)) {
if (view != null) {
this.objectMapper.writerWithView(view).writeValue(generator, payload);
}
else {
this.objectMapper.writeValue(generator, payload);
}
payload = out.toByteArray();
}
else {
this.objectMapper.writeValue(generator, payload);
}
payload = out.toByteArray();
}
else {
// Assuming a text-based target payload
@@ -21,8 +21,6 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import kotlin.reflect.KFunction;
import kotlin.reflect.jvm.ReflectJvmMapping;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Publisher;
@@ -123,7 +121,6 @@ public abstract class AbstractEncoderMethodReturnValueHandler implements Handler
handleEncodedContent(Flux.from(publisher), returnType, message));
}
@SuppressWarnings("unchecked")
private Flux<DataBuffer> encodeContent(
@Nullable Object content, MethodParameter returnType, DataBufferFactory bufferFactory,
@Nullable MimeType mimeType, Map<String, Object> hints) {
@@ -135,11 +132,10 @@ public abstract class AbstractEncoderMethodReturnValueHandler implements Handler
ResolvableType elementType;
if (adapter != null) {
publisher = adapter.toPublisher(content);
boolean isUnwrapped = KotlinDetector.isKotlinReflectPresent() &&
KotlinDetector.isKotlinType(returnType.getContainingClass()) &&
KotlinDelegate.isSuspend(returnType.getMethod()) &&
!COROUTINES_FLOW_CLASS_NAME.equals(returnValueType.toClass().getName());
ResolvableType genericType = isUnwrapped ? returnValueType : returnValueType.getGeneric();
Method method = returnType.getMethod();
boolean isUnwrapped = (method != null && KotlinDetector.isSuspendingFunction(method) &&
!COROUTINES_FLOW_CLASS_NAME.equals(returnValueType.toClass().getName()));
ResolvableType genericType = (isUnwrapped ? returnValueType : returnValueType.getGeneric());
elementType = getElementType(adapter, genericType);
}
else {
@@ -218,19 +214,4 @@ public abstract class AbstractEncoderMethodReturnValueHandler implements Handler
*/
protected abstract Mono<Void> handleNoContent(MethodParameter returnType, Message<?> message);
/**
* Inner class to avoid a hard dependency on Kotlin at runtime.
*/
private static class KotlinDelegate {
private static boolean isSuspend(@Nullable Method method) {
if (method == null) {
return false;
}
KFunction<?> function = ReflectJvmMapping.getKotlinFunction(method);
return (function != null && function.isSuspend());
}
}
}
@@ -177,8 +177,7 @@ class MessagingRSocket implements RSocket {
}
})
.thenMany(Flux.defer(() -> responseRef.get() != null ?
responseRef.get() :
Mono.error(new IllegalStateException("Something went wrong: reply Mono not set"))));
responseRef.get() : Mono.error(new IllegalStateException("Expected response"))));
}
private DataBuffer retainDataAndReleasePayload(Payload payload) {
@@ -125,6 +125,12 @@ public abstract class AbstractBrokerMessageHandler
return this.brokerChannel;
}
/**
* Return destination prefixes prefixes to use to filter messages to forward
* to the broker. Messages that have a destination and where the destination
* doesn't match are ignored.
* <p>By default this is not set.
*/
public Collection<String> getDestinationPrefixes() {
return this.destinationPrefixes;
}
@@ -36,6 +36,7 @@ import org.springframework.messaging.converter.CompositeMessageConverter;
import org.springframework.messaging.converter.DefaultContentTypeResolver;
import org.springframework.messaging.converter.GsonMessageConverter;
import org.springframework.messaging.converter.JsonbMessageConverter;
import org.springframework.messaging.converter.KotlinSerializationJsonMessageConverter;
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.converter.StringMessageConverter;
@@ -101,6 +102,8 @@ public abstract class AbstractMessageBrokerConfiguration implements ApplicationC
private static final boolean jsonbPresent;
private static final boolean kotlinSerializationJsonPresent;
static {
ClassLoader classLoader = AbstractMessageBrokerConfiguration.class.getClassLoader();
@@ -108,6 +111,7 @@ public abstract class AbstractMessageBrokerConfiguration implements ApplicationC
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
jsonbPresent = ClassUtils.isPresent("javax.json.bind.Jsonb", classLoader);
kotlinSerializationJsonPresent = ClassUtils.isPresent("kotlinx.serialization.json.Json", classLoader);
}
@@ -411,6 +415,9 @@ public abstract class AbstractMessageBrokerConfiguration implements ApplicationC
else if (jsonbPresent) {
converters.add(new JsonbMessageConverter());
}
else if (kotlinSerializationJsonPresent) {
converters.add(new KotlinSerializationJsonMessageConverter());
}
}
return new CompositeMessageConverter(converters);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 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.
@@ -21,6 +21,7 @@ import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler;
import org.springframework.messaging.tcp.TcpOperations;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.util.Assert;
/**
@@ -55,6 +56,9 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration {
@Nullable
private TcpOperations<byte[]> tcpClient;
@Nullable
private TaskScheduler taskScheduler;
private boolean autoStartup = true;
@Nullable
@@ -178,8 +182,27 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration {
* specified are effectively ignored.
* @since 4.3.15
*/
public void setTcpClient(TcpOperations<byte[]> tcpClient) {
public StompBrokerRelayRegistration setTcpClient(TcpOperations<byte[]> tcpClient) {
this.tcpClient = tcpClient;
return this;
}
/**
* Some STOMP clients (e.g. stomp-js) always send heartbeats at a fixed rate
* but others (Spring STOMP client) do so only when no other messages are
* sent. However messages with a non-broker {@link #getDestinationPrefixes()
* destination prefix} aren't forwarded and as a result the broker may deem
* the connection inactive.
* <p>When this {@link TaskScheduler} is set, it is used to reset a count of
* the number of messages sent from client to broker since the beginning of
* the current heartbeat period. This is then used to decide whether to send
* a heartbeat to the broker when ignoring a message with a non-broker
* destination prefix.
* @since 5.3
*/
public StompBrokerRelayRegistration setTaskScheduler(@Nullable TaskScheduler taskScheduler) {
this.taskScheduler = taskScheduler;
return this;
}
/**
@@ -233,7 +256,6 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration {
@Override
protected StompBrokerRelayMessageHandler getMessageHandler(SubscribableChannel brokerChannel) {
StompBrokerRelayMessageHandler handler = new StompBrokerRelayMessageHandler(
getClientInboundChannel(), getClientOutboundChannel(),
brokerChannel, getDestinationPrefixes());
@@ -259,6 +281,9 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration {
if (this.tcpClient != null) {
handler.setTcpClient(this.tcpClient);
}
if (this.taskScheduler != null) {
handler.setTaskScheduler(this.taskScheduler);
}
handler.setAutoStartup(this.autoStartup);
@@ -16,7 +16,6 @@
package org.springframework.messaging.simp.stomp;
import org.springframework.messaging.tcp.TcpConnectionHandler;
import org.springframework.util.concurrent.ListenableFuture;
/**
@@ -30,7 +29,7 @@ import org.springframework.util.concurrent.ListenableFuture;
* @author Rossen Stoyanchev
* @since 4.2
*/
public interface ConnectionHandlingStompSession extends StompSession, TcpConnectionHandler<byte[]> {
public interface ConnectionHandlingStompSession extends StompSession, StompTcpConnectionHandler<byte[]> {
/**
* Return a future that will complete when the session is ready for use.
@@ -45,6 +45,7 @@ import org.springframework.scheduling.TaskScheduler;
import org.springframework.util.AlternativeJdkIdGenerator;
import org.springframework.util.Assert;
import org.springframework.util.IdGenerator;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureCallback;
@@ -133,6 +134,13 @@ public class DefaultStompSession implements ConnectionHandlingStompSession {
return this.sessionId;
}
@Override
public StompHeaderAccessor getConnectHeaders() {
StompHeaderAccessor accessor = createHeaderAccessor(StompCommand.CONNECT);
accessor.addNativeHeaders(this.connectHeaders);
return accessor;
}
/**
* Return the configured session handler.
*/
@@ -256,7 +264,7 @@ public class DefaultStompSession implements ConnectionHandlingStompSession {
private Message<byte[]> createMessage(StompHeaderAccessor accessor, @Nullable Object payload) {
accessor.updateSimpMessageHeadersFromStompHeaders();
Message<byte[]> message;
if (StringUtils.isEmpty(payload) || (payload instanceof byte[] && ((byte[]) payload).length == 0)) {
if (ObjectUtils.isEmpty(payload)) {
message = MessageBuilder.createMessage(EMPTY_PAYLOAD, accessor.getMessageHeaders());
}
else {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 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.
@@ -41,10 +41,10 @@ import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.messaging.support.MessageHeaderInitializer;
import org.springframework.messaging.tcp.FixedIntervalReconnectStrategy;
import org.springframework.messaging.tcp.TcpConnection;
import org.springframework.messaging.tcp.TcpConnectionHandler;
import org.springframework.messaging.tcp.TcpOperations;
import org.springframework.messaging.tcp.reactor.ReactorNettyCodec;
import org.springframework.messaging.tcp.reactor.ReactorNettyTcpClient;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.util.Assert;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureCallback;
@@ -99,13 +99,15 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler
private static final ListenableFutureTask<Void> EMPTY_TASK = new ListenableFutureTask<>(new VoidCallable());
private static final Message<byte[]> HEARTBEAT_MESSAGE;
private static final StompHeaderAccessor HEART_BEAT_ACCESSOR;
private static final Message<byte[]> HEARTBEAT_MESSAGE;
static {
EMPTY_TASK.run();
StompHeaderAccessor accessor = StompHeaderAccessor.createForHeartbeat();
HEARTBEAT_MESSAGE = MessageBuilder.createMessage(StompDecoder.HEARTBEAT_PAYLOAD, accessor.getMessageHeaders());
HEART_BEAT_ACCESSOR = StompHeaderAccessor.createForHeartbeat();
HEARTBEAT_MESSAGE = MessageBuilder.createMessage(
StompDecoder.HEARTBEAT_PAYLOAD, HEART_BEAT_ACCESSOR.getMessageHeaders());
}
@@ -138,7 +140,10 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler
private final DefaultStats stats = new DefaultStats();
private final Map<String, StompConnectionHandler> connectionHandlers = new ConcurrentHashMap<>();
private final Map<String, RelayConnectionHandler> connectionHandlers = new ConcurrentHashMap<>();
@Nullable
private TaskScheduler taskScheduler;
/**
@@ -404,6 +409,22 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler
return this.connectionHandlers.size();
}
/**
* Configure the {@link TaskScheduler} to use to reset client-to-broker
* message count in the current heartbeat period. For more details, see
* {@link org.springframework.messaging.simp.config.StompBrokerRelayRegistration#setTaskScheduler(TaskScheduler)}.
* @param taskScheduler the scheduler to use
* @since 5.3
*/
public void setTaskScheduler(@Nullable TaskScheduler taskScheduler) {
this.taskScheduler = taskScheduler;
}
@Nullable
public TaskScheduler getTaskScheduler() {
return this.taskScheduler;
}
@Override
protected void startInternal() {
@@ -429,11 +450,15 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler
logger.debug("Forwarding " + accessor.getShortLogMessage(EMPTY_PAYLOAD));
}
SystemStompConnectionHandler handler = new SystemStompConnectionHandler(accessor);
SystemSessionConnectionHandler handler = new SystemSessionConnectionHandler(accessor);
this.connectionHandlers.put(handler.getSessionId(), handler);
this.stats.incrementConnectCount();
this.tcpClient.connect(handler, new FixedIntervalReconnectStrategy(5000));
if (this.taskScheduler != null) {
this.taskScheduler.scheduleWithFixedDelay(new ClientSendMessageCountTask(), 5000);
}
}
private ReactorNettyTcpClient<byte[]> initTcpClient() {
@@ -469,7 +494,7 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler
throw new MessageDeliveryException("Message broker not active. Consider subscribing to " +
"receive BrokerAvailabilityEvent's from an ApplicationListener Spring bean.");
}
StompConnectionHandler handler = this.connectionHandlers.get(sessionId);
RelayConnectionHandler handler = this.connectionHandlers.get(sessionId);
if (handler != null) {
handler.sendStompErrorFrameToClient("Broker not available.");
handler.clearConnection();
@@ -526,11 +551,6 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler
stompAccessor.setSessionId(sessionId);
}
String destination = stompAccessor.getDestination();
if (command != null && command.requiresDestination() && !checkDestinationPrefix(destination)) {
return;
}
if (StompCommand.CONNECT.equals(command) || StompCommand.STOMP.equals(command)) {
if (logger.isDebugEnabled()) {
logger.debug(stompAccessor.getShortLogMessage(EMPTY_PAYLOAD));
@@ -541,14 +561,14 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler
if (getVirtualHost() != null) {
stompAccessor.setHost(getVirtualHost());
}
StompConnectionHandler handler = new StompConnectionHandler(sessionId, stompAccessor);
RelayConnectionHandler handler = new RelayConnectionHandler(sessionId, stompAccessor);
this.connectionHandlers.put(sessionId, handler);
this.stats.incrementConnectCount();
Assert.state(this.tcpClient != null, "No TCP client available");
this.tcpClient.connect(handler);
}
else if (StompCommand.DISCONNECT.equals(command)) {
StompConnectionHandler handler = this.connectionHandlers.get(sessionId);
RelayConnectionHandler handler = this.connectionHandlers.get(sessionId);
if (handler == null) {
if (logger.isDebugEnabled()) {
logger.debug("Ignoring DISCONNECT in session " + sessionId + ". Connection already cleaned up.");
@@ -559,13 +579,23 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler
handler.forward(message, stompAccessor);
}
else {
StompConnectionHandler handler = this.connectionHandlers.get(sessionId);
RelayConnectionHandler handler = this.connectionHandlers.get(sessionId);
if (handler == null) {
if (logger.isDebugEnabled()) {
logger.debug("No TCP connection for session " + sessionId + " in " + message);
}
return;
}
String destination = stompAccessor.getDestination();
if (command != null && command.requiresDestination() && !checkDestinationPrefix(destination)) {
// Not a broker destination but send a heartbeat to keep the connection
if (handler.shouldSendHeartbeatForIgnoredMessage()) {
handler.forward(HEARTBEAT_MESSAGE, HEART_BEAT_ACCESSOR);
}
return;
}
handler.forward(message, stompAccessor);
}
}
@@ -580,14 +610,14 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler
}
private class StompConnectionHandler implements TcpConnectionHandler<byte[]> {
private class RelayConnectionHandler implements StompTcpConnectionHandler<byte[]> {
private final String sessionId;
private final boolean isRemoteClientSession;
private final StompHeaderAccessor connectHeaders;
private final boolean isRemoteClientSession;
private final MessageChannel outboundChannel;
@Nullable
@@ -595,24 +625,47 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler
private volatile boolean isStompConnected;
private long clientSendInterval;
protected StompConnectionHandler(String sessionId, StompHeaderAccessor connectHeaders) {
@Nullable
private final AtomicInteger clientSendMessageCount;
private long clientSendMessageTimestamp;
protected RelayConnectionHandler(String sessionId, StompHeaderAccessor connectHeaders) {
this(sessionId, connectHeaders, true);
}
private StompConnectionHandler(String sessionId, StompHeaderAccessor connectHeaders, boolean isClientSession) {
private RelayConnectionHandler(String sessionId, StompHeaderAccessor connectHeaders, boolean isClientSession) {
Assert.notNull(sessionId, "'sessionId' must not be null");
Assert.notNull(connectHeaders, "'connectHeaders' must not be null");
this.sessionId = sessionId;
this.connectHeaders = connectHeaders;
this.isRemoteClientSession = isClientSession;
this.outboundChannel = getClientOutboundChannelForSession(sessionId);
if (isClientSession && taskScheduler != null) {
this.clientSendInterval = connectHeaders.getHeartbeat()[0];
}
if (this.clientSendInterval > 0) {
this.clientSendMessageCount = new AtomicInteger();
this.clientSendMessageTimestamp = System.currentTimeMillis();
}
else {
this.clientSendMessageCount = null;
}
}
public String getSessionId() {
return this.sessionId;
}
@Override
public StompHeaderAccessor getConnectHeaders() {
return this.connectHeaders;
}
@Nullable
protected TcpConnection<byte[]> getTcpConnection() {
return this.tcpConnection;
@@ -722,31 +775,31 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler
initHeartbeats(connectedHeaders);
}
private void initHeartbeats(StompHeaderAccessor connectedHeaders) {
if (this.isRemoteClientSession) {
return;
protected void initHeartbeats(StompHeaderAccessor connectedHeaders) {
if (taskScheduler != null) {
long interval = connectedHeaders.getHeartbeat()[1];
this.clientSendInterval = Math.max(interval, this.clientSendInterval);
}
}
TcpConnection<byte[]> con = this.tcpConnection;
Assert.state(con != null, "No TcpConnection available");
/**
* Whether to forward a heartbeat message in lieu of a message with a non-broker
* destination. This is done if client-side heartbeats are expected and if there
* haven't been any other messages in the current heartbeat period.
* @since 5.3
*/
protected boolean shouldSendHeartbeatForIgnoredMessage() {
return (this.clientSendMessageCount != null && this.clientSendMessageCount.get() == 0);
}
long clientSendInterval = this.connectHeaders.getHeartbeat()[0];
long clientReceiveInterval = this.connectHeaders.getHeartbeat()[1];
long serverSendInterval = connectedHeaders.getHeartbeat()[0];
long serverReceiveInterval = connectedHeaders.getHeartbeat()[1];
if (clientSendInterval > 0 && serverReceiveInterval > 0) {
long interval = Math.max(clientSendInterval, serverReceiveInterval);
con.onWriteInactivity(() ->
con.send(HEARTBEAT_MESSAGE).addCallback(
result -> {},
ex -> handleTcpConnectionFailure(
"Failed to forward heartbeat: " + ex.getMessage(), ex)), interval);
}
if (clientReceiveInterval > 0 && serverSendInterval > 0) {
final long interval = Math.max(clientReceiveInterval, serverSendInterval) * HEARTBEAT_MULTIPLIER;
con.onReadInactivity(
() -> handleTcpConnectionFailure("No messages received in " + interval + " ms.", null), interval);
/**
* Reset the clientSendMessageCount if the current heartbeat period has expired.
* @since 5.3
*/
void updateClientSendMessageCount(long now) {
if (this.clientSendMessageCount != null && this.clientSendInterval > (now - clientSendMessageTimestamp)) {
this.clientSendMessageCount.set(0);
this.clientSendMessageTimestamp = now;
}
}
@@ -824,6 +877,10 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler
}
}
if (this.clientSendMessageCount != null) {
this.clientSendMessageCount.incrementAndGet();
}
final Message<?> messageToSend = (accessor.isMutable() && accessor.isModified()) ?
MessageBuilder.createMessage(message.getPayload(), accessor.getMessageHeaders()) : message;
@@ -911,9 +968,9 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler
}
private class SystemStompConnectionHandler extends StompConnectionHandler {
private class SystemSessionConnectionHandler extends RelayConnectionHandler {
public SystemStompConnectionHandler(StompHeaderAccessor connectHeaders) {
public SystemSessionConnectionHandler(StompHeaderAccessor connectHeaders) {
super(SYSTEM_SESSION_ID, connectHeaders, false);
}
@@ -927,6 +984,30 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler
sendSystemSubscriptions();
}
protected void initHeartbeats(StompHeaderAccessor connectedHeaders) {
TcpConnection<byte[]> con = getTcpConnection();
Assert.state(con != null, "No TcpConnection available");
long clientSendInterval = getConnectHeaders().getHeartbeat()[0];
long clientReceiveInterval = getConnectHeaders().getHeartbeat()[1];
long serverSendInterval = connectedHeaders.getHeartbeat()[0];
long serverReceiveInterval = connectedHeaders.getHeartbeat()[1];
if (clientSendInterval > 0 && serverReceiveInterval > 0) {
long interval = Math.max(clientSendInterval, serverReceiveInterval);
con.onWriteInactivity(() ->
con.send(HEARTBEAT_MESSAGE).addCallback(
result -> {},
ex -> handleTcpConnectionFailure(
"Failed to forward heartbeat: " + ex.getMessage(), ex)), interval);
}
if (clientReceiveInterval > 0 && serverSendInterval > 0) {
final long interval = Math.max(clientReceiveInterval, serverSendInterval) * HEARTBEAT_MULTIPLIER;
con.onReadInactivity(
() -> handleTcpConnectionFailure("No messages received in " + interval + " ms.", null), interval);
}
}
private void sendSystemSubscriptions() {
int i = 0;
for (String destination : getSystemSubscriptions().keySet()) {
@@ -1005,9 +1086,27 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler
throw new MessageDeliveryException(message, ex);
}
}
@Override
protected boolean shouldSendHeartbeatForIgnoredMessage() {
return false;
}
}
private class ClientSendMessageCountTask implements Runnable {
@Override
public void run() {
long now = System.currentTimeMillis();
for (RelayConnectionHandler handler : connectionHandlers.values()) {
handler.updateClientSendMessageCount(now);
}
}
}
private static class VoidCallable implements Callable<Void> {
@Override
@@ -98,6 +98,14 @@ public abstract class StompClientSupport {
* <p>By default this is set to "10000,10000" but subclasses may override
* that default and for example set it to "0,0" if they require a
* TaskScheduler to be configured first.
* <p><strong>Note:</strong> that a heartbeat is sent only in case of
* inactivity, i.e. when no other messages are sent. This can present a
* challenge when using an external broker since messages with a non-broker
* destination represent activity but aren't actually forwarded to the broker.
* In that case you can configure a `TaskScheduler` through the
* {@link org.springframework.messaging.simp.config.StompBrokerRelayRegistration}
* which ensures a heartbeat is forwarded to the broker also when only
* messages with a non-broker destination are sent.
* @param heartbeat the value for the CONNECT "heart-beat" header
* @see <a href="https://stomp.github.io/stomp-specification-1.2.html#Heart-beating">
* https://stomp.github.io/stomp-specification-1.2.html#Heart-beating</a>
@@ -0,0 +1,42 @@
/*
* 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.
* 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.messaging.simp.stomp;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.tcp.TcpConnectionHandler;
/**
* A {@link TcpConnectionHandler} for use with STOMP connections, exposing
* further information about the connection.
*
* @author Rossen Stoyanchev
* @since 5.3
* @param <P> the type of payload for inbound and outbound messages
*/
public interface StompTcpConnectionHandler<P> extends TcpConnectionHandler<P> {
/**
* Return the {@link SimpMessageHeaderAccessor#getSessionId() sessionId}
* associated with the STOMP connection.
*/
String getSessionId();
/**
* Return the headers that will be sent in the STOMP CONNECT frame.
*/
StompHeaderAccessor getConnectHeaders();
}
@@ -18,6 +18,7 @@ package org.springframework.messaging.simp.user;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.Principal;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -310,6 +311,12 @@ public class MultiServerUserRegistry implements SimpUserRegistry, SmartApplicati
return this.name;
}
@Nullable
@Override
public Principal getPrincipal() {
return null;
}
@Override
public boolean hasSessions() {
if (this.sessionLookup != null) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 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.
@@ -16,6 +16,7 @@
package org.springframework.messaging.simp.user;
import java.security.Principal;
import java.util.Set;
import org.springframework.lang.Nullable;
@@ -33,6 +34,15 @@ public interface SimpUser {
*/
String getName();
/**
* Return the user associated with the session, if available. Typically, the
* user information is available unless the user is connected to a different
* server in a multi-server user registry scenario.
* @since 5.3
*/
@Nullable
Principal getPrincipal();
/**
* Whether the user has any sessions.
*/
@@ -186,7 +186,7 @@ public class ReactorNettyTcpClient<P> implements TcpOperations<P> {
return handleShuttingDownConnectFailure(handler);
}
Mono<Void> connectMono = this.tcpClient
Mono<Void> connectMono = extendTcpClient(this.tcpClient, handler)
.handle(new ReactorNettyHandler(handler))
.connect()
.doOnError(handler::afterConnectFailure)
@@ -195,6 +195,19 @@ public class ReactorNettyTcpClient<P> implements TcpOperations<P> {
return new MonoToListenableFutureAdapter<>(connectMono);
}
/**
* Provides an opportunity to initialize the {@link TcpClient} for the given
* {@link TcpConnectionHandler} which may implement sub-interfaces such as
* {@link org.springframework.messaging.simp.stomp.StompTcpConnectionHandler}
* that expose further information.
* @param tcpClient the candidate TcpClient
* @param handler the handler for the TCP connection
* @return the same handler or an updated instance
*/
protected TcpClient extendTcpClient(TcpClient tcpClient, TcpConnectionHandler<P> handler) {
return tcpClient;
}
@Override
public ListenableFuture<Void> connect(TcpConnectionHandler<P> handler, ReconnectStrategy strategy) {
Assert.notNull(handler, "TcpConnectionHandler is required");
@@ -207,7 +220,7 @@ public class ReactorNettyTcpClient<P> implements TcpOperations<P> {
// Report first connect to the ListenableFuture
CompletableFuture<Void> connectFuture = new CompletableFuture<>();
this.tcpClient
extendTcpClient(this.tcpClient, handler)
.handle(new ReactorNettyHandler(handler))
.connect()
.doOnNext(conn -> connectFuture.complete(null))
@@ -274,8 +274,7 @@ public class StompBrokerRelayMessageHandlerIntegrationTests {
}
public void expectMessages(MessageExchange... messageExchanges) throws InterruptedException {
List<MessageExchange> expectedMessages =
new ArrayList<>(Arrays.<MessageExchange>asList(messageExchanges));
List<MessageExchange> expectedMessages = new ArrayList<>(Arrays.asList(messageExchanges));
while (expectedMessages.size() > 0) {
Message<?> message = this.queue.poll(10000, TimeUnit.MILLISECONDS);
assertThat(message).as("Timed out waiting for messages, expected [" + expectedMessages + "]").isNotNull();
@@ -451,7 +450,7 @@ public class StompBrokerRelayMessageHandlerIntegrationTests {
@Override
public final boolean match(Message<?> message) {
StompHeaderAccessor headers = StompHeaderAccessor.wrap(message);
if (!this.command.equals(headers.getCommand()) || (this.sessionId != headers.getSessionId())) {
if (!this.command.equals(headers.getCommand()) || !this.sessionId.equals(headers.getSessionId())) {
return false;
}
return matchInternal(headers, message.getPayload());
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 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.
@@ -17,7 +17,6 @@
package org.springframework.messaging.simp.stomp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
@@ -39,10 +38,12 @@ import org.springframework.messaging.tcp.ReconnectStrategy;
import org.springframework.messaging.tcp.TcpConnection;
import org.springframework.messaging.tcp.TcpConnectionHandler;
import org.springframework.messaging.tcp.TcpOperations;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureTask;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@@ -59,6 +60,8 @@ class StompBrokerRelayMessageHandlerTests {
private StubTcpOperations tcpClient;
ArgumentCaptor<Runnable> messageCountTaskCaptor = ArgumentCaptor.forClass(Runnable.class);
@BeforeEach
void setup() {
@@ -66,7 +69,7 @@ class StompBrokerRelayMessageHandlerTests {
this.outboundChannel = new StubMessageChannel();
this.brokerRelay = new StompBrokerRelayMessageHandler(new StubMessageChannel(),
this.outboundChannel, new StubMessageChannel(), Arrays.asList("/topic")) {
this.outboundChannel, new StubMessageChannel(), Collections.singletonList("/topic")) {
@Override
protected void startInternal() {
@@ -77,6 +80,8 @@ class StompBrokerRelayMessageHandlerTests {
this.tcpClient = new StubTcpOperations();
this.brokerRelay.setTcpClient(this.tcpClient);
this.brokerRelay.setTaskScheduler(mock(TaskScheduler.class));
}
@@ -127,18 +132,51 @@ class StompBrokerRelayMessageHandlerTests {
@Test
void destinationExcluded() {
this.brokerRelay.start();
this.brokerRelay.handleMessage(connectMessage("sess1", "joe"));
SimpMessageHeaderAccessor accessor = StompHeaderAccessor.create(StompCommand.CONNECTED);
accessor.setLeaveMutable(true);
this.tcpClient.handleMessage(MessageBuilder.createMessage(new byte[0], accessor.getMessageHeaders()));
accessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
accessor.setSessionId("sess1");
accessor.setDestination("/user/daisy/foo");
this.brokerRelay.handleMessage(MessageBuilder.createMessage(new byte[0], accessor.getMessageHeaders()));
assertThat(this.tcpClient.getSentMessages().size()).isEqualTo(2);
StompHeaderAccessor headers = this.tcpClient.getSentHeaders(0);
assertThat(headers.getCommand()).isEqualTo(StompCommand.CONNECT);
assertThat(headers.getSessionId()).isEqualTo(StompBrokerRelayMessageHandler.SYSTEM_SESSION_ID);
headers = this.tcpClient.getSentHeaders(1);
assertThat(headers.getCommand()).isEqualTo(StompCommand.CONNECT);
assertThat(headers.getSessionId()).isEqualTo("sess1");
}
@Test // gh-22822
void destinationExcludedWithHeartbeat() {
Message<byte[]> connectMessage = connectMessage("sess1", "joe");
MessageHeaderAccessor.getAccessor(connectMessage, StompHeaderAccessor.class).setHeartbeat(10000, 10000);
this.brokerRelay.start();
this.brokerRelay.handleMessage(connectMessage);
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
headers.setSessionId("sess1");
headers.setDestination("/user/daisy/foo");
this.brokerRelay.handleMessage(MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders()));
SimpMessageHeaderAccessor accessor = StompHeaderAccessor.create(StompCommand.CONNECTED);
accessor.setLeaveMutable(true);
this.tcpClient.handleMessage(MessageBuilder.createMessage(new byte[0], accessor.getMessageHeaders()));
assertThat(this.tcpClient.getSentMessages().size()).isEqualTo(1);
StompHeaderAccessor headers1 = this.tcpClient.getSentHeaders(0);
assertThat(headers1.getCommand()).isEqualTo(StompCommand.CONNECT);
assertThat(headers1.getSessionId()).isEqualTo(StompBrokerRelayMessageHandler.SYSTEM_SESSION_ID);
// Run the messageCountTask to clear the message count
verify(this.brokerRelay.getTaskScheduler()).scheduleWithFixedDelay(this.messageCountTaskCaptor.capture(), eq(5000L));
this.messageCountTaskCaptor.getValue().run();
accessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
accessor.setSessionId("sess1");
accessor.setDestination("/user/daisy/foo");
this.brokerRelay.handleMessage(MessageBuilder.createMessage(new byte[0], accessor.getMessageHeaders()));
assertThat(this.tcpClient.getSentMessages().size()).isEqualTo(3);
assertThat(this.tcpClient.getSentHeaders(2).getMessageType()).isEqualTo(SimpMessageType.HEARTBEAT);
}
@Test
@@ -227,6 +265,7 @@ class StompBrokerRelayMessageHandlerTests {
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.CONNECT);
headers.setSessionId(sessionId);
headers.setUser(new TestPrincipal(user));
headers.setLeaveMutable(true);
return MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders());
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 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.
@@ -16,11 +16,14 @@
package org.springframework.messaging.simp.user;
import java.security.Principal;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.lang.Nullable;
/**
* @author Rossen Stoyanchev
*/
@@ -41,6 +44,12 @@ public class TestSimpUser implements SimpUser {
return name;
}
@Nullable
@Override
public Principal getPrincipal() {
return null;
}
@Override
public Set<SimpSession> getSessions() {
return new HashSet<>(this.sessions.values());
@@ -0,0 +1,214 @@
/*
* 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.
* 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.messaging.converter
import kotlinx.serialization.Serializable
import org.assertj.core.api.Assertions
import org.junit.jupiter.api.Test
import org.springframework.core.MethodParameter
import org.springframework.messaging.support.MessageBuilder
import java.nio.charset.StandardCharsets
import kotlin.reflect.typeOf
@Suppress("UsePropertyAccessSyntax")
class KotlinSerializationJsonMessageConverterTests {
private val converter = KotlinSerializationJsonMessageConverter()
@Test
fun readObject() {
val payload = """
{
"bytes": [
1,
2
],
"array": [
"Foo",
"Bar"
],
"number": 42,
"string": "Foo",
"bool": true,
"fraction": 42
}
""".trimIndent()
val message = MessageBuilder.withPayload(payload.toByteArray(StandardCharsets.UTF_8)).build()
val result = converter.fromMessage(message, SerializableBean::class.java) as SerializableBean
Assertions.assertThat(result.bytes).containsExactly(0x1, 0x2)
Assertions.assertThat(result.array).containsExactly("Foo", "Bar")
Assertions.assertThat(result.number).isEqualTo(42)
Assertions.assertThat(result.string).isEqualTo("Foo")
Assertions.assertThat(result.bool).isTrue()
Assertions.assertThat(result.fraction).isEqualTo(42.0f)
}
@Test
@Suppress("UNCHECKED_CAST")
fun readArrayOfObjects() {
val payload = """
[
{
"bytes": [
1,
2
],
"array": [
"Foo",
"Bar"
],
"number": 42,
"string": "Foo",
"bool": true,
"fraction": 42
}
]
""".trimIndent()
val message = MessageBuilder.withPayload(payload.toByteArray(StandardCharsets.UTF_8)).build()
val result = converter.fromMessage(message, Array<SerializableBean>::class.java) as Array<SerializableBean>
Assertions.assertThat(result).hasSize(1)
Assertions.assertThat(result[0].bytes).containsExactly(0x1, 0x2)
Assertions.assertThat(result[0].array).containsExactly("Foo", "Bar")
Assertions.assertThat(result[0].number).isEqualTo(42)
Assertions.assertThat(result[0].string).isEqualTo("Foo")
Assertions.assertThat(result[0].bool).isTrue()
Assertions.assertThat(result[0].fraction).isEqualTo(42.0f)
}
@Test
@Suppress("UNCHECKED_CAST")
@ExperimentalStdlibApi
fun readGenericCollection() {
val payload = """
[
{
"bytes": [
1,
2
],
"array": [
"Foo",
"Bar"
],
"number": 42,
"string": "Foo",
"bool": true,
"fraction": 42
}
]
""".trimIndent()
val method = javaClass.getDeclaredMethod("handleList", List::class.java)
val param = MethodParameter(method, 0)
val message = MessageBuilder.withPayload(payload.toByteArray(StandardCharsets.UTF_8)).build()
val result = converter.fromMessage(message, typeOf<List<SerializableBean>>()::class.java, param) as List<SerializableBean>
Assertions.assertThat(result).hasSize(1)
Assertions.assertThat(result[0].bytes).containsExactly(0x1, 0x2)
Assertions.assertThat(result[0].array).containsExactly("Foo", "Bar")
Assertions.assertThat(result[0].number).isEqualTo(42)
Assertions.assertThat(result[0].string).isEqualTo("Foo")
Assertions.assertThat(result[0].bool).isTrue()
Assertions.assertThat(result[0].fraction).isEqualTo(42.0f)
}
@Test
fun readFailsOnInvalidJson() {
val payload = """
this is an invalid JSON document
""".trimIndent()
val message = MessageBuilder.withPayload(payload.toByteArray(StandardCharsets.UTF_8)).build()
Assertions.assertThatExceptionOfType(MessageConversionException::class.java).isThrownBy {
converter.fromMessage(message, SerializableBean::class.java)
}
}
@Test
fun writeObject() {
val serializableBean = SerializableBean(byteArrayOf(0x1, 0x2), arrayOf("Foo", "Bar"), 42, "Foo", true, 42.0f)
val message = converter.toMessage(serializableBean, null)
val result = String((message!!.payload as ByteArray), StandardCharsets.UTF_8)
Assertions.assertThat(result)
.contains("\"bytes\":[1,2]")
.contains("\"array\":[\"Foo\",\"Bar\"]")
.contains("\"number\":42")
.contains("\"string\":\"Foo\"")
.contains("\"bool\":true")
.contains("\"fraction\":42.0")
}
@Test
fun writeObjectWithNullableProperty() {
val serializableBean = SerializableBean(byteArrayOf(0x1, 0x2), arrayOf("Foo", "Bar"), 42, null, true, 42.0f)
val message = converter.toMessage(serializableBean, null)
val result = String((message!!.payload as ByteArray), StandardCharsets.UTF_8)
Assertions.assertThat(result)
.contains("\"bytes\":[1,2]")
.contains("\"array\":[\"Foo\",\"Bar\"]")
.contains("\"number\":42")
.contains("\"string\":null")
.contains("\"bool\":true")
.contains("\"fraction\":42.0")
}
@Test
fun writeArrayOfObjects() {
val serializableBean = SerializableBean(byteArrayOf(0x1, 0x2), arrayOf("Foo", "Bar"), 42, "Foo", true, 42.0f)
val expectedJson = """
[{"bytes":[1,2],"array":["Foo","Bar"],"number":42,"string":"Foo","bool":true,"fraction":42.0}]
""".trimIndent()
val message = converter.toMessage(arrayOf(serializableBean), null)
val result = String((message!!.payload as ByteArray), StandardCharsets.UTF_8)
Assertions.assertThat(result).isEqualTo(expectedJson)
}
@Test
@ExperimentalStdlibApi
fun writeGenericCollection() {
val serializableBean = SerializableBean(byteArrayOf(0x1, 0x2), arrayOf("Foo", "Bar"), 42, "Foo", true, 42.0f)
val expectedJson = """
[{"bytes":[1,2],"array":["Foo","Bar"],"number":42,"string":"Foo","bool":true,"fraction":42.0}]
""".trimIndent()
val method = javaClass.getDeclaredMethod("handleList", List::class.java)
val param = MethodParameter(method, 0)
val message = converter.toMessage(arrayListOf(serializableBean), null, param)
val result = String((message!!.payload as ByteArray), StandardCharsets.UTF_8)
Assertions.assertThat(result).isEqualTo(expectedJson)
}
@Suppress("UNUSED_PARAMETER")
fun handleList(payload: List<SerializableBean>) {}
@Serializable
@Suppress("ArrayInDataClass")
data class SerializableBean(
val bytes: ByteArray,
val array: Array<String>,
val number: Int,
val string: String?,
val bool: Boolean,
val fraction: Float
)
}
@@ -29,8 +29,6 @@ import io.r2dbc.spi.R2dbcTimeoutException;
import io.r2dbc.spi.R2dbcTransientException;
import io.r2dbc.spi.R2dbcTransientResourceException;
import io.r2dbc.spi.Wrapped;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import org.springframework.core.Ordered;
@@ -69,11 +67,6 @@ public abstract class ConnectionFactoryUtils {
*/
public static final int CONNECTION_SYNCHRONIZATION_ORDER = 1000;
private static final Log logger = LogFactory.getLog(ConnectionFactoryUtils.class);
private ConnectionFactoryUtils() {}
/**
* Obtain a {@link Connection} from the given {@link ConnectionFactory}.
@@ -112,48 +105,34 @@ public abstract class ConnectionFactoryUtils {
if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
conHolder.requested();
if (!conHolder.hasConnection()) {
if (logger.isDebugEnabled()) {
logger.debug("Fetching resumed R2DBC Connection from ConnectionFactory");
}
return fetchConnection(connectionFactory).doOnNext(conHolder::setConnection);
}
return Mono.just(conHolder.getConnection());
}
// Else we either got no holder or an empty thread-bound holder here.
if (logger.isDebugEnabled()) {
logger.debug("Fetching R2DBC Connection from ConnectionFactory");
}
Mono<Connection> con = fetchConnection(connectionFactory);
if (synchronizationManager.isSynchronizationActive()) {
return con.flatMap(connection -> {
return Mono.just(connection).doOnNext(conn -> {
// Use same Connection for further R2DBC actions within the transaction.
// Thread-bound object will get removed by synchronization at transaction completion.
ConnectionHolder holderToUse = conHolder;
if (holderToUse == null) {
holderToUse = new ConnectionHolder(conn);
}
else {
holderToUse.setConnection(conn);
}
holderToUse.requested();
synchronizationManager
.registerSynchronization(new ConnectionSynchronization(holderToUse, connectionFactory));
holderToUse.setSynchronizedWithTransaction(true);
if (holderToUse != conHolder) {
synchronizationManager.bindResource(connectionFactory, holderToUse);
}
}) // Unexpected exception from external delegation call -> close Connection and rethrow.
.onErrorResume(e -> releaseConnection(connection, connectionFactory).then(Mono.error(e)));
});
return con.flatMap(connection -> Mono.just(connection).doOnNext(conn -> {
// Use same Connection for further R2DBC actions within the transaction.
// Thread-bound object will get removed by synchronization at transaction completion.
ConnectionHolder holderToUse = conHolder;
if (holderToUse == null) {
holderToUse = new ConnectionHolder(conn);
}
else {
holderToUse.setConnection(conn);
}
holderToUse.requested();
synchronizationManager
.registerSynchronization(new ConnectionSynchronization(holderToUse, connectionFactory));
holderToUse.setSynchronizedWithTransaction(true);
if (holderToUse != conHolder) {
synchronizationManager.bindResource(connectionFactory, holderToUse);
}
}) // Unexpected exception from external delegation call -> close Connection and rethrow.
.onErrorResume(e -> releaseConnection(connection, connectionFactory).then(Mono.error(e))));
}
return con;
}).onErrorResume(NoTransactionException.class, e -> Mono.from(connectionFactory.create()));
}
@@ -356,9 +335,7 @@ public abstract class ConnectionFactoryUtils {
@Override
public Mono<Void> suspend() {
if (this.holderActive) {
return TransactionSynchronizationManager.forCurrentTransaction()
.flatMap(synchronizationManager -> {
return TransactionSynchronizationManager.forCurrentTransaction().flatMap(synchronizationManager -> {
synchronizationManager.unbindResource(this.connectionFactory);
if (this.connectionHolder.hasConnection() && !this.connectionHolder.isOpen()) {
// Release Connection on suspend if the application doesn't keep
@@ -371,7 +348,6 @@ public abstract class ConnectionFactoryUtils {
return Mono.empty();
});
}
return Mono.empty();
}
@@ -388,11 +364,10 @@ public abstract class ConnectionFactoryUtils {
@Override
public Mono<Void> beforeCompletion() {
// Release Connection early if the holder is not open anymore
// (that is, not used by another resource
// that has its own cleanup via transaction synchronization),
// to avoid issues with strict transaction implementations that expect
// the close call before transaction completion.
// Release Connection early if the holder is not open anymore (that is,
// not used by another resource that has its own cleanup via transaction
// synchronization), to avoid issues with strict transaction implementations
// that expect the close call before transaction completion.
if (!this.connectionHolder.isOpen()) {
return TransactionSynchronizationManager.forCurrentTransaction().flatMap(synchronizationManager -> {
synchronizationManager.unbindResource(this.connectionFactory);
@@ -414,8 +389,7 @@ public abstract class ConnectionFactoryUtils {
if (this.holderActive) {
// The bound ConnectionHolder might not be available anymore,
// since afterCompletion might get called from a different thread.
return TransactionSynchronizationManager.forCurrentTransaction()
.flatMap(synchronizationManager -> {
return TransactionSynchronizationManager.forCurrentTransaction().flatMap(synchronizationManager -> {
synchronizationManager.unbindResourceIfPossible(this.connectionFactory);
this.holderActive = false;
if (this.connectionHolder.hasConnection()) {
@@ -426,7 +400,6 @@ public abstract class ConnectionFactoryUtils {
return Mono.empty();
});
}
this.connectionHolder.reset();
return Mono.empty();
}
@@ -107,8 +107,6 @@ public abstract class ScriptUtils {
private static final Log logger = LogFactory.getLog(ScriptUtils.class);
// utility constructor
private ScriptUtils() {}
/**
* Split an SQL script into separate statements delimited by the provided
@@ -335,14 +333,11 @@ public abstract class ScriptUtils {
return DataBufferUtils.join(DataBufferUtils.read(resource.getResource(), dataBufferFactory, 8192))
.handle((it, sink) -> {
try (InputStream is = it.asInputStream()) {
InputStreamReader in = resource.getCharset() != null ? new InputStreamReader(is, resource.getCharset())
: new InputStreamReader(is);
InputStreamReader in = (resource.getCharset() != null ?
new InputStreamReader(is, resource.getCharset()) : new InputStreamReader(is));
LineNumberReader lnr = new LineNumberReader(in);
String script = readScript(lnr, commentPrefixes, separator, blockCommentEndDelimiter);
sink.next(script);
sink.complete();
}
@@ -548,9 +543,9 @@ public abstract class ScriptUtils {
* @see org.springframework.r2dbc.connection.ConnectionFactoryUtils#releaseConnection
*/
public static Mono<Void> executeSqlScript(Connection connection, EncodedResource resource,
DataBufferFactory dataBufferFactory, boolean continueOnError, boolean ignoreFailedDrops, String commentPrefix,
@Nullable String separator, String blockCommentStartDelimiter, String blockCommentEndDelimiter)
throws ScriptException {
DataBufferFactory dataBufferFactory, boolean continueOnError, boolean ignoreFailedDrops,
String commentPrefix, @Nullable String separator, String blockCommentStartDelimiter,
String blockCommentEndDelimiter) throws ScriptException {
return executeSqlScript(connection, resource, dataBufferFactory, continueOnError,
ignoreFailedDrops, new String[] { commentPrefix }, separator,
@@ -587,10 +582,10 @@ public abstract class ScriptUtils {
* @see org.springframework.r2dbc.connection.ConnectionFactoryUtils#getConnection
* @see org.springframework.r2dbc.connection.ConnectionFactoryUtils#releaseConnection
*/
public static Mono<Void> executeSqlScript(Connection connection, EncodedResource resource, DataBufferFactory dataBufferFactory,
boolean continueOnError,
boolean ignoreFailedDrops, String[] commentPrefixes, @Nullable String separator,
String blockCommentStartDelimiter, String blockCommentEndDelimiter) throws ScriptException {
public static Mono<Void> executeSqlScript(Connection connection, EncodedResource resource,
DataBufferFactory dataBufferFactory, boolean continueOnError, boolean ignoreFailedDrops,
String[] commentPrefixes, @Nullable String separator, String blockCommentStartDelimiter,
String blockCommentEndDelimiter) throws ScriptException {
if (logger.isDebugEnabled()) {
logger.debug("Executing SQL script from " + resource);
@@ -622,17 +617,15 @@ public abstract class ScriptUtils {
});
if (logger.isDebugEnabled()) {
executeScript = executeScript.doOnComplete(() -> {
long elapsedTime = System.currentTimeMillis() - startTime;
logger.debug("Executed SQL script from " + resource + " in " + elapsedTime + " ms.");
});
}
return executeScript.onErrorMap(ex -> !(ex instanceof ScriptException),
ex -> new UncategorizedScriptException("Failed to execute database script from resource [" + resource + "]",
ex))
ex -> new UncategorizedScriptException(
"Failed to execute database script from resource [" + resource + "]", ex))
.then();
}
@@ -644,22 +637,21 @@ public abstract class ScriptUtils {
.collect(Collectors.summingLong(count -> count));
if (logger.isDebugEnabled()) {
execution = execution.doOnNext(rowsAffected -> logger.debug(rowsAffected + " returned as update count for SQL: " + statement));
execution = execution.doOnNext(rowsAffected ->
logger.debug(rowsAffected + " returned as update count for SQL: " + statement));
}
return execution.onErrorResume(ex -> {
boolean dropStatement = StringUtils.startsWithIgnoreCase(statement.trim(), "drop");
if (continueOnError || (dropStatement && ignoreFailedDrops)) {
if (logger.isDebugEnabled()) {
logger.debug(ScriptStatementFailedException.buildErrorMessage(statement, statementNumber.get(), resource),
ex);
logger.debug(ScriptStatementFailedException.buildErrorMessage(
statement, statementNumber.get(), resource), ex);
}
}
else {
return Mono.error(new ScriptStatementFailedException(statement, statementNumber.get(), resource, ex));
}
return Mono.empty();
}).then();
}
@@ -167,6 +167,7 @@ public abstract class AbstractRoutingConnectionFactory implements ConnectionFact
*/
protected ConnectionFactory resolveSpecifiedConnectionFactory(Object connectionFactory)
throws IllegalArgumentException {
if (connectionFactory instanceof ConnectionFactory) {
return (ConnectionFactory) connectionFactory;
}
@@ -174,17 +175,14 @@ public abstract class AbstractRoutingConnectionFactory implements ConnectionFact
return this.connectionFactoryLookup.getConnectionFactory((String) connectionFactory);
}
else {
throw new IllegalArgumentException(
"Illegal connection factory value - only 'io.r2dbc.spi.ConnectionFactory' and 'String' supported: "
+ connectionFactory);
throw new IllegalArgumentException("Illegal connection factory value - " +
"only 'io.r2dbc.spi.ConnectionFactory' and 'String' supported: " + connectionFactory);
}
}
@Override
public Mono<Connection> create() {
return determineTargetConnectionFactory() //
.map(ConnectionFactory::create) //
.flatMap(Mono::from);
return determineTargetConnectionFactory().map(ConnectionFactory::create).flatMap(Mono::from);
}
@Override
@@ -56,7 +56,6 @@ public class MapConnectionFactoryLookup implements ConnectionFactoryLookup {
/**
* Create a new instance of the {@link MapConnectionFactoryLookup} class.
*
* @param connectionFactoryName the name under which the supplied {@link ConnectionFactory} is to be added
* @param connectionFactory the {@link ConnectionFactory} to be added
*/

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