近年来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的实现过程找到破题思路和解决办法。思路分享出来供大家学习。其中有错误之处盼大家指正,共同学习共同进步。
转载或引用请注明出处。