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

ДинамоДБ Клиент, использующий Micronaut, Maven и GraalVM

1. Обзор Это будет простая практическая статья, в которой я покажу, как ее реализовать… С тегами java, aws, микросервисы, micronaut.

Это будет простая практическая статья, в которой я покажу, как реализовать простой клиент 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) {
        Optional secretKey = 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 Maybe getEvent(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 Maybe getEvent(@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”