В эпоху микросервисов приходится все чаще и чаще писать интеграции для их взаимодействия как между собой, так и со сторонними системами. Кто-то создаёт отдельные библиотеки с интеграцией и переиспользует их в нескольких микросервисах, кто-то захламляет проект огромным количеством POJO классов, некоторые же создают один POJO класс с множеством вложенных классов. В этой статье я хотел бы поделиться подходом, используя который вы сможете спрятать большую часть кода, которая мешает чтению и пониманию проекта.

Для примера мы будем использовать интеграцию с API GitHub и привычные Spring аннотации для описания нашего клиента.

Подключаем зависимости

Добавим feign-spring4 для поддержки генерации клиента на основе spring аннотаций.

<dependency>
	<groupId>io.github.openfeign</groupId>
	<artifactId>feign-spring4</artifactId>
	<version>${feign.version}</version>
</dependency>

Добавляем feign-jackson для поддержки аннотаций, которые будут в сгенерированных POJO классах.

<dependency>
	<groupId>io.github.openfeign</groupId>
	<artifactId>feign-jackson</artifactId>
	<version>${feign.version}</version>
</dependency>

Добавляем плагин для генерации POJO классов

В секцию plugins добавляем конфигурацию для jsonschema2pojo-maven-plugin. Указываем, где у нас хранятся json схемы, какое название пакета будет использоваться в сгенерированных классах и отключаем добавление дополнительных полей, чтобы игнорировать те, которые мы не используем.

<build>
	<plugins>
		<plugin>
			<groupId>org.jsonschema2pojo</groupId>
			<artifactId>jsonschema2pojo-maven-plugin</artifactId>
			<version>1.2.1</version>
			<configuration>
				<sourceDirectory>${basedir}/src/main/resources/schema</sourceDirectory>
				<targetPackage>com.example.types</targetPackage>
				<includeAdditionalProperties>false</includeAdditionalProperties>
			</configuration>
			<executions>
				<execution>
					<goals>
						<goal>generate</goal>
					</goals>
				</execution>
			</executions>
		</plugin>
	</plugins>
</build>

Опишем json схему, по которой будут генерироваться наши POJO классы. Для того, чтобы генератор не пропустил классы из definitions, нужно добавить ссылки на них в properties.

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "description" : "Definition of GitHubApi",
  "type": "object",
  "properties" : {
    "contributor" : {
      "type" : "object",
      "$ref" : "#/definitions/Contributor"
    },
    "issue" : {
      "type" : "object",
      "$ref" : "#/definitions/Issue"
    }
  },
  "definitions": {
    "Contributor" : {
      "type" : "object",
      "properties": {
        "login": {
          "type": "string"
        },
        "contributions": {
          "type": "integer"
        }
      }
    },
    "Issue" : {
      "type" : "object",
      "properties": {
        "title": {
          "type": "string"
        },
        "body": {
          "type": "string"
        },
        "labels": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              }
            }
          }
        },
        "user": {
          "type" : "object",
          "$ref" : "#/definitions/User"
        }
      }
    },
    "User" : {
      "type" : "object",
      "properties": {
        "login": {
          "type": "string"
        },
        "avatar_url": {
          "type": "string"
        }
      }
    }
  }
}

Для упрощения создания схемы можно воспользоваться онлайн генератором схемы из json.

Переходим к описанию интеграции

Создадим простой интерфейс для описания конечных URI для получения данных.

interface GitHubApi {
    @GetMapping("/repos/{owner}/{repo}/contributors")
    List<Contributor> contributors(@PathVariable("owner") String owner, @PathVariable("repo") String repo);

   @GetMapping("/repos/{owner}/{repo}/issues")
    List<Issue> issues(@PathVariable("owner") String owner, @PathVariable("repo") String repo);
}

Создаем клиента для обращения к GitHub. Выбираем JacksonDecoder для поддержки аннотаций в POJO классах (таких как @JsonProperty). Добавляем SpringContract для использования spring аннотаций (@GetMapping, @PathVariable).

GitHubApi github = Feign.builder()
            .decoder(new JacksonDecoder())
            .contract(new SpringContract())
            .target(GitHubApi.class, "https://api.github.com");

Далее мы сможем использовать этот клиент для обращения к таким методам, как contributors и issues. При необходимости можно провести более глубокую настройку. Например, можно добавить логирование каждого запроса / ответа сервера, количество повторных вызовов в случае ошибок и многое другое.

Заключение

Безусловно, можно использовать множество реализаций генерации кода, таких как OpenAPI. Данный подход дает гибкость при описании POJO и простой настройке клиента.

Полезные ссылки

Комментарии (2)


  1. GreyN
    11.10.2023 22:09

    А чем файл со схемой отличается от одного большого класса с кучей вложенный классов-dto'шек?
    Ну кроме того, что dto'шки пишутся на привычной java, а язык описания схемы еще нужно освоить.
    Кроме того, в dto'шках могут быть удобные методы для работы со свойствами, а в схеме - нет.

    Я понимаю, схема удобна в качестве способа публикации API сервиса, но и в этом случае достаточно сгенерировать dto'шки 1 раз и положить их в исходниики.

    Какой выигрыш вы получаете постоянно перегенерируя dto'шки по схеме при каждой сборке проекта?


    1. gromspys Автор
      11.10.2023 22:09

      В случае с большим классом необходимо потратить достаточно много времени на его описание в соответствии с примером json ответа сервера. Если ли же будет несколько эндпоинтов, то на каждый из них нужно писать свой большой класс, что увеличивает количество этих классов. А что если нужно написать интеграцию с несколькими серверами? Помимо описания ответа сервера могут быть еще и сложные dto для запросов. Это также дополнительные классы.

      В случае генерации, для начала, можно воспользоваться онлайн генератором схемы из примера json ответа сервера. Дальше уже можно использовать эту схему как угодно:

      • Скорректировать ее по вашему усмотрению (переименовать классы, вынести все в defenitions и тд)

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

      "MyClass": { "existingJavaType": "com.example.demo.MyClass" }
      • И его уже использовать как $ref

      • Либо же можно просто скопировать сгенерированные файлы в привычное место и просто использовать их как обычные java классы. Тут уже решать вам.

      Выигрыш от генерации файлов я вижу следующий:

      • Генерация классов по готовому json ответу занимает меньше времени, чем описание их вручную (конечно, можно воспользоваться онлайн генераторами классов из json ответа сервера)

      • Описание всего ответа сразу перед глазами в удобном формате

      • Гибкая настройка генерации (можно добавить генерацию билдеров одной настройкой плагина)

      • Можно спрятать большое количество dto файлов, чтобы они не мешали чтению проекта

      • Возможность подменять внутренние классы своими реализациями