Issues while deserializing exception/throwable using Jackson in Java

前端 未结 6 1774
無奈伤痛
無奈伤痛 2021-02-19 13:42

I am facing issues while deserializing Exception and Throwable instances using Jackson (version 2.2.1). Consider the following snippet:



        
相关标签:
6条回答
  • 2021-02-19 14:06

    There seems to be a Jackson JIRA entry for this here. Jackson doesn't seem to be able to handle the declaringClass in java.lang.StackTraceElement, since the getter corresponding to this field is called getClassName().

    I fixed this issue by using a custom wrapper around StackTraceElement as suggested in the JIRA entry mentioned above. The custom wrapper (CustomStackTraceElement) will have the fields declaringClass, methodName, fileName, and lineNumber and the corresponding getters and setters in it. I modified the catch block (mentioned in the question) to be as follows:

    catch (NumberFormatException e) {
        RuntimeException runtimeException = new RuntimeException(e);
        e.printStackTrace();
        String serializedException = objectMapper.writeValueAsString(runtimeException);
        System.out.println(serializedException);
    
        String serializedStackTrace = objectMapper.writeValueAsString(transformStackTrace(runtimeException));
        String serializedStackTraceForCause = objectMapper.writeValueAsString(transformStackTrace(runtimeException.getCause()));
    
        Throwable throwable = objectMapper.readValue(serializedException, Throwable.class);
        List<CustomStackTraceElement> customStackTraceElementList = objectMapper.readValue(serializedStackTrace, List.class);
        List<CustomStackTraceElement> customStackTraceElementListForCause = objectMapper.readValue(serializedStackTraceForCause, List.class);
    
        throwable.setStackTrace(reverseTransformStackTrace(customStackTraceElementList));
        throwable.getCause().setStackTrace(reverseTransformStackTrace(customStackTraceElementListForCause));
        throwable.printStackTrace();
    }
    

    The StackTraceElement[] will be converted into List<CustomStackTraceElement> by the following method during serialization:

    private static List<CustomStackTraceElement> transformStackTrace(Throwable throwable)
    {
        List<CustomStackTraceElement> list = new ArrayList<>();
        for (StackTraceElement stackTraceElement : throwable.getStackTrace()) {
            CustomStackTraceElement customStackTraceElement =
                new CustomStackTraceElement(stackTraceElement.getClassName(),
                                            stackTraceElement.getMethodName(),
                                            stackTraceElement.getFileName(),
                                            stackTraceElement.getLineNumber());
    
            list.add(customStackTraceElement);
        }
    
        return list;
    }
    

    ... and the reverse transformation will be done during deserialization:

    private static StackTraceElement[] reverseTransformStackTrace(List<CustomStackTraceElement> customStackTraceElementList)
    {
        StackTraceElement[] stackTraceElementArray = new StackTraceElement[customStackTraceElementList.size()];
        for (int i = 0; i < customStackTraceElementList.size(); i++) {
            CustomStackTraceElement customStackTraceElement = customStackTraceElementList.get(i);
            StackTraceElement stackTraceElement =
                new StackTraceElement(customStackTraceElement.getDeclaringClass(),
                                      customStackTraceElement.getMethodName(),
                                      customStackTraceElement.getFileName(),
                                      customStackTraceElement.getLineNumber());
    
            stackTraceElementArray[i] = stackTraceElement;
        }
    
        return stackTraceElementArray;
    }
    

    Now, after deserialization, the Throwable object has the expected stack trace in it.

    0 讨论(0)
  • 2021-02-19 14:08

    Is it so necessary to use json serialization? Looks liks there are some bugs with throwables. Why not use system api:

    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(  );
    ObjectOutputStream objectOutputStream = new ObjectOutputStream( byteArrayOutputStream );
    objectOutputStream.writeObject( e );
    
    ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream( byteArrayOutputStream.toByteArray() );
    ObjectInputStream objectInputStream = new ObjectInputStream( byteArrayInputStream );
    Throwable t = (Throwable) objectInputStream.readObject();
    
    0 讨论(0)
  • 2021-02-19 14:08

    It seems that the output you get in version 2.2.1 is not the same as I get with version 2.2.0 (which according to the website is the latest 2.x version). Besides the latest available 2.x version on the Maven Repository is 2.2.2. So I would try to either downgrade it to 2.2.0 or to upgrade it to 2.2.2. If any of the changes brings you the expected result, I would go further with that version and open a BUG in Jackson's JIRA.

    And of course don't forget

    objectMapper.configure( DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    

    from Michael's answer.

    0 讨论(0)
  • 2021-02-19 14:16

    Add this:

    objectMapper.configure( DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    

    And make out of the deserialized exception the same way, as for the first time:

    System.out.println( objectMapper.writeValueAsString( throwable ) );
    

    I used the following code:

    public static void main( String[] args ) throws IOException
    {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure( SerializationFeature.INDENT_OUTPUT, true );
        objectMapper.configure( DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        objectMapper.setVisibility( PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY );
        objectMapper.enableDefaultTyping( ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY );
    
        try
        {
            Integer.parseInt( "String" );
        }
        catch( NumberFormatException e )
        {
            Throwable throwable = objectMapper.readValue( objectMapper.writeValueAsString( e ), Throwable.class );
            System.out.println( objectMapper.writeValueAsString( throwable ) );
        }
    }
    

    Added this jars: jackson-annotations-2.2.0.jar, jackson-core-2.2.0.jar and jackson-databind-2.2.0.jar.

    After execution, the following is printed:

    {
    "@class" : "java.lang.NumberFormatException",
    "detailMessage" : "For input string: \"String\"",
    "cause" : null,
    "stackTrace" : [ {
        "declaringClass" : "java.lang.NumberFormatException",
        "methodName" : "forInputString",
        "fileName" : "NumberFormatException.java",
        "lineNumber" : 48,
        "className" : "java.lang.NumberFormatException",
        "nativeMethod" : false
    }, {
        "declaringClass" : "java.lang.Integer",
        "methodName" : "parseInt",
        "fileName" : "Integer.java",
        "lineNumber" : 449,
        "className" : "java.lang.Integer",
        "nativeMethod" : false
    }, {
        "declaringClass" : "java.lang.Integer",
        "methodName" : "parseInt",
        "fileName" : "Integer.java",
        "lineNumber" : 499,
        "className" : "java.lang.Integer",
        "nativeMethod" : false
    }, {
        "declaringClass" : "com.sample.bla.Main",
        "methodName" : "main",
        "fileName" : "Main.java",
        "lineNumber" : 24,
        "className" : "com.sample.bla.Main",
        "nativeMethod" : false
    }, {
        "declaringClass" : "sun.reflect.NativeMethodAccessorImpl",
        "methodName" : "invoke0",
        "fileName" : "NativeMethodAccessorImpl.java",
        "lineNumber" : -2,
        "className" : "sun.reflect.NativeMethodAccessorImpl",
        "nativeMethod" : true
    }, {
        "declaringClass" : "sun.reflect.NativeMethodAccessorImpl",
        "methodName" : "invoke",
        "fileName" : "NativeMethodAccessorImpl.java",
        "lineNumber" : 39,
        "className" : "sun.reflect.NativeMethodAccessorImpl",
        "nativeMethod" : false
    }, {
        "declaringClass" : "sun.reflect.DelegatingMethodAccessorImpl",
        "methodName" : "invoke",
        "fileName" : "DelegatingMethodAccessorImpl.java",
        "lineNumber" : 25,
        "className" : "sun.reflect.DelegatingMethodAccessorImpl",
        "nativeMethod" : false
    }, {
        "declaringClass" : "java.lang.reflect.Method",
        "methodName" : "invoke",
        "fileName" : "Method.java",
        "lineNumber" : 597,
        "className" : "java.lang.reflect.Method",
        "nativeMethod" : false
    }, {
        "declaringClass" : "com.intellij.rt.execution.application.AppMain",
        "methodName" : "main",
        "fileName" : "AppMain.java",
        "lineNumber" : 120,
        "className" : "com.intellij.rt.execution.application.AppMain",
        "nativeMethod" : false
        } ],
        "message" : "For input string: \"String\"",
        "localizedMessage" : "For input string: \"String\""
    }
    
    0 讨论(0)
  • 2021-02-19 14:17

    Try using polymorphism so that jackson deserializer knows what kind of Throwable to create:

    /**
     * Jackson module to serialize / deserialize Throwable
     */
    public class ThrowableModule extends SimpleModule {
      public ThrowableModule() {
        super("Throwable", new Version(1, 0, 0, null, null, null));
      }
    
      @Override
      public void setupModule(SetupContext context) {
        context.setMixInAnnotations(Throwable.class, ThrowableAnnotations.class);
      }
    
      /**
       * Add annotation to Throwable so that the class name is serialized with the instance data.
       */
      @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "class")
      static abstract class ThrowableAnnotations {
      }
    }
    
    0 讨论(0)
  • 2021-02-19 14:24

    I've had a similar issue. I'm using this code now, and it allows me to serialize and deserialize exceptions with proper types (i.e. a RuntimeException will be a RuntimeException again :)):

    public static ObjectMapper createObjectMapper() {
        ObjectMapper mapper = new ObjectMapper(null, null, new DefaultDeserializationContext.Impl(
                new BeanDeserializerFactory(new DeserializerFactoryConfig()) {
                    private static final long serialVersionUID = 1L;
    
                    @Override
                    public JsonDeserializer<Object> buildThrowableDeserializer(
                            DeserializationContext ctxt, JavaType type, BeanDescription beanDesc)
                            throws JsonMappingException {
                        return super.buildBeanDeserializer(ctxt, type, beanDesc);
                    }
    
                }));
    
        mapper.setVisibility(PropertyAccessor.ALL, Visibility.NONE);
        mapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY);
        mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
    
        mapper.addMixIn(Throwable.class, ThrowableMixin.class);
        mapper.addMixIn(StackTraceElement.class, StackTraceElementMixin.class);
    
        return mapper;
    }
    
    @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@class")
    @JsonAutoDetect(fieldVisibility = Visibility.ANY)
    @JsonIgnoreProperties({ "message", "localizedMessage", "suppressed" })
    abstract class ThrowableMixin {
    
        @JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class, property = "$id")
        private Throwable cause;
    }
    
    abstract class StackTraceElementMixin {
    
        @JsonProperty("className")
        private String declaringClass;
    
    }
    

    I'm manipulating the BeanDeserializerFactory to make buildThrowableDeserializer not treat Throwable any special but just like any other Object. Then using Mixins to define the "special" handling of Throwable and StackTraceElement to my liking.

    0 讨论(0)
提交回复
热议问题