GSON自定义适配器,重写fromJson类属性为字符串的反序列化,解决gson替换fastjson兼容性问题

        近年来fastjson的漏洞隔三差五的就被纰漏,程序员们苦不堪言,很多公司都将fastjson列入技术选型黑名单。而之前使用fastjson的系统改造,我这能想到的最好的技术方案就是将JSONUtils公共解析方法替换。

        我们的目标技术选型为GSON,移除fastjson依赖,创建JSONUtils中fastjson全包名的类来兼容接口入参,这样引用JSONUtils的其他类不用修改,达到只修改工具类组件即可全局修改的目的。

        测试中json与对象互转没异常,但历史数据或三方服务给到的json字符串有无法转换为目标对象的个例,为了达到无侵入其他类的代码块完成升级改造的目标,我们进行了研究。

        注:面对庞大、复杂、经年累月集成了各类服务和应用的系统,不侵入其他类代码完成改造,可有有效避免漏改错该的情况出现(各种痛苦只有经历过的人才知道(T_T))。

        上文说到了无法转换成对象的json如下:

{"str":"字符串","json":{"age":12},"arr":["小明","狂徒张三"]}

{
	"str": "字符串",
	"json": {
		"age": 12
	},
	"arr": ["小明", "狂徒张三"]
}

        目标实体类如下:

/**
 * 哇哈哈哈
 * @author 
 */
public class TestBean {
	
    private String str;
    private String json;
    private String arr;

    public String getStr() {
        return str;
    }

    public void setStr(String str) {
        this.str = str;
    }

    public String getJson() {
        return json;
    }

    public void setJson(String json) {
        this.json = json;
    }

    public String getArr() {
        return arr;
    }

    public void setArr(String arr) {
        this.arr = arr;
    }

    @Override
    public String toString() {
        return "TestBean{" +
                "str='" + str + '\'' +
                ", json='" + json + '\'' +
                ", arr='" + arr + '\'' +
                '}';
    }
}

        以上字符串fastjson转换为TestBean对象无报错,使用gson转换对象报错信息如下:

Exception in thread "main" com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected a string but was BEGIN_OBJECT at line 1 column 22 path $.json
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:270)
	at com.google.gson.Gson.fromJson(Gson.java:1058)
	at com.google.gson.Gson.fromJson(Gson.java:1016)
	at com.google.gson.Gson.fromJson(Gson.java:959)
	at com.google.gson.Gson.fromJson(Gson.java:927)
	at JSONUtils.main(JSONUtils.java:112)
Caused by: java.lang.IllegalStateException: Expected a string but was BEGIN_OBJECT at line 1 column 22 path $.json
	at com.google.gson.stream.JsonReader.nextString(JsonReader.java:835)
	at com.google.gson.internal.bind.TypeAdapters$15.read(TypeAdapters.java:394)
	at com.google.gson.internal.bind.TypeAdapters$15.read(TypeAdapters.java:382)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:161)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:266)
	... 5 more

Process finished with exit code 1

或者

Exception in thread "main" com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected a string but was BEGIN_ARRAY at line 1 column 9 path $.arr
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:270)
	at com.google.gson.Gson.fromJson(Gson.java:1058)
	at com.google.gson.Gson.fromJson(Gson.java:1016)
	at com.google.gson.Gson.fromJson(Gson.java:959)
	at com.google.gson.Gson.fromJson(Gson.java:927)
	at JSONUtils.main(JSONUtils.java:113)
Caused by: java.lang.IllegalStateException: Expected a string but was BEGIN_ARRAY at line 1 column 9 path $.arr
	at com.google.gson.stream.JsonReader.nextString(JsonReader.java:835)
	at com.google.gson.internal.bind.TypeAdapters$15.read(TypeAdapters.java:394)
	at com.google.gson.internal.bind.TypeAdapters$15.read(TypeAdapters.java:382)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:161)
	at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:266)
	... 5 more

Process finished with exit code 1

错误原因是类属性为String,但是属性名在json中对应的value却是以“{”或“[”开头的,百度谷歌搜索没有解决办法。于是debug gson的源码,发现gson在做类型解析时使用了适配器设计模式,来应对各种类型的解析。

 跟进getAdapter方法,发现对应类型的适配器从factories中获得

  public <T> TypeAdapter<T> getAdapter(TypeToken<T> type) {
        ……
      for (TypeAdapterFactory factory : factories) {
        TypeAdapter<T> candidate = factory.create(this, type);
        if (candidate != null) {
          call.setDelegate(candidate);
          typeTokenCache.put(type, candidate);
          return candidate;
        }
      }
        ……
    }

查看gson构造器发现factories支持用户自定义,并且可覆盖gson原有类型的适配器

GsonBuilder可通过registerTypeAdapterFactory注入

  /**
   * Register a factory for type adapters. Registering a factory is useful when the type
   * adapter needs to be configured based on the type of the field being processed. Gson
   * is designed to handle a large number of factories, so you should consider registering
   * them to be at par with registering an individual type adapter.
   *
   * @since 2.1
   */
  public GsonBuilder registerTypeAdapterFactory(TypeAdapterFactory factory) {
    factories.add(factory);
    return this;
  }

方法注释翻译:为类型适配器注册一个工厂。当需要根据正在处理的字段的类型配置类型适配器时,注册工厂是有用的。Gson的设计目的是处理大量的工厂,因此您应该考虑将它们注册为与注册单个类型适配器相同的类型。

那么源码给了技术解决方案:重写String类型的适配器。

要兼容之前的String类型那么要把源String解析拿到,在此之上进行修改。

源STRING_FACTORY:

public static final TypeAdapterFactory STRING_FACTORY = newFactory(String.class, STRING);

public static final TypeAdapter<String> STRING = new TypeAdapter<String>() {
    @Override
    public String read(JsonReader in) throws IOException {
      JsonToken peek = in.peek();
      if (peek == JsonToken.NULL) {
        in.nextNull();
        return null;
      }
      /* coerce booleans to strings for backwards compatibility */
      if (peek == JsonToken.BOOLEAN) {
        return Boolean.toString(in.nextBoolean());
      }
      return in.nextString();
    }
    @Override
    public void write(JsonWriter out, String value) throws IOException {
      out.value(value);
    }
  };

debug过程中发现也就是in.nextString()读取StringReader字符遇到了“{”或“[”gson抛出异常,所以我们在此基础上增加(peek == JsonToken.BEGIN_OBJECT || peek == JsonToken.BEGIN_ARRAY)的处理peek处理,达到兼容的目的。创建CustomTypeAdapterFactory类重写字符串类型适配器。

            //关键改造代码块
            if(peek == JsonToken.BEGIN_OBJECT || peek == JsonToken.BEGIN_ARRAY){
                JsonElement jsonElement = readJsonElement(in);
                return jsonElement.toString();
            }
readJsonElement方法代码块(此处借鉴gson处理JsonElement反序列的过程):
        public JsonElement readJsonElement(JsonReader in) throws IOException {
            switch (in.peek()) {
                case STRING:
                    return new JsonPrimitive(in.nextString());
                case NUMBER:
                    String number = in.nextString();
                    return new JsonPrimitive(new LazilyParsedNumber(number));
                case BOOLEAN:
                    return new JsonPrimitive(in.nextBoolean());
                case NULL:
                    in.nextNull();
                    return JsonNull.INSTANCE;
                case BEGIN_ARRAY:
                    JsonArray array = new JsonArray();
                    in.beginArray();
                    while (in.hasNext()) {
                        array.add(readJsonElement(in));
                    }
                    in.endArray();
                    return array;
                case BEGIN_OBJECT:
                    JsonObject object = new JsonObject();
                    in.beginObject();
                    while (in.hasNext()) {
                        object.add(in.nextName(), readJsonElement(in));
                    }
                    in.endObject();
                    return object;
                case END_DOCUMENT:
                case NAME:
                case END_OBJECT:
                case END_ARRAY:
                default:
                    throw new IllegalArgumentException();
            }
        }

        gson调用的代码展示:

    public static void main(String[] args) {
        Gson gson = new GsonBuilder()
                .registerTypeAdapterFactory(CustomTypeAdapterFactory.STRING_FACTORY)
                .create();
        String jsonStr = "{\"arr\":[\"小明\",\"狂徒张三\"],\"str\":\"字符串\",\"json\":{\"age\":12}}";
        TestBean testBean = gson.fromJson(jsonStr, TestBean.class);
        System.out.println(testBean.toString());
    }

运行结果:

TestBean{str='字符串', json='{"age":12}', arr='["小明","狂徒张三"]'}

结果正常异常消失,打包工具类jar包上传到maven私服,重新打包其他项目,原项目运行正常。

附CustomTypeAdapterFactory代码:

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.internal.LazilyParsedNumber;
import com.google.gson.internal.bind.TypeAdapters;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;

import java.io.IOException;

/**
 * @author 哇哈哈哈
 * @deprecated GSON自定义的字符串解析
 */
public class CustomTypeAdapterFactory {


    private static final TypeAdapter<String> STRING = new TypeAdapter<String>() {
        @Override
        public String read(JsonReader in) throws IOException {
            JsonToken peek = in.peek();
            if (peek == JsonToken.NULL) {
                in.nextNull();
                return null;
            }
            //关键改造代码块
            if(peek == JsonToken.BEGIN_OBJECT || peek == JsonToken.BEGIN_ARRAY){
                JsonElement jsonElement = readJsonElement(in);
                return jsonElement.toString();
            }
            /* coerce booleans to strings for backwards compatibility */
            if (peek == JsonToken.BOOLEAN) {
                return Boolean.toString(in.nextBoolean());
            }
            return in.nextString();
        }
        @Override
        public void write(JsonWriter out, String value) throws IOException {
            out.value(value);
        }

        public JsonElement readJsonElement(JsonReader in) throws IOException {
            switch (in.peek()) {
                case STRING:
                    return new JsonPrimitive(in.nextString());
                case NUMBER:
                    String number = in.nextString();
                    return new JsonPrimitive(new LazilyParsedNumber(number));
                case BOOLEAN:
                    return new JsonPrimitive(in.nextBoolean());
                case NULL:
                    in.nextNull();
                    return JsonNull.INSTANCE;
                case BEGIN_ARRAY:
                    JsonArray array = new JsonArray();
                    in.beginArray();
                    while (in.hasNext()) {
                        array.add(readJsonElement(in));
                    }
                    in.endArray();
                    return array;
                case BEGIN_OBJECT:
                    JsonObject object = new JsonObject();
                    in.beginObject();
                    while (in.hasNext()) {
                        object.add(in.nextName(), readJsonElement(in));
                    }
                    in.endObject();
                    return object;
                case END_DOCUMENT:
                case NAME:
                case END_OBJECT:
                case END_ARRAY:
                default:
                    throw new IllegalArgumentException();
            }
        }
    };

    public static final TypeAdapterFactory STRING_FACTORY = TypeAdapters.newFactory(String.class, STRING);

}

        后记:过程中通过阅读源码找到问题出现的原因,再通过gson的实现过程找到破题思路和解决办法。思路分享出来供大家学习。其中有错误之处盼大家指正,共同学习共同进步。

        转载或引用请注明出处。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值