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

現代java開發指南 第一部分

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

第一部分第二部分第三部分html

===================java

與歷史上任何其餘的語言相比,這裏要排除c語言和cobol語言,如今愈來愈多的工做中,有用的代碼用Java語言寫出。在20年前Java首次發佈時,它引了軟件界的風暴。在那時,相對c++語言,Java語言要更簡單,更安全,並且在一段時間後,Java語言的性能也獲得了提高(這依賴於具體的使用狀況,一個大型的Java程序於相同的c++程序相比,可能會慢一點,或者同樣快,或者更快一些)。比起c++,Java犧牲很是少性能,卻提供了巨大的生產力提高。python

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

然而,最近一些年,Java獲得了很多負面的評價,特別是在互聯網初創公司中。相對於別的語言如Ruby和python,Java顯得死板,並且與配置自由的框架如Rails相比,java的網頁開發框架須要使用大量的xml文件作爲配置文件。進一步說,java在大型企業中普遍使用致使了java所採用的編程模式和作法在一個很是大的具備鮮明等級關係的技術團隊中會頗有用,可是這些編程模式和作法對於快速開發打破常規的初創公司來講,不是很合適。git

可是,Java已經改變。Java最近增長了lambda表達式和traits。以庫的形式提供了像erlang和go所支持的輕量級線程。而且最重要的是,提供了一個現代的、輕量級的方式用於取代陳舊笨重以大量xml爲基礎的方法,指導API、庫和框架的設計。程序員

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

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

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

相關文章
相關標籤/搜索