[翻譯]現(xiàn)代java開發(fā)指南 第三部分

現(xiàn)代java開發(fā)指南 第三部分

第三部分:Web開發(fā)

第一部分哪审,第二部分

===========================

歡迎來到現(xiàn)代 Java 開發(fā)指南第三部分。在第一部分中医吊,我們嘗試著編了寫現(xiàn)代Java代碼帜篇,在之后的第二部分中俊扭,探索了JVM應(yīng)用的部署,管理妥箕,監(jiān)控和測試。現(xiàn)在更舞,是時候研究現(xiàn)代JavaWeb開發(fā)了畦幢。還是老規(guī)矩,先回答一下讀者的問題缆蝉。

第二部分中宇葱,可以看到 JVM 是如何重視監(jiān)控和怎樣暴露 JVM 運行時行為數(shù)據(jù)瘦真。有一位讀者提到一個我用過很多次但是第二部分沒有說的工具——JITWatch。它幫助我們分析 JVM 更深層次的信息黍瞧,因此這個工具只推薦給對 Java 或其它語言性能高度關(guān)心的專家使用诸尽。調(diào)用這個工具只用在 JVM 的選項中增加 -XX:+UnlockDiagnosticVMOptions -XX:+TraceClassLoading -XX:+LogCompilation -XX:+PrintAssembly,這樣就能得到 JVM 怎么優(yōu)化你的代碼和什么時候優(yōu)化你的代碼的信息雷逆。還有它還能查看哪些方法被編譯成機器碼(加上-XX:+PrintAssembly選項弦讽,甚至還能查看編繹成的機器碼),哪些方法內(nèi)聯(lián)膀哲,哪些方法不被內(nèi)聯(lián)等等很多信息往产。更多的信息,可以查看項面維基某宪。

有一些讀者對 Capsule 提出意見仿村,認為 Capsule 沒有按 JVM 的打包標(biāo)準(zhǔn)。這不完全對兴喂,因為 Capsule 是一個無狀態(tài)可執(zhí)行不用安裝的程序蔼囊,因此本身來說,它就不用跟 JVM 的打包標(biāo)準(zhǔn)完全一致衣迷。如果你的應(yīng)用畏鼓,要求有一些狀態(tài)(如在安裝時,需要一個用戶向?qū)В┖耍珻apsule 并不合適你云矫。另外一部分讀者表示對 Capsule 運行時依賴 Maven 的可用性表示不放心。這于這點我要說汗菜,很顯然让禀,對于軟件在可用性/關(guān)鍵性任務(wù)的范圍有每個人都有不同的觀點,而不同的應(yīng)用在使用安全性和使用便捷性也應(yīng)該有不同的權(quán)衡陨界。你可以創(chuàng)建一個不支持自動升級的 Capsule巡揍,或者一個包括所有依賴的 Capsule。你還可以在啟動時選擇 Java 運行時和 JVM 的配置菌瘪。我認為腮敌,如果選擇使用外部 Maven 倉庫依賴,就沒有理由去懷疑外部庫意外的錯誤或其它的問題因為在依賴問題在構(gòu)建過程就已經(jīng)解決麻车。而在前一種方案中缀皱,Capsule 必須顯式說明它的依賴,并且能夠列出整個依賴庫动猬。同樣啤斗,如果把組織內(nèi)部 Maven 倉庫用做 capsule 的依賴,那就沒有理由不把當(dāng)成運維的服務(wù)器赁咙,確保它和其它服務(wù)器一樣保證運行時的可用性(特別注意 Maven 倉庫軟件并不為人所知的crash )钮莲。

現(xiàn)在讓我們回到手邊要做的事免钻。

現(xiàn)代 JavaWeb 開發(fā)

因為 JavaWeb 服務(wù)器與 Web 一樣老,因此在 JavaWeb 上長期存在的成功傳統(tǒng)和實踐很快就要扔掉崔拥,現(xiàn)在可能是一個好的時候來解釋這一系列中“現(xiàn)代”意思极舔。

在本文中,我說“現(xiàn)代”的意思链瓦,就是“與現(xiàn)代主流軟件開發(fā)趨勢一致”拆魏。這些趨勢并不是完全任意的堆砌,他們一個一個契合在一起慈俯。出現(xiàn)于這個期間大量小型快速發(fā)展的創(chuàng)業(yè)公司更偏愛精益開發(fā)方法渤刃。這些都要求一個更好使用,更少安裝贴膘、部署和配置卖子,集開發(fā)和運維于一體的工具。廣受歡迎的云計算通過資源管理刑峡,也就虛擬化(無論是工具上還是在系統(tǒng)級)鼓勵這些方法洋闽。系統(tǒng)級部署和資源分配也支持異構(gòu)架構(gòu)的發(fā)展。所謂異構(gòu)架構(gòu)就是指尋找適合的工具(也有可能是不同的工具)做合適的事突梦。

傳統(tǒng)的 JavaWeb 服務(wù)器诫舅,也就是典型的應(yīng)用服務(wù)器,都有一個特別的特性:支持在一個 JVM 上運行多個應(yīng)用宫患。這個應(yīng)用服務(wù)器提供能分開應(yīng)用的運行時環(huán)境骚勘,而且升級,安裝和啟動都是獨立的撮奏。一個應(yīng)用可能運行在一個配置好的,已經(jīng)運行的環(huán)境中当宴,這種方法很多時候都工作良好畜吊,你也有理由繼續(xù)使用這種方案,但是這種方案户矢,離“現(xiàn)化”太遠了玲献。在不同的應(yīng)用中分配不同的資源這件事是并不簡單,而且在一定程度上跟現(xiàn)在使用 hypervisor 和 os 容器來運行應(yīng)用的方案是矛盾的梯浪。因為現(xiàn)在針對 hypervisor 和 os 容器設(shè)計的工具和技術(shù)在多應(yīng)用服務(wù)器上效率并不高捌年,即使這些多應(yīng)用服務(wù)器只是用來運行一個應(yīng)用,而且這些多應(yīng)用服務(wù)器的運維也不“現(xiàn)代”:安裝配置 web 或者 app 服務(wù)器是不可缺少的挂洛,部署應(yīng)用需要很多步礼预,每一步可能都很麻煩。

現(xiàn)代的方法虏劲,就是在其它語言和運行平臺使用的方法--單應(yīng)用服務(wù)器托酸。單應(yīng)用服務(wù)器中褒颈,web 容器是嵌入到應(yīng)用中(而不是把應(yīng)用部署到we b容囂中)。這樣做就可以簡單的部署励堡,管理谷丸,配置和在系統(tǒng)級進行資源的分配。這就是為什么应结,一但現(xiàn)代的方法被引入Java中刨疼,傳統(tǒng)的應(yīng)用服務(wù)器(我的意思是任何打算運行多個應(yīng)用的 servlet 或者全功能的 J2e 服務(wù)器)就死了

在這里鹅龄,我們調(diào)研的工具和技術(shù)并非覆蓋全部的的領(lǐng)域揩慕。特別是在 web 和 web 相關(guān)的領(lǐng)域中,開發(fā)砾层,工具漩绵,庫和框架激增。這種增長部分原因是肛炮,不像嵌入式開發(fā)和大型機開發(fā)止吐,web開發(fā)在初創(chuàng)公司和開發(fā)愛好者中廣受歡迎。這類人是新技術(shù)的早期采納者和體驗者侨糟,有時也會為了探索技術(shù)的邊界碍扔,或者學(xué)習(xí),還有自我證明發(fā)明一種新的擇術(shù)秕重。這樣的結(jié)果就是數(shù)以百計的庫被發(fā)明出來不同,全都為了解決同樣的目標(biāo),只是使用的方法略有不同溶耘。這種事情發(fā)生在 Java 的世界里二拐,也發(fā)生在其他的語言生態(tài)中。

同時凳兵,我們不會討論那種有巨大的 MVC 結(jié)構(gòu)乡括,模板系統(tǒng)或者設(shè)計來就是在服務(wù)器端渲染 html 的“全功能”的 web 框架丘侠。有很多理由不這么做姑廉,第一個就是塑娇,我從來沒有使用過那種框架,所以我不會評論他們的適用性或“現(xiàn)化化”形庭,第二铅辞,這個主題就非常復(fù)雜,需要更多的討論萨醒,而在別的地方已經(jīng)有了(這里,這里), 第三斟珊,web 開發(fā)正在朝客戶端渲染和SPA方向發(fā)展(如 angular),本質(zhì)上正在朝著以前c/s的架構(gòu)發(fā)展富纸,數(shù)據(jù)和命令都通過http對服務(wù)器進行交互倍宾。這種轉(zhuǎn)變沒太完全雏节,特別的,它依靠手機瀏覽器的 js 效率的提升高职,但是可以肯定的講钩乍,我們將會看到越來越少HTML在服務(wù)器端生成。因此怔锌,我們會只討論 http “數(shù)據(jù)” 服務(wù)的庫和框架寥粹。

http 服務(wù)和JAX-RS 與 Dropwizard

Java 與其他語言不同的一點是 JCP(Java Community Process)的工作,它的工作是標(biāo)準(zhǔn)化 API(即使對于不屬于語言規(guī)范或甚至標(biāo)準(zhǔn)運行時的庫)也是如此埃元,然后由各種商業(yè)或開源組織實現(xiàn)涝涤。這些 JSR(Java Specification Requests)是由專家組制作的,它能把一項技術(shù)從普遍變成成熟并成為標(biāo)準(zhǔn)岛杀。當(dāng) JSR 通過時阔拳,就會非常有用,因為幾乎所有迎合相關(guān)領(lǐng)域的庫都將實現(xiàn)這個標(biāo)準(zhǔn) API类嗤,這使得切換實現(xiàn)不那么痛苦糊肠。

對于服務(wù)器實現(xiàn)(代碼中框架更為普遍)來說,標(biāo)準(zhǔn)對于客戶端(每個調(diào)用或多或少都是獨立的并且可以被替換)而言更重要遗锣。 您可以使用三個不同的 HTT P客戶端和 3 個不同的 JDBC API货裹,但是您的服務(wù)器通常運行在單個框架中。 出于這個原因精偿,弧圆。 單純的 API 美學(xué)不應(yīng)該傾向于支持非標(biāo)準(zhǔn)的API。

相比于客戶端(每次請求或多或少比較獨立和能被替代)笔咽,標(biāo)準(zhǔn)化對服務(wù)器應(yīng)用更重要(因為框架代碼無處不在)搔预。你可以使用三個不同的 http 客戶端和三個不同的 JDDC api 在同一個方法中,但是你的服務(wù)器通常運行在一個框架中叶组。出于這個原因斯撮,你應(yīng)該更喜歡標(biāo)準(zhǔn)服務(wù)器API而不是非標(biāo)準(zhǔn)服務(wù)器API,除非非標(biāo)準(zhǔn)服務(wù)器 API 為你的應(yīng)用提供了一些非常重要的優(yōu)勢扶叉,或者更適合您的特定用例。單純的 API 美學(xué)不應(yīng)該傾向于支持非標(biāo)準(zhǔn)的 API帕膜。

那么輕量級的 Web 服務(wù)器最好應(yīng)該實現(xiàn)標(biāo)準(zhǔn)的 API枣氧。談到 HTT P服務(wù)時,有幾個相關(guān)的 API 需要關(guān)注垮刹。第一個是古老的 Servlet API(目前是 Servlet 3.0的 JSR-315 和 Servlet 3.1的 JSR-340 )达吞。幾乎所有的 JavaWeb 服務(wù)器都實現(xiàn)了 Servlet API,其中一些是“現(xiàn)代”的(在我們之前討論的意思)荒典,而在這里面最流行的是 Jetty酪劫。與傳統(tǒng)的 JavaWeb 服務(wù)器不同吞鸭,Jetty 不是獨立的 We b應(yīng)用程序容器,而是嵌入在應(yīng)用程序中的 Web 服務(wù)庫覆糟。它就是為"現(xiàn)代"編寫的刻剥。不過傳統(tǒng)的 Web 服務(wù)器,如 Tomcat滩字,現(xiàn)在也已經(jīng)有了嵌入式模式造虏。因為 Servlet 是一個相對較低級別的 HTTP 服務(wù)器 API,我們不會在這里直接使用它們麦箍,所所以讓我們繼續(xù)討論下一個標(biāo)準(zhǔn) API -- JAX-RS(目前版本2.0漓藕,在JSR-339中說明)。現(xiàn)在已經(jīng)有幾種 JAX-RS 的實現(xiàn)挟裂,像 Apache CXF享钞,RESTEasyRestlet,但最流行的應(yīng)該是 Jersey诀蓉。

JAX-RS 實現(xiàn)通常是在 Servlet 服務(wù)之上來使用栗竖。 因此,通過將 Jetty 和 Jersey 組合在一起來構(gòu)建一個現(xiàn)代化的 JavaWeb服務(wù)微框架是非常自然的事交排,而這正是我們下一步將要使用的工具:Dropwizard划滋。

所以,Dropwizard 把 Jetty,Jersey埃篓,Jackson处坪,我們在第 2 部分介紹的現(xiàn)代性能監(jiān)測庫 Metrics(它恰好是由 Dropwizard 背后的人 Coda Hale 創(chuàng)建的)和其他一些庫,組合成一個完整架专,簡單同窘,現(xiàn)代的 JavaWeb 服務(wù)微框架。

我們現(xiàn)在將用 Dropwizard 編寫第一個現(xiàn)代 JavaWeb 服務(wù)部脚。 如果你還沒有閱讀第一部分想邦,我建議你現(xiàn)在就回頭看一下,這樣能熟悉一下 Gradle 的基本用法委刘,因為我們將使用 Gradle 做為構(gòu)建工具丧没。

我們將創(chuàng)建一個新的 jmodern-web 目錄,cd 進入該目錄锡移,輸入 gradle init --type java-library 創(chuàng)建一個 Gradle項目呕童,刪除文件(src/main/java/Library.javasrc/test/java/LibraryTest.java

然后,編輯 build.gradle:

apply plugin: 'java'
apply plugin: 'application'

sourceCompatibility = '1.8'

mainClassName = 'jmodern.Main'
version = '0.1.0'

repositories {
    mavenCentral()
}

configurations {
    capsule
}

dependencies {
    compile 'io.dropwizard:dropwizard-core:0.7.0'
    capsule 'co.paralleluniverse:capsule:0.4.0'
    testCompile 'junit:junit:4.11'
}

task capsule(type: Jar, dependsOn: classes) {
    archiveName = "jmodern-web.jar"

    from jar // embed our application jar
    from { configurations.runtime } // embed dependencies

    from(configurations.capsule.collect { zipTree(it) }) { include 'Capsule.class' } // we just need the single Capsule class

    manifest {
        attributes(
            'Main-Class'  :   'Capsule',
            'Application-Class'   : mainClassName,
            'Application-Version' : version,
            'Min-Java-Version' : '1.8.0',
            'JVM-Args' : run.jvmArgs.join(' '),
            'System-Properties' : run.systemProperties.collect { k,v -> "$k=$v" }.join(' '),
        )
    }
}

src/main/java/jmodern/Main.java 文件修改如下:

package jmodern;

import io.dropwizard.Application;
import io.dropwizard.*;
import io.dropwizard.setup.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
import javax.ws.rs.*;
import javax.ws.rs.core.*;

public class Main extends Application<Configuration> {
    public static void main(String[] args) throws Exception {
        new Main().run(new String[]{"server"});
    }

    @Override
    public void initialize(Bootstrap<Configuration> bootstrap) {
    }

    @Override
    public void run(Configuration configuration, Environment environment) {
        environment.jersey().register(new HelloWorldResource());
    }

    @Path("/hello-world")
    @Produces(MediaType.APPLICATION_JSON)
    public static class HelloWorldResource {
        private final AtomicLong counter = new AtomicLong();

        @GET
        public Map<String, Object> sayHello(@QueryParam("name") String name) {
            Map<String, Object> res = new HashMap<>();
            res.put("id", counter.incrementAndGet());
            res.put("content", "Hello, " + (name != null ? name : "World"));
            return res;
        }
    }
}

這幾乎是最簡單的 Dropwizard 服務(wù)了淆珊。 sayHello 方法返回一個 Map夺饲,Map會自動改為 JSON 對象。 在 shell 中鍵入 gradle run,運行應(yīng)用往声,或者先用 gradle capsule 構(gòu)建一個 capsule擂找,然后使用 java -jar build/libs/jmodern-web.jar 運行應(yīng)用。要測試業(yè)務(wù)邏輯需要在瀏覽器中輸入 http://localhost:8080/hello-worldhttp://localhost:8080/hello-world?name=Modern+Developer 進行測試浩销。

現(xiàn)在讓我們用 Dropwizard 的其它特性改進我們的服務(wù):

package jmodern;

import com.codahale.metrics.*;
import com.codahale.metrics.annotation.*;
import com.fasterxml.jackson.annotation.*;
import com.google.common.base.Optional;
import io.dropwizard.Application;
import io.dropwizard.*;
import io.dropwizard.setup.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicLong;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import org.hibernate.validator.constraints.*;

public class Main extends Application<Main.JModernConfiguration> {
    public static void main(String[] args) throws Exception {
        new Main().run(new String[]{"server", System.getProperty("dropwizard.config")});
    }

    @Override
    public void initialize(Bootstrap<JModernConfiguration> bootstrap) {
    }

    @Override
    public void run(JModernConfiguration cfg, Environment env) {
        JmxReporter.forRegistry(env.metrics()).build().start(); // Manually add JMX reporting (Dropwizard regression)

        env.jersey().register(new HelloWorldResource(cfg));
    }

    // YAML Configuration
    public static class JModernConfiguration extends Configuration {
        @JsonProperty private @NotEmpty String template;
        @JsonProperty private @NotEmpty String defaultName;

        public String getTemplate()    { return template; }
        public String getDefaultName() { return defaultName; }
    }

    // The actual service
    @Path("/hello-world")
    @Produces(MediaType.APPLICATION_JSON)
    public static class HelloWorldResource {
        private final AtomicLong counter = new AtomicLong();
        private final String template;
        private final String defaultName;

        public HelloWorldResource(JModernConfiguration cfg) {
            this.template = cfg.getTemplate();
            this.defaultName = cfg.getDefaultName();
        }

        @Timed // monitor timing of this service with Metrics
        @GET
        public Saying sayHello(@QueryParam("name") Optional<String> name) throws InterruptedException {
            final String value = String.format(template, name.or(defaultName));
            Thread.sleep(ThreadLocalRandom.current().nextInt(10, 500));
            return new Saying(counter.incrementAndGet(), value);
        }
    }

    // JSON (immutable!) payload
    public static class Saying {
        private long id;
        private @Length(max = 10) String content;

        public Saying(long id, String content) {
            this.id = id;
            this.content = content;
        }

        public Saying() {} // required for deserialization

        @JsonProperty public long getId() { return id; }
        @JsonProperty public String getContent() { return content; }
    }
}

我們做了一些改進贯涎。 首先,用一個不可變的 java 類來表示 JSON 對象撼嗓。 其次柬采,為服務(wù)添加了隨機睡眠功能,以及增加了@Timed 注解且警,這樣 Metrics 庫就能自動監(jiān)控報告我們服務(wù)的延遲粉捻。 最后,我們使用 DropWizard YAML配置我們的服務(wù)斑芜。 雖然這對于一個簡單的 “Hello肩刃,World” 服務(wù)來說可能過于復(fù)雜了,但它可以作為復(fù)雜應(yīng)用程序的基礎(chǔ)杏头。額外的代碼為我們帶來了監(jiān)測盈包,可配置性和類型安全。 為了使用配置醇王,我們需要創(chuàng)建一個配置類呢燥,并對我們的構(gòu)建文件進行一些調(diào)整。

template: Hello, %s!
defaultName: Stranger

然后寓娩,增加以下代碼到 build.gradle叛氨,這是為了在運行代碼時,能找到配置文件:

run {
    systemProperty "dropwizard.config", "build/resources/main/jmodern.yml"
}

最后棘伴,我們希望在 capsule 中默認包含配置文件寞埠,因此我們將添加以下部分:

from { sourceSets.main.resources }

同時,也把 System-Properties 進行調(diào)整:

System-Properties' : (run.systemProperties + ["dropwizard.config": '$CAPSULE_DIR/jmodern.yml']).collect { k,v -> "$k=$v" }.join(' '),

現(xiàn)在我們用 gradle capsule 構(gòu)建部署 capsule焊夸,并使用 java -jar build/libs/jmodern-web.ja 啟動服務(wù)器仁连。 您現(xiàn)在可以在 http://localhost:8080/hello-worldhttp://localhost:8080/hello-world?name=Modern+Developer 測試服務(wù)。

如果想調(diào)整默認配置阱穗,只要在項目目錄下創(chuàng)建 foo.yml 文件:

template: Howdy, %s!
defaultName: fella

使用這個配置文件饭冬,覆蓋 dropwizard.config 屬性:

java -Ddropwizard.config=foo.yml -jar build/libs/jmodern-web.jar

我們可以啟動 VisualVM(請參閱第2部分),并查看應(yīng)用服務(wù)報告揪阶,特別的昌抠,我們應(yīng)用的時間花費:

1

我們打開 Dropwizard 管理控制臺:

2

打開 http://localhost:8081/metrics,返回以下一個JSON對象:

3

就是這樣遣钳!配置文件也可以用來修改Dropwizard的很多內(nèi)部變量,如設(shè)置日志級別等等。有關(guān)詳細信息蕴茴,請參閱Dropizard文檔劝评。

總而言之,Dropwizard 是一個精簡倦淀、有趣的現(xiàn)代化微型框架蒋畜,它可讓你部署簡單,配置輕松以及開箱即用的出色的監(jiān)控能力撞叽。另一個有類似功能的框架是 Spring Boot姻成。不幸的是,Boot 沒有使用 JAX-RS 標(biāo)準(zhǔn) API愿棋,但有一個項目試圖修復(fù)這個問題科展。

Dropwizard具有極好的開箱即用體驗,但更高級的用戶可能會發(fā)現(xiàn)它也有一些限制(例如糠雨,Dropwizard 的某些組件很難被其他組件替代:比如日志引擎)才睹。這些用戶可能會發(fā)現(xiàn)將 Jersey, Jetty 和其他庫進行組裝是非常有必要的,并且可以自己制定管道甘邀,以構(gòu)建一個最適合其組織的輕量級服務(wù)器琅攘。這樣做應(yīng)該不需要很多工作,而且只需要一次就可以適用所有自己的項目松邪。Dropwizard 是一個很好的起點坞琴,如果它適合你(它應(yīng)該在大多數(shù)情況下),你可以放心地堅持使用下去逗抑。在這篇文章中的大部分示例中我們使用 Dropwizard剧辐,但是示例中所做的你都可以單獨使用Jetty,或者與Jersey結(jié)合使用來完成锋八。而在 Dropwizard浙于,更改配置和自動監(jiān)控則無需額外的工作。

http 客戶端

增加下面代碼到構(gòu)建文件:

compile 'io.dropwizard:dropwizard-client:0.7.0'

導(dǎo)入以下庫到 jmoern.Main

import io.dropwizard.client.*;
import com.sun.jersey.api.client.Client;

增加下面代碼到 JModernConfiguration

@Valid @NotNull @JsonProperty JerseyClientConfiguration httpClient = new JerseyClientConfiguration();
public JerseyClientConfiguration getJerseyClientConfiguration() { return httpClient; }

我們將實例化客戶端挟纱,并注冊一個新服務(wù)羞酗,我們將其稱為 Consumer,并添加到 run 方法中:

Client client = new JerseyClientBuilder(env).using(cfg.getJerseyClientConfiguration()).build("client");
env.jersey().register(new ConsumerResource(client));

下面是我們的服務(wù):

@Path("/consumer")
@Produces(MediaType.TEXT_PLAIN)
public static class ConsumerResource {
    private final Client client;

    public ConsumerResource(Client client) {
        this.client = client;
    }

    @Timed
    @GET
    public String consume() {
        Saying saying = client.resource(UriBuilder.fromUri("http://localhost:8080/hello-world").queryParam("name", "consumer").build())
                .get(Saying.class);
        return String.format("The service is saying: %s (id: %d)",  saying.getContent(), saying.getId());
    }
}

注意到方法返回的 JSON 對像是如何反序列化成 Saying 對象的紊服;它也可以是 Map檀轨,string 以及其他類型(Dropwizard使用的是Jersey JAX-RS客戶端的舊版本,新的API類似)欺嗤。而且由于 Dropwizard 開箱即用地支持 Jersey JAX-RS 客戶端参萄,因此會自動發(fā)持請求的性能指標(biāo)。

要測試我們的新服務(wù)煎饼,啟動我們的應(yīng)用程序( gradle run 讹挎,記住)并將瀏覽器指向 http://localhost:8080/consumer

所以 JAX-RS 標(biāo)準(zhǔn)也標(biāo)準(zhǔn)化了客戶端的 API筒溃。但是马篮,正如我們之前所說的,當(dāng)談到客戶端 API 時怜奖,我們也可以使用非標(biāo)準(zhǔn)的API浑测。一個頗受歡迎的HTTP客戶端 Retrofit 是由 Square 提供的。如你所見歪玲,JAX-RS 客戶端可以自動將 Java 對象序列化并反序列化為 JSON 對象(或 XML)迁央。Retrofit 把這種轉(zhuǎn)化用在 Java/REST 轉(zhuǎn)換上(這種轉(zhuǎn)換并不總是一件好事;領(lǐng)域模型的轉(zhuǎn)換通常具有抽象漏洞滥崩,但如果你只限于用于簡單的協(xié)議岖圈,應(yīng)該會很有幫助),包括服務(wù) URL夭委,而不僅僅是 JSON 到 Java 接口的轉(zhuǎn)換幅狮。不幸的是,Retrofit 使用與 JAX-RS(服務(wù)器)相同的注解名稱株灸。因此我們要在不同的包中定義崇摄,這會使我們的示例有點難看。幸運的是慌烧,Retrofit 有 Netflix 提供的稱為 Feign 的克隆/衍生產(chǎn)品逐抑。Feign 和 Retrofit 之間的差異并對我來說并不完全清楚。盡管看起來 Retrofit 更廣泛地被采用(它更成熟)屹蚊,而 Feign 更容易定制厕氨。無論如何,這兩者非常相似汹粤,可以互換使用命斧。

試試 Feign,將以下依賴添加到 build.gradle :

compile 'com.netflix.feign:feign-core:6.1.2'
compile 'com.netflix.feign:feign-jaxrs:6.1.2'
compile 'com.netflix.feign:feign-jackson:6.1.2'

導(dǎo)入到 Main:

import feign.Feign;
import feign.jackson.*;
import feign.jaxrs.*;

我們用 Feign 代替 JAX-RS:

Feign.Builder feignBuilder = Feign.builder()
        .contract(new JAXRSModule.JAXRSContract()) // we want JAX-RS annotations
        .encoder(new JacksonEncoder()) // we want Jackson because that's what Dropwizard uses already
        .decoder(new JacksonDecoder());
env.jersey().register(new ConsumerResource(feignBuilder));

現(xiàn)在我們的消費服務(wù)看起來如下:

@Path("/consumer")
@Produces(MediaType.TEXT_PLAIN)
public static class ConsumerResource {
    private final HelloWorldAPI hellowWorld;

    public ConsumerResource(Feign.Builder feignBuilder) {
        this.hellowWorld = feignBuilder.target(HelloWorldAPI.class, "http://localhost:8080");
    }

    @Timed
    @GET
    public String consume() {
        Saying saying = hellowWorld.hi("consumer");
        return String.format("The service is saying: %s (id: %d)",  saying.getContent(), saying.getId());
    }
}

最后嘱兼,我們添加 HelloWorldAPI 接口国葬,該接口把 REST API 說明定入代碼中(你可以將接口定義放在我們的 Main 類中;不需要創(chuàng)建新的Java文件):

interface HelloWorldAPI {
    @GET @Path("/hello-world")
    Saying hi(@QueryParam("name") String name);

    @GET @Path("/hello-world")
    Saying hi();
}

此接口使用 JAX-RS 注解說明如何把方法轉(zhuǎn)換 http 為請求。實際執(zhí)行轉(zhuǎn)換是由 Feign(或Retrofit)自動完成的芹壕。

啟動應(yīng)用后汇四,訪問 http://localhost:8080/consumer 以測試新的服務(wù)。

如果想看到更復(fù)雜的 REST API 是如何轉(zhuǎn)換為Java代碼的踢涌, 這個簡單的例子演示使用 Retroift 消費 GitHub 的API通孽,還有這里使用 Feign。 Retrofit 和 Feign 功能都非常豐富睁壁,可以很好地控制請求的轉(zhuǎn)換和執(zhí)行方式背苦。此時互捌,我會推薦 Retroift 而不是 Feign,因為 Retrofit 更成熟行剂,它利用了高效的 NIO 網(wǎng)絡(luò) API疫剃,而 Feign使用慢速的 HttpURLConnection API( 更好的傳輸機制可以添加進 Feign 中,但我還沒有找到)硼讽。

還有其他一些較底層的 HTTP 客戶端 API(例如 Apache HTTP Client,Dropwizard 也直接支持)牲阁,但在大多數(shù)情況下固阁,我們剛才試過的高層次的 API(JAX-RS Client 或 Retorfit / Feign)效果最佳。

數(shù)據(jù)庫訪問

JDK 包含用于(關(guān)系)數(shù)據(jù)庫訪問的標(biāo)準(zhǔn) API城菊,稱為 JDBC (Java數(shù)據(jù)庫連接)备燃。幾乎所有的 SQL 數(shù)據(jù)庫都支持 JDBC。但是 JDBC 是一個非常低級的 API凌唬,有時可能會令人厭煩并齐。Java 還有一個標(biāo)準(zhǔn)的高級數(shù)據(jù)庫訪問 API - 實際上是一個ORM--被 JSR-220 和 JSR-317 叫做 JPA(Java Persistance API)。JPA的知名實現(xiàn)包括 Hibernate 客税, OpenJPAEclipseLink 况褪。請不要使用他們,我相信以后你會感謝我更耻。并不是說他們工作的不好测垛,是因為他們往往比他們的帶來麻煩比價值更多。ORM 鼓勵復(fù)雜的對象圖和復(fù)雜的模式秧均,這往往會導(dǎo)致生成非常復(fù)雜的 SQL 語句食侮,這些語句很難優(yōu)化。另外目胡,ORM 并不以其出色的性能而聞名锯七。

直接使用 JDBC 通常更好,但也許最好的方法是使用我們現(xiàn)在提供的工具之一誉己。它位于低級 JDBC 和高級的 ORM 之間眉尸。它不是標(biāo)準(zhǔn)的,這意味著每個工具都有它自己的 API巫延。但正如我們所說的效五,不使用標(biāo)準(zhǔn) API 適合于客戶端 API。在下面我們的例子中炉峰,我們使用H2 嵌入式數(shù)據(jù)庫畏妖。

我們將從 JDBI 開始,這也由 Dropwizrd 直接支持疼阔。要有效地使用 JDBI 戒劫,你需要權(quán)衡最佳模式和簡單代碼半夷,直到您達到一個很好的中間地帶(JDBI 對于非常復(fù)雜的模式并不理想)。

我們添加這些依賴關(guān)系:

compile 'io.dropwizard:dropwizard-db:0.7.0'
compile 'io.dropwizard:dropwizard-jdbi:0.7.0'
runtime 'com.h2database:h2:1.4.178'

并且導(dǎo)入:

import io.dropwizard.db.*;
import io.dropwizard.jdbi.*;
import org.skife.jdbi.v2.*;
import org.skife.jdbi.v2.util.*;

然后迅细,我們增加 DataSource 工廠類到 JModernConfiguration :

DBI dbi = new DBIFactory().build(env, cfg.getDataSourceFactory(), "db");
env.jersey().register(new DBResource(dbi));

為了配置數(shù)據(jù)庫巫橄,我們需要將以下內(nèi)容添加到 jmodern.yml

database:
  driverClass: org.h2.Driver
  url: jdbc:h2:mem:test
  user: u
  password: p

最后,讓我們創(chuàng)建數(shù)據(jù)庫資源:

@Path("/db")
@Produces(MediaType.APPLICATION_JSON)
public static class DBResource {
    private final DBI dbi;

    public DBResource(DBI dbi) {
        this.dbi = dbi;

        try (Handle h = dbi.open()) {
            h.execute("create table something (id int primary key auto_increment, name varchar(100))");
            String[] names = { "Gigantic", "Bone Machine", "Hey", "Cactus" };
            Arrays.stream(names).forEach(name -> h.insert("insert into something (name) values (?)", name));
        }
    }

    @Timed
    @POST @Path("/add")
    public Map<String, Object> add(String name) {
        try (Handle h = dbi.open()) {
            int id = h.createStatement("insert into something (name) values (:name)").bind("name", name)
                    .executeAndReturnGeneratedKeys(IntegerMapper.FIRST).first();
            return find(id);
        }
    }

    @Timed
    @GET @Path("/item/{id}")
    public Map<String, Object> find(@PathParam("id") Integer id) {
        try (Handle h = dbi.open()) {
            return h.createQuery("select id, name from something where id = :id").bind("id", id).first();
        }
    }

    @Timed
    @GET @Path("/all")
    public List<Map<String, Object>> all(@PathParam("id") Integer id) {
        try (Handle h = dbi.open()) {
            return h.createQuery("select * from something").list();
        }
    }
}

對于那些了解 JDBC 的人茵典,這些代碼有很多熟悉的和不同的地方湘换。JDBI 有一個流暢的接口,并且方法返回Java集合统阿,并將其自動地序列化為 JSON 對象彩倚。總之扶平,這就像一個有趣的"現(xiàn)代"JDBC帆离。

啟動應(yīng)用程序并將瀏覽器指向 http://localhost:8080/db/all 以查看所有條目,或者在http://localhost:8080/db/item/ 2 處查看第二個條目结澄。然后哥谷,您也可以通過控制臺創(chuàng)建新的條目:

curl --data Velouria http://localhost:8080/db/add

JDBI 還可以像 Retrofit 一樣,提供一個數(shù)據(jù)庫使用量身定制的定制界面麻献。通過將 JDBI 將表行映射為 Java 對象们妥,我們還可以獲得一些小技巧。

這是我們的對象:

public static class Something {
    @JsonProperty public final int id;
    @JsonProperty public final String name;

    public Something(int id, String name) {
        this.id = id;
        this.name = name;
    }
}

@JsonProperty 注釋將確保這個屬性自動將它 JSON 序列化勉吻,但為了使 JDBI 能夠與 Something 一起工作王悍,我們還需要創(chuàng)建一個 ResultSetMapper ,它 將JDBC ResultSet 轉(zhuǎn)換為Something 對象:

public static class SomethingMapper implements ResultSetMapper<Something> {
    public Something map(int index, ResultSet r, StatementContext ctx) throws SQLException {
        return new Something(r.getInt("id"), r.getString("name"));
    }
}

現(xiàn)在有意思的事情就要開始了餐曼!這是我們的 DAO 類(或JDBI說法中的 SQL 對象 ) - JDBI SQL 對象是數(shù)據(jù)庫就像 Retrofit 對于 REST 的改造:

@RegisterMapper(SomethingMapper.class)
interface ModernDAO {
    @SqlUpdate("insert into something (name) values (:name)")
    @GetGeneratedKeys
    int insert(@Bind("name") String name);

    @SqlQuery("select * from something where id = :id")
    Something findById(@Bind("id") int id);

    @SqlQuery("select * from something")
    List<Something> all();
}

那現(xiàn)在压储,我們新的數(shù)據(jù)庫資源可以這樣寫:

@Path("/db")
@Produces(MediaType.APPLICATION_JSON)
public static class DBResource {
    private final ModernDAO dao;

    public DBResource(DBI dbi) {
        this.dao = dbi.onDemand(ModernDAO.class);

        try (Handle h = dbi.open()) {
            h.execute("create table something (id int primary key auto_increment, name varchar(100))");
            String[] names = { "Gigantic", "Bone Machine", "Hey", "Cactus" };
            Arrays.stream(names).forEach(name -> h.insert("insert into something (name) values (?)", name));
        }
    }

    @Timed
    @POST @Path("/add")
    public Something add(String name) {
        return find(dao.insert(name));
    }

    @Timed
    @GET @Path("/item/{id}")
    public Something find(@PathParam("id") Integer id) {
        return dao.findById(id);
    }

    @Timed
    @GET @Path("/all")
    public List<Something> all(@PathParam("id") Integer id) {
        return dao.all();
    }
}

JDBI并不是一個完整的 ORM 解決方案:它不會自動生成 SQL 語句,也不會自動生成完整的對象圖源譬,但它確使我們獲得了數(shù)據(jù)庫訪問的快捷方法集惋,其量級遠低于任何 JPA 實現(xiàn)。

使用 JDBI 時踩娘,Dropwizard 會自動添加一個運行狀況檢查( http://localhost:8081/healthcheck)刮刑,用于測試數(shù)據(jù)庫的連通性,并用監(jiān)控 DAO 的性能指標(biāo):

3

下面我們會看到的數(shù)據(jù)庫訪問庫 jOOQ养渴,它與 JDBI 流暢 API 類似(它沒有與 JDB I的 SQL 對象類似的API)雷绢,但它采用了不同的方法:它使用方法調(diào)用鏈而不是字符串,生成 SQ L語句(并且它可以生成的SQL兼容的多種數(shù)據(jù)庫)理卑。

我們將添加這個依賴關(guān)系:

compile 'org.jooq:jooq:3.3.2'

導(dǎo)入庫:

import org.jooq.Record;
import org.jooq.RecordMapper;
import static org.jooq.impl.DSL.*;

在 run 方法中翘紊,注冊數(shù)據(jù)庫資源:

DataSource ds = cfg.getDataSourceFactory().build(env.metrics(), "db"); // Dropwizard will monitor the connection pool
env.jersey().register(new DBResource(ds));

我們的新 DBResource 如下所示:

@Path("/db")
@Produces(MediaType.APPLICATION_JSON)
public static class DBResource {
    private final DataSource ds;
    private static final RecordMapper<Record, Something> toSomething =
            record -> new Something(record.getValue(field("id", Integer.class)), record.getValue(field("name", String.class)));

    public DBResource(DataSource ds) throws SQLException {
        this.ds = ds;

        try (Connection conn = ds.getConnection()) {
            conn.createStatement().execute("create table something (id int primary key auto_increment, name varchar(100))");

            String[] names = { "Gigantic", "Bone Machine", "Hey", "Cactus" };
            DSLContext context = using(conn);
            Arrays.stream(names).forEach(name -> context.insertInto(table("something"), field("name")).values(name).execute());
        }
    }

    @Timed
    @POST @Path("/add")
    public Something add(String name) throws SQLException {
        try (Connection conn = ds.getConnection()) {
            // this does not work
            int id = using(conn).insertInto(table("something"), field("name")).values(name).returning(field("id"))
                       .fetchOne().into(Integer.class);
            return find(id);
        }
    }

    @Timed
    @GET @Path("/item/{id}")
    public Something find(@PathParam("id") Integer id) throws SQLException {
        try (Connection conn = ds.getConnection()) {
            return using(conn).select(field("id"), field("name")).from(table("something"))
                    .where(field("id", Integer.class).equal(id)).fetchOne().map(toSomething);
        }
    }

    @Timed
    @GET @Path("/all")
    public List<Something> all(@PathParam("id") Integer id) throws SQLException {
        try (Connection conn = ds.getConnection()) {
            return using(conn).select(field("id"), field("name")).from(table("something")).fetch().map(toSomething);
        }
    }
}

現(xiàn)在,jOOQ 還沒有實現(xiàn) DDL(像 create table 這樣的 SQL 語句)藐唠,所以你會注意到我們使用 JDBC 創(chuàng)建表帆疟。不過這也很好鹉究,因為 jOOQ 是作為一個JDBC包裝器實現(xiàn)的,無論如何都需要 JDBC(我還沒有能使add的正確工作的方法(可能是因為自動生成的主鍵的原因)jOOQ 的開發(fā)人員:如果你正在閱讀這個踪宠,請幫幫忙)自赔。

這個例子實際上并沒有正確的使用 JOOQ 正義,因為它的最大優(yōu)點是能夠從數(shù)據(jù)庫的 scheme 生成 class柳琢,并且能夠以類型安全的方式執(zhí)行我們之前完成的所有操作 - 以及更復(fù)雜的操作绍妨。對我個人來說,JOOQ 有點太智能了柬脸,但是如果你的模式很復(fù)雜痘绎,它可能是一個非常有用的工具。

依賴注入

依賴注入是否有用或無用取決于你問的對象肖粮。我相信 DI 在復(fù)雜的代碼庫中非常有用;對于簡單代碼來說尔苦,這不必要涩馆。Java 有一個由 JSR-330 指定的簡單標(biāo)準(zhǔn) DI API。JSR-330 有以下實現(xiàn): Spring IoC 允坚, Guice 魂那, DaggerSisu (建立在Guice之上)和 HK2 稠项。這些實現(xiàn)都是由大公司或組織開發(fā)的涯雅。鑒于這種情況,人們往往面臨著兩難選擇展运。我認為你不要害怕:如果你堅持JSR-330標(biāo)準(zhǔn)活逆,或者稍有偏差的實現(xiàn),您可以隨時更改您的DI解決方案拗胜。但如果你想讓你的應(yīng)用程序完全由用戶配置(XML文件的形式)蔗候,選擇Spring(這就是為什么我們選擇Spring for Galaxy);如果都不是,那么從Dagger開始埂软,只有當(dāng)它不再滿足你的需求時才去找別的東西锈遥。

我們來看看Dagger。首先勘畔,讓我們添加Dagger依賴關(guān)系:

compile 'com.squareup.dagger:dagger:1.2.1'
compile 'com.squareup.dagger:dagger-compiler:1.2.1'

為了保持整潔所灸,我們只留下 HelloWorldResource 。不過炫七,這一次爬立,我們不手動創(chuàng)建服務(wù)并將配置對象傳遞給它,而是使用Dagger 從 YAML 文件讀取我們的配置万哪,然后將它們注入到我們的服務(wù)中懦尝。

這是服務(wù)代碼:

@Path("/hello-world")
@Produces(MediaType.APPLICATION_JSON)
public static class HelloWorldResource {
    private final AtomicLong counter = new AtomicLong();
    @Inject @Named("template") String template;
    @Inject @Named("defaultName") String defaultName;

    HelloWorldResource() {
    }

    @Timed // monitor timing of this service with Metrics
    @GET
    public Saying sayHello(@QueryParam("name") Optional<String> name) throws InterruptedException {
        final String value = String.format(template, name.or(defaultName));
        Thread.sleep(ThreadLocalRandom.current().nextInt(10, 500));
        return new Saying(counter.incrementAndGet(), value);
    }
}

請注意@Inject@Named 注釋知纷。這些是 JSR-330 標(biāo)準(zhǔn)的一部分,所以無論我們使用哪種 DI 工具陵霉,我們的服務(wù)代碼都將保持不變琅轧。要實際連接并注入依賴關(guān)系,我們使用 Dagge r特定的模式踊挠。Dagger 在模塊類中指定了依賴配置乍桂。這是我們的:

@Module(injects = HelloWorldResource.class)
class ModernModule {
    private final JModernConfiguration cfg;

    public ModernModule(JModernConfiguration cfg) {
        this.cfg = cfg;
    }

    @Provides @Named("template") String provideTemplate() {
        return cfg.getTemplate();
    }

    @Provides @Named("defaultName") String provideDefaultName() {
        return cfg.getDefaultName();
    }
}

Dagger 最有用的功能之一是它在編譯時使用注釋處理器驗證所有依賴關(guān)系是否滿足。例如效床,如果我們忘記定義 provideDefaultName 睹酌,那么當(dāng)我們鍵入時,這就 是NetBeans 中顯示的內(nèi)容:

5

為了獲得完整配置的 HelloWorldResource 實例剩檀,我們在應(yīng)用程序的 run 方法中放入了這個實例:

ObjectGraph objectGraph = ObjectGraph.create(new ModernModule(cfg));
env.jersey().register(objectGraph.get(HelloWorldResource.class));

你會發(fā)現(xiàn)憋沿, ModernModule 類復(fù)制 JModernConfiguration 的一些行為。使用 @Module 簡單注解JModernConfiguration 沪猴,以及使用 @Provides 注解 getTemplategetDefaultName 方法非常簡單辐啄。Dagger 禁止子類型注解

高級主題:阻塞與非阻塞 VS 同步與異步

在這個話題上运嗜,我們需要對阻塞與非阻塞 API 的更多理論討論壶辜。阻塞或同步是方法會阻塞調(diào)用線程直到它們完成。當(dāng)然担租,阻塞(或非阻塞)的概念只有在這些方法可能需要很長時間才能完成時(例如幾十毫秒到幾十秒)才有意義砸民。另一種類型的API,通常稱為非阻塞奋救,但在這里我們稱它們?yōu)榘胱枞ɑ虬氘惒剑┝氩危窃诓僮髌陂g不會阻塞調(diào)用線程的方法。他們只啟動一項操作并返回 Feature 對象尝艘。Feature 對象用于等待待等待操作成然后在方便的時間完成后面的操作冗荸。最后,第三種類型的 API-真正的非阻塞或異步 API利耍,它也不會阻塞調(diào)用線程蚌本。但它的方法需要一個額外的參數(shù) - 一個回調(diào)函數(shù) ,它是在操作完成時將執(zhí)行的代碼(在某個未知的線程上)隘梨。有時候程癌,Java API 混合了最后兩種類型,既有回調(diào)又有 返因Feature對象轴猎。

必須明確:異步 API 的總是比阻塞的 API 更復(fù)雜(即使語言本身試圖使回調(diào)更容易使用箩退,通過使用如 promise袱耽,comprehensions, monad 等函數(shù)式方案)敞嗡。除了支持多線程的 Clojure 之外痰滋,異步的問題在 Java 這樣的語言中尤其糟糕筐摘,包括基本上所有其他的 JVM 語言。我們在這里不會詳細討論 clojure 不限制副作用的問題。在這些語言中使用非阻塞 API 需要嚴格的規(guī)范,并且需要對復(fù)雜的并發(fā)問題有清晰的理解援雇。阻塞 API 則沒有這些問題。

為什么有人會使用異??步 API椎扬?答案很簡單:性能惫搏。更深刻一點,內(nèi)核線程進行任務(wù)切換的成本不可忽略(這里不是說可以快速釋放線程內(nèi)存堆棧蚕涤,快速釋放線程堆棧這將更好地用于數(shù)據(jù)高速緩存)】鹋猓現(xiàn)代 Web 應(yīng)用程序通常會將實際處理委托給無數(shù)的服務(wù),有些會做離線 map-reduce揖铜,其他可能會做一些在線處理茴丰,面向客戶端的 Web 服務(wù)器的主要功能是協(xié)調(diào):它調(diào)用許多其他服務(wù)并組裝數(shù)據(jù)。它幾乎不做任何處理天吓,但它執(zhí)行大量的 IO 操作 - 有些可以并行完成贿肩,有些需要連續(xù)調(diào)用。這意味著 Web 服務(wù)器在相對較少的 CPU 工作時間內(nèi)會生成很多線程調(diào)度事件(線程阻塞和解除阻塞)失仁,這種時候,操作系統(tǒng)的線程調(diào)度開銷變得繁重们何。因此萄焦,人們?yōu)榱私鉀Q這個內(nèi)核線程調(diào)度性能問題而將代碼置于異步 API 這種不自然的扭曲之中。一些現(xiàn)代 We b框架/庫也非常喜歡使用非阻塞 API(我們沒有討論過其中的任何一個冤竹,因為我們說明拂封,他們都是錯誤的)。

這是錯誤的方法 鹦蠕。為了迎合不合理的實現(xiàn)冒签,人們放棄了適當(dāng)?shù)某橄螅ň€程),而不是簡單地修復(fù)不合理實現(xiàn)钟病。輕量級(或用戶級)線程已在 Erlang萧恕,Go 中使用,現(xiàn)在通過 Quasar 庫在 JVM 中使用 - 可讓您使用簡單的阻塞 API肠阱,而不存在任何性能問題票唆。

這種情況在計算機科學(xué)中非常罕見的。一種充滿了折衷和警告的導(dǎo)步方法幾乎總是擊敗另一種同步方法屹徘。異步代碼與同步代碼相比具有許多缺點和絕對劣勢走趋。即使輕量級線程的不完美實現(xiàn)也比異步編程更好,特別是當(dāng)語言對共享狀態(tài)突變不做防范時噪伊。這個規(guī)則可能有一些例外(畢竟簿煌,在 C S中氮唯,即使絕對不是絕對如此),但它們遠少于建議使用 goto 語句時的情況姨伟。

同步和異步是可以相互轉(zhuǎn)換的(每個都可以使用“恒定時間”轉(zhuǎn)換轉(zhuǎn)換為另一個)惩琉,但同步對人類來說是更好的抽象,我可以證明這一點授滓。我們來看兩個 API:

interface Sync {
    Object pull();
}

和:

interface Async {
    void push(Callback cb);
}

interface Callback {
    void got(Object obj);
}

現(xiàn)在讓我們使用 Sync 實現(xiàn) Async:

Async syncToAsync(Sync sync) {
    return new Async() {
        public void push(final Callback cb) {
            new Thread(() -> {
                  for(;;)
                      cb.got(sync.pull());
              }).start();
        }
    }
}

現(xiàn)在琳水,用您最喜歡的編程語言實現(xiàn)相反的功能,即將 Async 轉(zhuǎn)換為 Sync 般堆。這將更加棘手在孝,總是需要引入一些中間數(shù)據(jù)存儲,如隊列或緩沖區(qū)淮摔。當(dāng)然私沮,你需要考慮到 Callback.got 可以在任何線程上調(diào)用,所以你需要考慮與該數(shù)據(jù)結(jié)構(gòu)的并發(fā)性和橙。因此仔燕,從AsyncSync 的轉(zhuǎn)換不僅不那么簡單,而且引入了不必要的數(shù)據(jù)存儲:如果真沒有引入多余的數(shù)據(jù)存儲魔招,是因為它可能已經(jīng)內(nèi)置到系統(tǒng)中(例如以IO緩沖區(qū)的形式)晰搀。所以 Async 使用 Sync 簡單的實現(xiàn),但是相反的轉(zhuǎn)換既浪費又浪費時間办斑,并且需要管理并發(fā)外恕。但這對限制或管理副作用的語言(如 Clojure 或 Haskell )來說不是什么問題。

Comsat 項目將標(biāo)準(zhǔn)(和非標(biāo)準(zhǔn)但良好的)JavaWeb 相關(guān) API 與 Quasar fibers(輕量級線程)集成在一起乡翅。Comsat 的下一個版本將支持本文討論的工具(可能有 jOOQ 和 Retrofit / Feign例外)鳞疲,這樣你就可以編寫相同簡單的阻塞代碼,但可以獲得異步代碼的性能和可伸縮性優(yōu)勢蠕蚜。在未來的博客文章中尚洽,我們將展示 Comsat 如何不破壞你的代碼,同時讓您的應(yīng)用程序具更好的可伸縮性靶累。

高級主題:使用Web Actor與Web服務(wù)交互

雖然通常你應(yīng)該堅持使用標(biāo)準(zhǔn)的服務(wù)器 API腺毫,但有時候替代方案會帶來顯著的優(yōu)勢。這里沒有涉及的主題之一是使用 WebSocket 或 SSE 等技術(shù)的交互式 Web 服務(wù)挣柬。雖然 Java 的標(biāo)準(zhǔn) API 支持兩者拴曲,但是特別是使用 WebSocket 可能會導(dǎo)致復(fù)雜的并發(fā)問題,因為標(biāo)準(zhǔn) Java WebSocket API( JSR-356 )是異步的凛忿。這意味著 WebSocket 消息可能會同時到達服務(wù)器端澈灼,比如來自同一用戶的 HTTP 請求。這樣的化,異步 API 要管理可變的共享狀態(tài)叁熔,這種情狀很糟糕委乌。 Comsat 提供了一種稱為 Web Actors 的 API,它能為每一個用戶對話分配一個 actor荣回,它意味著接收同步化遭贸,使得狀態(tài)管理更容易。要了解有關(guān)Web Actors的更多信息心软,請閱讀介紹性博客文章壕吹。

結(jié)論

這篇就結(jié)束了“現(xiàn)代 Java 開發(fā)的意見指南”(盡管我可能會發(fā)布一個回應(yīng)反饋的文章)。我希望你喜歡閱讀它删铃,就像我喜歡寫它一樣耳贬。我希望我能夠傳達出 Java 生態(tài)系統(tǒng)不僅是巨大的,而且還充滿活力和與時俱進:用 Lambdas 和流代替冗長的數(shù)據(jù)操作代碼; Markdown 取代 HTML;fiber猎唁,channel 和 actor 取代鎖和回調(diào)咒劲;簡單的嵌入式服務(wù)器取代了重量級,笨重的應(yīng)用服務(wù)器诫隅。在所有這些功能下面腐魂,是強大,靈活的 JVM逐纬,它強調(diào)性能和監(jiān)控蛔屹,它能支持運行時代碼注入和替換。

原文地址:An Opinionated Guide to Modern Java, Part 3: Web Development


水平有限豁生,如果看不懂請直接看英文版兔毒。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市沛硅,隨后出現(xiàn)的幾起案子眼刃,更是在濱河造成了極大的恐慌绕辖,老刑警劉巖摇肌,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異仪际,居然都是意外死亡围小,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門树碱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來肯适,“玉大人,你說我怎么就攤上這事成榜】蛱颍” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長刘绣。 經(jīng)常有香客問我樱溉,道長,這世上最難降的妖魔是什么纬凤? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任福贞,我火速辦了婚禮,結(jié)果婚禮上停士,老公的妹妹穿的比我還像新娘挖帘。我一直安慰自己,他們只是感情好恋技,可當(dāng)我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布拇舀。 她就那樣靜靜地躺著,像睡著了一般猖任。 火紅的嫁衣襯著肌膚如雪你稚。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天朱躺,我揣著相機與錄音刁赖,去河邊找鬼。 笑死长搀,一個胖子當(dāng)著我的面吹牛宇弛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播源请,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼枪芒,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了谁尸?” 一聲冷哼從身側(cè)響起舅踪,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎良蛮,沒想到半個月后抽碌,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡决瞳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年货徙,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片皮胡。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡痴颊,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出屡贺,到底是詐尸還是另有隱情蠢棱,我是刑警寧澤锌杀,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站泻仙,受9級特大地震影響抛丽,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜饰豺,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一亿鲜、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧冤吨,春花似錦蒿柳、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至怠李,卻和暖如春圾叼,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背捺癞。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工夷蚊, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人髓介。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓惕鼓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親唐础。 傳聞我的和親對象是個殘疾皇子箱歧,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,901評論 2 345

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

  • 現(xiàn)代java開發(fā)指南 第二部分 第二部分:部署、監(jiān)控 & 管理一膨,性能分析和基準(zhǔn)測試 第一部分呀邢,第二部分 歡迎來到現(xiàn)...
    htoo閱讀 2,086評論 2 28
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)豹绪,斷路器价淌,智...
    卡卡羅2017閱讀 134,600評論 18 139
  • 看著學(xué)校的新生們在歡聲笑語中緊張著進行著軍事演練仲智,清晨6點穿梭于一群迷彩服中間,猛的總能觸及到你自己大一時候的回憶...
    Perfect丶余生閱讀 199評論 0 0
  • 2017-4-14你的自證預(yù)言 去年有一段時間姻氨,我兒子的班主任老師經(jīng)常找我钓辆,說要我花都多一些心思在兒子身上,兒子在...
    觀瀾2017閱讀 164評論 0 0
  • 十功戚、出井入合 六十五難曰∶經(jīng)言所出為井,所入為合似嗤,其法奈何啸臀?然所出為井,井者東方春也烁落,萬物之始生乘粒,故言所出為井也。...
    妙瑜的記事本閱讀 686評論 0 0