Elim的博客

用来记录一些原创性的总结


Elim

JAXBContext介绍及性能优化

摘要 本文主要介绍基于package创建JAXBContext,以及阐述JAXBContext存在的性能问题及其优化。

JAXBContext介绍

JAXBContext是我们在使用JAXB时的入口类,我们需要通过它的实例来建立XML和Java类之间的映射关系,需要通过它来创建用于转换Java对象到XML的Marshaller或是创建用于转换XML到Java对象的Unmarshaller。JAXBContext的实例需要通过JAXBContext.newInstance(..)方法产生,JAXBContext中定义了重载的5个newInstance(..)方法,定义如下:

public static JAXBContext newInstance( String contextPath );
public static JAXBContext newInstance( String contextPath, ClassLoader classLoader );
public static JAXBContext newInstance( String contextPath, ClassLoader classLoader, Map<String,?>  properties  );
public static JAXBContext newInstance( Class... classesToBeBound );
public static JAXBContext newInstance( Class[] classesToBeBound, Map<String,?> properties );

上述5个方法的区别在于是基于package的还是基于class的。contextPath用于指定需要绑定XML的Java类所在的包定义;classLoader用于指定使用的类加载器,未指定时将使用当前线程上下文所绑定的类加载器;properties用于指定JAXB实现者特定的属性;classesToBeBound用于直接指定需要绑定的Java类。

之前介绍的都是通过直接指定相关的Class作为入参的形式创建JAXBContext,如下所示:

JAXBContext jaxbContext = JAXBContext.newInstance(Response.class, Person.class);

这里着重介绍一下基于package创建JAXBContext的示例,如下所示:

JAXBContext jaxbContext = JAXBContext.newInstance("com.xxx.jaxb");

但是这时候并不是指定的包中所有的Class都会用来创建JAXBContext。按照JAXB的规范,我们需要在对应的包中创建一个jaxb.index文件,然后在其中指定创建JAXBContext时需要用到的Class,每个Class名称占一行,只需要写Class名称即可,如我们在创建JAXBContext时需要用到Response和Person类,我们就可以中jaxb.index文件中如下定义:

Response
Person

然后后续就可以用对应的JAXBContext来进行相应的Marshal和UnMarshal操作了。通过指定包名称创建JAXBContext的方式可以把程序中各种可能的场景中需要使用到的Class都初始化到一个JAXBContext中,这样可以保证这个JAXBContext可以在各种可能的场景下都能满足需求,JAXBContext的初始化是非常耗CPU的,重用JAXBContext有助于提升性能。另一方面我们如果在某一时刻调整使用的Class时可以不用修改源码,直接修改jaxb.index文件中定义的Class即可,这可比修改源码要简单的多。

为了保证文章的完整性,以下给出一个基于包名称初始化JAXBContext的完整示例。

@Test
public void testPerson() throws Exception {
    Person person = new Person();
    person.setId(1);
    person.setName("张三");
    Response response = this.newResponse(person);
    JAXBContext jaxbContext = JAXBContext.newInstance("com.xxx.jaxb");
    Marshaller marshaller = jaxbContext.createMarshaller();
    marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
    marshaller.marshal(response, System.out);
}

private Response newResponse(Data realData) {
    Response response = new Response();
    response.setReturnCode(100);
    response.setMessage("AAAA");
    ResultData resultData = new ResultData();
    resultData.setRealData(realData);
    response.setResultData(resultData);
    return response;
}
@XmlRootElement
public class Response {

    private int returnCode;
    private String message;
    @XmlElement(name = "data")
    private ResultData resultData;

    //省略get/set

}
public class ResultData {
    @XmlElementRef
    private Data realData;

    //省略get/set
}
public class Data {

}
@XmlRootElement(name="dept")
public class Person extends Data {
    @XmlAttribute
    private Integer id;
    private String name;

    //省略get/set
}
Response
Person

生成的XML如下:

<response>
    <returnCode>100</returnCode>
    <message>AAAA</message>
    <data>
        <dept id="1">
            <name>张三</name>
        </dept>
    </data>
</response>

使用直接指定class创建JAXBContext的方式我们的代码可以是如下这样:

@Test
public void testPerson2() throws Exception {
    Person person = new Person();
    person.setId(1);
    person.setName("张三");
    Response response = this.newResponse(person);
    JAXBContext jaxbContext = JAXBContext.newInstance(Person.class, Response.class);
    Marshaller marshaller = jaxbContext.createMarshaller();
    marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
    marshaller.marshal(response, System.out);
}

通过package创建JAXBContext时也允许创建指定多个package,多个package之间通过英文冒号分隔,如下我们在创建JAXBContext时就同时指定了com.xxx.jaxbcom.xxx.jaxb2两个package。

JAXBContext jaxbContext = JAXBContext.newInstance("com.xxx.jaxb:com.xxx.jaxb2");

性能优化

每次在创建JAXBContext实例时,JAXBContext内部都需要维护好Java类和XML之间的映射关系,这个操作是十分消耗性能的。幸运的是JAXBContext是线程安全的,可以共享。关于JAXBContext存在的性能问题笔者准备了如下测试代码:

@Test
public void testPerformance() throws Exception {
    for (int j=0; j<5; j++) {
        int times = 100000;
        long t1 = System.currentTimeMillis();
        for (int i=0; i<times; i++) {
            JAXBContext jaxbContext = JAXBContext.newInstance(Root.class);
            Marshaller marshaller = jaxbContext.createMarshaller();
            marshaller.marshal(new Root(), new StringWriter());
        }
        long t2 = System.currentTimeMillis();
        System.out.println("不共用JAXBContext耗时:" + (t2-t1) + "ms");
        
        long t3 = System.currentTimeMillis();
        JAXBContext jaxbContext = JAXBContext.newInstance(Root.class);
        for (int i=0; i<times; i++) {
            Marshaller marshaller = jaxbContext.createMarshaller();
            marshaller.marshal(new Root(), new StringWriter());
        }
        long t4 = System.currentTimeMillis();
        System.out.println("共用JAXBContext耗时:" + (t4-t3) + "ms");
    }
}

循环测试5次,每次分别在共享JAXBContext和不共享JAXBContext的情况下进行十万次marshal操作,由于这里我们不关心生成的XML,也避免输出到控制台或文件中的IO影响,这里每次都创建一个StringWriter,StringWriter是基于内存操作的。在笔者64位ubuntu17.10系统、处理器是Intel® Core™ i5-7500 CPU @ 3.40GHz × 4、8GB内存的台式机下运行结果如下所示:

不共用JAXBContext耗时:17735ms
共用JAXBContext耗时:240ms
不共用JAXBContext耗时:16003ms
共用JAXBContext耗时:272ms
不共用JAXBContext耗时:15614ms
共用JAXBContext耗时:223ms
不共用JAXBContext耗时:15313ms
共用JAXBContext耗时:215ms
不共用JAXBContext耗时:15241ms
共用JAXBContext耗时:235ms

从测试结果我们可以很明显的看到共享JAXBContext的情况下性能有很明显的提升。共享JAXB时如果我们的所有的应用场景需要使用到的Class比较少时我们可以简单的在创建JAXBContext对象时传递所有需哟使用到的Class,把它保存起来之后都可以一直这样在我们的应用中使用它了。但是如果Class的定义在进行XML转换为Java对象时有冲突就不能这么做了,比如你有一个Person1和Person2两个Person类,它们映射的XML节点都是person,那么在把XML节点person转换为Java对象时是转换为Person1还是Person2呢?有这种场景获取Class相对较多时建议创建一个JaxbUtil工具类,所有的JAXBContext对象都由该工具类产生,在该工具类内部实现JAXBContext的重用,以下是一个简单的示例。

private static Map<List<Class<?>>, JAXBContext> jaxbContextMap = new ConcurrentHashMap<>();

public static JAXBContext getJAXBContext(Class<?> ...classes) throws JAXBException {
    List<Class<?>> classList = new ArrayList<>(Arrays.asList(classes));
    if (!jaxbContextMap.containsKey(classList)) {
        JAXBContext jaxbContext = JAXBContext.newInstance(classes);
        jaxbContextMap.put(classList, jaxbContext);
        return jaxbContext;
    }
    return jaxbContextMap.get(classList);
}

需要注意的是虽然JAXBContext是线程安全的,但是它由它产生的Marshaller和Unmarshaller对象是线程不安全的,切勿进行重用。