8 Aralık 2019 Pazar

HATEOAS - Hypermedia As The Engine Of Application State

Giriş
HATEOS ile ilgili bir yazı burada

HATEOS ve Richarson Modeli
Açıklaması şöyle
HATEOAS and the Richardson maturity model

Going into literature to find solutions addressing this problem, you may come across the Richardson maturity model of HTTP application APIs. It starts at its base with plain old XML, meaning that XML content is posted to a web-service endpoint.

At level 1, the API introduces the concept of resources, allowing to manipulate entities of the backend individually and thus breaking a large service endpoint down into multiple resources.

A level 2 API makes further use of specific HTTP verbs like PUT, DELETE, or PATCH to refine what the meaning of an operation is. Martin Fowler calls it providing a standard set of verbs so that we handle similar situations in the same way, removing unnecessary variation.

At level 3, discoverability is baked into the API by adding context-specific hyperlinks to each response, giving the client insights on what operations could be done next with a given resource, or linking related resources. At this level, the ‘hyper-text’ exchanged with the API acts as the engine of application state, rooted in the backend and revealed to the client - in our case the web frontend - via hyperlinks.
Kısaca HATEOAS Nedir?
Açıklaması şöyle. Burada amaç uygulamanın "State" bilgisinin hypermedia ile zenginleştirilmesi ve keşfedilebilirlik (discoverability) yeteneğinin gelmesi. 
HATEOAS (Hypermedia as the Engine of the Application State) is an architectural component that allows driving application state (resources’ representations) enhanced with hypermedia support.
Spring HATEOAS
Eğer Spring Data Rest kullanmıyorsa ve HATEOS çıktı istiyorsak bu mümkün.
Örnek
Maven'da şu satırı dahil ederiz
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
RepresentationModel sınıfından kalıtırız. Şöyle yaparız
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.hateoas.RepresentationModel;

import java.io.Serializable;
import java.util.List;

@Getter 
@Setter
@EqualsAndHashCode(callSuper = false)
@ToString
public class Student extends RepresentationModel<Student> implements Serializable {
  private String id;
  private String name;
  private List<Lecture> lectureList;
}
Artık bu model'in add() gibi metodları vardır. Modeli kullanmak için şöyle yaparız
@Override
@Transactional(propagation = Propagation.REQUIRED)
public Student getStudentById(String studentId) {

  Optional<StudentEntity> studentEntity = Optional.of(this.studentRepository
.findById(studentId))
    .orElse(null);

  if (studentEntity.isPresent()) {
    Student student = this.studentMapper.toDto(studentEntity.get());

    //adding hateoas links to student object
    final Link selfLink = WebMvcLinkBuilder.linkTo(StudentController.class)
.slash(student.getId()).withSelfRel();
    student.add(selfLink);

    student.getLectureList().forEach(lectureRef -> {
      final Link selfLectureLink = WebMvcLinkBuilder.linkTo(LectureController.class)
.slash(lectureRef.getId()).withSelfRel();
      lectureRef.add(selfLectureLink);
    });

    return student;
  }
  return null;
}
WebMvcLinkBuilder metodlarının açıklaması şöyle
linkTo method : finds the controller class & its root mapping, which are “/student” for StudentController & “/lecture” for LectureController in our example.

slash method : add the variable to the link, which are studentId & lectureId in our example

withSelfRel method : defines that the href is a self link.

withRel method : relation type for the href can be defined with this method if required like “lectureRel”.
Bunu controller'da kullanmak için şöyle yaparız
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
public class StudentController {

  @GetMapping(produces = {"application/hal+json"})
  ResponseEntity<CollectionModel<StudentRef>> getAllStudentRefList() {
    List<StudentRef> studentRefList = this.studentService.getAllStudentRefList();

    CollectionModel<StudentRef> studentRefCollectionModel = null;
    if (CollectionUtils.isNotEmpty(studentRefList)) {
      Link link = WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(StudentController.class).getAllStudentRefList()).withSelfRel();
      studentRefCollectionModel = CollectionModel.of(studentRefList, link);
    }

    return Optional.ofNullable(studentRefCollectionModel)
        .map(ResponseEntity::ok)
        .orElseGet(() -> ResponseEntity.notFound().build());
  }

  @GetMapping(value = "/{studentId}", produces = {"application/hal+json"})
  ResponseEntity<Student> getStudentById(@PathVariable final String studentId) {
    final Student student = this.studentService.getStudentById(studentId);

    return Optional.ofNullable(student)
        .map(ResponseEntity::ok)
        .orElseGet(() -> ResponseEntity.notFound().build());
  }

}
Spring Data Rest ve HATEOAS
Spring Data Rest HATEOAS çıktısı verir. Açıklaması şöyle. Bunun için @RepositoryRestResource anotasyonu kullanılır
Spring Data REST uses Spring HATEOAS to automatically expose resources for entities managed by Spring Data repositories and leverages hypermedia aspects to do pagination, link entities etc. So it covers the 80% use case for the basic stuff and allows you to selectively add more complex processes using manually implemented controllers later on.
HATEOAS Bölümleri
 HATEOAS çıktısı 3 bölümden oluşur
1. _embedded
2. _links
3. page

1. _embedded
_embedded içinde nesneler bulunur. Her nesne iki kısımdan oluşur

1. Alanlar : Nesneden erişilebilen ancak repository olmayan diğer nesnelerdir.
2. _link : Nesneden erişilebilen ve repository olan diğer nesnelerdir. Bu _links ile 2. bölümdeki _links karıştırılıyor. 2. bölümdeki _links getirilen nesnenin kendi Repository'si ile ilgili. Bu bölümdeki _links ise nesnenin kendi içindeki @OneToMany tarzı ilişkiler için

Örnek
Şöyle yaparız
{
  "_embedded": {
    "fooplatform": [ //fooplatform nesneneri dizisi
      {
         "description":"Blah blah"
         "bar": {
            "automatic": false
          }
        "_links":{
          "self":{
            "href": "http://localhost:8080/foo/1234567890"
          },
          "baz":{
            "href":"http://localhost:8080/baz/123{?projection}"
          }
          "anotherbaz":{
            "href":"http://localhost:8080/anotherbaz/1234/anotherbaz"
          }
        }
      {...} //İkinci foolplatform nesnesi
    ]
  }
  "_links":{
    "first":{"href":"http://localhost:8080/foo?page=0?size=1"}
    "self":{...}
    "next":{...}
    "last":{...}
    "profile":{...}
    "search":{...}
  }
  "page":{
    "size":1,
    "totalElements":320,
    "totalPages":320,
    "number":1,
  }
}

Örnek
Sunucumuzun kök adresine get gönderirsek çıktı olarak şunu alırız. Bu çıktıda comments ve posts nesneleri Repository ile erişilebilen nesnelerdir.
curl -X GET http://localhost:8080

200 OK
{ _links : {
    comments : { href : "…" },
    posts :  { href : "…" }
  }
}
Örnek
Elimizde şöyle bir kod olsun.
public class User {
  private String name;
  private Address address;
}

public class Address {
  private String street;
}
Her iki sınıfın da repository sınıfı da varsa çıktı olarak şunu alırız. Address nesnesine sadece link verilir.
{
  "name": "John",
  "_links": {
    "self": "http://localhost:8080/users/1",
    "address": "http://localhost:8080/addresses/1"
  }
}
Eğer Address sınıfının repository kodu olmasaydı Address nesnesinin alanları da çıktıya "_embedded" başlığı altında dahil edilir.

Eğer User sınıfının nasıl göründüğünü kontrol etmek istersek @Projection anotasyonu kullanılır.

Örnek
Şöyle bir çıktı alırız
{
"name" : "Foo",
"street" : "street Bar",
"streetNumber" : 2,
"streetLetter" : "b",
"postCode" : "D-1253",
"town" : "Munchen",
"country" : "Germany",
"phone" : "+34 4410122000",
"vat" : "000000001",
"employees" : 225,
"_links" : {
     "self" : {
          "href" : "http://localhost:8080/app/companies/1"
     },
     "sector" : {
          "href" : "http://localhost:8080/app/companies/1/sector"
     }
  }
}
2. _links
Burada Repository içinde dolaşmak için linkler var. Linkler şöyle
first
self
next
last
profile
search

profile
Açıklaması şöyle. Yani Repository için uygulanabilecek ilave bilgileri verir.
The HAL media type is only concerned with embedded resources and with links. How does a client know which http verb (like GET, PUT, POST, PATCH) is required to interact with a link? For the use case described above, one could argue that the http verb for invoking a business action must always be POST, as the request typically changes the state in the backend and is not necessarily idempotent. Submitting an already submitted production order in our sample code would for example lead to an exception.

The HAL specification proposes to solve this problem via additional documentation provided by the API. Spring Data REST by default includes a basic profile resource for each exposed entity. For our sample, the URL is http://localhost:8080/api/profile/productionOrders. If you invoke it you see that we would need to provide additional documentation to include our custom methods.
Örnek
Elimizde şöyle bir kod olsun
public class ProductionOrder {

  @Id
  private Long id;
  private String name;
  private LocalDate expectedCompletionDate;
  private ProductionOrderState state;

}
Şöyle yaparız
$ curl http://localhost:8080/api/productionOrders
{
  "_embedded" : {
    "productionOrders" : [ {
      "name" : "Order 1",
      "expectedCompletionDate" : null,
      "state" : "DRAFT",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/api/productionOrders/1"
        },
        "productionOrder" : {
          "href" : "http://localhost:8080/api/productionOrders/1"
        }
      }
    }, {
      "name" : "Order 2",
      "expectedCompletionDate" : null,
      "state" : "DRAFT",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/api/productionOrders/2"
        },
        "productionOrder" : {
          "href" : "http://localhost:8080/api/productionOrders/2"
        }
      }
    } ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/api/productionOrders"
    },
    "profile" : {
      "href" : "http://localhost:8080/api/profile/productionOrders"
    }
  }
}
3. page
Sorgunun Repository içindeki yerini gösterir. Şu alanlar var
size : Sorgudaki page size
totalElements : Repository'deki toplam nesne sayısı
totalPages : Sorgudaki page size ile toplam kaç sayfa olacağı
number : Açıklama yaz


Hiç yorum yok:

Yorum Gönder