Vertx Kotlin 协程最新体验 3.9.2

1,876 阅读1分钟

1. 和 Spring Boot Kotlin 对比

没有 Spring Boot 那么省心,有些代码需要自己封装,但是也比较方便,废话少说,直接上代码。 协程很好的消除了回调地狱,比写 future 代码要简洁很多,同样的也好理解。本文用的vertx版本是 3.9.2

2. 依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.sweet</groupId>
  <artifactId>vertxkt</artifactId>
  <version>1.0.0-SNAPSHOT</version>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

    <kotlin.version>1.3.72</kotlin.version>
    <kotlin.compiler.incremental>true</kotlin.compiler.incremental>

    <maven-shade-plugin.version>2.4.3</maven-shade-plugin.version>
    <maven-surefire-plugin.version>2.22.2</maven-surefire-plugin.version>
    <exec-maven-plugin.version>1.5.0</exec-maven-plugin.version>

    <vertx.version>3.9.2</vertx.version>
    <junit-jupiter.version>5.4.0</junit-jupiter.version>

    <log.version>2.8.2</log.version>

    <main.verticle>com.sweet.vertxkt.MainVerticle</main.verticle>
  </properties>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>io.vertx</groupId>
        <artifactId>vertx-stack-depchain</artifactId>
        <version>${vertx.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <dependency>
      <groupId>io.vertx</groupId>
      <artifactId>vertx-web</artifactId>
    </dependency>
    <dependency>
      <groupId>io.vertx</groupId>
      <artifactId>vertx-mysql-client</artifactId>
    </dependency>
    <dependency>
      <groupId>io.vertx</groupId>
      <artifactId>vertx-lang-kotlin-coroutines</artifactId>
    </dependency>
    <dependency>
      <groupId>io.vertx</groupId>
      <artifactId>vertx-lang-kotlin</artifactId>
    </dependency>

    <dependency>
      <groupId>io.vertx</groupId>
      <artifactId>vertx-junit5</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-api</artifactId>
      <version>${junit-jupiter.version}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-engine</artifactId>
      <version>${junit-jupiter.version}</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-api</artifactId>
      <version>${log.version}</version>
    </dependency>

    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-core</artifactId>
      <version>${log.version}</version>
    </dependency>

    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-slf4j-impl</artifactId>
      <version>${log.version}</version>
    </dependency>

    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-jcl</artifactId>
      <version>${log.version}</version>
    </dependency>

    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-jul</artifactId>
      <version>${log.version}</version>
    </dependency>

    <dependency>
      <groupId>com.lmax</groupId>
      <artifactId>disruptor</artifactId>
      <version>3.3.6</version>
    </dependency>

  </dependencies>

  <build>
      <sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
      <testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
    <plugins>
      <plugin>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-maven-plugin</artifactId>
        <version>${kotlin.version}</version>
        <configuration>
          <jvmTarget>1.8</jvmTarget>
        </configuration>
        <executions>
          <execution>
            <id>compile</id>
            <goals>
              <goal>compile</goal>
            </goals>
          </execution>
          <execution>
            <id>test-compile</id>
            <goals>
              <goal>test-compile</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <artifactId>maven-shade-plugin</artifactId>
        <version>${maven-shade-plugin.version}</version>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>shade</goal>
            </goals>
            <configuration>
              <transformers>
                <transformer
                  implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                  <manifestEntries>
                    <Main-Class>io.vertx.core.Launcher</Main-Class>
                    <Main-Verticle>${main.verticle}</Main-Verticle>
                  </manifestEntries>
                </transformer>
                <transformer
                  implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                  <resource>META-INF/services/io.vertx.core.spi.VerticleFactory</resource>
                </transformer>
              </transformers>
              <artifactSet>
              </artifactSet>
              <outputFile>${project.build.directory}/${project.artifactId}-${project.version}-fat.jar
              </outputFile>
            </configuration>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>${maven-surefire-plugin.version}</version>
      </plugin>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>exec-maven-plugin</artifactId>
        <version>${exec-maven-plugin.version}</version>
        <configuration>
          <mainClass>io.vertx.core.Launcher</mainClass>
          <arguments>
            <argument>run</argument>
            <argument>${main.verticle}</argument>
          </arguments>
        </configuration>
      </plugin>
    </plugins>
  </build>

  <repositories>
    <repository>
      <id>alimaven</id>
      <name>aliyun maven</name>
      <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
      <releases>
        <enabled>true</enabled>
      </releases>
      <snapshots>
        <enabled>false</enabled>
      </snapshots>
    </repository>
  </repositories>
</project>

3. 编写启动类

package com.sweet.vertxkt

import io.vertx.core.Vertx
import io.vertx.kotlin.core.deployVerticleAwait
import io.vertx.kotlin.coroutines.CoroutineVerticle

fun main() {
  System.setProperty("vertx.logger-delegate-factory-class-name", "io.vertx.core.logging.Log4j2LogDelegateFactory");
  Vertx.vertx().deployVerticle(MainVerticle())
}

class MainVerticle : CoroutineVerticle() {
  override suspend fun start() {
    vertx.deployVerticle(DBService())
    vertx.deployVerticleAwait(HttpServerVerticle())
  }
}

4. 数据访问类

因为没有模型类,直接用的json一把梭

package com.sweet.vertxkt

import io.vertx.core.eventbus.EventBus
import io.vertx.core.eventbus.Message
import io.vertx.core.json.JsonArray
import io.vertx.core.json.JsonObject
import io.vertx.core.logging.LoggerFactory
import io.vertx.kotlin.coroutines.*
import io.vertx.kotlin.sqlclient.beginAwait
import io.vertx.kotlin.sqlclient.commitAwait
import io.vertx.kotlin.sqlclient.executeAwait
import io.vertx.mysqlclient.MySQLConnectOptions
import io.vertx.mysqlclient.MySQLPool
import io.vertx.sqlclient.*
import kotlinx.coroutines.launch

class DBService : CoroutineVerticle() {

  private val logger = LoggerFactory.getLogger(this.javaClass)

  override suspend fun start() {
    super.start()
    val mysqlConf = JsonObject()
      .put("port", 3306)
      .put("host", "localhost")
      .put("database", "cat1")
      .put("user", "root")
      .put("password", "123456");
    val connectOptions = MySQLConnectOptions(mysqlConf)

    val poolOptions = PoolOptions(
      JsonObject().put("maxSize", 5))

    val sqlClient = MySQLPool.pool(vertx, connectOptions, poolOptions)
    val eventBus = vertx.eventBus()
    dbCreateEvent(eventBus, sqlClient)
    dbQueryEvent(eventBus, sqlClient)
    dbAutoTx(eventBus, sqlClient)
  }

  private fun dbAutoTx(eventBus: EventBus, sqlClient: MySQLPool) {
    eventBus.awaitLocalConsumer<JsonArray>("db.autoTx") {
      logger.info("dbAutoTx for: {}", it.body())
      val array = dbAutoTxHandler(it.body(), sqlClient)
      it.reply(array)
    }
  }

  private fun dbCreateEvent(eventBus: EventBus, sqlClient: MySQLPool) {
    eventBus.awaitLocalConsumer<JsonArray>("db.create") {
      logger.info("dbCreateEvent for: {}", it.body())
      dbCreateEventHandler(it, sqlClient)
    }
  }

  private fun dbQueryEvent(eventBus: EventBus, sqlClient: MySQLPool) {
    eventBus.awaitLocalConsumer<String>("db.query") {
      dbQueryItemHandler(it, sqlClient)
    }
  }

  private suspend fun dbCreateEventHandler(message: Message<JsonArray>, sqlClient: MySQLPool) {
    val list = message.body()
    logger.info("dbCreateEventHandler: {}", list)
    var beginAwait: Transaction? = null
    try {
      beginAwait = sqlClient.beginAwait()
      for (item in list) {
        val executeAwait = beginAwait.query(item.toString()).executeAwait()
      }
      beginAwait.commitAwait()
      message.reply(message.body())
    } catch (e: Exception) {
      logger.error("dbCreateEventHandlerError: ", e)
      logger.error("catch 0000")
      message.reply(JsonArray().add("ERROR0"))
      beginAwait?.rollback()
    } finally {
      beginAwait?.close()
    }
  }

  private suspend fun dbQueryItemHandler(message: Message<String>, sqlClient: MySQLPool) {
    try {
      val rowSet = sqlClient.query(message.body()).executeAwait()
      val list = rowSet.map {
        val id = it.getLong("id")
        val name = it.getString("name")
        val age = it.getInteger("age")
        JsonObject()
          .put("id", id)
          .put("name", name)
          .put("age", age)
      }
      message.reply(JsonArray(list))
    } catch (e: Exception) {
      e.printStackTrace()
      message.reply("[0]")
    }
  }

  private suspend fun dbAutoTxHandler(array: JsonArray, sqlClient: MySQLPool): JsonArray {
    val resultArray = JsonArray()
    sqlClient.autoTx({ tx ->
      for (any in array) {
        tx.query(any.toString()).executeAwait()
      }
      resultArray.add("SUCCESS")
    }, { exception ->
      logger.error("dbAutoTxHandlerError", exception)
      resultArray.clear().add("ERROR->${exception.message?.substring(0, 10)}")
    })
    return resultArray
  }

  // 扩展方法:自动管理事务
  private suspend fun Pool.autoTx(f: suspend (Transaction) -> Unit, ex: (Exception) -> Unit) {
    var beginAwait: Transaction? = null
    try {
      beginAwait = this.beginAwait()
      logger.info("autoTx::begin")
      f(beginAwait)
      beginAwait.commitAwait()
      logger.info("autoTx::commit")
    } catch (e: Exception) {
      logger.error("autoTx::Error:: {}", e.message)
      beginAwait?.rollback()
      logger.info("autoTx::rollback")
      ex(e)
    } finally {
      beginAwait?.close()
      logger.info("autoTx::close")
    }
  }

  // 扩展方法:等待 EventBus 上的事件
  private fun <T> EventBus.awaitLocalConsumer(address: String, handler: suspend (Message<T>) -> Unit) {
    this.localConsumer<T>(address) {
      launch(vertx.dispatcher()) {
        handler(it)
      }
    }
  }
}

扩展方法真香,实现了自动提交事务,以及协程等待EventBus上的事件

5. HTTP Server 类

实现简单的 CRUD,以及事务操作

package com.sweet.vertxkt

import io.vertx.core.json.JsonArray
import io.vertx.core.logging.LoggerFactory
import io.vertx.ext.web.Route
import io.vertx.ext.web.Router
import io.vertx.ext.web.RoutingContext
import io.vertx.ext.web.handler.BodyHandler
import io.vertx.kotlin.core.eventbus.requestAwait
import io.vertx.kotlin.core.http.listenAwait
import io.vertx.kotlin.coroutines.CoroutineVerticle
import io.vertx.kotlin.coroutines.dispatcher
import kotlinx.coroutines.launch

class HttpServerVerticle : CoroutineVerticle() {

  private val logger = LoggerFactory.getLogger(this.javaClass)

  override suspend fun start() {
    val eventBus = vertx.eventBus()
    val router = Router.router(vertx)
    router.route().handler(BodyHandler.create())
      .failureHandler {
        logger.error(it.failure())
        it.response().end("ERROR")
      }

    router.route("/query")
      .coroutineHandler {
        val value = it.bodyAsJson.getValue("sql")
        logger.debug("value: {}", value)
        val resp = eventBus.requestAwait<JsonArray>("db.query", value)
        it.response().end(resp.body().encodePrettily())
      }

    router.route("/user")
      .coroutineHandler {
        val array = it.bodyAsJsonArray
        logger.debug("value: {}", array)
        val resp = eventBus.requestAwait<JsonArray>("db.create", array)
        it.response().end(resp.body().encodePrettily())
      }

    router.route("/user2")
      .coroutineHandler {
        val array = it.bodyAsJsonArray
        logger.debug("value: {}", array)
        val resp = eventBus.requestAwait<JsonArray>("db.autoTx", array)
        it.response().end(resp.body().encodePrettily())
      }

    val httpServer = vertx
      .createHttpServer()
      .requestHandler(router)
      .listenAwait(8090)

    logger.info("http server listen port: {}", httpServer.actualPort())
  }

  override suspend fun stop() {

  }

  private fun Route.coroutineHandler(fn: suspend (RoutingContext) -> Unit) {
    handler { ctx ->
      launch(ctx.vertx().dispatcher()) {
        try {
          fn(ctx)
        } catch (e: Exception) {
          ctx.fail(e)
        }
      }
    }
  }
}

6. log4j2 日志配置

resource目录下创建log4j2.xml

<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
  <Properties>
    <Property name="log-pattern">%d{yyyy-MM-dd HH:mm:ss} |- %highlight{%5p}{TRACE=blue, DEBUG=green, INFO=green, WARN=yellow, ERROR=red, FATAL=red} in %style{%C{1}:%L}{cyan} [%style{%t}{magenta}] - %m%n</Property>
  </Properties>

  <Appenders>
    <Console name="Console" target="SYSTEM_OUT">
      <PatternLayout pattern="${log-pattern}" />
    </Console>
    <RollingRandomAccessFile name="FILE" fileName="logs/app.log" append="true" filePattern="logs/lsp_app_%i.log.%d{yyyyMMdd}">
      <PatternLayout>
        <Pattern>%d %-5p [%c] %m%n</Pattern>
      </PatternLayout>
      <Policies>
        <SizeBasedTriggeringPolicy size="10MB" />
      </Policies>
    </RollingRandomAccessFile>
  </Appenders>

  <!-- Logger levels: trace, debug, info, warn, error, fatal -->
  <Loggers>        <!--name 是你需要打log的包名-->
    <AsyncLogger name="com.sweet" level="DEBUG" additivity="false" includeLocation="true">
      <AppenderRef ref="Console" />
      <AppenderRef ref="FILE" />
    </AsyncLogger>

    <AsyncLogger name="io.vertx.sqlclient" level="INFO" additivity="false" includeLocation="true">
      <AppenderRef ref="Console" />
    </AsyncLogger>

    <Root level="WARN">
      <AppenderRef ref="Console"/>
    </Root>
  </Loggers>
</Configuration>

7. 打成可执行 jar

mvn clean package

8. 运行 jar

java -Dvertx.logger-delegate-factory-class-name=io.vertx.core.logging.Log4j2LogDelegateFactory -jar xxxx.jar

这里的 -D 参数为了让我们配置的日志生效