Platform V DataSpace: пишем код на Java при помощи удобного SDK

от автора

Привет, Хабр! Продолжаем рассказывать, как быстро и просто создавать микросервисные приложения. В прошлой статье мы написали frontend с помощью Platform V DataSpace. В примере был использован TypeScript, но, как мы и говорили, это необязательное требование.

Теперь рассмотрим, как разрабатывать backend-приложения на языке Java с помощью сервиса Platform V Functions и инструмента DataSpace SDK.

Platform V Functions — это FaaS-решение, позволяющее загружать исходный код сервиса в виде функции в OpenShift/k8s без создания docker-образов и настройки окружения.

Но основное внимание в статье уделим даже не Functions, а DataSpace SDK. Это инструмент для удобного взаимодействия с DataSpace по протоколу JSON-RPC. По ходу статьи мы рассмотрим основные фичи, которые DataSpace SDK предоставляет Java-разработчику.

Приложение «Промоакция»

В качестве примера снова возьмём приложение «Промоакция» из предыдущей статьи.

Изменим архитектуру нашего приложения, разбив его на микросервисы. Теперь промокоды и подарки будут вестись раздельно разными сервисами, у каждого из которых будет свой DataSpace со своей моделью данных.

Архитектура приложения на этот раз будет выглядеть вот так:

Function 1 Vouchers — backend-сервис, отвечающий за ведение промокодов.

Function 2 Gifts — backend-сервис, отвечающий за ведение подарков.

Function 3 Report — backend-сервис, предоставляющий различные аналитические отчёты о подарках.

Разработка

Представим, что разработкой данного приложения занимаются два разработчика:

  • разработчик Vouchers реализует часть приложения, которая связана с управлением промокодами;

  • разработчик Gifts реализует часть приложения, которая связана с управлением подарками.

Для начала работы каждому разработчику нужно развернуть сервис DataSpace в своём пространстве в SmartMarket Studio. Подробнее о том, как это сделать, мы рассказывали здесь, в разделе «Работа» в SmartMarket Studio.

У каждого DataSpace будет своя модель данных:

Voucher и Gift теперь имеют связь OneToOne. Но тип этой связи «из внешней системы», так как они находятся в разных моделях данных.

Итак, сервисы DataSpace развёрнуты. Теперь создадим заготовки для наших сервисов.

Разработчик Vouchers создаёт в своём пространстве соответствующую функцию:

Разработчик Gifts создаёт в своём пространстве функции Gifts Function, Reports Function:

Теперь разработаем «начинки» для функций — это будут хорошо известные всем Spring Boot приложения.

Сервис Voucher

Переходим на вкладку «Детали» и скачиваем инструмент DataSpace SDK — он был сгенерирован после развёртывания сервиса DataSpace Vouchers.

Создадим проект со стандартной структурой. Для удобства можно взять за основу шаблонный проект в одной из наших функций-заготовок. Для этого в действиях выбираем пункт «Экспортировать»:

При этом добавим в src/libs jar, полученный из скачанного ранее архива.

Также нам потребуется java-sdk-core для подписи REST-запросов при помощи ak/sk. Скачиваем его по ссылке, достаём из архива и добавляем в src/libs нашего проекта.

В pom.xml проекта необходимо добавить следующие зависимости:

Данная зависимость содержит служебные классы, сгенерированные под нашу модель данных.
Это позволяет достичь строгой типизации при написании прикладного кода.

        <dependency>             <groupId>sbp.com.sbt.dataspace</groupId>             <artifactId>m7063364230573391874-model-sdk</artifactId>             <version>0.0.1</version>             <scope>system</scope>             <systemPath>${project.basedir}/src/libs/m7063364230573391874-model-sdk-0.0.3.jar</systemPath>         </dependency>          Зависимости необходимые для работы DataSpace SDK         <dependency>             <groupId>org.apache.httpcomponents</groupId>             <artifactId>httpclient</artifactId>         </dependency>         <dependency>             <groupId>io.projectreactor.netty</groupId>             <artifactId>reactor-netty</artifactId>         </dependency>         <dependency>             <groupId>org.springframework</groupId>             <artifactId>spring-webflux</artifactId>         </dependency>         <dependency>             <groupId>org.apache.commons</groupId>             <artifactId>commons-lang3</artifactId>         </dependency>         <dependency>             <groupId>com.google.guava</groupId>             <artifactId>guava</artifactId>             <version>26.0-jre</version>         </dependency>   Зависимость нужна для осуществления подписи REST-запросов при помощи ak/sk         <dependency>             <groupId>sbp.ts.faas</groupId>             <artifactId>java-sdk-core</artifactId>             <version>3.1.2</version>             <scope>system</scope>             <systemPath>${project.basedir}/src/libs/java-sdk-core-3.1.2.jar</systemPath>         </dependency>   Зависимость необходимая для работы java-sdk-core         <dependency>             <groupId>joda-time</groupId>             <artifactId>joda-time</artifactId>             <version>2.10.3</version>         </dependency> 

Сервис Vouchers будет предоставлять REST API, который принимает на вход промокод и тип подарка. В ответ он отдаёт сообщение с информацией о результате бронирования подарка.

Определим API в нашем контроллере:

@RestController public class VouchersController {     @Autowired     private VouchersService vouchersService;       @RequestMapping(value = "/getGiftByPromoCode")     public ResponseEntity<String> getGiftByPromoCode(@RequestParam String voucherCode, @RequestParam String giftKind) {         return ResponseEntity.ok()                 .contentType(MediaType.TEXT_PLAIN)                 .body(vouchersService.getGift(voucherCode, giftKind));     } }

Перейдём к конфигурации. Определим инстансы DataspaceCorePacketClient и DataspaceCoreSearchClient. Нам понадобится адрес сервиса DataSpace и ak/sk для авторизации на API gateway. Все эти значения мы получаем из соответствующих инфраструктурных переменных DATASPACE_URL, APP_KEY, APP_SECRET.

Также нам потребуется RestTemplate для осуществления вызовов к сервису Gifts:

@Configuration public class Config {       @Value("${DATASPACE_URL}")     private String dataSpaceUrl;     @Value("${APP_KEY}")     private String appKey;     @Value("${APP_SECRET}")     private String appSecret;       @Bean     public RestTemplate restTemplate() {         return new RestTemplate();     }       @Bean     public DataspaceCoreSearchClient searchClient() {         return new DataspaceCoreSearchClient(dataSpaceUrl,                 DataspaceSdkApiClientConfiguration.of(builder ->                         builder                                 .setApiGatewayConfiguration(AKSKApiGatewayConfiguration.of(appKey, appSecret))                 )         );     }       @Bean     public DataspaceCorePacketClient packetClient() {         return new DataspaceCorePacketClient(dataSpaceUrl,                 DataspaceSdkApiClientConfiguration.of(builder ->                         builder                                 .setApiGatewayConfiguration(AKSKApiGatewayConfiguration.of(appKey, appSecret))                 )         );     }

Также нам понадобятся:

  • адрес проекта;

  • appKey;

  • appSecret.

Найти эти значения можно в настройках проекта:

В конфигурационном файле config.yaml определим необходимые настройки:

gifts.url: https://gw-ift-sm.pv-api-test.sbc.space/fn_fa969687_4694_4b3e_a871_5g42q56he710 gifts.appKey: d9ad1de7d38f493793c407061dc1111e gifts.appSecret: a418e8315cf0222fbf4784811fe3dc8a

Перейдём к реализации VoucherService.

Алгоритм заказа подарка по промокоду будет выглядеть так:

  1. Запрос клиента поступает с фронта в сервис Vouchers, который выполняет валидацию промокода.

  2. Если валидация прошла успешно, сервис Vouchers вызывает сервис Gifts по REST.

  3. Сервис Gifts должен найти подходящий подарок и забронировать его либо вернуть ответ, содержащий информацию о том, что доступные подарки отсутствуют.

  4. Сервис Vouchers получает идентификатор подарка, привязывает его к промокоду и отправляет ответ с серийным номером подарка и наименованием компании клиенту.

  5. Если подарок не был найден, сервис отправляет соответствующий ответ клиенту:

    public String getGiftByPromoCode(String code,                                      String giftKind) {         try {             String voucherId = verifyPromoCode(code);               JsonNode giftResponse = getGift(giftKind, voucherId);               JsonNode error = giftResponse.get("error");             if (error != null) {                 return error.textValue();             }               updateVoucher(voucherId, giftResponse.get("giftId").textValue());             return "You have been given a gift from " + giftResponse.get("vendor") + ". Serial number: " + giftResponse.get("serialNumber");         } catch (Exception e) {             LOG.error(e.getMessage());             return e.getMessage();         }     }

В основном методе getGiftsByPromoCode нам необходимо проверить, что переданный промокод валиден. Для этого нужно убедиться, что промокод с таким кодом присутствует в базе данных, и он не был использован ранее.

Рассмотрим метод verifyPromoCode:

    public String verifyPromoCode(String code) throws SdkJsonRpcClientException {         try {             VoucherGet voucher = searchClient.getVoucher(voucherWith ->                     voucherWith                             .withCode()                             .withStatusForVoucherMain(StatusWithLinkable::withCode)                             .withGift()                               .setWhere(where -> where.codeEq(code)));               if (voucher.getGift().getEntityId() != null ||                     !voucher.getStatusForVoucherMain().getCode().equals(VoucherVoucherMainStatus.OPEN.getValue())) {                 throw new GiftAlreadyIssuedException(code);             }               return voucher.getObjectId();         } catch (ObjectNotFoundException objectNotFoundException) {             throw new VoucherNotFoundException(code);         }     }

Метод DataspaceCoreSearchClient#getVoucher из состава DataSpace SDK позволяет построить в типизированном формате запрос к сервису DataSpace Vouchers.

В лямбда-выражении мы указываем спецификацию с набором полей, которые хотим получить в ответе, а также задаём условие поиска.

Get-метод предполагает возникновение ObjectNotFoundException в случае, если по запросу ничего не нашлось.

Далее нужно убедиться, что у запрашиваемого промокода нет ссылки на уже полученный подарок, а статус — «ОТКРЫТ». В противном случае отправляем сообщение о том, что данный промокод уже был использован.

Мы провели валидацию промокода и получили его идентификатор, теперь нужно забронировать подходящий подарок.

В методе getGift вызовем сервис Gifts по REST. При этом подпишем наш запрос при помощи ключей ak/sk для корректной авторизации на ApiGateway:

    private JsonNode getGift(String giftKind, String voucherId) throws Exception {         final String GET_GIFT_URL = giftsFunctionUrl + GET_GIFT_ENDPOINT;           Request request = new Request();         request.setMethod("GET");         request.setBody("");         request.setKey(appKey);         request.setSecret(appSecret);         request.setUrl(GET_GIFT_URL);         request.addQueryStringParam("voucherId", voucherId);         request.addQueryStringParam("giftKind", giftKind);         new Signer().sign(request);           HttpHeaders requestHeaders = new HttpHeaders();         request.getHeaders().forEach((k, v) -> requestHeaders.put(k, Collections.singletonList(v)));           String urlTemplate = UriComponentsBuilder.fromHttpUrl(GET_GIFT_URL)                 .queryParam("voucherId", "{voucherId}")                 .queryParam("giftKind", "{giftKind}")                 .encode()                 .toUriString();           Map<String, String> params = new HashMap<>();         params.put("voucherId", voucherId);         params.put("giftKind", giftKind);           ResponseEntity<JsonNode> response = restTemplate.exchange(                 urlTemplate, HttpMethod.GET, new HttpEntity<>(requestHeaders), JsonNode.class, params);           return response.getBody();     }

В ответ получаем ошибку, которую пробрасываем на фронт, или атрибуты забронированного подарка.

Если мы получили положительный ответ от Gifts, нужно отметить, что обрабатываемый промокод использован и за ним закреплён подарок.

Рассмотрим метод updateVoucher:

    public void updateVoucher(String voucherId,                               String giftId) throws SdkJsonRpcClientException {         UpdateVoucherParam updateVoucherParam =                 UpdateVoucherParam.create()                         .setStatusForVoucherMain(VoucherVoucherMainStatus.ISSUED)                         .setGift(GiftReference.of(giftId));           Packet updatePacket = new Packet(voucherId);           updatePacket.voucher.update(VoucherRef.of(voucherId), updateVoucherParam);           packetClient.execute(updatePacket);     }

Метод DataspaceCorePacketClient#execute оперирует объектами типа Packet. Packet является реализацией паттерна UnitOfWork. Все команды, содержащиеся в рамках одного Packet, выполняются в одной транзакции на стороне сервиса DataSpace.

Создаём объект Packet. При этом задаём параметр idempotencePacketId — таким образом мы наделяем Packet свойством идемпотентности.

IdempotencePacketId выступает ключом идемпотентности. Это означает, что на все последующие вызовы Packet c таким же ключом DataSpace вернёт результат, который был получен при первом успешном вызове. При этом сами операции изменения состояния БД выполнены не будут. В качестве ключа идемпотентности используем идентификатор сущности Voucher.

Добавляем в Packet команду update сущности Voucher. При этом указываем идентификатор сущности, а также значения полей, которые нужно установить.

Вызываем метод DataspaceCorePacketClient#execute, чтобы отправить запрос в DataSpace.

В методе getGiftByPromoCode отправляем на фронт сообщение о полученном подарке или ошибку.

Сервис Gifts

Скачиваем jar с DataSpace SDK, но на этот раз из сервиса DataSpace Gifts:

Создаём проект, подключаем зависимости точно так же, как и в случае с сервисом Vouchers:

Сервис Gift будет предоставлять REST API, который принимает на вход идентификатор промокода и тип подарка.

В ответ он отдаёт JSON, в котором содержится информация о забронированном подарке или ошибка.

Определим API в нашем контроллере:

@RestController public class GiftsController {       @Autowired     private GiftsService giftsService;       @RequestMapping(value = "/getGift")     public ResponseEntity<JsonNode> getGift(@RequestParam String voucherId, @RequestParam String giftKind) {         return ResponseEntity.ok()                 .contentType(MediaType.APPLICATION_JSON)                 .body(giftsService.getGift(voucherId, giftKind));     } }

Определим инстансы DataspaceCorePacketClient и DataspaceCoreSearchClient. Получаем необходимые параметры из соответствующих инфраструктурных переменных DATASPACE_URL, APP_KEY, APP_SECRET.

@Configuration public class Config {     @Value("${DATASPACE_URL}")     private String dataSpaceUrl;     @Value("${APP_KEY}")     private String appKey;     @Value("${APP_SECRET}")     private String appSecret;       @Bean     public DataspaceCoreSearchClient searchClient() {         return new DataspaceCoreSearchClient(dataSpaceUrl,                 DataspaceSdkApiClientConfiguration.of(builder ->                         builder                                 .setApiGatewayConfiguration(AKSKApiGatewayConfiguration.of(appKey, appSecret))                 )         );     }       @Bean     public DataspaceCorePacketClient packetClient() {         return new DataspaceCorePacketClient(dataSpaceUrl,                 DataspaceSdkApiClientConfiguration.of(builder ->                         builder                                 .setApiGatewayConfiguration(AKSKApiGatewayConfiguration.of(appKey, appSecret))                 )         );     } }

Перейдём к реализации GiftsService.

Рассмотрим основной метод GiftsService#getGift:

    public JsonNode getGift(String voucherId, String kind) {         ObjectNode response = objectMapper.createObjectNode();         try {             updateRequestCount(voucherId, kind);             GraphCollection<GiftGet> gifts = searchClient.searchGift(giftWith ->                     giftWith                             .withKind()                             .withVendor(GiftVendorWithLinkable::withName)                             .withSerialNumber()                             .setWhere(where ->                                     where                                             .kindEq(GiftKind.valueOf(kind))                                             .and(where.voucherIsNull().or(where.voucherEq(voucherId)))                             )             );               if (gifts.isEmpty()) {                 LOG.error("Available gift not found");                 response.put("error", "Available gift not found");                 return response;             }               GiftGet gift = gifts.get(0);             String giftId = gift.getObjectId();               Packet packet = new Packet(giftId);             packet.gift.update(GiftRef.of(giftId),                     update -> update                             .setVoucher(VoucherReference.of(voucherId)));               packetClient.execute(packet);               response.put("giftId", giftId);             response.put("vendor", gift.getVendor().getName());             response.put("serialNumber", gift.getSerialNumber());           } catch (IdempotencyException idempotencyException) {             LOG.error(idempotencyException.getMessage());             return getGift(voucherId, kind);           } catch (Exception exception) {             LOG.error(exception.getMessage());             response.put("error", exception.getMessage());         }           return response;     }

Разберём его детально. 

В сервисе Gifts помимо самих подарков и компаний ведётся сущность GiftRequestCounter, которая хранит количество поступивших запросов для каждого типа подарка.

Предполагается, что она будет использована в аналитических отчётах:

    private void updateRequestCount(String voucherId, String kind) {         String idempotencePacketId = voucherId + kind;         Packet packet = new Packet(idempotencePacketId);           CreateGiftRequestCounterParam createGiftRequestCounterParam =                 CreateGiftRequestCounterParam.create()                         .setKind(GiftKind.valueOf(kind))                         .setLastRequest(LocalDateTime.now());           GiftRequestCounterRef giftRequestCounter = packet.giftRequestCounter.updateOrCreate(                 createGiftRequestCounterParam, KeyGiftRequestCounter.KIND);           UpdateGiftRequestCounterReq updateGiftRequestCounterReq =                 UpdateGiftRequestCounterReq.create()                         .setInc(IncGiftRequestCounterParam.create().setCounter(1));           packet.giftRequestCounter.update(giftRequestCounter, updateGiftRequestCounterReq);           packetClient.executeAsync(packet).subscribe();     }

В методе updateRequestCount мы отправляем асинхронно запрос на увеличение счётчика GiftRequestCounter в сервис DataSpace Gifts.

Создаём Packet с ключом идемпотентности, состоящим из идентификатора промокода и типа подарка, чтобы избежать лишнего накручивания счётчика при ретраях.

В Packet добавляем команду UpdateOrCreate. Эта команда позволяет за один вызов проверить наличие сущности в БД и обновить её, а если сущности нет, то создать. Также мы добавляем команду update с установленным параметром на увеличение счётчика. Затем отправляем запрос асинхронно при помощи метода DataspaceCorePacketClient#executeAsync.

Далее в основном методе сервиса getGift производим поиск доступного подарка, используя метод DataspaceCoreSearchClient#searchGift:

            GraphCollection<GiftGet> gifts = searchClient.searchGift(giftWith ->                     giftWith                             .withKind()                             .withVendor(GiftVendorWithLinkable::withName)                             .withSerialNumber()                             .setWhere(where ->                                     where                                             .kindEq(GiftKind.valueOf(kind))                                             .and(where.voucherIsNull().or(where.voucherEq(voucherId)))                             )             );

Если доступные подарки не были найдены, формируем ответ с ошибкой:

            if (gifts.isEmpty()) {                 LOG.error("Available gift not found");                 response.put("error", "Available gift not found");                 return response;             }

Если подходящие подарки нашлись, нам необходимо забронировать один из них, установив ссылку на соответствующий ваучер.

Снова воспользуемся функционалом DataspaceCorePacketClient. Создадим Packet и добавим в него команду на обновление сущности Gift.

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

Данный подход позволяет нам не допустить ситуацию, в которой один и тот же подарок будет забронирован для нескольких разных ваучеров, а также избежать выполнения лишних операций при ретраях:

GiftGet gift = gifts.get(0);             String giftId = gift.getObjectId();                                      Packet packet = new Packet(giftId);             packet.gift.update(GiftRef.of(giftId),                     update -> update                             .setVoucher(VoucherReference.of(voucherId)));               packetClient.execute(packet);

Сервис Reports

Перейдём к реализации сервиса, который предоставляет API для получения отчётов.

Данный сервис будет предоставлять отчёты о подарках, поэтому нам потребуется jar DataSpace SDK из сервиса DataSpace Gifts.

Создадим проект, добавим необходимую зависимость:

Реализуем API получения следующего отчёта:

Компания | тип подарка | кол-во подарков:

@RestController public class ReportController {     @Autowired     private ReportService reportService;       @RequestMapping(value = "/getGiftsReport")     public ResponseEntity<JsonNode> getGiftsReport() {         return ResponseEntity.ok()                 .contentType(MediaType.APPLICATION_JSON)                 .body(reportService.getGiftsReport());     } }

Рассмотрим реализацию основного метода ReportService#getGiftsReport с применением DataSpace SDK:

    public JsonNode getGiftsReport() {         ObjectNode response = objectMapper.createObjectNode();         try {               SelectionWith<? extends GiftGrasp> selectionWith = GiftGraph.createSelection()                     .$withGroup("vendor", groupSelector -> groupSelector.none(giftGrasp -> giftGrasp.vendor().name()))                     .$withGroup("kind", groupSelector -> groupSelector.none(GiftGrasp::kind))                     .$withGroup("giftsCount", groupSelector -> groupSelector.count(GiftGrasp::kind))                       .$addGroupBy(groupBy -> groupBy.vendor().name())                       .$addGroupBy(GiftGrasp::kind);               GraphCollection<Selection> selections = searchClient.selectionSearch(selectionWith);               ArrayNode reportRows = objectMapper.createArrayNode();             selections.forEach(selection -> {                 ObjectNode objectNode = objectMapper.createObjectNode();                 objectNode.put("vendor", selection.$getCalculated("vendor", String.class));                 objectNode.put("kind", selection.$getCalculated("kind", String.class));                 objectNode.put("giftsCount", selection.$getCalculated("giftsCount", Integer.class));                 reportRows.add(objectNode);             });             response.set("report", reportRows);           } catch (SdkJsonRpcClientException e) {             LOG.error(e.getMessage());             response.put("error", e.getMessage());         }           return response;     }

Конструкция SelectionWith позволяет построить запрос с группировками.

Метод $withGroup первым параметром принимает алиас поля, который будет отображён в результирующей выборке. Вторым параметром $withGroup принимает groupSelector, который позволяет указать выражение, на основе которого будут получены данные, будь то значение поля как оно есть или агрегирующая функция.

При помощи метода $addGroupBy мы добавляем поля, по которым будет выполнена группировка.

После формирования объекта SelectionWith выполняем вызов DataspaceCoreSearchClient#selectionSearch. Формируем JSON-ответ. Метод Selection#$getCalculated позволяет получить данные из объекта Selection, а также привести их к требуемому типу данных.

Публикация функций и тестирование

Приложения готовы, теперь необходимо упаковать каждое в zip-архив и загрузить в соответствующую функцию в SmartMarket Studio:

Затем жмём кнопку «Опубликовать» и ждём, пока функции задеплоятся.

После успешного деплоя на вкладке «Тестирование» мы можем проверить работоспособность наших API:

Итог

С помощью Platform V Functions и DataSpace SDK мы создали и развернули два полноценных микросервиса:

  • Подарки:

a) Ведение компаний-спонсоров и их подарков.

b)  Аналитический учёт пользовательских запросов.

  • ·Промоакции:

a)  Ведение промоакций и ваучеров в рамках сервиса.

b) Резервирование подарков в рамках промоакций (интеграция с сервисом «Подарки»).

В следующих статьях подробнее раскроем фичи и возможности Platform V Functions и расскажем, как ещё можно сократить время на разработку и реализовать микросервисный подход, используя инструменты Platform V.


ссылка на оригинал статьи https://habr.com/ru/company/sberbank/blog/662397/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *