web-dev-qa-db-ja.com

カスタムコントローラーでのエンティティ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をCustomerエンティティに変換できないためです。

URIによってエンティティを解決するためにspring-data-restメカニズムを使用する方法はありますか?

私には1つのアイデアしかありません-entityIdを抽出してリポジトリにリクエストを送信するためにURIを解析するカスタムJsonDeserializerを使用することです。しかし、「 http:// localhost:8080/api/rest/customers/8/product 」のようなURIがある場合、この戦略は役に立ちません。その場合、product.Id値。

18
Serg

私も長い間同じ問題を抱えていて、次のように解決しました。 @Florianは正しい方向に進んでおり、彼の提案のおかげで、変換を自動的に機能させる方法を見つけました。必要なものがいくつかあります。

  1. URIからエンティティへの変換を可能にする変換サービス(フレームワークで提供されるUriToEntityConverterを活用)
  2. コンバーターを呼び出すことが適切な場合を検出するデシリアライザー(デフォルトのSDR動作を台無しにしたくない)
  3. すべてをSDRにプッシュするカスタムJacksonモジュール

ポイント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です(それ以外の場合は、デフォルトの動作にフォールバックします)
7
Luigi Cristalli

これは実際の答えというよりはサイドノートですが、しばらく前に、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);
    }

}

そして、はい、このクラスの一部は単に役に立たない可能性があります。私の弁護では、それはほんの短いハックであり、他の問題を最初に見つけたので、実際にそれを必要とすることは決してありませんでした;-)

2
Florian Schaetz

信じられない。 MONTH(!)で頭を包んだ後、なんとかこれを解決

紹介の言葉:

Spring HATEOASは、エンティティへの参照としてURIを使用します。そしてそれは素晴らしいサポートを提供します 与えられたエンティティのためにこれらのURIリンクを取得するために 。たとえば、クライアントが他の子エンティティを参照するエンティティを要求すると、クライアントはそれらのURIを受け取ります。一緒に仕事ができてうれしいです。

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

RESTクライアントはこれらのURIでのみ機能します。RESTクライアントはこれらのURIの構造を知らないでください。= RESTクライアントは、URI文字列の最後にDB内部IDがあることを知りません。

ここまでは順調ですね。ただし、Spring Data HATEOASは、URIを対応するエンティティ(DBからロードされる)に変換する機能を提供していません。誰もがカスタムRESTコントローラーでそれを必要としています。(上記の質問を参照)

カスタムRESTコントローラーでユーザーを操作する例を考えてみてください。クライアントはこのリクエストを送信します

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

カスタムRESTコントローラーは(文字列)URIからUserModelにどのようにデシリアライズしますか?私は方法を見つけました: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の作者は、両方向のユーティリティメソッドを提供することを頑固に拒否しています。

1
Robert

@RequestBodyのHALの場合、メソッドパラメータとしてResource<T>を使用し、エンティティActionを使用して、関連するリソースURIの変換を許可します。

public ResponseEntity<ActionResult> doAction(@RequestBody Resource<Action> action){
1
pdorgambide

私は次の解決策にたどり着きました。少しハックですが、機能します。

まず、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<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を使用できるようにするコンポーネント。

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

第4に、入力としてStringを受け取るエンティティコンストラクター。これはURIである必要があります。

MyEntity

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

オプションで、上記のコードの一部をUtilsクラスに移動しました。

私も受け取っていた質問投稿からのエラーメッセージを見て、この解決策にたどり着きました。 SpringはStringからオブジェクトを構築する方法を知りませんか?方法をお見せします...

ただし、コメントで述べたように、ネストされたエンティティのURIでは機能しません。

0
GuiRitter

私の解決策はいくつかコンパクトになります。すべての場合に役立つかどうかはわかりませんが、.../entity/{id}のような単純な関係の場合は解析できます。 SDRとSpringBoot2.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> 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}",
    ...
}
0

残念ながら、Springデータを使用する riToEntityConverter (URIをエンティティに変換できる汎用コンバーター)RESTはBeanまたはサービスとして。

したがって、直接@Autowiredすることはできませんが、 Converterとして登録 in Default Formatting Conversion Serviceです。

したがって、@AutowiredDefault Formatting Conversion Serviceを管理し、それらを使用してURIをエンティティに変換します。次に例を示します。

@RestController
@RequiredArgsConstructor
public class InstanceController {

    private final DefaultFormattingConversionService formattingConversionService;

    @RequestMapping(path = "/api/instances", method = {RequestMethod.POST})
    public ResponseEntity<?> create(@RequestBody @Valid InstanceDTO instanceDTO) { // get something what you want from request

        // ...

        // extract URI from any string what you want to process
        final URI uri = "..."; // http://localhost:8080/api/instances/1
        // convert URI to Entity
        final Instance instance = formattingConversionService.convert(uri, Instance.class); // Instance(id=1, ...)

        // ...

    }

}