JAX-RS HATEOAS 使用 JSON 中的泽西岛、不需要的链接属性调查更新

2022-09-02 01:39:13

从 Jersey 2.9 开始,就可以通过声明性链接为超媒体驱动的 REST API 创建链接关系。

此代码,例如:

@InjectLink(
    resource = ItemResource.class,
    style = Style.ABSOLUTE,
    bindings = @Binding(name = "id", value = "${instance.id}"),
    rel = "self"
)
@XmlJavaTypeAdapter(Link.JaxbAdapter.class)
@XmlElement(name="link")
Link self;

...从理论上讲,预计会产生这样的JSON:

"link" : {
    "rel" : "self",
    "href" : "http://localhost/api/resource/1"
}

但是,泽西岛生成了不同的JSON,其中包含许多我不需要的属性:

"link" : {
   "rel" : "self",
   "uri" : "http://localhost/api/resource/1",
   "type": null,
   "uriBuilder" : null
}

另请注意,它使用 而不是 。我查看了 Jersey 对该对象的实现,发现了 JerseyLinkhrefuriLink

我想使用泽西岛的声明性链接,而不是推出我自己的实现。我最终使用 Jackson 注释只是为了忽略其他属性。JerseyLink

@JsonIgnoreProperties({ "uriBuilder", "params", "type", "rels" })

是否有人使用与泽西岛的声明性链接,并且具有预期的JSON输出(例如,而不是,没有额外的泽西岛属性),而不必使用或其他黑客攻击?hrefuriJsonIgnoreProperties

谢谢。

编辑

我使用一种我认为是黑客的方法解决了这个问题,但对我来说效果很好,不需要使用复杂的适配器。

我意识到我实际上可以公开一个不同的对象,而不是泽西岛注入的链接。

我创建了一个名为 ResourceLink 的包装器对象:

public class ResourceLink {
  private String rel;
  private URI href;

  //getters and setters
}

然后在我的表示对象中,我有一个 getter 方法:

public ResourceLink getLink() {
   ResourceLink link = new ResourceLink();
   link.setRel(self.getRel());
   link.setHref(self.getUri());

   return link;
}

因此,我使用 Jersey 来注入链接,但在我的表示对象的 getter 方法中返回了不同的对象。这将是将序列化为 JSON 的属性,而不是注入的链接对象,因为我没有为其创建 getter 方法。


答案 1

调查

环境:泽西岛 2.13(所有提供程序版本也是 2.13)。

无论使用声明性链接还是编程式链接,序列化都不应有所不同。我选择了程序化,只是因为我可以:-)

测试类:

@XmlRootElement
public class TestClass {
    private javax.ws.rs.core.Link link;

    public void setLink(Link link) { this.link = link; }

    @XmlElement(name = "link")
    @XmlJavaTypeAdapter(Link.JaxbAdapter.class)
    public Link getLink() { return link; }
}

@Path("/links")
public class LinkResource {  
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Response getResponse() {
        URI uri = URI.create("https://stackoverflow.com/questions/24968448");
        Link link = Link.fromUri(uri).rel("stackoverflow").build();
        TestClass test = new TestClass();
        test.setLink(link);
        return Response.ok(test).build();
    }
}

@Test
public void testGetIt() {
    WebTarget baseTarget = target.path("links");
    String json = baseTarget.request().accept(
            MediaType.APPLICATION_JSON).get(String.class);
    System.out.println(json); 
}

使用不同提供程序的结果(无需额外配置)

平纹针织-介质-莫克西

屬地

<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-moxy</artifactId>
</dependency>

结果(奇怪)

{
    "link": "javax.ws.rs.core.Link$JaxbLink@cce17d1b"
}

jersey-media-json-jackson

屬地

<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-json-jackson</artifactId>
</dependency>

结果(关闭,但有什么?params

{
    "link": {
        "params": {
            "rel": "stackoverflow"
        },
        "href": "https://stackoverflow.com/questions/24968448"
    }
}

jackson-jaxrs-json-provider

屬地

<dependency>
    <groupId>com.fasterxml.jackson.jaxrs</groupId>
    <artifactId>jackson-jaxrs-json-provider</artifactId>
    <version>2.4.0</version>
</dependency>

结果(两个不同的结果,具有两个不同的 JSON 提供程序)

resourceConfig.register(JacksonJsonProvider.class);

{
    "link": {
        "uri": "https://stackoverflow.com/questions/24968448",
        "params": {
            "rel": "stackoverflow"
        },
        "type": null,
        "uriBuilder": {
            "absolute": true
        },
        "rels": ["stackoverflow"],
        "title": null,
        "rel": "stackoverflow"
    }
}

resourceConfig.register(JacksonJaxbJsonProvider.class);

{
    "link": {
        "params": {
            "rel": "stackoverflow"
        },
        "href": "https://stackoverflow.com/questions/24968448"
    }
}

结论

我们用 对字段进行注释。让我们看一下此适配器的一个片段@XmlJavaTypeAdapter(Link.JaxbAdapter.class)

public static class JaxbAdapter extends XmlAdapter<JaxbLink, Link> {...}

所以从 ,我们被编组到LinkJaxbLink

public static class JaxbLink {

    private URI uri;
    private Map<QName, Object> params;
    ...
}

平纹针织-介质-莫克西

似乎是一个错误...请参阅下面的解决方案。

其他

另外两个依赖于 jackson-module-jaxb-annotations 来处理使用 JAXB 注释的编组。 将自动注册所需的 .对于 ,使用将不支持 JAXB 注释(没有 confgiruation),而 using 将给予 JAXB 注释支持。jersey-media-json-jacksonJaxbAnnotationModulejackson-jaxrs-json-providerJacksonJsonProviderJacksonJsonJaxbProvider

因此,如果我们 JAXB 注释支持,我们将被编组到 ,这将给出这个结果JaxbLink

{
    "link": {
        "params": {
            "rel": "stackoverflow"
        },
        "href": "https://stackoverflow.com/questions/24968448"
    }
}

我们可以获得所有不需要的属性的结果的方法是1),使用's或2),创建一个我们不注册.你似乎正在做其中之一。jackson-jaxrs-json-providerJacksonJsonProviderContextResolverObjectMapperJaxbAnnotationModule


解决 方案

上述内容仍然没有让我们到达我们想要到达的地方(即没有)。params

对于 jersey-media-json-jacksonjackson-jaxrs-json-provider...

...使用Jackson,此时我唯一能想到的就是创建自定义序列化程序

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import javax.ws.rs.core.Link;

public class LinkSerializer extends JsonSerializer<Link>{

    @Override
    public void serialize(Link link, JsonGenerator jg, SerializerProvider sp) 
            throws IOException, JsonProcessingException {
        jg.writeStartObject();
        jg.writeStringField("rel", link.getRel());
        jg.writeStringField("href", link.getUri().toString());
        jg.writeEndObject();
    }
}

然后为 创建一个,在其中注册序列化程序ContextResolverObjectMapper

@Provider
public class ObjectMapperContextResolver 
                          implements ContextResolver<ObjectMapper> {

    private final ObjectMapper mapper;

    public ObjectMapperContextResolver() {
        mapper = new ObjectMapper();
        SimpleModule simpleModule = new SimpleModule();
        simpleModule.addSerializer(Link.class, new LinkSerializer());
        mapper.registerModule(simpleModule);
    }

    @Override
    public ObjectMapper getContext(Class<?> type) {
        return mapper;
    }
}

这就是结果

{
    "link": {
        "rel": "stackoverflow",
        "href": "https://stackoverflow.com/questions/24968448"
    }
}

使用 jersey-media-moxy 时,类中似乎有一个 Bug 缺少二传手,因此编组将恢复为调用 ,如上所示。正如Garard Davidson在这里所建议的那样,解决方法只是创建另一个适配器。JaxbLinktoString

import java.net.URI;
import java.util.HashMap;  
import java.util.Map;  

import javax.ws.rs.core.Link;  
import javax.xml.bind.annotation.XmlAnyAttribute;
import javax.xml.bind.annotation.XmlAttribute;

import javax.xml.bind.annotation.adapters.XmlAdapter;  
import javax.xml.namespace.QName;  

public class LinkAdapter  
    extends XmlAdapter<LinkJaxb, Link> {  

    public LinkAdapter() {  
    }  

    public Link unmarshal(LinkJaxb p1) {  

        Link.Builder builder = Link.fromUri(p1.getUri());  
        for (Map.Entry<QName, Object> entry : p1.getParams().entrySet()) {  
            builder.param(entry.getKey().getLocalPart(), entry.getValue().toString());  
        }  
        return builder.build();  
    }  

    public LinkJaxb marshal(Link p1) {  

        Map<QName, Object> params = new HashMap<>();  
        for (Map.Entry<String,String> entry : p1.getParams().entrySet()) {  
            params.put(new QName("", entry.getKey()), entry.getValue());  
        }  
        return new LinkJaxb(p1.getUri(), params);  
    }  
}  

class LinkJaxb  {  

    private URI uri;  
    private Map<QName, Object> params;  


    public LinkJaxb() {  
        this (null, null);  
    }  

    public LinkJaxb(URI uri) {  
        this(uri, null);  
    }  

    public LinkJaxb(URI uri, Map<QName, Object> map) {  

        this.uri = uri;  
        this.params = map!=null ? map : new HashMap<QName, Object>();  

    }  

    @XmlAttribute(name = "href")  
    public URI getUri() {   
        return uri;  
    }  

    @XmlAnyAttribute  
    public Map<QName, Object> getParams() {   
        return params;  
    }  

    public void setUri(URI uri) {  
        this.uri = uri;  
    }  

    public void setParams(Map<QName, Object> params) {  
        this.params = params;  
    }    
}

改用此适配器

@XmlElement(name = "link")
@XmlJavaTypeAdapter(LinkAdapter.class)
private Link link;

将给我们所需的输出

{
    "link": {
        "href": "https://stackoverflow.com/questions/24968448",
        "rel": "stackoverflow"
    }
}

更新

现在我想起来了,这也将与杰克逊提供商合作。无需创建 Jackson 序列化程序/反序列化程序。Jackson 模块应该已经支持开箱即用的 JAXB 注释,前提是启用了 JAXB 注释。上面的示例显示了单独使用 JAXB/JSON 提供程序,但假设只启用了 JAXB 提供程序,则应使用该提供程序的 JAXB 版本。这实际上可能是更可取的解决方案。无需为:-D创建LinkAdapterJacksonFeatureJacksonFeatureContextResolversObjectMapper

也可以在包级别声明注释,如此所示


答案 2

我想与我的解决方案分享,使用Jackson和混合注释来序列化/反序列化Link对象。

链接咪新:

@JsonAutoDetect(
        fieldVisibility = JsonAutoDetect.Visibility.NONE,
        getterVisibility = JsonAutoDetect.Visibility.NONE,
        isGetterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonDeserialize(using = LinkMixin.LinkDeserializer.class)
public abstract class LinkMixin extends Link {

    private static final String HREF = "href";

    @JsonProperty(HREF)
    @Override
    public abstract URI getUri();

    @JsonAnyGetter
    public abstract Map<String, String> getParams();

    public static class LinkDeserializer extends JsonDeserializer<Link> {

        @Override
        public Link deserialize(
                final JsonParser p,
                final DeserializationContext ctxt) throws IOException {
            final Map<String, String> params = p.readValueAs(
                    new TypeReference<Map<String, String>>() {});
            if (params == null) {
                return null;
            }
            final String uri = params.remove(HREF);
            if (uri == null) {
                return null;
            }
            final Builder builder = Link.fromUri(uri);
            params.forEach(builder::param);
            return builder.build();
        }
    }
}

例:

final ObjectMapper mapper = new ObjectMapper();
mapper.addMixIn(Link.class, LinkMixin.class);
final Link link = Link.fromUri("http://example.com")
        .rel("self")
        .title("xxx")
        .param("custom", "my")
        .build();
final String json = mapper
        .writerWithDefaultPrettyPrinter()
        .writeValueAsString(Collections.singleton(link));
System.out.println(json);
final List<Link> o = mapper.readValue(json, new TypeReference<List<Link>>() {});
System.out.println(o);

输出:

[ {
  "href" : "http://example.com",
  "rel" : "self",
  "title" : "xxx",
  "custom" : "my"
} ]
[<http://example.com>; rel="self"; title="xxx"; custom="my"]