Spring工程師開啟Vertx的正確方式
阿新 • • 發佈:2020-10-23
文章已先發布在我的個人站點:yeyeck.com, 歡迎直接訪問
這兩天看了一下Java的非同步框架Vert.X, 入門大家去官網看就好了。我在GitHub上也翻了一些例子,都是一些非常簡單堆程式碼的形式,幾乎都寫在啟動類裡。這對一個無腦使用 Controller, Service, Dao 分層的人來說簡直不能忍。所以就寫了一個Demo來以Spring工程師的習慣組織一個Vert.X CRUD的程式碼。
- 程式碼點這裡 GitHub
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
目前資料庫裡的資料
然後使用postman進行測試
Get localhost:8888/article/1
Post localhost:8888/article
測試事務
Get localhost:8888/article/transaction/
把 update方法的sql語句改成錯的, 比如,再重啟測試transaction介面
String sql = "update xxxt_article set title = ?, abstract_text = ?, content = ? where id = ?";
資料庫也沒有新增成功.證明事務沒有commit
4 總結
由於Vert.X是非同步程式設計,所以利用Future和Promise來處理回撥是關鍵。