Spring工程師打開Vertx的正確方式

文章已先發布在我的個人站點:yeyeck.com, 歡迎直接訪問
這兩天看了一下Java的異步框架Vert.X, 入門大家去官網看就好了。我在GitHub上也翻了一些例子,都是一些非常簡單堆代碼的形式,幾乎都寫在啓動類裏。這對一個無腦使用 Controller, Service, Dao 分層的人來說簡直不能忍。所以就寫了一個Demo來以Spring工程師的習慣組織一個Vert.X CRUD的代碼。

1 項目文件結構

  • 因爲Vert.X裏用router來管理路由,所以把controller包用router代替。這樣就很Spring了。
    依賴, 很多包暫時用上,方便後續增加功能
<?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.yeyeck</groupId>
  <artifactId>vertx</artifactId>
  <version>1.0.0-SNAPSHOT</version>

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

    <maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
    <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>4.0.0-milestone5</vertx.version>
    <junit-jupiter.version>5.4.0</junit-jupiter.version>

    <main.verticle>com.yeyeck.vertx.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-auth-shiro</artifactId>
    </dependency>
    <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-mail-client</artifactId>
    </dependency>
    <dependency>
      <groupId>io.vertx</groupId>
      <artifactId>vertx-redis-client</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.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.12</version>
      <optional>true</optional>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>${maven-compiler-plugin.version}</version>
        <configuration>
          <release>11</release>
        </configuration>
      </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>sonatype-oss-snapshots</id>
      <name>Sonatype OSSRH Snapshots</name>
      <url>https://oss.sonatype.org/content/repositories/snapshots</url>
      <layout>default</layout>
      <releases>
        <enabled>false</enabled>
        <updatePolicy>never</updatePolicy>
      </releases>
      <snapshots>
        <enabled>true</enabled>
        <updatePolicy>never</updatePolicy>
      </snapshots>
    </repository>
  </repositories>

</project>

2 代碼

2.1 主類

import com.yeyeck.vertx.router.ArticleRouter;
import com.yeyeck.vertx.router.UserRouter;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Promise;
import io.vertx.core.Vertx;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.handler.BodyHandler;

public class MainVerticle extends AbstractVerticle {

  public static void main(String[] args) {
    Vertx vertx = Vertx.vertx();
    vertx.deployVerticle(new MainVerticle());
  }

  @Override
  public void start(Promise<Void> startPromise) throws Exception {
    // 創建一個 router
    Router router = Router.router(vertx);
    router.route().handler(BodyHandler.create());
    // 創建一個http server 並將所有請求交給 router 來管理
    vertx.createHttpServer().requestHandler(router).listen(8888, http -> {
      if (http.succeeded()) {
        startPromise.complete();
        System.out.println("HTTP server started on port 8888");
      } else {
        startPromise.fail(http.cause());
      }
    });
    // 在router上掛載url 
    new ArticleRouter().init(router);
    new UserRouter().init(router);

  }
}

2.2 Router 層

2.2.1 BasicRouter ———— 所有Router類的基類

先定義了一個抽象類,聲明一個抽象方法, 所有的 router 都要繼承這個類, 然後實現init(router)這個方法

import io.vertx.ext.web.Router;

public abstract class BasicRouter {
  public abstract void init(Router router);
}

2.2.2 ArticleRouter

BasicRouter 的具體實現,實現了簡單的增刪改查接口,處理回調的代碼很單一又不好統一處理,這是最頭疼的地方,整個項目都是這樣。還在想辦法改進。

import com.yeyeck.vertx.router.fo.ArticleFo;
import com.yeyeck.vertx.service.IArticleService;
import com.yeyeck.vertx.service.impl.ArticleServiceImpl;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;

public class ArticleRouter extends BasicRouter{

  private final IArticleService articleService = new ArticleServiceImpl();

  @Override
  public void init(Router router) {
    router.post("/article").handler(this::post);
    router.get("/article/:id").handler(this::get);
    router.put("/article/:id").handler(this::update);
    router.delete("/article/:id").handler(this::deleteArticle);
    router.get("/article/transaction/").handler(this::transaction);
  }

  private void post(RoutingContext routingContext) {
    JsonObject jsonObject = routingContext.getBodyAsJson();
    ArticleFo articleFo = new ArticleFo(jsonObject);
    articleService.addArticle(articleFo).onSuccess(res -> {
      routingContext.response().setStatusCode(200).end(String.valueOf(res));
    }).onFailure(throwable -> {
      routingContext.response().setStatusCode(500).end(throwable.toString());
    });
  }
  private void get(RoutingContext routingContext) {
    Integer id = Integer.parseInt(routingContext.request().getParam("id"));
    articleService.getById(id).onSuccess(article -> {
      routingContext.response().setStatusCode(200).end(article.toJson().toString());
    }).onFailure(throwable -> {
      routingContext.response().setStatusCode(500).end(throwable.toString());
    });
  }

  private void update(RoutingContext routingContext) {
    Integer id = Integer.parseInt(routingContext.request().getParam("id"));
    JsonObject jsonObject = routingContext.getBodyAsJson();
    articleService.update(id, new ArticleFo(jsonObject))
      .onSuccess(res -> {
        routingContext.response().setStatusCode(200).end(String.valueOf(res));
      }).onFailure(throwable -> {
      routingContext.response().setStatusCode(500).end(throwable.toString());
    });;
  }
 private void deleteArticle(RoutingContext routingContext) {
   Integer id = Integer.parseInt(routingContext.request().getParam("id"));
   articleService.deleteById(id)
     .onSuccess(res -> {
       routingContext.response().setStatusCode(200).end(String.valueOf(res));
     }).onFailure(throwable -> {
     routingContext.response().setStatusCode(500).end(throwable.toString());
   });;
 }

 private void transaction(RoutingContext routingContext) {
    articleService.testTransaction().onSuccess(integer -> {
      routingContext.response().setStatusCode(200).end(String.valueOf(integer));
    }).onFailure(throwable -> {
      routingContext.response().setStatusCode(500).end(throwable.toString());
    });
 }
}

2.3 Service 層

接口聲明

import com.yeyeck.vertx.pojo.Article;
import com.yeyeck.vertx.router.fo.ArticleFo;
import io.vertx.core.Future;

public interface IArticleService {
  Future<Integer> addArticle(ArticleFo articleFo);
  Future<Article> getById(Integer id);
  Future<Integer> update(Integer id, ArticleFo articleFo);
  Future<Integer> deleteById(Integer id);
  Future<Integer> testTransaction();
}

接口實現,這裏只貼兩個方法的代碼, 一個需要實現事務,一個不需要

  @Override
  public Future<Integer> deleteById(Integer id) {
    Promise<Integer> promise = Promise.promise();
    SqlUtil.pool().getConnection().onSuccess(connection -> {
      articleDao.deleteById(connection, id)
        .onSuccess(res -> {
          // 正確執行sql, 釋放connection
          connection.close();
          promise.complete(res);

        })
        .onFailure(throwable -> {
          // 執行sql發生錯誤, 釋放connection
          connection.close();
          promise.fail(throwable);

        });
      }).onFailure(promise::fail);    // 未拿到 connection
    return promise.future();
  }

 // Transaction Demo
  @Override
  public Future<Integer> testTransaction() {
    Promise<Integer> promise = Promise.promise();
    Article article = new Article();
    article.setTitle("transaction");
    article.setAbstractText("transaction");
    article.setContent("transaction");
    article.setId(33);
    SqlUtil.getConnection().onSuccess(connection -> {
      // 開始一個transaction
      connection.begin(ar -> {
        if (ar.succeeded()) {
          // transaction 開啓
          Transaction ts = ar.result();
          // 調用 dao 的方法執行SQL, 封裝 dao 的方法都傳入 connection 的就是爲了實現事務
          articleDao.add(connection, article)
            .onSuccess(integer -> articleDao.add(connection, article))
            .onSuccess(integer -> articleDao.add(connection, article))
            .onSuccess(integer -> articleDao.update(connection, article))
            .onSuccess(integer -> {
              // 都執行成功了才走到這裏, 提交事務
              ts.commit(tsar -> {
                if (tsar.succeeded()) {
                  promise.complete(1);
                  connection.close();
                } else {
                  promise.fail(tsar.cause());
                }
                connection.close();
              });
            }).onFailure(throwable -> {
              // 事務提交失敗
              promise.fail(throwable);
              connection.close();
            });
        } else {
          // transaction 失敗,關閉連接
          promise.fail(ar.cause());
          connection.close();
        }
      });
    });
    return promise.future();
  } 

2.4 Dao 層

一個管理SqlConnection的鏈接池

import io.vertx.core.Vertx;
import io.vertx.mysqlclient.MySQLConnectOptions;
import io.vertx.mysqlclient.MySQLPool;
import io.vertx.sqlclient.PoolOptions;


public class SqlUtil {
  private static final MySQLPool pool;
  static {
    MySQLConnectOptions connectOptions = new MySQLConnectOptions()
      .setHost("127.0.0.1")
      .setUser("root")
      .setPassword("password")
      .setPort(3306)
      .setDatabase("vertx");
    PoolOptions poolOptions = new PoolOptions().setMaxSize(5);
    pool = MySQLPool.pool(Vertx.vertx(), connectOptions, poolOptions);
  }
  private SqlUtil(){}

  public static MySQLPool pool() {
    return pool;
  }

  public static Future<SqlConnection> getConnection() {
    Promise<SqlConnection> promise = Promise.promise();
    pool.getConnection(ar -> {
      if (ar.succeeded()) {
        promise.complete(ar.result());
      } else {
        ar.cause().printStackTrace();
        promise.fail(ar.cause());
      }
    });
    return promise.future();
  }
}

準備數據

CREATE DATABASE /*!32312 IF NOT EXISTS*/`vertx` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin */;

USE `vertx`;

/*Table structure for table `t_article` */

DROP TABLE IF EXISTS `t_article`;

CREATE TABLE `t_article` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(100) COLLATE utf8mb4_bin NOT NULL,
  `abstract_text` varchar(200) COLLATE utf8mb4_bin NOT NULL,
  `content` mediumtext COLLATE utf8mb4_bin NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=29 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

/*Data for the table `t_article` */

insert  into `t_article`(`id`,`title`,`abstract_text`,`content`) values (1,'title 2','abstract 1','content123 1'),(2,'transaction','transaction','transaction'),(3,'transaction','transaction','transaction');

聲明接口

import com.yeyeck.vertx.pojo.Article;
import io.vertx.core.Future;
import io.vertx.sqlclient.SqlConnection;

public interface IArticleDao {
  Future<Integer> add(SqlConnection connection, Article article);
  Future<Article> getById(SqlConnection connection, Integer id);
  Future<Integer> update(SqlConnection connection, Article article);
  Future<Integer> deleteById(SqlConnection connection, Integer id);
}

實現 CRUD

import com.yeyeck.vertx.dao.IArticleDao;
import com.yeyeck.vertx.pojo.Article;
import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.mysqlclient.MySQLClient;
import io.vertx.sqlclient.Row;
import io.vertx.sqlclient.RowSet;
import io.vertx.sqlclient.SqlConnection;
import io.vertx.sqlclient.Tuple;

public class ArticleDaoImpl implements IArticleDao {

  @Override
  public Future<Integer> add(SqlConnection connection, Article article) {
    Promise<Integer> promise = Promise.promise();
    String sql = "insert into t_article(title, abstract_text, content) values (?, ?, ?)";
    Tuple params = Tuple.of(article.getTitle(), article.getAbstractText(), article.getContent());
    connection.preparedQuery(sql)
      .execute(params, ar ->{
        if(ar.succeeded()) {
          RowSet<Row> rows = ar.result();
          Long lastInsertId = rows.property(MySQLClient.LAST_INSERTED_ID);
          promise.complete(lastInsertId.intValue());
        } else {
          promise.fail(ar.cause());
        }
      });
    return promise.future();
  }

  @Override
  public Future<Article> getById(SqlConnection connection, Integer id) {
    Promise<Article> promise = Promise.promise();
    String sql = "select id, title, abstract_text, content from t_article where id = ?";
    Tuple params = Tuple.of(id);
    connection.preparedQuery(sql)
      .execute(params, ar ->{
        if(ar.succeeded()) {
          RowSet<Row> rows = ar.result();
          Row row = rows.iterator().next();
          Article article = new Article(row);
          promise.complete(article);
        } else {
          promise.fail(ar.cause());
        }
      });
    return promise.future();
  }

  @Override
  public Future<Integer> update(SqlConnection connection, Article article) {
    Promise<Integer> promise = Promise.promise();
    String sql = "update t_article set title = ?, abstract_text = ?, content = ? where id = ?";
    Tuple params = Tuple.of(article.getTitle(), article.getAbstractText(), article.getContent(), article.getId());
    connection.preparedQuery(sql)
      .execute(params, ar ->{
        if(ar.succeeded()) {
          promise.complete(1);
        } else {
          promise.fail(ar.cause());
        }
      });
    return promise.future();
  }

  @Override
  public Future<Integer> deleteById(SqlConnection connection, Integer id) {
    Promise<Integer> promise = Promise.promise();
    String sql = "delete from t_article where id = ?";
    Tuple params = Tuple.of(id);
    connection.preparedQuery(sql)
      .execute(params, ar ->{
        if(ar.succeeded()) {
          promise.complete(1);
        } else {
          promise.fail(ar.cause());
        }
      });
    return promise.future();
  }
}

2.5 Pojo

特殊的地方就是手動實現 數據庫返回結果Row和JsonObject的轉換了。Vert.X似乎希望我們自己去做這些事情,絲毫沒有考慮利用反射。

import io.vertx.core.json.JsonObject;
import io.vertx.sqlclient.Row;
import lombok.Data;

@Data
public class Article {
  private Integer id;
  private String title;
  private String abstractText;
  private String content;

  public Article(){}

  public Article(Row row) {
    this.id = row.getInteger("id");
    this.title = row.getString("title");
    this.content = row.getString("content");
    this.abstractText = row.getString("abstract_text");
  }

  public JsonObject toJson() {
    return new JsonObject().put("id", this.id)
      .put("title", this.title)
      .put("abstractText", this.abstractText)
      .put("content", content);
  }
}

3 測試代碼

首先運行主類 MainVerticle

目前數據庫裏的數據
image.png
然後使用postman進行測試
Get localhost:8888/article/1
image.png
Post localhost:8888/article
image.png
測試事務
Get localhost:8888/article/transaction/
image.png
把 update方法的sql語句改成錯的, 比如,再重啓測試transaction接口

String sql = "update xxxt_article set title = ?, abstract_text = ?, content = ? where id = ?";

image.png
數據庫也沒有新增成功.證明事務沒有commit

4 總結

由於Vert.X是異步編程,所以利用Future和Promise來處理回調是關鍵。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章