美文网首页R3 Corda
R3 Corda 和 springboot 集成

R3 Corda 和 springboot 集成

作者: lambeta | 来源:发表于2018-05-06 17:51 被阅读164次
    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

    相关文章

      网友评论

        本文标题:R3 Corda 和 springboot 集成

        本文链接:https://www.haomeiwen.com/subject/vmburftx.html