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
}

Тогда в моем объекте представления у меня есть метод получения:

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

   return link;
}

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

5 ответов

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://stackru.com/questions/24968448");
        Link link = Link.fromUri(uri).rel("stackru").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": "javax.ws.rs.core.Link$JaxbLink@cce17d1b"
}

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

зависимость

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

Результат (близко, но что с params?)

{
    "link": {
        "params": {
            "rel": "stackru"
        },
        "href": "https://stackru.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://stackru.com/questions/24968448",
        "params": {
            "rel": "stackru"
        },
        "type": null,
        "uriBuilder": {
            "absolute": true
        },
        "rels": ["stackru"],
        "title": null,
        "rel": "stackru"
    }
}

resourceConfig.register(JacksonJaxbJsonProvider.class);

{
    "link": {
        "params": {
            "rel": "stackru"
        },
        "href": "https://stackru.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, мы получим JaxbLink, который даст этот результат

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

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


Решения

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

За jersey-media-json-jackson а также jackson-jaxrs-json-provider ...

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

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": "stackru",
        "href": "https://stackru.com/questions/24968448"
    }
}

С помощью jersey-media-moxy, похоже, есть ошибка с отсутствующими сеттерами в JaxbLink класс, поэтому сортировка возвращается к вызову toString, что показано выше. Обходной путь, предложенный здесь Гарардом Дэвидсоном, состоит в том, чтобы просто создать еще один адаптер.

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://stackru.com/questions/24968448",
        "rel": "stackru"
    }
}

ОБНОВИТЬ

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

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

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

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

Используя предложенное решение по обновлению, я все еще получал релевантную часть в карте параметров.

Я сделал некоторые изменения в классе адаптера 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-ссылку на Link. То, что имело значение для меня, было маршалингом Link to Jaxb link.

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

Я просто добавил переменную экземпляра пользовательского типа Link (здесь ResourceLink который аналогичен вашему LinkJaxb) к моему классу сущности как @Transient свойство и после этого конфигурация Джексона автоматически включила этот атрибут в ответ 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));
    }   
}

Джерси Ресурс Провайдер

@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();
}

Первые зависимости:

      <dependency> 
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-json-jackson</artifactId> 
    <version>2.34</version>
</dependency> 
<dependency>
    <groupId>org.glassfish.jersey.ext</groupId>
    <artifactId>jersey-declarative-linking</artifactId>
    <version>2.34</version>
</dependency>

Второй - класс конфигурации:

      package app.rest.config;

import javax.ws.rs.ApplicationPath;
import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJaxbJsonProvider;
import org.glassfish.jersey.linking.DeclarativeLinkingFeature;
import org.glassfish.jersey.server.ResourceConfig;


@ApplicationPath(value = "rest")
public class RestApplicationConfig extends ResourceConfig{

    public RestApplicationConfig() {
        register(JacksonJaxbJsonProvider.class);
        register(DeclarativeLinkingFeature.class);
        packages("app.rest.controllers");
    }


}

В-третьих - создать класс адаптера

      package app.rest.config;

import javax.json.Json;
import javax.json.JsonObject;
import javax.json.bind.adapter.JsonbAdapter;
import javax.ws.rs.core.Link;
import javax.ws.rs.core.UriBuilder;


public class CustomJsonAdapter implements JsonbAdapter<Link, JsonObject> {

    @Override
    public JsonObject adaptToJson(Link link) throws Exception {
        StringBuilder builder = new StringBuilder();
        builder.append("http://");
        builder.append(link.getUri().getHost());
        builder.append(":");
        builder.append(link.getUri().getPort());
        builder.append(link.getUri().getRawPath());
        return Json.createObjectBuilder().add("href", builder.toString()).add("rel", link.getRel()).build();
    }

    @Override
    public Link adaptFromJson(JsonObject json) throws Exception {
        Link link = Link.fromUriBuilder(UriBuilder.fromUri(json.getString("href"))).rel(json.getString("rel")).build();
        return link;
    }

}

Forth - зарегистрируйте JsonbConfig с пользовательским классом адаптера из класса Link в Json

      package app.rest.config;

import javax.json.bind.Jsonb;
import javax.json.bind.JsonbBuilder;
import javax.json.bind.JsonbConfig;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;

@Provider
public class AppJsonConfig implements ContextResolver<Jsonb>  {

    @Override
    public Jsonb getContext(Class<?> type) {
        JsonbConfig jsonbConfig = new JsonbConfig();
        jsonbConfig.withAdapters(new CustomJsonAdapter());
        return JsonbBuilder.create(jsonbConfig);
    }

}

Пятое : создайте модель с аннотацией Link и @InjectLink

      package app.rest.model;

import java.io.Serializable;
import java.util.Objects;
import javax.json.bind.annotation.JsonbTypeAdapter;
import javax.ws.rs.core.Link;
import javax.xml.bind.annotation.XmlRootElement;
import org.glassfish.jersey.linking.Binding;
import org.glassfish.jersey.linking.InjectLink;
import org.glassfish.jersey.linking.InjectLink.Style;
import app.rest.config.CustomJsonAdapter;
import app.rest.controllers.RestController;


@XmlRootElement
public class StudentResource implements Serializable{

    private static final long serialVersionUID = 1L;

    private Long id;

    private String name;

    private String surname;

    @InjectLink(resource = RestController.class, 
                style = Style.ABSOLUTE,
                rel = "self", 
                method = "getStudentById",
                bindings = @Binding(name = "id", value = "${instance.id}"))
    @JsonbTypeAdapter(value = CustomJsonAdapter.class)
    private Link link;

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getSurname() {
        return surname;
    }
    public void setSurname(String surname) {
        this.surname = surname;
    }

    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }

    public Link getLink() {
        return link;
    } 
    public void setLink(Link link) {
        this.link = link;
    }
    @Override
    public int hashCode() {
        return Objects.hash(name, surname);
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        StudentResource other = (StudentResource) obj;
        return Objects.equals(name, other.name) && Objects.equals(surname, other.surname);
    }
}

И в конце оставьте контроллер

      package app.rest.controllers;

import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.Link;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import app.rest.model.*;

@Path(value = "student")
public class RestController {

    @Context
    private UriInfo uriInfo;

    private final ArrayList<StudentResource> students = new ArrayList<>();

    public RestController() {
        StudentResource s1 = new StudentResource();
        s1.setId(1L);
        s1.setName("test1");
        s1.setSurname("surTest1");
        students.add(s1);
        StudentResource s2 = new StudentResource();
        s2.setId(2L);
        s2.setName("new_St");
        s2.setSurname("surNew");
        students.add(s2);
    
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Response  getAllStudents() throws URISyntaxException{
        Link link = Link.fromUriBuilder(uriInfo.getAbsolutePathBuilder()).rel("self").type("GET").build();
        GenericEntity<List<StudentResource>> studentsEntities = new GenericEntity<List<StudentResource>>(students) {};
        return Response.status(Response.Status.OK).entity(studentsEntities).links(link).build();
    }

    @GET
    @Path(value = "/{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public Response getStudentById(@PathParam(value = "id") Long id ) {
        Iterator<StudentResource> iterator = students.iterator();
        StudentResource studentById= null;
        while (iterator.hasNext()) {
            StudentResource next = iterator.next();
            if(next.getId().equals(id)) {
                studentById = next;
                break;
            }
        }
        if(null!=studentById) return Response.status(Response.Status.OK).entity(studentById).build();
    
        else return Response.status(Response.Status.NO_CONTENT).build();
    }   

}

Развернуто и протестировано на Payara 5.201

Производит : получить http: // localhost:8080 / sampleappee / rest / student / 1

      {"id":1,"link":{"href":"http://localhost:8080/sampleappee/rest/student/1","rel":"self"},"name":"test1","surname":"surTest1"}
Другие вопросы по тегам