Подтвердить что ты не робот

JAX-RS HATEOAS Использование свойств "Джерси", "Нежелательная ссылка" в JSON

Начиная с 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
}

Обратите внимание также, что вместо href он использует uri. Я посмотрел на реализацию Джерси объекта Link и нашел JerseyLink.

Я хочу использовать декларативную ссылку на Джерси вместо того, чтобы внедрять мою собственную реализацию. Я закончил использование аннотаций Джексона, чтобы игнорировать другие свойства JerseyLink.

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

Кто-нибудь использовал декларативную связь с Джерси и имел ожидаемый вывод JSON (например, href вместо uri, без дополнительных свойств Джерси) без необходимости использовать JsonIgnoreProperties или другие хаки?

Спасибо.

ИЗМЕНИТЬ

Я решил это, используя подход, который, я думаю, взломан, но хорошо работает для меня и не требует использования сложного адаптера.

Я понял, что могу фактически выставить другой объект вместо ссылки, введенной Джерси.

Я создал объект-оболочку с именем 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;
}

Итак, я использовал Джерси, чтобы ввести ссылку, но возвратил другой объект в методе getter в моем объекте представления. Это будет свойство, которое будет сериализовано для JSON, а не для объекта с вложенными ссылками, потому что я не создал для него метод getter.

4b9b3361

Ответ 1

Invesitigation

Среда: Джерси 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); 
}

Результаты с разными поставщиками (без дополнительных конфигураций)

джерси-медиа-Moxy

Зависимость

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

Результат (странный)

{
    "link": "[email protected]"
}

джерси-медиа-JSON-джексон

Зависимость

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

джексон-jaxrs-JSON-провайдер

Зависимость

<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> {...}

Итак, из Link мы сортируемся до JaxbLink

public static class JaxbLink {

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

джерси-медиа-Moxy

Кажется, это ошибка... См. ниже в решениях.

Другие

Остальные два зависят от jackson-module-jaxb-annotations для обработки маршаллинга с использованием аннотаций JAXB. jersey-media-json-jackson автоматически зарегистрирует требуемый JaxbAnnotationModule. Для jackson-jaxrs-json-provider использование JacksonJsonProvider не будет поддерживать аннотации JAXB (без конфликации), а использование JacksonJsonJaxbProvider даст нам поддержку аннотации JAXB.

Итак, если у нас есть поддержка аннотаций JAXB, мы получим marshalled до JaxbLink, что даст этот результат

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

Способы получения результата со всеми нежелательными свойствами: 1), используйте jackson-jaxrs-json-provider JacksonJsonProvider или 2), создайте ContextResolver для ObjectMapper, где не зарегистрировать JaxbAnnotationModule. Кажется, вы делаете один из них.


Решение

Выше все еще не получается, куда мы хотим добраться (т.е. нет params).

Для jersey-media-json-jackson и jackson-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();
    }
}

Затем создайте ContextResolver для ObjectMapper, где мы регистрируем сериализатор

@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 появляется Ошибка с отсутствующие сеттеры в классе JaxbLink, поэтому маршаллинг возвращается к вызову toString, что показано выше. Работа, предложенная здесь Garard Davidson, - это просто создать другой адаптер

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

UPDATE

Теперь, когда я думаю об этом, LinkAdapter также будет работать с поставщиком Jackson. Не нужно создавать Jackson Serializer/Deserializer. Модуль Jackson должен уже поддерживать аннотации JAXB в поле, если включен параметр JacksonFeature. Приведенные выше примеры показывают использование JAXB/JSON-провайдеров отдельно, но при включенном только JacksonFeature должна использоваться версия поставщика JAXB. На самом деле это может быть более предпочтительным решением. Не нужно создавать ContextResolvers для ObjectMapper:-D

Также можно объявить аннотацию на уровне пакета, как показано здесь

Ответ 2

Я хотел бы поделиться с моим решением для сериализации/десериализации объектов Link, используя с Jackson и аннотации микширования.

LinkMixin:

@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"]

Ответ 3

Используя предлагаемое решение для обновления, я все еще получал rel-часть внутри карты params.

Я внес некоторые изменения в класс адаптера Link

public class LinkAdapter
    extends XmlAdapter<LinkJaxb, Link> {

    public LinkAdapter() {
    }

    public Link unmarshal(LinkJaxb p1) {

        Link.Builder builder = Link.fromUri(p1.getUri());

        return builder.build();
    }

    public LinkJaxb marshal(Link p1) {

        return new LinkJaxb(p1.getUri(), p1.getRel());
    }
}

class LinkJaxb  {

    private URI uri;
    private String rel;


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

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

    public LinkJaxb(URI uri,String rel) {

        this.uri = uri;
        this.rel = rel;

    }

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

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


}

Теперь он содержит только два параметра, которые необходимы (rel, href) Я действительно не понял, когда мне нужно развязать ссылку Jaxb на ссылку. Для меня имело значение ссылка на jaxb link marshaling.

Ответ 4

Спасибо, @peeskillet и @Nir Sivan, за ваши ответы. Но мне удалось заставить его работать без с помощью LinkAdapter или ContextResolver<ObjectMapper>.

Я просто добавил переменную экземпляра настраиваемого типа Link (здесь ResourceLink, которая аналогична вашему LinkJaxb), для моего класса сущности как свойство @Transient, и после этого конфигурация Джексона автоматически включала этот атрибут в Response JSON

Ссылка на ресурс - класс

import javax.xml.bind.annotation.XmlAttribute;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;

@JsonInclude(Include.NON_EMPTY)
public class ResourceLink  {

private String uri;
private String rel;


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

public ResourceLink(String uri) {
    this(uri, null);
}

public ResourceLink(String uri,String rel) {

    this.uri = uri;
    this.rel = rel;

}

@XmlAttribute(name = "href")
public String getUri() {
    return uri;
}
@XmlAttribute(name = "rel")
public String getRel(){return rel;}

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


}

Класс сущности

package com.bts.entities;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Transient;

import com.bts.rs.root.util.ResourceLink;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;

@Entity
@Table(name="cities")
@JsonInclude(Include.NON_EMPTY)
public class City {
    @Id
    @Column(name="city_id")
    private Integer cityId;

    @Column(name="name")
    private String name;

    @Column(name="status")
    private int status;

    @Column(name="del_status")
    private int delStatus;

    @Transient
    List<ResourceLink> links = new ArrayList<ResourceLink>();

    // private 
    public City () {

    }

    public City (Integer id, String name) {
        this.cityId = id;
        this.name = name;
        this.status = 0;
        this.delStatus = 0;
    }

    // getters and setters for Non-transient properties

    // Below is the getter for lInks transient attribute
    public List<ResourceLink> getLinks(){
        return this.links;
    }

    // a method to add links - need not be a setter necessarily
    public void addResourceLink (String uri,String rel) {
        this.links.add(new ResourceLink(uri, rel));
    }   
}

Поставщик ресурсов Jersy

@GET
@Path("/karanchadha")
@Produces({MediaType.APPLICATION_JSON})
@Transactional
public Response savePayment() {
    City c1 = new City();
    c1.setCityId(11);
    c1.setName("Jamshedpur");
    c1.addResourceLink("http://www.test.com/home", "self");
    c1.addResourceLink("http://www.test.com/2", "parent");

    return Response.status(201).entity(c1).build();
}