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/test
javax.persistence.jdbc.user=root
javax.persistence.jdbc.password=admin1234
hibernate.connection.pool_size=50
javax.persistence.schema-generation.database.action=create
hibernate.show_sql=false
hibernate.format_sql=false
hibernate.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. 这就是所有

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

是不是足够简单?