即使只有一个 link,如何在特定 rel 上将 link 显示为数组

How to display a link on a particular rel as an array even if there is only one link

for (Person person : company.getPersons()) {
    resource.add(linkTo(methodOn(PersonController.class).view(person.getId()))
      .withRel("persons"));
}

我想 return 一组链接 "persons" rel。如果我有多个人没关系,但如果我只有一个人,它 return 是一个元素,我的客户端代码期望数组失败。

在 spring 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 的东西是我们最终需要的额外东西,在某些资源上 classes 一个 rel 需要是 multi 而在其他资源上它不需要......所以它看起来像这样:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ForceMultiLink {

    String[] value();

}

您使用它来注释资源中的 getLinks() 方法 class

我有一个解决此问题的方法,与 Chris 的回答类似。主要区别在于我没有扩展 Jackson2HalModule,而是创建了一个新的处理程序实例化器并将其设置为我自己创建的 Jackson2HalModule 的新实例的处理程序实例化器。我希望 Spring HATEOAS 最终会原生支持此功能;我有一个 pull request 试图这样做。以下是我实施解决方法的方法:

步骤 1:创建 mixin class:

public abstract class HalLinkListMixin {
    @JsonProperty("_links") @JsonSerialize(using = HalLinkListSerializer.class)
    public abstract List<Link> getLinks();
}

这个 mixin class 会将 HalLinkListSerializer(稍后显示)序列化程序与 links 属性 相关联。

第 2 步:创建一个容器 class 来保存其 link 表示应始终为 link 对象数组的 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 的 link-list 序列化程序:

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

这 class 不幸的是重复了逻辑,但还算不错。关键区别在于,我没有使用 OptionalListJackson2Serializer,而是使用 ListJackson2Serializer,如果该 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;
    }

    ...
}

不要忘记 HAL 所需的 "self" 资源 link。 在那种情况下,只有一个 link.

并不常见