首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >R3 Corda 和 springboot 集成

R3 Corda 和 springboot 集成

作者头像
lambeta
发布2018-08-17 11:29:30
1.3K0
发布2018-08-17 11:29:30
举报
文章被收录于专栏:编舟记编舟记

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018.05.06 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 为什么Corda要集成springboot
  • 太长不读篇
  • 精读篇
    • 1. 独立的module依赖corda和cordapps
      • 2. 编写spring组件Connection RPC
        • 3. Gradle中添加 Run Server Task
          • 4. Integration test
          相关产品与服务
          容器服务
          腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档