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

    和这个世界上的许多项目一样,我现在正在开发的项目也有一些后台定时运行的任务。这是一个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的脚本。

  • 2013-12-11

    十年

    刚开始工作的时候,我很愿意了解关于计算机发展的各种各样故事,俗称八卦,因为我特别想知道,我所从事的职业是如何走到今天的。如今,我偶尔会梳理一下经历过的事情,讲给其他人听,他们的神情和我当年了解各种八卦时,别无二致。原来,我前面经历过的时代业已成为历史的一部分。

    十年前,初出茅庐不久的我,选择了一种表达方式,blog,那时候绝对算是个新东西,知道这个名字的人都算是站在时代的风口浪尖,更别说自己写了。不记得在blog上说过多少次了,我能坚持下来的东西不多,编程是一个,写blog居然成了另外一个。

    是的,我写blog,整整十年了。

    十年过去了,blog已经成了落寞贵族,连Google都不玩Reader,微博这个后来才兴起的东西,现在也快被微信挤掉了。人们喜欢表达,只是表达方式越来越简洁,越来越便利。不过,当我觉得自己需要整理一下自己的思路时,blog依然是我的首选,在我看来,安安静静写字时,会有更多的思考。

    曾经有许多人问过我,为什么不自己搭建一个独立blog,或者是在Github上弄个至少有代码样式的blog。懒,是一个很重要的原因。另外一个原因是,习惯。十年了,我甚至连blog的模板都没有换过,所以,今天看我的blog,样子上和十年前如出一辙,我还是从前的那个我。也许未来我会换,但我不知道那一天什么时候到来。

    十年前,我选择了blogbus作为blog的存放地,如今看来是件很幸运的事。但很多人不知道或者不记得当年的BSP之争,十年后,回过头来看,当年的BSP留到现在的几乎没有几家了,而我的blog十年了,依然还在,只有运气可以形容。

    还记得第一年写了100篇,之后,几乎就是一个稳步减产的过程。不管怎样,十年下来,我还是写了500多篇blog。偶尔翻看从前的blog,那是一个追忆似水年华的过程,我会感叹,年轻就是生猛,或者我还有这么傻的时候。感谢这些blog,让我有了一些可以追溯的往事,让我不曾遗忘曾经发生过的事情。

    我想我还会写下去,我现在格外相信坚持的力量,我的一些坚持让我变得和从前不一样,不再只是纯粹的三分钟热血。总的说来,我喜欢这种改变,并没有滑向“成为自己讨厌的人”的深渊。

    希望十年之后,我还可以说,我又坚持了十年。