[翻译]现代java开发指南 第二部分

现代java开发指南 第二部分

第二部分:部署、监控 & 管理,性能分析和基准测试

第一部分第二部分

欢迎来到现代 Java 开发指南第二部分。在第一部分中,我们已经展示了有关 Java 新的语言特性,库和工具。这些新的工具使 Java 变成了相当轻量级的开发环境,这个开发环境拥有新的构建工具、更容易使用的文档、富有表现力的代码还有用户级线程的并发。而在这部分中,我们将比代码层次更高一层,讨论 Java 的运维———— Java 的部署、监控&管理,性能分析和基准测试。尽管这里的例子都会用 Java 来做示意,但是我们讨论的内容与所有的 JVM 语言都相关,而不仅仅是 Java 语言。

在开始之前,我想简短地回答一下第一部分读者的问题,并且澄清一下说的不清楚的地方。第一部分中最受争议的地方出现在构建工具这一节。在那一节中,我写到现代的 Java 开发者使用 Gradle。有些读者对此提出异议,并且举出了例子来证明 Maven 同样也是一个很好的工具。我个人喜欢 Gradle 漂亮 DSL 和能使用指令式代码来编写非通用的构建操作,同时我也能够理解喜欢完全声明式的 Maven 的偏好,即使这样做需要大量的插件。因此,我承认:现代的 Java 开发者可能更喜欢 Maven 而不是 Gradle 。我还想说,虽然使用 Gradle 不用了解 Groovy ,甚至人们希望在不是那么标准的事情中也不用了解 Groovy 。但是我不会这样,我从 Gradle 的在线例子中已经学习了很多有用的 Groovy 的语句。

有些读者指出我在第一部分的代码示例中使用 Junit 和 Guava ,意味着我有意推广它们。好吧,我确实有这样的想法。Guava 是一个非常有用的库,而 JUnit 是一个很好的单元测试框架。虽然 TestNG 也很好,但是 JUnit 非常常见,很少有人会选择别的就算有优势的测试框架。

同样,就示例代码中测试使用 Hamcrest ,一个读者指出 AssertJ,可能是一个比 Hamcrest 更好的选择。

需要理解到本系列指南并不打算覆盖到 Java 的方方面面,能认识到这一点很重要。所以当然会有很多很好的库因为没有在文章中出现,我们没有去探索它们。我写这份指南的本意就是给大家示意一下现代 Java 开发可能是什么样的。

有些读者表达了他们更喜欢短的 Javadoc 注释,这种注释不必像 Javadoc 标准形式那样需要把所有的字段都写上。如下面的例子:

/**
 * This method returns the result.
 * @return the result
 */
 int getResult();

更喜欢这样:

/**
 * Returns the result
 */
 int getResult();

我完全同意。我在例子中简单示范了混合 Markdown 和标准的 Javadoc 标签的使用。这只是用来展示如何使用,并不是意图把这种使用方式当成指导方针。

最后,关于 Android 我有一些话要说。 Android 系统通过一系列变换之后,能够执行用 java (还有可能是别的 JVM 语言)写的代码,但是 Android 不是 JVM,并且事实上 Android 无论在正式场合和实际使用中也不完全是 Java (造成这个问题的原因是两个跨国公司,这里指谷歌和甲骨文,没有就 Java 的使用达成一个许可协议)。正因为 Android 不完全是 Java ,所以在第一部分中讨论的内容对 Android 可能有用或者也可能没有用,而且因为 Android 没有包括 JVM ,所以在这部分讨论的内容很少能应用到 Android 上面。

好了,现在让我们回到正文。

现代 Java 的打包和部署

对于不熟悉 Java 生态体系的人来说,Java(或者任何 JVM 语言)源文件,被编绎成 .class 文件(本质上是 Java 二进制文件),每一个类一个文件。打包这些 class 文件的基本机制就把这些文件打包在一起(这项工作通常由构建工具或者IDE来完成)放到JAR(Java存档)文件,JAR 文件叫 Java 二进制包。 JAR 文件仅仅是 Zip 压缩文件,它包括 class 文件,还有一个附加的清单文件用来描述内容,清单中还可以包括其它的关于分发的信息(如在被签名的 JARs 中,清单可以包括数字签名)。如果你打包一个应用(与此相反是打包一个库)到 JAR 中,清单文件应该指出应用的主类(也就是 main 函数所在类),在这种情况下,应用通过命令java -jar app.jar启动,我们称这个 JAR 文件为可执行的 JAR 。

Java 库被打包成 JAR 文件,然后部署到 Maven 仓库中(这个仓库能被所有的 JVM 构建工具使用,不仅仅是 Maven )。 Maven 仓库管理这些库二进制文件的版本和依赖(当你发一个请求想从Maven仓库中加载一个库,此外你请求了该库所有的依赖)。开源 Java 库经常托管在这个中央仓库中,或者其它类似的公开仓库中。并且组织机构通过 Artifactory 或者 Nexus 等工具,管理他们私有 Maven 仓库。你甚至能在 GitHub 上建立自己的 Maven 仓库。但是 Maven 仓库在构建过程中应该能正常使用,并且 Maven 仓库通常托管库形式 JAR 而不是可执行的 JAR 。

Java 网站应用传统上应该在应用服务器(或者 servlet 容器)中执行。这些容器能运行多个网站应用,能按需加载或卸载应用。 Java 网站应用以 WAR 的形式部署在 servlet 容器中。WAR 也是 JAR 文件,它的内容以某种标准形式排好,并且包括额外的配置信息。但是,正如我们将在第三部分看到一样,就现代 Java 开发而言, Java 应用服务器已死

Java 桌面应用经常被打包成与平台相关的二进制文件,还包括一个平台相关的 JVM。 JDK 工具包中有一个打包工具来做这个事情(这里是讲的是如何在 NetBeans 中使用它)。第三方工具 Packer 也提供了类似的功能。对于游戏和桌面应用来说,这种打包机非常好。但是对于服务器软件来说,这种打包机制就不是我想要的。此外,因为要打包一个 JVM 的拷贝,这种机制不能以补丁形式安全和平滑地升级应用。

对服务器端代码,我们想要的是一种简单、轻量、能自动的打包和部署的工具。这个工具最好能利用可执行 JAR 的简单和平台无关性。但是可执行 JAR 有几个不足的地方。每一个库通常打包到各自的 JAR 文件中,然后和所有的依赖一起打包成单个 JAR 文件,这一过程可能造成冲突,特别是已打包的资源库(没有 class 文件的库)一起打包时。还有,一个原生库在打包时不能直接放到 JAR 中。打包中可能最重要的是, JVM 配置信息(如 heap 的大小)对用户来说是遗漏的,这个工作必须在命令行下才能做。像 Maven’s Shade pluginGradle’s Shadow plugin 等工具,解决了资源冲突的问题,而 One-Jar 支持原生的库,但是这些工具都可能对应用产生影响,而且也没有解决 JVM 参数配置的问题。 Gradle 能把应用打包成一个 ZIP 文件,并且产生一个与系统相关的启脚本去配置 JVM ,但是这种方法要求安装应用。我们可以做的比这样更轻量级。同样,我们有强大的、普遍存在的资源像 Maven 仓库任我们使用,如果不充分利用它们是件令人可耻的事。

这一系列博客打算讲讲用现代 Java 工作是多么简单和有趣(不需牺牲任何性能),但是当我去寻找一种有趣、简单和轻量级的方法去打包、分发和部署服务器端的 Java 应用时,我两手空空。所以 Capsule 诞生了(如果你知道有其它更好的选择,请告诉我)。

Capsule 使用平台独立的可执行 JAR 包,但是没有依赖,并且(可选的)能整合强大和便捷的 Maven 仓库。一个 capsule 是一个 JAR 文件,它包括全部或者部分的 Capsule 项目 class,和一个包括部署配置的清单文件。当启动时(java -jar app.jar), capsule 会依次执行以下的动作:解压缩 JAR 文件到一个缓存目录中,下载依赖,寻找一个合适的 JVM 进行安装,然后配置和运行应用在一个新的JVM进程中。

现在让我们把 Capsule 拿出来溜一溜。我们把第一部JModern 项目做为开始的项目。这是我们的 build.gradle 文件:

apply plugin: 'java'
apply plugin: 'application'

sourceCompatibility = '1.8'

mainClassName = 'jmodern.Main'

repositories {
    mavenCentral()
}

configurations {
    quasar
}

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

    testCompile 'junit:junit:4.11'
}

run {
    jvmArgs "-javaagent:${configurations.quasar.iterator().next()}"
}

这里是我们的 jmodern.Main 类:

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
    }
}

为了测试一下我们的程序工作是正常的,我们运行一下gradle run

现在,我们来把这个应用打包成一个 capsule 。在构建文件中,我们将增加 capsule 配置。然后,我们增加依赖包:

capsule "co.paralleluniverse:capsule:0.3.1"

当前 Capsule 有两种方法来创建 capsule (虽然你也可以混合使用)。第一种方法是创建应用时把所有的依赖都加入到 capsule 中;第二种方法是第一次启动 capsule 时让它去下载依赖。我来试一下第一种—— "full" 模式。我们添加下面的任务到构建文件中:

task capsule(type: Jar, dependsOn: jar) {
    archiveName = "jmodern-capsule.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,
            'Min-Java-Version' : '1.8.0',
            'JVM-Args' : run.jvmArgs.join(' '), // copy JVM args from the run task
            'System-Properties' : run.systemProperties.collect { k,v -> "$k=$v" }.join(' '), // copy system properties
            'Java-Agents' : configurations.quasar.iterator().next().getName()
        )
    }
}

好了,现在我们输入gradle capsule构建 capsule ,然后运行:

java -jar build/libs/jmodern-capsule.jar

如果你想准确的知道 Capsule 现在在做什么,可以把-jar换成-Dcapsule.log=verbose,但是因为它是一个包括依赖的 capsule ,第一次运行时, Capsule 会解压 JAR 文件到一个缓存目录下
(这个目录是在当前用户的根文件夹中下.capsule/apps/jmodern.Main),然后启动一个新通过 capsule 清单文件配置好的 JVM 。如果你已经安装好了 Java7 ,你可以使用 Java7 启动 capsule (通过设置 JAVA_HOME 环境变量)。虽然 capsule 能在 java7 下启动,但是因为 capsule 指定了最小的 Java 版本是 Java8 (或者是 1.8,同样的意思), capsule 会寻找 Java8 并且用它来跑我们的应用。

现在讲讲第二方法。我们将创建一个有外部依赖的 capsule 。为了使创建工作简单点,我们先在构建文件中增加一个函数(你不需要理解他;做成 Gradle 的插件会更好,欢迎贡献。但是现在我们手动创建这个 capsule ):

// converts Gradle dependencies to Capsule dependencies
def getDependencies(config) {
    return config.getAllDependencies().collect {
        def res = it.group + ':' + it.name + ':' + it.version +
            (!it.artifacts.isEmpty() ? ':' + it.artifacts.iterator().next().classifier : '')
        if(!it.excludeRules.isEmpty()) {
            res += "(" + it.excludeRules.collect { it.group + ':' + it.module }.join(',') + ")"
        }
        return res
    }
}

然后,我们改变构建文件中capsule任务,让它能读:

task capsule(type: Jar, dependsOn: classes) {
    archiveName = "jmodern-capsule.jar"
    from sourceSets.main.output // this way we don't need to extract
    from { configurations.capsule.collect { zipTree(it) } }

    manifest {
        attributes(
            'Main-Class'  :   'Capsule',
            'Application-Class'   : mainClassName,
            'Extract-Capsule' : 'false', // no need to extract the capsule
            'Min-Java-Version' : '1.8.0',
            'JVM-Args' : run.jvmArgs.join(' '),
            'System-Properties' : run.systemProperties.collect { k,v -> "$k=$v" }.join(' '),
            'Java-Agents' : getDependencies(configurations.quasar).iterator().next(),
            'Dependencies': getDependencies(configurations.runtime).join(' ')
        )
    }
}

运行gradle capsule,再次运行:

java -jar build/libs/jmodern-capsule.jar

首次运行, capsule 将会下载我们项目的所有依赖到一个缓存目录下。其他的 capsule 共享这个目录。 相反你不需要把依赖列在 JAR 清单文件中,取而代之,你可以把项目依赖列在 pom 文件中(如果你使用 Maven 做为构建工具,这将特别有用),然后放在 capsule 的根目录。详细信息可以查看 Capsule 文档

最后,因为这篇文章的内容对于任何 JVM 语言都是有用的,所以这里有一个小例子用来示意把一个 Node.js 的应用打包成一个 capsule 。这个小应用使用了 Avatar ,该项目能够在 JVM 上运行 javascript 应用
,就像 Nodejs 一样。代码如下:

var http = require('http');

var server = http.createServer(function (request, response) {
  response.writeHead(200, {"Content-Type": "text/plain"});
  response.end("Hello World\n");
});
server.listen(8000);
console.log("Server running at http://127.0.0.1:8000/");

应用还有两个 Gradle 构建文件。一个用来创建full模式的 capsule ,另外一个用来创建external模式的 capsule 。这个例子示范了打包原生库依赖。创建该 capsule ,运行:

gradle -b build1.gradle capsule

就得到一个包括所有依赖的 capsule 。或者运行下面的命令:

gradle -b build2.gradle capsule

就得到一个不包括依赖的 capsule (里面包括 Gradle wrapper,所以你不需要安装 Gradle ,简单的输入./gradlew就能构建应用)。

运行它,输入下面的命令:

java -jar build/libs/hello-nodejs.jar

Jigsaw,原计划在包括在 Java9 中。该项目的意图是解决 Java 部署和一些其它的问题,例如:一个被精减的JVM发行版,减少启动时间(这里有一个有趣演讲关于 Jigsaw )。同时,对于现代 Java 开发打包和布署,Capsule 是一个非常合适的工具。Capsule 是无状态和不用安装的。

日志

在我们进入 Java 先进的监控特性之前,让我们把日志搞定。据我所知,Java 有大量的日志库,它们都是建立在 JDK 标准库之上。如果你需要日志,用不着想太多,直接使用 slf4j 做为日志 API 。它变成了事实上日志 API 的标准,而且已绑定几乎所有的日志引擎。一但你使用 SLF4J,你可以推迟选择日志引擎时机(你甚至能在部署的时候决定使用哪个日志引擎)。 SLF4J 在运行时选择日志引擎,这个日志引擎可以是任何一个只要做为依赖添加的库。大部分库现在都使用SLF4J,如果开发中有一个库没有使用SLF4J,它会让你把这个库的日志导回SLF4J,然后你就可以再选择你的日志引擎。谈谈选择日志引擎事,如果你想选择一个简单的,那就 JDK 的java.util.logging。如果你想选择一个重型的、高性能的日志引擎,就选择 Log4j2 (除了你感觉真的有必要尝试一下其它的日志引擎)。

现在我们来添加日志到我们的应用中。在依赖部分,我们增加:

compile "org.slf4j:slf4j-api:1.7.7"    // the SLF4J API
runtime "org.slf4j:slf4j-jdk14:1.7.7"  // SLF4J binding for java.util.logging

如果运行gradle dependencies命令,我们可以看到当前的应用有哪些依赖。就当前来说,我们依赖了 Log4j ,这不是我们想要的。因此好得在build.gradle的配置部分增加一行代码:

all*.exclude group: "org.apache.logging.log4j", module: "*"

好了,我们来给我们的应用添加一些日志:

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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Main {
    static final Logger log = LoggerFactory.getLogger(Main.class);

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

        new Fiber<Void>(() -> {
            for (int i = 0; i < 100000; i++) {
                Strand.sleep(100);
                log.info("Sending {}", i); // log something
                ch.send(i);
                if (i % 10 == 0)
                    log.warn("Sent {} messages", i + 1); // log something
            }
            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
    }
}

然后运行应用(gradle run),你会看见日志打印到标准输出(这个默认设置;我们不打算深入配置日志引擎,你想做的话,可以参考想关文档)。infowarn级的日志都默认输出。日志的输出等级可以在配置文件中设置(现在我们不打算改了),或者一会可以看到,我们在运行时进行修改设置,

用jcmd和jstat进行监控和管理

JDK 中已经包括了几个用于监控和管理的工具,而这里我们只会简短介绍其中的一对工具:jcmdjstat

为了演示它们,我们要使我们的应用程序别那么快的终止。所以我们把for循环次数从10改成1000000,然后在终端下运行应用gradle run。在另外一个终端中,我们运行jcmd。如果你的JDK安装正确并且jcmd在你的目录中,你会看到下面的信息:

22177 jmodern.Main
21029 org.gradle.launcher.daemon.bootstrap.GradleDaemon 1.11 /Users/pron/.gradle/daemon 10800000 86d63e7b-9a18-43e8-840c-649e25c329fc -XX:MaxPermSize=256m -XX:+HeapDumpOnOutOfMemoryError -Xmx1024m -Dfile.encoding=UTF-8
22182 sun.tools.jcmd.JCmd

上面信息列出了所有正在JVM上运行的程序。再远行下面的命令:

jcmd jmodern.Main help

你会看到打印出了特定 JVM 程序的 jcmd 支持的命令列表。我们来试一下:

jcmd jmodern.Main Thread.print

打印出了 JVM 中所有线程的当前堆栈信息。试一下这个:

jcmd jmodern.Main PerfCounter.print

这将打印出一长串各种 JVM 性能计数器(你问问谷歌这些参数的意思)。你可以试一下其他的命令(如GC.class_histogram)。

jstat对于 JVM 来说就像 Linux 中的 top ,只有它能查看关于 GC 和 JIT 的活动信息。假设我们应用的 pid 是95098(可以用 jcmdjps 找到这个值)。现在我们运行:

jstat -gc 95098 1000

它将会每 1000 毫秒打印 GC 的信息。看起来像这样:

 S0C    S1C    S0U    S1U      EC       EU        OC         OU       PC     PU    YGC     YGCT    FGC    FGCT     GCT
80384.0 10752.0  0.0   10494.9 139776.0 16974.0   148480.0   125105.4    ?      ?        65    1.227   8      3.238    4.465
80384.0 10752.0  0.0   10494.9 139776.0 16985.1   148480.0   125105.4    ?      ?        65    1.227   8      3.238    4.465
80384.0 10752.0  0.0   10494.9 139776.0 16985.1   148480.0   125105.4    ?      ?        65    1.227   8      3.238    4.465
80384.0 10752.0  0.0   10494.9 139776.0 16985.1   148480.0   125105.4    ?      ?        65    1.227   8      3.238    4.465

这些数字表示各种 GC 区域当前的容量。想知道每一个的意思,查看 jsata 文档

使用JMX进行监控和管理

JVM 最大的一个优点就是它能在运行时监控和管理时,暴露每一个操作的详细信息。JMX(Java Management Extensions),是 JVM 运行时管理和监控的标准。 JMX 详细说明了 MBeans ,该对象用来暴露有关 JVM 、 JDK 库和 JVM 应用的监控和管理操作方法。 JMX 还定义了连接 JVM 实例的标准方法,包括本地连接和远程连接的方式。还有定义了如何与 MBeans 交互。实际上, jcmd 就是使用 JMX 获得相关的信息的。在本文后面,我们也写一个自己的 MBeans ,但是还是首先来看看内置的 MBeans 如何使用。

当我们的应用运行在一个终端,运行 jvisualvm 命令(该工具是 JDK 的一部分)在另一个终端。这会启动 VisualVM 。在我们开始使用之前,还需要装一些插件。打开 Tools->Plugins 菜单,选择可以可以使用的插件。当前的演示,我们只需要VisualVM-MBeans,但是你可能除了 VisualVM-Glassfish 和 BTrace Workbench ,其他的插件都装上。现在在左边面板选择 jmodern.Main ,然后选择监控页。你会看到如下信息:

figure
figure

该监控页把 JMX-MBeans 暴露的使用信息用图表的型式表达出来。我们也可以通过 Mbeans 选项卡选择一些 MBeans (有些需要安装完成插件后才能使用),我们能查看和交互已注册的 MBeans 。例如有个常用的堆图,就在 java.lang/Memory 中(双击属性值展开它):

figure2
figure2

现在我们选择 java.util.logging/Logging MBean 。在右边面板中,属性 LoggerNames 会列出所有已注册的 logger ,包括我们添加到 jmodern.Main (双击属性值展开它):

figure3
figure3

MBeans 使我们不仅能够探测到监测值,还可以改变这些值,然后调用各种管理操作。选择 Operations 选项卡(在右面板中,位于属性选项卡的右边)。我们现在在运行时通过 JMX-MBean 改变日志等级。在 setLoggerLevel 属性中,第一个地方填上 jmodern.Main ,第二个地方填上 WARNING ,载图如下:

figure4
figure4

现在,点击 setLoggerLevel 按钮, info 级的日志信息不再会打印出来。如果调整成 SEVERE ,就没有信息打印。 VisualVM 对 MBean 都会生成简单的 GUI,不用费力的去写界面。

我们也可以在远程使用 VisualVM 访问我们的应用,只用增加一些系统的设置。在构建文件中的run部分中增加如下代码:

systemProperty "com.sun.management.jmxremote", ""
systemProperty "com.sun.management.jmxremote.port", "9999"
systemProperty "com.sun.management.jmxremote.authenticate", "false"
systemProperty "com.sun.management.jmxremote.ssl", "false"

(在生产环境中,你应该打开安全选项)

正如我们所看到的,除了 MBean 探测, VisualVM 也可以使用 JMX 提供的数据创建自定义监控视图:监控线程状态和当前所有线程的堆栈情况,查看 GC 和通用内存使用情况,执行堆转储和核心转储操作,分析转储堆和核心堆,还有更多的其它功能。因此,在现代 Java 开发中, VisualVM 是最重要的工具之一。这是 VisualVM 跟踪插件提供的监控信息截图:

figure5
figure5

现代 Java 开发人员有时可能会喜欢一个 CLI 而不是漂亮的 GUI 。 jmxterm 提供了一个 CLI 形式的 JMX-MBeans 。不幸的是,它还不支持 Java7 和 Java8 ,但开发人员表示将很快来到(如果没有,我们将发布一个补丁,我们已经有一个分支在做这部分工作了)。

不过,有一件事是肯定的。现代 Java 开发人员喜欢 REST-API (如果没有其他的原因,因为它们无处不在,并且很容易构建 web-GUI )。虽然 JMX 标准支持一些不同的本地和远程连接器,但是标准中没有包括 HTTP 连接器(应该会在 Java9 中)。现在,有一个很好的项目 Jolokia,填补这个空白。它能让我们使用 RESTful 的方式访问 MBeans 。让我们来试一试。将以下代码合并到build.gradle文件中:

configurations {
    jolokia
}

dependencies {
    runtime "org.jolokia:jolokia-core:1.2.1"
    jolokia "org.jolokia:jolokia-jvm:1.2.1:agent"
}

run {
    jvmArgs "-javaagent:${configurations.jolokia.iterator().next()}=port=7777,host=localhost"
}

(我发现 Gradle 总是要求对于每一个依赖重新设置 Java agent,这个问题一直困扰我。)

改变构建文件 capsule 任务的 Java-Agents属性,可以让 Jolokia 在 capsule 中可用。代码如下:

'Java-Agents' : getDependencies(configurations.quasar).iterator().next() +
               + " ${getDependencies(configurations.jolokia).iterator().next()}=port=7777,host=localhost",

通过 gradle run 或者 gradle capsule; java -jar build/libs/jmodern-capsule.jar 运行应用,然后打开浏览器输入 http://localhost:7777/jolokia/version 。如果 Jolokia 正常工作,会返回一个JSON。现在我们要查看一下应用的堆使用情况,可以这样做:

curl http://localhost:7777/jolokia/read/java.lang:type\=Memory/HeapMemoryUsage

设置日志等级,你可以这样做:

curl http://localhost:7777/jolokia/exec/java.util.logging:type\=Logging/setLoggerLevel\(java.lang.String,java.lang.String\)/jmodern.Main/WARNING

Jolokia 提供了 Http API ,这就就使用 GET 和 POST 方法进行操作。同时还提供安全访问的方法。需要更多的信息,请查看文档

有了 JolokiaHttpAPI 就能通过Web进行管理。这里有一个例子,它使用Cubism为 GUI 进行 JMX MBeans进行管理。还有如 hawtio , JBoss 创建的项目,它使用 JolokiaHttpAPI 构建了一个全功能的网页版的管理应用。与 VisualVM 静态分析功能不同的是, hawatio 意图是为生产环境提供一个持续监控和管理的工具。

写一个自定义的MBeans

写一个 Mbeans 并注册很容易:

package jmodern;

import co.paralleluniverse.fibers.Fiber;
import co.paralleluniverse.strands.Strand;
import co.paralleluniverse.strands.channels.*;
import java.lang.management.ManagementFactory;
import java.util.concurrent.atomic.AtomicInteger;
import javax.management.MXBean;
import javax.management.ObjectName;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Main {
    static final Logger log = LoggerFactory.getLogger(Main.class);

    public static void main(String[] args) throws Exception {
        final AtomicInteger counter = new AtomicInteger();
        final Channel<Object> ch = Channels.newChannel(0);

        // create and register MBean
        ManagementFactory.getPlatformMBeanServer().registerMBean(new JModernInfo() {
            @Override
            public void send(String message) {
                try {
                    ch.send(message);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }

            @Override
            public int getNumMessagesReceived() {
                return counter.get();
            }
        }, new ObjectName("jmodern:type=Info"));

        new Fiber<Void>(() -> {
            for (int i = 0; i < 100000; i++) {
                Strand.sleep(100);
                log.info("Sending {}", i); // log something
                ch.send(i);
                if (i % 10 == 0)
                    log.warn("Sent {} messages", i + 1); // log something
            }
            ch.close();
        }).start();

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

    }

    @MXBean
    public interface JModernInfo {
        void send(String message);
        int getNumMessagesReceived();
    }
}

我们添加了一个 JMX-MBean ,让我们监视第二个 fiber 收到消息的数量,也暴露了一个发送操作,能将一条消息进入 channel 。当我们运行应用程序时,我们可以在 VisualVM 中看到监控的属性:

figure6
figure6

双击,绘图:

figure8
figure8

Operations 选项卡中,使用我们定义在MBean的操作,来发个消息:

figure9
figure9

使用Metrics进行健康和性能监控

Metrics 一个简洁的监控 JVM 应用性能和健康的现代库,由 Coda Hale 在 Yammer 时创建的。 Metrics 库中包含一些通用的指标集和发布类,如直方图,计时器,统计议表盘等。现在我们来看看如何使用。

首先,我们不需要使用 Jolokia ,把它从构建文件中移除掉,然后添加下面的代码:

compile "com.codahale.metrics:metrics-core:3.0.2"

Metrics 通过 JMX-MBeans 发布指标,你可以将这些指标值写入 CSV 文件,或者做成 RESTful 接口,还可以发布到 Graphite 和 Ganglia
中。在这里只是简单发布到 JMX (第三部分中讨论到 Dropwizard 时,会使用 HTTP )。这是我们修改后的 Main.class

package jmodern;

import co.paralleluniverse.fibers.Fiber;
import co.paralleluniverse.strands.Strand;
import co.paralleluniverse.strands.channels.*;
import com.codahale.metrics.*;
import static com.codahale.metrics.MetricRegistry.name;
import java.util.concurrent.ThreadLocalRandom;
import static java.util.concurrent.TimeUnit.*;

public class Main {
    public static void main(String[] args) throws Exception {
        final MetricRegistry metrics = new MetricRegistry();
        JmxReporter.forRegistry(metrics).build().start(); // starts reporting via JMX

        final Channel<Object> ch = Channels.newChannel(0);

        new Fiber<Void>(() -> {
            Meter meter = metrics.meter(name(Main.class, "messages" , "send", "rate"));
            for (int i = 0; i < 100000; i++) {
                Strand.sleep(ThreadLocalRandom.current().nextInt(50, 500)); // random sleep
                meter.mark(); // measures event rate

                ch.send(i);
            }
            ch.close();
        }).start();

        new Fiber<Void>(() -> {
            Counter counter = metrics.counter(name(Main.class, "messages", "received"));
            Timer timer = metrics.timer(name(Main.class, "messages", "duration"));

            Object x;
            long lastReceived = System.nanoTime();
            while ((x = ch.receive()) != null) {
                final long now = System.nanoTime();
                timer.update(now - lastReceived, NANOSECONDS); // creates duration histogram
                lastReceived = now;
                counter.inc(); // counts

                System.out.println("--> " + x);
            }
        }).start().join(); // join waits for this fiber to finish

    }
}

在例子中,使用了 Metrics 记数器。现在运行应用,启动 VisualVM :

figure9
figure9

性能分析

性能分析是一个应用是否满足我们对性能要求的关键方法。只有经过性能分析我们才能知道哪一部分代码影响了整体执行速度,然后集中精力只改进这一部分代码。一直以来,Java 都有很好的性能分析工具,它们有的在 IDE 中,有的是一个单独的工具。而最近 Java 的性能分析工具变得更精确和轻量级,这要得益于 HotSpot 把 JRcokit
JVM 中的代码合并自己的代码中。在这部分讨论的工具不是开源的,在这里讨论它们是因为这些工具已经包括在标准的 OracleJDK 中,你可以在开发环境中自由使用(但是在生产环境中你需要一个商业许可)。

开始一个测试程序,修改后的代码:

package jmodern;

import co.paralleluniverse.fibers.Fiber;
import co.paralleluniverse.strands.Strand;
import co.paralleluniverse.strands.channels.*;
import com.codahale.metrics.*;
import static com.codahale.metrics.MetricRegistry.name;
import java.util.concurrent.ThreadLocalRandom;
import static java.util.concurrent.TimeUnit.*;

public class Main {
    public static void main(String[] args) throws Exception {
        final MetricRegistry metrics = new MetricRegistry();
        JmxReporter.forRegistry(metrics).build().start(); // starts reporting via JMX

        final Channel<Object> ch = Channels.newChannel(0);

        new Fiber<Void>(() -> {
            Meter meter = metrics.meter(name(Main.class, "messages", "send", "rate"));
            for (int i = 0; i < 100000; i++) {
                Strand.sleep(ThreadLocalRandom.current().nextInt(50, 500)); // random sleep
                meter.mark();

                ch.send(i);
            }
            ch.close();
        }).start();

        new Fiber<Void>(() -> {
            Counter counter = metrics.counter(name(Main.class, "messages", "received"));
            Timer timer = metrics.timer(name(Main.class, "messages", "duration"));

            Object x;
            long lastReceived = System.nanoTime();
            while ((x = ch.receive()) != null) {
                final long now = System.nanoTime();
                timer.update(now - lastReceived, NANOSECONDS);
                lastReceived = now;
                counter.inc();

                double y = foo(x);
                System.out.println("--> " + x + " " + y);
            }
        }).start().join();
    }

    static double foo(Object x) { // do crazy work
        if (!(x instanceof Integer))
            return 0.0;

        double y = (Integer)x % 2723;
        for(int i=0; i<10000; i++) {
            String rstr = randomString('A', 'Z', 1000);
            y *= rstr.matches("ABA") ? 0.5 : 2.0;
            y = Math.sqrt(y);
        }
        return y;
    }

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

foo 方法进行了一些没有意义的计算,不用管它。当运行应用(gradle run)时,你会注意到 Quasar 发出了警告,警告说有一个 fiber 占用了过多的 CPU 时间。为了弄清楚发生了什么,我们开始进行性能分析:

我们使用的分析器能够统计非常精确的信息,同时具有非常低的开销。该工具包括两个组件:第一个是 Java Flight Recorder 已经嵌入到 HotSpotVM 中。它能记录 JVM 中发生的事件,可以和 jcmd 配合使用,在这部分我们通过第二个工具来控制它。第二个工具是 JMC (Java Mission Control),也在 JDK 中。它的作用等同于 VisualVM ,只是它比较难用。在这里我们用 JMC 来控制 Java Flight Recorder ,分析记录的信息(我希望 Oracle 能把这部分功能移到 VisualVM 中)。

Flight Recorder 在默认已经加入到应用中,只是不会记录任何信息也不会影响性能。先停止应用,然后把这行代码加到 build.gradle 中的 run

jvmArgs "-XX:+UnlockCommercialFeatures", "-XX:+FlightRecorder"

UnlockCommercialFeatures 标志是必须的,因为 Flight Recorder 是商业版的功能,不过可以在开发中自由使用。现在,我们重新启动应用。

在另一个终端中,我们使用 jmc 打开 Mission Control 。在左边的面板中,右击 jmodern.Main ,选择 Start Flight Recording… 。在引导窗口中选择 Event settings 下拉框,点击 Profiling - on server ,然后 Next > ,注意不是 Finish

figure12
figure12

接下来,选择 Heap StatisticsAllocation Profiling ,点击 Finish

figure14
figure14

JMC 会等 Flight Recorder 记录结束后,打开记录文件进行分析,在那时你可以关掉你的应用。

Code 部分的 Hot Methods 选项卡中,可以看出 randomString 是罪魁祸首,它占用了程序执行时间的 90%:

figure15
figure15

Memory 部分的 Garbage Collection 选项卡中,展示了在记录期间堆的使用情况:

figure16
figure16

在 GC 时间选项卡中,显示了GC的回收情况:

figure17
figure17

也可以查看内存分配的情况:

figure18
figure18

应用堆的内容:

figure19
figure19

Java Flight Recorder 还有一个不被支持的API,能记录应用事件。

高级话题:使用Byteman进行性能分析和调试

像第一部分一样,我们用高级话题来结束本期话题。首先讨论的是用 Byteman 进行性能分析和调试。我在第一部分提到, JVM 最强大的特性之一就是在运行时动态加载代码(这个特性远超本地原生应用加载动态链接库)。不只这个,JVM 还给了我们来回变换运行时代码的能力。

JBoss 开发的 Byteman 工具能充分利用 JVM 的这个特性。 Byteman 能让我们在运行应用时注入跟踪、调试和性能测试相关代码。这个话题之所以是一个高级话题,是因为当前 Byteman 只支持 Java7 ,对 Java8 的支持还不可靠,需要打补丁才能工作。这个项目当前开发活跃,但是正在落后。因此在这里使用一些 Byteman 非常基础的代码。

这是主类:

package jmodern;

import java.util.concurrent.ThreadLocalRandom;

public class Main {
    public static void main(String[] args) throws Exception {
        for (int i = 0;; i++) {
            System.out.println("Calling foo");
            foo(i);
        }
    }

    private static String foo(int x) throws InterruptedException {
        long pause = ThreadLocalRandom.current().nextInt(50, 500);
        Thread.sleep(pause);
        return "aaa" + pause;
    }
}

foo 模拟调用服务器操作,这些操作要花费一定时间进行。

接下来,把下面的代码合并到构建文件中:

configurations {
    byteman
}

dependencies {
  byteman "org.jboss.byteman:byteman:2.1.4.1"
}

run {
    jvmArgs "-javaagent:${configurations.byteman.iterator().next()}=listener:true,port:9977"
    // remove the quasar agent
}

想在 capsule 中试一试 Byteman 使用,在构建文件中改一下 Java-Agents 属性:

'Java-Agents' : "${getDependencies(configurations.byteman).iterator().next()}=listener:true,port:9977",

现在,从这里下载 Byteman ,因为需要使用 Byteman 中的命令行工具,解压文件,设置环境变量 BYTEMAN_HOME 指向 Byteman 的目录。

启动应用gradle run。打印结果如下:

Calling foo
Calling foo
Calling foo
Calling foo
Calling foo

我们想知道每次调用 foo 需要多长有时间,但是我们没有测量并记录这个信息。现在使用 Byteman 在运行时插入相关日志记录信息。

打开编辑器,在项目目录中创建文件 jmodern.btm

RULE trace foo entry
CLASS jmodern.Main
METHOD foo
AT ENTRY
IF true
DO createTimer("timer")
ENDRULE

RULE trace foo exit
CLASS jmodern.Main
METHOD foo
AT EXIT
IF true
DO traceln("::::::: foo(" + $1 + ") -> " + $! + " : " + resetTimer("timer") + "ms")
ENDRULE

上面列的是 Byteman rules ,就是当前我们想应用在程序上的 rules。我们在另一个终端中运行命令:

$BYTEMAN_HOME/bin/bmsubmit.sh -p 9977 jmodern.btm

之后,运行中的应用打印信息:

Calling foo
::::::: foo(152) -> aaa217 : 217ms
Calling foo
::::::: foo(153) -> aaa281 : 281ms
Calling foo
::::::: foo(154) -> aaa282 : 283ms
Calling foo
::::::: foo(155) -> aaa166 : 166ms
Calling foo
::::::: foo(156) -> aaa160 : 161ms

查看哪个 rules 正在使用:

$BYTEMAN_HOME/bin/bmsubmit.sh -p 9977

卸载 Byteman 脚本:

$BYTEMAN_HOME/bin/bmsubmit.sh -p 9977 -u

运行该命令之后,注入的日志代码就被移出。

Byteman 是在 JVM 灵活代码变换的基础上创建的一个相当强大的工具。你可以使用这个工具来检查变量和日志事件,插入延迟代码等操作,甚至还可以轻松设置一些自定义的 Byteman 行为。更多的信息,参考Byteman documentation

高级话题:使用JMH进行基准测试

当代硬件构架和编译技术的进步使考察代码性能的唯一方法就是基准测试。一方面,由于现代 CPU 和编译器非常聪明(可以看这里),它能为代码(可以是 c,甚至是汇编)自动地创建一个理论上非常高效的运行环境,就像 90 年代末一些游戏程序员做的那些非常不可思议的事一样。另一方面,正是因为聪明的 CPU 和编译器,让微基准测试非常困难,因为这样的话,代码的执行速度非常依赖具体的执行环境(如:代码速度受 CPU 缓存状态的影响,而 CPU 缓存状态又受其它线程操作的影响)。而对一个 Java 进行微基准测试又会更加的困难,因为 JVM 有 JIT ,而 JIT 是一个以性能优化为导向的编绎器,它能在运行时影响代码执行的上下文环境。因此在 JVM 中,同一段代码在微基准测试和实际程序中执行时间可能不一样,有时可能快,有时也可能慢。

JMH 是由 Oracle 创建的 Java 基准测试工具。你可以相信由 JMH 测试出来的数据(可以看看这个由 JMH 主要作者Aleksey Shipilev的演讲幻灯片)。 Google 也做了一个基准测试的工具叫 Caliper,但是这个工具很不成熟,有时还会有错误的结果。不要使用它。

我们马上来使用一下 JMH ,但是在这之前首先有一个忠告:过早优化是万恶之源。在基测试中,两种算法或者数据结构中,一种比另一种快 100 倍,而这个算法只占你应用运行时间的 1% ,这样测试是没有意义的。因为就算你把这个算法改进的非常快行但也只能加快你的应用 2% 时间。基准测试只能是已经对应用进行了性能测试后,用来发现哪一个小部分改变能得到最大的加速成果。

增加依赖:

testCompile 'org.openjdk.jmh:jmh-core:0.8'
testCompile 'org.openjdk.jmh:jmh-generator-annprocess:0.8'

然后增加bench任务:

task bench(type: JavaExec, dependsOn: [classes, testClasses]) {
    classpath = sourceSets.test.runtimeClasspath // we'll put jmodern.Benchamrk in the test directory
    main = "jmodern.Benchmark";
}

最后,把测试代码放到 src/test/java/jmodern/Benchmark.java 文件中。我之前提到过 90 年代的游戏程序员,是为了说明古老的技术现在仍然有用,这里我们测试一个开平方根的计算,使用 fast inverse square root algorithm(平方根倒数速算法,这是 90 年代的程序):

package jmodern;

import java.util.concurrent.TimeUnit;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.profile.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import org.openjdk.jmh.runner.parameters.TimeValue;

@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class Benchmark {
    public static void main(String[] args) throws Exception {
        new Runner(new OptionsBuilder()
                .include(Benchmark.class.getName() + ".*")
                .forks(1)
                .warmupTime(TimeValue.seconds(5))
                .warmupIterations(3)
                .measurementTime(TimeValue.seconds(5))
                .measurementIterations(5)
                .build()).run();
    }

    private double x = 2.0; // prevent constant folding

    @GenerateMicroBenchmark
    public double standardInvSqrt() {
        return 1.0/Math.sqrt(x);
    }

    @GenerateMicroBenchmark
    public double fastInvSqrt() {
        return invSqrt(x);
    }

    static double invSqrt(double x) {
        double xhalf = 0.5d * x;
        long i = Double.doubleToLongBits(x);
        i = 0x5fe6ec85e7de30daL - (i >> 1);
        x = Double.longBitsToDouble(i);
        x = x * (1.5d - xhalf * x * x);
        return x;
    }
}

随便说一下,像第一部分中讨论的 Checker 一样, JMH 使用使用注解处理器。但是不同 Checker , JMH 做的不错,你能在所有的 IDE 中使用它。在下面的图中,我们可以看到, NetBeans 中,一但忘加 @State 注解, IDE 就会报错:

feature17
feature17

写入命令 gradle bench ,运行基准测试。会得到以下结果:

Benchmark                       Mode   Samples         Mean   Mean error    Units
j.Benchmark.fastInvSqrt         avgt        10        2.708        0.019    ns/op
j.Benchmark.standardInvSqrt     avgt        10       12.824        0.065    ns/op

很漂亮吧,但是你得知道 fast-inv-sqrt 结果是一个粗略近似值, 只在需要大量开平方的地方适用(如图形计算中)。

在下面的例子中, JMH 用来报到 GC 使用的时间和方法栈的调用时间:

package jmodern;

import java.util.*;
import java.util.concurrent.*;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.profile.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import org.openjdk.jmh.runner.parameters.TimeValue;

@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class Benchmark {
    public static void main(String[] args) throws Exception {
        new Runner(new OptionsBuilder()
                .include(Benchmark.class.getName() + ".*")
                .forks(2)
                .warmupTime(TimeValue.seconds(5))
                .warmupIterations(3)
                .measurementTime(TimeValue.seconds(5))
                .measurementIterations(5)
                .addProfiler(GCProfiler.class)    // report GC time
                .addProfiler(StackProfiler.class) // report method stack execution profile
                .build()).run();
    }

    @GenerateMicroBenchmark
    public Object arrayList() {
        return add(new ArrayList<>());
    }

    @GenerateMicroBenchmark
    public Object linkedList() {
        return add(new LinkedList<>());
    }

    static Object add(List<Integer> list) {
        for (int i = 0; i < 4000; i++)
            list.add(i);
        return list;
    }
}

这是 JMH 的打印出来的信息:

Iteration   3: 33783.296 ns/op
          GC | wall time = 5.000 secs,  GC time = 0.048 secs, GC% = 0.96%, GC count = +97
             |
       Stack |  96.9%   RUNNABLE jmodern.generated.Benchmark_arrayList.arrayList_AverageTime_measurementLoop
             |   1.8%   RUNNABLE java.lang.Integer.valueOf
             |   1.3%   RUNNABLE java.util.Arrays.copyOf
             |   0.0%            (other)
             |

JMH 是一个功能非常丰富的框架。不幸的是,在文档方面有些薄弱,不过有一个相当好代码示例教程,用来展示 Java 中微基测试的陷阱。你也可以读读这篇介绍 JMH 的入门文章。

目前为止我们已经学了什么?

在这篇文章中,我们讨论了在 JVM 管理、监控和性能测试方面最好的几个工具。 JVM 除了很好的性能外,它还非常深思熟虑地提供了能深度洞察它运行状态的能力,这就是我不会用其它的技术来取代 JVM 做为重要的、长时间运行的服务器端应用平台的主要原因。
此外,我们还见识到了当使用 Byteman 等工具修改运行时代码时, JVM 是多么强大。

我们还介绍了 Capsule ,一个轻量级的、单文件、无状态、不用安装的部署工具。另外,通过一个公开或者组织内部的 Maven 仓库,它还支持整个Java应用自动升级,或者还是仅仅升级一个依赖库。

第三部分中,我们将讨论如何使用 DropwizardComsat , Web Actors ,和 DI 来写一个轻量级、可扩展的http服务。

原文地址:An Opinionated Guide to Modern Java, Part 2: Deployment, Monitoring & Management, Profiling and Benchmarking


水平有限,如果看不懂请直接看英文版。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 148,637评论 1 318
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 63,443评论 1 266
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 99,164评论 0 218
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,075评论 0 188
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,080评论 1 266
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,365评论 1 184
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,901评论 2 283
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,649评论 0 176
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,122评论 0 223
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,734评论 2 225
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,093评论 1 236
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,548评论 2 222
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,028评论 3 216
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,765评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,291评论 0 178
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,162评论 2 239
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,293评论 2 242

推荐阅读更多精彩内容

  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,145评论 6 345
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,036评论 18 139
  • 现代java开发指南 第一部分 第一部分:Java已不是你父亲那一代的样子 第一部分,第二部分 与历史上任何其他的...
    htoo阅读 2,052评论 1 37
  • 时已到深秋,学校操场上的小树林依旧热闹,蝉叫声,篮球声,同学们的嬉闹声……如此深夜,最安静不下来的不过是学校的日子...
    秦陌尘阅读 249评论 2 2
  • 4个月前了解过一点点RxJava的皮毛,当时就经常看到Retrofit+MVP+RxJava这套组合,当时还在学校...
    英勇青铜5阅读 1,684评论 10 15