发送响应时未使用MessageBodyWriter

时间:2016-08-03 21:45:35

标签: jersey jackson jax-rs dropwizard

我有一个自定义注释设置与JacksonAnnotationIntrospector根据API版本吐出正确的属性名称。根据API版本,还有一个辅助类,它会再次发出正确的ObjectMapper。

public class ObjectMapperFactory {

  private static final ObjectMapper objectMapper_V1 = new ObjectMapper().setAnnotationIntrospector(new VersioningPropertiesIntrospector(Entity.ApiVersion.V1));
  private static final ObjectMapper objectMapper_V2016 = new ObjectMapper().setAnnotationIntrospector(new VersioningPropertiesIntrospector(Entity.ApiVersion.V2016));

  public static ObjectMapper getObjectMapper(Entity.ApiVersion version) {
    switch (version) {
        case V1:
            return objectMapper_V1;

        case V2016:
            return objectMapper_V2016;

        case INVALID:
            return null;
    }

    return null;
  }
}

还有一个用于测试序列化的辅助函数

public static String serializeEntity(Entity.ApiVersion version, Object object) {
    try {
        return getObjectMapper(version).writeValueAsString(object);
    } catch (JsonProcessingException e) {
        log.error(e.toString());
    }

    return "Invalid API version.";
}

在这样的单元测试中:

@Test
public void testSerializeUserWithStateField() {
    User user = new User();
    user.setVersion(Entity.ApiVersion.V2016);
    user.setState(EntityState.CREATED.name());

    String userJson = serializeEntity(user.getVersion(), user);

    assertThat(userJson, equalTo("{\"lifecycleState\":\"CREATED\"}"));
}

现在,说我有这样的事情:

@GET
@Path("users/{userId}")
public Response getUser(@PrincipalContext Principal principal,
                    @AuthorizationRequestContext AuthorizationRequest authorizationRequest,
                    @PathParam("userId") String userId) {

    final String decodedId = Optional
        .ofNullable(RequestValidationHelper.decodeUrlEncodedOCID(userId))
        .filter(StringUtils::isNotEmpty)
        .orElseThrow(BadArgumentException::new);

    User user = userStore.getUser(decodedId)
        .orElseThrow(OperationNotAllowedException::new);

    log.debug("Successfully retrieved user '{}'", decodedId);
    return Response.status(Response.Status.OK)
            .header(HttpHeaders.ETAG, user.getEtag())
            .entity(user)
            .build();
}

用户扩展实体:

public abstract class Entity {

  private String id;
  private String userId;

  @JsonIgnore
  private String etag;

  @VersioningProperties({
        @VersioningProperties.Property(version = ApiVersion.V1, value = "state"),
        @VersioningProperties.Property(version = ApiVersion.V2016, value = "lifecycleState")
})
  private String state;

  @JsonIgnore
  private ApiVersion version = ApiVersion.INVALID;

  public enum ApiVersion {
    INVALID,
    V1,
    V2016
  }
}

我知道映射器会单独返回正确的JSON。我可以在构造响应时在.entity()中插入对serializeEntity的调用,但这会导致我们的测试出现问题,这会检查Response中的实体是否为同一类型(例如User)。如果他们找到单个对象的序列化版本或序列化列表的字符串<>无论什么物体,它们都会破碎。

如果我正确理解它,那么在序列化指定的对象时我们应该选择并使用带有@Provider注释的MessageBodyWriter(我们使用Dropwizard和Jersey)。

@Provider
public class EntityMessageBodyWriter implements MessageBodyWriter<Entity> {
  @Override
  public long getSize(Entity entity, Class<?> aClass, Type type, Annotation[] annotations, MediaType mediaType) {
    return 0;
  }

  @Override
  public boolean isWriteable(Class<?> aClass, Type type, Annotation[] annotations, MediaType mediaType) {
    return Entity.class.isAssignableFrom(aClass);
  }

  @Override
  public void writeTo(Entity entity, Class<?> aClass, Type type, Annotation[] annotations,
                    MediaType mediaType, MultivaluedMap<String, Object> multivaluedMap, OutputStream outputStream)
        throws IOException, WebApplicationException {
    outputStream.write(serializeEntity(entity.getVersion(), entity).getBytes());
  }
}

但事实并非如此。我没有为每个对象创建一个单独的MessageBodyWriter,因为文档说你可以使用超类,所有子类也将匹配(假设你在isWriteable()函数中返回true,我做了)。我还尝试过使用JSON媒体类型指定@Produces并指定一个子类,比如User,而不是Entity,但似乎没有任何工作。

我还尝试使用以下命令注册MessageBodyWriter:

JerseyEnvironment jersey = env.jersey();
jersey.register(new IdentityEntityMessageBodyWriter());

但所有这一切几乎打破了我们所有的测试(500s,409s等)。

根据API版本state尝试更改的变量,在对V2016 API调用的响应中永远不会设置为lifecycleState。我怎样才能让它正常工作?

1 个答案:

答案 0 :(得分:2)

从你的榜样中找出问题有点困难。

我为您编写了一个最小的示例,说明了如何将其与DW集成。

首先要注意的是:

注释MessageBodyWriter对您没有帮助。当您有一个处理类的注入框架时,这可以工作。您可以使用Annotation自动将其注册到Jersey,这是此注释的作用。所以在DW中(除非你使用Guicey或类路径扫描等),这不会起作用,你必须手动完成。

首先,我的注释:

public class VersioningPropertiesIntrospector extends JacksonAnnotationIntrospector {

    private static final long serialVersionUID = 1L;
    private String version;

    public VersioningPropertiesIntrospector(String version) {
        this.version = version;
    }

    @Override
    public PropertyName findNameForSerialization(Annotated a) {
         PropertyName propertyName = findNameFromVersioningProperties(a);
         if (propertyName != null) {
             return propertyName;
         }
        return super.findNameForSerialization(a);
    }

    @Override
    public PropertyName findNameForDeserialization(Annotated a) {
         PropertyName propertyName = findNameFromVersioningProperties(a);
         if (propertyName != null) {
             return propertyName;
         }
        return super.findNameForDeserialization(a);
    }

    private PropertyName findNameFromVersioningProperties(Annotated a) {
        VersioningProperties annotation = a.getAnnotation(VersioningProperties.class);
        if (annotation == null) {
            return null;
        }
        for (Property property : annotation.value()) {
            if (version.equals(property.version())) {
                return new PropertyName(property.value());
            }
        }
        return null;
    }

}

接下来,我的注释版本化东西:)

public class Person {

    @VersioningProperties ( {
        @VersioningProperties.Property(version="A", value="test1")
        ,@VersioningProperties.Property(version="B", value="test2")
    })
    public String name = UUID.randomUUID().toString();

    public String x = "A"; // or B
}

我从这篇文章中借用了这两个:Specifying different JSON property names according to API version with Jackson

模特:

public class Application extends io.dropwizard.Application<Configuration>{

    @Override
    public void run(Configuration configuration, Environment environment) throws Exception {

        environment.jersey().register(HelloResource.class);

        ObjectMapper aMapper = environment.getObjectMapper().copy().setAnnotationIntrospector(new VersioningPropertiesIntrospector("A"));
        ObjectMapper bMapper = environment.getObjectMapper().copy().setAnnotationIntrospector(new VersioningPropertiesIntrospector("B"));
        environment.jersey().register(new MyMessageBodyWriter(aMapper, bMapper));
    }

    public static void main(String[] args) throws Exception {
        new Application().run("server", "/home/artur/dev/repo/sandbox/src/main/resources/config/test.yaml");
    }
}

我正在使用该物业&#34; x&#34;确定要使用的版本。其余的与你的例子类似。

所以如果&#34; x&#34;是&#34; A&#34;,该属性命名为&#34; test1&#34;否则如果&#34; B&#34;它将被命名为&#34; test2&#34;。

应用程序然后以这样开始:

@Path("test")
public class HelloResource {

    @GET
    @Path("asd")
    @Produces(MediaType.APPLICATION_JSON)
    public Person p(String x) {
        Person p = new Person();
        p.x = x;
        return p;
    }
}

注意我正在使用泽西环境注册MessageBodyWriter。我也在使用DW已经提供给我们的ObjectMapper。此OM具有一些已设置且有用的配置(例如,DateTime处理和类似功能)。

我的资源:

public class MyMessageBodyWriter implements MessageBodyWriter<Person> {

    private ObjectMapper aMapper;
    private ObjectMapper bMapper;

    MyMessageBodyWriter(ObjectMapper aMapper, ObjectMapper bMapper) {
        this.aMapper = aMapper;
        this.bMapper = bMapper;
    }

    @Override
    public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
        return Person.class.isAssignableFrom(type);
    }

    @Override
    public long getSize(Person t, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
        return 0;
    }

    @Override
    public void writeTo(Person t, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
            MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream)
            throws IOException, WebApplicationException {

        switch(t.x) {
        case "A": aMapper.writeValue(entityStream, t);
        break;
        case "B" : bMapper.writeValue(entityStream, t);
        break;
        }
    }
}

我知道将一个主体传递给GET资源是不好的做法,但这只是我可以切换Person属性来演示正在发生的事情。

最后是我的MessageBodyWriter:

artur@pandaadb:~/tmp/test$ curl -v -XGET "localhost:9085/api/test/asd"  -d "A"
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 9085 (#0)
> GET /api/test/asd HTTP/1.1
> Host: localhost:9085
> User-Agent: curl/7.47.0
> Accept: */*
> Content-Length: 1
> Content-Type: application/x-www-form-urlencoded
> 
* upload completely sent off: 1 out of 1 bytes
< HTTP/1.1 200 OK
< Date: Tue, 09 Aug 2016 09:59:13 GMT
< Content-Type: application/json
< Vary: Accept-Encoding
< Content-Length: 56
< 
* Connection #0 to host localhost left intact
{"x":"A","test1":"adec4590-47af-4eeb-a15a-67a532c22b72"}artur@pandaadb:~/tmp/test$ 
artur@pandaadb:~/tmp/test$ 
artur@pandaadb:~/tmp/test$ curl -v -XGET "localhost:9085/api/test/asd"  -d "B"
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 9085 (#0)
> GET /api/test/asd HTTP/1.1
> Host: localhost:9085
> User-Agent: curl/7.47.0
> Accept: */*
> Content-Length: 1
> Content-Type: application/x-www-form-urlencoded
> 
* upload completely sent off: 1 out of 1 bytes
< HTTP/1.1 200 OK
< Date: Tue, 09 Aug 2016 09:59:17 GMT
< Content-Type: application/json
< Vary: Accept-Encoding
< Content-Length: 56
< 
* Connection #0 to host localhost left intact
{"x":"B","test2":"6c56650c-6c87-418f-8b1a-0750a8091c46"}artur@pandaadb:~/tmp/test$ 

现在,调用我的API,我得到:

{{1}}

请注意,属性名称已正确切换,具体取决于我传递给curl命令的正文。

所以,我不能100%确定你的测试失败的原因。

我相信OM有一些缓存,你不能来回切换AnnotationIntrospector(这只是一个假设,因为我不能重置我的OM)。无论如何,这可能是一个更好的选择,只有两个不同的。

我希望这可以帮助您解决问题。

如果您正在使用测试,则还需要确保在单元测试中正确注册了所有内容。

设置几个断点,sysout和其他有用的小朋友,他们会指出你正确的事情。

干杯!

阿图尔