Статьи

Spring MVC и ограничение HATEOAS

HATEOAS  — это принцип архитектуры REST, в котором гипермедиа используется для изменения состояния приложения. Чтобы изменить состояние, возвращаемое представление ресурса содержит ссылки, тем самым «ограничивая» клиента на дальнейших действиях.

Проект Spring-HATEOAS призван помочь тем, кто пишет код Spring MVC, создавать такие ссылки и собирать ресурсы, возвращаемые клиентам. 

В приведенном ниже примере будет описан простой сценарий, показывающий, как создаются и возвращаются ссылки для ресурса  Bet . Каждая операция на ресурсе описана ниже:

  • createBet  — эта   операция POST создаст ставку.
  • updateBet  — эта   операция PUT обновит ставку.
  • getBet  — эта   операция GET будет получать ставку.
  • cancelBet  — эта   операция УДАЛИТЬ отменит ставку.
@Controller
@RequestMapping("/bets")
public class BetController {
 
 private BetService betService;
 private BetResourceAssembler betResourceAssembler;
 
 public BetController(BetService betService,
   BetResourceAssembler betResourceAssembler) {
  this.betService = betService;
  this.betResourceAssembler = betResourceAssembler;
 }
 
 @RequestMapping(method = RequestMethod.POST)
 ResponseEntity<BetResource> createBet(@RequestBody Bet body) {
  Bet bet = betService.createBet(body.getMarketId(),
    body.getSelectionId(), body.getPrice(), body.getStake(),
    body.getType());
  BetResource resource = betResourceAssembler.toResource(bet);
  return new ResponseEntity<BetResource>(resource, HttpStatus.CREATED);
 }
 
 @RequestMapping(method = RequestMethod.PUT, value = "/{betId}")
 ResponseEntity<BetResource> updateBet(@PathVariable Long betId,
   @RequestBody Bet body) throws BetNotFoundException, BetNotUnmatchedException {
  Bet bet = betService.updateBet(betId, body);
  BetResource resource = betResourceAssembler.toResource(bet);
  return new ResponseEntity<BetResource>(resource, HttpStatus.OK);
 }
 
 @RequestMapping(method = RequestMethod.GET, value = "/{betId}")
 ResponseEntity<BetResource> getBet(@PathVariable Long betId) throws BetNotFoundException {
  Bet bet = betService.getBet(betId);
  BetResource resource = betResourceAssembler.toResource(bet);
  if (bet.getStatus() == BetStatus.UNMATCHED) {
   resource.add(linkTo(BetController.class).slash(bet.getId()).withRel("cancel"));
  }
  return new ResponseEntity<BetResource>(resource, HttpStatus.OK);
 }
 
 @RequestMapping(method = RequestMethod.GET)
 ResponseEntity<List<BetResource>> getBets() {
  List<Bet> betList = betService.getAllBets();
  List<BetResource> resourceList = betResourceAssembler.toResources(betList);
  return new ResponseEntity<List<BetResource>>(resourceList, HttpStatus.OK);
 }
 
 @RequestMapping(method = RequestMethod.DELETE, value = "/{betId}")
 ResponseEntity<BetResource> cancelBet(@PathVariable Long betId) {
  Bet bet = betService.cancelBet(betId);
  BetResource resource = betResourceAssembler.toResource(bet);
  return new ResponseEntity<BetResource>(resource, HttpStatus.OK);
 }
 
 @ExceptionHandler
 ResponseEntity handleExceptions(Exception ex) {
  ResponseEntity responseEntity = null;
  if (ex instanceof BetNotFoundException) {
   responseEntity = new ResponseEntity(HttpStatus.NOT_FOUND);
  } else if (ex instanceof BetNotUnmatchedException) {
   responseEntity = new ResponseEntity(HttpStatus.CONFLICT);
  } else {
   responseEntity = new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR);
  }
  return responseEntity;
 }
  
}

Все операции создадут 
BetResource  для возврата клиенту. Это делается путем вызова
toResource  на 
классе BetResourceAssembler :
public class BetResourceAssembler extends ResourceAssemblerSupport<Bet, BetResource> {
 
 public BetResourceAssembler() {
  super(BetController.class, BetResource.class);
 }
 
 public BetResource toResource(Bet bet) {
  BetResource resource = instantiateResource(bet);
  resource.bet = bet;
        resource.add(linkTo(BetController.class).slash(bet.getId()).withSelfRel());
  return resource;
 }
 
}

Этот класс расширяет 
ResourceAssemblerSupport,  который требует реализации 
метода
toResource, поскольку он реализует 
 интерфейс
ResourceAssembler . Вот где делается сопоставление между 
Bet и 
BetResource  . В этом случае 
BetResource  — это просто оболочка для 
Bet,  поэтому это просто случай установки 
 атрибута
ставки . Метод 
instantiateResource  возвращает 
BetResource  без каких-либо ссылок, поэтому ссылки могут быть добавлены на этом этапе, если это необходимо. В этом примере добавлена ​​ссылка на себя. Альтернативный подход будет использовать 
createResourceWithId,  который будет возвращать 
BetResource  с собственной ссылкой. 
public class BetResource extends ResourceSupport {
  
 public Bet bet;
  
}

Также в этом примере ссылки добавляются к 
BetResource  в 
 классе
BetController, чтобы обеспечить применение ограничения HATEOAS. Если служба REST получает запрос GET, выполняется проверка статуса 
ставки . Если 
ставка  является 
НЕПРЕВЗОЙДЕННОЙ , то ссылка , чтобы отменить 
ставки может быть добавлена к 
BetResource . Это делается аналогично собственной ссылке, но с именем атрибута отношения отмены.

Альтернативный подход к этому заключается в создании ссылки на метод, а не на создание URI. 
resource.add(linkTo(methodOn(BetController.class).cancelBet(betId))
.withRel("cancel")); 
MethodOn  бы создать прокси из 
BetController  класса и в результате возвращаемого типа
cancelBet  метода должен быть способны проксированием. Поэтому в этом примере возвращаемый тип 
 метода
cancelBet будет
 HttpEntity <Bet>,  а не 
ResponseEntity <Bet>.  Если последнее, то вероятное исключение из сервера будет:

[org.springframework.http.ResponseEntitycom.city81.hateoas.rest.BetResource> com.city81.hateoas.controller.BetController.getBet (java.lang.Long) выбрасывает com.city81.hateoas.BetNotFoundExceptionfra.srafra.sprf..sg: org .framework.AopConfigException: Не удалось сгенерировать подкласс CGLIB класса [class org.springframework.http.ResponseEntity]: общие причины этой проблемы включают использование конечного класса или невидимого класса; Вложенное исключение — java.lang.IllegalArgumentException: у Суперкласса нет нулевых конструкторов, но никакие аргументы не были предоставлены


Вернемся к запросу GET, и возвращенный JSON для запроса 
 ресурса
Bet, который имеет статус 
UNMATC HED, показан ниже:

{

 «links»: [

  {«rel»: «self», «href»:
http: // localhost : 8080 / hateoas-1-SNAPSHOT / bets / 0 },

  {«rel»: «cancel», «href»:
http: // localhost: 8080 / hateoas-1-SNAPSHOT / bets / 0 }],

 «bet» : { «ID»: 0, «marketId»: 1, «selectionId»: 22, «цена»: 4,0, «застолбить»: 2,0 «Тип»: «НАЗАД», «статус»: «НЕПРЕВЗОЙДЕННАЯ»
}


Поэтому клиент может использовать собственную ссылку для получения и обновления 
ставки , а также ссылку отмены для ее эффективного удаления.

В этом посте описываются лишь некоторые функциональные возможности проекта Spring-HATEOAS, который постоянно развивается. Чтобы получить актуальное и более подробное объяснение, посетите 
 страницы
GitHub .