解析自定义控制器中的实体 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<ActionResult> 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
}

由于案例弹簧无法将URI转换为客户实体。

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

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


答案 1

我也一直遇到同样的问题很长一段时间了,并通过以下方式解决了它。@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点,在自定义存储库RestConfigurerAdapter中,

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(否则它只是回退到默认行为)

答案 2

这更像是一个旁注,而不是一个真正的答案,但不久前我设法复制并粘贴了一个类,通过使用SDR中使用的方法从URL解析实体(只是更粗糙)。可能有一个更好的方法,但在那之前,也许这会有所帮助......

@Service
public class EntityConverter {

    @Autowired
    private MappingContext<?, ?> mappingContext;

    @Autowired
    private ApplicationContext applicationContext;

    @Autowired(required = false)
    private List<RepositoryRestConfigurer> configurers = Collections.emptyList();

    public <T> T convert(Link link, Class<T> 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<FormattingConversionService> converter = new DomainClassConverter<FormattingConversionService>(
                conversionService);
        converter.setApplicationContext(applicationContext);
    }

}

是的,这个类的某些部分很可能根本没有用。在我的辩护中,这只是一个短暂的黑客攻击,我从来没有真正需要它,因为我首先发现了其他问题;-)