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


現代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的目錄,切換到擊剛才創建的目錄,執行:

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,初始的代碼如下:

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:

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文件,修改該文件:

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為系統屬性,方便運行時讀取。當輸入以下命令:

gradle run

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

接下來,運行一下測試:

gradle build

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

figure2

IDE

有些人說IDE會穩藏編程語言的問題。好吧,對於這個問題,我沒有意見,但是不管你使用任何語言,一個好的IDE總是有幫助的,而Java在這方面做的最好。當然在文章中選擇IDE不是重要的部分,總是要提一下,在Java世界中,有三大IDE: EclipseIntelliJ 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插件:

configurations {
    markdownDoclet
}

然后,在dependencies中添加一行:

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

最后,構建文件的最后增加這個部分:

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,函數如下:

/**
 * ## 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類:

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<String> 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函數改成下面的形式:

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表達式對字符串數組進行排序,根據數組元素的第三個字符:

Arrays.sort(array, (a, b) -> a.charAt(2) - b.charAt(2));

Java8也增加了能實現方法的接口(將這種接口換變成“traits”)。例如,FooBar接口有兩個方法,一個是抽象方法foo,另一個是有默認實現的bar。另一個useFooBar調用FooBar

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,例如:

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中:

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:

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<Integer> 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替換成原生的線程:

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)。

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<Integer> ch1 = Channels.newChannel(0);
        final Channel<String> 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表達式,最新的代碼可以寫成這樣:

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:

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函數,要讓我們的應用可容錯,代碼會變的更加復雜。

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添加以下代碼:

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

打開終端,啟動程序。程序啟動后,回到IDE,修改BadActor

@Upgrade
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 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文件夾中,使用命令:


cp build/libs/jmodern.jar modules

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

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

高級話題:可插拔類型

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

在Java8中,一個潛在最有用的新特性,是類型注解和可拔類型系統。Java編繹器現在允許在任何地方增加對類型的注解(一會我們舉個例子)。這里結合注解預處理器,打發可插拔類型系統。這些是可選的類型系統,可以關閉或打開,能給Java代碼夠增加強大的基於類型檢查的靜態驗證功能。Checker框架就這樣一個庫,它允許高級開發者寫自己的可插拔類型系統,包括繼承,類型接口等。它自己包括了幾種類型系統,如檢查可空類型,污染類型,正則表達式,物理單位類型,不可變數據等等。

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

configurations {
    checker
}

dependencies {
    checker 'org.checkerframework:jdk8:1.8.1'
    compile 'org.checkerframework:checker:1.8.1'
}

到相應的configurations,dependencies部分。

然后,增加下面部分到構建文件中:

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的空值類型系統,物理單位類型系統,污染數據類型系統。

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

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值,。

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

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的文檔,你也可以定義自己的物理單位。

最后,試試污染類型系統,它能幫你跟蹤被污染(潛在的危險)的數據,例如用戶數錄入的數據:

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM