我们正在使用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)
}
#``````````````````````````````````````
这种方法有任何明显的缺陷吗?这似乎相对简单,充满活力。
答案 0 :(得分:1)
您创建一个简单的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;
}
}