【问题标题】:Jackson serialize Object to JSON to base64 (without endless loop)Jackson 将 Object 序列化为 JSON 到 base64(没有无限循环)
【发布时间】:2020-05-10 01:33:04
【问题描述】:

有没有一种简单的方法可以使用 Jackson 将对象序列化为 base64 编码的 JSON? (对象 -> JSON -> base64)

我尝试使用自定义StdSerializer,但这(当然)会导致无限循环:

class MySerializer extends StdSerializer<Foo> {
  public void serialize(Foo value, JsonGenerator gen, SerializerProvider provider) {
    StringWriter stringWriter = new StringWriter();
    JsonGenerator newGen = gen.getCodec().getFactory().createGenerator(stringWriter);
    gen.getCodec().getFactory().getCodec().writeValue(newGen, value);
    String json = stringWriter.toString();
    String base64 = new String(Base64.getEncoder().encode(json.getBytes()));
    gen.writeString(base64);
  }
}

一种解决方法是将所有字段复制到另一个类并使用该类作为中间表示:

class TmpFoo {
  public String field1;
  public int field2;
  // ...
}

class MySerializer extends StdSerializer<Foo> {
  public void serialize(Foo value, JsonGenerator gen, SerializerProvider provider) {
    TmpFoo tmp = new TmpFoo();
    tmp.field1 = value.field1;
    tmp.field2 = value.field2;
    // etc.

    StringWriter stringWriter = new StringWriter();
    JsonGenerator newGen = gen.getCodec().getFactory().createGenerator(stringWriter);
    gen.getCodec().getFactory().getCodec().writeValue(newGen, tmp); // here "tmp" instead of "value"
    String json = stringWriter.toString();
    String base64 = new String(Base64.getEncoder().encode(json.getBytes()));
    gen.writeString(base64);
  }
}

不需要创建new ObjectMapper,因为我需要默认 ObjectMapper 的所有已注册模块和序列化程序。

我希望有一些更简单的方法来实现这一点。


编辑:示例

第 1 步:Java 对象

class Foo {
  String field1 = "foo";
  int field2 = 42;
}

第 2 步:JSON

{"field1":"foo","field2":42}

第 3 步:Base64

eyJmaWVsZDEiOiJmb28iLCJmaWVsZDIiOjQyfQ==

【问题讨论】:

    标签: java json jackson base64 jackson-databind


    【解决方案1】:

    根据this site,有一个解决方法可以避免这个递归问题:

    当我们定义一个自定义序列化器时,Jackson 会在内部覆盖 原始 BeanSerializer 实例 [...] 我们的 SerializerProvider 找到 每次都使用自定义的序列化程序,而不是默认的序列化程序,并且 这会导致无限循环。

    一种可能的解决方法是使用 BeanSerializerModifier 来存储 内部杰克逊之前类型文件夹的默认序列化程序 覆盖它。

    如果我正确理解了解决方法,您的 Serializer 应该如下所示:

    class FooSerializer extends StdSerializer<Foo> {
    
        private final JsonSerializer<Object> defaultSerializer;
    
        public FooSerializer(JsonSerializer<Object> defaultSerializer) {
            super(Foo.class);
            this.defaultSerializer = defaultSerializer;
        }
    
        @Override
        public void serialize(Foo value, JsonGenerator gen, SerializerProvider provider) throws IOException {
            StringWriter stringWriter = new StringWriter();
            JsonGenerator tempGen = provider.getGenerator().getCodec().getFactory().createGenerator(stringWriter);
            defaultSerializer.serialize(value, tempGen, provider);
    
            tempGen.flush();
    
            String json = stringWriter.toString();
            String base64 = new String(Base64.getEncoder().encode(json.getBytes()));
            gen.writeString(base64);
        }
    }
    

    除了序列化器,还需要一个修饰符:

    public class FooBeanSerializerModifier extends BeanSerializerModifier {
    
        @Override
        public JsonSerializer<?> modifySerializer(
          SerializationConfig config, BeanDescription beanDesc, JsonSerializer<?> serializer) {
    
            if (beanDesc.getBeanClass().equals(Foo.class)) {
                return new FooSerializer((JsonSerializer<Object>) serializer);
            }
            return serializer;
        }
    }
    

    示例模块:

    ObjectMapper mapper = new ObjectMapper();
    
    SimpleModule module = new SimpleModule();
    module.setSerializerModifier(new FooBeanSerializerModifier());
    
    mapper.registerModule(module);
    

    编辑:

    我添加了flush() 以刷新JsonGenerator tempGen。 另外,我使用 JUnit 创建了一个最小的测试环境,它使用 Foo 验证您的示例:可以在 here 找到 github 存储库。


    编辑:备选方案 2

    另一个(简单)选项是使用带有泛型的包装类:

    public class Base64Wrapper<T> {
    
        private final T wrapped;
    
        private Base64Wrapper(T wrapped) {
            this.wrapped = wrapped;
        }
    
        public T getWrapped() {
            return this.wrapped;
        }
    
        public static <T> Base64Wrapper<T> of(T wrapped) {
            return new Base64Wrapper<>(wrapped);
        }
    }
    
    public class Base64WrapperSerializer extends StdSerializer<Base64Wrapper> {
    
    
        public Base64WrapperSerializer() {
            super(Base64Wrapper.class);
        }
    
        @Override
        public void serialize(Base64Wrapper value, JsonGenerator gen, SerializerProvider provider) throws IOException {
            StringWriter stringWriter = new StringWriter();
            JsonGenerator tempGen = provider.getGenerator().getCodec().getFactory().createGenerator(stringWriter);
            provider.defaultSerializeValue(value.getWrapped(), tempGen);
            tempGen.flush();
    
            String json = stringWriter.toString();
            String base64 = new String(Base64.getEncoder().encode(json.getBytes()));
            gen.writeString(base64);
        }
    }
    

    一个示例用例是:

    final ObjectMapper mapper = new ObjectMapper();
    SimpleModule module = new SimpleModule();
    module.addSerializer(new Base64WrapperSerializer());
    mapper.registerModule(module);
    
    final Foo foo = new Foo();
    final Base64Wrapper<Foo> base64Wrapper = Base64Wrapper.of(foo);
    final String base64Json = mapper.writeValueAsString(base64Wrapper);
    

    这个例子可以在this GitHub (branch: wrapper) repo 中找到,用 JUnit 测试从你的 foo 例子中验证你的 BASE64 字符串。

    【讨论】:

    • 看起来很有希望,但不知何故不起作用。 BeanSerializerModifier 和 StdSerializer 被正确调用,但 stringWriter.toString() 导致空字符串。当写入 gen 而不是 tempGen 时,数据对象看起来不错,但当然 base64 编码是不可能的。
    • 我已经更新了我的答案:tempGen 缺少 flush()。你可以看看 github repo,我已经链接了。在那里,结果在测试中得到验证。希望,它现在可以工作了:)
    • @BenjaminM 我再次更新了我的答案。还有第二种方法可以获取对象的 base64 编码 json,方法是将其包装在上面我的示例中的 Base64Wrapper 中。 Base64Wrapper 有它的自定义序列化程序 Base64WrapperSerializer,它获取包装的 Object 默认序列化并将其编码为 base64。如果您有 Foo 的自定义序列化程序,并且想要在 json 序列化然后 base64 编码或仅 json 序列化而不使用 base64 之间进行选择,这种方法可能会更好。
    • 感谢您的热情:)!我已经搜索并尝试了更多的东西,现在我很确定,没有“简单”的方法来完成这个简单的任务。 base64 包装器看起来不错,但遗憾的是我不能使用它,因为我不能修改源对象。尽管如此,还是竖起大拇指。你赢得了赏金。
    【解决方案2】:

    您可以将现有对象转换为地图,而不是创建新对象。如下例所示

    import static java.nio.charset.StandardCharsets.UTF_8;
    
    public class FooSerializer extends StdSerializer<Foo> {
    
        public FooSerializer() {
            super(Foo.class);
        }
    
        @Override
        public void serialize(Foo foo, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) {
            try {
                ObjectMapper mapper = (ObjectMapper) jsonGenerator.getCodec();
                var map = toMap(foo); // if you need class info for deserialization than use toMapWithClassInfo
                String json = mapper.writeValueAsString(map);
                jsonGenerator.writeString(Base64.getEncoder().encodeToString(json.getBytes(UTF_8)));
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    
        public static Map<String, Object> toMap(Object o) throws Exception {
            Map<String, Object> result = new HashMap<>();
            Field[] declaredFields = o.getClass().getDeclaredFields();
            for (Field field : declaredFields) {
                field.setAccessible(true);
                result.put(field.getName(), field.get(o));
            }
            return result;
        }
    
    
        public static Map<String, Object> toMapWithClassInfo(Object obj) throws Exception {
            Map<String, Object> result = new HashMap<>();
            BeanInfo info = Introspector.getBeanInfo(obj.getClass());
            for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
                Method reader = pd.getReadMethod();
                if (reader != null)
                    result.put(pd.getName(), reader.invoke(obj));
            }
            return result;
        }
    }
    

    我提供了两种转换为地图的方法:有和没有类信息。选择一个,适用于您的问题。

    【讨论】:

    • 谢谢。使用 Map 作为中间表示似乎也可以。但这仍然感觉很麻烦,并且与使用具有相同属性的第二个类没有太大区别或更容易。
    【解决方案3】:

    序列化对象杰克逊搜索@JsonValue方法。您可以在Foo 类中添加@JsonValue 注释的encodedJsonString 方法。

    试试这个:

    @Getter
    @Setter
    public class Foo implements Serializable {
    
        private static final long serialVersionUID = 1L;
    
        public String field1;
        public int field2;
    
        @JsonValue
        public String toEncodedJsonString() {
            try {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                new ObjectOutputStream(baos).writeObject(this);
                return org.apache.commons.codec.binary.Base64.encodeBase64String(baos.toByteArray());
            }catch (Exception ex){
    
            }
            return null;
        }
    }
    

    【讨论】:

    • 我不想要二进制 Java 对象表示的序列化形式。我想要一个 JSON 文档中的 base64 编码 JSON 字符串。我将在问题中添加一个示例
    猜你喜欢
    • 2013-06-16
    • 1970-01-01
    • 2017-09-05
    • 2014-06-15
    • 1970-01-01
    • 1970-01-01
    • 2013-01-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多