[翻譯]現代java開發指南 第一部分

htoooth發表於2016-04-21

現代java開發指南 第一部分

第一部分:Java已不是你父親那一代的樣子

與歷史上任何其他的語言相比,這裡要排除c語言和cobol語言,現在越來越多的工作中,有用的程式碼用Java語言寫出。在20年前Java首次釋出時,它引了軟體界的風暴。在那時,相對c++語言,Java語言要更簡單,更安全,而且在一段時間後,Java語言的效能也得到了提升(這依賴於具體的使用情況,一個大型的Java程式於相同的c++程式相比,可能會慢一點,或者一樣快,或者更快一些)。比起c++,Java犧牲非常少效能,卻提供了巨大的生產力提升。

Java是一門blue-collar language,程式設計師值得信賴的工具,它只會採用已經被別的語言嘗試過的正確的理念,同時增加新的特性只會去解決主要的痛點問題。Java是否一直忠於它的使命是一個開放性的問題,但它確實是努力讓自已的道路不被當前的時尚所左右太遠。在智慧晶片,嵌入式裝置和大型主機上,java都在用於編寫程式碼。甚至被用來編寫對任務和安全要求苛刻的硬體實時軟體。

然而,最近一些年,Java得到了不少負面的評價,特別是在網際網路初創公司中。相對於別的語言如Ruby和python,Java顯得死板,而且與配置自由的框架如Rails相比,java的網頁開發框架需要使用大量的xml檔案做為配置檔案。進一步說,java在大型企業中廣泛使用導致了java所採用的程式設計模式和做法在一個非常大的具有鮮明等級關係的技術團隊中會很有用,但是這些程式設計模式和做法對於快速開發打破常規的初創公司來說,不是很合適。

但是,Java已經改變。Java最近增加了lambda表示式和traits。以庫的形式提供了像erlang和go所支援的輕量級執行緒。並且最重要的是,提供了一個現代的、輕量級的方式用於取代陳舊笨重以大量xml為基礎的方法,指導API、庫和框架的設計。

最近一些年,Java生態圈發生了一些有趣的事:大量的以jvm為基礎的程式語言變得流行;其中一些語言設計的十分好(我個人喜歡Clojure和Kotlin)。但是與這些可行或者推薦的語言相比,Java與其它基於JVM的語言來說,確實有幾個優點:熟悉,技持,成熟,和社群。通過新代工具和新代的庫,Java實際上在這幾個方面做了很多的工作。因此,許多的矽谷初創公司,一但他們成長壯大後,就會回到Java,或者至少是回到JVM上,這點就不會另人驚奇了。

這份介紹性指南的目標是想學習如何寫現代精簡Java程式碼的程式設計師(900萬),或者是那些聽到了或體驗過Java壞的方面的Python/Ruby/Javascript程式設計師。並且指南展示了Java中已經改變的方面和這些改變的方面如何讓Java獲得另人讚歎的效能,靈活性和可監控性而不會犧牲太多的Java沉穩方面。

JVM

對Java術語簡單價紹一下,Java在概念上被分為三個部分:Java,Java執行時庫和Java虛擬機器,或者叫JVM。如果你熟悉Node.js,Java語言類同於JavaScript,執行時庫類同於Node.js,JVM類同於V8引擎。JVM和執行時庫被打包成大家所熟知的Java執行時環境,或者叫JRE(雖然常常人們說JVM實際上指的是JRE)。Java開發工具,JDK,是指某一個JRE的發行版,通常包括很多開發工具像java編繹器javac,還有很多程式監控和效能分析工具。JRE通常有幾個分支,如支援嵌入式裝置開發版本,但是本部落格中,我們只會涉及到JRE支援伺服器(桌面)開發的版本,這就是眾所周知的 JavaSE(Java標準版)。

有一些專案實現了JVM和JRE的標準,其中一些是開源的專案,還有一些是商業專案。有些JVM非常特殊,如有些JVM執行硬體實時嵌入式裝置軟體,還有JVM可以在巨大的記憶體上執行軟體。但是我們將會使用HotSpot,一個由Oracle支援的的自由,通用的JVM實現,同時HotSpot也是開源OpenJDK專案的一部分。

Java構建JVM,JVM同時執行Java(雖然JVM最近為了其它語言做了一些專門的修改)。但是什麼是JVM,Cliff Click的這個演講解釋了什麼是JVM,簡單來說,JVM是一臺抽象現實的魔法機器。JVM使用漂亮,簡單和有用的抽象,好像無限的記憶體和多型,這些聽起來實現代價很高,並且實現這些特徵用如此高效的形式以致於他們能很容易能與沒有提供這些有用抽象的執行時競爭。更需要說明的是,JVM擁有最好記憶體回收演算法並能在大範圍的產品中使用,JVM的JIT允許內聯和優化虛方法的呼叫(這是許多語言中最有用的抽像的核心),在儲存虛方法的用處的同時,使呼叫虛方法非常方便和快捷。JVM的JIT(即時編繹器)是基礎的高階效能優化編繹器,和你的應用一起執行。

當然JVM也隱藏了很多的作業系統級別的細節,如記憶體模型(程式碼在不同的CPU上執行怎樣看待其它的CPU操作引起的變數的狀態的變化)和使用定時器。JVM還提供執行時動態連結,熱程式碼交換,監控幾乎所有在JVM上執行的程式碼,還有庫中的程式碼。

這並不是說JVM是完美的。當前Java的陣列缺失存放複雜結構體的能力(計劃將在Java9中解決),還有適當的尾呼叫優化。儘管JVM有這樣的問題,但是JVM的成熟,測試良好,快速,靈活,還有豐富的執行時分析和監控,讓我不會考慮執行一個關鍵重要的伺服器程式在別的任何基礎之上(除了JVM別無選擇)。

理論已經足夠了。在我們深入講解之前,你應該下載在這裡下載最新的JDK,或者使用你係統自帶的包管理器安裝最新的OpenJDK。

構建

讓我們開啟現代Java構建工具旅程。在很長的一段歷史時間內,Java出現過幾個構建工具,如Ant和Maven,他們大多數都基於XML。但是現代的Java開發者使用Gradle(最近成為Android的官方構建工具)。Gradle是一個成熟,深入開發,現代Java構建工具,它使用了在Groovy基礎上的DSL語言來說明構建過程。他整合了Maven的簡單性和Ant的強大性和靈活性,同時拋棄所有的XML。但是Gradle並不是沒有錯誤:當他使最通用的部分簡單和可宣告式的同時,就會有很多事情變得非常不通用,這就要求返回來使用命令式的Groovy。

現在讓我們使用Gradle建立一個新的Java專案。首先,我們從這裡下載Gradle,安裝。現在我們開始建立專案,專案名叫JModern。建立一個叫Jmodern的目錄,切換到擊剛才建立的目錄,執行:

bash gradle init --type java-library Gradle 建立了專案的初始資料夾結構,包括子類(Library.javaLibraryTest.java),我們將在後面刪除這兩個檔案:

figure1

程式碼在src/main/java/目錄下,測試程式碼在src/test/java目錄下。我們將主類命名為jmodern.Main(所以主類的原始檔就在src/main/java/jmodern/Main.java),這個程式將會把Hello World程式做一點小小的變化。同時為了使用Gradle更方便,將會使用Google's Guava。使用你喜歡的編輯器建立src/main/java/jmodern/Main.java,初始的程式碼如下:

```java package jmodern;

import com.google.common.base.Strings;

public class Main { public static void main(String[] args) { System.out.println(triple("Hello World!")); System.out.println("My name is " + System.getProperty("jmodern.name")); }

static String triple(String str) {
    return Strings.repeat(str, 3);
}

} ```

相應建立一個小的測試用例:在src/test/java/jmodern/MainTest.java:

```java package jmodern;

import static org.hamcrest.CoreMatchers.; import static org.junit.Assert.; import org.junit.Test;

public class MainTest { @Test public void testTriple() { assertThat(Main.triple("AB"), equalTo("ABABAB")); } } ```

在專案根目錄,找到build.gradle檔案,修改該檔案:

```groovy apply plugin: 'java' apply plugin: 'application'

sourceCompatibility = '1.8'

mainClassName = 'jmodern.Main'

repositories { mavenCentral() }

dependencies { compile 'com.google.guava:guava:17.0'

testCompile 'junit:junit:4.11' // A dependency for a test framework.

}

run { systemProperty 'jmodern.name', 'Jack' }

```

構建程式設定jmoder.Main為主類,宣告Guava為該程式的依賴庫,並且jmodern.name為系統屬性,方便執行時讀取。當輸入以下命令:

bash gradle run

Gradle會從Maven中心倉庫下載Guava,編繹程式,然後執行程式,把jmodern.name設定成"Jack"。總的過程就是這樣。

接下來,執行一下測試:

bash gradle build

生成的測試報告在build/reports/tests/index.html

figure2

IDE

有些人說IDE會穩藏程式語言的問題。好吧,對於這個問題,我沒有意見,但是不管你使用任何語言,一個好的IDE總是有幫助的,而Java在這方面做的最好。當然在文章中選擇IDE不是重要的部分,總是要提一下,在Java世界中,有三大IDE: (Eclipse)[https://www.eclipse.org/],IntelliJ IDEA,和NetBeans,你應該以後使用一下後兩者。IntelliJ可能是三者之中最強大的IDE,而NetBeans應該是最符合程式設計師直覺和最易於使用(我認為也最好看)的IDE。NetBeans通過Gradle的外掛對Gradle有最好的支援。Eclipse是最受歡迎的IDE。我在很多年前感覺Eclipse變得混亂,就不使用Eclipse了。當然如果你是一個長期使用Eclipse的使用者,也沒有什麼問題。

安裝完Gradle外掛,我們的小專案在NetBeans中的樣子如下:

figure3

我最喜歡NetBeans的Gradle外掛功能不僅是因為IDE列出了所有有關專案的依賴,還有其它的配置外掛也能列出,所以我們只需要在構建檔案中宣告他們一次。如果你在專案中增加新的依賴庫,在NetBeans中右鍵單擊專案,選擇Reload Project,然後IDE將下載你新增加的依賴庫。如果你右鍵單擊Dependencies結點,選擇Download Sources,IDE會下載依賴庫的原始碼和相關javadoc,這樣你就可以除錯第三方庫的程式碼,還能檢視第三方庫的文件。

用Markdown編寫文件

長期以來,Java通過Javadoc生成很好的API文件,而且Java開發者也習慣寫Javadoc形式的註釋。但是現代的Java開發者喜歡使用Markdown,喜歡使用Markdown為Javadoc增加點樂趣。為了達在Javadoc使用Markdown,我們在構建檔案中dependencies部分的前面,增加Pegdown DocletJavadoc外掛:

groovy configurations { markdownDoclet }

然後,在dependencies中新增一行:

groovy markdownDoclet 'ch.raffael.pegdown-doclet:pegdown-doclet:1.1.1'

最後,構建檔案的最後增加這個部分:

groovy javadoc.options { docletpath = configurations.markdownDoclet.files.asType(List) // gradle should relly make this simpler doclet = "ch.raffael.doclets.pegdown.PegdownDoclet" addStringOption("parse-timeout", "10") }

終於,可以在Javadoc註釋使用Markdown,還有語法高亮。

你可能會想關掉你的IDE的註釋格式化功能(在Netbeans: Preferences -> Editor -> Formatting, choose Java and Comments, and uncheck Enable Comments Formatting)。IntelliJ 有一個外掛能高亮在Javadoc中的Markdown語法。

為了測試新增的設定,我們給方法randomString增加Markdown格式的javadoc,函式如下:

java /** * ## The Random String Generator * * This method doesn't do much, except for generating a random string. It: * * * Generates a random string at a given length, `length` * * Uses only characters in the range given by `from` and `to`. * * Example: * *java * randomString(new Random(), 'a', 'z', 10); * * * @param r the random number generator * @param from the first character in the character range, inclusive * @param to the last character in the character range, inclusive * @param length the length of the generated string * @return the generated string of length `length` */ public static String randomString(Random r, char from, char to, int length) ...

然後使用命令gradle javadocbuild/docs/javadoc/生成html格式文件:

figure4

一般我不常用這個功能,因為IDE對這個功能的語法高亮支援的不太好。但是當你需要在文件中寫例子時,這個功能能讓你的工作變得更輕鬆。

用Java8寫簡潔的程式碼

最近釋出的Java8給Java語言帶來了很大的改變,因為java原生支援lambda表示式。lambda表示式解決了一個重大的問題,在過去人們解決做一些簡單事卻寫不合理的冗長的程式碼。為了展示lambda有多大的幫助,我拿出我能想到的令人很惱火的,簡單的資料操作程式碼,並把這段程式碼改用Java8寫出。這個例子產生了一個list,裡面包含了隨機生成的學生名字,然後進行按他們的頭字母進行分組,並以美觀的形式列印出來。現在,修改Main類:

```java package jmodern;

import java.util.List; import java.util.Map; import java.util.Random; import static java.util.stream.Collectors.*; import static java.util.stream.IntStream.range;

public class Main { public static void main(String[] args) { // generate a list of 100 random names List students = range(0, 100).mapToObj(i -> randomString(new Random(), 'A', 'Z', 10)).collect(toList());

    // sort names and group by the first letter
    Map<Character, List<String>> directory = students.stream().sorted().collect(groupingBy(name -> name.charAt(0)));

    // print a nicely-formatted student directory
    directory.forEach((letter, names) -> System.out.println(letter + "\n\t" + names.stream().collect(joining("\n\t"))));
}

public static String randomString(Random r, char from, char to, int length) {
    return r.ints(from, to + 1).limit(length).mapToObj(x -> Character.toString((char)x)).collect(Collectors.joining());
}

} ```

Java自動推導了所有lambda的引數型別,Java確保了引數是型別安全的,並且如果你使用IDE,IDE中的自動完成和重構功能對這些引數都可以用的。Java不會像c++使用auto和c#中的var還有Go一樣,自動推導區域性變數,因為這樣會讓程式碼的可讀性降低。但是這並不意味著要需要手動輸入這些型別。例如,游標在students.stream().sorted().collect(Collectors.groupingBy(name -> name.charAt(0)))這一行程式碼上,在NetBeans中按下Alt+Enter,IDE會推匯出結果適當的型別(這裡是Map<Character, String>)。

如果想感覺一下函數語言程式設計的風格,將main函式改成下面的形式:

java public static void main(String[] args) { range(0, 100) .mapToObj(i -> randomString(new Random(), 'A', 'Z', 10)) .sorted() .collect(groupingBy(name -> name.charAt(0))) .forEach((letter, names) -> System.out.println(letter + "\n\t" + names.stream().collect(joining("\n\t")))); } 跟以前的程式碼確實不一樣(看哪,沒有型別),但是這應該不太容易理解這段程式碼的意思。

就算Java有lambda,但是Java仍然沒有函式型別。其實,lambda在java中被轉換成近似為functional介面,即有一個抽象方法的介面。這種自動轉換使遺留程式碼能夠和lambda在一起很好的工作。例如:Arrays.sort方法是需要一個Comparateor介面的例項,這個介面簡單描述成單一的揭抽象 int compare(T o1, T o2)方法。在java8中,可以使用lambda表示式對字串陣列進行排序,根據陣列元素的第三個字元:

java Arrays.sort(array, (a, b) -> a.charAt(2) - b.charAt(2)); Java8也增加了能實現方法的介面(將這種介面換變成“traits”)。例如,FooBar介面有兩個方法,一個是抽象方法foo,另一個是有預設實現的bar。另一個useFooBar呼叫FooBar

```java interface FooBar { int foo(int x); default boolean bar(int x) { return true; } }

int useFooBar(int x, FooBar fb) { return fb.bar(x) ? fb.foo(x) : -1; } ```

雖然FooBar有兩個方法,但是隻有一個foo是抽象的,所以FooBar也是一個函式介面,並且可以使用lambda表示式建立FooBar,例如:

java useFooBar(3, x -> x * x)

將會返回9。

通過Fibers實現輕量級併發控制

有許多人和我一樣,都對併發資料結構感興趣,而這一塊是JVM的後花園。一方面,JVM對於CPU的併發原語提供了低階方法如CAS結構和記憶體柵欄,另一方面結合記憶體回收機制提供了平臺中立的記憶體模型。但是,對那些使用併發控制的程式設計師來說,並不是為了擴充套件他們的軟體,而使用併發控制,而是他們不得不使用併發控制使自己的軟體可擴充套件。從這方面說,Java併發控制並不是很好,是有問題。

真的,Java從開始就被設計成為併發控制,並且在每一個版本中都強調他的併發控制資料結構。Java已經高質量的實現了很多非常有用的併發資料結構(如併發HashMap,併發SkipListMap,併發LinkedQueue),有些都沒有在Erlang和Go中實現。Java的併發控制通常領先c++5年或者更長的時間。但是你會發現正確高效地使用這些併發控制資料結構非常困難。當我們使用執行緒和鎖時,剛開始你會發現它們工作的很好,到了後面當你需要更多併發控制時,發現這些方法不能很好的擴充套件。然後我們使用執行緒池和事件,這兩個東西有很好的擴充套件性,但是你會發現很難去解釋共享變數,特別是在語言級別沒有對共享變數的可變性進行限制。進一步說,如果你的問題是核心級執行緒不能很好的擴充套件,那麼對事件的非同步處理是一個壞想法。為什麼不簡單修復執行緒的問題呢?這恰恰是Erlang和Go所採用的方式:輕量級的使用者執行緒。輕量級使用者執行緒通過簡單,阻塞式的程式設計方法高效使用同步結構,將核心級的併發控制對映到程式級的併發控制,而不用犧牲可擴充套件性,同時比鎖和訊號更簡單。

Quasar是一個我們建立的開源庫,它給JVM增加了真正的輕量級執行緒(在Quasar叫纖程),同得能夠很好的同系統級執行緒很好在一起的工作。Quasar同Go的CSP一樣,同時有一個基結Erlang的Actor系統。對付併發控制,纖程是一個很好的選擇。纖程簡單、優美和高效。現在讓我們來看看它:

首先,我們設定構建指令碼,新增以下的程式碼在build.gradle中:

```groovy configurations { quasar }

dependencies { compile "co.paralleluniverse:quasar-core:0.5.0:jdk8" quasar "co.paralleluniverse:quasar-core:0.5.0:jdk8" }

run { jvmArgs "-javaagent:${configurations.quasar.iterator().next()}" // gradle should make this simpler, too } ```

更新依賴,編輯Main.java:

```java package jmodern;

import co.paralleluniverse.fibers.Fiber; import co.paralleluniverse.strands.Strand; import co.paralleluniverse.strands.channels.Channel; import co.paralleluniverse.strands.channels.Channels;

public class Main { public static void main(String[] args) throws Exception { final Channel ch = Channels.newChannel(0);

    new Fiber<Void>(() -> {
        for (int i = 0; i < 10; i++) {
            Strand.sleep(100);
            ch.send(i);
        }
        ch.close();
    }).start();

    new Fiber<Void>(() -> {
        Integer x;
        while((x = ch.receive()) != null)
            System.out.println("--> " + x);
    }).start().join(); // join waits for this fiber to finish
}

} ```

現在有通過channel,有兩個纖程可以進行通訊。

Strand.sleep,和Strand類的所有方法,在原生Java執行緒和fiber中都能很好的執行。現在我們將第一個fiber替換成原生的執行緒:

java new Thread(Strand.toRunnable(() -> { for (int i = 0; i < 10; i++) { Strand.sleep(100); ch.send(i); } ch.close(); })).start();

這也執行的很好(當然我們已在我們的應用中執行百萬級的fiber,也用了幾千執行緒)。

我們處一下channel selection (模擬Go的select)。

```java package jmodern;

import co.paralleluniverse.fibers.Fiber; import co.paralleluniverse.strands.Strand; import co.paralleluniverse.strands.channels.Channel; import co.paralleluniverse.strands.channels.Channels; import co.paralleluniverse.strands.channels.SelectAction; import static co.paralleluniverse.strands.channels.Selector.*;

public class Main { public static void main(String[] args) throws Exception { final Channel ch1 = Channels.newChannel(0); final Channel ch2 = Channels.newChannel(0);

    new Fiber<Void>(() -> {
        for (int i = 0; i < 10; i++) {
            Strand.sleep(100);
            ch1.send(i);
        }
        ch1.close();
    }).start();

    new Fiber<Void>(() -> {
        for (int i = 0; i < 10; i++) {
            Strand.sleep(130);
            ch2.send(Character.toString((char)('a' + i)));
        }
        ch2.close();
    }).start();

    new Fiber<Void>(() -> {
        for (int i = 0; i < 10; i++) {
            SelectAction<Object> sa
                    = select(receive(ch1),
                            receive(ch2));
            switch (sa.index()) {
                case 0:
                    System.out.println(sa.message() != null ? "Got a number: " + (int) sa.message() : "ch1 closed");
                    break;
                case 1:
                    System.out.println(sa.message() != null ? "Got a string: " + (String) sa.message() : "ch2 closed");
                    break;
            }
        }
    }).start().join(); // join waits for this fiber to finish
}

} ```

從Quasar 0.6.0開始,可以在選擇狀態中使用使用lambda表示式,最新的程式碼可以寫成這樣:

java for (int i = 0; i < 10; i++) { select( receive(ch1, x -> System.out.println(x != null ? "Got a number: " + x : "ch1 closed")), receive(ch2, x -> System.out.println(x != null ? "Got a string: " + x : "ch2 closed"))); }

看看fiber的高效能io:

```java package jmodern;

import co.paralleluniverse.fibers.; import co.paralleluniverse.fibers.io.; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.; import java.nio.charset.;

public class Main { static final int PORT = 1234; static final Charset charset = Charset.forName("UTF-8");

public static void main(String[] args) throws Exception {
    new Fiber(() -> {
        try {
            System.out.println("Starting server");
            FiberServerSocketChannel socket = FiberServerSocketChannel.open().bind(new InetSocketAddress(PORT));
            for (;;) {
                FiberSocketChannel ch = socket.accept();
                new Fiber(() -> {
                    try {
                        ByteBuffer buf = ByteBuffer.allocateDirect(1024);
                        int n = ch.read(buf);
                        String response = "HTTP/1.0 200 OK\r\nDate: Fri, 31 Dec 1999 23:59:59 GMT\r\n"
                                        + "Content-Type: text/html\r\nContent-Length: 0\r\n\r\n";
                        n = ch.write(charset.newEncoder().encode(CharBuffer.wrap(response)));
                        ch.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }).start();
    System.out.println("started");
    Thread.sleep(Long.MAX_VALUE);
}

} ```

我們做了什麼?首先我們啟動了一個一直迴圈的fiber,用於接收TCP連線。對於每一個連線上的連線,這個fiber會啟動另外一個fiber去讀請求,傳送回應,然後關閉。這段程式碼是阻塞IO的,在後臺使用非同步EPoll IO,所以它和非同步IO伺服器,有一樣的擴充套件性。(我們將在Quasar中極大的提高IO效能)。

可容錯的Actor和熱程式碼的更換

Actor模型,受歡迎是有一半原因是Erlang,意圖是編寫可容錯,高可維護的應用。它將應用分割成獨立可容錯的容器單元-Actors,標準化處理錯誤中恢復方式。

當我們開始Actor,將compile "co.paralleluniverse:quasar-actors:0.5.0" 加到你的構建指令碼中的依賴中去。

我們重寫Main函式,要讓我們的應用可容錯,程式碼會變的更加複雜。

```java package jmodern;

import co.paralleluniverse.actors.; import co.paralleluniverse.fibers.; import co.paralleluniverse.strands.Strand; import java.util.Objects; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit;

public class Main { public static void main(String[] args) throws Exception { new NaiveActor("naive").spawn(); Strand.sleep(Long.MAX_VALUE); }

static class BadActor extends BasicActor<String, Void> {
    private int count;

    @Override
    protected Void doRun() throws InterruptedException, SuspendExecution {
        System.out.println("(re)starting actor");
        for (;;) {
            String m = receive(300, TimeUnit.MILLISECONDS);
            if (m != null)
                System.out.println("Got a message: " + m);
            System.out.println("I am but a lowly actor that sometimes fails: - " + (count++));

            if (ThreadLocalRandom.current().nextInt(30) == 0)
                throw new RuntimeException("darn");

            checkCodeSwap(); // this is a convenient time for a code swap
        }
    }
}

static class NaiveActor extends BasicActor<Void, Void> {
    private ActorRef<String> myBadActor;

    public NaiveActor(String name) {
        super(name);
    }

    @Override
    protected Void doRun() throws InterruptedException, SuspendExecution {
        spawnBadActor();

        int count = 0;
        for (;;) {
            receive(500, TimeUnit.MILLISECONDS);
            myBadActor.send("hi from " + self() + " number " + (count++));
        }
    }

    private void spawnBadActor() {
        myBadActor = new BadActor().spawn();
        watch(myBadActor);
    }

    @Override
    protected Void handleLifecycleMessage(LifecycleMessage m) {
        if (m instanceof ExitMessage && Objects.equals(((ExitMessage) m).getActor(), myBadActor)) {
            System.out.println("My bad actor has just died of '" + ((ExitMessage) m).getCause() + "'. Restarting.");
            spawnBadActor();
        }
        return super.handleLifecycleMessage(m);
    }
}

} ```

程式碼中有一個NaiveActor產生一個BadActor,這個產生出來的的Actor會偶然失敗。由於我們的父actor監控子Actor,當子Actor過早的死去,父actor會得到通知,然後重新啟動一個新的Actor。

這個例子,Java相當的惱人,特別是當它用instanceof測試訊息的型別和轉換訊息的型別的時候。這一方面通過模式匹配Clojure和Kotlin做的比較好(以後我會發一篇關於Kotlin的文章)。所以,是的,所有的型別檢查和型別轉換相當另人討厭。這種型別程式碼鼓勵你去試一下Kotlin,你真的該去使用一下(我就試過,我非常喜歡Kotlin,但是要用於生產環境使用它還有待成熟)。就個人來說,這種惱人非常小。

回到主要問題來。一個基於Actor的可容錯系統關鍵的元件是減少當機時間不管是由於應用的錯誤,還是由於系統維護。我們將在第二部分探索JVM的管理,接下來展示一下Actor的熱程式碼交換。

在熱程式碼交換的問題上,有幾種方法(例如:JMX,將在第二部分講)。但是現在我們通過監控檔案系統來實現。首先在專案目錄下建立一個叫modules子資料夾,在build.gradlerun新增以下程式碼:

groovy systemProperty "co.paralleluniverse.actors.moduleDir", "${rootProject.projectDir}/modules"

開啟終端,啟動程式。程式啟動後,回到IDE,修改BadActor

```java @Upgrade static class BadActor extends BasicActor { private int count;

@Override
protected Void doRun() throws InterruptedException, SuspendExecution {
    System.out.println("(re)starting actor");
    for (;;) {
        String m = receive(300, TimeUnit.MILLISECONDS);
        if (m != null)
            System.out.println("Got a message: " + m);
        System.out.println("I am a lowly, but improved, actor that still sometimes fails: - " + (count++));

        if (ThreadLocalRandom.current().nextInt(100) == 0)
            throw new RuntimeException("darn");

        checkCodeSwap(); // this is a convenient time for a code swap
    }
}

} ```

我們增加了@Upgrade註解,因為我們想讓這個類進行升級,這個類修改後失敗變少了。現在程式還在執行,新開一個終端,通過gradle jar,重新構建程式。不熟悉java程式設計師,JAR(Java Archive)用來打包Java模組(在第二部分會討論Java打包和部署)。最後,在第二個終端中,複製build/libs/jmodern.jarmodeules資料夾中,使用命令:

```bash

cp build/libs/jmodern.jar modules ```

你會看到程式更新執行了(這個時候取決於你的作業系統,大概要十秒)。注意不像我們在失敗後重新啟動BadActor,當我們交換程式碼時,程式中的中間變數儲存下來了。

設計一個基於Actor設計可容錯的系統是一個很大的主題,但是我希望你已經對它有點感覺。

高階話題:可插拔型別

結束之前,我們將探索一個危險的領域。我們接下來介紹的工具還沒有加入到現代Java開發工具箱中,因為使用它仍然很繁瑣,不過它將會從IDE融合中得到好處,現在這個工具仍然很陌生。雖然如此,如果這個工具持繼開發並且不斷充實,它帶來的可能性非常的酷,如果他不會在瘋子手中被亂用,它將會非常有價值,這就是為什麼我們把它列在這裡。

在Java8中,一個潛在最有用的新特性,是型別註解和可拔型別系統。Java編繹器現在允許在任何地方增加對型別的註解(一會我們舉個例子)。這裡結合註解前處理器,打發可插拔型別系統。這些是可選的型別系統,可以關閉或開啟,能給Java程式碼夠增加強大的基於型別檢查的靜態驗證功能。Checker框架就這樣一個庫,它允許高階開發者寫自己的可插拔型別系統,包括繼承,型別介面等。它自己包括了幾種型別系統,如檢查可空型別,汙染型別,正規表示式,物理單位型別,不可變資料等等。

Checker目前還不能很好的與IDE一起工作,所有這節,我將不使用IDE。首先修改build.gradle,增加:

```groovy configurations { checker }

dependencies { checker 'org.checkerframework:jdk8:1.8.1' compile 'org.checkerframework:checker:1.8.1' } `` 到相應的configurations,dependencies`部分。

然後,增加下面部分到構建檔案中:

groovy compileJava { options.fork = true options.forkOptions.jvmArgs = ["-Xbootclasspath/p:${configurations.checker.asPath}:${System.getenv('JAVA_HOME')}/lib/tools.jar"] options.compilerArgs = ['-processor', 'org.checkerframework.checker.nullness.NullnessChecker,org.checkerframework.checker.units.UnitsChecker,org.checkerframework.checker.tainting.TaintingChecker'] } 正如我說的,笨重的。

最後一行說明我們使用Checker的空值型別系統,物理單位型別系統,汙染資料型別系統。

現在我們做一些實驗。首先,試一下空值型別系統,他能防止空指標的錯誤。

```java package jmodern;

import org.checkerframework.checker.nullness.qual.*;

public class Main { public static void main(String[] args) { String str1 = "hi"; foo(str1); // we know str1 to be non-null

    String str2 = System.getProperty("foo");
    // foo(str2); // <-- doesn't compile as str2 may be null
    if (str2 != null)
        foo(str2); // after the null test it compiles
}

static void foo(@NonNull String s) {
    System.out.println("==> " + s.length());
}

} `` Checker的開發者很友好,註解了整個JD可空的返回型別,所以當有@NonNull註解時,從庫中返回值不要返回null`值,。

接下來,我們試一下單位型別系統,防止單位型別轉換錯誤。

```java package jmodern;

import org.checkerframework.checker.units.qual.*;

public class Main { @SuppressWarnings("unsafe") private static final @m int m = (@m int)1; // define 1 meter @SuppressWarnings("unsafe") private static final @s int s = (@s int)1; // define 1 second

public static void main(String[] args) {
    @m double meters = 5.0 * m;
    @s double seconds = 2.0 * s;
    // @kmPERh double speed = meters / seconds; // <-- doesn't compile
    @mPERs double speed = meters / seconds;

    System.out.println("Speed: " + speed);
}

} ```

非常酷吧,根據Checker的文件,你也可以定義自己的物理單位。

最後,試試汙染型別系統,它能幫你跟蹤被汙染(潛在的危險)的資料,例如使用者數錄入的資料:

```java package jmodern;

import org.checkerframework.checker.tainting.qual.*;

public class Main { public static void main(String[] args) { // process(parse(read())); // <-- doesn't compile, as process cannot accept tainted data process(parse(sanitize(read()))); }

static @Tainted String read() {
    return "12345"; // pretend we've got this from the user
}

@SuppressWarnings("tainting")
static @Untainted String sanitize(@Tainted String s) {
    if(s.length() > 10)
        throw new IllegalArgumentException("I don't wanna do that!");
    return (@Untainted String)s;
}

// doesn't change the tainted qualifier of the data
@SuppressWarnings("tainting")
static @PolyTainted int parse(@PolyTainted String s) {
    return (@PolyTainted int)Integer.parseInt(s); // apparently the JDK libraries aren't annotated with @PolyTainted
}

static void process(@Untainted int data) {
    System.out.println("--> " + data);
}

} ```

Checker通過型別介面給於Java可插拔互動型別。並且可以通過工具和預編繹庫增加型別註解。Haskell都做不到這一點。

Checker還沒有到他的黃金時段,如果使用明智的話,它會成為現代Java開發者手中強有力的工具之一。

結束

我們已經看到了Java8中的變化,還有相應現代的工具和庫,Java相對於與舊的版本來說,相似性不高。但是Java仍然是大型應用中的亮點,而且Jva和它的生態圈比新的簡單的語言,更為成熟和高效。我們瞭解現代Java程式設計師是怎樣寫程式碼的,但是我們很難一開始就解開Java和Jvm的全部力量。特別當我們知道了Java的監控和效能分析工具,和新的微應用網路應用開發框架。在接下來的文章中我們會談到這幾個話題。

假如你想了解一個開頭,第二部分,我們會討論現代Java打包方法(使用Capsule,有點像npm,但是更酷),監控和管理(使用VisualVM, JMX, JolokiaMetrics) ,效能分析(使用 Java Flight Recorder, Mission Control, 和 Byteman),基準測試(JMH)。第三部分,我們會討論用DropwizardComsatWeb Actors,JSR-330寫一個輕量級可擴充套件的HTTP服務。

原文地址:Not Your Father's Java: An Opinionated Guide to Modern Java Development, Part 1

相關文章