HOME/Articles/

JSONObject.toBean不能正确反序列化thrift生成的java类

Article Outline

现象

在使用json-lib中JSONObject.toBeanJsonObject转化为由thrift生成的Java类时,发现得到的javabean中的数据均为空(null或者默认值)。

复现

测试代码: pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>cn.keaper</groupId>
    <artifactId>thrift-test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.apache.thrift</groupId>
            <artifactId>libthrift</artifactId>
            <version>0.11.0</version>
        </dependency>
        <dependency>
            <groupId>net.sf.json-lib</groupId>
            <artifactId>json-lib</artifactId>
            <version>2.4</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-all</artifactId>
            <version>1.3</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>
        <dependency>
            <groupId>net.sf.ezmorph</groupId>
            <artifactId>ezmorph</artifactId>
            <version>1.0.6</version>
        </dependency>
        <dependency>
            <groupId>commons-beanutils</groupId>
            <artifactId>commons-beanutils</artifactId>
            <version>1.9.3</version>
        </dependency>
    </dependencies>

    <properties>
        <thrift.executable.shell>/usr/local/bin/thrift</thrift.executable.shell>
    </properties>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.thrift.tools</groupId>
                <artifactId>maven-thrift-plugin</artifactId>
                <version>0.1.10</version>
                <configuration>
                    <thriftExecutable>${thrift.executable.shell}</thriftExecutable>
                    <generator>java</generator>
                    <outputDirectory>src/main/java</outputDirectory>
                </configuration>
                <executions>
                    <execution>
                        <id>thrift-sources</id>
                        <phase>generate-sources</phase>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

定义一个thrift struct作为测试的类 student.thrift

namespace java cn.keaper

struct Student {
    1: string name;
    2: i32 age;
}

再自定义一个普通的javaBean,作对比之用Person.java

public class Person {

    String name;
    int age;

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

测试代码:

@Test
public void testJSON() {
    String json = "{name:\"edward\",age:\"20\"}";
    JSONObject jsonObject = JSONObject.fromObject(json);
    System.out.println(jsonObject.toString());

    Student student = (Student) JSONObject.toBean(jsonObject,Student.class);
    System.out.println(student.toString());

    Person person = (Person) JSONObject.toBean(jsonObject,Person.class);
    System.out.println(person.toString());
}

数据结果:

{"name":"edward","age":"20"}
Student(name:null, age:0)
九月 16, 2018 4:58:42 下午 net.sf.json.JSONObject toBean
信息: Property 'name' of class cn.keaper.Student has no write method. SKIPPED.
九月 16, 2018 4:58:42 下午 net.sf.json.JSONObject toBean
信息: Property 'age' of class cn.keaper.Student has no write method. SKIPPED.
Person{name='edward', age=20}

我们可以看到我们自定义的javaBean使用toBean方法转换是没有问题的,但是thrift生成的java类在使用toBean方法时是不能正确获取到数据的。

问题溯源

通过单步跟踪发现: 在JSONObject.toBean方法中执行到以下代码:

PropertyDescriptor pd = PropertyUtils.getPropertyDescriptor(bean, key);
if (pd != null && pd.getWriteMethod() == null) {
    log.info("Property '" + key + "' of " + bean.getClass() + " has no write method. SKIPPED.");
}else{
    ......
}

这里的执行对应了上文的日志输出,可见问题的原因是没有找到合适的set方法去对类的属性赋值。

我们来看看thrift生成的java类中setter,getter:

public String getName() {
    return this.name;
}

public Student setName(String name) {
    this.name = name;
    return this;
}
.....

public int getAge() {
    return this.age;
}

public Student setAge(int age) {
    this.age = age;
    setAgeIsSet(true);
    return this;
}

这里可以看到其中是有set方法的。 那么为什么没找到呢?继续跟踪,中间过程略长,已略过,最后我们来到了Introspector.getTargetPropertyInfo()

try {
    if (argCount == 0) {
        if (name.startsWith(GET_PREFIX)) {
            // Simple getter
            pd = new PropertyDescriptor(this.beanClass, name.substring(3), method, null);
        } else if (resultType == boolean.class && name.startsWith(IS_PREFIX)) {
            // Boolean getter
            pd = new PropertyDescriptor(this.beanClass, name.substring(2), method, null);
        }
    } else if (argCount == 1) {
        if (int.class.equals(argTypes[0]) && name.startsWith(GET_PREFIX)) {
            pd = new IndexedPropertyDescriptor(this.beanClass, name.substring(3), null, null, method, null);
        } else if (void.class.equals(resultType) && name.startsWith(SET_PREFIX)) {
            // Simple setter
            pd = new PropertyDescriptor(this.beanClass, name.substring(3), null, method);
            if (throwsException(method, PropertyVetoException.class)) {
                pd.setConstrained(true);
            }
        }
    } else if (argCount == 2) {
        if (void.class.equals(resultType) && int.class.equals(argTypes[0]) && name.startsWith(SET_PREFIX)) {
            pd = new IndexedPropertyDescriptor(this.beanClass, name.substring(3), null, null, null, method);
            if (throwsException(method, PropertyVetoException.class)) {
                pd.setConstrained(true);
            }
        }
    }
} catch (IntrospectionException ex) {
    // This happens if a PropertyDescriptor or IndexedPropertyDescriptor
    // constructor fins that the method violates details of the deisgn
    // pattern, e.g. by having an empty name, or a getter returning
    // void , or whatever.
    pd = null;
}

可见其中判断set方法的依据是参数个数为1,返回类型为void方法名以set开头,所以回头看thrift生成的java类

public Student setName(String name) {
    this.name = name;
    return this;
}
.....

public Student setAge(int age) {
    this.age = age;
    setAgeIsSet(true);
    return this;
}

其中的set方法与我们自定义javaBean的set方法不同的是它返回void,而是返回对象本身,所以它不能够被识别为一个setter,所以导致最后没有序列化成功。

总结

  1. Introspector这个类是jdk中用来处理javaBean的类。

    The Introspector class provides a standard way for tools to learn about the properties, events, and methods supported by a target Java Bean. 所以其中对setter的处理是符合Java Bean规范的。

  2. json-lib中使用的是commons-beanutils包,这个包中使用的就是jdk中Introspector来处理Java Bean,所以导致按照Java Bean 规范并不能正确的得到一个Java Bean。

  3. fastJson

    fastjson是阿里巴巴的开源JSON解析库,它可以解析JSON格式的字符串,支持将Java Bean序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean。

经测试,使用fastjson的JSON.parseObject方法是可以正确反序列化出thrift生成的JAVA类的,而且直接传入json字符串即可。