Skip to main content

UI&协议层编码

恭喜,你终于来到了最上面一层,也就是UI&协议层,对大多数现在的项目来说,这一层很可能就是REST API。

myddd-vertx对REST API的远比Spring Boot来的优雅与简单,并且更好理解。

1. bootstrap类#

在你使用myddd starter生成项目后,myddd已经把你准备好了很多工作,那整个项目的启动类就是MyBootstrapVerticle

class MyBootstrapVerticle (port:Int = 8080) : BootstrapVerticle(port = port){    override fun abstractModules(vertx: Vertx): AbstractModule {        return GuiceModule(vertx)    }
    override fun routers(vertx: Vertx, router: Router): () -> Unit {        return {            DocumentRoute(vertx,router)        }    }}

如果没有特殊的需求,你只需要往这个类中,不断添加类似DocumentRoute就好了。

Route在vert.x的概念有点类似于Spring Boot中的controller

2. GuiceModule类#

所有的接口与实现,请在这个类中进行申明。

class GuiceModule(vertx: Vertx):AbstractWebModule(vertx = vertx) {
    override fun configure() {        super.configure()
        bind(DocumentRepository::class.java).to(DocumentRepositoryHibernate::class.java)        bind(DocumentApplication::class.java).to(DocumentApplicationImpl::class.java)    }}

这个类里面看起来东西很少,实质是因为在AbstractWebModule,myddd-vertx已经帮你把一些常见的IOC,如数据库,ID主键生成等都定义好了。

3. 编写你自己的Route#

我们生成的代码中,包含了一个DocumentRoute示例,它足以向你说明,Route是怎么样实现的


class DocumentRoute(vertx: Vertx, router: Router): AbstractRouter(vertx = vertx,router = router)  {
    init {        createDocumentRoute()        queryDocumentRoute()    }
    private val documentApplication by lazy { InstanceFactory.getInstance(DocumentApplication::class.java) }
    private fun createDocumentRoute(){
        createPostRoute(path = "/$version/documents"){ route ->
            route.handler(DocumentValidationHandler().createDocumentValidationHandler())
            route.handler {                GlobalScope.launch(vertx.dispatcher()) {                    try {                        val body = it.bodyAsJson                        val documentDTO = body.mapTo(DocumentDTO::class.java)                        val created = documentApplication.createDocument(documentDTO).await()                        it.jsonFormatEnd(JsonObject.mapFrom(created).toBuffer())                    }catch (t:Throwable){                        it.fail(t)                    }                }            }

        }    }
    private fun queryDocumentRoute(){
        createGetRoute(path = "/$version/documents/:id"){ route ->            route.handler {                GlobalScope.launch(vertx.dispatcher()) {                    try {
                        val id = it.pathParam("id").toLong()                        val queryDocument = documentApplication.queryDocumentById(id).await()                        if(Objects.isNull(queryDocument))throw BusinessLogicException(WebErrorCode.MEDIA_NOT_FOUND)
                        it.jsonFormatEnd(JsonObject.mapFrom(queryDocument!!).toBuffer())                    }catch (t:Throwable){                        it.fail(t)                    }                }            }        }
    }}

你所需要做的,就是根据自己的用例需求,不断的添加自己的Route就可以了。

一切都是如此简单。

4. ValidationHandler#

myddd-vertx在REST API这一层,还支持ValidationHandler机制,通常前端会传递一大堆参数,有些必填,有些又要求是数字 ,对它们的验证非常繁索及复杂。

好在,基于vert.x的json schema可以很优雅与简单的处理这个事情。

class DocumentValidationHandler: AbstractValidationHandler() {
    private val createDocumentSchema by lazy {        Schemas.objectSchema()            .requiredProperty("mediaId",Schemas.stringSchema())            .requiredProperty("name",Schemas.stringSchema())            .requiredProperty("documentType",Schemas.enumSchema("File","Dir"))            .requiredProperty("md5",Schemas.stringSchema())            .requiredProperty("suffix",Schemas.stringSchema())
    }
    fun createDocumentValidationHandler() : ValidationHandler {        return ValidationHandler            .builder(schemaParser)            .body(Bodies.json(createDocumentSchema))            .build()    }
}

通过定义上面的这个ValidationHandler,可以一次性对调用API传入的数据做一次性的验证。

 //只要在route中的handler添加这个ValidationHandler就可以了 route.handler(DocumentValidationHandler().createDocumentValidationHandler())

是不是足够简单方便.

这种语法怎么编写,请参阅vert.x官方网站的文档 Vert.x Json Schema

5. 单元测试#

就算是在这最上面的一层,myddd-vertx也不是通常把整个服务运行起来这种方式去测试的,我们仍然是通常TDD的方式来测试自己的逻辑

UI&协议层的单元测试示例:

class DocumentRouteTest: AbstractRouteTest() {

    @Test    fun testCreateDocumentRoute(vertx: Vertx,testContext: VertxTestContext){        GlobalScope.launch(vertx.dispatcher()) {            try {
                val requestJson = json {                    obj(                        "mediaId" to randomString(),                        "name" to randomString(),                        "documentType" to "File",                        "md5" to randomString(),                        "suffix" to randomString()                    )                }
                val response = webClient.post(port,host,"/v1/documents")                    .sendJson(requestJson)                    .await()                testContext.verify {                    Assertions.assertEquals(200,response.statusCode())                }            }catch (t:Throwable){                testContext.failNow(t)            }            testContext.completeNow()        }    }
    @Test    fun testQueryDocumentRoute(vertx: Vertx,testContext: VertxTestContext){        GlobalScope.launch(vertx.dispatcher()) {            try {
                val errorResponse = webClient.get(port,host,"/v1/documents/-1")                    .send()                    .await()                testContext.verify {                    Assertions.assertEquals(400,errorResponse.statusCode())                }
                val requestJson = json {                    obj(                        "mediaId" to randomString(),                        "name" to randomString(),                        "documentType" to "File",                        "md5" to randomString(),                        "suffix" to randomString()                    )                }
                val response = webClient.post(port,host,"/v1/documents")                    .sendJson(requestJson)                    .await()                val body = response.bodyAsJsonObject()                val id = body.getString("id")
                val queryResponse = webClient.get(port,host,"/v1/documents/$id")                    .send()                    .await()
                testContext.verify {                    Assertions.assertEquals(200,queryResponse.statusCode())                }            }catch (t:Throwable){                testContext.failNow(t)            }            testContext.completeNow()        }    }}

在这一层,都不需要把整个项目运行起来,去通常什么POST MAN或浏览器来确认自己的API是不是对的。

不需要!!!

6. config.properties#

UI&协议层中,有一个非常重要的配置,就是config.properties


port=8080
javax.persistence.jdbc.url=jdbc:mysql://127.0.0.1:3306/testjavax.persistence.jdbc.user=rootjavax.persistence.jdbc.password=admin1234hibernate.connection.pool_size=50javax.persistence.schema-generation.database.action=createhibernate.show_sql=falsehibernate.format_sql=falsehibernate.highlight_sql=false

你可以很容易的看到,这个配置文件是做一些诸如数据库运行端口配置的。

这个与其它层有所区别,其它层的数据库配置都是直接在persistence.xml中定义的。因为那些层大多是在单元测试时需要它。

而在这一层,persistence.xml就极其简单

<persistence xmlns="http://java.sun.com/xml/ns/persistence"             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"             version="2.0">
    <persistence-unit name="default">        <provider>org.hibernate.reactive.provider.ReactivePersistenceProvider</provider>        <class>com.myddd.vertx.starter.domain.Document</class>    </persistence-unit>
</persistence>

这样是为了真正在生产时运行它,能将这个config.properties放入到JAR的外面。

7. 这就是所有#

是的,到这一层后,这就是全部了。

是不是足够简单?