自定义类型处理器的应用
问题描述:
一个JSON字符串在转对象的时候报JSON解析异常的错误,我仔细看了一下错误堆栈,是枚举导致的数组越界问题。
[
{
"fee":0,
"amount":15,
"orderNo":"9136104331757999",
"storeId":0,
"bankCode":"",
"bankName":"",
"userName":"测试",
"accountId":0,
"payMethod":10,
"storeName":"测试小队",
"bankBranch":"",
"cardNumber":"60380500",
"StoreName":""
}
]
报错的方法:
String s="[{\"fee\": 0, \"amount\": 15, \"orderNo\": \"9136104331757999\", \"storeId\": 941, \"bankCode\": \"\", \"bankName\": \"\", \"userName\": \"测试\", \"accountId\": 0, \"payMethod\": 10, \"storeName\": \"测试小队\", \"bankBranch\": \"\", \"cardNumber\": \"60380500\",\"storeId\":0,\"StoreName\":\"\"}]";
PayAccount[] payAccount = JsonUtil.toBean(s, PayAccount[].class);
JSON转换工具类
public class JsonUtil {
private static final ParserConfig parseConfig = new ParserConfig();
/**
* 转成具体的泛型bean对象
*
* @param text json字符串
* @param clazz bean类型
* @param <T>
* @return
*/
public static <T> T toBean(String text, Class<T> clazz) {
return JSON.parseObject(text, clazz, parseConfig);
}
}
枚举对象
我看了一下fastjson在反序列化枚举类的源码,发现其在反序列化枚举对象的时候是把枚举的值当做数组下标来取值的,如果枚举对象的第一个值不是从0开始一次递增,就会有问题,开头说的问题就是原因导致的,详细源码看下面截图:
需要取的枚举是value=10的对象,然而对于有枚举对象组成的数组来说,value=10的对象是ordinalEnums[9],但是按照fastjson的处理取的确实ordinalEnums[10],数组越界。
好的。找到了问题之后我们就要考虑如何解决问题。
最简单的方法就是哪里有问题就从哪里解决,我们可以覆盖fastjson的源码,使其按照枚举value值对应的下标去数组中取值。代码我已经写出来了,如下:
Fastjson jar包提供的EnumDeserializer类的deserializer()方法:
重写后的序列化方法:(增加了按照枚举值取下标,从而取到对应的数组值)
简单可行,并且很完美的解决了我们的问题。
但是看题目事情应该不会这么简单,并且在实际的项目开发中不仅有枚举的序列化和反序列化问题,还有json对象的序列化和反序列化问题,今天我们就借着这个问题来总结一下SpringBoot中自定义JSON对象以及JSON对象中的枚举属性的序列化和反序列化问题。
定义一个枚举类:
public enum JobStatus {
DIMISSION("离职",0),
INCUMBENCY("在职",1),
INDETERMINATE("待入职",2);
private String label;
private Integer value;
JobStatus(String label, Integer value) {
this.label = label;
this.value = value;
}
@Override
public String getLabel() {
return label;
}
@Override
public Integer getValue() {
return value;
}
}
1、枚举类型的处理
我们通过继承MyBatis提供的BaseTypeHandler来实现自定义的枚举类型处理器:
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedTypes;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* <p>
* description: 枚举对象的typehandler
* </p>
*/
@MappedTypes(value = {JobStatus.class})
public class EnumTypeHandler extends BaseTypeHandler<BaseEnum> {
private Class<BaseEnum> type;
public EnumTypeHandler() {
}
public EnumTypeHandler(Class<BaseEnum> type) {
if (type == null) throw new IllegalArgumentException("Type argument cannot be null");
this.type = type;
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, BaseEnum parameter, JdbcType jdbcType) throws SQLException {
ps.setInt(i, parameter.getValue());
}
@Override
public BaseEnum getNullableResult(ResultSet rs, String columnName) throws SQLException {
return convert(rs.getInt(columnName));
}
@Override
public BaseEnum getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return convert(rs.getInt(columnIndex));
}
@Override
public BaseEnum getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return convert(cs.getInt(columnIndex));
}
private BaseEnum convert(int status) {
BaseEnum[] objs = type.getEnumConstants();
for (BaseEnum em : objs) {
if (em.getValue() == status) {
return em;
}
}
return null;
}
}
在MappedTypes后配置需要进行处理的枚举类。
枚举类型的反序列化方法,与上面我们定义的EnumDeserializer功能相同:
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.DefaultJSONParser;
import com.alibaba.fastjson.parser.JSONLexer;
import com.alibaba.fastjson.parser.deserializer.ObjectDeserializer;
import java.lang.reflect.Type;
import java.util.regex.Pattern;
/**
* 自定义枚举类型的反序列化
* 接口获取枚举的value值时,将其反序列化成枚举对象
*
*/
public class EnumTypeDeserializer implements ObjectDeserializer {
@Override
public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
JSONLexer lexer = parser.lexer;
Integer value = null;
Object parse = parser.parse();
if (parse == null)
return null;
String s = parse.toString();
if (Pattern.matches("\\d+", s)){
value = (int)parse;
} else {
if (s.startsWith("{")){
JSONObject jo = (JSONObject) parse;
value = jo.getIntValue("value");
}
}
if (value == null)
return null;
if (value < -999999)
return null;
Class<T> clazz = (Class<T>)type;
T[] enumConstants = clazz.getEnumConstants();
return (T)BaseEnum.valueOfEnum1(enumConstants, value);
}
@Override
public int getFastMatchToken() {
return 0;
}
}
枚举基类
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Field;
public interface BaseEnum {
String DEFAULT_VALUE_NAME = "value";
String DEFAULT_LABEL_NAME = "label";
default Integer getValue() {
Field field = ReflectionUtils.findField(this.getClass(), DEFAULT_VALUE_NAME);
if (field == null)
return null;
try {
field.setAccessible(true);
return Integer.parseInt(field.get(this).toString());
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
default String getLabel() {
Field field = ReflectionUtils.findField(this.getClass(), DEFAULT_LABEL_NAME);
if (field == null)
return null;
try {
field.setAccessible(true);
return field.get(this).toString();
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
static <T extends Enum<T>> T valueOfEnum(Class<T> enumClass, Integer value) {
if (value == null)
throw new IllegalArgumentException("DisplayedEnum value should not be null");
if (enumClass.isAssignableFrom(com.zfkr.qianyue.common.enums.BaseEnum.class))
throw new IllegalArgumentException("illegal DisplayedEnum type");
T[] enums = enumClass.getEnumConstants();
for (T t: enums) {
com.zfkr.qianyue.common.enums.BaseEnum displayedEnum = (com.zfkr.qianyue.common.enums.BaseEnum)t;
if (displayedEnum.getValue().equals(value))
return (T) displayedEnum;
}
throw new IllegalArgumentException("cannot parse integer: " + value + " to " + enumClass.getName());
}
static <T> T valueOfEnum1(T[] enums, Integer value) {
for (T t: enums) {
com.zfkr.qianyue.common.enums.BaseEnum displayedEnum = (com.zfkr.qianyue.common.enums.BaseEnum)t;
if (displayedEnum.getValue().equals(value))
return (T) displayedEnum;
}
throw new IllegalArgumentException("cannot parse integer: " + value + " to " );
}
}
2、JSON对象的处理
通过继承MyBatis的BaseTypeHandler来实现我们自定义的JSON类型处理器:
/**
* json对象的typehandler
*
*/
@MappedTypes(value = {JSON1.class, jSON2.class})
public class JsonTypeHandler<T> extends BaseTypeHandler<T> {
private static final SerializeConfig config;
private static final ParserConfig parseConfig;
private static final SerializerFeature[] features = {
SerializerFeature.WriteMapNullValue, // 输出空置字段
SerializerFeature.WriteNullListAsEmpty, // list字段如果为null,输出为[],而不是null
SerializerFeature.WriteNullNumberAsZero, // 数值字段如果为null,输出为0,而不是null
SerializerFeature.WriteNullBooleanAsFalse, // Boolean字段如果为null,输出为false,而不是null
SerializerFeature.WriteNullStringAsEmpty // 字符类型字段如果为null,输出为"",而不是null
};
static {
config = new SerializeConfig();
parseConfig = new ParserConfig();
}
private Class<T> clazz;
public JsonTypeHandler(Class<T> clazz) {
if (clazz == null) throw new IllegalArgumentException("Type argument cannot be null");
this.clazz = clazz;
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, toJson(parameter));
}
@Override
public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
return this.toObject(rs.getString(columnName), clazz);
}
@Override
public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return this.toObject(rs.getString(columnIndex), clazz);
}
@Override
public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return this.toObject(cs.getString(columnIndex), clazz);
}
private String toJson(T object) {
try {
return JSON.toJSONString(object, config, features);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("unchecked")
private T toObject(String content, Class<?> clazz) {
if (content != null && !content.isEmpty()) {
if (content.startsWith("[")) {//数组类型
List<?> objects = JSON.parseArray(content, clazz.getComponentType());
return (T) objects.toArray();
} else {
return (T) JSON.parseObject(content, clazz, parseConfig);
}
} else {
return null;
}
}
}
3、将我们自定义的枚举类型处理添加到解析配置器和序列化配置器中
/**
* json对象的typehandler
*/
@MappedTypes(value = {JSON1.class, jSON2.class})
public class JsonTypeHandler<T> extends BaseTypeHandler<T> {
private static final SerializeConfig config;
private static final ParserConfig parseConfig;
private static final SerializerFeature[] features = {
SerializerFeature.WriteMapNullValue, // 输出空置字段
SerializerFeature.WriteNullListAsEmpty, // list字段如果为null,输出为[],而不是null
SerializerFeature.WriteNullNumberAsZero, // 数值字段如果为null,输出为0,而不是null
SerializerFeature.WriteNullBooleanAsFalse, // Boolean字段如果为null,输出为false,而不是null
SerializerFeature.WriteNullStringAsEmpty // 字符类型字段如果为null,输出为"",而不是null
};
static {
config = new SerializeConfig();
parseConfig = new ParserConfig();
//获取枚举类型处理器中定义的需要处理的枚举类
Annotation[] annotations = EnumTypeHandler.class.getAnnotations();
MappedTypes annotation = (MappedTypes) annotations[0];
//设置枚举类型的反序列化方法为自定义的EnumTypeDeserializer
EnumTypeDeserializer enumTypeObjectDeserializer = new EnumTypeDeserializer();
for (Class<?> aClass : annotation.value()) {
parseConfig.getDeserializers().put(aClass, enumTypeObjectDeserializer);
}
}
private Class<T> clazz;
public JsonTypeHandler(Class<T> clazz) {
if (clazz == null) throw new IllegalArgumentException("Type argument cannot be null");
this.clazz = clazz;
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, toJson(parameter));
}
@Override
public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
return this.toObject(rs.getString(columnName), clazz);
}
@Override
public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return this.toObject(rs.getString(columnIndex), clazz);
}
@Override
public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return this.toObject(cs.getString(columnIndex), clazz);
}
private String toJson(T object) {
try {
return JSON.toJSONString(object, config, features);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("unchecked")
private T toObject(String content, Class<?> clazz) {
if (content != null && !content.isEmpty()) {
if (content.startsWith("[")) {//数组类型
List<?> objects = JSON.parseArray(content, clazz.getComponentType());
return (T) objects.toArray();
} else {
return (T) JSON.parseObject(content, clazz, parseConfig);
}
} else {
return null;
}
}
}
在SpringBoot的yml文件中配置自定义类型处理器所在的包,项目启动之后自定义类型处理器即可被扫描到。问题解决。