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

时间:2017-03-28 16:45:59

标签: java json serialization jackson resteasy

我们正在使用Java EE 7(RESTEasy / Hibernate / Jackson)开发RESTful API。

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

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

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

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

其他信息

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

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

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;
    }
}

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

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

如果可能的话,在运行时为任何属性动态切换@JsonIdentityReference的alwaysAsId参数,或者对ObjectMapper / ObjectWriter进行等效更改,那将是非常棒的。

更新:工作(?)解决方案 我们还没有机会对此进行全面测试,但我一直在研究一种利用覆盖Jackson的AnnotationIntrospector类的解决方案。它似乎按预期工作。

@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(...);
}

在序列化时,我们复制我们的ObjectMapper(以便再次运行AnnotationIntrospector)并应用CustomAnnotationIntrospector,如下所示:

#``````````````````````````````````````
a <- list("2016", "MALE", "25", "50")
b <- list("2017", "FEMALE", "5", "100")
c <- list("2017", "MALE", "15", "75")
d <- list("2016", "MALE", "10", "35")
e <- list("2017", "FEMALE","55", "20")

data <- rbind(a,b,c,d,e)

#``````````````````````````````````````
## UI

library(shiny)

if(interactive()){

  ui = fluidPage(

navbarPage("",

           #``````````````````````````````````````
           ## SHOES TAB

           tabPanel("SHOES", 

                    fluidRow(

                      column(2, "",
                             selectInput("shoes_year", "YEAR", choices = c("2017", "2016", "2015", "2014"))),


                      column(9, "SHOES"))),


           #``````````````````````````````````````
           ## HATS

           tabPanel("HATS",

                    fluidRow(

                      column(2, "",
                             selectInput("hats_year", "YEAR", choices = c("2017", "2016", "2015", "2014"))),

                      column(9, "HATS"))),

  #``````````````````````````````````````
  ## COATS

  tabPanel("COATS",
           fluidRow(
             column(2, "",
                    selectInput("coats_year", "YEAR", choices = c("2017",     "2016", "2015", "2014"))),

         column(9, "COATS")))

))

  #``````````````````````````````````````

  server = function(input, output, session) {

    observeEvent(input[["shoes_year"]],
                 {
                   updateSelectInput(session = session,
                                     inputId = "hats_year",
                                     selected = input[["shoes_year"]])
                 })

    observeEvent(input[["hats_year"]],
                 {
                   updateSelectInput(session = session,
                                     inputId = "shoes_year",
                                     selected = input[["hats_year"]])
                 })

  }

  #``````````````````````````````````````


  shinyApp(ui, server)
}

#``````````````````````````````````````

这种方法有任何明显的缺陷吗?这似乎相对简单,充满活力。

2 个答案:

答案 0 :(得分:1)

答案是Jackson's mixin feature

您创建一个简单的Java类,其具有与实体的anotated方法完全相同的方法签名。您使用修改后的值注释该方法。方法的主体是微不足道的(它不会被称为):

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中获取注释,但是调用实体的方法。

答案 1 :(得分:1)

如果您正在寻找需要扩展到所有资源的通用解决方案,您可以尝试以下方法。我尝试使用泽西和杰克逊的解决方案。它也适用于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.java

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

ExpandFieldThreadLocal.java

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.java

    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);
        }

    }

}

Operator.java

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

最后一步是注册新的ExpandFieldJacksonProvider。在泽西岛,我们通过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;
    }

}