作为一门新兴的现代化编程语言,Kotlin正获得广泛的关注,Spring社区也将支持Kotlin
语言作为下一阶段的重要工作,甚至抛出了Spring Loves Kotin和A Match Made in Heaven这般暧昧的论调。本文暂不去讨论Kotlin
语言的细节,而是通过使用Kotlin
和Spring 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));
下面对各模块进行简要说明,
Producer
: 作为数据源产生数据,并将数据通过MQ
传给后面的Processor
进行处理;
Processor
: 在MQ
中读取Producer
产生的消息并加以处理,并将处理后的结果通过MQ
传给后面的Consumer
;
Consumer
: 在MQ
中读取Processor
产生的消息并转发至Redis
中的Pub-Sub Topic
;
Notifier
: 订阅Redis
中的Pub-Sub Topic
并处理由Consumer
发布的消息,并通过SSE
转发给订阅消息的Client
;
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
文件)应该不会影响大家对代码的理解。
下面简要说明一下该系统的业务逻辑:
Producter
每隔1秒钟产生一个Product
消息,每个Product
包含唯一的id
和随机产生的name
、price
以及createdTime
;
Processor
处理Product
消息,根据预先定义的ExchangeRate
来计算Product
在多种货币下的价格,并生成ProductExchange
对象;
Consumer
收到ProductExchange
对象后,过滤价格小于500
的,并将其转发至Redis
的Pub-Sub Topic
中;
Notifier
订阅Pub-Sub Topic
,并将ProductExchange
按照Client
请求的货币种类转换成ProductLocal
对象,并以SSE
事件的形式返回给Client
.
代码实现
接下来我们根据上面讨论的系统架构、项目模块以及业务逻辑来实现代码。
Core
功能模块的公共部分都放置在Core模块
内,我们可以在Core.kt文件里定义功能模块所需的domain、常量以及工具类。首先我们先使用Kotlin
的特性之一,数据类来构建Product
和ProductExchange
,
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分别来实现Producer
、Processor
以及Consumer
模块。最新版的Spring Cloud Stream彻底拥抱了函数式
,使用Routing Function替代了早期版本中的@EnableBinding
、@StreamEmitter
以及@StreamListener
等注解,其对应关系为,
对于Producer
,我们只需注册一个类型为Supplier
的Bean
,而Reactive
的Producer
,只需要能够一个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
中名为products
的Exchange
,我们只需要在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
对应Input
与Output
两个Binding
,所以配置中需要配置两个destination
,Input
来自于products
,Output
指向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
|
Input
的Binding
需要额外配置group
的名称,这样可以有多个Consumer
同时消费Exchange
中的数据来提高并行处理能力。
Consumer
接下来我们实现Consumer
,按照最新的Spring Cloud Stream
规范,Consumer
应该对应一个java.util.function.Consumer
实现。这里有一点需要注意,由于我们采用的是Reactive
形式,而Reactive
有自己的Void
类型,而不是Java
的void
关键字,所以这里的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
的序列化/反序列化器。
当然,我们也需要配置一下Consumer
的destination
和group
,
1 2
| spring.cloud.stream.bindings.productExchangeConsumer-in-0.destination=product_exchanges spring.cloud.stream.bindings.productExchangeConsumer-in-0.group=product_exchanges_consumer
|
Notifier
最后是Notifier
。Notifier
本身不需要Spring Cloud Stream
的支持,只需要订阅Redis
中的Pub-Sub Topic
,与Consumer
,用于监听Redis
的ReactiveRedisMessageListenerContainer也需要自定义一个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
的监听,同时将订阅的数据转发至一个Reactor
的Processor
中,这个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") ) ) ) } } } } } ) } }
|
以上代码初看比较奇特,其中的beans
、bean
、router
都属于基于Kotlin Type-Safe Builders的DSL
。这里我们不展开说明,只需要了解Spring Kotlin DSL
提供了一种更为简单直接的配置方式。至此,所有模块都实现完毕。
运行
下面我们开始运行代码。
在运行之前,保证本地已经运行了RabbitMQ
和Redis
。
首先是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
来订阅Redis
的Pub-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
仍然表现的足够简洁与高效。Spring
与Kotlin
的初次相见就已擦出火花,相信在未来,Spring
与Kotlin
还会迸射出更加绮丽的色彩。
本文的源代码已放置在Github,欢迎大家一起讨论。谢谢!