为具有复合 ID 的实体自定义 HATEOAS 链接生成 [英] Customizing HATEOAS link generation for entities with composite ids
问题描述
我在 PageAndSortingRepository
上配置了一个 RepositoryRestResource
来访问一个包含复合 Id 的实体:
I have configured a RepositoryRestResource
on a PageAndSortingRepository
that accesses an Entity that includes a composite 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 中的 hrefself 部分是错误的,对于我查询的任何其他客户也是如此:"http://<server>/api/customer/1"
When i access the server at "http://<server>/api/customers/1_1"
for instance, I get the correct resource back as json, but the href in the _links section for self is the wrong and also the same for any other customer i query: "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,但我很高兴如何更改此默认行为.
I suppose this is because of my composite Id, But I am chuffed as to how i can change this default behaviour.
我已经查看了 ResourceSupport
和 ResourceProcessor
类,但不确定我需要更改多少才能解决此问题.
I've had a look at the ResourceSupport
and the ResourceProcessor
class but am not sure how much i need to change in order fix this issue.
懂春天的人可以帮帮我吗?
Can someone who knows spring lend me a hand?
推荐答案
不幸的是,直到 2.1.0.RELEASE 的所有 Spring Data JPA/Rest 版本都无法满足您开箱即用的需求.源代码隐藏在 Spring Data Commons/JPA 本身中.Spring Data JPA 仅支持 Id
和 EmbeddedId
作为标识符.
Unfortunately, all Spring Data JPA/Rest versions up to 2.1.0.RELEASE are not able to serve your need out of the box.
The source is buried inside Spring Data Commons/JPA itself. Spring Data JPA supports only Id
and EmbeddedId
as identifier.
摘录JpaPersistentPropertyImpl
:
static {
// [...]
annotations = new HashSet<Class<? extends Annotation>>();
annotations.add(Id.class);
annotations.add(EmbeddedId.class);
ID_ANNOTATIONS = annotations;
}
Spring Data Commons 不支持组合属性的概念.它独立地对待一个类的每个属性.
Spring Data Commons doesn't support the notion of combined properties. It treats every property of a class independently from each other.
当然,你可以破解 Spring Data Rest.但是这样很麻烦,并没有从根本上解决问题,降低了框架的灵活性.
Of course, you can hack Spring Data Rest. But this is cumbersome, doesn't solve the problem at its heart and reduces the flexibility of the framework.
这里是黑客.这应该能让您了解如何解决您的问题.
Here's the hack. This should give you an idea how to tackle your problem.
在您的配置中覆盖 repositoryExporterHandlerAdapter
并返回一个 CustomPersistentEntityResourceAssemblerArgumentResolver
.此外,覆盖 backendIdConverterRegistry
并将 CustomBackendIdConverter
添加到已知的 id 转换器
列表:
In your configuration override repositoryExporterHandlerAdapter
and return a CustomPersistentEntityResourceAssemblerArgumentResolver
.
Additionally, override backendIdConverterRegistry
and add CustomBackendIdConverter
to the list of known 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:
Create CustomBackendIdConverter
. This class is responsible for rendering your custom entity ids:
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
:
CustomPersistentEntityResourceAssemblerArgumentResolver
in turn should return a 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
以进行进一步处理.
CustomPersistentEntityResourceAssembler
needs to override getSelfLinkFor
. As you can see entity.getIdProperty()
return either id or startVersion property of your Customer
class which in turn gets used to retrieve the real value with the help of a BeanWrapper
. Here we are short circuit the whole framework with the use of instanceof
operator. Hence your Customer
class should implement Serializable
for further processing.
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:
That's it! You should see this URIs:
{
"_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.3.RELEASE 进行测试.
Imho, if you are working on a green field project I would suggest to ditch IdClass
entirely and go with technical simple ids based on Long class. This was tested with Spring Data Rest 2.1.0.RELEASE, Spring data JPA 1.6.0.RELEASE and Spring Framework 4.0.3.RELEASE.
这篇关于为具有复合 ID 的实体自定义 HATEOAS 链接生成的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!