En

JazzTeam Software Development Company

Agile Java Development

Создание Windows службы на Java

Введение

В рамках одного из проектов требовалось разработать Windows службу, которая могла бы выполнять ряд действий с помощью Windows API, Websocket и стандартных средств Java. Далее в статье будут описаны шаги, которые были сделаны для создания такой службы.
Потребность в Windows службе возникла из-за необходимости иметь программу со следующими возможностями:

Создание минимизированной версии JRE

Так как GraalVM всё ещё не поддерживает создание исполняемых файлов под Windows, было решено воспользоваться другими возможностями, которые предоставляет экосистема Java, а именно создание минимизированной версии JRE.

Для того, чтобы создать минимизированную версию JRE, для начала необходимо узнать зависимости на определенные пакеты, которые будут включены в JRE. 

В первую очередь необходимо собрать jar-файл “fat jar” со всеми зависимостями.

Затем выполнить команду jdeps -s <путь к jar-файлу>, чтобы получить список всех зависимостей. Например:

jdeps -s application.jar
application.jar -> java.base
application.jar -> java.datatransfer
application.jar -> java.desktop
application.jar -> java.logging
application.jar -> java.net.http
application.jar -> java.sql
application.jar -> java.xml
application.jar -> jdk.unsupported
application.jar -> not found

Далее создаём нашу версию JRE с данными зависимостями:

jlink –module-path <путь к папке jmods, которая находится в jdk> –add-modules

java.base,java.datatransfer,java.desktop,java.logging,java.net.http,java.sql,java.xml,jdk.unsupported –strip-debug –compress 2 –no-header-files –no-man-pages –output <имя папки, которая будет содержать сгенерированную JRE>

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

После выполнения этих действий JRE будет занимать порядка 30 mb, вместо сотен.

Создание Windows службы из любого приложения

Java не имеет стандартных средств по созданию служб, поэтому были изучены сторонние инструменты и был выбран WinSW в силу его бесплатности и простоты использования.

WinSW

WinSW – это утилита, которая позволяет запустить и обернуть любой процесс как Windows службу. Для того, чтобы начать с ней работать, необходимо скачать исполняемый и конфигурационный файлы по этой ссылке https://github.com/kohsuke/winsw/releases.

Необходимо поместить эти два файла в директорию. Переименовать исполняемый файл на своё усмотрение и дать такое же название файлу конфигурации, затем поместить в эту директорию jar-файл приложения и созданную JRE.

В конфигурационном файле необходимо прописать минимальную конфигурацию:

<configuration>
  <!-- ID of the service. It should be unique across the Windows system-->
  <id>идентификатор службы</id>
  <!-- Display name of the service -->
  <name>имя</name>
  <!-- Service description -->
  <description>Описание</description>
  <!-- Path to the executable, which should be started -->
  <executable>jre\bin\java.exe</executable>
  <arguments>-jar application.jar</arguments>
</configuration>

jre\bin\java.exe – относительный путь внутри нашей папки к исполняемому файлу нашей JRE.

После этих действий можно установить службу, для этого необходимо выполнить команду от имени администратора:

winsw.exe install

Список команд можно посмотреть здесь.

Взаимодействие Java и Windows API

Для использования функций Windows (таких как создание нового процесса или добавление ключей реестра) в нашем приложении был использован JNA.
JNA (Java Native Access) предоставляет Java-программам легкий доступ к библиотекам, написанным на другом языке, без написания чего-либо, кроме кода Java. JNA позволяет напрямую вызывать нативные функции, используя обычный вызов метода Java. Большинство методов не требуют специальной обработки или конфигурации; не требуется шаблон или сгенерированный код.
Подключить и работать с JNA очень просто, для этого необходимо скачать jar-файл или подключить зависимость в сборщик проекта – в нашем случает Maven:

<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna-platform</artifactId>
<version>5.0.0</version>
</dependency>

В нашем проекте мы использовали JNA для достижения следующих целей: заблокировать и сделать вновь доступным диспетчер задач 1) по комбинации Ctrl+Shift+Esc и 2) в меню, доступном по комбинации Ctrl+Alt+Del.

Для достижения этого были использованы класс Advapi32Util (удобная обёртка над библиотекой advapi32.dll) и интерфейс WinReg с полезными константами, которые предоставляют функциональность для внесения изменений в реестр Windows (Рисунок 1. Класс TaskManager с методами enable() и disable() для изменения ключей реестра диспетчера задач).

Рисунок 1. Класс TaskManager с методами enable() и disable() для изменения ключей реестра диспетчера задач.

Рисунок 2. Метод для создания нового процесса для определённого пользователя Windows.

private static final Function activeSessionFunc = Function.getFunction("kernel32", "WTSGetActiveConsoleSessionId");
private static final Function userTokenFunc = Function.getFunction("wtsapi32", "WTSQueryUserToken");
public static WinNT.HANDLE getActiveUserToken() {
        PointerByReference pointer = new PointerByReference();
        WinDef.ULONG id = (WinDef.ULONG) activeSessionFunc.invoke(WinDef.ULONG.class, null);
        userTokenFunc.invoke(WinDef.BOOL.class, new Object[]{id, pointer});
        return new WinNT.HANDLE(pointer.getValue());
    }

Работа с процессами

Для работы и слежения за процессами в Windows был использован, добавленный в Java 9, класс ProcessHandle. ProcessHandle позволяет получать и производить различные манипуляции с процессами. В частности, при решении задачи, требовалось собирать PID процессов, фильтровать процессы на основе имени и принудительно завершать необходимые процессы.

Рисунок 3. Класс ProcessHandler с методами takeSnapshot() для создания снимка текущих процессов и closeNewProcesses() для завершения процессов, отличных от снимка.

Взаимодействие с другими компонентами системы

WebSocket

Для Java существует стандартизированный API для работы с WebSocket.

<dependency>
    <groupId>javax.websocket</groupId>
    <artifactId>javax.websocket-api</artifactId>
    <version>1.1</version>
    <scope>provided</scope>
</dependency>

Но одного API недостаточно, поэтому для запуска кода была выбрана одна из его реализаций – Tyrus.

<dependency>
<groupId>org.glassfish.tyrus</groupId>
<artifactId>tyrus-server</artifactId>
<version>1.14</version>
</dependency>
<dependency>
<groupId>org.glassfish.tyrus</groupId>
 	<artifactId>tyrus-container-grizzly-server</artifactId>
<version>1.14</version>
</dependency>

Далее можно создать минималистичный сервер и указать обработчики (EndPoints).

 var server = new Server(
                "localhost",
                8080,
                "/endpoints",
                null,
                EndPoint1.class,
    EndPoint2.class,
    ...);
        try {
            server.start();
            Thread.currentThread().join();
        } catch (Exception e) {
            log.error("Ooops! ", e);
        } finally {
            server.stop();
            log.info("Finally...");
        }

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

@ServerEndpoint(value = "endpoint/url")
public class SampleEndpoint {
 
    @OnOpen
    public void onOpen(Session session) throws IOException {
        // Вызывается при инициализации нового соединения
    }
 
    @OnMessage
    public void onMessage(Session session, Message message) throws IOException {
        // Вызывается при получении сообщения от клиента
    }
 
    @OnClose
    public void onClose(Session session) throws IOException {
        // Вызывается при закрытии соединения
    }
 
    @OnError
    public void onError(Session session, Throwable throwable) {
        // Вызывается при возникновении ошибок
    }
}

HTTP-клиент

С выпуском 11-ой версии Java в ней появился удобный HTTP-клиент, поэтому потребность в сторонних клиентах исчезла.

Для создания экземпляра клиента необходимо воспользоваться билдером. В простейшем случае:

var client = HttpClient .newBuilder() .build()

Далее необходимо создать запрос(request), например:

var request = HttpRequest.newBuilder()
                .uri(URI.create("https://myserver.com"))
                .timeout(Duration.ofMillis(1000))
                .header("Content-Type", "application/json")
                .POST(bean.toJSON())
                .build();

Затем этот запрос можно использовать для отправки на сервер:

var response = client.send(closeSession(sessionId, token), HttpResponse.BodyHandlers.ofString());


Заключение

Благодаря модульной организации версий Java 9 и выше, утилите WinSW, обновлённому Process API для взаимодействия с процессами операционной системы и библиотеки JNA (Java Native Access), которая предоставляет программам Java простой доступ к нативным библиотекам, мы смогли создать Windows службу с использованием языка Java, на котором была реализована и серверная часть. Что в итоге позволило не вводить в процесс разработки новый язык.


, , , , ,

Leave a Reply

Your email address will not be published. Required fields are marked *