开源社区工作日志

在这篇文章里,我将以一个RESTEasy的代码开发任务为例,记录我开源社区中的日常工作。其中包括我使用的工具,工作中的一些习惯和方法,以及工作流程。

在RESTEasy的邮件列表里面看到这样一个问题:

开源社区工作日志_第1张图片

大意就是RESTEasy的Jackson是否支持JAXB标记。因为RESTEasy的JacksonProvider并没有支持Jackson+JAXB,所以应该是需要添加这部分的支持才可以。但是在真正动手之前,我还想和用户再沟通一下,这样有时候可以收集到一些你想不到的需求。于是我回复了一封邮件如下:

开源社区工作日志_第2张图片

没多久,用户给出了回复:

开源社区工作日志_第3张图片

是一封很详细的回复,把自己所需讲得很明白。有的时候,可能我得不到如此详尽的回复,或者用户提出问题后,就不再参与,收集不到更细节的需求,那么我就会根据用户的问题得到的启发,着手我的工作。这封邮件回复很详细,用户想要的和我一开始设想的是一样的,TA要的是JacksonProvider对JAXB的支持。

于是我开始使用Google在网上搜索Jackson和JAXB联合使用相关的内容。然后我找到了下述页面:

http://wiki.fasterxml.com/JacksonJAXBAnnotations

https://github.com/FasterXML/jackson-module-jaxb-annotations

通过上面的文章,了解到了Jackson从1.1开始支持JAXB标记。RESTEasy使用的Jackson版本是1.9.9,因此,应该是可以支持的。

因此,工作的第一步是搞明白问题本身:理解问题与解答问题是同等重要的,有时候所耗费的时间也是差不多的,甚至有时候理解问题要花费比解决问题更长的时间。

接下来我要做的是写代码,重现用户的问题。一般用户的问题都是针对他们自己的具体项目,有很多不相关的东西揉在用户提供过来的代码当中,不利于问题的分析。因此,我比较喜欢跟据用户的描述,自己写一个case,把问题重现出来,同时这个case只关注这个问题本身,去掉无关的东西。因此,我写了这样一个case在这里:

https://gist.github.com/3980877

写完后,我会给在邮件列表中回信,让用户确认我写的这个case重现了他们的问题。如果不对,我再修改这个case。此外,我还会让用户帮忙创建一个JIRA Issue,用来跟踪和记录针对这个问题的工作过程。下面是我给用户Michael在邮件列表中的回信:

开源社区工作日志_第4张图片

很不巧的是,我在写这封信之前,有两周特别忙,因此实际上这封信的发出时间和上次与用户沟通相隔了两周。因此,这封信没有得到回音,这是社区里面经常会遇到的问题,如果你不是很及时地解决了用户的问题,用户在等待的期间精力又转去别处了,或者注意不到你两周后的回复。

但是社区一般开发资源非常有限,不可能对每一个用户的问题都能及时跟进和解决。特别是像RESTEasy这样有很大用户群的项目,用户的反馈特别多,而主要的开发人员只有Bill, Ron和我三个人。

因此,我等待了一天用户的回信,Miachel没有给我回复。但是这个问题之前沟通得比较充分了,因此我相信利用现有的信息可以继续我手头的工作,于是我不再等待用户的回复,开始着手解决这个问题。

首先我针对这个问题创建了一个JIRA Issue:

开源社区工作日志_第5张图片

然后我把这件事情放在我的todo list里面:

开源社区工作日志_第6张图片

因为社区的事情多且杂,每天光处理邮件就要几十封,如果光靠脑子记,是肯定记不住这么多事情的。因此,用一个好的TODO LIST软件来管理自己要做的事情是非常重要的。我比较喜欢使用Things这个软件来管理要做的事情。如果你对这个软件感兴趣,它的主页在这里:

http://culturedcode.com/things/

接下来要进行的就是实际的代码开发工作了。我首先找到RESTEasy的Jackson相关的代码,并进行阅读和分析工作:

开源社区工作日志_第7张图片

RESTEasy有20多万行代码,因此对项目的熟悉程度非常重要。这依赖于日常的不断积累,包括系统有计划地阅读RESTEasy的代码;使用RESTEasy做各种实验项目;看RESTEasy的文档,等等。也就是常说的做任何事要坚持。

因此,我可以快速地定位到要改动的地方,即ResteasyJacksonProvider:

public class ResteasyJacksonProvider extends JacksonJsonProvider


注意到JacksonJsonProvider是Jackson这个开源项目提供的,而不是RESTEasy自己的东西,因此我去Jackson的网站上下载相关代码。

在开源社区里面就是这样,一个项目依赖另一个项目,你经常需要进行跨项目的代码分析。因此,阅读代码的能力和分析代码的能力要好好地积累和培养,要能够比较快地去阅读新的代码和新的项目,这是一个经验与技术相结合,需要时间去积累的能力。

最后,我从Jackson的源代码中找到JacksonJsonProvider这个类,进行阅读和分析。这是个700多行的Java Class,对于开源项目中的Class来讲,算是一个中等规模的类。我不可能逐行地,漫无目的地去读它,那样的话没有任何意义。因此我必须要针对手里的问题来分析这个代码。

因此,回到刚才说的,理解问题本身很重要。我要做的是使RESTEasy的Jackson Provider要支持JAXB标记。而从Jackson的文档中我已经知道,Jackson从1.1开始支持JAXB标记。而RESTEasy使用的Jackson版本是1.9.9,因此,应该是可以支持的。

于是我深入阅读了Jackson的源代码,学习了Jackson是支持JAXB的使用方法。而我通过阅读ResteasyJacksonProvider,知道了RESTEasy里面的JacksonProvider是对Jackson自带的JacksonJsonProvider的简单封装。因此,我要做的就是通过ResteasyJacksonProvider使用JacksonJsonProvider提供的JAXB解析能力。

思路确定下来以后,就是具体的代码分析过程,经过大概一个多小时的分析,我了解到了JacksonJsonProvider是通过_mapperConfig来控制是否支持JAXB:

protected final MapperConfigurator _mapperConfig;


MapperConfigurator包含有这样一个方法:

public synchronized void setAnnotationsToUse(Annotations[] annotationsToUse) {
    _setAnnotations(mapper(), annotationsToUse);
}


这个就是设定是否支持JAXB标记的地方。而JacksonJsonProvider默认只支持Jackson标记,并没有开启JAXB标记:

    /**
     * Default annotation sets to use, if not explicitly defined during
     * construction: only Jackson annotations are used for the base
     * class. Sub-classes can use other settings.
     */
    public final static Annotations[] BASIC_ANNOTATIONS = {
        Annotations.JACKSON
    };

    /**
     * Default constructor, usually used when provider is automatically
     * configured to be used with JAX-RS implementation.
     */
    public JacksonJsonProvider()
    {
        this(null, BASIC_ANNOTATIONS);
    }


注意到_mapperConfig是protected:

protected final MapperConfigurator _mapperConfig;


因此,我可以直接在ResteasyJacksonProvider操作这个instance,因此,最后的任务变得非常简单,就是在ResteasyJacksonProvider添加代码,注入JAXB标记即可:

    public ResteasyJacksonProvider() {
        super();
        Annotations[] ANNOTATIONS = {Annotations.JACKSON, Annotations.JAXB};
        _mapperConfig.setAnnotationsToUse(ANNOTATIONS);
    }


实际上最终的改动非常简单,但是整个分析过程和沟通过程要花去几个小时。

最后,还要在RESTEasy中添加相关的单元测试:

    @XmlRootElement
    public static class XmlResourceWithJAXB {
        String attr1;
        String attr2;

        @XmlElement(name = "attr_1")
        public String getAttr1() {
            return attr1;
        }

        public void setAttr1(String attr1) {
            this.attr1 = attr1;
        }

        @XmlElement
        public String getAttr2() {
            return attr2;
        }

        public void setAttr2(String attr2) {
            this.attr2 = attr2;
        }
    }

    public static class XmlResourceWithJacksonAnnotation {
        String attr1;
        String attr2;

        @JsonProperty("attr_1")
        public String getAttr1() {
            return attr1;
        }

        public void setAttr1(String attr1) {
            this.attr1 = attr1;
        }

        @XmlElement
        public String getAttr2() {
            return attr2;
        }

        public void setAttr2(String attr2) {
            this.attr2 = attr2;
        }
    }


    @Path("/jaxb")
    public static class JAXBService {

        @GET
        @Produces("application/json")
        public XmlResourceWithJAXB getJAXBResource() {
            XmlResourceWithJAXB resourceWithJAXB = new XmlResourceWithJAXB();
            resourceWithJAXB.setAttr1("XXX");
            resourceWithJAXB.setAttr2("YYY");
            return resourceWithJAXB;
        }

        @GET
        @Path(("/json"))
        @Produces("application/json")
        public XmlResourceWithJacksonAnnotation getJacksonAnnotatedResource() {
            XmlResourceWithJacksonAnnotation resource = new XmlResourceWithJacksonAnnotation();
            resource.setAttr1("XXX");
            resource.setAttr2("YYY");
            return resource;
        }


    }

    @Test
    public void testJacksonJAXB() throws Exception {

        {
            DefaultHttpClient client = new DefaultHttpClient();
            HttpGet get = new HttpGet(generateBaseUrl() + "/jaxb");
            HttpResponse response = client.execute(get);
            BufferedReader reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
            Assert.assertTrue(reader.readLine().contains("attr_1"));
        }

        {
            DefaultHttpClient client = new DefaultHttpClient();
            HttpGet get = new HttpGet(generateBaseUrl() + "/jaxb/json");
            HttpResponse response = client.execute(get);
            BufferedReader reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
            Assert.assertTrue(reader.readLine().contains("attr_1"));

        }

    }


虽然改动很简单,但是测试却很长。对于RESTEasy这种规模的项目,你的一个小改动也许造成意想不到的BUG,因此测试必须充分。最后,要在本机整体跑一遍单元测试,不但要跑新加的测试,所有的测试都要跑一遍,这叫做Regression Test,以免新的Patch导致原来没问题的地方出错:

开源社区工作日志_第8张图片

开源社区工作日志_第9张图片

确定没有问题后,就可以针对这些改动创建一个新的Branch,然后提交到自己fork的项目当中:

git checkout -b RESTEASY-795-trunk
git push origin RESTEASY-795-trunk


然后,我就可以在github上面创建一个pull request了:

开源社区工作日志_第10张图片

这样,我们的实体工作就算是完成了,等待RESTEasy社区leader Bill把request给合并到upstream就可以了。

接下来,我会在JIRA里面加上Pull Request的地址,方便以后查阅:

开源社区工作日志_第11张图片

以上就是一个问题解决的过程。如果遇到比较热心的用户,会很积极地与你交流沟通解决问题,因此通常我会在提交Pull Request之前,在JIRA当中给用户一个Example和我Patch过的RESTEasy,让用户去帮忙验证一下这个Patch是否解决了问题。就像在这个issue当中做的一样:

https://issues.jboss.org/browse/RESTEASY-789





参与开源社区开发是一件非常有意思的事情。此外,你还能收获巨大的成就感,结识世界各地的技术朋友。希望能有更多的来自中国的技术爱好者,参与到世界级的开源社区工作当中。

你可能感兴趣的:(github,resteasy)