作为一门新兴的现代化编程语言,Kotlin正获得广泛的关注,Spring社区也将支持Kotlin语言作为下一阶段的重要工作,甚至抛出了Spring Loves KotinA Match Made in Heaven这般暧昧的论调。本文暂不去讨论Kotlin语言的细节,而是通过使用KotlinSpring Cloud Stream构建实时消息系统来领略一番Spring Loves Kotlin的魅力。

系统架构

实时消息系统可适用于多种业务场景,本文将实现一个非常典型的案例,其它复杂案例都可以通过此案例扩展,其架构如下图所示:

graph LR;
    A(1.Producer)-->|MQ|B(2.Processor);
    B-->|MQ|C(3.Consumer);
    C-->|Pub-Sub|D(4.Notifier);
    D-->|Http|E((5.Client));

下面对各模块进行简要说明,

  1. Producer: 作为数据源产生数据,并将数据通过MQ传给后面的Processor进行处理;
  2. Processor: 在MQ中读取Producer产生的消息并加以处理,并将处理后的结果通过MQ传给后面的Consumer;
  3. Consumer: 在MQ中读取Processor产生的消息并转发至Redis中的Pub-Sub Topic;
  4. Notifier: 订阅Redis中的Pub-Sub Topic并处理由Consumer发布的消息,并通过SSE转发给订阅消息的Client;
  5. Client: 通过Http订阅由Notifier发布的SSE事件;

本文中的案例将会实现模块1~4,模块5Client不做实现,可通过curl等http客户端进行模拟。

项目模块

按照上面讨论的系统架构,我们创建一个多模块的Gradle项目,并用Kotlin DSL作为描述语言,项目结构如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.
├── gradle
│   └── wrapper
├── rmkt-consumer
│   ├── src
│   └── build.gradle.kts
├── rmkt-core
│   ├── src
│   └── build.gradle.kts
├── rmkt-notifier
│   ├── src
│   └── build.gradle.kts
├── rmkt-processor
│   ├── src
│   └── build.gradle.kts
├── rmkt-producer
│   ├── src
│   └── build.gradle.kts
├── HELP.md
├── README.md
├── build.gradle.kts
├── gradlew
├── gradlew.bat
└── settings.gradle.kts

这些模块都可以与上面讨论过的系统架构一一对应,唯一的不同点是项目中多出了一个core模块,该模块用于放置各模块的公用部分,例如domain对象、常量定义以及公共方法等,稍后我们会详细说明。本文不会讨论Kotlin DSL的细节,由于没有涉及过多的知识点,各模块的build.gradle.kts文件(类似于Maven中的pom.xml文件)应该不会影响大家对代码的理解。

下面简要说明一下该系统的业务逻辑:

  1. Producter每隔1秒钟产生一个Product消息,每个Product包含唯一的id和随机产生的nameprice以及createdTime;
  2. Processor处理Product消息,根据预先定义的ExchangeRate来计算Product在多种货币下的价格,并生成ProductExchange对象;
  3. Consumer收到ProductExchange对象后,过滤价格小于500的,并将其转发至RedisPub-Sub Topic中;
  4. Notifier订阅Pub-Sub Topic,并将ProductExchange按照Client请求的货币种类转换成ProductLocal对象,并以SSE事件的形式返回给Client.

代码实现

接下来我们根据上面讨论的系统架构、项目模块以及业务逻辑来实现代码。

Core

功能模块的公共部分都放置在Core模块内,我们可以在Core.kt文件里定义功能模块所需的domain、常量以及工具类。首先我们先使用Kotlin的特性之一,数据类来构建ProductProductExchange

1
2
3
4
5
6
7
8
9
10
11
data class Product(
val id: String,
val name: String,
val price: Double,
val createdTime: LocalDateTime
)

data class ProductExchange(
val product: Product,
val localPrices: Map<String, Double>
)

然后再使用枚举类构建ExchangeRate

1
2
3
4
5
6
7
enum class ExchangeRate(val rate: Double) {
USD(1.00),
CNY(6.7906),
JPY(105.3246),
EUR(0.8535),
GBP(0.773)
}

最后我们再使用伴生对象来模拟Java中的常量定义,

1
2
3
4
5
6
class Constants {
companion object {
const val PRODUCT_EXCHANGE_TOPIC = "topic:pe"
const val DEFAULT_CURRENCY = "USD"
}
}

最后我们再使用对象声明来实现一个单例模式,这是一个单例的ObjectMapper,预定义了一些特性,可用于后续的JSON对象的序列化/反序列化,需要使用该对象的时候,只需要使用ObjectMapperExtension.instance即可。

1
2
3
4
5
object ObjectMapperExtension {
val instance: ObjectMapper = jacksonObjectMapper()
.registerModules(Jdk8Module(), JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
}

Producer

接下来我们将借助Spring Cloud Stream分别来实现ProducerProcessor以及Consumer模块。最新版的Spring Cloud Stream彻底拥抱了函数式,使用Routing Function替代了早期版本中的@EnableBinding@StreamEmitter以及@StreamListener等注解,其对应关系为,

Annotation Routing Function
Source, @StreamEmitter java.util.function.Supplier
Sink, @StreamListener java.util.function.Consumer
Processor, @EnableBinding(Processor.class) java.util.function.Function

对于Producer,我们只需注册一个类型为SupplierBean,而ReactiveProducer,只需要能够一个Supplier<Flux<T>>即可,代码如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@SpringBootApplication
class ProducerApplication

fun main(args: Array<String>) {
runApplication<ProducerApplication>(*args)
}

@Configuration
class SourceConfig {

@Bean
fun productSource(): Supplier<Flux<Product>> = Supplier {
Flux.interval(Duration.ofSeconds(1)).map {
Product(
UUID.randomUUID().toString(),
RandomStringUtils.randomAlphanumeric(5, 10),
Random.nextDouble(1000.00),
LocalDateTime.now()
)
}.onBackpressureDrop().log()
}

}

以上代码会每秒钟产生一个ID为UUID,名字为5~10随机字母,价格为0~1000的随机商品。
数据由Supplier产生后,我们需要告知Spring Cloud Stream这些数据的Destination,这个Destination应该指向RabbitMQ中名为productsExchange,我们只需要在application.properties加入下面配置即可,Spring Cloud Stream会自动创建这个Exchange

1
spring.cloud.stream.bindings.productSource-out-0.destination=products

这里的Binding名称就是Supplier的Bean名称productSource

Processor

接下来我们实现Processor,按照上一节的说明,Processor应该是一个Function,入参是Flux<Product>,出参是Flux<ProductExchange>,并完成localPrices的计算,详细代码如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@SpringBootApplication
class ProcessorApplication

fun main(args: Array<String>) {
runApplication<ProcessorApplication>(*args)
}

@Configuration
class ProcessorConfig {

@Bean
fun productProcessor(): Function<Flux<Product>, Flux<ProductExchange>> = Function {
it.log()
.map { product ->
ProductExchange(
product,
ExchangeRate.values()
.map { exchange -> exchange.name to exchange.rate * product.price }
.toMap()
)
}.log()
}

}

由于Processor对应InputOutput两个Binding,所以配置中需要配置两个destinationInput来自于productsOutput指向product_exchanges

1
2
3
4
spring.cloud.stream.bindings.productProcessor-in-0.destination=products
spring.cloud.stream.bindings.productProcessor-in-0.group=product_processor

spring.cloud.stream.bindings.productProcessor-out-0.destination=product_exchanges

InputBinding需要额外配置group的名称,这样可以有多个Consumer同时消费Exchange中的数据来提高并行处理能力。

Consumer

接下来我们实现Consumer,按照最新的Spring Cloud Stream规范,Consumer应该对应一个java.util.function.Consumer实现。这里有一点需要注意,由于我们采用的是Reactive形式,而Reactive有自己的Void类型,而不是Javavoid关键字,所以这里的Consumer<Flux<T>>应该使用Function<Flux<?>, Mono<Void>>代替,相反的,如果是非Reactive模式下,还是应该正常使用java.util.function.Consumer作为函数类型。

另外,Consumer在过滤掉低价格的ProductExchange后,还需要将消息转发至Redis中的Pub-Sub Topic中,这里我们还需要配置一个JSON的序列化/反序列化器,这样Pub到Redis中的消息会以JSON数据格式表示,而不是默认的普通String ,这里的ObjectMapper将使用在Core模块里定义好的ObjectMapperExtension

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@SpringBootApplication
class ConsumerApplication

fun main(args: Array<String>) {
runApplication<ConsumerApplication>(*args)
}

@Configuration
class ConsumerConfig {

@Bean
fun productExchangeConsumer(operations: ReactiveRedisOperations<String, ProductExchange>)
: Function<Flux<ProductExchange>, Mono<Void>> =
Function {
it.filter { pe -> pe.product.price > 500.00 }
.log()
.flatMap { pe -> operations.convertAndSend(PRODUCT_EXCHANGE_TOPIC, pe) }
.then()
}

@Bean
fun productExchangeReactiveRedisOperations(factory: ReactiveRedisConnectionFactory)
: ReactiveRedisOperations<String, ProductExchange> =

Jackson2JsonRedisSerializer(ProductExchange::class.java).also {
it.setObjectMapper(ObjectMapperExtension.instance)
}.let {
RedisSerializationContext.newSerializationContext<String, ProductExchange>(StringRedisSerializer())
.value(it).build()
}.let { ReactiveRedisTemplate<String, ProductExchange>(factory, it) }

}

这段代码中,我们定义了两个@Bean,一个是作为Consumer用来处理数据的Function,另外一个就是用来发布消息的ReactiveRedisTemplate,并且配置了JSON的序列化/反序列化器。
当然,我们也需要配置一下Consumerdestinationgroup

1
2
spring.cloud.stream.bindings.productExchangeConsumer-in-0.destination=product_exchanges
spring.cloud.stream.bindings.productExchangeConsumer-in-0.group=product_exchanges_consumer

Notifier

最后是NotifierNotifier本身不需要Spring Cloud Stream的支持,只需要订阅Redis中的Pub-Sub Topic,与Consumer,用于监听RedisReactiveRedisMessageListenerContainer也需要自定义一个JSON的序列化/反序列化器,我们把这部分逻辑封装在NotifyService中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class NotifyService(private val container: ReactiveRedisMessageListenerContainer) {

private val processor = ReplayProcessor.create<ProductExchange>()

@PostConstruct
fun init() =
processor.sink().let { sink ->
container.receive(
listOf(ChannelTopic.of(PRODUCT_EXCHANGE_TOPIC)),
RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer()),
RedisSerializationContext.SerializationPair.fromSerializer(
Jackson2JsonRedisSerializer(ProductExchange::class.java).also {
it.setObjectMapper(ObjectMapperExtension.instance)
}
)
)
.map { m -> m.message }
.doOnNext { sink.next(it) }
.log()
.subscribe({}, {})
}

fun notifyEvents(currency: String): Flux<ServerSentEvent<ProductLocal>> =
processor.map { p ->
(if (currency.toUpperCase() !in p.localPrices.keys) DEFAULT_CURRENCY else currency.toUpperCase())
.let {
ProductLocal(
p.product.id,
p.product.name,
p.product.createdTime,
BigDecimal(p.localPrices[it] ?: 0.0).setScale(4, RoundingMode.HALF_EVEN),
it
)
}.let { ServerSentEvent.builder(it).id(it.id).build() }
}
}

data class ProductLocal(
val id: String,
val name: String,
val createdTime: LocalDateTime,
val localPrice: BigDecimal,
val localCurrency: String
)

上述代码比较容易理解,init方法用于在容器启动时,完成对Pub-Sub Topic的监听,同时将订阅的数据转发至一个ReactorProcessor中,这个Processor就是之后SSE事件的数据源。另外我们还提供一个notifyEvents方法,用于接收currency参数,将缓存在Processor中的数据加工成ProductLocal,并作为SSE发给客户端。

接下来我们要向Spring容器注册这个service,以及API的Endpoint。与之前采用的@Configuration方式不同,这里我们尝试使用Spring Kotlin DSL来注册Bean。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@SpringBootApplication
class NotifierApplication

fun main(args: Array<String>) {
runApplication<NotifierApplication>(*args) {
addInitializers(
beans {
bean<ReactiveRedisMessageListenerContainer>()
bean<NotifyService>()
bean {
ref<NotifyService>().let { notifyService ->
router {
GET("/pl/{currency}") {
ServerResponse.ok().body(
BodyInserters.fromServerSentEvents(
notifyService.notifyEvents(
it.pathVariable("currency")
)
)
)
}
}
}
}
}
)
}
}

以上代码初看比较奇特,其中的beansbeanrouter都属于基于Kotlin Type-Safe BuildersDSL。这里我们不展开说明,只需要了解Spring Kotlin DSL提供了一种更为简单直接的配置方式。至此,所有模块都实现完毕。

运行

下面我们开始运行代码。

在运行之前,保证本地已经运行了RabbitMQRedis

注意

首先是Producer,从console中我们可以看到Product数据源源不断产生并虽送到了MQ,

1
2
3
4
5
6
7
8
9
10
11
2020-10-05 14:32:38.518  INFO 5925 --- [oundedElastic-1] o.s.a.r.c.CachingConnectionFactory       : Created new connection: rabbitConnectionFactory.publisher#42e0654e:0/SimpleConnection@75a0a52e [delegate=amqp://guest@127.0.0.1:5672/, localPort= 52496]
2020-10-05 14:32:39.273 INFO 5925 --- [ parallel-1] reactor.Flux.OnBackpressureDrop.1 : onNext(Product(id=6c2b236e-40f8-44ff-a1cf-7a94910944ac, name=cv9pATmM, price=395.31181210651545, createdTime=2020-10-05T14:32:39.272939))
2020-10-05 14:32:40.272 INFO 5925 --- [ parallel-1] reactor.Flux.OnBackpressureDrop.1 : onNext(Product(id=8a10bf35-5e37-4e74-96da-7c8471e56937, name=8Qoagte, price=765.5943349019311, createdTime=2020-10-05T14:32:40.272813))
2020-10-05 14:32:41.272 INFO 5925 --- [ parallel-1] reactor.Flux.OnBackpressureDrop.1 : onNext(Product(id=03751003-daa9-4f19-be69-5ac434b70b7f, name=FihxILXp9, price=351.88328720033377, createdTime=2020-10-05T14:32:41.272239))
2020-10-05 14:32:42.268 INFO 5925 --- [ parallel-1] reactor.Flux.OnBackpressureDrop.1 : onNext(Product(id=d7788f46-009a-4a95-ae06-df88ca133d87, name=6DmSTJE, price=504.01347237601976, createdTime=2020-10-05T14:32:42.268429))
2020-10-05 14:32:43.267 INFO 5925 --- [ parallel-1] reactor.Flux.OnBackpressureDrop.1 : onNext(Product(id=26a06bf0-91a7-4d2f-b15a-f666e072716b, name=J7G766jO0, price=597.8645160007096, createdTime=2020-10-05T14:32:43.267724))
2020-10-05 14:32:44.268 INFO 5925 --- [ parallel-1] reactor.Flux.OnBackpressureDrop.1 : onNext(Product(id=9c0fd106-e00c-44b5-8187-2566737395bb, name=kyWUhn3J, price=152.01109964002336, createdTime=2020-10-05T14:32:44.268194))
2020-10-05 14:32:45.268 INFO 5925 --- [ parallel-1] reactor.Flux.OnBackpressureDrop.1 : onNext(Product(id=110c91ae-41b2-4223-bf33-3759c4f4dc52, name=vlULf6xJ8, price=506.1068481643988, createdTime=2020-10-05T14:32:45.268531))
2020-10-05 14:32:46.270 INFO 5925 --- [ parallel-1] reactor.Flux.OnBackpressureDrop.1 : onNext(Product(id=45c84ed6-7208-4e19-9ead-e9b778512a77, name=8kzXJ, price=15.856892691681669, createdTime=2020-10-05T14:32:46.270622))
2020-10-05 14:32:47.270 INFO 5925 --- [ parallel-1] reactor.Flux.OnBackpressureDrop.1 : onNext(Product(id=569b85c0-7bdd-45ae-a83a-db237779d90f, name=pAZswdD, price=205.12230260372854, createdTime=2020-10-05T14:32:47.270448))
......

接下来是Processor,从console中我们可以看到Product被处理,ProductExchange数据不断产生,

1
2
3
4
5
6
7
8
9
10
11
12
13
2020-10-05 14:32:38.772  INFO 5898 --- [uct_processor-1] o.s.a.r.c.CachingConnectionFactory       : Attempting to connect to: [localhost:5672]
2020-10-05 14:32:38.778 INFO 5898 --- [uct_processor-1] o.s.a.r.c.CachingConnectionFactory : Created new connection: rabbitConnectionFactory.publisher#a62b940:0/SimpleConnection@c4b6d1e [delegate=amqp://guest@127.0.0.1:5672/, localPort= 52498]
2020-10-05 14:32:39.276 INFO 5898 --- [uct_processor-1] reactor.Flux.Map.1 : onNext(Product(id=6c2b236e-40f8-44ff-a1cf-7a94910944ac, name=cv9pATmM, price=395.31181210651545, createdTime=2020-10-05T14:32:39.272939))
2020-10-05 14:32:39.276 INFO 5898 --- [uct_processor-1] reactor.Flux.Map.2 : onNext(ProductExchange(product=Product(id=6c2b236e-40f8-44ff-a1cf-7a94910944ac, name=cv9pATmM, price=395.31181210651545, createdTime=2020-10-05T14:32:39.272939), localPrices={USD=395.31181210651545, CNY=2684.404391290504, JPY=41636.0584853939, EUR=337.39863163291096, GBP=305.57603075833646}))
2020-10-05 14:32:40.277 INFO 5898 --- [uct_processor-1] reactor.Flux.Map.1 : onNext(Product(id=8a10bf35-5e37-4e74-96da-7c8471e56937, name=8Qoagte, price=765.5943349019311, createdTime=2020-10-05T14:32:40.272813))
2020-10-05 14:32:40.277 INFO 5898 --- [uct_processor-1] reactor.Flux.Map.2 : onNext(ProductExchange(product=Product(id=8a10bf35-5e37-4e74-96da-7c8471e56937, name=8Qoagte, price=765.5943349019311, createdTime=2020-10-05T14:32:40.272813), localPrices={USD=765.5943349019311, CNY=5198.844890585054, JPY=80635.91708581193, EUR=653.4347648387983, GBP=591.8044208791928}))
2020-10-05 14:32:41.276 INFO 5898 --- [uct_processor-1] reactor.Flux.Map.1 : onNext(Product(id=03751003-daa9-4f19-be69-5ac434b70b7f, name=FihxILXp9, price=351.88328720033377, createdTime=2020-10-05T14:32:41.272239))
2020-10-05 14:32:41.276 INFO 5898 --- [uct_processor-1] reactor.Flux.Map.2 : onNext(ProductExchange(product=Product(id=03751003-daa9-4f19-be69-5ac434b70b7f, name=FihxILXp9, price=351.88328720033377, createdTime=2020-10-05T14:32:41.272239), localPrices={USD=351.88328720033377, CNY=2389.4986500625864, JPY=37061.966471060274, EUR=300.3323856254849, GBP=272.005781005858}))
2020-10-05 14:32:42.277 INFO 5898 --- [uct_processor-1] reactor.Flux.Map.1 : onNext(Product(id=d7788f46-009a-4a95-ae06-df88ca133d87, name=6DmSTJE, price=504.01347237601976, createdTime=2020-10-05T14:32:42.268429))
2020-10-05 14:32:42.277 INFO 5898 --- [uct_processor-1] reactor.Flux.Map.2 : onNext(ProductExchange(product=Product(id=d7788f46-009a-4a95-ae06-df88ca133d87, name=6DmSTJE, price=504.01347237601976, createdTime=2020-10-05T14:32:42.268429), localPrices={USD=504.01347237601976, CNY=3422.5538855166, JPY=53085.01737261533, EUR=430.17549867293286, GBP=389.6024141466633}))
2020-10-05 14:32:43.270 INFO 5898 --- [uct_processor-1] reactor.Flux.Map.1 : onNext(Product(id=26a06bf0-91a7-4d2f-b15a-f666e072716b, name=J7G766jO0, price=597.8645160007096, createdTime=2020-10-05T14:32:43.267724))
2020-10-05 14:32:43.270 INFO 5898 --- [uct_processor-1] reactor.Flux.Map.2 : onNext(ProductExchange(product=Product(id=26a06bf0-91a7-4d2f-b15a-f666e072716b, name=J7G766jO0, price=597.8645160007096, createdTime=2020-10-05T14:32:43.267724), localPrices={USD=597.8645160007096, CNY=4059.858782354419, JPY=62969.84100196834, EUR=510.27736440660567, GBP=462.14927086854857}))
......

然后是Consumer,我们可以直接通过redis-cli来订阅RedisPub-Sub Topic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
127.0.0.1:6379> subscribe topic:pe
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "topic:pe"
3) (integer) 1
1) "message"
2) "topic:pe"
3) "{\"product\":{\"id\":\"3e155fa7-9f42-4685-aeba-534342bc2f05\",\"name\":\"tvnzfHSJh\",\"price\":908.6521294566128,\"createdTime\":\"2020-10-05T14:36:03.278801\"},\"localPrices\":{\"USD\":908.6521294566128,\"CNY\":6170.293150288076,\"JPY\":95703.42207416597,\"EUR\":775.5345924912191,\"GBP\":702.3880960699616}}"
1) "message"
2) "topic:pe"
3) "{\"product\":{\"id\":\"563638ee-deef-4102-92f8-f8a95dbcea71\",\"name\":\"4n93HpIDy\",\"price\":730.6959805104445,\"createdTime\":\"2020-10-05T14:36:06.273764\"},\"localPrices\":{\"USD\":730.6959805104445,\"CNY\":4961.864125254225,\"JPY\":76960.26186887037,\"EUR\":623.6490193656645,\"GBP\":564.8279929345737}}"
1) "message"
2) "topic:pe"
3) "{\"product\":{\"id\":\"f578f6ef-2858-4f2c-8ddb-53aff31e7b60\",\"name\":\"tBJPIJg\",\"price\":997.1833424008273,\"createdTime\":\"2020-10-05T14:36:07.274668\"},\"localPrices\":{\"USD\":997.1833424008273,\"CNY\":6771.473204907058,\"JPY\":105027.93666503018,\"EUR\":851.0959827391061,\"GBP\":770.8227236758395}}"
1) "message"
2) "topic:pe"
3) "{\"product\":{\"id\":\"8825530f-4c92-4989-80a7-11344752eb95\",\"name\":\"ffAND\",\"price\":656.5872951974707,\"createdTime\":\"2020-10-05T14:36:08.278398\"},\"localPrices\":{\"USD\":656.5872951974707,\"CNY\":4458.621686767945,\"JPY\":69154.79423175553,\"EUR\":560.3972564510412,\"GBP\":507.5419791876448}}"
1) "message"
2) "topic:pe"
3) "{\"product\":{\"id\":\"bed9f33f-2c00-4e6f-a73a-27c2d4472a25\",\"name\":\"t1eCrzm\",\"price\":569.2236131405484,\"createdTime\":\"2020-10-05T14:36:09.276028\"},\"localPrices\":{\"USD\":569.2236131405484,\"CNY\":3865.3698673922086,\"JPY\":59953.24936458301,\"EUR\":485.8323538154581,\"GBP\":440.00985295764394}}"
......

最后是Notifier,我们可以直接使用curl命令来调用API,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
curl http://localhost:8080/pl/jpy
id:8a10bf35-5e37-4e74-96da-7c8471e56937
data:{"id":"8a10bf35-5e37-4e74-96da-7c8471e56937","name":"8Qoagte","createdTime":"2020-10-05T14:32:40.272813","localPrice":80635.9171,"localCurrency":"JPY"}

id:d7788f46-009a-4a95-ae06-df88ca133d87
data:{"id":"d7788f46-009a-4a95-ae06-df88ca133d87","name":"6DmSTJE","createdTime":"2020-10-05T14:32:42.268429","localPrice":53085.0174,"localCurrency":"JPY"}

id:26a06bf0-91a7-4d2f-b15a-f666e072716b
data:{"id":"26a06bf0-91a7-4d2f-b15a-f666e072716b","name":"J7G766jO0","createdTime":"2020-10-05T14:32:43.267724","localPrice":62969.8410,"localCurrency":"JPY"}

id:110c91ae-41b2-4223-bf33-3759c4f4dc52
data:{"id":"110c91ae-41b2-4223-bf33-3759c4f4dc52","name":"vlULf6xJ8","createdTime":"2020-10-05T14:32:45.268531","localPrice":53305.5013,"localCurrency":"JPY"}

id:fb014f7e-3709-4223-85a2-6b4b6b6927b3
data:{"id":"fb014f7e-3709-4223-85a2-6b4b6b6927b3","name":"LpoMwa","createdTime":"2020-10-05T14:32:48.271629","localPrice":75403.9377,"localCurrency":"JPY"}
......

总结

本文只是做了一个Spring + Kotlin的初步整合应用,并少量涉及了Spring针对Kotlin所做的一些定制和优化。从这些简单的示例中,我们仍然能够感受到Kotlin语言的强大表现力和独特的魅力。即使不去使用DSL等稍微高级的特性,Kotlin仍然表现的足够简洁与高效。SpringKotlin的初次相见就已擦出火花,相信在未来,SpringKotlin还会迸射出更加绮丽的色彩。

本文的源代码已放置在Github,欢迎大家一起讨论。谢谢!