복붙노트

[SPRING] 커스텀 컨트롤러에서 엔티티 URI 해석하기 (Spring HATEOAS)

SPRING

커스텀 컨트롤러에서 엔티티 URI 해석하기 (Spring HATEOAS)

나는 spring-data-rest를 기반으로하는 프로젝트를 가지고 있으며, 또한 몇 가지 커스텀 엔드 포인트를 가지고있다.

POST 데이터를 보내려면 json을 사용하고 있습니다.

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

스프링 데이터를 저장하는 것은 괜찮지 만 사용자 정의 컨트롤러에서는 작동하지 않습니다.

예 :

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
}

bacause case spring은 URI를 Customer 엔터티로 변환 할 수 없다.

엔티티를 URI로 해석하기 위해 스프링 - 데이터 - 휴식 메커니즘을 사용하는 방법이 있습니까?

나는 단 하나의 아이디어 만 가지고있다. 사용자 정의 JsonDeserializer를 사용하여 entityId를 추출하고 저장소에 요청을하기 위해 URI를 파싱 할 때 사용한다. 하지만이 전략은 "http : // localhost : 8080 / api / rest / customers / 8 / product"와 같은 URI가있는 경우에는 도움이되지 않습니다.이 경우 product.Id 값이 없습니다.

해결법

  1. ==============================

    1.나는 정말로 오랜 시간 동안 똑같은 문제를 겪어 왔으며 다음과 같은 방법으로 해결했다. @ 플로리안은 바른 길 위에 있었고 그의 제안 덕분에 변환 작업을 자동으로 수행 할 수있는 방법을 찾았습니다. 필요한 몇 가지가 있습니다 :

    나는 정말로 오랜 시간 동안 똑같은 문제를 겪어 왔으며 다음과 같은 방법으로 해결했다. @ 플로리안은 바른 길 위에 있었고 그의 제안 덕분에 변환 작업을 자동으로 수행 할 수있는 방법을 찾았습니다. 필요한 몇 가지가 있습니다 :

    포인트 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에서 인스턴스화를 활성화하는 것이 었습니다.

  2. ==============================

    2.이것은 실제 답변 대신 보조 정보에 더 가깝지만, SDR (더 많은 원유) 방식에서 사용되는 방법을 사용하여 URL에서 엔티티를 해결하는 클래스를 복사하여 붙여 넣을 수있었습니다. 아마도 훨씬 더 좋은 방법이있을 것입니다.하지만 그때까지는 아마도 도움이 될 것입니다 ...

    이것은 실제 답변 대신 보조 정보에 더 가깝지만, 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);
        }
    
    }
    

    그리고 예,이 클래스의 일부는 단순히 쓸모가 없을 가능성이 있습니다. 내 방어에서, 그것은 단지 짧은 해킹이었고 실제로 다른 문제를 발견했기 때문에 실제로 그것을 필요로하지 않았습니다. ;-)

  3. ==============================

    3.@RequestBody가있는 HAL의 경우 메소드 매개 변수로 Resource 를 사용하는 대신 엔티티 액션을 사용하여 관련 리소스 URI를 변환 할 수 있습니다.

    @RequestBody가있는 HAL의 경우 메소드 매개 변수로 Resource 를 사용하는 대신 엔티티 액션을 사용하여 관련 리소스 URI를 변환 할 수 있습니다.

    public ResponseEntity<ActionResult> doAction(@RequestBody Resource<Action> action){
    
  4. ==============================

    4.나는 그것을 믿을 수 없다. MONTH (!) 동안 이것 주위에 나의 머리를 감싸고 난 후에 나는 그럭저럭 이것을 처리 할 수 ​​있었다!

    나는 그것을 믿을 수 없다. MONTH (!) 동안 이것 주위에 나의 머리를 감싸고 난 후에 나는 그럭저럭 이것을 처리 할 수 ​​있었다!

    소개의 일부 단어 :

    HATEOAS는 엔티티에 대한 참조로 URI를 사용합니다. 그리고 주어진 엔티티에 대한 URI 링크를 얻을 수있는 큰 지원을 제공합니다. 예를 들어, 클라이언트가 다른 하위 엔티티를 참조하는 엔티티를 요청하면 클라이언트는 해당 URI를 수신합니다. 함께 일하게되어서 좋네요.

    GET /users/1
    { 
      "username": "foobar",
      "_links": {
         "self": {
           "href": "http://localhost:8080/user/1"  //<<<== HATEOAS Link
          }
      }
    }
    

    REST 클라이언트는 이러한 uris와 만 작동합니다. REST 클라이언트는 이러한 URI의 구조를 알 필요가 없다. REST 클라이언트는 URI 문자열 끝에 DB 내부 ID가 있다는 것을 모릅니다.

    여태까지는 그런대로 잘됐다. 그러나 스프링 데이터 HATEOAS는 URI를 다시 해당 엔티티 (DB에서로드 됨)로 변환하는 기능을 제공하지 않습니다. 모든 사용자는 맞춤형 REST 컨트롤러에서이를 필요로합니다. (위 질문 참조)

    사용자 정의 REST 컨트롤러에서 사용자와 작업하려는 예제를 생각해보십시오. 클라이언트가이 요청을 보냅니다.

    POST /checkAdress
    {
       user: "/users/1"
       someMoreOtherParams: "...",
       [...]
    }
    

    사용자 정의 REST 컨트롤러는 (String) uri에서 UserModel로 deserialize해야합니까? 방법을 찾았습니다. RepositoryRestConfigurer에서 Jackson 직렬화를 구성해야합니다.

    RepositoryRestConfigurer.java

    public class RepositoryRestConfigurer extends RepositoryRestConfigurerAdapter {
    @Autowired
      UserRepo userRepo;
    
      @Override
      public void configureJacksonObjectMapper(ObjectMapper objectMapper) {
        SimpleModule module = new SimpleModule();
        module.addDeserializer(UserModel.class, new JsonDeserializer<UserModel>() {
        @Override
            public UserModel deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
                String uri = p.getValueAsString();
                //extract ID from URI, with regular expression (1)
                Pattern regex = Pattern.compile(".*\\/" + entityName + "\\/(\\d+)");
                Matcher matcher = regex.matcher(uri);
                if (!matcher.matches()) throw new RuntimeException("This does not seem to be an URI for an '"+entityName+"': "+uri);
                String userId = matcher.group(1);
                UserModel user = userRepo.findById(userId)   
                  .orElseThrow(() -> new RuntimeException("User with id "+userId+" does not exist."))
                return user;
            }
        });
        objectMapper.registerModule(module);
    }
    
    }
    

    (1)이 문자열 파싱은 추악합니다. 알아. 하지만 이것은 org.springframework.hateoas.EntityLinks와 그 구현의 반대입니다. 그리고 spring-hateos의 저자는 양방향으로 유용한 방법을 제공하기를 완강히 거절합니다.

  5. ==============================

    5.나는 다음 해결책에 도달했다. 조금 익숙하지 만 작동합니다.

    나는 다음 해결책에 도달했다. 조금 익숙하지 만 작동합니다.

    첫째, URI를 개체로 변환하는 서비스.

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

    둘째, Spring 컨텍스트 외부에서 EntityConverter를 사용할 수있는 구성 요소.

    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;
        }
    }
    

    셋째, 다른 엔티티를 입력으로 사용하는 엔티티 생성자.

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

    네 번째로 String을 입력으로 사용하는 엔티티 생성자. URI 여야합니다.

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

    선택적으로 위 코드의 일부를 Utils 클래스로 옮겼습니다.

    질문 게시글의 오류 메시지를보고이 솔루션에 도달했습니다. Spring은 String으로부터 객체를 생성하는 방법을 모른다. 내가 어떻게 보여 줄지 ...

    그러나 주석에서 말한 것처럼 중첩 된 엔티티의 URI는 작동하지 않습니다.

  6. ==============================

    6.내 솔루션은 일부 컴팩트 것입니다. 그것은 모든 경우에 유용 할 수 있지만 ... / entity / {id}와 같은 간단한 관계에 대해서는 파싱 할 수 있다는 것을 확신하지 못합니다. SDR 및 스프링 부트 2.0.3에서 테스트했습니다. 릴리스

    내 솔루션은 일부 컴팩트 것입니다. 그것은 모든 경우에 유용 할 수 있지만 ... / entity / {id}와 같은 간단한 관계에 대해서는 파싱 할 수 있다는 것을 확신하지 못합니다. SDR 및 스프링 부트 2.0.3에서 테스트했습니다. 릴리스

    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> T convert(Link link, Class<T> 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<CategoryForm, Category> {
    
        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}",
        ...
    }
    
  7. from https://stackoverflow.com/questions/37186417/resolving-entity-uri-in-custom-controller-spring-hateoas by cc-by-sa and MIT license