发送响应时未使用 MessageBodyWriter
MessageBodyWriter not being used when sending responses
我使用 JacksonAnnotationIntrospector 设置了自定义注释,以根据 API 版本吐出正确的 属性 名称。有一个助手 class 再次根据 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
}
}
我知道映射器 return 是正确的 JSON 孤立的。我可以在构造响应时在 .entity() 中插入对 serializeEntity 的调用,但这会导致我们的测试出现问题,测试会检查响应中的实体是否为同一类型(例如,用户)。例如,如果他们找到单个对象的序列化版本或任何对象的序列化列表 <> 的字符串,他们就会崩溃。
如果我理解正确,在序列化指定对象时(我们使用的是 Dropwizard 和 Jersey),应该选择并使用带有 @Provider 注释的 MessageBodyWriter。
@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,因为文档说你可以使用 superclass 并且所有 subclasses 也将被匹配(假设你 return true in isWriteable() 函数,我就是这么做的)。我还尝试过使用 JSON 媒体类型指定 @Produces 并仅指定一个子 class,例如用户,而不是实体,但似乎没有任何效果。
我还尝试使用以下方式注册 MessageBodyWriter:
JerseyEnvironment jersey = env.jersey();
jersey.register(new IdentityEntityMessageBodyWriter());
但所做的只是破坏了我们拥有的几乎所有测试(500s、409s 等)。
我尝试根据 API 版本 state
更改的变量在响应 V2016 API 调用时从未设置为 lifecycleState
。我怎样才能让它正常工作?
很难从您的示例中找出问题所在。
我为您编写了一个最小示例,说明如何将其与 DW 集成。
首先要注意的是:
注释 MessageBodyWriter 对您没有帮助。当你有一个处理你的 类 的注入框架时,这会起作用。您可以使用注解自动将其注册到 Jersey,这就是该注解的作用。所以在 DW 中(除非你使用 Guicey 或类路径扫描等)这是行不通的,你必须手动完成。
首先,我的注解:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface VersioningProperties {
Property[] value();
@interface Property {
String version();
String value();
}
}
接下来,我的注释版本控制 :)
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;
}
}
这两个都是我借用的 post:
模特:
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
}
我正在使用 属性 "x" 来确定要使用哪个版本。其余与您的示例类似。
所以如果 "x" 是 "A",属性 将被命名为 "test1",否则如果 "B" 将被命名为 "test2"。
然后应用程序启动如下:
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");
}
}
请注意,我正在向 jersey 环境注册 MessageBodyWriter。我也在使用 DW 已经提供给我们的 ObjectMapper。这个 OM 有一些已经设置好并且有用的配置(例如 DateTime 处理和类似的功能)。
我的资源:
@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;
}
}
我知道将正文传递到 GET 资源中是不好的做法,但这只是为了让我可以切换 Person 属性 来演示正在发生的事情。
最后是我的 MessageBodyWriter:
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;
}
}
}
现在,调用我的 API,我得到:
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$
请注意,属性 名称已根据我传递给我的 curl 命令的正文正确切换。
所以,我不是 100% 确定您的测试失败的原因。
我相信 OM 涉及某种缓存,您不能在其中来回切换 AnnotationIntrospector(这只是一个假设,因为我不能只重置我的 OM)。无论如何,只有 2 个不同的可能是更好的选择。
我希望这可以帮助您解决问题。
如果您正在使用测试,则需要确保在单元测试中也正确注册了所有内容。
设置几个断点、sysout 和其他有用的小朋友,他们会为您指明正确的方向。
干杯!
阿图尔
我使用 JacksonAnnotationIntrospector 设置了自定义注释,以根据 API 版本吐出正确的 属性 名称。有一个助手 class 再次根据 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
}
}
我知道映射器 return 是正确的 JSON 孤立的。我可以在构造响应时在 .entity() 中插入对 serializeEntity 的调用,但这会导致我们的测试出现问题,测试会检查响应中的实体是否为同一类型(例如,用户)。例如,如果他们找到单个对象的序列化版本或任何对象的序列化列表 <> 的字符串,他们就会崩溃。
如果我理解正确,在序列化指定对象时(我们使用的是 Dropwizard 和 Jersey),应该选择并使用带有 @Provider 注释的 MessageBodyWriter。
@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,因为文档说你可以使用 superclass 并且所有 subclasses 也将被匹配(假设你 return true in isWriteable() 函数,我就是这么做的)。我还尝试过使用 JSON 媒体类型指定 @Produces 并仅指定一个子 class,例如用户,而不是实体,但似乎没有任何效果。
我还尝试使用以下方式注册 MessageBodyWriter:
JerseyEnvironment jersey = env.jersey();
jersey.register(new IdentityEntityMessageBodyWriter());
但所做的只是破坏了我们拥有的几乎所有测试(500s、409s 等)。
我尝试根据 API 版本 state
更改的变量在响应 V2016 API 调用时从未设置为 lifecycleState
。我怎样才能让它正常工作?
很难从您的示例中找出问题所在。
我为您编写了一个最小示例,说明如何将其与 DW 集成。
首先要注意的是:
注释 MessageBodyWriter 对您没有帮助。当你有一个处理你的 类 的注入框架时,这会起作用。您可以使用注解自动将其注册到 Jersey,这就是该注解的作用。所以在 DW 中(除非你使用 Guicey 或类路径扫描等)这是行不通的,你必须手动完成。
首先,我的注解:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface VersioningProperties {
Property[] value();
@interface Property {
String version();
String value();
}
}
接下来,我的注释版本控制 :)
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;
}
}
这两个都是我借用的 post:
模特:
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
}
我正在使用 属性 "x" 来确定要使用哪个版本。其余与您的示例类似。
所以如果 "x" 是 "A",属性 将被命名为 "test1",否则如果 "B" 将被命名为 "test2"。
然后应用程序启动如下:
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");
}
}
请注意,我正在向 jersey 环境注册 MessageBodyWriter。我也在使用 DW 已经提供给我们的 ObjectMapper。这个 OM 有一些已经设置好并且有用的配置(例如 DateTime 处理和类似的功能)。
我的资源:
@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;
}
}
我知道将正文传递到 GET 资源中是不好的做法,但这只是为了让我可以切换 Person 属性 来演示正在发生的事情。
最后是我的 MessageBodyWriter:
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;
}
}
}
现在,调用我的 API,我得到:
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$
请注意,属性 名称已根据我传递给我的 curl 命令的正文正确切换。
所以,我不是 100% 确定您的测试失败的原因。
我相信 OM 涉及某种缓存,您不能在其中来回切换 AnnotationIntrospector(这只是一个假设,因为我不能只重置我的 OM)。无论如何,只有 2 个不同的可能是更好的选择。
我希望这可以帮助您解决问题。
如果您正在使用测试,则需要确保在单元测试中也正确注册了所有内容。
设置几个断点、sysout 和其他有用的小朋友,他们会为您指明正确的方向。
干杯!
阿图尔