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