一尘不染

自定义具有复合ID的实体的HATEOAS链接生成

spring

我在上配置了RepositoryRestResourcePageAndSortingRepository用于访问包含复合ID的实体:

@Entity
@IdClass(CustomerId.class)
public class Customer {
    @Id BigInteger id;
    @Id int startVersion;
    ...
}

public class CustomerId {
    BigInteger id;
    int startVersion;
    ...
}

@RepositoryRestResource(collectionResourceRel = "customers", path = "customers", itemResourceRel = "customers/{id}_{startVersion}")
public interface CustomerRepository extends PagingAndSortingRepository<Customer, CustomerId> {}

"http://<server>/api/customers/1_1"例如,当我访问服务器时,我以json的形式返回了正确的资源,但是_links部分中针对self的href是错误的,并且对于我查询的任何其他客户也是相同的:"http://<server>/api/customer/1"

即:

{
  "id" : 1,
  "startVersion" : 1,
  ...
  "firstname" : "BOB",
  "_links" : {
    "self" : {
      "href" : "http://localhost:9081/reps/api/reps/1" <-- This should be /1_1
    }
  }
}

我想这是因为我的复合ID,但是我对如何更改此默认行为感到烦恼。

我看过ResourceSupportResourceProcessor类,但不确定是否需要更改多少才能解决此问题。


阅读 303

收藏
2020-04-20

共1个答案

一尘不染

不幸的是,所有Spring Data JPA / Rest最高版本为2.1.0.RELEASE都无法立即满足你的需求。该源代码隐藏在Spring Data Commons / JPA本身中。Spring Data JPA仅支持Id和EmbeddedId作为标识符。

摘录JpaPersistentPropertyImpl

static {

    // [...]

    annotations = new HashSet<Class<? extends Annotation>>();
    annotations.add(Id.class);
    annotations.add(EmbeddedId.class);

    ID_ANNOTATIONS = annotations;
}

Spring Data Commons不支持组合属性的概念。它彼此独立地对待一个类的每个属性。

当然,你可以修改Spring Data Rest。但这很麻烦,不能从根本上解决问题,并且降低了框架的灵活性。

这是hack。这应该给你一个解决问题的思路。

在你的配置中覆盖repositoryExporterHandlerAdapter并返回CustomPersistentEntityResourceAssemblerArgumentResolver。此外,覆盖backendIdConverterRegistry并添加CustomBackendIdConverter到已知列表id converter

import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.rest.core.projection.ProxyProjectionFactory;
import org.springframework.data.rest.webmvc.RepositoryRestHandlerAdapter;
import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration;
import org.springframework.data.rest.webmvc.spi.BackendIdConverter;
import org.springframework.data.rest.webmvc.support.HttpMethodHandlerMethodArgumentResolver;
import org.springframework.data.web.config.EnableSpringDataWebSupport;
import org.springframework.hateoas.ResourceProcessor;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.plugin.core.OrderAwarePluginRegistry;
import org.springframework.plugin.core.PluginRegistry;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

@Configuration
@Import(RepositoryRestMvcConfiguration.class)
@EnableSpringDataWebSupport
public class RestConfig extends RepositoryRestMvcConfiguration {
    @Autowired(required = false) List<ResourceProcessor<?>> resourceProcessors = Collections.emptyList();
    @Autowired
    ListableBeanFactory beanFactory;

    @Override
    @Bean
    public PluginRegistry<BackendIdConverter, Class<?>> backendIdConverterRegistry() {

        List<BackendIdConverter> converters = new ArrayList<BackendIdConverter>(3);
        converters.add(new CustomBackendIdConverter());
        converters.add(BackendIdConverter.DefaultIdConverter.INSTANCE);

        return OrderAwarePluginRegistry.create(converters);
    }

    @Bean
    public RequestMappingHandlerAdapter repositoryExporterHandlerAdapter() {

        List<HttpMessageConverter<?>> messageConverters = defaultMessageConverters();
        configureHttpMessageConverters(messageConverters);

        RepositoryRestHandlerAdapter handlerAdapter = new RepositoryRestHandlerAdapter(defaultMethodArgumentResolvers(),
                resourceProcessors);
        handlerAdapter.setMessageConverters(messageConverters);

        return handlerAdapter;
    }

    private List<HandlerMethodArgumentResolver> defaultMethodArgumentResolvers()
    {

        CustomPersistentEntityResourceAssemblerArgumentResolver peraResolver = new CustomPersistentEntityResourceAssemblerArgumentResolver(
                repositories(), entityLinks(), config().projectionConfiguration(), new ProxyProjectionFactory(beanFactory));

        return Arrays.asList(pageableResolver(), sortResolver(), serverHttpRequestMethodArgumentResolver(),
                repoRequestArgumentResolver(), persistentEntityArgumentResolver(),
                resourceMetadataHandlerMethodArgumentResolver(), HttpMethodHandlerMethodArgumentResolver.INSTANCE,
                peraResolver, backendIdHandlerMethodArgumentResolver());
    }
}

创建CustomBackendIdConverter。此类负责呈现你的自定义实体ID:

import org.springframework.data.rest.webmvc.spi.BackendIdConverter;

import java.io.Serializable;

public class CustomBackendIdConverter implements BackendIdConverter {

    @Override
    public Serializable fromRequestId(String id, Class<?> entityType) {
        return id;
    }

    @Override
    public String toRequestId(Serializable id, Class<?> entityType) {
        if(entityType.equals(Customer.class)) {
            Customer c = (Customer) id;
            return c.getId() + "_" +c.getStartVersion();
        }
        return id.toString();

    }

    @Override
    public boolean supports(Class<?> delimiter) {
        return true;
    }
}

CustomPersistentEntityResourceAssemblerArgumentResolver反过来应该返回一个CustomPersistentEntityResourceAssembler

import org.springframework.core.MethodParameter;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.projection.ProjectionDefinitions;
import org.springframework.data.rest.core.projection.ProjectionFactory;
import org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler;
import org.springframework.data.rest.webmvc.config.PersistentEntityResourceAssemblerArgumentResolver;
import org.springframework.data.rest.webmvc.support.PersistentEntityProjector;
import org.springframework.hateoas.EntityLinks;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;

public class CustomPersistentEntityResourceAssemblerArgumentResolver extends PersistentEntityResourceAssemblerArgumentResolver {
    private final Repositories repositories;
    private final EntityLinks entityLinks;
    private final ProjectionDefinitions projectionDefinitions;
    private final ProjectionFactory projectionFactory;

    public CustomPersistentEntityResourceAssemblerArgumentResolver(Repositories repositories, EntityLinks entityLinks,
                                                             ProjectionDefinitions projectionDefinitions, ProjectionFactory projectionFactory) {

        super(repositories, entityLinks,projectionDefinitions,projectionFactory);

        this.repositories = repositories;
        this.entityLinks = entityLinks;
        this.projectionDefinitions = projectionDefinitions;
        this.projectionFactory = projectionFactory;
    }

    public boolean supportsParameter(MethodParameter parameter) {
        return PersistentEntityResourceAssembler.class.isAssignableFrom(parameter.getParameterType());
    }

    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

        String projectionParameter = webRequest.getParameter(projectionDefinitions.getParameterName());
        PersistentEntityProjector projector = new PersistentEntityProjector(projectionDefinitions, projectionFactory,
                projectionParameter);

        return new CustomPersistentEntityResourceAssembler(repositories, entityLinks, projector);
    }
}

CustomPersistentEntityResourceAssembler需要覆盖getSelfLinkFor。如你所见,entity.getIdProperty()返回Customer类的id或startVersion属性,而该属性又被用于借助来检索实际值BeanWrapper。在这里,我们使用instanceof运算符将整个框架短路。因此,你的Customer班级应实施Serializable进一步处理。

import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.model.BeanWrapper;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler;
import org.springframework.data.rest.webmvc.support.Projector;
import org.springframework.hateoas.EntityLinks;
import org.springframework.hateoas.Link;
import org.springframework.util.Assert;

public class CustomPersistentEntityResourceAssembler extends PersistentEntityResourceAssembler {

    private final Repositories repositories;
    private final EntityLinks entityLinks;

    public CustomPersistentEntityResourceAssembler(Repositories repositories, EntityLinks entityLinks, Projector projector) {
        super(repositories, entityLinks, projector);

        this.repositories = repositories;
        this.entityLinks = entityLinks;
    }

    public Link getSelfLinkFor(Object instance) {

        Assert.notNull(instance, "Domain object must not be null!");

        Class<? extends Object> instanceType = instance.getClass();
        PersistentEntity<?, ?> entity = repositories.getPersistentEntity(instanceType);

        if (entity == null) {
            throw new IllegalArgumentException(String.format("Cannot create self link for %s! No persistent entity found!",
                    instanceType));
        }

        Object id;

        //this is a hack for demonstration purpose. don't do this at home!
        if(instance instanceof Customer) {
            id = instance;
        } else {
            BeanWrapper<Object> wrapper = BeanWrapper.create(instance, null);
            id = wrapper.getProperty(entity.getIdProperty());
        }

        Link resourceLink = entityLinks.linkToSingleResource(entity.getType(), id);
        return new Link(resourceLink.getHref(), Link.REL_SELF);
    }
}

而已!你应该看到以下URI:

{
  "_embedded" : {
    "customers" : [ {
      "name" : "test",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/demo/customers/1_1"
        }
      }
    } ]
  }
}

恕我直言,如果你正在从事绿色项目,我建议你IdClass完全放弃并使用基于Long类的技术简单ID。这已通过Spring Data Rest 2.1.0.RELEASE,Spring data JPA 1.6.0.RELEASE和Spring Framework 4.0.3RELEASE进行了测试。

2020-04-20