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

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

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

第一部分,第二部分

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

歡迎來到現(xiàn)代 Java 開發(fā)指南第三部分。在第一部分中,我們嘗試著編了寫現(xiàn)代Java代碼,在之后的第二部分中,探索了JVM應(yīng)用的部署,管理,監(jiān)控和測(cè)試?,F(xiàn)在,是時(shí)候研究現(xiàn)代JavaWeb開發(fā)了。還是老規(guī)矩,先回答一下讀者的問題。

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

有一些讀者對(duì) Capsule 提出意見,認(rèn)為 Capsule 沒有按 JVM 的打包標(biāo)準(zhǔn)。這不完全對(duì),因?yàn)?Capsule 是一個(gè)無狀態(tài)可執(zhí)行不用安裝的程序,因此本身來說,它就不用跟 JVM 的打包標(biāo)準(zhǔn)完全一致。如果你的應(yīng)用,要求有一些狀態(tài)(如在安裝時(shí),需要一個(gè)用戶向?qū)В?,Capsule 并不合適你。另外一部分讀者表示對(duì) Capsule 運(yùn)行時(shí)依賴 Maven 的可用性表示不放心。這于這點(diǎn)我要說,很顯然,對(duì)于軟件在可用性/關(guān)鍵性任務(wù)的范圍有每個(gè)人都有不同的觀點(diǎn),而不同的應(yīng)用在使用安全性和使用便捷性也應(yīng)該有不同的權(quán)衡。你可以創(chuàng)建一個(gè)不支持自動(dòng)升級(jí)的 Capsule,或者一個(gè)包括所有依賴的 Capsule。你還可以在啟動(dòng)時(shí)選擇 Java 運(yùn)行時(shí)和 JVM 的配置。我認(rèn)為,如果選擇使用外部 Maven 倉庫依賴,就沒有理由去懷疑外部庫意外的錯(cuò)誤或其它的問題因?yàn)樵谝蕾噯栴}在構(gòu)建過程就已經(jīng)解決。而在前一種方案中,Capsule 必須顯式說明它的依賴,并且能夠列出整個(gè)依賴庫。同樣,如果把組織內(nèi)部 Maven 倉庫用做 capsule 的依賴,那就沒有理由不把當(dāng)成運(yùn)維的服務(wù)器,確保它和其它服務(wù)器一樣保證運(yùn)行時(shí)的可用性(特別注意 Maven 倉庫軟件并不為人所知的crash )。

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

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

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

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

傳統(tǒng)的 JavaWeb 服務(wù)器,也就是典型的應(yīng)用服務(wù)器,都有一個(gè)特別的特性:支持在一個(gè) JVM 上運(yùn)行多個(gè)應(yīng)用。這個(gè)應(yīng)用服務(wù)器提供能分開應(yīng)用的運(yùn)行時(shí)環(huán)境,而且升級(jí),安裝和啟動(dòng)都是獨(dú)立的。一個(gè)應(yīng)用可能運(yùn)行在一個(gè)配置好的,已經(jīng)運(yùn)行的環(huán)境中,這種方法很多時(shí)候都工作良好,你也有理由繼續(xù)使用這種方案,但是這種方案,離“現(xiàn)化”太遠(yuǎn)了。在不同的應(yīng)用中分配不同的資源這件事是并不簡(jiǎn)單,而且在一定程度上跟現(xiàn)在使用 hypervisor 和 os 容器來運(yùn)行應(yīng)用的方案是矛盾的。因?yàn)楝F(xiàn)在針對(duì) hypervisor 和 os 容器設(shè)計(jì)的工具和技術(shù)在多應(yīng)用服務(wù)器上效率并不高,即使這些多應(yīng)用服務(wù)器只是用來運(yùn)行一個(gè)應(yīng)用,而且這些多應(yīng)用服務(wù)器的運(yùn)維也不“現(xiàn)代”:安裝配置 web 或者 app 服務(wù)器是不可缺少的,部署應(yīng)用需要很多步,每一步可能都很麻煩。

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

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

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

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

Java 與其他語言不同的一點(diǎn)是 JCP(Java Community Process)的工作,它的工作是標(biāo)準(zhǔn)化 API(即使對(duì)于不屬于語言規(guī)范或甚至標(biāo)準(zhǔn)運(yùn)行時(shí)的庫)也是如此,然后由各種商業(yè)或開源組織實(shí)現(xiàn)。這些 JSR(Java Specification Requests)是由專家組制作的,它能把一項(xiàng)技術(shù)從普遍變成成熟并成為標(biāo)準(zhǔn)。當(dāng) JSR 通過時(shí),就會(huì)非常有用,因?yàn)閹缀跛杏舷嚓P(guān)領(lǐng)域的庫都將實(shí)現(xiàn)這個(gè)標(biāo)準(zhǔn) API,這使得切換實(shí)現(xiàn)不那么痛苦。

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

相比于客戶端(每次請(qǐng)求或多或少比較獨(dú)立和能被替代),標(biāo)準(zhǔn)化對(duì)服務(wù)器應(yīng)用更重要(因?yàn)榭蚣艽a無處不在)。你可以使用三個(gè)不同的 http 客戶端和三個(gè)不同的 JDDC api 在同一個(gè)方法中,但是你的服務(wù)器通常運(yùn)行在一個(gè)框架中。出于這個(gè)原因,你應(yīng)該更喜歡標(biāo)準(zhǔn)服務(wù)器API而不是非標(biāo)準(zhǔn)服務(wù)器API,除非非標(biāo)準(zhǔn)服務(wù)器 API 為你的應(yīng)用提供了一些非常重要的優(yōu)勢(shì),或者更適合您的特定用例。單純的 API 美學(xué)不應(yīng)該傾向于支持非標(biāo)準(zhǔn)的 API。

那么輕量級(jí)的 Web 服務(wù)器最好應(yīng)該實(shí)現(xiàn)標(biāo)準(zhǔn)的 API。談到 HTT P服務(wù)時(shí),有幾個(gè)相關(guān)的 API 需要關(guān)注。第一個(gè)是古老的 Servlet API(目前是 Servlet 3.0的 JSR-315 和 Servlet 3.1的 JSR-340 )。幾乎所有的 JavaWeb 服務(wù)器都實(shí)現(xiàn)了 Servlet API,其中一些是“現(xiàn)代”的(在我們之前討論的意思),而在這里面最流行的是 Jetty。與傳統(tǒng)的 JavaWeb 服務(wù)器不同,Jetty 不是獨(dú)立的 We b應(yīng)用程序容器,而是嵌入在應(yīng)用程序中的 Web 服務(wù)庫。它就是為"現(xiàn)代"編寫的。不過傳統(tǒng)的 Web 服務(wù)器,如 Tomcat,現(xiàn)在也已經(jīng)有了嵌入式模式。因?yàn)?Servlet 是一個(gè)相對(duì)較低級(jí)別的 HTTP 服務(wù)器 API,我們不會(huì)在這里直接使用它們,所所以讓我們繼續(xù)討論下一個(gè)標(biāo)準(zhǔn) API -- JAX-RS(目前版本2.0,在JSR-339中說明)?,F(xiàn)在已經(jīng)有幾種 JAX-RS 的實(shí)現(xiàn),像 Apache CXF,RESTEasyRestlet,但最流行的應(yīng)該是 Jersey

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

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

我們現(xiàn)在將用 Dropwizard 編寫第一個(gè)現(xiàn)代 JavaWeb 服務(wù)。 如果你還沒有閱讀第一部分,我建議你現(xiàn)在就回頭看一下,這樣能熟悉一下 Gradle 的基本用法,因?yàn)槲覀儗⑹褂?Gradle 做為構(gòu)建工具。

我們將創(chuàng)建一個(gè)新的 jmodern-web 目錄,cd 進(jìn)入該目錄,輸入 gradle init --type java-library 創(chuàng)建一個(gè) Gradle項(xiàng)目,刪除文件(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;
        }
    }
}

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

現(xiàn)在讓我們用 Dropwizard 的其它特性改進(jìn)我們的服務(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; }
    }
}

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

template: Hello, %s!
defaultName: Stranger

然后,增加以下代碼到 build.gradle,這是為了在運(yùn)行代碼時(shí),能找到配置文件:

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

最后,我們希望在 capsule 中默認(rèn)包含配置文件,因此我們將添加以下部分:

from { sourceSets.main.resources }

同時(shí),也把 System-Properties 進(jìn)行調(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 啟動(dòng)服務(wù)器。 您現(xiàn)在可以在 http://localhost:8080/hello-worldhttp://localhost:8080/hello-world?name=Modern+Developer 測(cè)試服務(wù)。

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

template: Howdy, %s!
defaultName: fella

使用這個(gè)配置文件,覆蓋 dropwizard.config 屬性:

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

我們可以啟動(dòng) VisualVM(請(qǐng)參閱第2部分),并查看應(yīng)用服務(wù)報(bào)告,特別的,我們應(yīng)用的時(shí)間花費(fèi):

1

我們打開 Dropwizard 管理控制臺(tái):

2

打開 http://localhost:8081/metrics,返回以下一個(gè)JSON對(duì)象:

3

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

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

Dropwizard具有極好的開箱即用體驗(yàn),但更高級(jí)的用戶可能會(huì)發(fā)現(xiàn)它也有一些限制(例如,Dropwizard 的某些組件很難被其他組件替代:比如日志引擎)。這些用戶可能會(huì)發(fā)現(xiàn)將 Jersey, Jetty 和其他庫進(jìn)行組裝是非常有必要的,并且可以自己制定管道,以構(gòu)建一個(gè)最適合其組織的輕量級(jí)服務(wù)器。這樣做應(yīng)該不需要很多工作,而且只需要一次就可以適用所有自己的項(xiàng)目。Dropwizard 是一個(gè)很好的起點(diǎn),如果它適合你(它應(yīng)該在大多數(shù)情況下),你可以放心地堅(jiān)持使用下去。在這篇文章中的大部分示例中我們使用 Dropwizard,但是示例中所做的你都可以單獨(dú)使用Jetty,或者與Jersey結(jié)合使用來完成。而在 Dropwizard,更改配置和自動(dòng)監(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; }

我們將實(shí)例化客戶端,并注冊(cè)一個(gè)新服務(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 對(duì)像是如何反序列化成 Saying 對(duì)象的;它也可以是 Map,string 以及其他類型(Dropwizard使用的是Jersey JAX-RS客戶端的舊版本,新的API類似)。而且由于 Dropwizard 開箱即用地支持 Jersey JAX-RS 客戶端,因此會(huì)自動(dòng)發(fā)持請(qǐng)求的性能指標(biāo)。

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

所以 JAX-RS 標(biāo)準(zhǔn)也標(biāo)準(zhǔn)化了客戶端的 API。但是,正如我們之前所說的,當(dāng)談到客戶端 API 時(shí),我們也可以使用非標(biāo)準(zhǔn)的API。一個(gè)頗受歡迎的HTTP客戶端 Retrofit 是由 Square 提供的。如你所見,JAX-RS 客戶端可以自動(dòng)將 Java 對(duì)象序列化并反序列化為 JSON 對(duì)象(或 XML)。Retrofit 把這種轉(zhuǎn)化用在 Java/REST 轉(zhuǎn)換上(這種轉(zhuǎn)換并不總是一件好事;領(lǐng)域模型的轉(zhuǎn)換通常具有抽象漏洞,但如果你只限于用于簡(jiǎn)單的協(xié)議,應(yīng)該會(huì)很有幫助),包括服務(wù) URL,而不僅僅是 JSON 到 Java 接口的轉(zhuǎn)換。不幸的是,Retrofit 使用與 JAX-RS(服務(wù)器)相同的注解名稱。因此我們要在不同的包中定義,這會(huì)使我們的示例有點(diǎn)難看。幸運(yùn)的是,Retrofit 有 Netflix 提供的稱為 Feign 的克隆/衍生產(chǎn)品。Feign 和 Retrofit 之間的差異并對(duì)我來說并不完全清楚。盡管看起來 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)在我們的消費(fèi)服務(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 類中;不需要?jiǎng)?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 為請(qǐng)求。實(shí)際執(zhí)行轉(zhuǎn)換是由 Feign(或Retrofit)自動(dòng)完成的。

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

如果想看到更復(fù)雜的 REST API 是如何轉(zhuǎn)換為Java代碼的, 這個(gè)簡(jiǎn)單的例子演示使用 Retroift 消費(fèi) GitHub 的API,還有這里使用 Feign。 Retrofit 和 Feign 功能都非常豐富,可以很好地控制請(qǐng)求的轉(zhuǎn)換和執(zhí)行方式。此時(shí),我會(huì)推薦 Retroift 而不是 Feign,因?yàn)?Retrofit 更成熟,它利用了高效的 NIO 網(wǎng)絡(luò) API,而 Feign使用慢速的 HttpURLConnection API( 更好的傳輸機(jī)制可以添加進(jìn) 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 是一個(gè)非常低級(jí)的 API,有時(shí)可能會(huì)令人厭煩。Java 還有一個(gè)標(biāo)準(zhǔn)的高級(jí)數(shù)據(jù)庫訪問 API - 實(shí)際上是一個(gè)ORM--被 JSR-220 和 JSR-317 叫做 JPA(Java Persistance API)。JPA的知名實(shí)現(xiàn)包括 Hibernate , OpenJPAEclipseLink 。請(qǐng)不要使用他們,我相信以后你會(huì)感謝我。并不是說他們工作的不好,是因?yàn)樗麄兺人麄兊膸砺闊┍葍r(jià)值更多。ORM 鼓勵(lì)復(fù)雜的對(duì)象圖和復(fù)雜的模式,這往往會(huì)導(dǎo)致生成非常復(fù)雜的 SQL 語句,這些語句很難優(yōu)化。另外,ORM 并不以其出色的性能而聞名。

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

我們將從 JDBI 開始,這也由 Dropwizrd 直接支持。要有效地使用 JDBI ,你需要權(quán)衡最佳模式和簡(jiǎn)單代碼,直到您達(dá)到一個(gè)很好的中間地帶(JDBI 對(duì)于非常復(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.*;

然后,我們?cè)黾?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();
        }
    }
}

對(duì)于那些了解 JDBC 的人,這些代碼有很多熟悉的和不同的地方。JDBI 有一個(gè)流暢的接口,并且方法返回Java集合,并將其自動(dòng)地序列化為 JSON 對(duì)象??傊?,這就像一個(gè)有趣的"現(xiàn)代"JDBC。

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

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

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

這是我們的對(duì)象:

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 注釋將確保這個(gè)屬性自動(dòng)將它 JSON 序列化,但為了使 JDBI 能夠與 Something 一起工作,我們還需要?jiǎng)?chuàng)建一個(gè) ResultSetMapper ,它 將JDBC ResultSet 轉(zhuǎn)換為Something 對(duì)象:

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 對(duì)象 ) - JDBI SQL 對(duì)象是數(shù)據(jù)庫就像 Retrofit 對(duì)于 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并不是一個(gè)完整的 ORM 解決方案:它不會(huì)自動(dòng)生成 SQL 語句,也不會(huì)自動(dòng)生成完整的對(duì)象圖,但它確使我們獲得了數(shù)據(jù)庫訪問的快捷方法,其量級(jí)遠(yuǎn)低于任何 JPA 實(shí)現(xiàn)。

使用 JDBI 時(shí),Dropwizard 會(huì)自動(dòng)添加一個(gè)運(yùn)行狀況檢查( http://localhost:8081/healthcheck),用于測(cè)試數(shù)據(jù)庫的連通性,并用監(jiān)控 DAO 的性能指標(biāo):

3

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

我們將添加這個(gè)依賴關(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 方法中,注冊(cè)數(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 還沒有實(shí)現(xiàn) DDL(像 create table 這樣的 SQL 語句),所以你會(huì)注意到我們使用 JDBC 創(chuàng)建表。不過這也很好,因?yàn)?jOOQ 是作為一個(gè)JDBC包裝器實(shí)現(xiàn)的,無論如何都需要 JDBC(我還沒有能使add的正確工作的方法(可能是因?yàn)樽詣?dòng)生成的主鍵的原因)jOOQ 的開發(fā)人員:如果你正在閱讀這個(gè),請(qǐng)幫幫忙)。

這個(gè)例子實(shí)際上并沒有正確的使用 JOOQ 正義,因?yàn)樗淖畲髢?yōu)點(diǎn)是能夠從數(shù)據(jù)庫的 scheme 生成 class,并且能夠以類型安全的方式執(zhí)行我們之前完成的所有操作 - 以及更復(fù)雜的操作。對(duì)我個(gè)人來說,JOOQ 有點(diǎn)太智能了,但是如果你的模式很復(fù)雜,它可能是一個(gè)非常有用的工具。

依賴注入

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

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

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

為了保持整潔,我們只留下 HelloWorldResource 。不過,這一次,我們不手動(dòng)創(chuàng)建服務(wù)并將配置對(duì)象傳遞給它,而是使用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);
    }
}

請(qǐng)注意@Inject@Named 注釋。這些是 JSR-330 標(biāo)準(zhǔn)的一部分,所以無論我們使用哪種 DI 工具,我們的服務(wù)代碼都將保持不變。要實(shí)際連接并注入依賴關(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 最有用的功能之一是它在編譯時(shí)使用注釋處理器驗(yàn)證所有依賴關(guān)系是否滿足。例如,如果我們忘記定義 provideDefaultName ,那么當(dāng)我們鍵入時(shí),這就 是NetBeans 中顯示的內(nèi)容:

5

為了獲得完整配置的 HelloWorldResource 實(shí)例,我們?cè)趹?yīng)用程序的 run 方法中放入了這個(gè)實(shí)例:

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

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

高級(jí)主題:阻塞與非阻塞 VS 同步與異步

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

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

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

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

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

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

interface Sync {
    Object pull();
}

和:

interface Async {
    void push(Callback cb);
}

interface Callback {
    void got(Object obj);
}

現(xiàn)在讓我們使用 Sync 實(shí)現(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)在,用您最喜歡的編程語言實(shí)現(xiàn)相反的功能,即將 Async 轉(zhuǎn)換為 Sync 。這將更加棘手,總是需要引入一些中間數(shù)據(jù)存儲(chǔ),如隊(duì)列或緩沖區(qū)。當(dāng)然,你需要考慮到 Callback.got 可以在任何線程上調(diào)用,所以你需要考慮與該數(shù)據(jù)結(jié)構(gòu)的并發(fā)性。因此,從AsyncSync 的轉(zhuǎn)換不僅不那么簡(jiǎn)單,而且引入了不必要的數(shù)據(jù)存儲(chǔ):如果真沒有引入多余的數(shù)據(jù)存儲(chǔ),是因?yàn)樗赡芤呀?jīng)內(nèi)置到系統(tǒng)中(例如以IO緩沖區(qū)的形式)。所以 Async 使用 Sync 簡(jiǎn)單的實(shí)現(xiàn),但是相反的轉(zhuǎn)換既浪費(fèi)又浪費(fèi)時(shí)間,并且需要管理并發(fā)。但這對(duì)限制或管理副作用的語言(如 Clojure 或 Haskell )來說不是什么問題。

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

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

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

結(jié)論

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

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


水平有限,如果看不懂請(qǐng)直接看英文版。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容