vertx, kotlin, postgrest reactive client demo

build.gradle

plugins {
  id 'java'
  id 'java-library'
  id 'application'
  id 'org.jetbrains.kotlin.jvm' version '1.3.40'
  id 'com.github.johnrengelman.shadow' version '5.2.0'
  id 'maven-publish'
//  id("io.vertx.vertx-plugin") version "0.8.0" apply false
}

group 'citi.rio'
version '1.0-SNAPSHOT'

sourceCompatibility = 1.8
def mainClass = 'citi.ken.Starter'

dependencies {w
  // kotlin
  implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kt_version"
  implementation "org.jetbrains.kotlin:kotlin-reflect:$kt_version"
  implementation "com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_version"

  implementation "io.vertx:vertx-core:$vertx_version"
  implementation "io.vertx:vertx-web:$vertx_version"
  implementation "io.vertx:vertx-pg-client:$vertx_version"
//    implementation "io.vertx:vertx-redis-client:$vertx_version"
//    implementation "io.vertx:vertx-jdbc-client:$vertx_version"

  // kotlin vertx
  implementation "io.vertx:vertx-lang-kotlin:$vertx_version"
  implementation "io.vertx:vertx-lang-kotlin-coroutines:$vertx_version"

  // log
  implementation "org.slf4j:jul-to-slf4j:$slf4j_version"
  runtime "ch.qos.logback:logback-classic:$logback_version"

  // java -cp .\h2-1.4.199.jar org.h2.tools.Server -tcp -tcpAllowOthers -web -webAllowOthers -browser -ifNotExists
  testImplementation("com.h2database:h2:1.4.199")
  testImplementation "org.junit.jupiter:junit-jupiter:5.4.2"
  testImplementation "io.vertx:vertx-junit5:$vertx_version"
  testImplementation "io.vertx:vertx-web-client:$vertx_version"
  testImplementation("org.testcontainers:postgresql:1.11.3")

  testImplementation "junit:junit:4.12"
}

repositories {
  mavenLocal()
  maven {
    url = uri("https://www.artifactrepository.citigroup.net:443/artifactory/maven-icg-dev")
    credentials {
      username = "ocean-devops"
      password = "APBQW78wqY4oewwXFBtTfcN17ZG"
    }
  }
  mavenCentral()
  jcenter()
}

sourceSets {
  main {
    java {
      srcDirs += 'src/main/kotlin'
      outputDir = file("$buildDir/classes/java/main")
    }
    kotlin {
      outputDir = file("$buildDir/classes/kotin/main")
    }
  }
}

compileKotlin {
  kotlinOptions.jvmTarget = "1.8"
  dependencies {
    compile files("$sourceSets.main.kotlin.outputDir")
  }
}
compileTestKotlin {
  kotlinOptions.jvmTarget = "1.8"
}

def mainVerticleName = 'citi.ken.verticle.MainVerticle'
def watchForChange = 'src/**/*'
def doOnChange = 'gradle classes'

application {
  mainClassName = mainClass
}

run {
  args = ['run', mainVerticleName, "--redeploy=$watchForChange", "--launcher-class=$mainClassName",
          "--on-redeploy=$doOnChange", '--conf=config/config_local.json']
}

task sourceJar(type: Jar) {
  classifier 'sources'
  from sourceSets.main.allJava
}

publishing {
  publications {
    maven(MavenPublication) {
      artifact tasks.sourceJar
      groupId = group
      artifactId = "$project.name"
      from components.java
    }
  }
}

task testRunner() {
  doLast {
    def libs = configurations.runtimeClasspath.findAll {
      it.name.contains('rio_registryservice') || it.name.contains('ocean-common')
    }
    configurations.runtimeClasspath.collect { println it.name }
  }
}

// tasks demo
//task startUmb1(type: Exec, group: 'gemfire', dependsOn: [installGemfire, build]) {
//  workingDir projectDir
//  environment 'GEMFIRE_HOME', installDir
//  environment 'PATH', gemfirePath
////    environment 'JAVA_HOME', 'C:/Users/hw83770/Java/jdk1.8.0_161'
//  if (System.getProperty('os.name').toLowerCase(Locale.ROOT).contains('windows')) {
//    commandLine cmd, "run --file=${projectDir}/scripts/start_umb1.gfsh"
//  } else {
//    commandLine 'sh', '-c', "gfsh run --file=${projectDir}/scripts/start_umb1.gfsh"
//  }
//}
//
//task cleanYumeServer(group: 'gemfire') {
//  doLast {
//    delete 'yume_locator'
//    delete 'yume_server1'
//    delete 'yume_server2'
//  }
//}

gradle.properties

version=1.0-SNAPSHOT
### remote
#gemfireRepositoryUrl = https://globaldeploymentservicesforunixsystems.citigroup.net/cgi-bin/down.cgi?lookup=pivotal-gemfire-9.8.4.tgz&location=Linux&dl=yes
### local
gemfireRepositoryUrl=C:/Users/hw83770/Desktop/gemfire-9.8.4/pivotal-gemfire-9.8.4.tgz
gemfireReleaseUrl=
# dependency versions
#assertjVersion = 3.6.2
#awaitilityVersion = 1.7.0
#junitVersion = 4.12
#mockitocoreVersion = 2.19.1
#log4jVersion = 2.11.0
#systemrulesVersion = 1.16.1
#lombokVersion = 1.18.8
#guavaVersion = 25.1-jre

kt_version=1.3.40
vertx_version=3.8.2
jackson_version=2.9.10
slf4j_version=1.7.25
logback_version=1.2.3
jackson_version=2.9.10

config_local.json

{
  "http.port": 9991,
  "pgClient": {
    "database": "meta_int",
    "host": "oceanap02d.nam.nsroot.net",
    "user": "admin",
    "password": "password",
    "port": 5432,
    "max_pool_size": 30
  }
}

log-back.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <Pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</Pattern>
        </encoder>
    </appender>

    <logger name="citi.ken" level="info"/>

    <root level="info">
        <appender-ref ref="STDOUT"/>
    </root>

</configuration>

constants.kt

package citi.ken.config

/**
 * @Author hw83770
 * @Date 17:04 2019/10/30
 *
 */

const val item = "account"

const val API_GET = "/$item/:Id"
const val API_LIST_ALL = "/$item"
const val API_CREATE = "/$item"
const val API_UPDATE = "/$item/:Id"
const val API_DELETE = "/$item/:Id"
const val API_DELETE_ALL = "/$item"

package citi.ken.domain

import java.time.LocalDateTime

/**
 * @Author hw83770
 * @Date 17:18 2019/10/30
 *
 */
data class Account(val id: Int) {
  var username: String = ""
  var nickname: String = ""
  var avatar: String = ""
  var createTime: String = LocalDateTime.now().toString()
  var updateTime: String = LocalDateTime.now().toString()

  constructor(
    id: Int,
    username: String,
    nickname: String,
    avatar: String,
    createTime: String,
    updateTime: String
  ) : this(id) {
    this.username = username
    this.nickname = nickname
    this.avatar = avatar
    this.createTime = createTime
    this.updateTime = updateTime
  }
}

package citi.ken.domain

import citi.ken.orm.Table
import citi.ken.orm.column
import io.vertx.sqlclient.Row

/**
 * @Author hw83770
 * @Date 10:08 2019/10/31
 *
 */

object Accounts : Table<Account>(tblName = "accounts") {

  val id = column("id", "int")
  val username = column("username", "String")
  val nickname = column("nickname", "String")
  val avatar = column("avatar", "String")
  val created_at = column("created_at", "timestamp")
  val updated_at = column("updated_at", "timestamp")

  fun mapToEntity(from: Row): Account = run {
    return Account(
      id = from.getInteger(id.col),
      username = from.getString(username.col),
      nickname = from.getString(nickname.col).orEmpty(),
      avatar = from.getString(avatar.col).orEmpty(),
      createTime = from.getLocalDateTime(created_at.col).toString(),
      updateTime = from.getLocalDateTime(updated_at.col).toString()
    )
  }

}


package citi.ken.orm

/**
 * @Author hw83770
 * @Date 18:21 2019/10/30
 *
 */
data class Column(
  val col: String,
  val type: String
)

---

package citi.ken.orm

/**
 * @Author hw83770
 * @Date 10:42 2019/10/31
 *
 */
class Expression {
}


package citi.ken.orm

import java.util.LinkedHashMap
import java.util.concurrent.atomic.AtomicInteger
import kotlin.reflect.KClass
import kotlin.text.StringBuilder

/**
 * @Author hw83770
 * @Date 18:11 2019/10/30
 *
 */
abstract class Table<E : Any>(tblName: String, entityClass: KClass<E>? = null) {
  private val _refCounter = AtomicInteger()
  private val _columns = LinkedHashMap<String, Column>()
  private var _primaryKeyName: String? = null

  val tblName: String = tblName

//  val entityClass: KClass<E>? =
//    (entityClass ?: referencedKotlinType.jvmErasure as KClass<E>).takeIf { it != Nothing::class }

//  protected abstract fun mapToEntity(): E

  fun registerColumn(colums: Column): Unit {
    if (colums.col in _columns.keys) {
      throw IllegalArgumentException("Duplicate column name: ${colums.col}")
    }
    _columns[colums.col] = colums
//    return ColumnRegistration(name)
  }

  fun select(vararg colums: Column): Query {
    val cols = colums.asList()
    return Query(cols = cols, from = this.tblName)
  }

  fun insert(cols: List<Column>, values: List<String>, source: String): String {
    val expression = StringBuilder("insert into $source (")
    val str = cols.map { it.col }
    expression.append(str.joinToString())
    expression.append(") values (")
    expression.append(values.joinToString())
    expression.append(")")
    return expression.toString()
  }

  fun insertPrepare(cols: List<Column>, source: String): String {
    val expression = StringBuilder("insert into $source (")
    val str = cols.map { it.col }
    // generate $1 $2 ...
    val seqs = generateSequence(1) { it + 1 }.map { "$$it" }
    val values = seqs.take(str.size).toList()

    expression.append(str.joinToString())
    expression.append(") values (")
    expression.append(values.joinToString())
    expression.append(")")
    return expression.toString()
  }

}

data class Query(
  val cols: List<Column>,
  val from: String
) {
  fun where(block: () -> Boolean) {
    block()
  }
}


fun <E : Any> Table<E>.column(name: String, type: String): Column {
  val col = Column(name, type)
  this.registerColumn(colums = col)
  return col
}

package citi.ken.service

import citi.ken.domain.Account
import citi.ken.domain.Accounts
import io.vertx.core.Vertx
import io.vertx.pgclient.PgPool
import org.slf4j.LoggerFactory
import io.vertx.kotlin.pgclient.pgConnectOptionsOf
import io.vertx.kotlin.pgclient.preparedQueryAwait
import io.vertx.kotlin.pgclient.queryAwait
import io.vertx.kotlin.sqlclient.poolOptionsOf
import io.vertx.sqlclient.Row
import io.vertx.sqlclient.Tuple
import java.time.LocalDateTime


/**
 * @Author hw83770
 * @Date 17:14 2019/10/30
 *
 */
class PgclientService(val vertx: Vertx) {

  val logger = LoggerFactory.getLogger(PgclientService::class.java)
  private val pgClient by lazy(LazyThreadSafetyMode.PUBLICATION) { createPgClient() }


  suspend fun getAll(): List<Account>? = runSafely<List<Account>> {
    val queryAwait = pgClient.queryAwait("select * from ${Accounts.tblName}")
    return queryAwait.toList().map { Accounts.mapToEntity(it) }
  }

  suspend fun getCertain(accountId: String): Account? = runSafely<Account> {
    val queryAwait = pgClient.queryAwait("select * from ${Accounts.tblName} where ${Accounts.id.col} = $accountId")
    val res: Row = queryAwait.iterator().next()
    return Accounts.mapToEntity(res)
  }

  suspend fun insert(account: Account): Account? {
    val sql = Accounts.insertPrepare(
      cols = listOf(Accounts.username, Accounts.nickname, Accounts.created_at),
//      values = listOf(account.username, account.nickname, LocalDateTime.now()),
      source = Accounts.tblName
    )
    logger.info("sql: $sql")
    return runSafely {
      val queryAwait = pgClient
        .preparedQueryAwait(sql, Tuple.of(account.username, account.nickname, LocalDateTime.now()))
      if (queryAwait.rowCount() > 0) account else null
    }
  }

  private fun createPgClient(): PgPool {
    val config = Vertx.currentContext().config().getJsonObject("pgClient")

    val connectOptions = pgConnectOptionsOf(
      database = config.getString("database"),
      host = config.getString("host"),
      user = config.getString("user"),
      password = config.getString("password"),
      port = config.getInteger("port")
    ).addProperty("search_path", "test")

    val poolOptions = poolOptionsOf(maxSize = config.getInteger("max_pool_size"))
    return PgPool.pool(vertx, connectOptions, poolOptions)
  }

  inline fun <T> runSafely(block: () -> T?): T? {
    return try {
      block()
    } catch (e: Throwable) {
      this.logger.error(e.message)
      null
    }
  }

}

package citi.ken.utils

import io.vertx.core.http.HttpHeaders
import io.vertx.core.json.Json
import io.vertx.ext.web.RoutingContext
import io.vertx.kotlin.coroutines.awaitResult
import io.vertx.pgclient.PgPool
import io.vertx.pgclient.impl.RowImpl
import io.vertx.sqlclient.Row
import io.vertx.sqlclient.RowSet
import io.vertx.sqlclient.SqlResult
import java.util.stream.Collector

/**
 * @Author hw83770
 * @Date 17:54 2019/10/30
 *
 */

fun RoutingContext.toJson(obj: Any?) {
  response()
    .putHeader(HttpHeaders.CONTENT_TYPE, "application/json; charset=utf-8")
    .end(if (obj is String) obj else Json.encode(obj))
}

package citi.ken.verticle

import citi.ken.config.API_CREATE
import citi.ken.config.API_GET
import citi.ken.config.API_LIST_ALL
import citi.ken.domain.Account
import citi.ken.service.PgclientService
import citi.ken.utils.toJson
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import io.vertx.core.Handler
import io.vertx.core.http.HttpHeaders
import io.vertx.core.http.HttpMethod
import io.vertx.ext.web.Router
import io.vertx.ext.web.RoutingContext
import io.vertx.ext.web.handler.BodyHandler
import io.vertx.ext.web.handler.CorsHandler
import io.vertx.ext.web.handler.LoggerHandler
import io.vertx.kotlin.coroutines.CoroutineVerticle
import io.vertx.kotlin.coroutines.dispatcher
import kotlinx.coroutines.launch
import org.slf4j.LoggerFactory

/**
 * @Author hw83770
 * @Date 17:46 2019/10/30
 *
 */
class AccountRestVerticleCo : CoroutineVerticle() {
  private val logger = LoggerFactory.getLogger(AccountRestVerticleCo::class.java)

  private val defaultPort = 9991
  private val router by lazy(LazyThreadSafetyMode.NONE) { createRouter() }
  val mapper = jacksonObjectMapper()

  private lateinit var pgclientService: PgclientService


  private val cors: CorsHandler = CorsHandler.create("*")
    .allowedHeaders(
      setOf(
        HttpHeaders.CONTENT_TYPE.toString(),
        HttpHeaders.ORIGIN.toString(),
        HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN.toString()
      )
    )
    .allowedMethods(
      setOf(
        HttpMethod.GET,
        HttpMethod.POST,
        HttpMethod.PUT,
        HttpMethod.PATCH,
        HttpMethod.DELETE,
        HttpMethod.OPTIONS
      )
    )

  override suspend fun start() {
    val port = config.getInteger("http.port", defaultPort)
    pgclientService = PgclientService(vertx)
    vertx.createHttpServer().requestHandler(router).listen(port)
  }

  private fun createRouter(): Router = Router.router(vertx).apply {
    // cors handle
    route().handler(cors)
    route().handler(BodyHandler.create())
    route().handler(LoggerHandler.create())

    get("/configs").handler { it.toJson(config.toString()) }
    get(API_GET).handler(getCertain)
    get(API_LIST_ALL).handler(getAll)
    post(API_CREATE).handler(createOne)

    //todo:
//      post(API_UPDATE).handler(updateTodo)
//      delete(API_DELETE).handler(deleteTodo)
//      delete(API_DELETE_ALL).handler(deleteAll)

  }


  private val getAll = Handler<RoutingContext> { ctx ->
    doAsync(ctx) { pgclientService.getAll() }
  }

  private val getCertain = Handler<RoutingContext> { ctx ->
    val id = ctx.request().getParam("Id")
    doAsync(ctx) { pgclientService.getCertain(id) }
  }

  private val createOne = Handler<RoutingContext> { ctx ->
    val data: ByteArray = ctx.body.bytes
    val acc: Account = mapper.readValue(data)
    doAsync(ctx) { pgclientService.insert(acc) }
  }


  fun <T> doAsync(ctx: RoutingContext, block: suspend () -> T) {
    launch(vertx.dispatcher()) {
      logger.info("doAsync1: ${Thread.currentThread().name}")
      val t = block.invoke()
//      Thread.sleep(1000)
      logger.info("doAsync2: ${Thread.currentThread().name}")
      ctx.toJson(t ?: "no data")
    }
    logger.info("doAsync3: ${Thread.currentThread().name}")
  }

}


package citi.ken.verticle

/**
 * @Author hw83770
 * @Date 16:48 2019/10/30
 * MainVerticle to deploy other component verticles
 */
import io.vertx.core.AbstractVerticle
import io.vertx.core.DeploymentOptions
import org.slf4j.LoggerFactory

class MainVerticle : AbstractVerticle() {
  val log = LoggerFactory.getLogger(MainVerticle::class.java)

  override fun start() {
    log.info("MainVerticle started")
    vertx.deployVerticle(AccountRestVerticleCo(), DeploymentOptions().setConfig(config()))
  }
}

package citi.ken

import io.vertx.core.Launcher
import io.vertx.core.VertxOptions
import org.slf4j.bridge.SLF4JBridgeHandler

/**
 * @Author hw83770
 * @Date 16:50 2019/10/30
 *
 */
object Starter {

  /**
   *  @param program argument:
   *  deploy kotlin verticle:
   *  run citi.ken.verticle.MainVerticle -conf=config/config_local.json
   *
   *  deploy java verticle:
   *  run citi.ken.verticle.JavaMainVerticle -conf=config/config_local.json
   */
  @JvmStatic
  fun main(args: Array<String>) {
    SLF4JBridgeHandler.removeHandlersForRootLogger()
    SLF4JBridgeHandler.install()
    MyLauncher().dispatch(args)
  }
}

class MyLauncher : Launcher() {

  override fun beforeStartingVertx(options: VertxOptions?) {
    options?.apply {
      eventLoopPoolSize = 4
      workerPoolSize = 4
    }
  }

}

rest_test.http


### hello
GET http://localhost:9991/configs HTTP/1.1

### get all
GET http://localhost:9991/account HTTP/1.1

### get one
GET http://localhost:9991/account/1 HTTP/1.1 

###
POST http://localhost:9991/account HTTP/1.1
content-type: application/json

{
    "username": "sample1",
    "nickname": "test_nick",
    "avatar": "test/url_avatar",
    "createTime":  "2015-11-11 18:27:50"
}


###
https://localhost:8080/geode/v1/publisher?limit=50


###
https://localhost:8080/geode/v1/publisher?limit=50
accept: application/json;charset=UTF-8
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Authorization: Basic aHc4Mzc3MDoxMjM=
Connection: keep-alive
Host: localhost:8080
Referer: https://localhost:8080/geode/swagger-ui.html
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36


###
https://localhost:8080/geode/v1/publisher/publisher%3Akey%3A0
accept: application/json;charset=UTF-8
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Authorization: Basic aHc4Mzc3MDoxMjM=
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260