Рубрики
Без рубрики

Включите свою службу REST с помощью пакетного API

API-интерфейсы RESTful довольно распространены в настоящее время как четкий и хорошо структурированный стандарт, ориентированный на ресурсы. Бу… С тегами java, spring boot, rest, json.

API-интерфейсы RESTful довольно распространены в настоящее время как четкий и хорошо структурированный стандарт, ориентированный на ресурсы. Но нам часто приходится сталкиваться с прецедентом, требующим вызова нескольких API для сбора всех необходимых данных. Это быстро становится проблемой, если ваш клиент является мобильным устройством с ограниченной сетью и интересуется только подмножеством данных всех ваших ответов API.

Популярным решением этой проблемы является GraphQL , язык запросов от Facebook. Хотя GraphQL является мощным инструментом, он по-прежнему требует, чтобы вы определили все схемы и функции, прежде чем клиент сможет его использовать. В этом посте я покажу вам другой подход, создав API пакетных запросов, который может быстро интегрироваться в вашу службу REST без необходимости определять какую-либо схему.

Код

Для API пакетных запросов нам понадобится механизм, способный последовательно создавать и выполнять запросы, а затем собирать все ответы. Для всей сложной части я буду использовать эту библиотеку Json Patch , которая предоставляет множество готовых функций, таких как: создание и извлечение объекта JSON, условные и циклические запросы, агрегатные функции ,… Вы можете протестировать его функции здесь

Ниже приведен код для настройки механизма пакетной обработки:

@Bean
public BatchEngine getBatchEngine(ObjectMapper objectMapper, RequestDispatcherService dispatcherService) {
    Configuration conf = Configuration.builder()
            .options(Option.SUPPRESS_EXCEPTIONS)
            .jsonProvider(new JacksonJsonProvider(objectMapper))
            .mappingProvider(new JacksonMappingProvider(objectMapper))
            .build();
    JsonBuilder jsonBuilder = new JsonBuilder(Functions.basic());
    return new BatchEngine(conf, jsonBuilder, dispatcherService);
}

А дальше идет Контроллер. Логика довольно проста, нам нужно только создать исходный запрос и передать его в механизм пакетной обработки вместе с шаблоном пакетной обработки. После ответа на возврат пакетного движка мы просто искажаем его с помощью ResponseEntity и возвращаем.

@RequestMapping(value = "/batch", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity batchRequests(@RequestHeader MultiValueMap headers, @RequestBody BatchRequest batchRequest) {
    Request request = buildOriginalRequest(headers, batchRequest.getData());
    Response response = batchEngine.execute(request, batchRequest.getTemplate());
    return new ResponseEntity(response.getBody(), HttpStatus.resolve(response.getStatus()));
    return responseEntity;
}

private Request buildOriginalRequest(MultiValueMap headers, Object body) {
    Request request = new Request();
    request.setBody(body);
    HashMap> headerMap = new HashMap<>();
    headers.forEach(headerMap::put);
    request.setHeaders(headerMap);
    return request;
}

Пакетный запрос – это простой объект Json:

{
  "data": { input data needed to build requests }
  "template": { batch template to instruct how to build & execute requests }
}

Нам также необходимо реализовать простой RequestDispatcher:

@Service
public class RequestDispatcherService implements RequestDispatcher {

    @Autowired
    private RestTemplate restTemplate;

    @Override
    public Response dispatch(Request request, JsonProvider jsonProvider, DispatchOptions options) {
        HttpHeaders requestHeaders = new HttpHeaders();
        requestHeaders.putAll(request.getHeaders());
        HttpEntity httpEntity = request.getBody() != null ?
                new HttpEntity<>(request.getBody(), requestHeaders) 
                : new HttpEntity<>(requestHeaders);
        ResponseEntity responseEntity = restTemplate.exchange(
                "Your service host" + request.getUrl(),
                HttpMethod.resolve(request.getHttpMethod()),
                httpEntity,
                Object.class);
        return buildResponse(responseEntity);
    }

    private Response buildResponse(ResponseEntity responseEntity) {
        Response response = new Response();
        Map> headers = new HashMap<>();
        responseEntity.getHeaders().forEach(headers::put);
        response.setHeaders(headers);
        response.setStatus(responseEntity.getStatusCodeValue());
        response.setBody(responseEntity.getBody());
        return response;
    }

}

И это сделано. Теперь давайте проверим это.

Реальный сценарий

В настоящее время в нашей системе (продукт Fintech) у нас есть такой сценарий: после входа пользователя в нашу систему мы получим всю информацию о пользователе, которая включает:

  • Профиль пользователя
  • Профиль компании пользователя
  • Информация о кошельке пользователя

Обычно нам приходится вызывать 4 API для сбора всех ответов, но нам нужно только подмножество данных. С помощью пакетного API нам нужно сделать только один вызов с таким телом запроса:

{
  "data": {
    "token": "user's token"
  },
  "template": {
    "requests": [
      {
        "http_method": "GET",
        "url": "/oauth/payload",
        "headers": {
          "Authorization": "Bearer @{$.original.body.token}@"
        },
        "body": null,
        "requests": [
          {
            "http_method": "POST",
            "url": "/report/users",
            "headers": {
              "Authorization": "Bearer @{$.original.body.token}@"
            },
            "body": {
              "id": "$.responses[0].body.data.user_id"
            },
            "requests": [
              {
                "http_method": "POST",
                "url": "/report/wallets",
                "headers": {
                  "Authorization": "Bearer @{$.original.body.token}@"
                },
                "body": {
                  "user_id": "$.responses[0].body.data.user_id"
                },
                "requests": [
                  {
                    "http_method": "POST",
                    "url": "/report/companies",
                    "headers": {
                      "Authorization": "Bearer @{$.original.body.token}@"
                    },
                    "body": {
                      "id": "$.responses[1].body.data.users[0].company_id"
                    }
                  }
                ]
              }
            ]
          }
        ]
      }
    ],
    "responses":[
      {
        "body": {
          "id": "$.responses[1].body.data.users[0].id",
          "full_name": "@{$.responses[1].body.data.users[0].first_name}@ @{$.responses[1].body.data.users[0].last_name}@",
          "email": "$.responses[1].body.data.users[0].email",
          "company": {
            "id": "$.responses[3].body.data.companies[0].id",
            "name": "$.responses[3].body.data.companies[0].name"
          },
          "wallets": [
            {
              "id": "$.id",
              "balance": "$.balance",
              "currency": "$.currency",
              "__array_schema": "$.responses[2].body.data.wallets"
            }
          ]
        }
      }
    ]
  }
}

Я провел тест производительности в обоих направлениях. Ниже приведен результат теста при выполнении нескольких запросов:

А далее следует результат теста при выполнении пакетного запроса:

Как вы можете видеть, способ пакетного запроса занимал в среднем всего ~141 мс в то время как путь с несколькими запросами занял ~369 мс .

Далее давайте посмотрим на размер ответа. Ниже приведен размер ответа при выполнении нескольких запросов:

А вот размер ответа при выполнении пакетного запроса:

Удалив все ненужные данные из ответа, мы смогли уменьшить размер ответа более чем в 30 раз (9730 > 272)

Вывод

Как вы можете видеть, с помощью пакетного API мы можем архивировать почти тот же результат, что и GraphQL, без ограничения заранее определенной схемы

Первоначально опубликовано на Первоначально опубликовано на

Оригинал: “https://dev.to/rey5137/power-up-your-rest-service-with-batch-api-3773”