ym88659208ym87991671
Пример использования DataSpace SDK с функцией на Java | Документация для разработчиков
Skip to main content

Пример использования DataSpace SDK с функцией на Java

Обновлено 13 сентября 2022

Вступление

В инструкции Пример использования DataSpace с функцией на JavaScript показано, как можно быстро разработать приложение, применив микросервисный подход с использованием продуктов Platform V DataSpace и Functions. В примере был использован TypeScript, но данное требование не обязательно.

В данной же инструкции описана возможность разработки backend-приложений на языке Java при использовании сервиса Platform V Functions и инструмента DataSpace SDK.

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

Подробное описание работы с DataSpace SDK можно найти в разделе Использование DataSpace SDK.

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

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

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

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

На схеме:

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

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

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

Разработка

Для начала необходимо развернуть два сервиса DataSpace Vouchers и DataSpace Gifts в Studio. Подробнее о том, как это сделать, описано в статье «Platform V: микросервис за 15 минут» в разделе Работа в Studio.

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

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

После того как сервисы DataSpace будут развернуты, необходимо создать заготовки для сервисов. Создадим в Studio три Java-функции: Vouchers Function, Gifts Function, Reports Function.

Vouchers Function:

Gifts Function:

Reports Function:

Далее перейдем к разработке внутренней части данных функций. Они будут представлять собой Spring Boot приложения.

Сервис Vouchers

После того, как сервис DataSpace Vouchers был развернут, был также сгенерирован инструмент DataSpace SDK. Необходимо перейти на вкладку Детали и скачать его:

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

При этом необходимо добавить в 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>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>1.39.0</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.15.8</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))
)
);
}

В конфигурационном файле config.yaml необходимо определить настройку, которая будет содержать адрес сервиса Gifts Function:

gifts.url: https://gw-ift-sm.pv-api-test.sbc.space/fn_fa969687_4694_4b3e_a871_8e76d13aa928

Чтобы узнать URL функции Gifts Function, после ее создания необходимо перейти на вкладку Детали:

Далее перейдем к реализации VoucherService.

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

  1. Запрос клиента поступает с front в сервис 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();
}

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

Реализацию самого сервиса Gifts рассмотрим позднее.

Если от 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-архив и загрузить в соответствующую функцию в Studio:

Затем необходимо нажать кнопку Опубликовать и дождаться деплоя функции.

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

Заметили ошибку?

Выделите текст и нажмите Ctrl + Enter, чтобы сообщить нам о ней