为具有复合 ID 的实体自定义 HATEOAS 链接生成
Posted
技术标签:
【中文标题】为具有复合 ID 的实体自定义 HATEOAS 链接生成【英文标题】:Customizing HATEOAS link generation for entities with composite ids 【发布时间】:2014-07-11 04:20:40 【问题描述】:我在PageAndSortingRepository
上配置了一个RepositoryRestResource
,该PageAndSortingRepository
访问一个包含复合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,但我很高兴如何更改此默认行为。
我查看了 ResourceSupport
和 ResourceProcessor
类,但不确定我需要进行多少更改才能解决此问题。
知道春天的人可以帮帮我吗?
【问题讨论】:
【参考方案1】:不幸的是,2.1.0.RELEASE 之前的所有 Spring Data JPA/Rest 版本都无法开箱即用地满足您的需求。
源代码隐藏在 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。但这很麻烦,并没有从根本上解决问题,降低了框架的灵活性。
这里是黑客。这应该让您知道如何解决您的问题。
在您的配置中覆盖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.3.RELEASE 测试的。
【讨论】:
没问题。这是一个有趣的问题。希望能帮助到你。也许你需要实现fromRequestId
来反序列化你的id。
好答案!我还没有尝试过,但是这个存储库会接受 POST 请求吗?如何通过 REST API 插入数据?【参考方案2】:
首先,创建一个 SpringUtil 来从 spring 中获取 bean。
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class SpringUtil implements ApplicationContextAware
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException
if(SpringUtil.applicationContext == null)
SpringUtil.applicationContext = applicationContext;
public static ApplicationContext getApplicationContext()
return applicationContext;
public static Object getBean(String name)
return getApplicationContext().getBean(name);
public static <T> T getBean(Class<T> clazz)
return getApplicationContext().getBean(clazz);
public static <T> T getBean(String name,Class<T> clazz)
return getApplicationContext().getBean(name, clazz);
然后,实现 BackendIdConverter。
import com.alibaba.fastjson.JSON;
import com.example.SpringUtil;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.webmvc.spi.BackendIdConverter;
import org.springframework.stereotype.Component;
import javax.persistence.EmbeddedId;
import javax.persistence.Id;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.net.URLDecoder;
import java.net.URLEncoder;
@Component
public class CustomBackendIdConverter implements BackendIdConverter
@Override
public boolean supports(Class<?> delimiter)
return true;
@Override
public Serializable fromRequestId(String id, Class<?> entityType)
if (id == null)
return null;
//first decode url string
if (!id.contains(" ") && id.toUpperCase().contains("%7B"))
try
id = URLDecoder.decode(id, "UTF-8");
catch (UnsupportedEncodingException e)
e.printStackTrace();
//deserialize json string to ID object
Object idObject = null;
for (Method method : entityType.getDeclaredMethods())
if (method.isAnnotationPresent(Id.class) || method.isAnnotationPresent(EmbeddedId.class))
idObject = JSON.parseObject(id, method.getGenericReturnType());
break;
//get dao class from spring
Object daoClass = null;
try
daoClass = SpringUtil.getBean(Class.forName("com.example.db.dao." + entityType.getSimpleName() + "DAO"));
catch (ClassNotFoundException e)
e.printStackTrace();
//get the entity with given primary key
JpaRepository simpleJpaRepository = (JpaRepository) daoClass;
Object entity = simpleJpaRepository.findOne((Serializable) idObject);
return (Serializable) entity;
@Override
public String toRequestId(Serializable id, Class<?> entityType)
if (id == null)
return null;
String jsonString = JSON.toJSONString(id);
String encodedString = "";
try
encodedString = URLEncoder.encode(jsonString, "UTF-8");
catch (UnsupportedEncodingException e)
e.printStackTrace();
return encodedString;
之后。你可以为所欲为。
下面有一个示例。
如果实体具有单个属性 pk,您可以使用 localhost:8080/demo/1 正常。根据我的代码,假设 pk 有注释“@Id”。 如果实体已经组成了pk,假设pk是demoId类型,并且有 注解“@EmbeddedId”,可以使用 localhost:8080/demo/demoId json 获取/放置/删除。并且您的自我链接将是相同的。【讨论】:
【参考方案3】:虽然不可取,但我已通过在我的 JPA 实体上使用 @EmbeddedId
而不是 IdClass
注释来解决此问题。
像这样:
@Entity
public class Customer
@EmbeddedId
private CustomerId id;
...
public class CustomerId
@Column(...)
BigInteger key;
@Column(...)
int startVersion;
...
我现在在返回的实体上看到正确生成的链接1_1
。
如果有人仍然可以指导我找到不需要我更改模型表示的解决方案,我们将不胜感激。幸运的是,我的应用程序开发没有取得太大进展,因此在更改时会引起严重关注,但我想对于其他人来说,执行这样的更改会产生很大的开销:(例如,更改在 JPQL 中引用此模型的所有查询查询)。
【讨论】:
我知道这是一篇旧帖子,但我想我会补充一点,您可以将@Transient
添加到 getKey
方法中,该方法将在 Customer
中返回 id.key
,这将允许您可以快速访问您的 API,但不会影响您的 REST 表示。它很丑,但让你的 API 更干净。
恕我直言最好的方法【参考方案4】:
我遇到了一个类似的问题,即数据休息的复合键场景不起作用。 @ksokol 详细解释为解决问题提供了必要的输入。主要为 data-rest-webmvc 和 data-jpa 更改了我的 pom
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-rest-webmvc</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.7.1.RELEASE</version>
</dependency>
它解决了与复合键相关的所有问题,我不需要进行自定义。感谢 ksokol 的详细解释。
【讨论】:
您好@Alagesan,您知道解决问题的Spring Data JPA 还是REST 版本吗?谢谢以上是关于为具有复合 ID 的实体自定义 HATEOAS 链接生成的主要内容,如果未能解决你的问题,请参考以下文章
Spring Boot:HATEOAS 和自定义 JacksonObjectMapper
使用自定义 ID 插入数据的 Code-First 实体框架
带有 RepositoryRestResource-s 和常规控制器的 Spring REST HATEOAS 中的根请求的自定义响应