让Spring / Jackson智能地反序列化模型子类

时间:2017-03-31 10:48:50

标签: java json spring jackson

给出如此的模型层次结构:

// WARNING: This is pseudo-code for giving an example!
public abstract class BaseVehicle {
    private String make;
    private String model;

    // Constructors, getters & setters down here
}

public class Motorcycle extends BaseVehicle {
    private int numCylinders;

    // Constructors, getters & setters down here
}

public class Car extends BaseVehicle {
    // ...etc.
}

并给出以下有效负载类(将被发送到Spring控制器):

public class Payload {
    @JsonIgnore
    @JsonProperty(value = "orgId")
    private String orgId;

    @JsonIgnore
    @JsonProperty(value = "isInitialized")
    private Boolean isInitialized;

    @JsonIgnore
    @JsonProperty(value = "vehicle")
    private BaseVehicle vehicle;

    // Constructors, getters & setters down here
}

我想知道是否有可能让Spring控制器(使用Jackson进行JSON序列化)配置为只接收它收到的BaseVehicle中的Payload个实例,但要动态推断实际发送了BaseVehicle个子类:

@RequestMapping(value='/payload', method=RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
@ResponseBody MyAppResponse onPayload(@RequestBody Payload payload) {
    logger.info("Received a payload with a vehicle of type: " + payload.getVehicle().getClass().getName());
}

因此,如果我碰巧发送包含Payload作为Motorcycle字段的vehicle JSON,那么当logger.info(...)语句触发时,代码会看到{ {1}}是vehicle(和其他任何Motorcycle子类一样)?

这是可能的,如果是这样,怎么办?

1 个答案:

答案 0 :(得分:1)

  

但是我非常喜欢允许JSON保持原样的解决方案。

正如我在上面的评论中提到的,您可以分析有效载荷工具JSON对象树,以便进行一些分析,尝试检测有效载荷元素类型。

@JsonDeserialize(using = BaseVehicleJsonDeserializer.class)
abstract class BaseVehicle {

    @JsonProperty
    private String make;

    @JsonProperty
    private String model;

}

@JsonDeserialize(as = Car.class)
final class Car
        extends BaseVehicle {
}

@JsonDeserialize(as = Motorcycle.class)
final class Motorcycle
        extends BaseVehicle {

    @JsonProperty
    private int numCylinders;

}

这里的诀窍是@JsonDeserialize注释。 BaseVehicleJsonDeserializer可以按如下方式实施:

final class BaseVehicleJsonDeserializer
        extends JsonDeserializer<BaseVehicle> {

    @Override
    public BaseVehicle deserialize(final JsonParser parser, final DeserializationContext context)
            throws IOException {
        final TreeNode treeNode = parser.readValueAsTree();
        final Class<? extends BaseVehicle> baseVehicleClass = Stream.of(treeNode)
                // Check if the tree node is ObjectNode
                .filter(tn -> tn instanceof ObjectNode)
                // And cast
                .map(tn -> (ObjectNode) tn)
                // Now "bind" the object node with if the object node can be supported by the resolver
                .flatMap(objectNode -> Stream.of(BaseVehicleTypeResolver.cachedBaseVehicleTypeResolvers).filter(resolver -> resolver.matches(objectNode)))
                // If found, just get the detected vehicle class
                .map(BaseVehicleTypeResolver::getBaseVehicleClass)
                // Take the first resolver only
                .findFirst()
                // Or throw a JSON parsing exception
                .orElseThrow(() -> new JsonParseException(parser, "Cannot parse: " + treeNode));
        // Convert the JSON tree to the resolved class instance
        final ObjectMapper objectMapper = (ObjectMapper) parser.getCodec();
        return objectMapper.treeToValue(treeNode, baseVehicleClass);
    }

    // Known strategies here
    private enum BaseVehicleTypeResolver {

        CAR_RESOLVER {
            @Override
            protected Class<? extends BaseVehicle> getBaseVehicleClass() {
                return Car.class;
            }

            @Override
            protected boolean matches(final ObjectNode objectNode) {
                return !objectNode.has("numCylinders");
            }
        },

        MOTORCYCLE_RESOLVER {
            @Override
            protected Class<? extends BaseVehicle> getBaseVehicleClass() {
                return Motorcycle.class;
            }

            @Override
            protected boolean matches(final ObjectNode objectNode) {
                return objectNode.has("numCylinders");
            }
        };

        // Enum.values() returns array clones every time it's invoked
        private static final BaseVehicleTypeResolver[] cachedBaseVehicleTypeResolvers = BaseVehicleTypeResolver.values();

        protected abstract Class<? extends BaseVehicle> getBaseVehicleClass();

        protected abstract boolean matches(ObjectNode objectNode);

    }

}

正如您所看到的,这种方法或多或少是脆弱和复杂的,但它试图进行一些分析。现在,如何使用它:

final ObjectMapper mapper = new ObjectMapper();
Stream.of(
        "{\"orgId\":\"foo\",\"isInitialized\":true,\"vehicle\":{\"make\":\"foo\",\"model\":\"foo\"}}",
        "{\"orgId\":\"bar\",\"isInitialized\":true,\"vehicle\":{\"make\":\"bar\",\"model\":\"bar\",\"numCylinders\":4}}"
)
        .map(json -> {
            try {
                return mapper.readValue(json, Payload.class);
            } catch ( final IOException ex ) {
                throw new RuntimeException(ex);
            }
        })
        .map(Payload::getVehicle)
        .map(BaseVehicle::getClass)
        .forEach(System.out::println);

输出:

  

class q43138817.Car
  class q43138817.Motorcycle