• 2014-05-15

    不部不知道

    Tag:部署

    这是一个关于部署的话题,一句话,部署要趁早。

    故事1

    我们的应用有一个静态导出功能,在自己开发环境中,我们做了无数次导出,没有任何问题。但上到真正的环境中,导出功能一下子不起作用了。

    经过紧张的调试,我们把目光聚集到一个配置上,它是一个用来处理导出的URL,这个URL的地址是一个内部地址。在真正的环境里,每一台机器都会有内部IP和外部IP,我们用来登陆进行配置的是一个内部IP,但用浏览器登陆时,我们却用的是外部IP。所以,当试图访问一个内部IP地址时,自然就访问不到了,于是,导出过程就挂掉了。

    改正很简单,只要把配置改成外部IP即可。

    故事2

    做Web应用,开启gzip压缩是不可避免的。我们在自己的Nginx服务上测试好的配置文件,部署到了真正的环境中,gzip死活不起作用了。最初,我以为是自己的配置没有起作用,结果,用内部地址访问以下,一切正常。可为什么用域名访问就访问,难道域名解析还和gzip压缩有关?

    经过半天紧张的调试,我们把焦点定位在了负载均衡器上。请求通过域名在真正环境是要先到达负载均衡器的,然后,由负载均衡器分发到后台的Nginx上。这种访问模式实际上成了反向代理,所以,在Nginx上,要想gzip在这种模式下工作,需要把gzip的代理模式打开,比如:
         gzip_proxied any

    故事3

    如果我们做的是一个老应用的改版,为了搜索引擎,无可避免的一件事是要兼容老应用的一些URL。初想起来,有一个很简单的做法,在Nginx上配置一些URL rewrite规则即可。

      rewrite /old/url /new/url permanent;

    在我们测试环境一切正常,但是,上了真正的环境,这个跳转一下子不起作用了。原来,在真正的环境里,给外部访问所用的端口与内部部署的端口是不一样的,而这个跳转是正常的,只不过,它跳到了内部的端口,而这个端口在外部是不可见的,于是,出了问题。

    给出一个quick and dirty的解决方案很容易,给出一个绝对地址用于跳转即可:

      rewrite /old/url http://outside/new/url permanent;

    上面的几个小故事遇到的问题其实是一样的,部署环境和测试环境的差异。即便我们的软件写得完美无缺,环境永远是不可轻视的。除非我们能做到让所有环境完全一致,从头到尾,否则还是那句话,部署要趁早。

  • 对于很多团队来说,开发和运维现在还是两个世界的人,开发人员写着属于自己的代码,然后丢给运维人员。但作为开发人员,我们必须知道,运维的方式对于开发上的抉择是有影响的。

    和这个世界上的许多项目一样,我现在正在开发的项目也有一些后台定时运行的任务。这是一个Java应用,但我并不想把这些定时任务扔进Java EE容器里,没有必要让这些后台应用和前台应用抢资源。所以,我们就把它做成了一个独立的应用。好,问题来了,谁来做定时调度?

    因为我们的应用最终会部署在Linux操作系统上,所以,我的第一个直觉就是采用Cron。这是一个已经存在了几十年的解决方案,没有任何问题,而且,开发团队几乎不需要做任何额外工作。这个方案一直存在到我们和运维团队交流为止。

    “我们不允许使用任何系统任务”,运维团队开门见山地否决了我们的解决方案。运维团队给出的理由是,他们无法保证一台机器上只运行一个应用,如果其中一个应用挂了,运维人员也许会清理一些资源,换句话说,如果你的应用用了这些东西,也许会被一不小心地删掉了。“所以,按照我们规定,每个应用只能开辟自己的目录,运用自己目录下东西。”

    这是一个合理的要求,所以,我们需要调整自己的设计方案,把原来交由系统处理的调度转成由自己的应用处理。当然,在Java世界,这不是太大的难度,Quartz框架很好地帮我们处理了这些。

    其实,与调度方案同时被推翻的还有我的另外一个方案。这次我原本想尝试把我们的日志写到系统日志里。如果你不知道的话,rsyslog可以让我们把自己的日志写到/var/log下。很显然,这样的方案在这样约束下也是不行的。我们只好回到Java的传统方式上,把日志写到自己的目录下。

    这是两个由运维反过来影响开发方案的小例子。运维是开发的一种很重要的组成部分,运维团队的一些工作方式直接影响到开发上的一些决策。所以,如果开发和运维还是两个团队,开发团队不妨多找运维团队聊聊,更多地了解关于部署的方方面面。当然,更好的解决方案是走向通往DevOps的康庄大道。

  • Jackson雕虫技(一)
    Jackson雕虫技(二)
    Jackson雕虫技(三)

    忽略空字段

    有时候,我们返回对象的字段可能会有为空的情况。缺省情况下,这些字段会以null的形式呈现出来。但如果我们希望忽略这些字段该如何处理呢?如果是针对某个具体的类,我们可以使用JsonInclude这个annotation。

    @JsonInclude(JsonInclude.Include.NON_NULL)
    class Entity {
       ...
    }

    如果所有类都能够遵循这样的规则,那就要在全局配置了,配在Object Mapper上:

      mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

    Joda-Time

    在《你应该更新的Java知识》里面,我推荐大家在Java 8之前使用Joda Time,而非JDK原生的Date和Calendar。但是,如果我们使用了Joda Time,到了Jackson该怎么办呢?

    Java对象和JSON的相互转换,只是一个序列化和反序列化的过程。我们在之前的雕虫技中已经见识过如何针对自己的类自定义序列化和反序列化。所以,本质上来说,为Joda Time自定义一套,自然也不是什么难事。不过,通用如Joda Time这样的程序库,自然也应该有现成的支持。我们直接拿过来用就好了。

    实际上,Jackson已经为Joda Time提供了官方支持。如果你和我一样喜欢Gradle,下面就是依赖的添加方式,其中,jacksonVersion自然是Jackson的版本号。

      "com.fasterxml.jackson.datatype:jackson-datatype-joda:$jacksonVersion"

    有了依赖,我们只要在Object Mapper上注册一个模块即可。

      mapper.registerModule(new JodaModule());

    现在就可以自己的对象里使用DateTime、LocalDate、LocalTime这样的类型了,有了JodaModule,Jackson就会为我们照顾好这些类型的转换。

    我们习惯的日期表示方式一般是年月日时分秒,但Joda Time缺省的做法却是一个数字,一个以毫秒为单位计算出的数字,它叫时间戳。如果你希望让它变成我们喜欢的样子,可以这样做:

      mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);

    如果你还让自己的日期表现方式中加入时区,那就再加上时区:

      mapper.setTimeZone(TimeZone.getDefault());

    Guava

    同样在《你应该更新的Java知识》,我还提到一个观点,只要是Java项目就应该使用Guava。我也展示过Guava中一些库的用法。我现在已经无可救药地爱上了Guava中的不变集合。但同Joda Time一样,Guava不是标准的JDK的一部分,也需要额外的支持。值得高兴的是,这个支持也有现成的,也来自Jackson的官方,依赖如下:

      "com.fasterxml.jackson.datatype:jackson-datatype-guava:$jacksonVersion"

    同样,它也要Object Mapper上注册一个模块。

      mapper.registerModule(new GuavaModule());

    如此一来,Jackson就可以支持Guava提供的大部分内容,包括Optional和一些不变集合,以及一些新增的集合。

  • Jackson雕虫技(一)
    Jackson雕虫技(二)

    使用Builder模式

    在日常开发中,我们希望自己编写的类尽可能不变的,对于参数比较多的类,我们通常采用的方法是Builder模式。但如果我们使用Builder模式构造这样的不变对象,当我们将json反序列化成Java对象该怎么办呢?

    Jackson已经为这种做法做好了准备,我们可以告诉它这个类是采用Builder模式构建的:

    @JsonDeserialize(builder = DefaultHttpRequest.Builder.class)
    public class DefaultHttpRequest implements HttpRequest {
     ...
    }

    我们使用了一个Annotaiton:@JsonDeserialize,通过builder参数告诉它,用哪个类做Builder。这里用的就是这个类的一个内嵌类:Builder。

    public class DefaultHttpRequest implements HttpRequest {
       ...

      public static final class Builder {

        ...

        public Builder withVersion(String version) {
          this.version = version;
          return this;
       }

        ...

        public DefaultHttpRequest build() {
          ...
          return request;
       }
      }
    }

    其中,以with开头的就是用来传参数的方法,而build方法则用以构建最终的对象,这是一个缺省的约定。我们还可以按照自己的需要进行订制,只不过要给我们的Builder加上另外一个Annotation:@JsonPOJOBuilder。下面是一个例子:

    @JsonPOJOBuilder(buildMethodName="create", withPrefix="con")
    public static final class Builder {
       ...
    }

    这样一来,所有传参的方法都是以con开头,而构建对象的方法名则改成了create。

    使用对象简化解析

    在《Jackson雕虫技(二)》,我们提到了可以用自定义解析的方式解析对象,但一个一个字段解析写起来并不直观。其实,我们还可以借用已有的对象解析机制简化这个过程。下面是MocoProxyContainerDeserializer,它根据当前正在解析的目标进行处理,要么解析成一个URL,要么解析成一个Proxy的配置。

    public class ProxyContainerDeserializer extends JsonDeserializer {
       @Override
       public ProxyContainer deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
        JsonToken currentToken = jp.getCurrentToken();
         if (currentToken == JsonToken.VALUE_STRING) {
          return builder().withUrl(jp.getText().trim()).build();
        } else if (currentToken == JsonToken.START_OBJECT) {
          InternalProxyContainer container = get(jp.readValuesAs(InternalProxyContainer.class), 0);
          return container.toProxyContainer();
       }

        throw ctxt.mappingException(TextContainer.class, currentToken);
      }
    }

    这里的关键是readValuesAs,我们没有直接解析接下来的Token,而是确定解析目标之后,又借用了解析器本身的能力,把它解析成一个对象。至于InternalProxyContainer,它只是一个简单的对象类,用以装载解析的结果。

    private static class InternalProxyContainer {
      public String url;
      public String from;
      public String to;
      public String failover;
      public String playback;

      public ProxyContainer toProxyContainer() {
        ...
      }
    }

  • 2014-02-01

    Moco 0.9.1发布

    Tag:Moco

    前版信息:Moco 0.9发布

    我很高兴地宣布,Moco 0.9.1发布了。

    Moco是什么?

    Moco是一个可以轻松搭建测试服务器的框架/工具/程序库。

    变更

    按照版本号来说,这是一个小的修复版本,但实际的修改一点都不少。

    首先,这个版本增加了runner API,让我们可以自己在测试代码里控制Moco服务器的启停。最常见的做法是在集成测试的最初,启动一个服务器,结束之后关闭,下面是一个例子:

    @Before
    public void setup() {
       HttpServer server = httpserver(port());
       server.response("foo");
       runner = runner(server);
       runner.start();
    }

    @After
    public void tearDown() {
       runner.stop();
    }

    在配置API方面,增加了对proxy的批处理方法,我们可以一次性的代理一组URL,比如:

    server.proxy(from("/proxy").to("http://remoteUrl/target"));

    proxy还增加了一个playback的配置,它也是用来在本地存储远程服务器的内容,与failover不同的是,当本地内容可用时,它就不再访问远程服务器,换言之,它是本地优先的。

    server.request(by(uri("/proxy_playback")))
         .response(proxy("http://remoteUrl/target"), playback(path-to-local-playback)));

    这次的发布对模板也进行了改进,增加了对模板变量的支持,你可以根据自己的需要定制模板内容:

    server.request(by(uri("/template"))).response(template("${var}", "var", "TEMPLATE"));

    这次发布还增加了一个全局配置选项:Response。它的作用是,给所有的应答添加一个同样的内容,比如HTTP头。它主要用于独立运行模式,模拟很多类似的请求:

    [
       {
           "response" : {
               "headers" : {
                   "foo" : "bar"
               }
           },
           "include": "src/test/resources/settings/foo.json"
       }
    ]

    本次发布修正了Moco存在的一些问题,比如HTTP连接关闭,内容验证,服务器完全关闭等问题,让Moco的质量更上了一个台阶。本次发布还有一个重大的调整,采用Proguard将Standalone的JAR规模大幅度缩小,上一次发布Moco的Standalone包有8M多,而这次只有不到5M。

    更多的细节请参考ReleaseNotes

    感谢

    感谢崔鹏飞,协助发现修复了HTTP连接关闭的问题。

    感谢黄云坤,实现Proguard压缩Standalone JAR包。

    感谢Marcin Grzejszczak,协助发现并修复了服务器完全关闭的问题,并贡献了gmoco的脚本。