目录
- 引出问题
- 分析原因
- 解决方法
内容
NPE问题的发生
发生条件
1、在一个POJO类上定义了方法名为isXxx这样的public方法;
2、系统统一拦截器对POJO对象进行JSON格式的日志打印;
3、JSON.toJSON方法的调用采用的是fastJson默认配置(序列化时会同时处理类中的fields、getXxx、isXxx);
4、isXxx方法内部未做判空,抛出NPE;
问题分析
乍一看,都怪isXxx方法内部没做判空,导致NPE的产生;
认真思考一下,还是因为开发同学没意识到,在fastjson做序列化时会会把isXxx方法和getXxx方法同时做调用,做序列化处理;getXxx方法肯定不会抛出NPE异常对吧,这是每个开发同学共知的;那如果我们也把isXxx方法做相同保证,就没问题了;
问题解决
1、先看fastjson的源码—fastjson-1.2.78.jar
com.alibaba.fastjson.util.TypeUtils
1、决策一个Class序列化时,应该有哪些属性
public static SerializeBeanInfo buildBeanInfo(Class<?> beanType //
, Map<String,String> aliasMap //
, PropertyNamingStrategy propertyNamingStrategy //
, boolean fieldBased //
){
JSONType jsonType = TypeUtils.getAnnotation(beanType,JSONType.class);
String[] orders = null;
final int features;
String typeName = null, typeKey = null;
if(jsonType != null){
orders = jsonType.orders();
typeName = jsonType.typeName();
if(typeName.length() == 0){
typeName = null;
}
PropertyNamingStrategy jsonTypeNaming = jsonType.naming();
if (jsonTypeNaming != PropertyNamingStrategy.NeverUseThisValueExceptDefaultValue) {
propertyNamingStrategy = jsonTypeNaming;
}
features = SerializerFeature.of(jsonType.serialzeFeatures());
for(Class<?> supperClass = beanType.getSuperclass()
; supperClass != null && supperClass != Object.class
; supperClass = supperClass.getSuperclass()){
JSONType superJsonType = TypeUtils.getAnnotation(supperClass,JSONType.class);
if(superJsonType == null){
break;
}
typeKey = superJsonType.typeKey();
if(typeKey.length() != 0){
break;
}
}
for(Class<?> interfaceClass : beanType.getInterfaces()){
JSONType superJsonType = TypeUtils.getAnnotation(interfaceClass,JSONType.class);
if(superJsonType != null){
typeKey = superJsonType.typeKey();
if(typeKey.length() != 0){
break;
}
}
}
if(typeKey != null && typeKey.length() == 0){
typeKey = null;
}
} else{
features = 0;
}
// fieldName,field ,先生成fieldName的快照,减少之后的findField的轮询
Map<String,Field> fieldCacheMap = new HashMap<String,Field>();
ParserConfig.parserAllFieldToCache(beanType, fieldCacheMap);
List<FieldInfo> fieldInfoList = fieldBased
? computeGettersWithFieldBase(beanType, aliasMap, false, propertyNamingStrategy) //
: computeGetters(beanType, jsonType, aliasMap, fieldCacheMap, false, propertyNamingStrategy);
FieldInfo[] fields = new FieldInfo[fieldInfoList.size()];
fieldInfoList.toArray(fields);
FieldInfo[] sortedFields;
List<FieldInfo> sortedFieldList;
if(orders != null && orders.length != 0){
sortedFieldList = fieldBased
? computeGettersWithFieldBase(beanType, aliasMap, true, propertyNamingStrategy) //
: computeGetters(beanType, jsonType, aliasMap, fieldCacheMap, true, propertyNamingStrategy);
} else{
sortedFieldList = new ArrayList<FieldInfo>(fieldInfoList);
Collections.sort(sortedFieldList);
}
sortedFields = new FieldInfo[sortedFieldList.size()];
sortedFieldList.toArray(sortedFields);
if(Arrays.equals(sortedFields, fields)){
sortedFields = fields;
}
return new SerializeBeanInfo(beanType, jsonType, typeName, typeKey, features, fields, sortedFields);
}
2、仅仅基于Class中的filed做序列化
public static List<FieldInfo> computeGettersWithFieldBase(
Class<?> clazz, //
Map<String,String> aliasMap, //
boolean sorted, //
PropertyNamingStrategy propertyNamingStrategy){
Map<String,FieldInfo> fieldInfoMap = new LinkedHashMap<String,FieldInfo>();
for(Class<?> currentClass = clazz; currentClass != null; currentClass = currentClass.getSuperclass()){
Field[] fields = currentClass.getDeclaredFields();
computeFields(currentClass, aliasMap, propertyNamingStrategy, fieldInfoMap, fields);
}
return getFieldInfos(clazz, sorted, fieldInfoMap);
}
private static List<FieldInfo> getFieldInfos(Class<?> clazz, boolean sorted, Map<String,FieldInfo> fieldInfoMap){
List<FieldInfo> fieldInfoList = new ArrayList<FieldInfo>();
String[] orders = null;
JSONType annotation = TypeUtils.getAnnotation(clazz,JSONType.class);
if(annotation != null){
orders = annotation.orders();
}
if(orders != null && orders.length > 0){
LinkedHashMap<String,FieldInfo> map = new LinkedHashMap<String,FieldInfo>(fieldInfoMap.size());
for(FieldInfo field : fieldInfoMap.values()){
map.put(field.name, field);
}
for(String item : orders){
FieldInfo field = map.get(item);
if(field != null){
fieldInfoList.add(field);
map.remove(item);
}
}
fieldInfoList.addAll(map.values());
} else{
fieldInfoList.addAll(fieldInfoMap.values());
if(sorted){
Collections.sort(fieldInfoList);
}
}
return fieldInfoList;
}
3、除了属性,还要考虑方法
这里就有把isXxx这样的方法的返回值,也作为序列化的一部分输出;
public static List<FieldInfo> computeGetters(Class<?> clazz, //
JSONType jsonType, //
Map<String,String> aliasMap, //
Map<String,Field> fieldCacheMap, //
boolean sorted, //
PropertyNamingStrategy propertyNamingStrategy //
){
Map<String,FieldInfo> fieldInfoMap = new LinkedHashMap<String,FieldInfo>();
boolean kotlin = TypeUtils.isKotlin(clazz);
// for kotlin
Constructor[] constructors = null;
Annotation[][] paramAnnotationArrays = null;
String[] paramNames = null;
short[] paramNameMapping = null;
Method[] methods = clazz.getMethods();
Arrays.sort(methods, new MethodInheritanceComparator());
for(Method method : methods){
String methodName = method.getName();
int ordinal = 0, serialzeFeatures = 0, parserFeatures = 0;
String label = null;
if(Modifier.isStatic(method.getModifiers())){
continue;
}
Class<?> returnType = method.getReturnType();
if(returnType.equals(Void.TYPE)){
continue;
}
if(method.getParameterTypes().length != 0){
continue;
}
if(returnType == ClassLoader.class
|| returnType == InputStream.class
|| returnType == Reader.class){
continue;
}
if(methodName.equals("getMetaClass")
&& returnType.getName().equals("groovy.lang.MetaClass")){
continue;
}
if(methodName.equals("getSuppressed")
&& method.getDeclaringClass() == Throwable.class){
continue;
}
if(kotlin && isKotlinIgnore(clazz, methodName)){
continue;
}
/**
* 如果在属性或者方法上存在JSONField注解,并且定制了name属性,不以类上的propertyNamingStrategy设置为准,以此字段的JSONField的name定制为准。
*/
Boolean fieldAnnotationAndNameExists = false;
JSONField annotation = TypeUtils.getAnnotation(method, JSONField.class);
if(annotation == null){
annotation = getSuperMethodAnnotation(clazz, method);
}
if(annotation == null && kotlin){
if(constructors == null){
constructors = clazz.getDeclaredConstructors();
Constructor creatorConstructor = TypeUtils.getKotlinConstructor(constructors);
if(creatorConstructor != null){
paramAnnotationArrays = TypeUtils.getParameterAnnotations(creatorConstructor);
paramNames = TypeUtils.getKoltinConstructorParameters(clazz);
if(paramNames != null){
String[] paramNames_sorted = new String[paramNames.length];
System.arraycopy(paramNames, 0, paramNames_sorted, 0, paramNames.length);
Arrays.sort(paramNames_sorted);
paramNameMapping = new short[paramNames.length];
for(short p = 0; p < paramNames.length; p++){
int index = Arrays.binarySearch(paramNames_sorted, paramNames[p]);
paramNameMapping[index] = p;
}
paramNames = paramNames_sorted;
}
}
}
if(paramNames != null && paramNameMapping != null && methodName.startsWith("get")){
String propertyName = decapitalize(methodName.substring(3));
int p = Arrays.binarySearch(paramNames, propertyName);
if (p < 0) {
for (int i = 0; i < paramNames.length; i++) {
if (propertyName.equalsIgnoreCase(paramNames[i])) {
p = i;
break;
}
}
}
if(p >= 0){
short index = paramNameMapping[p];
Annotation[] paramAnnotations = paramAnnotationArrays[index];
if(paramAnnotations != null){
for(Annotation paramAnnotation : paramAnnotations){
if(paramAnnotation instanceof JSONField){
annotation = (JSONField) paramAnnotation;
break;
}
}
}
if(annotation == null){
Field field = ParserConfig.getFieldFromCache(propertyName, fieldCacheMap);
if(field != null){
annotation = TypeUtils.getAnnotation(field, JSONField.class);
}
}
}
}
}
if(annotation != null){
if(!annotation.serialize()){
continue;
}
ordinal = annotation.ordinal();
serialzeFeatures = SerializerFeature.of(annotation.serialzeFeatures());
parserFeatures = Feature.of(annotation.parseFeatures());
if(annotation.name().length() != 0){
String propertyName = annotation.name();
if(aliasMap != null){
propertyName = aliasMap.get(propertyName);
if(propertyName == null){
continue;
}
}
FieldInfo fieldInfo = new FieldInfo(propertyName, method, null, clazz, null, ordinal,
serialzeFeatures, parserFeatures, annotation, null, label);
fieldInfoMap.put(propertyName, fieldInfo);
continue;
}
if(annotation.label().length() != 0){
label = annotation.label();
}
}
if(methodName.startsWith("get")){
if(methodName.length() < 4){
continue;
}
if(methodName.equals("getClass")){
continue;
}
if(methodName.equals("getDeclaringClass") && clazz.isEnum()){
continue;
}
char c3 = methodName.charAt(3);
String propertyName;
Field field = null;
if(Character.isUpperCase(c3) //
|| c3 > 512 // for unicode method name
){
if(compatibleWithJavaBean){
propertyName = decapitalize(methodName.substring(3));
} else{
propertyName = TypeUtils.getPropertyNameByMethodName(methodName);
}
propertyName = getPropertyNameByCompatibleFieldName(fieldCacheMap, methodName, propertyName, 3);
} else if(c3 == '_'){
propertyName = methodName.substring(3);
field = fieldCacheMap.get(propertyName);
if (field == null) {
String temp = propertyName;
propertyName = methodName.substring(4);
field = ParserConfig.getFieldFromCache(propertyName, fieldCacheMap);
if (field == null) {
propertyName = temp; //减少修改代码带来的影响
}
}
} else if(c3 == 'f'){
propertyName = methodName.substring(3);
} else if(methodName.length() >= 5 && Character.isUpperCase(methodName.charAt(4))){
propertyName = decapitalize(methodName.substring(3));
} else{
propertyName = methodName.substring(3);
field = ParserConfig.getFieldFromCache(propertyName, fieldCacheMap);
if (field == null) {
continue;
}
}
boolean ignore = isJSONTypeIgnore(clazz, propertyName);
if(ignore){
continue;
}
if (field == null) {
// 假如bean的field很多的情况一下,轮询时将大大降低效率
field = ParserConfig.getFieldFromCache(propertyName, fieldCacheMap);
}
if(field == null && propertyName.length() > 1){
char ch = propertyName.charAt(1);
if(ch >= 'A' && ch <= 'Z'){
String javaBeanCompatiblePropertyName = decapitalize(methodName.substring(3));
field = ParserConfig.getFieldFromCache(javaBeanCompatiblePropertyName, fieldCacheMap);
}
}
JSONField fieldAnnotation = null;
if(field != null){
fieldAnnotation = TypeUtils.getAnnotation(field, JSONField.class);
if(fieldAnnotation != null){
if(!fieldAnnotation.serialize()){
continue;
}
ordinal = fieldAnnotation.ordinal();
serialzeFeatures = SerializerFeature.of(fieldAnnotation.serialzeFeatures());
parserFeatures = Feature.of(fieldAnnotation.parseFeatures());
if(fieldAnnotation.name().length() != 0){
fieldAnnotationAndNameExists = true;
propertyName = fieldAnnotation.name();
if(aliasMap != null){
propertyName = aliasMap.get(propertyName);
if(propertyName == null){
continue;
}
}
}
if(fieldAnnotation.label().length() != 0){
label = fieldAnnotation.label();
}
}
}
if(aliasMap != null){
propertyName = aliasMap.get(propertyName);
if(propertyName == null){
continue;
}
}
if(propertyNamingStrategy != null && !fieldAnnotationAndNameExists){
propertyName = propertyNamingStrategy.translate(propertyName);
}
FieldInfo fieldInfo = new FieldInfo(propertyName, method, field, clazz, null, ordinal, serialzeFeatures, parserFeatures,
annotation, fieldAnnotation, label);
fieldInfoMap.put(propertyName, fieldInfo);
}
if(methodName.startsWith("is")){
if(methodName.length() < 3){
continue;
}
if(returnType != Boolean.TYPE
&& returnType != Boolean.class){
continue;
}
char c2 = methodName.charAt(2);
String propertyName;
Field field = null;
if(Character.isUpperCase(c2)){
if(compatibleWithJavaBean){
propertyName = decapitalize(methodName.substring(2));
} else{
propertyName = Character.toLowerCase(methodName.charAt(2)) + methodName.substring(3);
}
propertyName = getPropertyNameByCompatibleFieldName(fieldCacheMap, methodName, propertyName, 2);
} else if(c2 == '_'){
propertyName = methodName.substring(3);
field = fieldCacheMap.get(propertyName);
if (field == null) {
String temp = propertyName;
propertyName = methodName.substring(2);
field = ParserConfig.getFieldFromCache(propertyName, fieldCacheMap);
if (field == null) {
propertyName = temp;
}
}
} else if(c2 == 'f'){
propertyName = methodName.substring(2);
} else{
propertyName = methodName.substring(2);
field = ParserConfig.getFieldFromCache(propertyName, fieldCacheMap);
if (field == null) {
continue;
}
}
boolean ignore = isJSONTypeIgnore(clazz, propertyName);
if(ignore){
continue;
}
if(field == null) {
field = ParserConfig.getFieldFromCache(propertyName, fieldCacheMap);
}
if(field == null){
field = ParserConfig.getFieldFromCache(methodName, fieldCacheMap);
}
JSONField fieldAnnotation = null;
if(field != null){
fieldAnnotation = TypeUtils.getAnnotation(field, JSONField.class);
if(fieldAnnotation != null){
if(!fieldAnnotation.serialize()){
continue;
}
ordinal = fieldAnnotation.ordinal();
serialzeFeatures = SerializerFeature.of(fieldAnnotation.serialzeFeatures());
parserFeatures = Feature.of(fieldAnnotation.parseFeatures());
if(fieldAnnotation.name().length() != 0){
propertyName = fieldAnnotation.name();
if(aliasMap != null){
propertyName = aliasMap.get(propertyName);
if(propertyName == null){
continue;
}
}
}
if(fieldAnnotation.label().length() != 0){
label = fieldAnnotation.label();
}
}
}
if(aliasMap != null){
propertyName = aliasMap.get(propertyName);
if(propertyName == null){
continue;
}
}
if(propertyNamingStrategy != null){
propertyName = propertyNamingStrategy.translate(propertyName);
}
//优先选择get
if(fieldInfoMap.containsKey(propertyName)){
continue;
}
FieldInfo fieldInfo = new FieldInfo(propertyName, method, field, clazz, null, ordinal, serialzeFeatures, parserFeatures,
annotation, fieldAnnotation, label);
fieldInfoMap.put(propertyName, fieldInfo);
}
}
Field[] fields = clazz.getFields();
computeFields(clazz, aliasMap, propertyNamingStrategy, fieldInfoMap, fields);
return getFieldInfos(clazz, sorted, fieldInfoMap);
}
2、解决问题
方案一: 形成共识,保证isXxx不会NPE
对于一个可能会被fastjson序列化输出的Class,其中的getXxx、isXxx方法,一定要判空,保证不会产生NPE;
方案二: fastjson的处理办法
这里还有两个办法,
1、 JSON.toJSONString时,只输出fields,而不根据getXxx、isXxx两类方法构造出的虚拟fields;
SerializeConfig globalInstance = new SerializeConfig(true);
System.out.println(JSON.toJSON(family, globalInstance));
2、在Class上添加注解,排出一些属性
针对isBoy方法,就’boy’排出;
@JSONType(ignores = {"xxx"})
代扣参考
/**
* Alipay.com Inc.
* Copyright (c) 2004-2021 All Rights Reserved.
*/
package org.hinsteny.fastjson;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.annotation.JSONType;
import com.alibaba.fastjson.serializer.SerializeConfig;
import java.io.Serializable;
/**
* @author yatao.tyt
* @version $Id: ToJsonStringOfFields.java, v 0.1 2021-12-13 5:51 PM yatao.tyt Exp$
*/
public class ToJsonStringOfFields {
public static void main(String[] args) {
Father father = new Father("MyFather", 30, true);
Mother mother = new Mother();
Family family = new Family("MyFamily", father, mother);
// 默认常规操作--输出后发现Family中多了一个fatherInHome字段,这个就要看下是否符合预期了;
System.out.println(JSON.toJSON(family));
family = new Family("MyFamily", null, mother);
// NPE报错,这里就有点不太能接受了,本想只是把一个对象按照JSON格式化输出,但是由于代码不规范加上数据原因抛出了NPE;
//System.out.println(JSON.toJSON(family));
//System.out.println(JSON.toJSONString(family));
// 说下这个问题产生的原因
/**
* 1、团队同学实践DDD开发模式,在一个POJO类上定义了public的领域方法;
* 2、JSON.toJSON方法的调用采用的是fastJson默认配置(序列化时会同时处理类中的fields、getXxx、isXxx);
* 3、系统统一拦截器对数据领域对象进行JSON格式的日志打印;
*/
// 如何解决这个问题呢?
/**
* 两个方面分析
* 1、如果团队同学都用的DDD开发模式,那很容易产生isXxx这样的方法,那我们应该避免系统中对此类对象进行类似于拦截器日志打印JSON字符串输出这类操作;
* 2、避免不了1的话,那就要把JSON这个工具搞明白,用清楚;
*/
// 方案一: JSON.toJSONString时,只输出fields,而不根据getXxx、isXxx两类方法构造出的虚拟fields;
SerializeConfig globalInstance = new SerializeConfig(true);
System.out.println(JSON.toJSON(family, globalInstance));
// 方案二: 在要JSON输出的类上面显示声明要输出哪些属性,排除那些属性;
/**
* 在类上添加如下注解
* <P>@JSONType(ignores = {"fatherInHome"})</P>
*/
System.out.println(JSON.toJSON(family));
}
@JSONType(ignores = {"fatherInHome"})
public static class Family implements Serializable {
private static final long serialVersionUID = 7830552878571921405L;
public Family(String name, Father father, Mother mother) {
this.name = name;
this.father = father;
this.mother = mother;
}
private String name;
private Father father;
private Mother mother;
public boolean isFatherInHome() {
return father.isInHome();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Father getFather() {
return father;
}
public void setFather(Father father) {
this.father = father;
}
public Mother getMother() {
return mother;
}
public void setMother(Mother mother) {
this.mother = mother;
}
@Override
public String toString() {
return "Family{" +
"name='" + name + '\'' +
", father=" + father +
", mother=" + mother +
'}';
}
}
public static class Father implements Serializable {
private static final long serialVersionUID = -3850777720843804181L;
private String name;
private int age;
private boolean inHome;
public Father(String name, int age, boolean inHome) {
this.name = name;
this.age = age;
this.inHome = inHome;
}
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;
}
public boolean isInHome() {
return inHome;
}
public void setInHome(boolean inHome) {
this.inHome = inHome;
}
}
public static class Mother implements Serializable {
private static final long serialVersionUID = -2758058771419796216L;
private String name;
private 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;
}
}
}