关于java:Gson:如何在没有注释的情况下从序列化中排除特定字段

Gson: How to exclude specific fields from Serialization without annotations

我正在努力学习GSON,我也在努力排除外场干扰。这是我的课

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Student {    
  private Long                id;
  private String              firstName        ="Philip";
  private String              middleName       ="J.";
  private String              initials         ="P.F";
  private String              lastName         ="Fry";
  private Country             country;
  private Country             countryOfBirth;
}

public class Country {    
  private Long                id;
  private String              name;
  private Object              other;
}

我可以使用gsonbuilder并为诸如firstNamecountry这样的字段名添加一个排除策略,但我似乎无法排除某些字段(如country.name)的属性。

使用方法public boolean shouldSkipField(FieldAttributes fa)时,fieldattributes不包含足够的信息,无法将字段与像country.name这样的过滤器匹配。

如果能帮我解决这个问题,我将不胜感激。

P.S:我想避免注释,因为我想改进这一点,并使用regex过滤掉字段。

谢谢你

编辑:我想看看是否可以模拟struts2 JSON插件的行为。

使用GSON

1
2
3
4
5
6
7
<interceptor-ref name="json">
  <param name="enableSMD">true</param>
  <param name="excludeProperties">
    login.password,
    studentList.*\.sin
  </param>
</interceptor-ref>

编辑:我重新打开问题并添加了以下内容:

为了进一步澄清这个问题,我添加了第二个相同类型的字段。基本上我想排除country.name,但不排除countrOfBirth.name。我也不想把国家作为一种类型排除在外。所以类型是相同的,它是对象图中我想要精确定位和排除的实际位置。


一般来说,任何不希望序列化的字段都应该使用"transient"修饰符,这也适用于JSON序列化程序(至少它适用于我使用过的一些字段,包括GSON)。

如果不希望名称出现在序列化JSON中,请给它一个临时关键字,例如:

1
private transient String name;

GSON文档中的更多详细信息


Nishant提供了一个很好的解决方案,但有一个更简单的方法。只需使用@expose注释标记所需字段,例如:

1
@Expose private Long id;

删除任何不想序列化的字段。然后用这种方式创建GSON对象:

1
Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();


所以,您要排除firstNamecountry.name。这是你的ExclusionStrategy应该是什么样子

1
2
3
4
5
6
7
8
9
10
11
12
13
    public class TestExclStrat implements ExclusionStrategy {

        public boolean shouldSkipClass(Class<?> arg0) {
            return false;
        }

        public boolean shouldSkipField(FieldAttributes f) {

            return (f.getDeclaringClass() == Student.class && f.getName().equals("firstName"))||
            (f.getDeclaringClass() == Country.class && f.getName().equals("name"));
        }

    }

如果你仔细观察,它会返回true代表Student.firstNamecountry.name,这是你想要排除的。

你需要像这样应用这个ExclusionStrategy

1
2
3
4
5
6
7
    Gson gson = new GsonBuilder()
        .setExclusionStrategies(new TestExclStrat())
        //.serializeNulls() <-- uncomment to serialize NULL fields as well
        .create();
    Student src = new Student();
    String json = gson.toJson(src);
    System.out.println(json);

这将返回:

{"middleName":"J.","initials":"P.F","lastName":"Fry","country":{"id":91}}

我假设Country对象是在学生班中用id = 91L初始化的。

你可能会喜欢上。例如,您不希望序列化名称中包含"name"字符串的任何字段。执行此操作:

1
2
3
public boolean shouldSkipField(FieldAttributes f) {
    return f.getName().toLowerCase().contains("name");
}

这将返回:

1
{"initials":"P.F","country": {"id": 91 }}

编辑:根据需要添加更多信息。

这个ExclusionStrategy可以做到这一点,但是您需要传递"完全限定的字段名"。见下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    public class TestExclStrat implements ExclusionStrategy {

        private Class<?> c;
        private String fieldName;
        public TestExclStrat(String fqfn) throws SecurityException, NoSuchFieldException, ClassNotFoundException
        {
            this.c = Class.forName(fqfn.substring(0, fqfn.lastIndexOf(".")));
            this.fieldName = fqfn.substring(fqfn.lastIndexOf(".")+1);
        }
        public boolean shouldSkipClass(Class<?> arg0) {
            return false;
        }

        public boolean shouldSkipField(FieldAttributes f) {

            return (f.getDeclaringClass() == c && f.getName().equals(fieldName));
        }

    }

下面是我们如何通用地使用它。

1
2
3
4
5
6
7
    Gson gson = new GsonBuilder()
        .setExclusionStrategies(new TestExclStrat("in.naishe.test.Country.name"))
        //.serializeNulls()
        .create();
    Student src = new Student();
    String json = gson.toJson(src);
    System.out.println(json);

它返回:

1
{"firstName":"Philip" ,"middleName":"J.","initials":"P.F","lastName":"Fry","country": {"id": 91 }}


在阅读了所有可用的答案之后,我发现,在我的例子中,最灵活的方法是使用自定义的@Exclude注释。因此,我实施了简单的策略(我不想使用@Expose标记所有字段,也不想使用transient,这与应用程序中的Serializable序列化冲突):

注释:

1
2
3
4
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Exclude {
}

策略:

1
2
3
4
5
6
7
8
9
10
11
12
public class AnnotationExclusionStrategy implements ExclusionStrategy {

    @Override
    public boolean shouldSkipField(FieldAttributes f) {
        return f.getAnnotation(Exclude.class) != null;
    }

    @Override
    public boolean shouldSkipClass(Class<?> clazz) {
        return false;
    }
}

用途:

1
new GsonBuilder().setExclusionStrategies(new AnnotationExclusionStrategy()).create();


我遇到了这个问题,在这个问题中,我只想从序列化中排除少量字段,因此我开发了一个相当简单的解决方案,它使用GSON的@Expose注释和自定义排除策略。

使用@Expose的唯一内置方法是设置GsonBuilder.excludeFieldsWithoutExposeAnnotation(),但顾名思义,没有显式@Expose的字段将被忽略。因为我只有几个字段要排除,所以我发现向每个字段添加注释的前景非常麻烦。

我实际上想要的是相反的,除非我明确地使用@Expose排除它,否则所有内容都包含在其中。我使用了以下排除策略来实现这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
new GsonBuilder()
        .addSerializationExclusionStrategy(new ExclusionStrategy() {
            @Override
            public boolean shouldSkipField(FieldAttributes fieldAttributes) {
                final Expose expose = fieldAttributes.getAnnotation(Expose.class);
                return expose != null && !expose.serialize();
            }

            @Override
            public boolean shouldSkipClass(Class<?> aClass) {
                return false;
            }
        })
        .addDeserializationExclusionStrategy(new ExclusionStrategy() {
            @Override
            public boolean shouldSkipField(FieldAttributes fieldAttributes) {
                final Expose expose = fieldAttributes.getAnnotation(Expose.class);
                return expose != null && !expose.deserialize();
            }

            @Override
            public boolean shouldSkipClass(Class<?> aClass) {
                return false;
            }
        })
        .create();

现在我可以很容易地排除一些带有@Expose(serialize = false)@Expose(deserialize = false)注释的字段(注意,两个@Expose属性的默认值都是true)。当然,您可以使用@Expose(serialize = false, deserialize = false),但更简洁的方法是声明字段transient,而不是声明字段transient,它仍然与这些自定义排除策略一起生效。


您可以使用gson探索json树。

尝试如下操作:

1
2
gson.toJsonTree(student).getAsJsonObject()
.get("country").getAsJsonObject().remove("name");

还可以添加一些属性:

1
gson.toJsonTree(student).getAsJsonObject().addProperty("isGoodStudent", false);

用GSON 2.2.4测试。


我想出了一个类工厂来支持这个功能。传递要排除的字段或类的任意组合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class GsonFactory {

    public static Gson build(final List<String> fieldExclusions, final List<Class<?>> classExclusions) {
        GsonBuilder b = new GsonBuilder();
        b.addSerializationExclusionStrategy(new ExclusionStrategy() {
            @Override
            public boolean shouldSkipField(FieldAttributes f) {
                return fieldExclusions == null ? false : fieldExclusions.contains(f.getName());
            }

            @Override
            public boolean shouldSkipClass(Class<?> clazz) {
                return classExclusions == null ? false : classExclusions.contains(clazz);
            }
        });
        return b.create();

    }
}

要使用,请创建两个列表(每个列表都是可选的),然后创建GSON对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static {
 List<String> fieldExclusions = new ArrayList<String>();
 fieldExclusions.add("id");
 fieldExclusions.add("provider");
 fieldExclusions.add("products");

 List<Class<?>> classExclusions = new ArrayList<Class<?>>();
 classExclusions.add(Product.class);
 GSON = GsonFactory.build(null, classExclusions);
}

private static final Gson GSON;

public String getSomeJson(){
    List<Provider> list = getEntitiesFromDatabase();
    return GSON.toJson(list);
}


我用自定义注释解决了这个问题。这是我的"Skipserialisation"注释类:

1
2
3
4
@Target (ElementType.FIELD)
public @interface SkipSerialisation {

}

这是我的GSONBuilder:

1
2
3
4
5
6
7
8
9
10
11
12
13
gsonBuilder.addSerializationExclusionStrategy(new ExclusionStrategy() {

  @Override public boolean shouldSkipField (FieldAttributes f) {

    return f.getAnnotation(SkipSerialisation.class) != null;

  }

  @Override public boolean shouldSkipClass (Class<?> clazz) {

    return false;
  }
});

例子:

1
2
3
4
5
6
7
8
9
public class User implements Serializable {

  public String firstName;

  public String lastName;

  @SkipSerialisation
  public String email;
}


或者可以说哪些字段将不会显示:

1
Gson gson = gsonBuilder.excludeFieldsWithModifiers(Modifier.TRANSIENT).create();

在类的属性上:

1
private **transient** boolean nameAttribute;


我使用了这个策略:我排除了没有用@serializedname注释标记的每个字段,即:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Dummy {

    @SerializedName("VisibleValue")
    final String visibleValue;
    final String hiddenValue;

    public Dummy(String visibleValue, String hiddenValue) {
        this.visibleValue = visibleValue;
        this.hiddenValue = hiddenValue;
    }
}


public class SerializedNameOnlyStrategy implements ExclusionStrategy {

    @Override
    public boolean shouldSkipField(FieldAttributes f) {
        return f.getAnnotation(SerializedName.class) == null;
    }

    @Override
    public boolean shouldSkipClass(Class<?> clazz) {
        return false;
    }
}


Gson gson = new GsonBuilder()
                .setExclusionStrategies(new SerializedNameOnlyStrategy())
                .create();

Dummy dummy = new Dummy("I will see this","I will not see this");
String json = gson.toJson(dummy);

它回来了

{"VisibleValue":"I will see this"}


另一种方法(如果需要在运行时决定排除字段,则特别有用)是向GSON实例注册一个类型适配器。示例如下:

1
2
Gson gson = new GsonBuilder()
.registerTypeAdapter(BloodPressurePost.class, new BloodPressurePostSerializer())

在下面的例子中,服务器期望两个值中的一个,但是由于它们都是int,所以gson将对它们进行序列化。我的目标是从发布到服务器的JSON中省略零(或更小)的任何值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class BloodPressurePostSerializer implements JsonSerializer<BloodPressurePost> {

    @Override
    public JsonElement serialize(BloodPressurePost src, Type typeOfSrc, JsonSerializationContext context) {
        final JsonObject jsonObject = new JsonObject();

        if (src.systolic > 0) {
            jsonObject.addProperty("systolic", src.systolic);
        }

        if (src.diastolic > 0) {
            jsonObject.addProperty("diastolic", src.diastolic);
        }

        jsonObject.addProperty("units", src.units);

        return jsonObject;
    }
}


Kotlin的@Transient注释显然也起到了作用。

1
2
3
4
5
data class Json(
    @field:SerializedName("serialized_field_1") val field1: String,
    @field:SerializedName("serialized_field_2") val field2: String,
    @Transient val field3: String
)

输出:

1
{"serialized_field_1":"VALUE1","serialized_field_2":"VALUE2"}

我有Kotlin版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FIELD)
internal annotation class JsonSkip

class SkipFieldsStrategy : ExclusionStrategy {

    override fun shouldSkipClass(clazz: Class<*>): Boolean {
        return false
    }

    override fun shouldSkipField(f: FieldAttributes): Boolean {
        return f.getAnnotation(JsonSkip::class.java) != null
    }
}

以及如何将其添加到改造GSOnConverterFactory中:

1
2
3
4
5
6
7
8
9
val gson = GsonBuilder()
                .setExclusionStrategies(SkipFieldsStrategy())
                //.serializeNulls()
                //.setDateFormat(DateFormat.LONG)
                //.setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE)
                //.setPrettyPrinting()
                //.registerTypeAdapter(Id.class, IdTypeAdapter())
                .create()
        return GsonConverterFactory.create(gson)

我只是把@Expose注释放在这里,这里是我使用的版本

1
2
compile 'com.squareup.retrofit2:retrofit:2.0.2'
compile 'com.squareup.retrofit2:converter-gson:2.0.2'

Model类中:

1
2
3
4
@Expose
int number;

public class AdapterRestApi {

Adapter类中:

1
2
3
4
5
6
7
8
9
10
11
12
13
public EndPointsApi connectRestApi() {
    OkHttpClient client = new OkHttpClient.Builder()
            .connectTimeout(90000, TimeUnit.SECONDS)
            .readTimeout(90000,TimeUnit.SECONDS).build();

    Retrofit retrofit = new Retrofit.Builder()
            .baseUrl(ConstantRestApi.ROOT_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .client(client)
            .build();

    return retrofit.create  (EndPointsApi.class);
}


这就是我一直使用的:

GSON中实现的默认行为是忽略空对象字段。

意味着gson对象不将具有空值的字段序列化为json。如果Java对象中的字段为NULL,则Gson将其排除。

您可以使用此函数将某些对象转换为空值或由您自己设置的良好值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
     /**
   * convert object to json
   */

  public String toJson(Object obj) {
    // Convert emtpy string and objects to null so we don't serialze them
    setEmtpyStringsAndObjectsToNull(obj);
    return gson.toJson(obj);
  }

  /**
   * Sets all empty strings and objects (all fields null) including sets to null.
   *
   * @param obj any object
   */

  public void setEmtpyStringsAndObjectsToNull(Object obj) {
    for (Field field : obj.getClass().getDeclaredFields()) {
      field.setAccessible(true);
      try {
        Object fieldObj = field.get(obj);
        if (fieldObj != null) {
          Class fieldType = field.getType();
          if (fieldType.isAssignableFrom(String.class)) {
            if(fieldObj.equals("")) {
              field.set(obj, null);
            }
          } else if (fieldType.isAssignableFrom(Set.class)) {
            for (Object item : (Set) fieldObj) {
              setEmtpyStringsAndObjectsToNull(item);
            }
            boolean setFielToNull = true;
            for (Object item : (Set) field.get(obj)) {
              if(item != null) {
                setFielToNull = false;
                break;
              }
            }
            if(setFielToNull) {
              setFieldToNull(obj, field);
            }
          } else if (!isPrimitiveOrWrapper(fieldType)) {
            setEmtpyStringsAndObjectsToNull(fieldObj);
            boolean setFielToNull = true;
            for (Field f : fieldObj.getClass().getDeclaredFields()) {
              f.setAccessible(true);
              if(f.get(fieldObj) != null) {
                setFielToNull = false;
                break;
              }
            }
            if(setFielToNull) {
              setFieldToNull(obj, field);
            }
          }
        }
      } catch (IllegalAccessException e) {
        System.err.println("Error while setting empty string or object to null:" + e.getMessage());
      }
    }
  }

  private void setFieldToNull(Object obj, Field field) throws IllegalAccessException {
    if(!Modifier.isFinal(field.getModifiers())) {
      field.set(obj, null);
    }
  }

  private boolean isPrimitiveOrWrapper(Class fieldType)  {
    return fieldType.isPrimitive()
        || fieldType.isAssignableFrom(Integer.class)
        || fieldType.isAssignableFrom(Boolean.class)
        || fieldType.isAssignableFrom(Byte.class)
        || fieldType.isAssignableFrom(Character.class)
        || fieldType.isAssignableFrom(Float.class)
        || fieldType.isAssignableFrom(Long.class)
        || fieldType.isAssignableFrom(Double.class)
        || fieldType.isAssignableFrom(Short.class);
  }