جاوا و تکنولوژی های آن

java programming language

در این وبلاگ به بررسی نکات موجود در جاوا و تکنولوژی های آن می پردازیم

طبقه بندی موضوعی


یکی از مباحث مهم در پیاده سازی کامل RESTful API وجود HATEOAS در سرویس است که در این بخش به نقش آن و نحوه پیاده سازی در Spring REST میپردازیم


HATEOAS : Hypermedia As The Engine Of Application State


ما با HATEOAS این امکان را فراهم میکنیم که کلاینت بدون اینکه وابسته به سرور باشد بصورت مستقل توسعه یابد و نیازی نباشد که لیست آدرس ریسورس ها را داشته باشد در حقیقت با داشتن آدرس پایه به کلیه قسمت های API دسترسی پیدا میکند و میتواند لیست موجود ریسورس ها را دریافت کند همچنین بداند برای چه ریسورسی باید از کدام آدرس های موجود در لیست استفاده کند 


چرا نیاز است API ما قابلیت شناسایی سرویس را برای کلاینت داشته باشد ؟


در حالت ابتدایی یک سرویس RESTful میتوان محدودیت شناسایی کردن عملیات روی ریسورس ها را لمس کرد و تنها پروتکل ارتباطی در سرویس RESTful بر پایه تکست است و اگر قرار باشد تعامل با کلاینت ها برقرار کند Hypertext تنها راه ارتباطی است و محدودیتی که وجود دارد کلاینت ها باید داکیومنت سرویس را داشته و پیاده سازی کنند و این دور از مفهوم API است 


در حقیقت پیاده سازی سمت سرور سرویس باید قابل فهم برای کلاینت باشد تا بتواند تنها از طریق Hypertext از API استفاده کند 



در ادامه سناریوهای پیاده سازی HATEOAS را بررسی خواهیم کرد :


* باید به این نکته توجه کرد که فرض شده کلاینت عملیات Authentication را انجام می دهد و در حین تست مشکلی با احراز هویت وجود ندارد


1- سناریوی کشف متد های مجاز :


هنگامی که کلاینت درخواست استفاده از متدی که مجاز به استفاده از آن نیست را بدهد باید در جواب کد وضعیت 405 METHOD NOT ALLOWED را دریافت کند

همچنین API باید بتواند به کلاینت کمک کند تا متد های مجاز آن ریسورس خاص را شناسایی کند و برای این منظور میتوانیم در HTTP header از فیلد Allow در response استفاده کنیم 

تست ارسال فیلد هدر Allow در پاسخ :

@Test
public void whenInvalidPOSTIsSentToValidURIOfResource_thenAllowHeaderListsTheAllowedActions(){
    // Given
    String uriOfExistingResource = restTemplate.createResource();
 
    // When
    Response res = givenAuth().post(uriOfExistingResource);
 
    // Then
    String allowHeader = res.getHeader(HttpHeaders.ALLOW);
    assertThat( allowHeader, AnyOf.anyOf(
      containsString("GET"), containsString("PUT"), containsString("DELETE") ) );
}



2- سناریوی کشف URI ریسورس های ایجاد شده جدید:


عملیات ایجاد یک ریسورس جدید باید همیشه در بر گیرنده URI ریسورس جدید در response باشد برای این منظور میتوان از فیلد Location در هدر استفاده کرد 

اگر کلاینت یک درخواست GET به آن URI ارسال کند ریسورس قابل دستیابی خواهد بود 

@Test
public void whenResourceIsCreated_thenUriOfTheNewlyCreatedResourceIsDiscoverable() {
    // When
    Foo newResource = new Foo(randomAlphabetic(6));
    Response createResp = givenAuth().contentType("application/json")
      .body(unpersistedResource).post(getFooURL());
    String uriOfNewResource= createResp.getHeader(HttpHeaders.LOCATION);
 
    // Then
    Response response = givenAuth().header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
      .get(uriOfNewResource);
 
    Foo resourceFromServer = response.body().as(Foo.class);
    assertThat(newResource, equalTo(resourceFromServer));
}

توضیح تست ساده بالا :

ابتدا یک ریسورس جدید به نام FOO ایجاد کردیم سپس با استفاده از HTTP response ریسورس FOO که اکنون در URI جدید قابل استفاده است را به کلاینت معرفی میکنیم 

سپس یک درخواست GET به URI ریسورس جدید ارسال میکنیم و جواب را دریافت و با نمونه سرور آن مقایسه میکنیم 



3- سناریوی کشف تمامی ریسورس هایی که در یک URI قابل دسترس است و تمامی از جنس همان URI هستند:


وقتی ما به یک ریسورس خاص مثل FOO دسترسی پیدا کردیم باید بتوانیم مابقی ریسورس های از جنس FOO را بصورت لیست داشته باشیم بنابراین باید ما بتوانیم با استفاده از یک ریسورس عملیاتی داشته باشیم که لیست ریسورس های دیگر از آن نوع را در اختیار کلاینت قرار دهد و برای این منظور از فیلد Link در هدر استفاده میکنیم :

@Test
public void whenResourceIsRetrieved_thenUriToGetAllResourcesIsDiscoverable() {
    // Given
    String uriOfExistingResource = createAsUri();
 
    // When
    Response getResponse = givenAuth().get(uriOfExistingResource);
 
    // Then
    String uriToAllResources = HTTPLinkHeaderUtil
      .extractURIByRel(getResponse.getHeader("Link"), "collection");
 
    Response getAllResponse = givenAuth().get(uriToAllResources);
    assertThat(getAllResponse.getStatusCode(), is(200));
}




4- دیگر پتانسیل های قابل کشف URI ها و Microformat ها :


با فیلد Link هدر میتوان پتانسیل های دیگری برای URI ایجاد کرد، برای مثال کلاینت بتواند با استفاده از GET روی یک ریسورس، ریسورس دیگری را ایجاد کرد 



جداسازی قابلیت کشف، از طریق Event ها :


قابلیت کشف ریسورس ها باید به عنوان یک لایه جدا از Http Web Controller قرار گیرد تا در صورت قطع بودن موقت سرویس حالت کاری و شرایط تعاملی برای کلاینت قابل فهم باشد



ابتدا event ها را ایجاد میکنیم :


public class SingleResourceRetrieved extends ApplicationEvent {
    private HttpServletResponse response;
 
    public SingleResourceRetrieved(Object source, HttpServletResponse response) {
        super(source);
 
        this.response = response;
    }
 
    public HttpServletResponse getResponse() {
        return response;
    }
}
public class ResourceCreated extends ApplicationEvent {
    private HttpServletResponse response;
    private long idOfNewResource;
 
    public ResourceCreated(Object source, 
      HttpServletResponse response, long idOfNewResource) {
        super(source);
 
        this.response = response;
        this.idOfNewResource = idOfNewResource;
    }
 
    public HttpServletResponse getResponse() {
        return response;
    }
    public long getIdOfNewResource() {
        return idOfNewResource;
    }
}


بعد برای نمونه یک کنترلر با دو عملیات ساده ایجاد میکنیم :

یکی برای پیدا کردن ریسورس با استفاده از id و دیگری ایجاد یک ریسورس جدید

@RestController
@RequestMapping(value = "/foos")
public class FooController {
 
    @Autowired
    private ApplicationEventPublisher eventPublisher;
 
    @Autowired
    private IFooService service;
 
    @GetMapping(value = "foos/{id}")
    public Foo findById(@PathVariable("id") Long id, HttpServletResponse response) {
        Foo resourceById = Preconditions.checkNotNull(service.findOne(id));
 
        eventPublisher.publishEvent(new SingleResourceRetrieved(this, response));
        return resourceById;
    }
 
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public void create(@RequestBody Foo resource, HttpServletResponse response) {
        Preconditions.checkNotNull(resource);
        Long newId = service.create(resource).getId();
 
        eventPublisher.publishEvent(new ResourceCreated(this, response, newId));
    }
}


حالا ما میتوانیم این event ها را بصورت مستقل و به هر تعداد که نیاز باشد توسط Listener ها رسیدگی کنیم که هر یک از آن Listener ها میتواند روی وظیفه خاصی تمرکز داشته باشند و منجر به کمتر کردن محدودیت های HATEOAS شود


این Listener ها باید آخرین ابجکت هایی باشند که صدا زده میشوند برای همین نیازی نیست که دسترسی مستقیم به آنها داشته باشیم و بصورت public تعریف شده باشند 



ایجاد کردن URI برای ریسورس تازه ایجاد شده :


همانطور که پیشتر دیدیم وقتی یک ریسورس جدید ایجاد شد باید در انتها آدرس URI آن در هدر Location برگشت داده شود ما با Listener زیر اینکار را انجام میدهیم :

@Component
class ResourceCreatedDiscoverabilityListener
  implements ApplicationListener<ResourceCreated>{
 
    @Override
    public void onApplicationEvent(ResourceCreated resourceCreatedEvent){
       Preconditions.checkNotNull(resourceCreatedEvent);
 
       HttpServletResponse response = resourceCreatedEvent.getResponse();
       long idOfNewResource = resourceCreatedEvent.getIdOfNewResource();
 
       addLinkHeaderOnResourceCreation(response, idOfNewResource);
   }
   void addLinkHeaderOnResourceCreation
     (HttpServletResponse response, long idOfNewResource){
       URI uri = ServletUriComponentsBuilder.fromCurrentRequestUri().
         path("/{idOfNewResource}").buildAndExpand(idOfNewResource).toUri();
       response.setHeader("Location", uri.toASCIIString());
    }
}

در این کد با استفاده از ServletUriComponentsBuilder توانستیم به درخواست جاری کلاینت کمک کنیم که URI جدید را بعد از ایجاد دریافت کند اگر مقدار برگشتی API از نوع ResponseEntity بود میتوانیم از Location هم استفاده کنیم 



وقتی یک ریسورس داشتیم و از همان ریسورس کلاینت میخواست به کلیه ریسورس های همان نوع دسترسی پیدا کند:


@Component
class SingleResourceRetrievedDiscoverabilityListener
 implements ApplicationListener<SingleResourceRetrieved>{
 
    @Override
    public void onApplicationEvent(SingleResourceRetrieved resourceRetrievedEvent){
        Preconditions.checkNotNull(resourceRetrievedEvent);
 
        HttpServletResponse response = resourceRetrievedEvent.getResponse();
        addLinkHeaderOnSingleResourceRetrieval(request, response);
    }
    void addLinkHeaderOnSingleResourceRetrieval(HttpServletResponse response){
        String requestURL = ServletUriComponentsBuilder.fromCurrentRequestUri().
          build().toUri().toASCIIString();
        int positionOfLastSlash = requestURL.lastIndexOf("/");
        String uriForResourceCreation = requestURL.substring(0, positionOfLastSlash);
 
        String linkHeaderValue = LinkUtil
          .createLinkHeader(uriForResourceCreation, "collection");
        response.addHeader(LINK_HEADER, linkHeaderValue);
    }
}

دقت کنید که از لینک ها با استفاده از collection بهم مرتبط شده اند و که در بسیاری از microformat قابل مشاهده است ولی هنوز بصورت استاندارد تعریف نشده است 


هدر Link یکی از هدر های پر کاربرد برای کشف ریسورس ها میباشد ابزاری که این هدر را میتواند ایجاد کند به سادگی کد زیر است :

public class LinkUtil {
    public static String createLinkHeader(String uri, String rel) {
        return "<" + uri + ">; rel="" + rel + """;
    }
}



قابلیت کشف از Root :


وقتی یک کلاینت برای اولین بار میخواهد از یک API استفاده کند اولین چیزی که با آن روبرو میشود پایه ای ترین آدرسی است که از API دارد و به آن Root گفته میشود 

حال این امکان که ریسورس های API از طریق Root قابل شناسایی باشند کمک بزرگی به کلاینت خواهد بود که در کد زیر نحوه پیاده سازی آنرا خواهیم دید :

@GetMapping("/")
@ResponseStatus(value = HttpStatus.NO_CONTENT)
public void adminRoot(final HttpServletRequest request, final HttpServletResponse response) {
    String rootUri = request.getRequestURL().toString();
 
    URI fooUri = new UriTemplate("{rootUri}{resource}").expand(rootUri, "foos");
    String linkToFoos = LinkUtil.createLinkHeader(fooUri.toASCIIString(), "collection");
    response.addHeader("Link", linkToFoos);
}



مشکل ترین سد راه این امکان عالی، پیاده سازی آن هم در سمت سرور و هم در کلاینت است 

Spring پروژه ای بنام Spring HATEOAS دارد که برای این منظور در آدرس https://spring.io/projects/spring-hateoas قابل دسترس است اما از طرف کلاینت خیلی انتخاب زیادی وجود ندارد 


با بالغ تر شدن طراحی سرویس ها و API ، امروزه میتوان با استفاده از g-RPC و GraphQL راه حل های نوین تری در راه اندازی سرویس ارائه داد که مطالعه در این زمینه خالی از لطف نیست چه بسا به مرور استفاده از REST کمرنگ تر و روش های جدیدتر که تکامل بیشتری دارند جایگزین آن شود پس قبل از طراحی و توسعه سرویس نیاز است مطالعه ای روی این جایگزین های احتمالی داشته باشید تا در آینده نیاز به طراحی مجدد کمتری داشته باشید







نظرات  (۰)

هیچ نظری هنوز ثبت نشده است

ارسال نظر

ارسال نظر آزاد است، اما اگر قبلا در بیان ثبت نام کرده اید می توانید ابتدا وارد شوید.
شما میتوانید از این تگهای html استفاده کنید:
<b> یا <strong>، <em> یا <i>، <u>، <strike> یا <s>، <sup>، <sub>، <blockquote>، <code>، <pre>، <hr>، <br>، <p>، <a href="" title="">، <span style="">، <div align="">
تجدید کد امنیتی