[翻译]现代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)
public static class ConsumerResource {
    private final Client client;

    public ConsumerResource(Client client) {
        this.client = client;
    }

    @Timed
    @GET
    public String consume() {
        Saying saying = client.resource(UriBuilder.fromUri("http://localhost:8080/hello-world").queryParam("name", "consumer").build())
                .get(Saying.class);
        return String.format("The service is saying: %s (id: %d)",  saying.getContent(), saying.getId());
    }
}

注意到方法返回的 JSON 对像是如何反序列化成 Saying 对象的;它也可以是 Map,string 以及其他类型(Dropwizard使用的是Jersey JAX-RS客户端的旧版本,新的API类似)。而且由于 Dropwizard 开箱即用地支持 Jersey JAX-RS 客户端,因此会自动发持请求的性能指标。

要测试我们的新服务,启动我们的应用程序( gradle run ,记住)并将浏览器指向 http://localhost:8080/consumer

所以 JAX-RS 标准也标准化了客户端的 API。但是,正如我们之前所说的,当谈到客户端 API 时,我们也可以使用非标准的API。一个颇受欢迎的HTTP客户端 Retrofit 是由 Square 提供的。如你所见,JAX-RS 客户端可以自动将 Java 对象序列化并反序列化为 JSON 对象(或 XML)。Retrofit 把这种转化用在 Java/REST 转换上(这种转换并不总是一件好事;领域模型的转换通常具有抽象漏洞,但如果你只限于用于简单的协议,应该会很有帮助),包括服务 URL,而不仅仅是 JSON 到 Java 接口的转换。不幸的是,Retrofit 使用与 JAX-RS(服务器)相同的注解名称。因此我们要在不同的包中定义,这会使我们的示例有点难看。幸运的是,Retrofit 有 Netflix 提供的称为 Feign 的克隆/衍生产品。Feign 和 Retrofit 之间的差异并对我来说并不完全清楚。尽管看起来 Retrofit 更广泛地被采用(它更成熟),而 Feign 更容易定制。无论如何,这两者非常相似,可以互换使用。

试试 Feign,将以下依赖添加到 build.gradle :

compile 'com.netflix.feign:feign-core:6.1.2'
compile 'com.netflix.feign:feign-jaxrs:6.1.2'
compile 'com.netflix.feign:feign-jackson:6.1.2'

导入到 Main:

import feign.Feign;
import feign.jackson.*;
import feign.jaxrs.*;

我们用 Feign 代替 JAX-RS:

Feign.Builder feignBuilder = Feign.builder()
        .contract(new JAXRSModule.JAXRSContract()) // we want JAX-RS annotations
        .encoder(new JacksonEncoder()) // we want Jackson because that's what Dropwizard uses already
        .decoder(new JacksonDecoder());
env.jersey().register(new ConsumerResource(feignBuilder));

现在我们的消费服务看起来如下:

@Path("/consumer")
@Produces(MediaType.TEXT_PLAIN)
public static class ConsumerResource {
    private final HelloWorldAPI hellowWorld;

    public ConsumerResource(Feign.Builder feignBuilder) {
        this.hellowWorld = feignBuilder.target(HelloWorldAPI.class, "http://localhost:8080");
    }

    @Timed
    @GET
    public String consume() {
        Saying saying = hellowWorld.hi("consumer");
        return String.format("The service is saying: %s (id: %d)",  saying.getContent(), saying.getId());
    }
}

最后,我们添加 HelloWorldAPI 接口,该接口把 REST API 说明定入代码中(你可以将接口定义放在我们的 Main 类中;不需要创建新的Java文件):

interface HelloWorldAPI {
    @GET @Path("/hello-world")
    Saying hi(@QueryParam("name") String name);

    @GET @Path("/hello-world")
    Saying hi();
}

此接口使用 JAX-RS 注解说明如何把方法转换 http 为请求。实际执行转换是由 Feign(或Retrofit)自动完成的。

启动应用后,访问 http://localhost:8080/consumer 以测试新的服务。

如果想看到更复杂的 REST API 是如何转换为Java代码的, 这个简单的例子演示使用 Retroift 消费 GitHub 的API,还有这里使用 Feign。 Retrofit 和 Feign 功能都非常丰富,可以很好地控制请求的转换和执行方式。此时,我会推荐 Retroift 而不是 Feign,因为 Retrofit 更成熟,它利用了高效的 NIO 网络 API,而 Feign使用慢速的 HttpURLConnection API( 更好的传输机制可以添加进 Feign 中,但我还没有找到)。

还有其他一些较底层的 HTTP 客户端 API(例如 Apache HTTP Client,Dropwizard 也直接支持),但在大多数情况下,我们刚才试过的高层次的 API(JAX-RS Client 或 Retorfit / Feign)效果最佳。

数据库访问

JDK 包含用于(关系)数据库访问的标准 API,称为 JDBC (Java数据库连接)。几乎所有的 SQL 数据库都支持 JDBC。但是 JDBC 是一个非常低级的 API,有时可能会令人厌烦。Java 还有一个标准的高级数据库访问 API - 实际上是一个ORM--被 JSR-220 和 JSR-317 叫做 JPA(Java Persistance API)。JPA的知名实现包括 HibernateOpenJPAEclipseLink 。请不要使用他们,我相信以后你会感谢我。并不是说他们工作的不好,是因为他们往往比他们的带来麻烦比价值更多。ORM 鼓励复杂的对象图和复杂的模式,这往往会导致生成非常复杂的 SQL 语句,这些语句很难优化。另外,ORM 并不以其出色的性能而闻名。

直接使用 JDBC 通常更好,但也许最好的方法是使用我们现在提供的工具之一。它位于低级 JDBC 和高级的 ORM 之间。它不是标准的,这意味着每个工具都有它自己的 API。但正如我们所说的,不使用标准 API 适合于客户端 API。在下面我们的例子中,我们使用H2 嵌入式数据库。

我们将从 JDBI 开始,这也由 Dropwizrd 直接支持。要有效地使用 JDBI ,你需要权衡最佳模式和简单代码,直到您达到一个很好的中间地带(JDBI 对于非常复杂的模式并不理想)。

我们添加这些依赖关系:

compile 'io.dropwizard:dropwizard-db:0.7.0'
compile 'io.dropwizard:dropwizard-jdbi:0.7.0'
runtime 'com.h2database:h2:1.4.178'

并且导入:

import io.dropwizard.db.*;
import io.dropwizard.jdbi.*;
import org.skife.jdbi.v2.*;
import org.skife.jdbi.v2.util.*;

然后,我们增加 DataSource 工厂类到 JModernConfiguration :

DBI dbi = new DBIFactory().build(env, cfg.getDataSourceFactory(), "db");
env.jersey().register(new DBResource(dbi));

为了配置数据库,我们需要将以下内容添加到 jmodern.yml

database:
  driverClass: org.h2.Driver
  url: jdbc:h2:mem:test
  user: u
  password: p

最后,让我们创建数据库资源:

@Path("/db")
@Produces(MediaType.APPLICATION_JSON)
public static class DBResource {
    private final DBI dbi;

    public DBResource(DBI dbi) {
        this.dbi = dbi;

        try (Handle h = dbi.open()) {
            h.execute("create table something (id int primary key auto_increment, name varchar(100))");
            String[] names = { "Gigantic", "Bone Machine", "Hey", "Cactus" };
            Arrays.stream(names).forEach(name -> h.insert("insert into something (name) values (?)", name));
        }
    }

    @Timed
    @POST @Path("/add")
    public Map<String, Object> add(String name) {
        try (Handle h = dbi.open()) {
            int id = h.createStatement("insert into something (name) values (:name)").bind("name", name)
                    .executeAndReturnGeneratedKeys(IntegerMapper.FIRST).first();
            return find(id);
        }
    }

    @Timed
    @GET @Path("/item/{id}")
    public Map<String, Object> find(@PathParam("id") Integer id) {
        try (Handle h = dbi.open()) {
            return h.createQuery("select id, name from something where id = :id").bind("id", id).first();
        }
    }

    @Timed
    @GET @Path("/all")
    public List<Map<String, Object>> all(@PathParam("id") Integer id) {
        try (Handle h = dbi.open()) {
            return h.createQuery("select * from something").list();
        }
    }
}

对于那些了解 JDBC 的人,这些代码有很多熟悉的和不同的地方。JDBI 有一个流畅的接口,并且方法返回Java集合,并将其自动地序列化为 JSON 对象。总之,这就像一个有趣的"现代"JDBC。

启动应用程序并将浏览器指向 http://localhost:8080/db/all 以查看所有条目,或者在http://localhost:8080/db/item/ 2 处查看第二个条目。然后,您也可以通过控制台创建新的条目:

curl --data Velouria http://localhost:8080/db/add

JDBI 还可以像 Retrofit 一样,提供一个数据库使用量身定制的定制界面。通过将 JDBI 将表行映射为 Java 对象,我们还可以获得一些小技巧。

这是我们的对象:

public static class Something {
    @JsonProperty public final int id;
    @JsonProperty public final String name;

    public Something(int id, String name) {
        this.id = id;
        this.name = name;
    }
}

@JsonProperty 注释将确保这个属性自动将它 JSON 序列化,但为了使 JDBI 能够与 Something 一起工作,我们还需要创建一个 ResultSetMapper ,它 将JDBC ResultSet 转换为 Something 对象:

public static class SomethingMapper implements ResultSetMapper<Something> {
    public Something map(int index, ResultSet r, StatementContext ctx) throws SQLException {
        return new Something(r.getInt("id"), r.getString("name"));
    }
}

现在有意思的事情就要开始了!这是我们的 DAO 类(或JDBI说法中的 SQL 对象 ) - JDBI SQL 对象是数据库就像 Retrofit 对于 REST 的改造:

@RegisterMapper(SomethingMapper.class)
interface ModernDAO {
    @SqlUpdate("insert into something (name) values (:name)")
    @GetGeneratedKeys
    int insert(@Bind("name") String name);

    @SqlQuery("select * from something where id = :id")
    Something findById(@Bind("id") int id);

    @SqlQuery("select * from something")
    List<Something> all();
}

那现在,我们新的数据库资源可以这样写:

@Path("/db")
@Produces(MediaType.APPLICATION_JSON)
public static class DBResource {
    private final ModernDAO dao;

    public DBResource(DBI dbi) {
        this.dao = dbi.onDemand(ModernDAO.class);

        try (Handle h = dbi.open()) {
            h.execute("create table something (id int primary key auto_increment, name varchar(100))");
            String[] names = { "Gigantic", "Bone Machine", "Hey", "Cactus" };
            Arrays.stream(names).forEach(name -> h.insert("insert into something (name) values (?)", name));
        }
    }

    @Timed
    @POST @Path("/add")
    public Something add(String name) {
        return find(dao.insert(name));
    }

    @Timed
    @GET @Path("/item/{id}")
    public Something find(@PathParam("id") Integer id) {
        return dao.findById(id);
    }

    @Timed
    @GET @Path("/all")
    public List<Something> all(@PathParam("id") Integer id) {
        return dao.all();
    }
}

JDBI并不是一个完整的 ORM 解决方案:它不会自动生成 SQL 语句,也不会自动生成完整的对象图,但它确使我们获得了数据库访问的快捷方法,其量级远低于任何 JPA 实现。

使用 JDBI 时,Dropwizard 会自动添加一个运行状况检查( http://localhost:8081/healthcheck),用于测试数据库的连通性,并用监控 DAO 的性能指标:

3

下面我们会看到的数据库访问库 jOOQ,它与 JDBI 流畅 API 类似(它没有与 JDB I的 SQL 对象类似的API),但它采用了不同的方法:它使用方法调用链而不是字符串,生成 SQ L语句(并且它可以生成的SQL兼容的多种数据库)。

我们将添加这个依赖关系:

compile 'org.jooq:jooq:3.3.2'

导入库:

import org.jooq.Record;
import org.jooq.RecordMapper;
import static org.jooq.impl.DSL.*;

在 run 方法中,注册数据库资源:

DataSource ds = cfg.getDataSourceFactory().build(env.metrics(), "db"); // Dropwizard will monitor the connection pool
env.jersey().register(new DBResource(ds));

我们的新 DBResource 如下所示:

@Path("/db")
@Produces(MediaType.APPLICATION_JSON)
public static class DBResource {
    private final DataSource ds;
    private static final RecordMapper<Record, Something> toSomething =
            record -> new Something(record.getValue(field("id", Integer.class)), record.getValue(field("name", String.class)));

    public DBResource(DataSource ds) throws SQLException {
        this.ds = ds;

        try (Connection conn = ds.getConnection()) {
            conn.createStatement().execute("create table something (id int primary key auto_increment, name varchar(100))");

            String[] names = { "Gigantic", "Bone Machine", "Hey", "Cactus" };
            DSLContext context = using(conn);
            Arrays.stream(names).forEach(name -> context.insertInto(table("something"), field("name")).values(name).execute());
        }
    }

    @Timed
    @POST @Path("/add")
    public Something add(String name) throws SQLException {
        try (Connection conn = ds.getConnection()) {
            // this does not work
            int id = using(conn).insertInto(table("something"), field("name")).values(name).returning(field("id"))
                       .fetchOne().into(Integer.class);
            return find(id);
        }
    }

    @Timed
    @GET @Path("/item/{id}")
    public Something find(@PathParam("id") Integer id) throws SQLException {
        try (Connection conn = ds.getConnection()) {
            return using(conn).select(field("id"), field("name")).from(table("something"))
                    .where(field("id", Integer.class).equal(id)).fetchOne().map(toSomething);
        }
    }

    @Timed
    @GET @Path("/all")
    public List<Something> all(@PathParam("id") Integer id) throws SQLException {
        try (Connection conn = ds.getConnection()) {
            return using(conn).select(field("id"), field("name")).from(table("something")).fetch().map(toSomething);
        }
    }
}

现在,jOOQ 还没有实现 DDL(像 create table 这样的 SQL 语句),所以你会注意到我们使用 JDBC 创建表。不过这也很好,因为 jOOQ 是作为一个JDBC包装器实现的,无论如何都需要 JDBC(我还没有能使add的正确工作的方法(可能是因为自动生成的主键的原因)jOOQ 的开发人员:如果你正在阅读这个,请帮帮忙)。

这个例子实际上并没有正确的使用 JOOQ 正义,因为它的最大优点是能够从数据库的 scheme 生成 class,并且能够以类型安全的方式执行我们之前完成的所有操作 - 以及更复杂的操作。对我个人来说,JOOQ 有点太智能了,但是如果你的模式很复杂,它可能是一个非常有用的工具。

依赖注入

依赖注入是否有用或无用取决于你问的对象。我相信 DI 在复杂的代码库中非常有用;对于简单代码来说,这不必要。Java 有一个由 JSR-330 指定的简单标准 DI API。JSR-330 有以下实现: Spring IoCGuiceDaggerSisu (建立在Guice之上)和 HK2 。这些实现都是由大公司或组织开发的。鉴于这种情况,人们往往面临着两难选择。我认为你不要害怕:如果你坚持JSR-330标准,或者稍有偏差的实现,您可以随时更改您的DI解决方案。但如果你想让你的应用程序完全由用户配置(XML文件的形式),选择Spring(这就是为什么我们选择Spring for Galaxy);如果都不是,那么从Dagger开始,只有当它不再满足你的需求时才去找别的东西。

我们来看看Dagger。首先,让我们添加Dagger依赖关系:

compile 'com.squareup.dagger:dagger:1.2.1'
compile 'com.squareup.dagger:dagger-compiler:1.2.1'

为了保持整洁,我们只留下 HelloWorldResource 。不过,这一次,我们不手动创建服务并将配置对象传递给它,而是使用Dagger 从 YAML 文件读取我们的配置,然后将它们注入到我们的服务中。

这是服务代码:

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

    HelloWorldResource() {
    }

    @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);
    }
}

请注意@Inject@Named 注释。这些是 JSR-330 标准的一部分,所以无论我们使用哪种 DI 工具,我们的服务代码都将保持不变。要实际连接并注入依赖关系,我们使用 Dagge r特定的模式。Dagger 在模块类中指定了依赖配置。这是我们的:

@Module(injects = HelloWorldResource.class)
class ModernModule {
    private final JModernConfiguration cfg;

    public ModernModule(JModernConfiguration cfg) {
        this.cfg = cfg;
    }

    @Provides @Named("template") String provideTemplate() {
        return cfg.getTemplate();
    }

    @Provides @Named("defaultName") String provideDefaultName() {
        return cfg.getDefaultName();
    }
}

Dagger 最有用的功能之一是它在编译时使用注释处理器验证所有依赖关系是否满足。例如,如果我们忘记定义 provideDefaultName ,那么当我们键入时,这就 是NetBeans 中显示的内容:

5

为了获得完整配置的 HelloWorldResource 实例,我们在应用程序的 run 方法中放入了这个实例:

ObjectGraph objectGraph = ObjectGraph.create(new ModernModule(cfg));
env.jersey().register(objectGraph.get(HelloWorldResource.class));

你会发现, ModernModule 类复制 JModernConfiguration 的一些行为。使用 @Module 简单注解JModernConfiguration ,以及使用 @Provides 注解 getTemplategetDefaultName 方法非常简单。Dagger 禁止子类型注解

高级主题:阻塞与非阻塞 VS 同步与异步

在这个话题上,我们需要对阻塞与非阻塞 API 的更多理论讨论。阻塞或同步是方法会阻塞调用线程直到它们完成。当然,阻塞(或非阻塞)的概念只有在这些方法可能需要很长时间才能完成时(例如几十毫秒到几十秒)才有意义。另一种类型的API,通常称为非阻塞,但在这里我们称它们为半阻塞(或半异步),它是在操作期间不会阻塞调用线程的方法。他们只启动一项操作并返回 Feature 对象。Feature 对象用于等待待等待操作成然后在方便的时间完成后面的操作。最后,第三种类型的 API-真正的非阻塞或异步 API,它也不会阻塞调用线程。但它的方法需要一个额外的参数 - 一个回调函数 ,它是在操作完成时将执行的代码(在某个未知的线程上)。有时候,Java API 混合了最后两种类型,既有回调又有 返因Feature对象。

必须明确:异步 API 的总是比阻塞的 API 更复杂(即使语言本身试图使回调更容易使用,通过使用如 promise,comprehensions, monad 等函数式方案)。除了支持多线程的 Clojure 之外,异步的问题在 Java 这样的语言中尤其糟糕,包括基本上所有其他的 JVM 语言。我们在这里不会详细讨论 clojure 不限制副作用的问题。在这些语言中使用非阻塞 API 需要严格的规范,并且需要对复杂的并发问题有清晰的理解。阻塞 API 则没有这些问题。

为什么有人会使用异​​步 API?答案很简单:性能。更深刻一点,内核线程进行任务切换的成本不可忽略(这里不是说可以快速释放线程内存堆栈,快速释放线程堆栈这将更好地用于数据高速缓存)。现代 Web 应用程序通常会将实际处理委托给无数的服务,有些会做离线 map-reduce,其他可能会做一些在线处理,面向客户端的 Web 服务器的主要功能是协调:它调用许多其他服务并组装数据。它几乎不做任何处理,但它执行大量的 IO 操作 - 有些可以并行完成,有些需要连续调用。这意味着 Web 服务器在相对较少的 CPU 工作时间内会生成很多线程调度事件(线程阻塞和解除阻塞),这种时候,操作系统的线程调度开销变得繁重。因此,人们为了解决这个内核线程调度性能问题而将代码置于异步 API 这种不自然的扭曲之中。一些现代 We b框架/库也非常喜欢使用非阻塞 API(我们没有讨论过其中的任何一个,因为我们说明,他们都是错误的)。

这是错误的方法 。为了迎合不合理的实现,人们放弃了适当的抽象(线程),而不是简单地修复不合理实现。轻量级(或用户级)线程已在 Erlang,Go 中使用,现在通过 Quasar 库在 JVM 中使用 - 可让您使用简单的阻塞 API,而不存在任何性能问题。

这种情况在计算机科学中非常罕见的。一种充满了折衷和警告的导步方法几乎总是击败另一种同步方法。异步代码与同步代码相比具有许多缺点和绝对劣势。即使轻量级线程的不完美实现也比异步编程更好,特别是当语言对共享状态突变不做防范时。这个规则可能有一些例外(毕竟,在 C S中,即使绝对不是绝对如此),但它们远少于建议使用 goto 语句时的情况。

同步和异步是可以相互转换的(每个都可以使用“恒定时间”转换转换为另一个),但同步对人类来说是更好的抽象,我可以证明这一点。我们来看两个 API:

interface Sync {
    Object pull();
}

和:

interface Async {
    void push(Callback cb);
}

interface Callback {
    void got(Object obj);
}

现在让我们使用 Sync 实现 Async:

Async syncToAsync(Sync sync) {
    return new Async() {
        public void push(final Callback cb) {
            new Thread(() -> {
                  for(;;)
                      cb.got(sync.pull());
              }).start();
        }
    }
}

现在,用您最喜欢的编程语言实现相反的功能,即将 Async 转换为 Sync 。这将更加棘手,总是需要引入一些中间数据存储,如队列或缓冲区。当然,你需要考虑到 Callback.got 可以在任何线程上调用,所以你需要考虑与该数据结构的并发性。因此,从AsyncSync 的转换不仅不那么简单,而且引入了不必要的数据存储:如果真没有引入多余的数据存储,是因为它可能已经内置到系统中(例如以IO缓冲区的形式)。所以 Async 使用 Sync 简单的实现,但是相反的转换既浪费又浪费时间,并且需要管理并发。但这对限制或管理副作用的语言(如 Clojure 或 Haskell )来说不是什么问题。

Comsat 项目将标准(和非标准但良好的)JavaWeb 相关 API 与 Quasar fibers(轻量级线程)集成在一起。Comsat 的下一个版本将支持本文讨论的工具(可能有 jOOQ 和 Retrofit / Feign例外),这样你就可以编写相同简单的阻塞代码,但可以获得异步代码的性能和可伸缩性优势。在未来的博客文章中,我们将展示 Comsat 如何不破坏你的代码,同时让您的应用程序具更好的可伸缩性。

高级主题:使用Web Actor与Web服务交互

虽然通常你应该坚持使用标准的服务器 API,但有时候替代方案会带来显着的优势。这里没有涉及的主题之一是使用 WebSocket 或 SSE 等技术的交互式 Web 服务。虽然 Java 的标准 API 支持两者,但是特别是使用 WebSocket 可能会导致复杂的并发问题,因为标准 Java WebSocket API( JSR-356 )是异步的。这意味着 WebSocket 消息可能会同时到达服务器端,比如来自同一用户的 HTTP 请求。这样的化,异步 API 要管理可变的共享状态,这种情状很糟糕。 Comsat 提供了一种称为 Web Actors 的 API,它能为每一个用户对话分配一个 actor,它意味着接收同步化,使得状态管理更容易。要了解有关Web Actors的更多信息,请阅读介绍性博客文章

结论

这篇就结束了“现代 Java 开发的意见指南”(尽管我可能会发布一个回应反馈的文章)。我希望你喜欢阅读它,就像我喜欢写它一样。我希望我能够传达出 Java 生态系统不仅是巨大的,而且还充满活力和与时俱进:用 Lambdas 和流代替冗长的数据操作代码; Markdown 取代 HTML;fiber,channel 和 actor 取代锁和回调;简单的嵌入式服务器取代了重量级,笨重的应用服务器。在所有这些功能下面,是强大,灵活的 JVM,它强调性能和监控,它能支持运行时代码注入和替换。

原文地址:An Opinionated Guide to Modern Java, Part 3: Web Development


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

posted @ 2018-04-07 23:36  htoooth  阅读(1010)  评论(0编辑  收藏  举报