Это будет простая практическая статья, в которой я покажу, как реализовать простой клиент rest DynamoDB с использованием Micronaut Framework и Maven, создайте собственный образ с помощью виртуальной машины Graal и простого сравнения использования ресурсов между клиентами на Spring Boot и на Micronaut с GraalVM.
Для тех, кто не знаком с Micronaut – это фреймворк для создания микросервисов и бессерверных приложений. Одно из ключевых различий между Spring Boot и Micronauts заключается в том, что Micronaut не использует отражение для выполнения IoC, поэтому время запуска приложения и потребление памяти не привязаны к размеру кодовой базы проекта.
Таким образом, наша задача – обрабатывать HTTP-запросы для извлечения или хранения некоторого События (идентификатор: строка, тело: строка)
. События будут храниться в DynamoDB.
Возможно, было бы проще просто посмотреть код на Github и следовать ему там.
Давайте начнем с зависимостей среды выполнения Maven для Micronaut и DynamoDB Пакет SDK
io.micronaut micronaut-bom ${micronaut.version} pom import io.micronaut micronaut-inject-java io.micronaut micronaut-runtime io.micronaut micronaut-http-server-netty com.amazonaws aws-java-sdk-dynamodb 1.11.762
Поскольку Micronaut не использует обработку отражения/аннотаций во время запуска, но делает это во время сборки – нам нужно добавить процессоры аннотаций в плагин maven-компилятора.
org.apache.maven.plugins maven-compiler-plugin 3.8.1 -parameters io.micronaut micronaut-inject-java ${micronaut.version} io.micronaut micronaut-validation ${micronaut.version} test-compile testCompile -parameters io.micronaut micronaut-inject-java ${micronaut.version} io.micronaut micronaut-validation ${micronaut.version}
3.1 Конфигурация
Простая конфигурация, в которой мы настраиваем подключение к DynamoDB. Для целей тестирования нам необходимо указать конечная точка динамо
.В случае реального приложения нам нужно указать регион вместо конечной точки.
@Factory public class Config { @Bean AmazonDynamoDBAsync dynamoDbAsyncClient(Environment environment) { OptionalsecretKey = environment.get("aws.secretkey", String.class); Optional accessKey = environment.get("aws.accesskey", String.class); String endpoint = environment.get("dynamo.endpoint", String.class, "http://localhost:8000"); if (!secretKey.isPresent() || !accessKey.isPresent()) { throw new IllegalArgumentException("Aws credentials not provided"); } BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey.get(), secretKey.get()); AmazonDynamoDBAsyncClientBuilder clientBuilder = AmazonDynamoDBAsyncClientBuilder.standard() .withCredentials(new AWSStaticCredentialsProvider(credentials)) .withEndpointConfiguration( new AwsClientBuilder.EndpointConfiguration(endpoint, null) ); return clientBuilder.build(); } }
3.2 Асинхронный динамический модуль Услуга
Простой сервис для сохранения/извлечения события в/из DynamoDB. Все асинхронные запросы к aws упакованы в конструкции RxJava для удобства обработки фьючерсов.
@Singleton public class DynamoDBService { public static final String TABLE_NAME = "events"; public static final String ID_COLUMN = "id"; public static final String BODY_COLUMN = "body"; private final AmazonDynamoDBAsync client; public DynamoDBService(AmazonDynamoDBAsync client) { this.client = client; } //Create DynamoDB table if not exists @PostConstruct public void createTableIfNotExists() { if (!isTableExists()) { createTable(); } } public MaybegetEvent(String eventId) { Map searchCriteria = new HashMap<>(); searchCriteria.put(ID_COLUMN, new AttributeValue().withS(eventId)); // Building request to get event by Id GetItemRequest request = new GetItemRequest() .withTableName(TABLE_NAME) .withKey(searchCriteria) .withAttributesToGet(BODY_COLUMN); // lets retrieve only body as id we already have return Maybe.fromFuture(client.getItemAsync(request)) .subscribeOn(Schedulers.io()) .filter(result -> result.getItem() != null) // check that request returned something .map(result -> new Event(eventId, result.getItem().get(BODY_COLUMN).getS())); //building Event from response } public Single saveEvent(String eventBody) { String id = UUID.randomUUID().toString(); Map item = new HashMap<>(); item.put(ID_COLUMN, new AttributeValue().withS(id)); item.put(BODY_COLUMN, new AttributeValue().withS(eventBody)); PutItemRequest putRequest = new PutItemRequest() .withTableName(TABLE_NAME) .withItem(item); return Single.fromFuture(client.putItemAsync(putRequest)) .subscribeOn(Schedulers.io()) .map(result -> id); } private boolean isTableExists() { ListTablesRequest tablesRequest = new ListTablesRequest() .withExclusiveStartTableName(TABLE_NAME); ListTablesResult result = client.listTables(tablesRequest); return result.getTableNames().contains(TABLE_NAME); } private CreateTableResult createTable() { KeySchemaElement keyDefinitions = new KeySchemaElement() .withAttributeName(ID_COLUMN) .withKeyType(KeyType.HASH); AttributeDefinition keyType = new AttributeDefinition() .withAttributeName(ID_COLUMN) .withAttributeType(ScalarAttributeType.S); CreateTableRequest request = new CreateTableRequest() .withTableName(TABLE_NAME) .withKeySchema(keyDefinitions) .withAttributeDefinitions(keyType) .withBillingMode(BillingMode.PAY_PER_REQUEST); return client.createTable(request); } }
Здесь мы представим наш REST Api с помощью метода GET для извлечения события из DynamoDB и POST для хранения события.
@Controller("/event") public class SimpleController { private final DynamoDBService dynamoDBService; public SimpleController(DynamoDBService dynamoDBService) { this.dynamoDBService = dynamoDBService; } @Get("/{eventId}") @Produces(MediaType.APPLICATION_JSON) public MaybegetEvent(@PathVariable String eventId) { Maybe event = dynamoDBService.getEvent(eventId); return event; } @Post("/") @Produces(MediaType.APPLICATION_JSON) public Single saveEvent(@Body String body) { Single event = dynamoDBService.saveEvent(body); return event; } }
5.1 Зависимости Maven
Для запуска интеграционного теста с DynamoDB нам нужна локальная зависимость DynamoDB, которая на самом деле не является DynamoDB, а SQLite с реализованными интерфейсами DynamoDB поверх нее.
com.amazonaws DynamoDBLocal 1.12.0 test io.micronaut micronaut-http-client test io.micronaut.test micronaut-test-junit5 test org.junit.jupiter junit-jupiter-api 5.6.0 test org.apache.maven.plugins maven-dependency-plugin 2.10 copy test-compile copy-dependencies test so,dll,dylib ${project.basedir}/target/native-libs dynamodb-local-oregon DynamoDB Local Release Repository https://s3-us-west-2.amazonaws.com/dynamodb-local/release
5.2 Сервер DynamoDB
Теперь нам нужно запустить DynamoDB перед тестовыми запусками, мы можем сделать это с помощью расширения jupiter.
public class LocalDynamoDbExtension implements AfterAllCallback, BeforeAllCallback { protected DynamoDBProxyServer server; public LocalDynamoDbExtension() { //here we set the path from "outputDirectory" of maven-dependency-plugin System.setProperty("sqlite4java.library.path", "target/native-libs"); } @Override public void afterAll(ExtensionContext extensionContext) throws Exception { stopUnchecked(server); } @Override public void beforeAll(ExtensionContext extensionContext) throws Exception { this.server = ServerRunner .createServerFromCommandLineArgs(new String[]{"-inMemory", "-port", "8000"}); server.start(); } protected void stopUnchecked(DynamoDBProxyServer dynamoDbServer) { try { dynamoDbServer.stop(); } catch (Exception e) { throw new RuntimeException(e); } } }
5.3 Запуск теста
Теперь мы можем создать интеграционный тест и проверить, делают ли наши методы REST Api то, что мы думаем.
@MicronautTest @ExtendWith(LocalDynamoDbExtension.class) public class SimpleControllerTest { @Inject @Client("/event") RxStreamingHttpClient client; @Inject DynamoDBService dynamoDBService; @Test public void getEventsTest() { //add event to database so we can query it via http String eventBody = "testMessage"; String eventId = dynamoDBService.saveEvent(eventBody).blockingGet(); HttpRequest request = HttpRequest.GET(eventId); HttpResponse> rsp = client.toBlocking().exchange(request, Argument.listOf(Event.class)); assertEquals(HttpStatus.OK, rsp.getStatus()); List
body = rsp.body(); assertEquals(1, body.size()); assertEquals(eventBody, body.get(0).getBody()); } @Test public void saveEventTest() { HttpRequest request = HttpRequest.POST("/", "postBody"); HttpResponse rsp = client.toBlocking().exchange(request, Argument.of(String.class)); Optional id = rsp.getBody(); assertTrue(id.isPresent()); Event event = dynamoDBService.getEvent(id.get()).blockingGet(); assertEquals(id.get(), event.getId()); assertEquals("postBody", event.getBody()); } }
Использование Виртуальной машины Graal мы можем создать заранее скомпилированный собственный образ, который очень полезен для небольших приложений. Собственный образ включает в себя классы приложения, классы из его зависимостей, классы из JDK. Он не запускается на JVM. Таким образом, в итоге вы получите автономный исполняемый образ, который вы можете запустить без какой-либо JVM.
Поскольку изображение уже скомпилировано, связано и частично инициализировано, оно запустится быстрее, и вы получите меньший объем памяти. Но имейте в виду, что за это есть цена – отсутствие JIT-компилятора, гораздо более простой GC (SerialGC), зависящий от платформы, сложный в использовании фреймворк, который в значительной степени зависит от отражения (Spring Framework).
Вы можете создать образ несколькими способами:
6.1 Создание образа с помощью многоступенчатого докера
Одним из доказательств является то, что вам нужно знать путь к классам приложения или загрузить все зависимости в папку и указать на нее во время создания образа.
FROM oracle/graalvm-ce:20.0.0-java11 as graalvm RUN gu install native-image COPY . /home/app/micronaut-dynamodb-client WORKDIR /home/app/micronaut-dynamodb-client RUN native-image --no-server -cp all-runtime-deps.jar FROM frolvlad/alpine-glibc RUN apk update && apk add libstdc++ EXPOSE 8080 COPY --from=graalvm /home/app/micronaut-dynamodb-client/micronaut-dynamodb-client /srv/micronaut-dynamodb-client ENTRYPOINT ["/srv/micronaut-dynamodb-client", "-Xmx68m"]
6.2 Создание образа с помощью Maven
Немного сложнее, чем с докером. Вам необходимо установить JDK GrallVM, установить инструмент собственного образа, установить GraalVM в качестве JDK для вашего проекта. После этого вы можете добавить плагин в maven, и плагин выполнит эту работу.
org.graalvm.nativeimage native-image-maven-plugin 20.0.0 native-image deploy com.yegor.micronaut.dynamodb.App -H:Name=dynamodb-client -H:IncludeResources="logback.xml|application.yml"
Когда собственный образ будет готов, мы сможем создать с его помощью образ Docker.
FROM frolvlad/alpine-glibc RUN apk update && apk add libstdc++ COPY target/micronaut-dynamodb-client /srv/micronaut-dynamodb-client EXPOSE 8080 ENTRYPOINT ["/srv/micronaut-dynamodb-client"]
В качестве примера я возьму приложение spring boot из этого поста, которое в основном делает то же самое, но с помощью Spring.
7.1 Размеры Изображений
Сначала давайте посмотрим на размеры изображений, запустив docker images
REPOSITORY TAG SIZE micronaut-dynamodb-native latest 84.4MB spring-boot-dynamodb latest 364MB
Очевидно, что докер с собственным изображением использует меньше места, потому что собственный образ удалил все, что не будет использоваться, включая JVM
7.2 Запуск приложения
Я запускаю каждое изображение и просто просматриваю журналы, чтобы получить информацию о завершении запуска приложения. Micronaut-Собственное изображение запустилось через 54 мс. Довольно впечатляюще:)
io.micronaut.runtime.Micronaut - Startup completed in 54ms. Server Running: http://5330567cbd7c:8080
Запуск приложения Spring Boot занял гораздо больше времени
com.example.dynamo_spring.App : Started App in 4.093 seconds (JVM running for 4.736)
7.3 Объем памяти
Чтобы распечатать данные о потреблении памяти и процессора, запустите статистику docker
. Но так как я не делаю никаких запросов к изображениям – номера процессоров не имеют значения.
NAME MEM USAGE micronaut-dynamodb-native 12.63MiB spring-boot-dynamodb 152MiB
Ура, вы дошли до конца!
Счастливого кодирования:)
Оригинал: “https://dev.to/byegor/dynamodb-client-using-micronaut-maven-and-graalvm-31fe”