for (Person person : company.getPersons()) {
resource.add(linkTo(methodOn(PersonController.class).view(person.getId()))
.withRel("persons"));
}
我想通过“people”rel返回一系列链接。如果我有多个人,那一切都没问题,但如果我只有一个人,它会返回一个元素,而我的客户端代码需要数组失败。
答案 0 :(得分:1)
在春天的hateoas 18中是不可能的。我们重载了内置的序列化程序来解决这个问题。这非常讨厌。
从技术上讲,客户端应将rel:{}解释为rel:[{}]为HAL兼容..但他们很少这样做..
你必须删除并覆盖内置的HATEOAS,我们这样做,但这有效地删除了所有其他转换器:
@Configuration
public class WebMVCConfig extends WebMvcConfigurerAdapter {
private static final String DELEGATING_REL_PROVIDER_BEAN_NAME = "_relProvider";
private static final String LINK_DISCOVERER_REGISTRY_BEAN_NAME = "_linkDiscovererRegistry";
private static final String HAL_OBJECT_MAPPER_BEAN_NAME = "_halObjectMapper";
public WebMVCConfig(){
}
@Autowired
private ListableBeanFactory beanFactory;
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
//Need to override some behaviour in the HAL Serializer...so let's do that
CurieProvider curieProvider = getCurieProvider(beanFactory);
RelProvider relProvider = beanFactory.getBean(DELEGATING_REL_PROVIDER_BEAN_NAME, RelProvider.class);
ObjectMapper halObjectMapper = beanFactory.getBean(HAL_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class);
halObjectMapper.registerModule(new MultiLinkAwareJackson2HalModule());
halObjectMapper.setHandlerInstantiator(new MultiLinkAwareJackson2HalModule.MultiLinkAwareHalHandlerInstantiator(relProvider, curieProvider));
MappingJackson2HttpMessageConverter halConverter = new TypeConstrainedMappingJackson2HttpMessageConverter(ResourceSupport.class);
halConverter.setSupportedMediaTypes(Arrays.asList(HAL_JSON));
halConverter.setObjectMapper(halObjectMapper);
converters.add(halConverter);
}
private static CurieProvider getCurieProvider(BeanFactory factory) {
try {
return factory.getBean(CurieProvider.class);
} catch (NoSuchBeanDefinitionException e) {
return null;
}
}
覆盖序列化程序真是太难看了。可能我们应该从头开始构建一个新的
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.ser.std.MapSerializer;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.google.common.collect.ImmutableSet;
import org.springframework.hateoas.hal.*;
import java.io.IOException;
import java.util.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Links;
import org.springframework.hateoas.RelProvider;
import org.springframework.hateoas.ResourceSupport;
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.SerializerProvider;
import javax.xml.bind.annotation.XmlElement;
/**
* Jackson 2 module implementation to render {@link org.springframework.hateoas.Link} and {@link org.springframework.hateoas.ResourceSupport} instances in HAL compatible JSON.
*
* Extends this class to make it possible for a relationship to be serialized as an array even if there is only 1 link
* This is done is in OptionalListJackson2Serializer::serialize method.
*
* Relationships to force as arrays are defined in relsToForceAsAnArray
*/
public class MultiLinkAwareJackson2HalModule extends Jackson2HalModule {
private static final long serialVersionUID = 7806951456457932384L;
private static final ImmutableSet<String> relsToForceAsAnArray = ImmutableSet.copyOf(Arrays.asList(
ContractConstants.REL_PROMOTION_TARGET,
ContractConstants.REL_PROFILE,
ContractConstants.REL_IMAGE_FLAG,
ContractConstants.REL_IMAGE,
ContractConstants.REL_IMAGE_PRIMARY,
ContractConstants.REL_IMAGE_SECONDARY,
ContractConstants.REL_IMAGE_MENU,
ContractConstants.REL_ITEM
));
private static abstract class MultiLinkAwareResourceSupportMixin extends ResourceSupport {
@Override
@XmlElement(name = "link")
@JsonProperty("_links")
//here's the only diff from org.springframework.hateoas.hal.ResourceSupportMixin
//we use a different HalLinkListSerializer
@JsonSerialize(include = JsonSerialize.Inclusion.NON_EMPTY, using = MultiLinkAwareHalLinkListSerializer.class)
@JsonDeserialize(using = MultiLinkAwareJackson2HalModule.HalLinkListDeserializer.class)
public abstract List<Link> getLinks();
}
public MultiLinkAwareJackson2HalModule() {
super();
//NOTE: super calls setMixInAnnotation(Link.class, LinkMixin.class);
//you must not override this as this is how Spring-HATEOAS determines if a
//Hal converter has been registered for not.
//If it determines a Hal converter has not been registered, it will register it's own
//that will override this one
//Use customized ResourceSupportMixin to use our LinkListSerializer
setMixInAnnotation(ResourceSupport.class, MultiLinkAwareResourceSupportMixin.class);
}
public static class MultiLinkAwareHalLinkListSerializer extends Jackson2HalModule.HalLinkListSerializer {
private final BeanProperty property;
private final CurieProvider curieProvider;
private final Set<String> relsAsMultilink;
public MultiLinkAwareHalLinkListSerializer(BeanProperty property, CurieProvider curieProvider, Set<String> relsAsMultilink) {
super(property, curieProvider);
this.property = property;
this.curieProvider = curieProvider;
this.relsAsMultilink = relsAsMultilink;
}
@Override
public void serialize(List<Link> value, JsonGenerator jgen, SerializerProvider provider) throws IOException,
JsonGenerationException {
// sort links according to their relation
Map<String, List<Object>> sortedLinks = new LinkedHashMap<String, List<Object>>();
List<Link> links = new ArrayList<Link>();
boolean prefixingRequired = curieProvider != null;
boolean curiedLinkPresent = false;
for (Link link : value) {
String rel = prefixingRequired ? curieProvider.getNamespacedRelFrom(link) : link.getRel();
if (!link.getRel().equals(rel)) {
curiedLinkPresent = true;
}
if (sortedLinks.get(rel) == null) {
sortedLinks.put(rel, new ArrayList<Object>());
}
links.add(link);
sortedLinks.get(rel).add(link);
}
if (prefixingRequired && curiedLinkPresent) {
ArrayList<Object> curies = new ArrayList<Object>();
curies.add(curieProvider.getCurieInformation(new Links(links)));
sortedLinks.put("curies", curies);
}
TypeFactory typeFactory = provider.getConfig().getTypeFactory();
JavaType keyType = typeFactory.uncheckedSimpleType(String.class);
JavaType valueType = typeFactory.constructCollectionType(ArrayList.class, Object.class);
JavaType mapType = typeFactory.constructMapType(HashMap.class, keyType, valueType);
//CHANGE HERE: only thing we are changing ins the List Serializer
//shame there's not a better way to override this very specific behaviour
//without copy pasta the whole class
MapSerializer serializer = MapSerializer.construct(new String[] {}, mapType, true, null,
provider.findKeySerializer(keyType, null), new MultiLinkAwareOptionalListJackson2Serializer(property, relsAsMultilink), null);
serializer.serialize(sortedLinks, jgen, provider);
}
public MultiLinkAwareHalLinkListSerializer withForcedRels(String[] relationships) {
ImmutableSet<String> relsToForce = ImmutableSet.<String>builder().addAll(this.relsAsMultilink).add(relationships).build();
return new MultiLinkAwareHalLinkListSerializer(this.property, this.curieProvider, relsToForce);
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider provider, BeanProperty property)
throws JsonMappingException {
return new MultiLinkAwareHalLinkListSerializer(property, curieProvider, this.relsAsMultilink);
}
}
public static class MultiLinkAwareOptionalListJackson2Serializer extends Jackson2HalModule.OptionalListJackson2Serializer {
private final BeanProperty property;
private final Map<Class<?>, JsonSerializer<Object>> serializers;
private final Set<String> relsAsMultilink;
public MultiLinkAwareOptionalListJackson2Serializer(BeanProperty property, Set<String> relsAsMultilink) {
super(property);
this.property = property;
this.serializers = new HashMap<Class<?>, JsonSerializer<Object>>();
this.relsAsMultilink = relsAsMultilink;
}
@Override
public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException,
JsonGenerationException {
List<?> list = (List<?>) value;
if (list.isEmpty()) {
return;
}
if(list.get(0) instanceof Link) {
Link link = (Link) list.get(0);
String rel = link.getRel();
if (list.size() > 1 || relsAsMultilink.contains(rel)) {
jgen.writeStartArray();
serializeContents(list.iterator(), jgen, provider);
jgen.writeEndArray();
} else {
serializeContents(list.iterator(), jgen, provider);
}
}
}
private void serializeContents(Iterator<?> value, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonGenerationException {
while (value.hasNext()) {
Object elem = value.next();
if (elem == null) {
provider.defaultSerializeNull(jgen);
} else {
getOrLookupSerializerFor(elem.getClass(), provider).serialize(elem, jgen, provider);
}
}
}
private JsonSerializer<Object> getOrLookupSerializerFor(Class<?> type, SerializerProvider provider)
throws JsonMappingException {
JsonSerializer<Object> serializer = serializers.get(type);
if (serializer == null) {
serializer = provider.findValueSerializer(type, property);
serializers.put(type, serializer);
}
return serializer;
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider provider, BeanProperty property)
throws JsonMappingException {
return new MultiLinkAwareOptionalListJackson2Serializer(property, relsAsMultilink);
}
}
public static class MultiLinkAwareHalHandlerInstantiator extends Jackson2HalModule.HalHandlerInstantiator {
private final MultiLinkAwareHalLinkListSerializer linkListSerializer;
public MultiLinkAwareHalHandlerInstantiator(RelProvider resolver, CurieProvider curieProvider) {
super(resolver, curieProvider, true);
this.linkListSerializer = new MultiLinkAwareHalLinkListSerializer(null, curieProvider, relsToForceAsAnArray);
}
@Override
public JsonSerializer<?> serializerInstance(SerializationConfig config, Annotated annotated, Class<?> serClass) {
if(serClass.equals(MultiLinkAwareHalLinkListSerializer.class)){
if (annotated.hasAnnotation(ForceMultiLink.class)) {
return this.linkListSerializer.withForcedRels(annotated.getAnnotation(ForceMultiLink.class).value());
} else {
return this.linkListSerializer;
}
} else {
return super.serializerInstance(config, annotated, serClass);
}
}
}
}
ForceMultiLink的东西是另外一件事我们最终需要在某些资源类上需要多个rel而在其他资源类上它不需要...所以它看起来像这样:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ForceMultiLink {
String[] value();
}
您可以使用它来注释资源类中的getLinks()方法
答案 1 :(得分:1)
我有一个针对此问题的解决方法,与Chris&#39;回答。主要区别在于我没有扩展Jackson2HalModule
,而是创建了一个新的handler-instantiator并将其设置为我自己创建的Jackson2HalModule
的新实例的handler-instantiator。我希望Spring HATEOAS最终能够原生支持这个功能;我有pull request试图这样做。以下是我实施解决方法的方法:
第1步:创建mixin类:
public abstract class HalLinkListMixin {
@JsonProperty("_links") @JsonSerialize(using = HalLinkListSerializer.class)
public abstract List<Link> getLinks();
}
此mixin类将HalLinkListSerializer
(稍后显示)序列化程序与links
属性相关联。
第2步:创建一个包含rels的容器类,其链接表示应始终是链接对象的数组:
public class HalMultipleLinkRels {
private final Set<String> rels;
public HalMultipleLinkRels(String... rels) {
this.rels = new HashSet<String>(Arrays.asList(rels));
}
public Set<String> getRels() {
return Collections.unmodifiableSet(rels);
}
}
第3步:创建我们的新序列化程序,它将覆盖Spring HATEOAS的链接列表序列化程序:
public class HalLinkListSerializer extends ContainerSerializer<List<Link>> implements ContextualSerializer {
private final BeanProperty property;
private CurieProvider curieProvider;
private HalMultipleLinkRels halMultipleLinkRels;
public HalLinkListSerializer() {
this(null, null, new HalMultipleLinkRels());
}
public HalLinkListSerializer(CurieProvider curieProvider, HalMultipleLinkRels halMultipleLinkRels) {
this(null, curieProvider, halMultipleLinkRels);
}
public HalLinkListSerializer(BeanProperty property, CurieProvider curieProvider, HalMultipleLinkRels halMultipleLinkRels) {
super(List.class, false);
this.property = property;
this.curieProvider = curieProvider;
this.halMultipleLinkRels = halMultipleLinkRels;
}
@Override
public void serialize(List<Link> value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
// sort links according to their relation
Map<String, List<Object>> sortedLinks = new LinkedHashMap<>();
List<Link> links = new ArrayList<>();
boolean prefixingRequired = curieProvider != null;
boolean curiedLinkPresent = false;
for (Link link : value) {
String rel = prefixingRequired ? curieProvider.getNamespacedRelFrom(link) : link.getRel();
if (!link.getRel().equals(rel)) {
curiedLinkPresent = true;
}
if (sortedLinks.get(rel) == null) {
sortedLinks.put(rel, new ArrayList<>());
}
links.add(link);
sortedLinks.get(rel).add(link);
}
if (prefixingRequired && curiedLinkPresent) {
ArrayList<Object> curies = new ArrayList<>();
curies.add(curieProvider.getCurieInformation(new Links(links)));
sortedLinks.put("curies", curies);
}
TypeFactory typeFactory = provider.getConfig().getTypeFactory();
JavaType keyType = typeFactory.uncheckedSimpleType(String.class);
JavaType valueType = typeFactory.constructCollectionType(ArrayList.class, Object.class);
JavaType mapType = typeFactory.constructMapType(HashMap.class, keyType, valueType);
MapSerializer serializer = MapSerializer.construct(new String[]{}, mapType, true, null,
provider.findKeySerializer(keyType, null), new ListJackson2Serializer(property, halMultipleLinkRels), null);
serializer.serialize(sortedLinks, jgen, provider);
}
@Override
public JavaType getContentType() {
return null;
}
@Override
public JsonSerializer<?> getContentSerializer() {
return null;
}
@Override
public boolean hasSingleElement(List<Link> value) {
return value.size() == 1;
}
@Override
protected ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) {
return null;
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
return new HalLinkListSerializer(property, curieProvider, halMultipleLinkRels);
}
private static class ListJackson2Serializer extends ContainerSerializer<Object> implements ContextualSerializer {
private final BeanProperty property;
private final Map<Class<?>, JsonSerializer<Object>> serializers = new HashMap<>();
private final HalMultipleLinkRels halMultipleLinkRels;
public ListJackson2Serializer() {
this(null, null);
}
public ListJackson2Serializer(BeanProperty property, HalMultipleLinkRels halMultipleLinkRels) {
super(List.class, false);
this.property = property;
this.halMultipleLinkRels = halMultipleLinkRels;
}
@Override
public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
List<?> list = (List<?>) value;
if (list.isEmpty()) {
return;
}
if (list.size() == 1) {
Object element = list.get(0);
if (element instanceof Link) {
Link link = (Link) element;
if (halMultipleLinkRels.getRels().contains(link.getRel())) {
jgen.writeStartArray();
serializeContents(list.iterator(), jgen, provider);
jgen.writeEndArray();
return;
}
}
serializeContents(list.iterator(), jgen, provider);
return;
}
jgen.writeStartArray();
serializeContents(list.iterator(), jgen, provider);
jgen.writeEndArray();
}
@Override
public JavaType getContentType() {
return null;
}
@Override
public JsonSerializer<?> getContentSerializer() {
return null;
}
@Override
public boolean hasSingleElement(Object value) {
return false;
}
@Override
protected ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) {
throw new UnsupportedOperationException("not implemented");
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
return new ListJackson2Serializer(property, halMultipleLinkRels);
}
private void serializeContents(Iterator<?> value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
while (value.hasNext()) {
Object elem = value.next();
if (elem == null) {
provider.defaultSerializeNull(jgen);
} else {
getOrLookupSerializerFor(elem.getClass(), provider).serialize(elem, jgen, provider);
}
}
}
private JsonSerializer<Object> getOrLookupSerializerFor(Class<?> type, SerializerProvider provider) throws JsonMappingException {
JsonSerializer<Object> serializer = serializers.get(type);
if (serializer == null) {
serializer = provider.findValueSerializer(type, property);
serializers.put(type, serializer);
}
return serializer;
}
}
}
这个类不幸地重复逻辑,但它并不太糟糕。关键的区别在于,我没有使用OptionalListJackson2Serializer
,而是使用ListJackson2Serializer
,这会强制将rel的链接表示为数组,如果rel存在于rel的容器中覆盖(HalMultipleLinkRels
):
第4步:创建自定义处理程序实例化程序:
public class HalHandlerInstantiator extends HandlerInstantiator {
private final Jackson2HalModule.HalHandlerInstantiator halHandlerInstantiator;
private final Map<Class<?>, JsonSerializer<?>> serializerMap = new HashMap<>();
public HalHandlerInstantiator(RelProvider relProvider, CurieProvider curieProvider, HalMultipleLinkRels halMultipleLinkRels) {
this(relProvider, curieProvider, halMultipleLinkRels, true);
}
public HalHandlerInstantiator(RelProvider relProvider, CurieProvider curieProvider, HalMultipleLinkRels halMultipleLinkRels, boolean enforceEmbeddedCollections) {
halHandlerInstantiator = new Jackson2HalModule.HalHandlerInstantiator(relProvider, curieProvider, enforceEmbeddedCollections);
serializerMap.put(HalLinkListSerializer.class, new HalLinkListSerializer(curieProvider, halMultipleLinkRels));
}
@Override
public JsonDeserializer<?> deserializerInstance(DeserializationConfig config, Annotated annotated, Class<?> deserClass) {
return halHandlerInstantiator.deserializerInstance(config, annotated, deserClass);
}
@Override
public KeyDeserializer keyDeserializerInstance(DeserializationConfig config, Annotated annotated, Class<?> keyDeserClass) {
return halHandlerInstantiator.keyDeserializerInstance(config, annotated, keyDeserClass);
}
@Override
public JsonSerializer<?> serializerInstance(SerializationConfig config, Annotated annotated, Class<?> serClass) {
if(serializerMap.containsKey(serClass)) {
return serializerMap.get(serClass);
} else {
return halHandlerInstantiator.serializerInstance(config, annotated, serClass);
}
}
@Override
public TypeResolverBuilder<?> typeResolverBuilderInstance(MapperConfig<?> config, Annotated annotated, Class<?> builderClass) {
return halHandlerInstantiator.typeResolverBuilderInstance(config, annotated, builderClass);
}
@Override
public TypeIdResolver typeIdResolverInstance(MapperConfig<?> config, Annotated annotated, Class<?> resolverClass) {
return halHandlerInstantiator.typeIdResolverInstance(config, annotated, resolverClass);
}
}
此实例化器将控制自定义序列化程序的生命周期。它维护一个Jackson2HalModule.HalHandlerInstantiator
的内部实例,并为所有其他序列化程序委托给该实例。
第5步:将所有内容放在一起:
@Configuration
public class ApplicationConfiguration {
private static final String HAL_OBJECT_MAPPER_BEAN_NAME = "_halObjectMapper";
private static final String DELEGATING_REL_PROVIDER_BEAN_NAME = "_relProvider";
@Autowired
private BeanFactory beanFactory;
private static CurieProvider getCurieProvider(BeanFactory factory) {
try {
return factory.getBean(CurieProvider.class);
} catch (NoSuchBeanDefinitionException e) {
return null;
}
}
@Bean
public ObjectMapper objectMapper() {
CurieProvider curieProvider = getCurieProvider(beanFactory);
RelProvider relProvider = beanFactory.getBean(DELEGATING_REL_PROVIDER_BEAN_NAME, RelProvider.class);
ObjectMapper halObjectMapper = beanFactory.getBean(HAL_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class);
//Create a new instance of Jackson2HalModule
SimpleModule module = new Jackson2HalModule();
//Provide the mix-in class so that we can override the serializer for links with our custom serializer
module.setMixInAnnotation(ResourceSupport.class, HalLinkListMixin.class);
//Register the module in the object mapper
halObjectMapper.registerModule(module);
//Set the handler instantiator on the mapper to our custom handler-instantiator
halObjectMapper.setHandlerInstantiator(new HalHandlerInstantiator(relProvider, curieProvider, halMultipleLinkRels()));
return halObjectMapper;
}
...
}
答案 2 :(得分:1)
请不要忘记HAL所需的“自身”资源链接。 在那种情况下,只有一个链接并不常见。