解析自定义控制器中的实体URI(Spring HATEOAS)

我有一个基于spring-data-rest的项目,它也有一些自定义端点。

为了发送POST数据,我正在使用json

{ "action": "REMOVE", "customer": "http://localhost:8080/api/rest/customers/7" } 

对于spring-data-rest来说这很好,但不适用于自定义控制器。

例如:

 public class Action { public ActionType action; public Customer customer; } @RestController public class ActionController(){ @Autowired private ActionService actionService; @RestController public class ActionController { @Autowired private ActionService actionService; @RequestMapping(value = "/customer/action", method = RequestMethod.POST) public ResponseEntity doAction(@RequestBody Action action){ ActionType actionType = action.action; Customer customer = action.customer;//<------There is a problem ActionResult result = actionService.doCustomerAction(actionType, customer); return ResponseEntity.ok(result); } } 

我打电话的时候

 curl -v -X POST -H "Content-Type: application/json" -d '{"action": "REMOVE","customer": "http://localhost:8080/api/rest/customers/7"}' http://localhost:8080/customer/action 

我有一个答案

 { "timestamp" : "2016-05-12T11:55:41.237+0000", "status" : 400, "error" : "Bad Request", "exception" : "org.springframework.http.converter.HttpMessageNotReadableException", "message" : "Could not read document: Can not instantiate value of type [simple type, class model.user.Customer] from String value ('http://localhost:8080/api/rest/customers/7'); no single-String constructor/factory method\n at [Source: java.io.PushbackInputStream@73af10c6; line: 1, column: 33] (through reference chain: api.controller.Action[\"customer\"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Can not instantiate value of type [simple type, class logic.model.user.Customer] from String value ('http://localhost:8080/api/rest/customers/7'); no single-String constructor/factory method\n at [Source: java.io.PushbackInputStream@73af10c6; line: 1, column: 33] (through reference chain: api.controller.Action[\"customer\"])", "path" : "/customer/action" * Closing connection 0 } 

bacause case spring无法将URI转换为Customer实体。

有没有办法使用spring-data-rest机制来解析URI的实体?

我只有一个想法 – 使用自定义JsonDeserializer解析URI以提取entityId并向存储库发出请求。 但是如果我有像“ http:// localhost:8080 / api / rest / customers / 8 / product ”这样的URI,那么这个策略对我没有帮助。在这种情况下,我没有product.Id值。

我很长时间以来一直遇到同样的问题,并通过以下方式解决了。 @Florian走在正确的轨道上,由于他的建议,我找到了一种方法让转换自动完成。 需要几件:

  1. 一种转换服务,用于实现从URI到实体的转换(利用框架提供的UriToEntityConverter)
  2. 一个解串器来检测何时适合调用转换器(我们不想搞乱默认的SDR行为)
  3. 一个定制的jackson模块,将一切推向SDR

对于第1点,可以将实施范围缩小到以下范围

 import org.springframework.context.ApplicationContext; import org.springframework.data.mapping.context.PersistentEntities; import org.springframework.data.repository.support.DomainClassConverter; import org.springframework.data.rest.core.UriToEntityConverter; import org.springframework.format.support.DefaultFormattingConversionService; public class UriToEntityConversionService extends DefaultFormattingConversionService { private UriToEntityConverter converter; public UriToEntityConversionService(ApplicationContext applicationContext, PersistentEntities entities) { new DomainClassConverter<>(this).setApplicationContext(applicationContext); converter = new UriToEntityConverter(entities, this); addConverter(converter); } public UriToEntityConverter getConverter() { return converter; } } 

对于第2点,这是我的解决方案

 import com.fasterxml.jackson.databind.BeanDescription; import com.fasterxml.jackson.databind.DeserializationConfig; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.deser.BeanDeserializerBuilder; import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier; import com.fasterxml.jackson.databind.deser.ValueInstantiator; import com.fasterxml.jackson.databind.deser.std.StdValueInstantiator; import your.domain.RootEntity; // <-- replace this with the import of the root class (or marker interface) of your domain import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.convert.TypeDescriptor; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.context.PersistentEntities; import org.springframework.data.rest.core.UriToEntityConverter; import org.springframework.util.Assert; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.Optional; public class RootEntityFromUriDeserializer extends BeanDeserializerModifier { private final UriToEntityConverter converter; private final PersistentEntities repositories; public RootEntityFromUriDeserializer(PersistentEntities repositories, UriToEntityConverter converter) { Assert.notNull(repositories, "Repositories must not be null!"); Assert.notNull(converter, "UriToEntityConverter must not be null!"); this.repositories = repositories; this.converter = converter; } @Override public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BeanDescription beanDesc, BeanDeserializerBuilder builder) { PersistentEntity entity = repositories.getPersistentEntity(beanDesc.getBeanClass()); boolean deserializingARootEntity = entity != null && RootEntity.class.isAssignableFrom(entity.getType()); if (deserializingARootEntity) { replaceValueInstantiator(builder, entity); } return builder; } private void replaceValueInstantiator(BeanDeserializerBuilder builder, PersistentEntity entity) { ValueInstantiator currentValueInstantiator = builder.getValueInstantiator(); if (currentValueInstantiator instanceof StdValueInstantiator) { EntityFromUriInstantiator entityFromUriInstantiator = new EntityFromUriInstantiator((StdValueInstantiator) currentValueInstantiator, entity.getType(), converter); builder.setValueInstantiator(entityFromUriInstantiator); } } private class EntityFromUriInstantiator extends StdValueInstantiator { private final Class entityType; private final UriToEntityConverter converter; private EntityFromUriInstantiator(StdValueInstantiator src, Class entityType, UriToEntityConverter converter) { super(src); this.entityType = entityType; this.converter = converter; } @Override public Object createFromString(DeserializationContext ctxt, String value) throws IOException { URI uri; try { uri = new URI(value); } catch (URISyntaxException e) { return super.createFromString(ctxt, value); } return converter.convert(uri, TypeDescriptor.valueOf(URI.class), TypeDescriptor.valueOf(entityType)); } } } 

然后对于第3点,在自定义RepositoryRestConfigurerAdapter中,

 public class MyRepositoryRestConfigurer extends RepositoryRestConfigurerAdapter { @Override public void configureJacksonObjectMapper(ObjectMapper objectMapper) { objectMapper.registerModule(new SimpleModule("URIDeserializationModule"){ @Override public void setupModule(SetupContext context) { UriToEntityConverter converter = conversionService.getConverter(); RootEntityFromUriDeserializer rootEntityFromUriDeserializer = new RootEntityFromUriDeserializer(persistentEntities, converter); context.addBeanDeserializerModifier(rootEntityFromUriDeserializer); } }); } } 

这对我来说很顺利,并且不会干扰框架的任何转换(我们有许多自定义端点)。 在第2点中,目的是仅在以下情况下从URI启用实例化:

  1. 被反序列化的实体是根实体(因此没有属性)
  2. 提供的字符串是一个实际的URI(否则它只是回到默认行为)

这是一个侧面注释而不是真正的答案,但不久前我设法复制并粘贴了一个类,通过使用SDR中使用的方法来解析URL中的实体(更粗略)。 可能有一个更好的方法,但在那之前,也许这有助于……

 @Service public class EntityConverter { @Autowired private MappingContext mappingContext; @Autowired private ApplicationContext applicationContext; @Autowired(required = false) private List configurers = Collections.emptyList(); public  T convert(Link link, Class target) { DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(); PersistentEntities entities = new PersistentEntities(Arrays.asList(mappingContext)); UriToEntityConverter converter = new UriToEntityConverter(entities, conversionService); conversionService.addConverter(converter); addFormatters(conversionService); for (RepositoryRestConfigurer configurer : configurers) { configurer.configureConversionService(conversionService); } URI uri = convert(link); T object = target.cast(conversionService.convert(uri, TypeDescriptor.valueOf(target))); if (object == null) { throw new IllegalArgumentException(String.format("%s '%s' was not found.", target.getSimpleName(), uri)); } return object; } private URI convert(Link link) { try { return new URI(link.getHref()); } catch (Exception e) { throw new IllegalArgumentException("URI from link is invalid", e); } } private void addFormatters(FormatterRegistry registry) { registry.addFormatter(DistanceFormatter.INSTANCE); registry.addFormatter(PointFormatter.INSTANCE); if (!(registry instanceof FormattingConversionService)) { return; } FormattingConversionService conversionService = (FormattingConversionService) registry; DomainClassConverter converter = new DomainClassConverter( conversionService); converter.setApplicationContext(applicationContext); } } 

是的,这类课程的某些部分很可能毫无用处。 在我的辩护中,它只是一个短暂的黑客,我从来没有实际需要它,因为我发现其他问题首先;-)

对于具有@RequestBody HAL,使用Resource作为方法参数而不是实体Action以允许转换相关资源URI

 public ResponseEntity doAction(@RequestBody Resource action){ 

我到达了以下解决方案。 这有点hackish,但有效。

首先,将URI转换为实体的服务。

EntityConverter

 import java.net.URI; import java.util.Collections; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.core.convert.TypeDescriptor; import org.springframework.data.geo.format.DistanceFormatter; import org.springframework.data.geo.format.PointFormatter; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.context.PersistentEntities; import org.springframework.data.repository.support.DefaultRepositoryInvokerFactory; import org.springframework.data.repository.support.DomainClassConverter; import org.springframework.data.repository.support.Repositories; import org.springframework.data.rest.core.UriToEntityConverter; import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer; import org.springframework.format.FormatterRegistry; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.format.support.FormattingConversionService; import org.springframework.hateoas.Link; import org.springframework.stereotype.Service; @Service public class EntityConverter { @Autowired private MappingContext mappingContext; @Autowired private ApplicationContext applicationContext; @Autowired(required = false) private List configurers = Collections.emptyList(); public  T convert(Link link, Class target) { DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(); Repositories repositories = new Repositories(applicationContext); UriToEntityConverter converter = new UriToEntityConverter( new PersistentEntities(Collections.singleton(mappingContext)), new DefaultRepositoryInvokerFactory(repositories), repositories); conversionService.addConverter(converter); addFormatters(conversionService); for (RepositoryRestConfigurer configurer : configurers) { configurer.configureConversionService(conversionService); } URI uri = convert(link); T object = target.cast(conversionService.convert(uri, TypeDescriptor.valueOf(target))); if (object == null) { throw new IllegalArgumentException(String.format("registerNotFound", target.getSimpleName(), uri)); } return object; } private URI convert(Link link) { try { return new URI(link.getHref().replace("{?projection}", "")); } catch (Exception e) { throw new IllegalArgumentException("invalidURI", e); } } private void addFormatters(FormatterRegistry registry) { registry.addFormatter(DistanceFormatter.INSTANCE); registry.addFormatter(PointFormatter.INSTANCE); if (!(registry instanceof FormattingConversionService)) { return; } FormattingConversionService conversionService = (FormattingConversionService) registry; DomainClassConverter converter = new DomainClassConverter( conversionService); converter.setApplicationContext(applicationContext); } } 

其次,一个组件能够在Spring上下文之外使用EntityConverter

ApplicationContextHolder

 import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; @Component public class ApplicationContextHolder implements ApplicationContextAware { private static ApplicationContext context; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { context = applicationContext; } public static ApplicationContext getContext() { return context; } } 

第三,将另一个实体作为输入的实体构造函数。

myEntity所

 public MyEntity(MyEntity entity) { property1 = entity.property1; property2 = entity.property2; property3 = entity.property3; // ... } 

第四,将String作为输入的实体构造函数,它应该是URI。

myEntity所

 public MyEntity(String URI) { this(ApplicationContextHolder.getContext().getBean(EntityConverter.class).convert(new Link(URI.replace("{?projection}", "")), MyEntity.class)); } 

或者,我已将上面的部分代码移到了Utils类。

通过查看问题post中的错误消息,我得到了这个解决方案,我也得到了。 Spring不知道如何从String构造对象? 我会告诉它如何……

然而,就像评论中所说的那样,对于嵌套实体的URI不起作用。

我的解决方案将是一些紧凑的。 不确定它对所有情况都有用,但对于像.../entity/{id}之类的简单关系,它可以解析。 我已经在SDR和Spring Boot 2.0.3.RELEASE上进行了测试

 import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.core.convert.TypeDescriptor; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.context.PersistentEntities; import org.springframework.data.repository.support.Repositories; import org.springframework.data.repository.support.RepositoryInvokerFactory; import org.springframework.data.rest.core.UriToEntityConverter; import org.springframework.hateoas.Link; import org.springframework.stereotype.Service; import java.net.URI; import java.util.Collections; @Service public class UriToEntityConversionService { @Autowired private MappingContext mappingContext; // OOTB @Autowired private RepositoryInvokerFactory invokerFactory; // OOTB @Autowired private Repositories repositories; // OOTB public  T convert(Link link, Class target) { PersistentEntities entities = new PersistentEntities(Collections.singletonList(mappingContext)); UriToEntityConverter converter = new UriToEntityConverter(entities, invokerFactory, repositories); URI uri = convert(link); Object o = converter.convert(uri, TypeDescriptor.valueOf(URI.class), TypeDescriptor.valueOf(target)); T object = target.cast(o); if (object == null) { throw new IllegalArgumentException(String.format("%s '%s' was not found.", target.getSimpleName(), uri)); } return object; } private URI convert(Link link) { try { return new URI(link.getHref()); } catch (Exception e) { throw new IllegalArgumentException("URI from link is invalid", e); } } } 

用法:

 @Component public class CategoryConverter implements Converter { private UriToEntityConversionService conversionService; @Autowired public CategoryConverter(UriToEntityConversionService conversionService) { this.conversionService = conversionService; } @Override public Category convert(CategoryForm source) { Category category = new Category(); category.setId(source.getId()); category.setName(source.getName()); category.setOptions(source.getOptions()); if (source.getParent() != null) { Category parent = conversionService.convert(new Link(source.getParent()), Category.class); category.setParent(parent); } return category; } } 

请求JSON,如:

 { ... "parent": "http://localhost:8080/categories/{id}", ... }