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


现代java开发指南 第三部分

第三部分:Web开发

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

===========================

欢迎来到现代 Java 开发指南第三部分。在第一部分中,我们尝试着编了写现代Java代码,在之后的第二部分中,探索了JVM应用的部署,管理,监控和测试。现在,是时候研究现代JavaWeb开发了。还是老规矩,先回答一下读者的问题。

第二部分中,可以看到 JVM 是如何重视监控和怎样暴露 JVM 运行时行为数据。有一位读者提到一个我用过很多次但是第二部分没有说的工具——JITWatch。它帮助我们分析 JVM 更深层次的信息,因此这个工具只推荐给对 Java 或其它语言性能高度关心的专家使用。调用这个工具只用在 JVM 的选项中增加 -XX:+UnlockDiagnosticVMOptions -XX:+TraceClassLoading -XX:+LogCompilation -XX:+PrintAssembly,这样就能得到 JVM 怎么优化你的代码和什么时候优化你的代码的信息。还有它还能查看哪些方法被编译成机器码(加上-XX:+PrintAssembly选项,甚至还能查看编绎成的机器码),哪些方法内联,哪些方法不被内联等等很多信息。更多的信息,可以查看项面维基

有一些读者对 Capsule 提出意见,认为 Capsule 没有按 JVM 的打包标准。这不完全对,因为 Capsule 是一个无状态可执行不用安装的程序,因此本身来说,它就不用跟 JVM 的打包标准完全一致。如果你的应用,要求有一些状态(如在安装时,需要一个用户向导),Capsule 并不合适你。另外一部分读者表示对 Capsule 运行时依赖 Maven 的可用性表示不放心。这于这点我要说,很显然,对于软件在可用性/关键性任务的范围有每个人都有不同的观点,而不同的应用在使用安全性和使用便捷性也应该有不同的权衡。你可以创建一个不支持自动升级的 Capsule,或者一个包括所有依赖的 Capsule。你还可以在启动时选择 Java 运行时和 JVM 的配置。我认为,如果选择使用外部 Maven 仓库依赖,就没有理由去怀疑外部库意外的错误或其它的问题因为在依赖问题在构建过程就已经解决。而在前一种方案中,Capsule 必须显式说明它的依赖,并且能够列出整个依赖库。同样,如果把组织内部 Maven 仓库用做 capsule 的依赖,那就没有理由不把当成运维的服务器,确保它和其它服务器一样保证运行时的可用性(特别注意 Maven 仓库软件并不为人所知的crash )。

现在让我们回到手边要做的事。

现代 JavaWeb 开发

因为 JavaWeb 服务器与 Web 一样老,因此在 JavaWeb 上长期存在的成功传统和实践很快就要扔掉,现在可能是一个好的时候来解释这一系列中“现代”意思。

在本文中,我说“现代”的意思,就是“与现代主流软件开发趋势一致”。这些趋势并不是完全任意的堆砌,他们一个一个契合在一起。出现于这个期间大量小型快速发展的创业公司更偏爱精益开发方法。这些都要求一个更好使用,更少安装、部署和配置,集开发和运维于一体的工具。广受欢迎的云计算通过资源管理,也就虚拟化(无论是工具上还是在系统级)鼓励这些方法。系统级部署和资源分配也支持异构架构的发展。所谓异构架构就是指寻找适合的工具(也有可能是不同的工具)做合适的事。

传统的 JavaWeb 服务器,也就是典型的应用服务器,都有一个特别的特性:支持在一个 JVM 上运行多个应用。这个应用服务器提供能分开应用的运行时环境,而且升级,安装和启动都是独立的。一个应用可能运行在一个配置好的,已经运行的环境中,这种方法很多时候都工作良好,你也有理由继续使用这种方案,但是这种方案,离“现化”太远了。在不同的应用中分配不同的资源这件事是并不简单,而且在一定程度上跟现在使用 hypervisor 和 os 容器来运行应用的方案是矛盾的。因为现在针对 hypervisor 和 os 容器设计的工具和技术在多应用服务器上效率并不高,即使这些多应用服务器只是用来运行一个应用,而且这些多应用服务器的运维也不“现代”:安装配置 web 或者 app 服务器是不可缺少的,部署应用需要很多步,每一步可能都很麻烦。

现代的方法,就是在其它语言和运行平台使用的方法--单应用服务器。单应用服务器中,web 容器是嵌入到应用中(而不是把应用部署到we b容嚣中)。这样做就可以简单的部署,管理,配置和在系统级进行资源的分配。这就是为什么,一但现代的方法被引入Java中,传统的应用服务器(我的意思是任何打算运行多个应用的 servlet 或者全功能的 J2e 服务器)就死了

在这里,我们调研的工具和技术并非覆盖全部的的领域。特别是在 web 和 web 相关的领域中,开发,工具,库和框架激增。这种增长部分原因是,不像嵌入式开发和大型机开发,web开发在初创公司和开发爱好者中广受欢迎。这类人是新技术的早期采纳者和体验者,有时也会为了探索技术的边界,或者学习,还有自我证明发明一种新的择术。这样的结果就是数以百计的库被发明出来,全都为了解决同样的目标,只是使用的方法略有不同。这种事情发生在 Java 的世界里,也发生在其他的语言生态中。

同时,我们不会讨论那种有巨大的 MVC 结构,模板系统或者设计来就是在服务器端渲染 html 的“全功能”的 web 框架。有很多理由不这么做,第一个就是,我从来没有使用过那种框架,所以我不会评论他们的适用性或“现化化”,第二,这个主题就非常复杂,需要更多的讨论,而在别的地方已经有了(这里,这里), 第三,web 开发正在朝客户端渲染和SPA方向发展(如 angular),本质上正在朝着以前c/s的架构发展,数据和命令都通过http对服务器进行交互。这种转变没太完全,特别的,它依靠手机浏览器的 js 效率的提升,但是可以肯定的讲,我们将会看到越来越少HTML在服务器端生成。因此,我们会只讨论 http “数据” 服务的库和框架。

http 服务和JAX-RS 与 Dropwizard

Java 与其他语言不同的一点是 JCP(Java Community Process)的工作,它的工作是标准化 API(即使对于不属于语言规范或甚至标准运行时的库)也是如此,然后由各种商业或开源组织实现。这些 JSR(Java Specification Requests)是由专家组制作的,它能把一项技术从普遍变成成熟并成为标准。当 JSR 通过时,就会非常有用,因为几乎所有迎合相关领域的库都将实现这个标准 API,这使得切换实现不那么痛苦。

对于服务器实现(代码中框架更为普遍)来说,标准对于客户端(每个调用或多或少都是独立的并且可以被替换)而言更重要。 您可以使用三个不同的 HTT P客户端和 3 个不同的 JDBC API,但是您的服务器通常运行在单个框架中。 出于这个原因,。 单纯的 API 美学不应该倾向于支持非标准的API。

相比于客户端(每次请求或多或少比较独立和能被替代),标准化对服务器应用更重要(因为框架代码无处不在)。你可以使用三个不同的 http 客户端和三个不同的 JDDC api 在同一个方法中,但是你的服务器通常运行在一个框架中。出于这个原因,你应该更喜欢标准服务器API而不是非标准服务器API,除非非标准服务器 API 为你的应用提供了一些非常重要的优势,或者更适合您的特定用例。单纯的 API 美学不应该倾向于支持非标准的 API。

那么轻量级的 Web 服务器最好应该实现标准的 API。谈到 HTT P服务时,有几个相关的 API 需要关注。第一个是古老的 Servlet API(目前是 Servlet 3.0的 JSR-315 和 Servlet 3.1的 JSR-340 )。几乎所有的 JavaWeb 服务器都实现了 Servlet API,其中一些是“现代”的(在我们之前讨论的意思),而在这里面最流行的是 Jetty。与传统的 JavaWeb 服务器不同,Jetty 不是独立的 We b应用程序容器,而是嵌入在应用程序中的 Web 服务库。它就是为"现代"编写的。不过传统的 Web 服务器,如 Tomcat,现在也已经有了嵌入式模式。因为 Servlet 是一个相对较低级别的 HTTP 服务器 API,我们不会在这里直接使用它们,所所以让我们继续讨论下一个标准 API -- JAX-RS(目前版本2.0,在JSR-339中说明)。现在已经有几种 JAX-RS 的实现,像 Apache CXFRESTEasyRestlet,但最流行的应该是 Jersey

JAX-RS 实现通常是在 Servlet 服务之上来使用。 因此,通过将 Jetty 和 Jersey 组合在一起来构建一个现代化的 JavaWeb服务微框架是非常自然的事,而这正是我们下一步将要使用的工具:Dropwizard

所以,Dropwizard 把 Jetty,Jersey,Jackson,我们在第 2 部分介绍的现代性能监测库 Metrics(它恰好是由 Dropwizard 背后的人 Coda Hale 创建的)和其他一些库,组合成一个完整,简单,现代的 JavaWeb 服务微框架。

我们现在将用 Dropwizard 编写第一个现代 JavaWeb 服务。 如果你还没有阅读第一部分,我建议你现在就回头看一下,这样能熟悉一下 Gradle 的基本用法,因为我们将使用 Gradle 做为构建工具。

我们将创建一个新的 jmodern-web 目录,cd 进入该目录,输入 gradle init --type java-library 创建一个 Gradle项目,删除文件(src/main/java/Library.javasrc/test/java/LibraryTest.java

然后,编辑 build.gradle:

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

sourceCompatibility = '1.8'

mainClassName = 'jmodern.Main'
version = '0.1.0'

repositories {
    mavenCentral()
}

configurations {
    capsule
}

dependencies {
    compile 'io.dropwizard:dropwizard-core:0.7.0'
    capsule 'co.paralleluniverse:capsule:0.4.0'
    testCompile 'junit:junit:4.11'
}

task capsule(type: Jar, dependsOn: classes) {
    archiveName = "jmodern-web.jar"

    from jar // embed our application jar
    from { configurations.runtime } // embed dependencies

    from(configurations.capsule.collect { zipTree(it) }) { include 'Capsule.class' } // we just need the single Capsule class

    manifest {
        attributes(
            'Main-Class'  :   'Capsule',
            'Application-Class'   : mainClassName,
            'Application-Version' : version,
            'Min-Java-Version' : '1.8.0',
            'JVM-Args' : run.jvmArgs.join(' '),
            'System-Properties' : run.systemProperties.collect { k,v -> "$k=$v" }.join(' '),
        )
    }
}

src/main/java/jmodern/Main.java 文件修改如下:

package jmodern;

import io.dropwizard.Application;
import io.dropwizard.*;
import io.dropwizard.setup.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
import javax.ws.rs.*;
import javax.ws.rs.core.*;

public class Main extends Application<Configuration> {
    public static void main(String[] args) throws Exception {
        new Main().run(new String[]{"server"});
    }

    @Override
    public void initialize(Bootstrap<Configuration> bootstrap) {
    }

    @Override
    public void run(Configuration configuration, Environment environment) {
        environment.jersey().register(new HelloWorldResource());
    }

    @Path("/hello-world")
    @Produces(MediaType.APPLICATION_JSON)
    public static class HelloWorldResource {
        private final AtomicLong counter = new AtomicLong();

        @GET
        public Map<String, Object> sayHello(@QueryParam("name") String name) {
            Map<String, Object> res = new HashMap<>();
            res.put("id", counter.incrementAndGet());
            res.put("content", "Hello, " + (name != null ? name : "World"));
            return res;
        }
    }
}

这几乎是最简单的 Dropwizard 服务了。 sayHello 方法返回一个 MapMap会自动改为 JSON 对象。 在 shell 中键入 gradle run,运行应用,或者先用 gradle capsule 构建一个 capsule,然后使用 java -jar build/libs/jmodern-web.jar 运行应用。要测试业务逻辑需要在浏览器中输入 http://localhost:8080/hello-worldhttp://localhost:8080/hello-world?name=Modern+Developer 进行测试。

现在让我们用 Dropwizard 的其它特性改进我们的服务:

package jmodern;

import com.codahale.metrics.*;
import com.codahale.metrics.annotation.*;
import com.fasterxml.jackson.annotation.*;
import com.google.common.base.Optional;
import io.dropwizard.Application;
import io.dropwizard.*;
import io.dropwizard.setup.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicLong;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import org.hibernate.validator.constraints.*;

public class Main extends Application<Main.JModernConfiguration> {
    public static void main(String[] args) throws Exception {
        new Main().run(new String[]{"server", System.getProperty("dropwizard.config")});
    }

    @Override
    public void initialize(Bootstrap<JModernConfiguration> bootstrap) {
    }

    @Override
    public void run(JModernConfiguration cfg, Environment env) {
        JmxReporter.forRegistry(env.metrics()).build().start(); // Manually add JMX reporting (Dropwizard regression)

        env.jersey().register(new HelloWorldResource(cfg));
    }

    // YAML Configuration
    public static class JModernConfiguration extends Configuration {
        @JsonProperty private @NotEmpty String template;
        @JsonProperty private @NotEmpty String defaultName;

        public String getTemplate()    { return template; }
        public String getDefaultName() { return defaultName; }
    }

    // The actual service
    @Path("/hello-world")
    @Produces(MediaType.APPLICATION_JSON)
    public static class HelloWorldResource {
        private final AtomicLong counter = new AtomicLong();
        private final String template;
        private final String defaultName;

        public HelloWorldResource(JModernConfiguration cfg) {
            this.template = cfg.getTemplate();
            this.defaultName = cfg.getDefaultName();
        }

        @Timed // monitor timing of this service with Metrics
        @GET
        public Saying sayHello(@QueryParam("name") Optional<String> name) throws InterruptedException {
            final String value = String.format(template, name.or(defaultName));
            Thread.sleep(ThreadLocalRandom.current().nextInt(10, 500));
            return new Saying(counter.incrementAndGet(), value);
        }
    }

    // JSON (immutable!) payload
    public static class Saying {
        private long id;
        private @Length(max = 10) String content;

        public Saying(long id, String content) {
            this.id = id;
            this.content = content;
        }

        public Saying() {} // required for deserialization

        @JsonProperty public long getId() { return id; }
        @JsonProperty public String getContent() { return content; }
    }
}

我们做了一些改进。 首先,用一个不可变的 java 类来表示 JSON 对象。 其次,为服务添加了随机睡眠功能,以及增加了@Timed 注解,这样 Metrics 库就能自动监控报告我们服务的延迟。 最后,我们使用 DropWizard YAML配置我们的服务。 虽然这对于一个简单的 “Hello,World” 服务来说可能过于复杂了,但它可以作为复杂应用程序的基础。额外的代码为我们带来了监测,可配置性和类型安全。 为了使用配置,我们需要创建一个配置类,并对我们的构建文件进行一些调整。

template: Hello, %s!
defaultName: Stranger

然后,增加以下代码到 build.gradle,这是为了在运行代码时,能找到配置文件:

run {
    systemProperty "dropwizard.config", "build/resources/main/jmodern.yml"
}

最后,我们希望在 capsule 中默认包含配置文件,因此我们将添加以下部分:

from { sourceSets.main.resources }

同时,也把 System-Properties 进行调整:

System-Properties' : (run.systemProperties + ["dropwizard.config": '$CAPSULE_DIR/jmodern.yml']).collect { k,v -> "$k=$v" }.join(' '),

现在我们用 gradle capsule 构建部署 capsule,并使用 java -jar build/libs/jmodern-web.ja 启动服务器。 您现在可以在 http://localhost:8080/hello-worldhttp://localhost:8080/hello-world?name=Modern+Developer 测试服务。

如果想调整默认配置,只要在项目目录下创建 foo.yml 文件:

template: Howdy, %s!
defaultName: fella

使用这个配置文件,覆盖 dropwizard.config 属性:

java -Ddropwizard.config=foo.yml -jar build/libs/jmodern-web.jar

我们可以启动 VisualVM(请参阅第2部分),并查看应用服务报告,特别的,我们应用的时间花费:

1

我们打开 Dropwizard 管理控制台:

2

打开 http://localhost:8081/metrics,返回以下一个JSON对象:

3

就是这样!配置文件也可以用来修改Dropwizard的很多内部变量,如设置日志级别等等。有关详细信息,请参阅Dropizard文档

总而言之,Dropwizard 是一个精简、有趣的现代化微型框架,它可让你部署简单,配置轻松以及开箱即用的出色的监控能力。另一个有类似功能的框架是 Spring Boot。不幸的是,Boot 没有使用 JAX-RS 标准 API,但有一个项目试图修复这个问题。

Dropwizard具有极好的开箱即用体验,但更高级的用户可能会发现它也有一些限制(例如,Dropwizard 的某些组件很难被其他组件替代:比如日志引擎)。这些用户可能会发现将 Jersey, Jetty 和其他库进行组装是非常有必要的,并且可以自己制定管道,以构建一个最适合其组织的轻量级服务器。这样做应该不需要很多工作,而且只需要一次就可以适用所有自己的项目。Dropwizard 是一个很好的起点,如果它适合你(它应该在大多数情况下),你可以放心地坚持使用下去。在这篇文章中的大部分示例中我们使用 Dropwizard,但是示例中所做的你都可以单独使用Jetty,或者与Jersey结合使用来完成。而在 Dropwizard,更改配置和自动监控则无需额外的工作。

http 客户端

增加下面代码到构建文件:

compile 'io.dropwizard:dropwizard-client:0.7.0'

导入以下库到 jmoern.Main

import io.dropwizard.client.*;
import com.sun.jersey.api.client.Client;

增加下面代码到 JModernConfiguration

@Valid @NotNull @JsonProperty JerseyClientConfiguration httpClient = new JerseyClientConfiguration();
public JerseyClientConfiguration getJerseyClientConfiguration() { return httpClient; }

我们将实例化客户端,并注册一个新服务,我们将其称为 Consumer,并添加到 run 方法中:

Client client = new JerseyClientBuilder(env).using(cfg.getJerseyClientConfiguration()).build("client");
env.jersey().register(new ConsumerResource(client));

下面是我们的服务:

@Path("/consumer")
@Produces(MediaType.TEXT_PLAIN)