«1. Обзор

Helidon — это новая среда микросервисов Java, исходный код которой недавно был открыт Oracle. Он использовался внутри проектов Oracle под названием J4C (Java for Cloud).

В этом руководстве мы рассмотрим основные концепции фреймворка, а затем перейдем к созданию и запуску микросервиса на основе Helidon.

2. Модель программирования

В настоящее время фреймворк поддерживает две модели программирования для написания микросервисов: Helidon SE и Helidon MP.

В то время как Helidon SE разработан как микрофреймворк, поддерживающий модель реактивного программирования, Helidon MP, с другой стороны, представляет собой среду выполнения Eclipse MicroProfile, которая позволяет сообществу Jakarta EE запускать микросервисы переносимым способом.

В обоих случаях микросервис Helidon представляет собой приложение Java SE, которое запускает крошечный HTTP-сервер из основного метода.

3. Helidon SE

В этом разделе мы более подробно рассмотрим основные компоненты Helidon SE: WebServer, Config и Security.

3.1. Настройка веб-сервера

Чтобы начать работу с API веб-сервера, нам нужно добавить необходимую зависимость Maven в файл pom.xml:

<dependency>
    <groupId>io.helidon.webserver</groupId>
    <artifactId>helidon-webserver</artifactId>
    <version>0.10.4</version>
</dependency>

Чтобы получить простое веб-приложение, мы можем использовать одно из следующих методы построителя: WebServer.create(serverConfig, маршрутизация) или просто WebServer.create(маршрутизация). Последний использует конфигурацию сервера по умолчанию, позволяющую серверу работать на произвольном порту.

Вот простое веб-приложение, работающее на предопределенном порту. Мы также зарегистрировали простой обработчик, который будет отвечать приветственным сообщением на любой HTTP-запрос с путем «/greet» и методом GET:

public static void main(String... args) throws Exception {
    ServerConfiguration serverConfig = ServerConfiguration.builder()
      .port(9001).build();
    Routing routing = Routing.builder()
      .get("/greet", (request, response) -> response.send("Hello World !")).build();
    WebServer.create(serverConfig, routing)
      .start()
      .thenAccept(ws ->
          System.out.println("Server started at: http://localhost:" + ws.port())
      );
}

Последняя строка — запуск сервера и ожидание обслуживания HTTP-запросов. . Но если мы запустим этот пример кода в основном методе, мы получим ошибку:

Exception in thread "main" java.lang.IllegalStateException: 
  No implementation found for SPI: io.helidon.webserver.spi.WebServerFactory

Веб-сервер на самом деле является SPI, и нам нужно предоставить реализацию во время выполнения. В настоящее время Helidon предоставляет реализацию NettyWebServer, основанную на Netty Core.

Вот зависимость Maven для этой реализации:

<dependency>
    <groupId>io.helidon.webserver</groupId>
    <artifactId>helidon-webserver-netty</artifactId>
    <version>0.10.4</version>
    <scope>runtime</scope>
</dependency>

Теперь мы можем запустить основное приложение и проверить его работу, вызвав настроенную конечную точку:

http://localhost:9001/greet

В этом примере мы настроили оба порт и путь, используя шаблон построителя.

Helidon SE также позволяет использовать шаблон конфигурации, в котором данные конфигурации предоставляются API конфигурации. Это тема следующего раздела.

3.2. Config API

Config API предоставляет инструменты для чтения данных конфигурации из источника конфигурации.

Helidon SE предоставляет реализации для многих источников конфигурации. Реализация по умолчанию предоставляется helidon-config, где источником конфигурации является файл application.properties, расположенный в пути к классам:

<dependency>
    <groupId>io.helidon.config</groupId>
    <artifactId>helidon-config</artifactId>
    <version>0.10.4</version>
</dependency>

Чтобы прочитать данные конфигурации, нам просто нужно использовать сборщик по умолчанию, который по умолчанию принимает конфигурацию данные из application.properties:

Config config = Config.builder().build();

Давайте создадим файл application.properties в каталоге src/main/resource со следующим содержимым:

server.port=9080
web.debug=true
web.page-size=15
user.home=C:/Users/app

Чтобы прочитать значения, мы можем использовать Config.get( ) с последующим удобным приведением к соответствующим типам Java:

int port = config.get("server.port").asInt();
int pageSize = config.get("web.page-size").asInt();
boolean debug = config.get("web.debug").asBoolean();
String userHome = config.get("user.home").asString();

Фактически, сборщик по умолчанию загружает первый найденный файл в таком порядке приоритета: application.yaml, application.conf, application.json и application. характеристики. Для последних трех форматов требуется дополнительная связанная зависимость конфигурации. Например, чтобы использовать формат YAML, нам нужно добавить соответствующую зависимость конфигурации YAML:

<dependency>
    <groupId>io.helidon.config</groupId>
    <artifactId>helidon-config-yaml</artifactId>
    <version>0.10.4</version>
</dependency>

Затем мы добавляем application.yml:

server:
  port: 9080  
web:
  debug: true
  page-size: 15
user:
  home: C:/Users/app

Аналогично, чтобы использовать CONF, который упрощенный формат JSON или форматы JSON, нам нужно добавить зависимость helidon-config-hocon.

Обратите внимание, что данные конфигурации в этих файлах могут быть переопределены переменными среды и свойствами системы Java.

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

ConfigSource configSource = ConfigSources.classpath("application.yaml").build();
Config config = Config.builder()
  .disableSystemPropertiesSource()
  .disableEnvironmentVariablesSource()
  .sources(configSource)
  .build();

«

«Помимо чтения данных конфигурации из пути к классам, мы также можем использовать конфигурации из двух внешних источников, то есть конфигурации git и etcd. Для этого нам понадобятся зависимости helidon-config-git и helidon-git-etcd.

Наконец, если все эти источники конфигурации не удовлетворяют наши потребности, Helidon позволяет нам предоставить реализацию для нашего источника конфигурации. Например, мы можем предоставить реализацию, которая может считывать данные конфигурации из базы данных.

3.3. API маршрутизации

API маршрутизации предоставляет механизм, с помощью которого мы связываем HTTP-запросы с методами Java. Мы можем добиться этого, используя метод и путь запроса в качестве критериев соответствия или объект RequestPredicate для использования дополнительных критериев.

Routing routing = Routing.builder()
  .get((request, response) -> {} );

Итак, для настройки маршрута мы можем просто использовать метод HTTP в качестве критерия:

Routing routing = Routing.builder()
  .get("/path", (request, response) -> {} );

Или мы можем объединить метод HTTP с путем запроса:

Routing routing = Routing.builder()
  .post("/save",
    RequestPredicate.whenRequest()
      .containsHeader("header1")
      .containsCookie("cookie1")
      .accepts(MediaType.APPLICATION_JSON)
      .containsQueryParameter("param1")
      .hasContentType("application/json")
      .thenApply((request, response) -> { })
      .otherwise((request, response) -> { }))
      .build();

Мы также можем использовать RequestPredicate для большего контроля. Например, мы можем проверить существующий заголовок или тип содержимого:

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

public class Book {
    private String id;
    private String name;
    private String author;
    private Integer pages;
    // ...
}

Итак, давайте сначала создадим модель для объекта, с которым мы работаем, класса Book:

public class BookResource implements Service {

    private BookManager bookManager = new BookManager();

    @Override
    public void update(Routing.Rules rules) {
        rules
          .get("/", this::books)
          .get("/{id}", this::bookById);
    }

    private void bookById(ServerRequest serverRequest, ServerResponse serverResponse) {
        String id = serverRequest.path().param("id");
        Book book = bookManager.get(id);
        JsonObject jsonObject = from(book);
        serverResponse.send(jsonObject);
    }

    private void books(ServerRequest serverRequest, ServerResponse serverResponse) {
        List<Book> books = bookManager.getAll();
        JsonArray jsonArray = from(books);
        serverResponse.send(jsonArray);
    }
    //...
}

Мы можем создать службы REST для класса Book, реализуя метод Service.update(). Это позволяет настроить подпути одного и того же ресурса:

<dependency>
    <groupId>io.helidon.webserver</groupId>
    <artifactId>helidon-webserver-json</artifactId>
    <version>0.10.4</version>
</dependency>

Мы также настроили тип носителя как JSON, поэтому для этой цели нам нужна зависимость helidon-webserver-json:

Routing routing = Routing.builder()
  .register(JsonSupport.get())
  .register("/books", new BookResource())
  .build();

Наконец, мы используйте метод register() построителя маршрутизации, чтобы привязать корневой путь к ресурсу. В этом случае пути, настроенные службой, начинаются с корневого пути:

http://localhost:9080/books
http://localhost:9080/books/0001-201810

Теперь мы можем запустить сервер и проверить конечные точки:

3.4. Безопасность

В этом разделе мы собираемся защитить наши ресурсы с помощью модуля «Безопасность».

<dependency>
    <groupId>io.helidon.security</groupId>
    <artifactId>helidon-security</artifactId>
    <version>0.10.4</version>
</dependency>
<dependency>
    <groupId>io.helidon.security</groupId>
    <artifactId>helidon-security-provider-http-auth</artifactId>
    <version>0.10.4</version>
</dependency>
<dependency>
    <groupId>io.helidon.security</groupId>
    <artifactId>helidon-security-integration-webserver</artifactId>
    <version>0.10.4</version>
</dependency>

Начнем с объявления всех необходимых зависимостей:

Зависимости helidon-security, helidon-security-provider-http-auth и helidon-security-integration-webserver доступны в Maven Central.

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

Map<String, MyUser> users = //...
UserStore store = user -> Optional.ofNullable(users.get(user));

HttpBasicAuthProvider httpBasicAuthProvider = HttpBasicAuthProvider.builder()
  .realm("myRealm")
  .subjectType(SubjectType.USER)
  .userStore(store)
  .build();

Security security = Security.builder()
  .addAuthenticationProvider(httpBasicAuthProvider)
  .build();

Первое, что нужно сделать, это создать экземпляр Security. Мы можем сделать это либо программно для простоты:

Или мы можем использовать конфигурационный подход.

#Config 4 Security ==> Mapped to Security Object
security:
  providers:
  - http-basic-auth:
      realm: "helidon"
      principal-type: USER # Can be USER or SERVICE, default is USER
      users:
      - login: "user"
        password: "user"
        roles: ["ROLE_USER"]
      - login: "admin"
        password: "admin"
        roles: ["ROLE_USER", "ROLE_ADMIN"]

  #Config 4 Security Web Server Integration ==> Mapped to WebSecurity Object
  web-server:
    securityDefaults:
      authenticate: true
    paths:
    - path: "/user"
      methods: ["get"]
      roles-allowed: ["ROLE_USER", "ROLE_ADMIN"]
    - path: "/admin"
      methods: ["get"]
      roles-allowed: ["ROLE_ADMIN"]

В этом случае мы объявим всю конфигурацию безопасности в файле application.yml, который мы загружаем через Config API:

Config config = Config.create();
Security security = Security.fromConfig(config);

И чтобы загрузить его, нам нужно просто создать объект Config, а затем мы вызываем метод Security.fromConfig():

Routing routing = Routing.builder()
  .register(WebSecurity.from(security).securityDefaults(WebSecurity.authenticate()))
  .build();

Когда у нас есть экземпляр Security, нам сначала нужно зарегистрировать его на веб-сервере с помощью метода WebSecurity.from():

Routing routing = Routing.builder()        
  .register(WebSecurity.from(config))
  .build();

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

Routing routing = Routing.builder()
  .register(WebSecurity.from(config))
  .get("/user", (request, response) -> response.send("Hello, I'm Helidon SE"))
  .get("/admin", (request, response) -> response.send("Hello, I'm Helidon SE"))
  .build();

Теперь мы можем добавить несколько обработчиков для путей /user и /admin, запустить сервер и попытаться получить доступ их:

4. Helidon MP

Helidon MP представляет собой реализацию Eclipse MicroProfile, а также предоставляет среду выполнения для запуска микросервисов на основе MicroProfile.

Поскольку у нас уже есть статья о Eclipse MicroProfile, мы проверим этот исходный код и изменим его для работы на Helidon MP.

<dependency>
    <groupId>io.helidon.microprofile.bundles</groupId>
    <artifactId>helidon-microprofile-1.2</artifactId>
    <version>0.10.4</version>
</dependency>
<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-json-binding</artifactId>
    <version>2.26</version>
</dependency>

После проверки кода мы удалим все зависимости и плагины и добавим зависимости Helidon MP в файл POM:

Зависимости helidon-microprofile-1.2 и jersey-media-json-binding доступны в Maven Central.

<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns="http://xmlns.jcp.org/xml/ns/javaee"
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
  http://xmlns.jcp.org/xml/ns/javaee/beans_2_0.xsd"
  version="2.0" bean-discovery-mode="annotated">
</beans>

Далее мы добавим файл beans.xml в каталог src/main/resource/META-INF со следующим содержимым:

@Override
public Set<Class<?>> getClasses() {
    return CollectionsHelper.setOf(BookEndpoint.class);
}

В классе LibraryApplication переопределите метод getClasses(), чтобы сервер не будет сканировать ресурсы:

public static void main(String... args) {
    Server server = Server.builder()
      .addApplication(LibraryApplication.class)
      .port(9080)
      .build();
    server.start();
}

Наконец, создайте основной метод и добавьте этот фрагмент кода:

«

И все. Теперь мы сможем вызывать все ресурсы книги.

5. Заключение