現(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,RESTEasy和 Restlet,但最流行的應(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.java 和 src/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è) Map,Map會(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-world 和 http://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-world 和 http://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):

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

打開 http://localhost:8081/metrics,返回以下一個(gè)JSON對(duì)象:
就是這樣!配置文件也可以用來修改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 , OpenJPA 和 EclipseLink 。請(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):

下面我們會(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 , Guice , Dagger , 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)容:

為了獲得完整配置的 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 注解 getTemplate 和 getDefaultName 方法非常簡(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ā)性。因此,從Async 到 Sync 的轉(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)直接看英文版。