web-dev-qa-db-ja.com

Spring HATEOAS組み込みリソースのサポート

REST APIを含めるためにHAL形式を使用したい 埋め込みリソース 。APIにSpring HATEOASを使用していますが、Spring HATEOASは埋め込みリソースをサポートしているようです。ただし、これの使用方法に関するドキュメントや例はありません。

Spring HATEOASを使用して埋め込みリソースを含める方法の例を提供できますか?

43
Glide

Springの HATEOASに関するドキュメント を必ず読んでください。基本を理解するのに役立ちます。

この回答では コア開発者は、ResourceResources、およびPagedResourcesの概念を指摘していますが、これらはドキュメントには含まれていません。

それがどのように機能するかを理解するのに少し時間がかかったので、いくつかの例を見ていきましょう。

単一のリソースを返す

リソース

import org.springframework.hateoas.ResourceSupport;


public class ProductResource extends ResourceSupport{
    final String name;

    public ProductResource(String name) {
        this.name = name;
    }
}

コントローラー

import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resource;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyController {
    @RequestMapping("products/{id}", method = RequestMethod.GET)
    ResponseEntity<Resource<ProductResource>> get(@PathVariable Long id) {
        ProductResource productResource = new ProductResource("Apfelstrudel");
        Resource<ProductResource> resource = new Resource<>(productResource, new Link("http://example.com/products/1"));
        return ResponseEntity.ok(resource);
    }
}

応答

{
    "name": "Apfelstrudel",
    "_links": {
        "self": { "href": "http://example.com/products/1" }
    }
}

複数のリソースを返す

Spring HATEOASには、Resourcesが複数のリソースでの応答を反映するために使用する組み込みサポートが付属しています。

    @RequestMapping("products/", method = RequestMethod.GET)
    ResponseEntity<Resources<Resource<ProductResource>>> getAll() {
        ProductResource p1 = new ProductResource("Apfelstrudel");
        ProductResource p2 = new ProductResource("Schnitzel");

        Resource<ProductResource> r1 = new Resource<>(p1, new Link("http://example.com/products/1"));
        Resource<ProductResource> r2 = new Resource<>(p2, new Link("http://example.com/products/2"));

        Link link = new Link("http://example.com/products/");
        Resources<Resource<ProductResource>> resources = new Resources<>(Arrays.asList(r1, r2), link);

        return ResponseEntity.ok(resources);
    }

応答

{
    "_links": {
        "self": { "href": "http://example.com/products/" }
    },
    "_embedded": {
        "productResources": [{
            "name": "Apfelstrudel",
            "_links": {
                "self": { "href": "http://example.com/products/1" }
            }, {
            "name": "Schnitzel",
            "_links": {
                "self": { "href": "http://example.com/products/2" }
            }
        }]
    }
}

キーproductResourcesを変更する場合は、リソースに注釈を付ける必要があります。

@Relation(collectionRelation = "items")
class ProductResource ...

リソースが埋め込まれたリソースを返す

これは、Spring Springを開始する必要がある場合です。 別の答え で@ chris-damourによって導入されたHALResourceは完全に適合します。

public class OrderResource extends HalResource {
    final float totalPrice;

    public OrderResource(float totalPrice) {
        this.totalPrice = totalPrice;
    }
}

コントローラー

    @RequestMapping(name = "orders/{id}", method = RequestMethod.GET)
    ResponseEntity<OrderResource> getOrder(@PathVariable Long id) {
        ProductResource p1 = new ProductResource("Apfelstrudel");
        ProductResource p2 = new ProductResource("Schnitzel");

        Resource<ProductResource> r1 = new Resource<>(p1, new Link("http://example.com/products/1"));
        Resource<ProductResource> r2 = new Resource<>(p2, new Link("http://example.com/products/2"));
        Link link = new Link("http://example.com/order/1/products/");

        OrderResource resource = new OrderResource(12.34f);
        resource.add(new Link("http://example.com/orders/1"));

        resource.embed("products", new Resources<>(Arrays.asList(r1, r2), link));

        return ResponseEntity.ok(resource);
    }

応答

{
    "_links": {
        "self": { "href": "http://example.com/products/1" }
    },
    "totalPrice": 12.34,
    "_embedded": {
        "products":     {
            "_links": {
                "self": { "href": "http://example.com/orders/1/products/" }
            },
            "_embedded": {
                "items": [{
                    "name": "Apfelstrudel",
                    "_links": {
                        "self": { "href": "http://example.com/products/1" }
                    }, {
                    "name": "Schnitzel",
                    "_links": {
                        "self": { "href": "http://example.com/products/2" }
                    }
                }]
            }
        }
    }
}
37
linqu

HATEOAS 1.0.0M1以前:これを行う公式の方法が見つかりませんでした...

public abstract class HALResource extends ResourceSupport {

    private final Map<String, ResourceSupport> embedded = new HashMap<String, ResourceSupport>();

    @JsonInclude(Include.NON_EMPTY)
    @JsonProperty("_embedded")
    public Map<String, ResourceSupport> getEmbeddedResources() {
        return embedded;
    }

    public void embedResource(String relationship, ResourceSupport resource) {

        embedded.put(relationship, resource);
    }  
}

次に、リソースにHALResourceを拡張させました

更新:HATEOAS 1.0.0M1のEntityModel(および実際にRepresentationalModelを拡張するもの)は、埋め込みリソースがgetContentを介して公開されている限り(またはjacksonがコンテンツプロパティをシリアル化する限り)ネイティブにサポートされるようになりました。のような:

    public class Result extends RepresentationalModel<Result> {
        private final List<Object> content;

        public Result(

            List<Object> content
        ){

            this.content = content;
        }

        public List<Object> getContent() {
            return content;
        }
    };

    EmbeddedWrappers wrappers = new EmbeddedWrappers(false);
    List<Object> elements = new ArrayList<>();

    elements.add(wrappers.wrap(new Product("Product1a"), LinkRelation.of("all")));
    elements.add(wrappers.wrap(new Product("Product2a"), LinkRelation.of("purchased")));
    elements.add(wrappers.wrap(new Product("Product1b"), LinkRelation.of("all")));

    return new Result(elements);

あなたが得る

{
 _embedded: {
   purchased: {
    name: "Product2a"
   },
  all: [
   {
    name: "Product1a"
   },
   {
    name: "Product1b"
   }
  ]
 }
}
31
Chris DaMour

これが私たちが見つけた小さな例です。まず、spring-hateoas-0.16を使用します

電子メールリストが埋め込まれたユーザープロファイルを返すGET /profileがあるイメージング。

メールリソースがあります。

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@Relation(value = "email", collectionRelation = "emails")
public class EmailResource {
    private final String email;
    private final String type;
}

プロファイル応答に埋め込む2つの電子メール

Resource primary = new Resource(new Email("[email protected]", "primary"));
Resource home = new Resource(new Email("[email protected]", "home"));

これらのリソースが埋め込まれていることを示すには、EmbeddedWrappersのインスタンスが必要です。

import org.springframework.hateoas.core.EmbeddedWrappers
EmbeddedWrappers wrappers = new EmbeddedWrappers(true);

wrappersの助けを借りて、各メールにEmbeddedWrapperインスタンスを作成し、リストに入れることができます。

List<EmbeddedWrapper> embeddeds = Arrays.asList(wrappers.wrap(primary), wrappers.wrap(home))

行うべきことは、これらの埋め込みでプロファイルリソースを構築することだけです。以下の例では、lombokを使用してコードを短縮しています。

@Data
@Relation(value = "profile")
public class ProfileResource {
    private final String firstName;
    private final String lastName;
    @JsonUnwrapped
    private final Resources<EmbeddedWrapper> embeddeds;
}

埋め込みフィールドの注釈@JsonUnwrappedを念頭に置いてください

これをすべてコントローラーから返す準備ができました

...
Resources<EmbeddedWrapper> embeddedEmails = new Resources(embeddeds, linkTo(EmailAddressController.class).withSelfRel());
return ResponseEntity.ok(new Resource(new ProfileResource("Thomas", "Anderson", embeddedEmails), linkTo(ProfileController.class).withSelfRel()));
}

これで、レスポンスに

{
"firstName": "Thomas",
"lastName": "Anderson",
"_links": {
    "self": {
        "href": "http://localhost:8080/profile"
    }
},
"_embedded": {
    "emails": [
        {
            "email": "[email protected]",
            "type": "primary"
        },
        {
            "email": "[email protected]",
            "type": "home"
        }
    ]
}
}

Resources<EmbeddedWrapper> embeddedsを使用することの興味深い部分は、さまざまなリソースをその中に入れることができ、リレーションによってそれらを自動的にグループ化することです。このために、@Relationパッケージの注釈org.springframework.hateoas.coreを使用します。

また、HALの埋め込みリソースについて good article があります

26

通常、HATEOASはREST出力を表すPOJOを作成する必要があり、HATEOASが提供するResourceSupportを拡張します。追加のPOJOを作成せずにこれを行うことができます。以下のコード:

@RestController
class CustomerController {

    List<Customer> customers;

    public CustomerController() {
        customers = new LinkedList<>();
        customers.add(new Customer(1, "Peter", "Test"));
        customers.add(new Customer(2, "Peter", "Test2"));
    }

    @RequestMapping(value = "/customers", method = RequestMethod.GET, produces = "application/hal+json")
    public Resources<Resource> getCustomers() {

        List<Link> links = new LinkedList<>();
        links.add(linkTo(methodOn(CustomerController.class).getCustomers()).withSelfRel());
        List<Resource> resources = customerToResource(customers.toArray(new Customer[0]));

        return new Resources<>(resources, links);

    }

    @RequestMapping(value = "/customer/{id}", method = RequestMethod.GET, produces = "application/hal+json")
    public Resources<Resource> getCustomer(@PathVariable int id) {

        Link link = linkTo(methodOn(CustomerController.class).getCustomer(id)).withSelfRel();

        Optional<Customer> customer = customers.stream().filter(customer1 -> customer1.getId() == id).findFirst();

        List<Resource> resources = customerToResource(customer.get());

        return new Resources<Resource>(resources, link);

    }

    private List<Resource> customerToResource(Customer... customers) {

        List<Resource> resources = new ArrayList<>(customers.length);

        for (Customer customer : customers) {
            Link selfLink = linkTo(methodOn(CustomerController.class).getCustomer(customer.getId())).withSelfRel();
            resources.add(new Resource<Customer>(customer, selfLink));
        }

        return resources;
    }
}
5
Peter Szanto

上記の回答を組み合わせて、はるかに簡単なアプローチを作成しました。

_return resWrapper(domainObj, embeddedRes(domainObj.getSettings(), "settings"))
_

これはカスタムユーティリティクラスです(以下を参照)。注意:

  • resWrapperの2番目の引数は、embeddedRes呼び出しの_..._を受け入れます。
  • resWrapper内の関係文字列を省略する別のメソッドを作成できます。
  • embeddedResの最初の引数はObjectであるため、ResourceSupportのインスタンスを指定することもできます
  • 式の結果は、_Resource<DomainObjClass>_を拡張する型です。したがって、すべてのSpring Data REST _ResourceProcessor<Resource<DomainObjClass>>_。]によって処理されます。それらのコレクションを作成し、new Resources<>()をラップすることもできます。

ユーティリティクラスを作成します。

_import com.fasterxml.jackson.annotation.JsonUnwrapped;
import Java.util.Arrays;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.Resources;
import org.springframework.hateoas.core.EmbeddedWrapper;
import org.springframework.hateoas.core.EmbeddedWrappers;

public class ResourceWithEmbeddable<T> extends Resource<T> {

    @SuppressWarnings("FieldCanBeLocal")
    @JsonUnwrapped
    private Resources<EmbeddedWrapper> wrappers;

    private ResourceWithEmbeddable(final T content, final Iterable<EmbeddedWrapper> wrappers, final Link... links) {

        super(content, links);
        this.wrappers = new Resources<>(wrappers);
    }


    public static <T> ResourceWithEmbeddable<T> resWrapper(final T content,
                                                           final EmbeddedWrapper... wrappers) {

        return new ResourceWithEmbeddable<>(content, Arrays.asList(wrappers));

    }

    public static EmbeddedWrapper embeddedRes(final Object source, final String rel) {
        return new EmbeddedWrappers(false).wrap(source, rel);
    }
}
_

サービスクラスに_import static package.ResourceWithEmbeddable.*_を含めるだけで使用できます。

JSONは次のようになります。

_{
    "myField1": "1field",
    "myField2": "2field",
    "_embedded": {
        "settings": [
            {
                "settingName": "mySetting",
                "value": "1337",
                "description": "umh"
            },
            {
                "settingName": "other",
                "value": "1488",
                "description": "a"
            },...
        ]
    }
}
_
3
Sam

これは私がspring-boot-starter-hateoas 2.1.1でそのようなjsonを構築した方法です:

{
    "total": 2,
    "count": 2,
    "_embedded": {
        "contacts": [
            {
                "id": "1-1CW-303",
                "role": "ASP",
                "_links": {
                    "self": {
                        "href": "http://localhost:8080/accounts/2700098669/contacts/1-1CW-303"
                    }
                }
            },
            {
                "id": "1-1D0-267",
                "role": "HSP",
                "_links": {
                    "self": {
                        "href": "http://localhost:8080/accounts/2700098669/contacts/1-1D0-267"
                    }
                }
            }
        ]
    },
    "_links": {
        "self": {
            "href": "http://localhost:8080/accounts/2700098669/contacts?limit=2&page=1"
        },
        "first": {
            "href": "http://localhost:8080/accounts/2700098669/contacts?limit=2&page=1"
        },
        "last": {
            "href": "http://localhost:8080/accounts/2700098669/contacts?limit=2&page=1"
        }
    }
}

このすべてのフィールドをカプセル化するメインクラスは

public class ContactsResource extends ResourceSupport{
    private long count;
    private long total;
    private final Resources<Resource<SimpleContact>> contacts;

    public long getTotal() {
        return total;
    }

    public ContactsResource(long total, long count, Resources<Resource<SimpleContact>> contacts){
        this.contacts = contacts;
        this.total = total;
        this.count = count;
    }

    public long getCount() {
        return count;
    }

    @JsonUnwrapped
    public Resources<Resource<SimpleContact>> getContacts() {
        return contacts;
    }
}

SimpleContactには単一の連絡先に関する情報があり、それは単なるポージョです

@Relation(value = "contact", collectionRelation = "contacts")
public class SimpleContact {
    private String id;
    private String role;

    public String getId() {
        return id;
    }

    public SimpleContact id(String id) {
        this.id = id;
        return this;
    }

    public String getRole() {
        return role;
    }

    public SimpleContact role(String role) {
        this.role = role;
        return this;
    }
}

ContactsResourceの作成:

public class ContactsResourceConverter {

    public static ContactsResource toResources(Page<SimpleContact> simpleContacts, Long accountId){

        List<Resource<SimpleContact>> embeddeds = simpleContacts.stream().map(contact -> {
            Link self = linkTo(methodOn(AccountController.class).getContactById(accountId, contact.getId())).
                    withSelfRel();
            return new Resource<>(contact, self);
        }
        ).collect(Collectors.toList());

        List<Link> listOfLinks = new ArrayList<>();
        //self link
        Link selfLink = linkTo(methodOn(AccountController.class).getContactsForAccount(
                accountId,
                simpleContacts.getPageable().getPageSize(),
                simpleContacts.getPageable().getPageNumber() + 1)) // +1 because of 0 first index
                .withSelfRel();
        listOfLinks.add(selfLink);

        ... another links           

        Resources<Resource<SimpleContact>> resources = new Resources<>(embeddeds);
        ContactsResource contactsResource = new ContactsResource(simpleContacts.getTotalElements(), simpleContacts.getNumberOfElements(), resources);
        contactsResource.add(listOfLinks);

        return contactsResource;
    }
}

そして、私はこのようにコントローラーからこれを呼び出しています:

return new ResponseEntity<>(ContactsResourceConverter.toResources(simpleContacts, accountId), HttpStatus.OK);
0
Eugene

この依存関係をpomに追加します。このリンクを確認してください: https://www.baeldung.com/spring-rest-hal

<dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-rest-hal-browser</artifactId>
</dependency>

これはあなたの反応をこのように変えます。

"_links": {
    "next": {
        "href": "http://localhost:8082/mbill/user/listUser?extra=ok&page=11"
    }
}
0
aditya lath

Springはビルダーを提供します https://github.com/spring-projects/spring-hateoas/issues/864

0
Sam