Dropwizard官方教程(二) 核心

Dropwizard核心

dropwizard-core模块为您提供了大多数应用程序所需的一切。

包括:

  • Jetty,一种高性能的HTTP服务器。
  • Jersey,功能齐全的RESTful Web框架。
  • Jackson,JVM的最佳JSON库。
  • Metrics,一个出色的应用程序指标库。
  • Guava,谷歌的优秀实用程序库。
  • Logback,Log4j的继承者,是Java最广泛使用的日志框架。
  • Hibernate Validator,Java Bean Validation标准的参考实现。

Dropwizard主要由胶水代码组成,可自动连接和配置这些组件。

组织您的项目

如果你打算开发一个客户端库供其他开发者访问你的服务,我们建议你把项目分为三个Maven的模块:project-apiproject-client,和 project-application

project-api应包含您的表示层 ; project-client应该使用表示层的这些类和HTTP客户端为您的应用程序实现完整的客户端,project-application应提供实际的应用程序实现,包括 资源

为了给出这个项目结构的具体示例,假设我们想要创建一个类似Stripe的API,客户端可以发出charges,服务器将charges返回给客户端。 stripe-api项目将保留我们的Charge对象,因为服务器和客户端都希望使用费用并促进代码重用,Charge对象存储在共享模块中。 stripe-app是Dropwizard应用程序。stripe-client抽象出原始的HTTP交互和反序列化逻辑。stripe-client的用户只需将Charge对象传递给函数和幕后,而不是使用HTTP客户端,stripe-client将调用HTTP端点。客户端库还可以处理连接池,并且可以提供更友好的解释错误消息的方式。通常,为您的应用程序分发客户端库将有助于其他开发人员更快地与服务集成。

如果您没有分发用于开发的客户端库的规划,可以把project-apiproject-application结合成一个单一的项目,比如像这样:

  • com.example.myapplication
    
    • api表示层。请求和响应实体。
    • cli命令
    • client:访问外部HTTP服务的客户端
    • core:业务实现; 其中未在API中使用的对象(如POJO,验证,加密等)驻留在此处。
    • jdbi数据库访问类
    • health健康检查
    • resources资源
    • MyApplication应用程序
    • MyApplicationConfiguration配置

应用

不出所料,Dropwizard应用程序的主要入口是Application该类。每个 Application都有一个名称,主要用于呈现命令行界面。在您的Application构造函数中,可以将BundlesCommands添加到您的应用程序中。

配置

Dropwizard提供了许多内置的配置参数。它们在示例项目的配置配置参考中有详细记录。

每个Application子类都有一个类型参数:Configuration 子类。通常位于应用程序主程序包的根目录。例如,您的User应用程序将具有两个类:UserApplicationConfiguration扩展自Configuration,和 UserApplication扩展自Application<UserApplicationConfiguration>

当您的应用程序运行配置命令(server命令)时,Dropwizard将解析提供的YAML配置文件,并通过将YAML字段名称映射到对象字段名称来构建应用程序配置类的实例。

注意

如果您的配置文件没有以.yml或者.yaml结束,Dropwizard尝试将其解析为JSON文件。

为了保持配置文件和类的可管理性,我们建议将相关配置参数分组到独立的配置类中。例如,如果您的应用程序需要一组配置参数才能连接到消息队列,我们建议您创建一个新MessageQueueFactory类:

public class MessageQueueFactory {
    @NotEmpty
    private String host;

    @Min(1)
    @Max(65535)
    private int port = 5672;

    @JsonProperty
    public String getHost() {
        return host;
    }

    @JsonProperty
    public void setHost(String host) {
        this.host = host;
    }

    @JsonProperty
    public int getPort() {
        return port;
    }

    @JsonProperty
    public void setPort(int port) {
        this.port = port;
    }

    public MessageQueueClient build(Environment environment) {
        MessageQueueClient client = new MessageQueueClient(getHost(), getPort());
        environment.lifecycle().manage(new Managed() {
            @Override
            public void start() {
            }

            @Override
            public void stop() {
                client.close();
            }
        });
        return client;
    }
}

在此示例中,我们的工厂将自动将我们的MessageQueueClient与应用程序的Environment生命周期连接起来。

然后,您的Configuration子类可以将其作为成员字段包含在内:

public class ExampleConfiguration extends Configuration {
    @Valid
    @NotNull
    private MessageQueueFactory messageQueue = new MessageQueueFactory();

    @JsonProperty("messageQueue")
    public MessageQueueFactory getMessageQueueFactory() {
        return messageQueue;
    }

    @JsonProperty("messageQueue")
    public void setMessageQueueFactory(MessageQueueFactory factory) {
        this.messageQueue = factory;
    }
}

然后您的Application子类可以使用您的工厂直接构造消息队列的客户端:

public void run(ExampleConfiguration configuration,
                Environment environment) {
    MessageQueueClient messageQueue = configuration.getMessageQueueFactory().build(environment);
}

然后,在应用程序的YAML文件中,您可以使用嵌套messageQueue字段:

messageQueue:
  host: mq.example.com
  port: 5673

@NotNull@NotEmpty@Min@Max,和@ValidDropwizard验证功能的一部分。如果您的YAML配置文件的messageQueue.host字段丢失(或者是空字符串),Dropwizard将拒绝启动并输出描述问题的错误消息。

一旦您的应用程序解析了YAML文件并构建了它的Configuration实例,Dropwizard就会调用您的Application子类来初始化您的应用程序的Environment

注意

您可以通过在启动应用程序时传递特殊的Java系统属性来覆盖配置。覆盖必须以dw.开头,然后是覆盖配置值的路径。

例如,要覆盖日志级别,您可以像这样启动应用程序:

java -Ddw.logging.level=DEBUG server my-config.json

即使配置文件中不存在相关配置设置,这也会有效,在这种情况下会添加。

您可以覆盖对象数组中的配置,如下所示:

java -Ddw.server.applicationConnectors[0].port=9090 server my-config.json

您可以在map中覆盖配置,如下所示:

java -Ddw.database.properties.hibernate.hbm2ddl.auto=none server my-config.json

您还可以使用','字符作为数组元素分隔符来覆盖作为字符串数组的配置。例如,要覆盖配置中的字符串数组myapp.myserver.hosts的配置设置,您可以像这样启动服务:

java -Ddw.myapp.myserver.hosts=server1,server2,server3 server my-config.json

如果您需要在其中一个值中使用','字符,则可以使用'\,'来转义它。

数组覆盖工具仅处理作为简单字符串数组的配置元素。此外,设置必须已作为数组存在于配置文件中; 如果配置文件中不存在被覆盖的配置,则此机制将不起作用。如果它不存在或不是数组设置,它将作为简单的字符串设置添加,包括作为字符串一部分的','字符。

环境变量

dropwizard-configuration模块还提供了使用SubstitutingSourceProviderEnvironmentVariableSubstitutor,用环境变量替换配置项的功能。

public class MyApplication extends Application<MyConfiguration> {
    // [...]
    @Override
    public void initialize(Bootstrap<MyConfiguration> bootstrap) {
        // Enable variable substitution with environment variables
        bootstrap.setConfigurationSourceProvider(
                new SubstitutingSourceProvider(bootstrap.getConfigurationSourceProvider(),
                                                   new EnvironmentVariableSubstitutor(false)
                )
        );

    }

    // [...]
}

需要在配置文件中明确写入待替换的配置项,并遵循Apache Commons Lang库中的StrSubstitutor的替换规则。

mySetting: ${DW_MY_SETTING}
defaultSetting: ${DW_DEFAULT_SETTING:-default value}

通常SubstitutingSourceProvider不限于替换环境变量,可以通过传递自定义StrSubstitutor来使用任意值替换配置源中的变量。

SSL

Dropwizard内置了SSL支持。您需要提供自己的java密钥库,这不在本文档的范围内(keytool是您需要的命令,Jetty的文档可以帮助您入门)。您可以在Dropwizard示例项目中使用测试密钥库。

server:
  applicationConnectors:
    - type: https
      port: 8443
      keyStorePath: example.keystore
      keyStorePassword: example
      validateCerts: false

默认情况下,仅允许TLSv1.2密码套件。较早版本的cURL,Java 6和7以及其他客户端可能无法与允许的密码套件进行通信,但这是一个有意识的决定,为了安全性,牺牲了互操作性。

Dropwizard通过指定自定义密码套件列表来实现变通方法。如果未指定支持的协议或密码套件列表,则使用JVM默认值。如果未指定排除的协议或密码套件列表,则默认值将从Jetty继承。

以下排除的密码套件列表将允许TLSv1和TLSv1.1客户端协商类似于Dropwizard 1.0之前的连接。

server:
  applicationConnectors:
    - type: https
      port: 8443
      excludedCipherSuites:
        - SSL_RSA_WITH_DES_CBC_SHA
        - SSL_DHE_RSA_WITH_DES_CBC_SHA
        - SSL_DHE_DSS_WITH_DES_CBC_SHA
        - SSL_RSA_EXPORT_WITH_RC4_40_MD5
        - SSL_RSA_EXPORT_WITH_DES40_CBC_SHA
        - SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA
        - SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA

由于Jetty的9.4.8版本(Dropwizard 1.2.3)通过Google的Conscrypt支持本机SSL,后者使用BoringSSL (Google的OpenSSL分支)来处理加密。您可以在Dropwizard中通过在应用中注册provider来启用它:

<dependency>
    <groupId>org.conscrypt</groupId>
    <artifactId>conscrypt-openjdk-uber</artifactId>
    <version>${conscrypt.version}</version>
</dependency>
static {
    Security.addProvider(new OpenSSLProvider());
}

并在配置中设置JCE提供程序:

server:
  type: simple
  connector:
    type: https
    jceProvider: Conscrypt

对于HTTP/2服务器,您需要添加ALPN Conscrypt提供程序作为依赖项。

<dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-alpn-conscrypt-server</artifactId>
</dependency>

引导 Bootstrapping

在Dropwizard应用程序可以提供命令行界面,解析配置文件或作为服务器运行之前,它必须首先经历引导阶段。此阶段对应于Application子类的initialize方法。您可以添加BundlesCommands或注册Jackson模块,以允许您将自定义类型包含在配置类中。

环境 Environments

Dropwizard Environment包含应用程序提供的所有资源,servlet,过滤器, 健康检查,Jersey提供程序,托管对象任务和Jersey属性。

每个Application子类都实现一个run方法。这是您应该创建新资源实例的地方,并将它们添加到给定的Environment类:

@Override
public void run(ExampleConfiguration config,
                Environment environment) {
    // encapsulate complicated setup logic in factories
    final Thingy thingy = config.getThingyFactory().build();

    environment.jersey().register(new ThingyResource(thingy));
    environment.healthChecks().register("thingy", new ThingyHealthCheck(thingy));
}

保持run方法清洁非常重要,因此如果创建某个实例很复杂,就像Thingy上面的类一样,将该逻辑提取到工厂中。

健康检查 Health Checks

健康检查是一种运行时测试,可用于验证应用程序在其生产环境中的行为。例如,您可能希望确保数据库客户端已连接到数据库:

public class DatabaseHealthCheck extends HealthCheck {
    private final Database database;

    public DatabaseHealthCheck(Database database) {
        this.database = database;
    }

    @Override
    protected Result check() throws Exception {
        if (database.isConnected()) {
            return Result.healthy();
        } else {
            return Result.unhealthy("Cannot connect to " + database.getUrl());
        }
    }
}

然后,您可以将此健康检查添加到应用程序的环境中:

environment.healthChecks().register("database", new DatabaseHealthCheck(database));

通过在管理端口上发送GET请求,/healthcheck您可以运行这些检查并查看结果:

$ curl http://dw.example.com:8081/healthcheck
{"deadlocks":{"healthy":true},"database":{"healthy":true}}

如果所有健康检查都报告成功,则返回200OK。如果有任何失败, 则返回带有错误消息和异常堆栈跟踪(如果抛出异常)的500 Internal Server Error

所有Dropwizard应用程序都附带默认安装的deadlocks健康检查,它使用Java 1.6的内置线程死锁检测来确定是否有任何线程死锁。

托管对象 Managed Objects

大多数应用程序涉及需要启动和停止的对象:线程池,数据库连接等.Dropwizard为此提供Managed接口。您可以让有类似需求的类实现#start()#stop()方法,或者编写一个包装类来执行此操作。向Managed应用程序添加实例,将Environment对象的生命周期与应用程序的HTTP服务器的生命周期联系起来。在服务器启动之前,将调用#start()。在服务器停止之后(以及在其正常关闭期之后),调用#stop()

例如,一个需要启动和停止的Riak客户端:

public class RiakClientManager implements Managed {
    private final RiakClient client;

    public RiakClientManager(RiakClient client) {
        this.client = client;
    }

    @Override
    public void start() throws Exception {
        client.start();
    }

    @Override
    public void stop() throws Exception {
        client.stop();
    }
}
public class MyApplication extends Application<MyConfiguration>{
    @Override
    public void run(MyApplicationConfiguration configuration, Environment environment) {
        RiakClient client = ...;
        RiakClientManager riakClientManager = new RiakClientManager(client);
        environment.lifecycle().manage(riakClientManager);
    }
}

如果RiakClientManager#start()抛出异常 - 例如,连接到服务器的错误 - 您的应用程序将无法启动,并将记录完整的异常。如果RiakClientManager#stop()抛出异常,将记录异常但您的应用程序仍然可以关闭。

应该注意的是,Environment具有内置的工厂方法ExecutorServiceScheduledExecutorService的管理的实例。查看LifecycleEnvironment#executorServiceLifecycleEnvironment#scheduledExecutorService了解详情。

绑定 Bundles

Dropwizard bundle是一组可重用的功能,用于定义应用程序的一套行为。例如,AssetBundledropwizard-assets模块提供了一种简单的方法,可以将应用程序src/main/resources/assets目录中的静态资源作为应用程序中的/assets/*(或任何其他路径)可用的文件提供。

可配置的绑定

一些捆绑包需要配置参数。这些bundle实现ConfiguredBundle并将要求您的应用程序的Configuration子类实现特定的接口。

例如:给定配置的bundle MyConfiguredBundleMyConfiguredBundleConfig。您的应用程序的Configuration子类需要实现MyConfiguredBundleConfig

public class MyConfiguredBundle implements ConfiguredBundle<MyConfiguredBundleConfig>{

    @Override
    public void run(MyConfiguredBundleConfig applicationConfig, Environment environment) {
        applicationConfig.getBundleSpecificConfig();
    }

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

    }
}

public interface MyConfiguredBundleConfig{

    String getBundleSpecificConfig();

}

提供资产Assets

您的应用程序静态资产可以从根路径提供,但不能同时提供。当使用Dropwizard支持Javascript应用程序时,往往都需要提供。要实现它,请将您的应用程序移动到子URL。

server:
  rootPath: /api/

注意

如果使用Simple服务器配置,则rootPath相对applicationContextPath计算 。因此,您可以从/application/api/路径访问您的API

然后使用扩展AssetsBundle构造函数从根路径提供assets文件夹中的资源 。index.htm作为默认页面。

@Override
public void initialize(Bootstrap<HelloWorldConfiguration> bootstrap) {
    bootstrap.addBundle(new AssetsBundle("/assets/", "/"));
}

AssetBundle添加到应用程序时,它使用默认名称assets注册为servlet 。如果应用程序需要具有多个AssetBundle 实例,则应使用扩展构造函数为其指定唯一名称AssetBundle

@Override
public void initialize(Bootstrap<HelloWorldConfiguration> bootstrap) {
    bootstrap.addBundle(new AssetsBundle("/assets/css", "/css", null, "css"));
    bootstrap.addBundle(new AssetsBundle("/assets/js", "/js", null, "js"));
    bootstrap.addBundle(new AssetsBundle("/assets/fonts", "/fonts", null, "fonts"));
}

SSL Reload

通过注册SslReloadBundle,您的应用程序可以在运行时重新加载新的证书信息,因此无需重新启动。

@Override
public void initialize(Bootstrap<HelloWorldConfiguration> bootstrap) {
    bootstrap.addBundle(new SslReloadBundle());
}

触发重新加载发送POST请求ssl-reload

curl -k -X POST 'https://localhost:<admin-port>/tasks/ssl-reload'

Dropwizard在执行重新加载时将使用相同的https配置(密钥库位置,密码等)。

注意

如果新证书有任何问题(例如密钥库中的密码错误),则不会加载新证书。因此,如果应用程序和管理端口使用不同的证书,并且其中一个证书无效,则不会重新加载任何证书。

重新加载失败时会返回http 500错误,因此请确保使用用于触发证书重新加载的任何工具来捕获此错误,并向相应的管理员发出警报。如果情况没有得到纠正,下次应用程序停止时,它将无法启动!

命令 Commands

命令是Dropwizard基于命令行上提供的参数运行的基本操作。例如server,内置命令会旋转启动服务器并运行您的应用程序。每个Command子类都有一个名称和一组命令行选项,Dropwizard将使用这些选项来解析给定的命令行参数。

下面是一个关于如何添加命令并让Dropwizard识别它的示例。

public class MyCommand extends Command {
    public MyCommand() {
        // The name of our command is "hello" and the description printed is
        // "Prints a greeting"
        super("hello", "Prints a greeting");
    }

    @Override
    public void configure(Subparser subparser) {
        // Add a command line option
        subparser.addArgument("-u", "--user")
                .dest("user")
                .type(String.class)
                .required(true)
                .help("The user of the program");
    }

    @Override
    public void run(Bootstrap<?> bootstrap, Namespace namespace) throws Exception {
        System.out.println("Hello " + namespace.getString("user"));
    }
}

一旦我们在initialize应用程序阶段添加它,Dropwizard就会识别我们的命令。

public class MyApplication extends Application<MyConfiguration>{
    @Override
    public void initialize(Bootstrap<DropwizardConfiguration> bootstrap) {
        bootstrap.addCommand(new MyCommand());
    }
}

要调用新功能,请运行以下命令:

java -jar <jarfile> hello dropwizard

可配置的命令

有些命令需要访问配置参数,需要继承ConfiguredCommand,并且应该使用应用程序的Configuration类作为其类型参数。默认情况下,Dropwizard会将命令行上的最后一个参数视为YAML配置文件的路径,解析并验证它,并为您的命令提供配置类的实例。

ConfiguredCommand可以指定其他命令行选项,同时将最后一个参数保留为YAML配置的路径。

@Override
public void configure(Subparser subparser) {
    super.configure(subparser);

    // Add a command line option
    subparser.addArgument("-u", "--user")
            .dest("user")
            .type(String.class)
            .required(true)
            .help("The user of the program");
}

对于命令行的更高级用法(例如,指定配置文件位置-c),请根据需要调整ConfiguredCommand类。

任务 Tasks

Task是应用程序通过HTTP请求在管理端口上提供访问的运行时操作。所有Dropwizard应用程序都有gc任务:该任务明确触发JVM的垃圾收集(例如,这非常有用,用于在非高峰时间或在给定应用程序不在轮换时运行完整垃圾收集); 和log-level任务,它在运行时配置任意数量的logger的级别(类似于Logback的JmxConfigurator)。Task的执行方法 可以与注释@Timed@Metered@ExceptionMetered结合使用。Dropwizard将自动记录有关您任务的运行时信息。这是一个基本的任务类:

public class TruncateDatabaseTask extends Task {
    private final Database database;

    public TruncateDatabaseTask(Database database) {
        super("truncate");
        this.database = database;
    }

    @Override
    public void execute(ImmutableMultimap<String, String> parameters, PrintWriter output) throws Exception {
        this.database.truncate();
    }
}

然后,您可以将此任务添加到应用程序的环境中:

environment.admin().addTask(new TruncateDatabaseTask(database));

可以通过向管理端口发送POST请求来运行任务/tasks/{task-name}。该任务将接收任何查询参数作为参数。例如:

$ curl -X POST http://dw.example.com:8081/tasks/gc
Running GC...
Done!

您还可以扩展PostBodyTask以创建带有body参数的任务。这是一个例子:

public class EchoTask extends PostBodyTask {
    public EchoTask() {
        super("echo");
    }

    @Override
    public void execute(ImmutableMultimap<String, String> parameters, String postBody, PrintWriter output) throws Exception {
        output.write(postBody);
        output.flush();
    }
}

日志 Logging

Dropwizard使用Logback作为其日志记录后端。它提供了一个slf4j实现,甚至封装了java.util.logging,Log4j和Apache Commons Logging所有用法。

slf4j提供以下日志记录级别:

  • ERROR

    可能仍允许应用程序继续运行的错误事件。

  • WARN

    潜在有害的情况。

  • INFO

    信息性消息,突出显示粗粒度级别的应用程序进度。

  • DEBUG

    对调试应用程序最有用的细粒度信息事件。

  • TRACE

    DEBUG级别更细粒度的信息事件。

注意

如果您不想使用Logback,可以将其从Dropwizard中排除并使用备用日志记录配置:

  • 从dropwizard-core工件中排除Logback

    <dependency>
        <groupId>io.dropwizard</groupId>
        <artifactId>dropwizard-core</artifactId>
        <version>{$dropwizard.version}</version>
        <exclusions>
            <exclusion>
                <groupId>ch.qos.logback</groupId>
                <artifactId>logback-classic</artifactId>
            </exclusion>
            <exclusion>
                <groupId>ch.qos.logback</groupId>
                <artifactId>logback-access</artifactId>
            </exclusion>
            <exclusion>
                <groupId>org.slf4j</groupId>
                <artifactId>log4j-over-slf4j</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    
  • 在Dropwizard配置中将日志配置标记为外部

    server:
      type: simple
      applicationContextPath: /application
      adminContextPath: /admin
      requestLog:
        type: external
    logging:
      type: external
    
  • 在应用程序中禁用bootstrapping Logback

    public class ExampleApplication extends Application<ExampleConfiguration> {
    
        @Override
        protected void bootstrapLogging() {
        }
    }
    

日志格式

Dropwizard的日志格式有一些特定的目标:

  • 人类可读的。
  • 机器可解析。
  • 对于操作人员来说,使用标准的UNIXy工具(例如tailgrep)可以很容易地弄清楚问题。

日志记录输出如下所示:

TRACE [2010-04-06 06:42:35,271] com.example.dw.Thing: Contemplating doing a thing.
DEBUG [2010-04-06 06:42:35,274] com.example.dw.Thing: About to do a thing.
INFO  [2010-04-06 06:42:35,274] com.example.dw.Thing: Doing a thing
WARN  [2010-04-06 06:42:35,275] com.example.dw.Thing: Doing a thing
ERROR [2010-04-06 06:42:35,275] com.example.dw.Thing: This may get ugly.
! java.lang.RuntimeException: oh noes!
! at com.example.dw.Thing.run(Thing.java:16)
!

一些注意事项:

  • 所有时间戳均采用UTC和ISO 8601格式。

  • 您可以非常轻松地grep特定级别的消息:

    tail -f dw.log | grep '^WARN'
    
  • 您可以非常轻松地grep来自特定类或包的消息:

    tail -f dw.log | grep 'com.example.dw.Thing'
    
  • 您甚至可以提取完整的异常堆栈跟踪以及随附的日志消息:

    tail -f dw.log | grep -B 1 '^\!'
    
  • 前缀!并不能适用于系统日志,因为堆栈跟踪是和主消息分开发送的。而是使用t(这是Logback附带的SyslogAppender的默认值)。这可以在定义appender时使用stackTracePrefix选项进行配置。

配置 Configuration

您可以指定默认日志级别,覆盖YAML配置文件中其他logger的级别,甚至为它们指定appender。后一种形式的配置是优选的,但前者也是可接受的。

# Logging settings.
logging:

  # The default level of all loggers. Can be OFF, 
  # ERROR, WARN, INFO, DEBUG, TRACE, or ALL.
  level: INFO

  # Logger-specific levels.
  loggers:

    # Overrides the level of com.example.dw.Thing 
    # and sets it to DEBUG.
    "com.example.dw.Thing": DEBUG

    # Enables the SQL query log and redirect it to a separate file
    "org.hibernate.SQL":
      level: DEBUG
      # This line stops org.hibernate.SQL (or anything 
      # under it) from using the root logger
      additive: false
      appenders:
        - type: file
          currentLogFilename: ./logs/example-sql.log
          archivedLogFilenamePattern: ./logs/example-sql-%d.log.gz
          archivedFileCount: 5

控制台日志

默认情况下,Dropwizard应用程序会往STDOUT记录INFO级别更高级别的日志。您可以通过编辑loggingYAML配置文件的部分来配置它:

logging:
  appenders:
    - type: console
      threshold: WARN
      target: stderr

在上面,我们只是记录WARNERROR消息到STDERR设备。

文件日志

Dropwizard还可以记录到一组自动轮换的日志文件。这是生产环境的推荐配置:

logging:

  appenders:
    - type: file
      # The file to which current statements will be logged.
      currentLogFilename: ./logs/example.log

      # When the log file rotates, the archived log will be renamed to this and gzipped. The
      # %d is replaced with the previous day (yyyy-MM-dd). Custom rolling windows can be created
      # by passing a SimpleDateFormat-compatible format as an argument: "%d{yyyy-MM-dd-hh}".
      archivedLogFilenamePattern: ./logs/example-%d.log.gz

      # The number of archived files to keep.
      archivedFileCount: 5

      # The timezone used to format dates. HINT: USE THE DEFAULT, UTC.
      timeZone: UTC

系统日志

最后,Dropwizard还可以将日志记录到syslog中。

注意

由于Java不使用本机syslog绑定,因此syslog服务器必须具有开放的网络套接字。

logging:

  appenders:
    - type: syslog
      # The hostname of the syslog server to which statements will be sent.
      # N.B.: If this is the local host, the local syslog instance will need to be configured to
      # listen on an inet socket, not just a Unix socket.
      host: localhost

      # The syslog facility to which statements will be sent.
      facility: local0

您可以组合任意数量的不同appenders,包括具有不同配置的同一个appender的多个实例:

logging:

  # Permit DEBUG, INFO, WARN and ERROR messages to be logged by appenders.
  level: DEBUG

  appenders:
    # Log warnings and errors to stderr
    - type: console
      threshold: WARN
      target: stderr

    # Log info, warnings and errors to our apps' main log.
    # Rolled over daily and retained for 5 days.
    - type: file
      threshold: INFO
      currentLogFilename: ./logs/example.log
      archivedLogFilenamePattern: ./logs/example-%d.log.gz
      archivedFileCount: 5

    # Log debug messages, info, warnings and errors to our apps' debug log.
    # Rolled over hourly and retained for 6 hours
    - type: file
      threshold: DEBUG
      currentLogFilename: ./logs/debug.log
      archivedLogFilenamePattern: ./logs/debug-%d{yyyy-MM-dd-hh}.log.gz
      archivedFileCount: 6

JSON日志格式

您可能更喜欢以结构化格式(如JSON)生成日志,以便通过分析软件或BI软件进行处理。为此,你需要将一个模块添加到项目中以支持JSON布局:

<dependency>
    <groupId>io.dropwizard</groupId>
    <artifactId>dropwizard-json-logging</artifactId>
    <version>${dropwizard.version}</version>
</dependency>

在配置文件中设置JSON布局。

对于一般日志:

logging:
  appenders:
    - type: console
      layout:
        type: json

json布局将产生以下日志消息:

{"timestamp":1515002688000, "level":"INFO","logger":"org.eclipse.jetty.server.Server","thread":"main","message":"Started @6505ms"}

对于请求记录:

server:
  requestLog:
    appenders:
      - type: console
        layout:
          type: access-json

access-json布局将产生以下日志消息:

{"timestamp":1515002688000, "method":"GET","uri":"/hello-world", "status":200, "protocol":"HTTP/1.1","contentLength":37,"remoteAddress":"127.0.0.1","requestTime":5, "userAgent":"Mozilla/5.0"}

通过HTTP配置日志

可以在Dropwizard应用程序的运行时通过LogConfigurationTask更改活动日志级别。例如,要为单个Logger配置日志级别:

curl -X POST -d "logger=com.example.helloworld&level=INFO" http://localhost:8081/tasks/log-level

日志过滤器

仅仅因为语句具有INFO级别,并不意味着它应该与其他INFO语句一起记录。可以创建日志记录过滤器,在编写日志语句之前拦截它们并确定它们是否被允许记录。日志过滤器可以用于常规语句和请求日志语句。以下示例将用于请求日志,因为有许多原因可能会从日志中排除某些请求:

  • 仅记录具有大型body的请求
  • 仅记录缓慢的请求
  • 仅记录导致非2xx状态代码的请求
  • 排除包含URL中敏感信息的请求
  • 排除运行状况检查请求

该示例将演示从日志中排除/secret请求。

@JsonTypeName("secret-filter-factory")
public class SecretFilterFactory implements FilterFactory<IAccessEvent> {
    @Override
    public Filter<IAccessEvent> build() {
        return new Filter<IAccessEvent>() {
            @Override
            public FilterReply decide(IAccessEvent event) {
                if (event.getRequestURI().equals("/secret")) {
                    return FilterReply.DENY;
                } else {
                    return FilterReply.NEUTRAL;
                }
            }
        };
    }
}

配置中的SecretFilterFactory引用类型。

server:
  requestLog:
    appenders:
      - type: console
        filterFactories:
          - type: secret-filter-factory

最后一步是将我们的类(在本例中com.example.SecretFilterFactory)添加到META-INF/services/io.dropwizard.logging.filter.FilterFactory资源文件夹中。

测试应用程序

Dropwizard的所有API在设计时都考虑了可测试性,因此您的应用程序也可以进行单元测试:

public class MyApplicationTest {
    private final Environment environment = mock(Environment.class);
    private final JerseyEnvironment jersey = mock(JerseyEnvironment.class);
    private final MyApplication application = new MyApplication();
    private final MyConfiguration config = new MyConfiguration();

    @Before
    public void setup() throws Exception {
        config.setMyParam("yay");
        when(environment.jersey()).thenReturn(jersey);
    }

    @Test
    public void buildsAThingResource() throws Exception {
        application.run(config, environment);

        verify(jersey).register(isA(ThingResource.class));
    }
}

我们强烈推荐Mockito满足您的所有mock需求。

横幅 Banners

我们认为应用程序应该在启动时打印出一个大的ASCII艺术横幅。你的也应该。这很有趣。只需在src/main/resources添加一个banner.txt文件,它将在您的应用程序启动时将其打印出来:

INFO  [2011-12-09 21:56:37,209] io.dropwizard.cli.ServerCommand: Starting hello-world
                                                 dP
                                                 88
  .d8888b. dP.  .dP .d8888b. 88d8b.d8b. 88d888b. 88 .d8888b.
  88ooood8  `8bd8'  88'  `88 88'`88'`88 88'  `88 88 88ooood8
  88.  ...  .d88b.  88.  .88 88  88  88 88.  .88 88 88.  ...
  `88888P' dP'  `dP `88888P8 dP  dP  dP 88Y888P' dP `88888P'
                                        88
                                        dP

INFO  [2011-12-09 21:56:37,214] org.eclipse.jetty.server.Server: jetty-7.6.0
...

可能会有争议所这是否是一个具有高投资回报率和敏捷工具的devops最佳实践,但老实说,我们喜欢这样。

我们建议您使用TAAG来满足您所有的ASCII艺术横幅需求。

资源 Resources

不出所料,您使用Dropwizard应用程序的大部分日常工作都将在资源类中进行,这些资源类对RESTful API中公开的资源进行建模。Dropwizard使用Jersey实现它,因此本节的大部分内容只是重新整理或收集各种Jersey文档。

Jersey是一个框架,用于将传入的HTTP请求的各个方面映射到POJO,然后将POJO的各个方面映射到传出的HTTP响应。这是一个基本的资源类:

@Path("/{user}/notifications")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class NotificationsResource {
    private final NotificationStore store;

    public NotificationsResource(NotificationStore store) {
        this.store = store;
    }

    @GET
    public NotificationList fetch(@PathParam("user") LongParam userId,
                                  @QueryParam("count") @DefaultValue("20") IntParam count) {
        final List<Notification> notifications = store.fetch(userId.get(), count.get());
        if (notifications != null) {
            return new NotificationList(userId, notifications);
        }
        throw new WebApplicationException(Status.NOT_FOUND);
    }

    @POST
    public Response add(@PathParam("user") LongParam userId,
                        @NotNull @Valid Notification notification) {
        final long id = store.add(userId.get(), notification);
        return Response.created(UriBuilder.fromResource(NotificationResource.class)
                                          .build(userId.get(), id))
                       .build();
    }
}

此类提供/{user}/notificationsGETPOST请求的响应,提供和使用application/json 表示的资源(用户的通知列表)。这里展示了相当多的功能,本节将详细说明正在使用的功能以及如何在应用程序中使用这些功能。

路径 Paths

重要

每个资源类都必须有一个@Path注释。

@Path注释不只是一个静态的字符串,它是一个URI模板。该{user}部分表示变量,当模板与URI匹配时,该变量的值可通过标有@PathParam注释的方法参数访问。

例如,传入的请求/1001/notifications将与URI模板匹配,并且该值"1001"可用作路径参数user

如果您的应用程序没有其@PathURI模板与传入请求的URI匹配的资源类,则Jersey将自动将返回404 Not Found给客户端。

方法 Methods

对资源类HTTP方法有:@GET@POST@PUT@DELETE@HEAD@OPTIONS@PATCH

可以通过@HttpMethod注释添加对任意新方法的支持。它们也必须添加到允许的方法列表中。默认情况下,这意味着诸如CONNECTTRACE被阻止的方法,并将返回 响应。405 Method Not Allowed

如果请求与资源类的路径匹配但与该类方法不匹配,则Jersey将自动返回405 Method Not Allowed给客户端。

然后,方法的返回值(在这种情况下,NotificationList实例)被映射到 协商的媒体类型,这种情况下,我们的资源仅支持JSON,因此NotificationList使用Jackson序列化为JSON。

监控 Metrics

每个资源的方法可以加@Timed@Metered@ExceptionMetered注解。Dropwizard增强了Jersey能够自动记录有关资源方法的运行时信息。

  • @Timed 测量对资源的请求持续时间
  • @Metered 测量资源的访问速度
  • @ExceptionMetered 测量处理资源的异常发生的频率

参数 Parameters

资源类上带注释的方法可以接受从请求映射过来的参数。*Param注释确定数据映射到其请求的一部分,并且注释的参数类型确定了数据是如何映射的。

例如:

  • 标有@PathParam("user")String参数,从URI模板中匹配的user变量中获取原始值,并将其作为一个String传递给方法。
  • 标有@QueryParam("count")注释的IntParam参数,从请求的查询字符串中获取第一个count值,并将其作为一个String传递给 IntParam的构造函数。 IntParam(以及所有其他io.dropwizard.jersey.params.*类)将字符串解析为一个Integer,如果值格式错误则返回400 Bad Request
  • 标有@FormParam("name")注释的Set<String>参数,从请求的表单中获取name所有值,并将它们作为一组字符串传递给方法。
  • 标有 *ParamNonEmptyStringParam将空字符串解释为缺少字符串,这在将空字符串和缺少字符串视为可互换的情况下很有用。

值得注意的是,您可以使用专门的参数对象封装绝大多数验证逻辑。详情AbstractParam

请求Body

如果您正在处理请求Body(例如,PUT请求中的application/json对象),则可以将其建模为没有*Param注释的参数。在 示例代码中,该add方法提供了一个很好的示例:

@POST
public Response add(@PathParam("user") LongParam userId,
                    @NotNull @Valid Notification notification) {
    final long id = store.add(userId.get(), notification);
    return Response.created(UriBuilder.fromResource(NotificationResource.class)
                   .build(userId.get(), id)
                   .build();
}

Jersey将请求实体映射到任何单个未绑定参数。在这种情况下,因为资源是注释的@Consumes(MediaType.APPLICATION_JSON),它使用Dropwizard提供的Jackson支持,除了解析JSON并将其映射到Notification实例之外,还通过Dropwizard的约束实体运行该实例。

如果反序列化Notification无效,Dropwizard 会向客户端返回响应。422 Unprocessable Entity

注意

如果请求实体参数仅使用@Valid注释,则仍然允许它 为null,因此@NotNull@Valid是确保对象存在并且验证是强大的组合。

媒体类型 Media Types

Jersey还提供完整的内容协商,因此如果您的资源类消费 application/json,但客户端发送text/plainbody,Jersey将自动回复406Not Acceptable。Jersey甚至足够聪明,可以通过客户端Accept消息头中的q值,来选择一个客户端和服务器都支持的最佳响应内容类型。

回应 Responses

如果您的客户希望自定义消息头或其他信息(或者,如果您只是希望对响应进行额外的控制),则可以返回显式构建的Response 对象:

return Response.noContent().language(Locale.GERMAN).build();

但是,一般情况下,我们建议您尽可能返回实际的域对象。它使 测试资源 更容易。

错误处理 Error Handling

与应用程序的正常场景(接收预期输入和返回预期输出)几乎同样重要的是出现问题时的应用程序行为。

如果您的资源类无意中抛出异常,Dropwizard将在ERROR级别(包括堆栈跟踪)下记录该异常并返回简洁,安全的application/json 500 Internal Server Error响应。响应将包含一个ID,可以从服务器日志中获取其他信息。

如果您的资源类需要向客户端返回错误(例如,请求的记录不存在),您有两个选择:抛出Exception的子类或重构您的方法以返回一个Response。如果可能的话,更建议抛出Exception,而不是返回 Response对象,因为这会使资源端点更清晰,并更容易测试。

将错误映射到响应的最不突兀的方法是抛出WebApplicationException

@GET
@Path("/{collection}")
public Saying reduceCols(@PathParam("collection") String collection) {
    if (!collectionMap.containsKey(collection)) {
        final String msg = String.format("Collection %s does not exist", collection);
        throw new WebApplicationException(msg, Status.NOT_FOUND)
    }

    // ...
}

在此示例中GET/foobar将返回请求

{"code":404,"message":"Collection foobar does not exist"}

您还可以获取资源可能抛出的异常并将其映射到适当的响应。例如,资源可能会抛出IllegalArgumentException,并且你希望自定义响应,并定义度量标准以跟踪事件发生的频率。这是一个ExceptionMapper的例子:

public class IllegalArgumentExceptionMapper implements ExceptionMapper<IllegalArgumentException> {
    private final Meter exceptions;
    public IllegalArgumentExceptionMapper(MetricRegistry metrics) {
        exceptions = metrics.meter(name(getClass(), "exceptions"));
    }

    @Override
    public Response toResponse(IllegalArgumentException e) {
        exceptions.mark();
        return Response.status(Status.BAD_REQUEST)
                .header("X-YOU-SILLY", "true")
                .type(MediaType.APPLICATION_JSON_TYPE)
                .entity(new ErrorMessage(Status.BAD_REQUEST.getStatusCode(),
                        "You passed an illegal argument!"))
                .build();
    }
}

然后注册异常映射器:

@Override
public void run(final MyConfiguration conf, final Environment env) {
    env.jersey().register(new IllegalArgumentExceptionMapper(env.metrics()));
    env.jersey().register(new Resource());
}

覆盖默认异常映射器

要覆盖特定的异常映射器,请注册您自己的类,该类实现与ExceptionMapper<T>默认映射器相同的类 。例如,我们可以自定义Jackson的异常:

public class JsonProcessingExceptionMapper implements ExceptionMapper<JsonProcessingException> {
    @Override
    public Response toResponse(JsonProcessingException exception) {
        // create the response
    }
}

使用此方法,不需要知道默认的异常映射器是什么,因为如果用户提供冲突的映射器,默认的将被覆盖。虽然不建议,但也可以通过设置server.registerDefaultExceptionMappersfalse来禁用所有默认的异常映射器。请参阅该类ExceptionMapperBinder以获取默认异常映射器的列表。

URI

虽然Jersey对超链接驱动的应用程序没有完美的支持,但它提供的 UriBuilder功能确实很好。

可以(并且建议) 使用来自资源类本身的路径初始化一个UriBuilder ,而不是重复资源URI :

UriBuilder.fromResource(UserResource.class).build(user.getId());

测试

与Dropwizard中的几乎所有内容一样,我们建议您将资源设计为可测试的。非请求注入的依赖项应通过构造函数传递并分配给final字段。

然后,测试包括创建资源类的实例并将其传递给mock。(还是使用:Mockito

public class NotificationsResourceTest {
    private final NotificationStore store = mock(NotificationStore.class);
    private final NotificationsResource resource = new NotificationsResource(store);

    @Test
    public void getsReturnNotifications() {
        final List<Notification> notifications = mock(List.class);
        when(store.fetch(1, 20)).thenReturn(notifications);

        final NotificationList list = resource.fetch(new LongParam("1"), new IntParam("20"));

        assertThat(list.getUserId(),
                  is(1L));

        assertThat(list.getNotifications(),
                   is(notifications));
    }
}

缓存 Caching

使用Dropwizard向您的资源类添加语句很简单,只需要加Cache-Control注解即可:

@GET
@CacheControl(maxAge = 6, maxAgeUnit = TimeUnit.HOURS)
public String getCachableValue() {
    return "yay";
}

@CacheControl注解将为返回消息添加所有的Cache-Control消息头。

表示 Representations

表示类是在处理各种Jersey 的MessageBodyReaderMessageBodyWriter时,那些API中的实体类。Dropwizard非常喜欢JSON,但它可以从任何POJO映射到自定义格式并返回。

基础JSON

Jackson很高兴将常规POJO转换为JSON并返回。这个文件:

public class Notification {
    private String text;

    public Notification(String text) {
        this.text = text;
    }

    @JsonProperty
    public String getText() {
        return text;
    }

    @JsonProperty
    public void setText(String text) {
        this.text = text;
    }
}

转换为此JSON:

{
    "text": "hey it's the value of the text field"
}

如果在某些时候,您需要更改JSON字段名称或Java字段而不影响另一个,则可以向@JsonProperty注释添加显式字段名称。

如果您更喜欢不可变对象而不是JavaBeans,那也是可行的:

public class Notification {
    private final String text;

    @JsonCreator
    public Notification(@JsonProperty("text") String text) {
        this.text = text;
    }

    @JsonProperty("text")
    public String getText() {
        return text;
    }
}

高级JSON

并非所有JSON表示都能很好地映射到应用程序处理的对象,因此有时需要使用自定义序列化程序和反序列化程序。只需注释您的对象,如下所示:

@JsonSerialize(using=FunkySerializer.class)
@JsonDeserialize(using=FunkyDeserializer.class)
public class Funky {
    // ...
}

然后创建一个FunkySerializer类,实现JsonSerializer<Funky>接口,创建一个FunkyDeserializer类实现JsonDeserializer<Funky>接口。

Snake Case

JSON的一个常见问题是字段名称camelCasesnake_case字段名称之间存在分歧。Java和Javascript的人倾向于喜欢camelCase; Ruby,Python和Perl人坚持认为 snake_case。要使Dropwizard自动将字段名称转换为snake_case(或者反过来),只需使用以下内容对类进行注释@JsonSnakeCase

@JsonSnakeCase
public class Person {
    private final String firstName;

    @JsonCreator
    public Person(@JsonProperty String firstName) {
        this.firstName = firstName;
    }

    @JsonProperty
    public String getFirstName() {
        return firstName;
    }
}

这将转换为此JSON:

{
    "first_name": "Coda"
}

流输出

如果您的应用程序碰巧返回了大量信息,那么使用流式输出可能会带来巨大的性能和效率提升。通过返回实现Jersey StreamingOutput 接口的对象,您的方法可以在块编码的输出流中流式传输响应实体。否则,您需要完全构造您的返回值,然后将其交给客户端。

HTML表示

要生成HTML页面,请查看Dropwizard的视图支持

自定义表示

但是,有时候,你需要制作或使用一些古怪的输出格式,那是不幸的,但也可以。您可以通过创建实现Jersey MessageBodyReader<T>MessageBodyWriter<T>接口的类来添加对任意输入和输出格式的支持。(确保它们使用了@Provider注释和 @Produces("text/gibberish")或者 @Consumes("text/gibberish")注释)完成后,只需在初始化时将它们的实例(或者它们的类,如果它们依赖于Jersey的@Context注入)添加到应用程序中Environment

Jersey过滤器

在某些情况下,您可能希望在到达资源之前过滤掉请求或修改请求。Jersey有一个丰富的过滤器和拦截器 api ,可直接在Dropwizard中使用。您可以通过抛出一个WebApplicationException来阻止请求到达您的资源。或者,您可以使用过滤器来修改入站请求或出站响应。

@Provider
public class DateNotSpecifiedFilter implements ContainerRequestFilter {
    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        String dateHeader = requestContext.getHeaderString(HttpHeaders.DATE);

        if (dateHeader == null) {
            Exception cause = new IllegalArgumentException("Date Header was not specified");
            throw new WebApplicationException(cause, Response.Status.BAD_REQUEST);
        }
    }
}

此示例筛选器检查“Date”标头的请求,如果缺少请求,则拒绝该请求。否则,请求将通过。

可以使用DynamicFeature将过滤器动态绑定到资源方法:

@Provider
public class DateRequiredFeature implements DynamicFeature {
    @Override
    public void configure(ResourceInfo resourceInfo, FeatureContext context) {
        if (resourceInfo.getResourceMethod().getAnnotation(DateRequired.class) != null) {
            context.register(DateNotSpecifiedFilter.class);
        }
    }
}

应用程序启动时,Jersey运行时将调用DynamicFeature。在此示例中,该功能检查使用注释的方法,@DateRequiredDateNotSpecified仅在这些方法上注册过滤器。

您通常在Application类中注册该功能,如下所示:

environment.jersey().register(DateRequiredFeature.class);

Servlet过滤器

创建过滤器的另一种方法是创建servlet过滤器。它们提供了一种注册过滤器的方法,这些过滤器既适用于servlet请求,也适用于资源请求。Jetty附带了一些 可能已经满足您需求的捆绑式过滤器。如果要创建自己的过滤器,此示例演示了一个与前一个示例类似的servlet过滤器:

public class DateNotSpecifiedServletFilter implements javax.servlet.Filter {
    // Other methods in interface omitted for brevity

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (request instanceof HttpServletRequest) {
            String dateHeader = ((HttpServletRequest) request).getHeader(HttpHeaders.DATE);

            if (dateHeader != null) {
                chain.doFilter(request, response); // This signals that the request should pass this filter
            } else {
                HttpServletResponse httpResponse = (HttpServletResponse) response;
                httpResponse.setStatus(HttpStatus.BAD_REQUEST_400);
                httpResponse.getWriter().print("Date Header was not specified");
            }
        }
    }
}

然后,可以通过将其包装在Application类中FilterHolder,并将其添加到应用程序上下文,需要设置此过滤器在哪个路径下生效的规则。这是一个例子:

environment.servlets().addFilter("DateNotSpecifiedServletFilter", new DateNotSpecifiedServletFilter())
                      .addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*");

它们是如何结合在一起的

当您的应用程序启动时,它将启动Jetty HTTP服务器,请参阅DefaultServerFactory。该服务器将有两个处理程序,一个用于您的应用程序端口,另一个用于您的管理端口。管理员处理程序创建并注册AdminServlet。它通过ServletContext处理所有应用程序运行状况检查和性能监控。

应用程序端口也有一个HttpServlet,它由Jersey 的DropwizardResourceConfig组成,它是Jersey的资源配置的扩展,执行扫描以查找根资源和提供程序类。最后,当你调用env.jersey().register(new SomeResource())时 ,实际上是在往DropwizardResourceConfig中添加。此配置是Jersey的Application,因此您的所有应用程序资源都是通过Servlet提供服务。

DropwizardResourceConfig 可以注册各种ResourceMethodDispatchAdapter,以启用以下功能:

  • 添加@Timed@Metered@ExceptionMetered的资源方法,都委托给带有metric功能的调度程序
  • 返回Guava Optional的资源是未装箱的。Present返回基础类型,而不存在404
  • 注释@CacheControl的资源方法被委托给在缓存控制消息头添加装饰的调度程序
  • 允许使用Jackson将请求实体解析为对象并从对象生成响应实体,同时执行验证

官网 https://www.dropwizard.io/1.3.5/docs/manual/core.html

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

推荐阅读更多精彩内容