R3 Corda 和 springboot 集成

R3 corda

为什么Corda要集成springboot

因为Corda内置的Corda Webserver已经被标记成弃用了,一般不再提供支持;再者,springboot的生态明显占优。

太长不读篇

  1. 独立的module依赖corda和cordapps
  2. Connection RPC
  3. Run server task
  4. Integration test

精读篇

1. 独立的module依赖corda和cordapps

在build.gradle文件添加corda和自行编写的cordapps的依赖,以及对于springboot的依赖

// build.gradle in your-api module
...
dependencies {
    compile     "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
    testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
    testCompile "junit:junit:$junit_version"
    testCompile "io.rest-assured:rest-assured:$rest_assured_version"
    testCompile "$corda_release_group:corda-node-driver:$corda_release_version"

    // corda dependency
    cordaCompile "$corda_release_group:corda-core:$corda_release_version"
    cordaCompile "$corda_release_group:corda-rpc:$corda_release_version"
    cordaRuntime "$corda_release_group:corda:$corda_release_version"

    // springboot dependency
    compile("org.springframework.boot:spring-boot-starter-websocket:$spring_boot_version") {
        exclude group: "org.springframework.boot", module: "spring-boot-starter-logging"
    }

    cordapp project(":your-cordapps")
}

除了上述核心的依赖之外,为了进行集成测试,特别加入了RestAssured的依赖,用于Restful风格的API测试。

2. 编写spring组件Connection RPC

Corda Webserver模块也是通过RPC的方式和Corda节点进行交互的,所以需要使用springboot的@Bean封装对于Corda RPC的Connection,然后通过依赖注入的方式启动springboot容器,进而编写API。如下:

// App.kt
@SpringBootApplication
open class App {
    @Value("\${config.rpc.host:localhost:10006}")
    private
    lateinit var cordaHost: String

    @Value("\${config.rpc.username:user1}")
    private
    lateinit var cordaUser: String

    @Value("\${config.rpc.password:test}")
    private
    lateinit var cordaPassword: String

    @Bean
    open fun rpcClient(): CordaRPCOps {
        log.info("Connecting to Corda on $cordaHost using username $cordaUser and password $cordaPassword")
        var maxRetries = 100
        do {
            try {
                return CordaRPCClient(NetworkHostAndPort.parse(cordaHost)).start(cordaUser, cordaPassword).proxy
            } catch (ex: ActiveMQNotConnectedException) {
                if (maxRetries-- > 0) {
                    Thread.sleep(1000)
                } else {
                    throw ex
                }
            }
        } while (true)
    }

    companion object {
        private val log = LoggerFactory.getLogger(this::class.java)
        @JvmStatic
        fun main(args: Array<String>) {
            SpringApplication.run(App::class.java, *args)
        }
    }
}

基于Kotlin和springboot,然后配置了一个连接Corda结对的rpc client CordaRPCOps Bean对象。一旦springboot启动完成,CordaRPCOps将作为一个实例化好的对象注入到其它的组件当中。这里,它将被注入到Controller对象中,使用方式如下:

// GoodController.kt
@RestController
@RequestMapping("/api/")
open class GoodController {
    @Autowired
    lateinit var rpcOps: CordaRPCOps
    ...
    
     val stateAndRef = rpcOps.vaultQueryByCriteria(
                criteria = QueryCriteria.LinearStateQueryCriteria(externalId = listOf(id)),
                contractStateType = Good::class.java).states.singleOrNull()
    ...                
}

3. Gradle中添加 Run Server Task

组件定义好之后,需要注入相应的参数,整个springboot容器才能启动成功,所以在your-api module的build.gradle中配置如下任务:

// build.gradle  in your-api module
task runPartyA(type: JavaExec) {
    classpath = sourceSets.main.runtimeClasspath
    main = 'com.good.App'
    environment "server.port", "10007"
    environment "config.rpc.username", "user1"
    environment "config.rpc.password", "test"
    environment "config.rpc.host", "localhost:10006"
    environment "spring.profiles.active", "dev"
}

当corda的节点启动之后,运行./gradlew runPartyA就可以启动springboot,一旦通过rpc连接成功,整个springboot的web server就算启动成功了。这时,你可以通过postman等工具访问。

4. Integration test

虽然springboot容器可以通过gradle启动运行,但是如何通过API测试的方式来保证API的准确和稳定呢?

如果按照以前使用springboot开发web应用的方式,集成测试是非常好写的,只需要加上@SpringBootTest等注解即可。但是Corda当中,这样的方式并不可行,因为本质上Corda节点和springboot应用是两个独立的项目,而且springboot能否运行是依赖于提前启动的Corda节点的。所以使用@SpringBootTest启动整个应用,并没有办法控制底层的Corda节点。

Corda测试包下的Node Driver给了一种测试方式,但是却无法支撑springboot的测试,所以需要增加辅助测试代码,以支持这种方式的测试。如下:

// src/test/kotlin/spring/SpringDriver.kt
fun <A> springDriver(
        defaultParameters: DriverParameters = DriverParameters(),
        dsl: SpringBootDriverDSL.() -> A
): A {
    return genericDriver(
            defaultParameters = defaultParameters,
            driverDslWrapper = { driverDSL: DriverDSLImpl -> SpringBootDriverDSL(driverDSL) },
            coerce = { it }, dsl = dsl
    )
}

@Suppress("DEPRECATION")
data class SpringBootDriverDSL(private val driverDSL: DriverDSLImpl) : InternalDriverDSL by driverDSL {
    companion object {
        private val log = contextLogger()
    }
    fun startSpringBootWebapp(clazz: Class<*>, handle: NodeHandle, checkUrl: String): CordaFuture<WebserverHandle> {
        val debugPort = if (driverDSL.isDebug) driverDSL.debugPortAllocation.nextPort() else null
        val process = startApplication(handle, debugPort, clazz)
        driverDSL.shutdownManager.registerProcessShutdown(process)
        val webReadyFuture = addressMustBeBoundFuture(driverDSL.executorService, (handle as NodeHandleInternal).webAddress, process)

        return webReadyFuture.map { queryWebserver(handle, process, checkUrl) }
    }

    private fun queryWebserver(handle: NodeHandle, process: Process, checkUrl: String): WebserverHandle {
        val protocol = if ((handle as NodeHandleInternal).useHTTPS) "https://" else "http://"
        val url = URL(URL("$protocol${handle.webAddress}"), checkUrl)
        val client = OkHttpClient.Builder().connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build()

        var maxRetries = 30

        while (process.isAlive && maxRetries > 0) try {
            val response = client.newCall(Request.Builder().url(url).build()).execute()
            response.use {
                if (response.isSuccessful) {
                    return WebserverHandle(handle.webAddress, process)
                }
            }

            TimeUnit.SECONDS.sleep(2)
            maxRetries--
        } catch (e: ConnectException) {
            log.debug("Retrying webserver info at ${handle.webAddress}")
        }

        throw IllegalStateException("Webserver at ${handle.webAddress} has died or was not reachable at URL $url")
    }

    private fun startApplication(handle: NodeHandle, debugPort: Int?, clazz: Class<*>): Process {
        val className = clazz.canonicalName

        return ProcessUtilities.startJavaProcessImpl(
                className = className, // cannot directly get class for this, so just use string
                jdwpPort = debugPort,
                extraJvmArguments = listOf(
                        "-Dname=node-${handle.p2pAddress}-webserver",
                        "-Djava.io.tmpdir=${System.getProperty("java.io.tmpdir")}"
                ),
                classpath = ProcessUtilities.defaultClassPath,
                workingDirectory = handle.baseDirectory,
                arguments = listOf(
                        "--base-directory", handle.baseDirectory.toString(),
                        "--server.port=${(handle as NodeHandleInternal).webAddress.port}",
                        "--config.rpc.host=${handle.rpcAddress}",
                        "--config.rpc.username=${handle.rpcUsers.first().username}",
                        "--config.rpc.password=${handle.rpcUsers.first().password}",
                        "--spring.profiles.active=mock"
                ),
                maximumHeapSize = "200m",
                errorLogPath = Paths.get("error.$className.log"))
    }
}

重写了一个SpringDriver类,然后通过这个辅助类,就可以按照Corda原来的Driver方式运行集成测试了。测试逻辑很简单,就是先通过springDriver提前启动节点,然后启动springboot应用,连接上节点暴露出的地址和端口,然后就可以测试API了。

// IntegrationTest.kt
class IntegrationTest {
    companion object {
        private val log = contextLogger()
    }

    val walmart = TestIdentity(CordaX500Name("walmart", "", "CN"))
    
    @Test
    fun `api test`() {
        springDriver(DriverParameters(isDebug = true, startNodesInProcess = true, extraCordappPackagesToScan = listOf("com.walmart.contracts"))) {
            val nodeHandles = listOf(startNode(providedName = walmart.name)).map { it.getOrThrow() }
            log.info("All nodes started")

            nodeHandles.forEach { node ->

                val handler = startSpringBootWebapp(App::class.java, node, "/api/walmart/status")

                val address = handler.getOrThrow().listenAddress
                log.info("webserver started on $address")

                given()
                        .port(address.port)
                        .body("""{ "code": "00001111", "issuer": "Walmart"}""")
                        .with()
                        .contentType(ContentType.JSON)
                        .`when`()
                        .post("/api/goods")
                        .then()
                        .statusCode(201)
            }
        }
    }

完毕。

-- 于 2018-05-06

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏爱撒谎的男孩

Spring MVC处理异常

注意:使用SimpleMappingExceptionResolver处理异常时,不可以使用@ExceptionHandler!

3575
来自专栏菩提树下的杨过

Spring 4.0.2 学习笔记(2) - 自动注入及properties文件的使用

接上一篇继续, 学习了基本的注入使用后,可能有人会跟我一样觉得有点不爽,Programmer的每个Field,至少要有一个setter,这样spring配置文件...

2176
来自专栏XAI

SpringMVC+MongoDB+Maven整合(微信回调Oauth授权)

个人小程序。里面是基于百度大脑 腾讯优图做的人脸检测。是关于人工智能的哦。 2017年第一篇自己在工作中的总结文档。土豪可以打赏哦。 https://git.o...

8577
来自专栏LEo的网络日志

python i18n实现

3817
来自专栏互联网技术杂谈

flume 1.8.0 开发之RPC

flume开发基础可见:https://cloud.tencent.com/developer/article/1195082

4295
来自专栏技术墨客

Spring核心——数据校验

在Java数据校验详解中详细介绍了Java数据校验相关的功能(简称Bean Validation,涵盖JSR-303、JSR-349、JSR-380),本文将在...

1051
来自专栏Spring相关

第3章—高级装配—配置profile bean

我们正常开发的过程中经常遇到的问题是,开发环境是一套环境,qa测试是一套环境,线上部署又是一套环境。这样从开发到测试再到部署,会对程序中的配置修改多次,尤其是从...

1012
来自专栏haifeiWu与他朋友们的专栏

阿里 RPC 框架 DUBBO 初体验

最近研究了一下阿里开源的分布式RPC框架dubbo,楼主写了一个 demo,体验了一下dubbo的功能。

3552
来自专栏Netkiller

PHP 高级编程之多线程

PHP 高级编程之多线程 http://netkiller.github.io/journal/php.thread.html ---- 目录 1. 多线程环境...

5225
来自专栏Java架构师学习

Spring5都有那些新特性与增强,需要了解的Java程序员来看一看

Spring FrameWork 5.0新的功能 JDK 8+和Java EE7+以上版本 整个框架的代码基于java8 通过使用泛型等特性提高可读性 对j...

3927

扫码关注云+社区

领取腾讯云代金券