有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

java使用Jackson在运行时将实体动态序列化为其ID或完整表示形式

我们正在使用JavaEE7(RESTEasy/Hibernate/Jackson)开发一个RESTful API

我们希望API在默认情况下使用其ID序列化所有子实体。我们这样做主要是为了保持与反序列化策略的一致性,在反序列化策略中,我们坚持接收ID

然而,我们也希望我们的用户能够选择通过自定义端点或查询参数(未确定)获得任何子实体的扩展视图。例如:

// http://localhost:8080/rest/operator/1
// =====================================
{
    "operatorId": 1,
    "organization": 34,
    "endUser": 23
}

// http://localhost:8080/rest/operator/1?expand=organization
// =====================================
{
    "operatorId": 1,
    "organization": {
        "organizationId": 34,
        "organizationName": "name"
    },
    "endUser": 23
}

// http://localhost:8080/rest/operator/1?expand=enduser
// =====================================
{
    "operatorId": 1,
    "organization": 34,
    "endUser": {
        "endUserId": 23,
        "endUserName": "other name"
    }
}

// http://localhost:8080/rest/operator/1?expand=organization,enduser
// =====================================
{
    "operatorId": 1,
    "organization": {
        "organizationId": 34,
        "organizationName": "name"
    },
    "endUser": {
        "endUserId": 23,
        "endUserName": "other name"
    }
}

是否有一种方法可以动态更改Jackson的行为,以确定指定的AbstractEntity字段是以完整形式序列化还是作为其ID序列化?怎么做呢


其他信息

我们知道使用子实体的ID序列化子实体的几种方法,包括:

public class Operator extends AbstractEntity {
    ...
    @JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="organizationId")
    @JsonIdentityReference(alwaysAsId=true)
    public getOrganization() { ... }
    ...
}

public class Operator extends AbstractEntity {
    ...
    @JsonSerialize(using=AbstractEntityIdSerializer.class)
    public getOrganization() { ... }
    ...
}

其中AbstractEntityIdSerializer使用实体的ID序列化实体

问题是,我们不知道用户如何覆盖默认行为并恢复到标准的Jackson对象序列化。理想情况下,他们还可以选择以完整形式序列化哪些子属性

如果可能的话,可以在运行时为任何属性动态切换@jsonidentialreference的alwaysAsId参数,或者对ObjectMapper/ObjectWriter进行等效更改


更新:正在工作(?)解决方案 我们还没有机会完全测试这一点,但我一直在研究一个利用重写Jackson的AnnotationIntroSector类的解决方案。它似乎在按预期工作

public class CustomAnnotationIntrospector extends JacksonAnnotationIntrospector {
    private final Set<String> expandFieldNames_;

    public CustomAnnotationIntrospector(Set<String> expandFieldNames) {
        expandFieldNames_ = expandFieldNames;
    }

    @Override
    public ObjectIdInfo findObjectReferenceInfo(Annotated ann, ObjectIdInfo objectIdInfo) {
        JsonIdentityReference ref = _findAnnotation(ann, JsonIdentityReference.class);
        if (ref != null) {

            for (String expandFieldName : expandFieldNames_) {
                String expandFieldGetterName = "get" + expandFieldName;
                String propertyName = ann.getName();

                boolean fieldNameMatches = expandFieldName.equalsIgnoreCase(propertyName);
                boolean fieldGetterNameMatches = expandFieldGetterName.equalsIgnoreCase(propertyName);

                if (fieldNameMatches || fieldGetterNameMatches) {
                    return objectIdInfo.withAlwaysAsId(false);
                }
            }

            objectIdInfo = objectIdInfo.withAlwaysAsId(ref.alwaysAsId());
        }

        return objectIdInfo;
    }
}

在序列化时,我们复制ObjectMapper(这样AnnotationIntrospector会再次运行),并按如下方式应用CustomAnnotationIntrospector:

@Context
private HttpRequest httpRequest_;

@Override
writeTo(...) {
    // Get our application's ObjectMapper.
    ContextResolver<ObjectMapper> objectMapperResolver = provider_.getContextResolver(ObjectMapper.class,
                                                                                      MediaType.WILDCARD_TYPE);
    ObjectMapper objectMapper = objectMapperResolver.getContext(Object.class);

    // Get Set of fields to be expanded (pre-parsed).
    Set<String> fieldNames = (Set<String>)httpRequest_.getAttribute("ExpandFields");

    if (!fieldNames.isEmpty()) {
        // Pass expand fields to AnnotationIntrospector.
        AnnotationIntrospector expansionAnnotationIntrospector = new CustomAnnotationIntrospector(fieldNames);

        // Replace ObjectMapper with copy of ObjectMapper and apply custom AnnotationIntrospector.
        objectMapper = objectMapper.copy();
        objectMapper.setAnnotationIntrospector(expansionAnnotationIntrospector);
    }

    ObjectWriter objectWriter = objectMapper.writer();
    objectWriter.writeValue(...);
}

这种方法有什么明显的缺陷吗?它似乎相对简单,而且是完全动态的


共 (2) 个答案

  1. # 1 楼答案

    答案是Jackson's mixin feature

    创建一个简单的Java类,它的方法签名与实体的指定方法完全相同。用修改后的值对该方法进行注释。方法的主体无关紧要(不会被调用):

    public class OperatorExpanded {
        ...
        @JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="organizationId")
        @JsonIdentityReference(alwaysAsId=false)
        public Organization getOrganization() { return null; }
        ...
    }
    

    您可以使用Jackson的模块系统将mixin绑定到要序列化的实体:这可以在运行时决定

    ObjectMapper mapper = new ObjectMapper();
    if ("organization".equals(request.getParameter("exapnd")) {
        SimpleModule simpleModule = new SimpleModule();
        simpleModule.setMixInAnnotation(Operator.class, OperatorExpanded.class);
        mapper.registerModule(simpleModule);
    }
    

    现在,映射器将从mixin中获取注释,但调用实体的方法

  2. # 2 楼答案

    如果您正在寻找一个需要扩展到所有资源的通用解决方案,您可以尝试以下方法。我试着用Jersey和Jackson来解决以下问题。它还应该与RestEasy一起使用

    基本上,您需要编写一个定制的jackson提供程序,为expand字段设置一个特殊的序列化程序。此外,还需要将扩展字段传递给序列化程序,以便决定如何对扩展字段进行序列化

    @Singleton
    public class ExpandFieldJacksonProvider extends JacksonJaxbJsonProvider {
    
    @Inject
    private Provider<ContainerRequestContext> provider;
    
    @Override
    protected JsonEndpointConfig _configForWriting(final ObjectMapper mapper, final Annotation[] annotations, final Class<?> defaultView) {
        final AnnotationIntrospector customIntrospector = mapper.getSerializationConfig().getAnnotationIntrospector();
        // Set the custom (user) introspector to be the primary one.
        final ObjectMapper filteringMapper = mapper.setAnnotationIntrospector(AnnotationIntrospector.pair(customIntrospector, new JacksonAnnotationIntrospector() {
            @Override
            public Object findSerializer(Annotated a) {
                // All expand fields should be annotated with '@ExpandField'.
                ExpandField expField = a.getAnnotation(ExpandField.class);
                if (expField != null) {
                    // Use a custom serializer for expand field
                    return new ExpandSerializer(expField.fieldName(), expField.idProperty());
                }
                return super.findSerializer(a);
            }
        }));
    
        return super._configForWriting(filteringMapper, annotations, defaultView);
    }
    
    @Override
    public void writeTo(final Object value, final Class<?> type, final Type genericType, final Annotation[] annotations, final MediaType mediaType, final MultivaluedMap<String, Object> httpHeaders,
            final OutputStream entityStream) throws IOException {
    
        // Set the expand fields to java's ThreadLocal so that it can be accessed in 'ExpandSerializer' class.
        ExpandFieldThreadLocal.set(provider.get().getUriInfo().getQueryParameters().get("expand"));
        super.writeTo(value, type, genericType, annotations, mediaType, httpHeaders, entityStream);
        // Once the serialization is done, clear ThreadLocal
        ExpandFieldThreadLocal.remove();
    }
    

    ExpandField。爪哇

    @Retention(RUNTIME)
    public @interface ExpandField {
        // name of expand field
        String fieldName();
        // name of Id property in expand field. For eg: oraganisationId
        String idProperty();
    }
    

    这是本地的。爪哇

    public class ExpandFieldThreadLocal {
    
        private static final ThreadLocal<List<String>> _threadLocal = new ThreadLocal<>();
    
        public static List<String> get() {
            return _threadLocal.get();
        }
    
        public static void set(List<String> expandFields) {
            _threadLocal.set(expandFields);
        }
    
        public static void remove() {
            _threadLocal.remove();
        }
    
    
    }
    

    ExpandFieldSerializer。爪哇

        public static class ExpandSerializer extends JsonSerializer<Object> {
        private String fieldName;
        private String idProperty;
    
        public ExpandSerializer(String fieldName,String idProperty) {
            this.fieldName = fieldName;
            this.idProperty = idProperty;
        }
    
        @Override
        public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException {
            // Get expand fields in current request which is set in custom jackson provider.
            List<String> expandFields = ExpandFieldThreadLocal.get();
            if (expandFields == null || !expandFields.contains(fieldName)) {
                try {
                    // If 'expand' is not present in query param OR if the 'expand' field does not contain this field, write only id.
                    serializers.defaultSerializeValue(value.getClass().getMethod("get"+StringUtils.capitalize(idProperty)).invoke(value),gen);
                } catch (Exception e) {
                    //Handle Exception here
                } 
            } else {
                serializers.defaultSerializeValue(value, gen);
            }
    
        }
    
    }
    

    接线员。爪哇

    public class Operator extends AbstractEntity {
    ...
    @ExpandField(fieldName = "organization",idProperty="organizationId")
    private organization;
    ...
    }
    

    最后一步是注册新的ExpandFieldJacksonProvider。在Jersey,我们通过一个javax.ws.rs.core.Application实例注册它,如下所示。我希望RestEasy中也有类似的东西。默认情况下,大多数JAX-RS库倾向于通过自动发现加载默认JacksonJaxbJsonProvider。您必须确保已禁用Jackson的自动发现功能,并且已注册新的ExpandFieldJacksonProvider

    public class JaxRsApplication extends Application{
    
        @Override
        public Set<Class<?>> getClasses() {
            Set<Class<?>> clazzes=new HashSet<>();
            clazzes.add(ExpandFieldJacksonProvider.class);
            return clazzes;
        }
    
    }