Vert.x Java開發(fā)指南——第四章 重構為Vert.x服務

感興趣的朋友潭千,可以關注微信服務號“猿學堂社區(qū)”刨晴,或加入“猿學堂社區(qū)”微信交流群

版權聲明:本文由作者自行翻譯路翻,未經(jīng)作者授權,不得隨意轉(zhuǎn)發(fā)茂契。

與我們最早的實現(xiàn)相比,前面的重構已經(jīng)是向前一大步真竖,因為我們提取出了獨立且可配置的Verticle厌小,并且在事件總線之上使用異步消息進行鏈接。我們還看到璧亚,我們可以同時部署一個指定Verticle的幾個實例,以便更好地處理負載以及更好地利用CPU內(nèi)核拐袜。

在這一節(jié),我們將看到如何設計和使用Vert.x服務蹬铺。服務的主要優(yōu)勢是甜攀,它定義了一個接口用于執(zhí)行Verticle公開的特定操作琐馆。對于所有事件總線消息工作,我們還可以利用代碼生成瘦麸,而不是像前一節(jié)那樣自己創(chuàng)建它。

step-3/src/main/java/
└── io
    └── vertx
        └── guides
            └── wiki
                ├── MainVerticle.java
                ├── database
                │   ├── ErrorCodes.java
                │   ├── SqlQuery.java
                │   ├── WikiDatabaseService.java
                │   ├── WikiDatabaseServiceImpl.java
                │   ├── WikiDatabaseVerticle.java
                │   └── package-info.java
                └── http
                    └── HttpServerVerticle.java

io.vertx.guides.wiki現(xiàn)在包含主Verticle厉碟,io.vertx.guides.wiki.database包含數(shù)據(jù)庫Verticle和服務屠缭,io.vertx.guides.wiki.http包含HTTP Server Verticle。

4.1 Maven配置變更

首先呵曹,我們需要添加下面兩個依賴到我們的項目。很明顯铐殃,我們需要vertx-service-proxy的API:

<dependency>
    <groupId>io.vertx</groupId>
    <artifactId>vertx-service-proxy</artifactId>
</dependency>

我們需要Vert.x代碼生成模塊作為一個編譯時依賴(所以是provided范圍):

<dependency>
    <groupId>io.vertx</groupId>
    <artifactId>vertx-codegen</artifactId>
    <scope>provided</scope>
</dependency>

接下來跨新,我們必須稍微調(diào)整一下maven-compiler-plugin的配置來使用代碼生成,它通過一個javac注解處理器完成:

<plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.5.1</version>
    <configuration>
        <source>1.8</source>
        <target>1.8</target>
        <useIncrementalCompilation>false</useIncrementalCompilation>
        <annotationProcessors>
            <annotationProcessor>io.vertx.codegen.CodeGenProcessor</annotationProcessor>
        </annotationProcessors>
        <generatedSourcesDirectory>${project.basedir}/src/main/generated</generatedSourcesDirectory>
        <compilerArgs>
            <arg>-AoutputDirectory=${project.basedir}/src/main</arg>
        </compilerArgs>
    </configuration>
</plugin>

注意蟹肘,生成代碼放置在src/main/generated目錄下俯树,一些集成開發(fā)環(huán)境諸如IntelliJ IDEA將自動識別為類路徑。

更新maven-clean-plugin插件移除這些生成文件也是一個好注意:

<plugin>
    <artifactId>maven-clean-plugin</artifactId>
    <version>3.0.0</version>
    <configuration>
        <filesets>
            <fileset>
                <directory>${project.basedir}/src/main/generated</directory>
            </fileset>
        </filesets>
    </configuration>
</plugin>

關于Vert.x Service的完整文檔位于http://vertx.io/docs/vertxservice-
proxy/java/
阳欲。

4.2 數(shù)據(jù)庫服務接口

定義一個服務接口與定義一個Java接口一樣簡單,除此之外球化,有一些規(guī)則需要遵守,以使代碼生成可以工作筒愚,并且還要確保與Vert.x中的其它代碼的互操作性。

接口的開始定義如下:

@ProxyGen
public interface WikiDatabaseService {
    @Fluent
    WikiDatabaseService fetchAllPages(Handler<AsyncResult<JsonArray>> resultHandler);
    @Fluent
    WikiDatabaseService fetchPage(String name, Handler<AsyncResult<JsonObject>> resultHandler);
    @Fluent
    WikiDatabaseService createPage(String title, String markdown, Handler<AsyncResult<Void>> resultHandler);
    @Fluent
    WikiDatabaseService savePage(int id, String markdown, Handler<AsyncResult<Void>> resultHandler);
    @Fluent
    WikiDatabaseService deletePage(int id, Handler<AsyncResult<Void>> resultHandler);
    // (...)

1句伶、ProxyGen注解用于觸發(fā)該服務的客戶端代理代碼生成陆淀。

2、Fluent注解是可選的轧苫,但是允許fluent接口,操作可以通過返回服務實例被鏈式調(diào)用(chained)身冬。這對于代碼生成器非常有用,當服務將被其它JVM語言消費時吏恭。

3重罪、參數(shù)類型需要是字符串、Java原始數(shù)據(jù)類型剿配、JSON對象或者數(shù)組、任何枚舉類型或者前面類型的java.util集合(List/Set/Map)呼胚。支持任意Java類的唯一方法是使用@DataObject注解,使它們作為Vert.x數(shù)據(jù)對象沪编。傳遞其它類型的最后機會是服務引用類型年扩。

4、由于服務提供異步結(jié)果厨幻,一個服務的最后參數(shù)需要是Handler<AsyncResult<T>>腿时,T是上面描述的適合于代碼生成的任何類型饭宾。

服務接口提供一個靜態(tài)方法用于創(chuàng)建實際服務實現(xiàn)以及事件總線上客戶端代碼使用的代理,這是一種好的習慣徽鼎。

我們聲明了create方法,簡單的委托給實現(xiàn)類及其構造函數(shù):

static WikiDatabaseService create(JDBCClient dbClient, HashMap<SqlQuery, String> sqlQueries, Handler<AsyncResult
<WikiDatabaseService>> readyHandler) {
    return new WikiDatabaseServiceImpl(dbClient, sqlQueries, readyHandler);
}

Vert.x代碼生成器創(chuàng)建代理類纬傲,并且類名以VertxEBProxy作為后綴肤频。這些代理類的構造方法需要一個Vert.x上下文的引用以及事件總線的目的地址作為參數(shù):

static WikiDatabaseService createProxy(Vertx vertx, String address) {
    return new WikiDatabaseServiceVertxEBProxy(vertx, address);
}

在上次迭代中作為內(nèi)部類的SqlQuery和ErrorCodes枚舉類型宵荒,本次迭代已被提取為包保護(package-protected)類型,具體查看SqlQuery.java和ErrorCodes.java报咳。

4.3 Database服務實現(xiàn)

服務實現(xiàn)是先前WikiDatabaseVerticle類代碼的直截了當?shù)囊浦病V饕獏^(qū)別在于暑刃,服務實現(xiàn)在構造函數(shù)(報告初始化結(jié)果)和服務方法(報告操作成功)中支持異步處理結(jié)果處理(Handler)膜眠。

類代碼如下:

class WikiDatabaseServiceImpl implements WikiDatabaseService {
    
    private static final Logger LOGGER = LoggerFactory
            .getLogger(WikiDatabaseServiceImpl.class);
    
    private final HashMap<SqlQuery, String> sqlQueries;
    
    private final JDBCClient dbClient;

    WikiDatabaseServiceImpl(JDBCClient dbClient,
            HashMap<SqlQuery, String> sqlQueries,
            Handler<AsyncResult<WikiDatabaseService>> readyHandler) {
        this.dbClient = dbClient;
        this.sqlQueries = sqlQueries;
        dbClient.getConnection(ar -> {
            if (ar.failed()) {
                LOGGER.error("Could not open a database connection", ar.cause());
                readyHandler.handle(Future.failedFuture(ar.cause()));
            } else {
                SQLConnection connection = ar.result();
                connection.execute(sqlQueries.get(SqlQuery.CREATE_PAGES_TABLE),
                        create -> {
                            connection.close();
                            if (create.failed()) {
                                LOGGER.error("Database preparation error",
                                        create.cause());
                                readyHandler.handle(Future.failedFuture(create
                                        .cause()));
                            } else {
                                readyHandler.handle(Future
                                        .succeededFuture(this));
                            }
                        });
            }
        });

    }

    @Override
    public WikiDatabaseService fetchAllPages(
            Handler<AsyncResult<JsonArray>> resultHandler) {
        dbClient.query(sqlQueries.get(SqlQuery.ALL_PAGES), res -> {
            if (res.succeeded()) {
                JsonArray pages = new JsonArray(res.result().getResults()
                        .stream().map(json -> json.getString(0)).sorted()
                        .collect(Collectors.toList()));
                resultHandler.handle(Future.succeededFuture(pages));
            } else {
                LOGGER.error("Database query error", res.cause());
                resultHandler.handle(Future.failedFuture(res.cause()));
            }
        });
        return this;
    }

    @Override
    public WikiDatabaseService fetchPage(String name,
            Handler<AsyncResult<JsonObject>> resultHandler) {
        dbClient.queryWithParams(
                sqlQueries.get(SqlQuery.GET_PAGE),
                new JsonArray().add(name),
                fetch -> {
                    if (fetch.succeeded()) {
                        JsonObject response = new JsonObject();
                        ResultSet resultSet = fetch.result();
                        if (resultSet.getNumRows() == 0) {
                            response.put("found", false);
                        } else {
                            response.put("found", true);
                            JsonArray row = resultSet.getResults().get(0);
                            response.put("id", row.getInteger(0));
                            response.put("rawContent", row.getString(1));
                        }
                        resultHandler.handle(Future.succeededFuture(response));
                    } else {
                        LOGGER.error("Database query error", fetch.cause());
                        resultHandler.handle(Future.failedFuture(fetch.cause()));
                    }
                });
        return this;
    }

    @Override
    public WikiDatabaseService createPage(String title, String markdown,
            Handler<AsyncResult<Void>> resultHandler) {
        JsonArray data = new JsonArray().add(title).add(markdown);
        dbClient.updateWithParams(sqlQueries.get(SqlQuery.CREATE_PAGE), data,
                res -> {
                    if (res.succeeded()) {
                        resultHandler.handle(Future.succeededFuture());
                    } else {
                        LOGGER.error("Database query error", res.cause());
                        resultHandler.handle(Future.failedFuture(res.cause()));
                    }
                });
        return this;
    }

    @Override
    public WikiDatabaseService savePage(int id, String markdown,
            Handler<AsyncResult<Void>> resultHandler) {
        JsonArray data = new JsonArray().add(markdown).add(id);
        dbClient.updateWithParams(sqlQueries.get(SqlQuery.SAVE_PAGE), data,
                res -> {
                    if (res.succeeded()) {
                        resultHandler.handle(Future.succeededFuture());
                    } else {
                        LOGGER.error("Database query error", res.cause());
                        resultHandler.handle(Future.failedFuture(res.cause()));

                    }
                });
        return this;
    }

    @Override
    public WikiDatabaseService deletePage(int id,
            Handler<AsyncResult<Void>> resultHandler) {
        JsonArray data = new JsonArray().add(id);
        dbClient.updateWithParams(sqlQueries.get(SqlQuery.DELETE_PAGE), data,
                res -> {
                    if (res.succeeded()) {
                        resultHandler.handle(Future.succeededFuture());
                    } else {
                        LOGGER.error("Database query error", res.cause());
                        resultHandler.handle(Future.failedFuture(res.cause()));
                    }
                });
        return this;
    }
}

在代理代碼生成可以工作之前架谎,還需要最后一步:服務包需要有一個package-info.java注解來聲明一個Vert.x模塊:

@ModuleGen(groupPackage = "io.vertx.guides.wiki.database", name = "wiki-database")
package io.vertx.guides.wiki.database;
import io.vertx.codegen.annotations.ModuleGen;

4.4 從數(shù)據(jù)庫Verticle公開數(shù)據(jù)庫服務

由于大多數(shù)數(shù)據(jù)庫處理代碼已經(jīng)被移動到WikiDatabaseServiceImpl類谷扣,WikiDatabaseVerticle類現(xiàn)在包含兩個方法:start方法用來注冊服務,以及一個工具方法加載SQL查詢:

public class WikiDatabaseVerticle extends AbstractVerticle {

    public static final String CONFIG_WIKIDB_JDBC_URL = "wikidb.jdbc.url";
    public static final String CONFIG_WIKIDB_JDBC_DRIVER_CLASS = "wikidb.jdbc.driver_class";
    public static final String CONFIG_WIKIDB_JDBC_MAX_POOL_SIZE = "wikidb.jdbc.max_pool_size";
    public static final String CONFIG_WIKIDB_SQL_QUERIES_RESOURCE_FILE = "wikidb.sqlqueries.resource.file";
    public static final String CONFIG_WIKIDB_QUEUE = "wikidb.queue";

    @Override
    public void start(Future<Void> startFuture) throws Exception {
        HashMap<SqlQuery, String> sqlQueries = loadSqlQueries();
        JDBCClient dbClient = JDBCClient.createShared(
                vertx,
                new JsonObject()
                        .put("url",config().getString(CONFIG_WIKIDB_JDBC_URL,"jdbc:hsqldb:file:db/wiki"))
                        .put("driver_class",config().getString(CONFIG_WIKIDB_JDBC_DRIVER_CLASS,"org.hsqldb.jdbcDriver"))
                        .put("max_pool_size",config().getInteger(CONFIG_WIKIDB_JDBC_MAX_POOL_SIZE, 30)));
        WikiDatabaseService.create(dbClient, sqlQueries, ready -> {
            if (ready.succeeded()) {
                ProxyHelper.registerService(WikiDatabaseService.class, vertx,
                        ready.result(), CONFIG_WIKIDB_QUEUE); ①
                startFuture.complete();
            } else {
                startFuture.fail(ready.cause());
            }
        });
    }

    /*
     * Note: this uses blocking APIs, but data is small...
     */
    private HashMap<SqlQuery, String> loadSqlQueries() throws IOException {
        String queriesFile = config().getString(
                CONFIG_WIKIDB_SQL_QUERIES_RESOURCE_FILE);
        InputStream queriesInputStream;
        if (queriesFile != null) {
            queriesInputStream = new FileInputStream(queriesFile);
        } else {
            queriesInputStream = getClass().getResourceAsStream(
                    "/db-queries.properties");
        }
        Properties queriesProps = new Properties();
        queriesProps.load(queriesInputStream);
        queriesInputStream.close();
        HashMap<SqlQuery, String> sqlQueries = new HashMap<>();
        sqlQueries.put(SqlQuery.CREATE_PAGES_TABLE,queriesProps.getProperty("create-pages-table"));
        sqlQueries.put(SqlQuery.ALL_PAGES,queriesProps.getProperty("all-pages"));
        sqlQueries.put(SqlQuery.GET_PAGE, queriesProps.getProperty("get-page"));
        sqlQueries.put(SqlQuery.CREATE_PAGE,queriesProps.getProperty("create-page"));
        sqlQueries.put(SqlQuery.SAVE_PAGE,queriesProps.getProperty("save-page"));
        sqlQueries.put(SqlQuery.DELETE_PAGE,queriesProps.getProperty("delete-page"));
        return sqlQueries;
    }
}

① 我們在此處注冊服務。

注冊服務需要一個接口類瑞凑,一個Vert.x上下文,一個實現(xiàn)類以及一個事件總線目的地址拨黔。

WikiDatabaseServiceVertxEBProxy生成類處理事件總線上接收到的消息并分發(fā)它們到WikiDatabaseServiceImpl绰沥。它做的事情實際上非常接近于我們在前面章節(jié)所做的:發(fā)送消息使用一個action頭指定哪個方法被調(diào)用贺待,并且參數(shù)被編碼為JSON。

4.5 獲得一個數(shù)據(jù)庫服務代理

重構為Vert.x服務的最后步驟是改寫HTTP Server Verticle來獲得數(shù)據(jù)庫服務的代理麸塞,并且在處理器(handler)中使用它代替事件總線:

首先,我們需要在啟動Verticle時哪工,創(chuàng)建代理:

private WikiDatabaseService dbService;
@Override
public void start(Future<Void> startFuture) throws Exception {
    String wikiDbQueue = config().getString(CONFIG_WIKIDB_QUEUE, "wikidb.queue"); ①
    dbService = WikiDatabaseService.createProxy(vertx, wikiDbQueue);
    HttpServer server = vertx.createHttpServer();
// (...)

① 我們僅僅需要確保使用的事件總線目的地址與WikiDatabaseVerticle發(fā)布的服務相同。

然后雁比,我們需要用數(shù)據(jù)庫服務的調(diào)用替換事件總線的調(diào)用:

private void indexHandler(RoutingContext context) {
    dbService.fetchAllPages(reply -> {
        if (reply.succeeded()) {
            context.put("title", "Wiki home");
            context.put("pages", reply.result().getList());
            templateEngine.render(
                    context,
                    "templates",
                    "/index.ftl",
                    ar -> {
                        if (ar.succeeded()) {
                            context.response().putHeader("Content-Type",
                                    "text/html");
                            context.response().end(ar.result());
                        } else {
                            context.fail(ar.cause());
                        }
                    });
        } else {
            context.fail(reply.cause());
        }
    });
}

private void pageRenderingHandler(RoutingContext context) {
    String requestedPage = context.request().getParam("page");
    dbService.fetchPage(
            requestedPage,
            reply -> {
                if (reply.succeeded()) {
                    JsonObject payLoad = reply.result();
                    boolean found = payLoad.getBoolean("found");
                    String rawContent = payLoad.getString("rawContent",
                            EMPTY_PAGE_MARKDOWN);
                    context.put("title", requestedPage);
                    context.put("id", payLoad.getInteger("id", -1));
                    context.put("newPage", found ? "no" : "yes");
                    context.put("rawContent", rawContent);
                    context.put("content", Processor.process(rawContent));
                    context.put("timestamp", new Date().toString());
                    templateEngine.render(
                            context,
                            "templates",
                            "/page.ftl",
                            ar -> {
                                if (ar.succeeded()) {
                                    context.response().putHeader(
                                            "Content-Type", "text/html");
                                    context.response().end(ar.result());
                                } else {
                                    context.fail(ar.cause());
                                }
                            });

                } else {
                    context.fail(reply.cause());
                }
            });
}

private void pageUpdateHandler(RoutingContext context) {
    String title = context.request().getParam("title");
    Handler<AsyncResult<Void>> handler = reply -> {
        if (reply.succeeded()) {
            context.response().setStatusCode(303);
            context.response().putHeader("Location", "/wiki/" + title);
            context.response().end();
        } else {
            context.fail(reply.cause());
        }
    };

    String markdown = context.request().getParam("markdown");
    if ("yes".equals(context.request().getParam("newPage"))) {
        dbService.createPage(title, markdown, handler);
    } else {
        dbService.savePage(
                Integer.valueOf(context.request().getParam("id")),
                markdown, handler);
    }
}

private void pageCreateHandler(RoutingContext context) {
    String pageName = context.request().getParam("name");
    String location = "/wiki/" + pageName;
    if (pageName == null || pageName.isEmpty()) {
        location = "/";
    }
    context.response().setStatusCode(303);
    context.response().putHeader("Location", location);
    context.response().end();
}

private void pageDeletionHandler(RoutingContext context) {
    dbService.deletePage(Integer.valueOf(context.request().getParam("id")),
            reply -> {
                if (reply.succeeded()) {
                    context.response().setStatusCode(303);
                    context.response().putHeader("Location", "/");
                    context.response().end();
                } else {
                    context.fail(reply.cause());
                }
            });
}

生成的WikiDatabaseServiceVertxProxyHandler類處理轉(zhuǎn)發(fā)調(diào)用為事件總線消息蠢终。

仍然完全可以直接通過事件總線消息使用Vert.x服務茴她,因為這正是生成的代理做的事情。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末丈牢,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子己沛,更是在濱河造成了極大的恐慌,老刑警劉巖泛粹,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異扒接,居然都是意外死亡,警方通過查閱死者的電腦和手機钾怔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門蒙挑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人忆蚀,你說我怎么就攤上這事姑裂。” “怎么了舶斧?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵察皇,是天一觀的道長。 經(jīng)常有香客問我什荣,道長,這世上最難降的妖魔是什么稻爬? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮桅锄,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘竞滓。我一直安慰自己吹缔,他們只是感情好,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布茶没。 她就那樣靜靜地躺著,像睡著了一般抓半。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上笛求,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天,我揣著相機與錄音探入,去河邊找鬼。 笑死蜂嗽,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的植旧。 我是一名探鬼主播辱揭,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼问窃,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了泡躯?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤较剃,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后写穴,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡啊送,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年欣孤,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片降传。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖婆排,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情段只,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布赞枕,位于F島的核電站,受9級特大地震影響炕婶,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜古话,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望陪踩。 院中可真熱鬧悉抵,春花似錦摘完、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽谈飒。三九已至,卻和暖如春杭措,著一層夾襖步出監(jiān)牢的瞬間费什,已是汗流浹背鸳址。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留稿黍,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓闻察,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子呢灶,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容