Принцип DRY (Не повторяйся) – это важная составляющая цикла разработки программного обеспечения. Его цель – избежать ненужной повторяемости в коде. В частности, имеется множество приложений, которые могут находиться в составе одной и той же микросервисной архитектуры и использовать один и тот же компонент. В результате код становится неудобно поддерживать, поскольку всякий раз, когда требуется внести изменение в этот компонент, с каждым из этих приложений приходится разбираться отдельно.
В этой статье давайте рассмотрим, как можно вынести такие компоненты из приложений в отдельный модуль. Тем самым мы одновременно стремимся упростить поддержку кода и сократить в нём количество повторов.
Краткое введение
Когда мы берёмся за проект по написанию библиотеки, важно держать в уме несколько существенных моментов. Упомяну некоторые из них:
- Предполагается, что библиотеки нужно делать максимально целостными. Библиотека должна делать лишь то, для чего она предназначена.
- Библиотеки должны обладать слабой связностью. Если они сильно зависят от других модулей, то их будет сложно поддерживать и интегрировать. Поэтому количество необходимых зависимостей в библиотеке требуется свести к абсолютно возможному минимуму.
Одна из наиболее серьёзных проблем, с которой мы можем столкнуться при реализации библиотечного проекта — это сильная взаимосвязь между библиотекой и тем проектом, в котором мы собираемся её использовать. Если вы из чистого интереса используете множество зависимостей, то скоро они начнут доставлять вам неприятные проблемы. Не забывайте, что наше приложение ни в коем случае не должно зависеть от библиотечного проекта. Мы должны иметь возможность обновлять зависимости или добавлять дополнительные зависимости, не провоцируя никаких проблем, которые были бы обусловлены этой липкой библиотекой.
Именно поэтому некоторые считают, что библиотеки – это плохо. Да, они могут быть уместны в некоторых сценариях — если говорить о проектах на Spring Boot, то для них существует много библиотек, которые можно просто взять и использовать. Можете себе представить, что было бы, если бы этих библиотек не существовало, и вы были бы вынуждены писать их самостоятельно, приступая к каждому новому проекту? Невесело, да. Если же вы умеете сами писать новые библиотеки, то никто не лишит вас этого преимущества.
Что мы собираемся сделать?
В этом демо-примере мы создадим api-клиентскую библиотеку, в состав которой будет входить feign-клиент, модели запрос/отклик, а также прокси (посредник). Если не написать такую библиотеку, то вот какие компоненты потребовалось бы добавлять к приложению всякий раз, когда мы захотим воспользоваться этим клиентом:
- URL и feign-конфигурации добавляются к свойствам приложения,
- Клиентский feign-интерфейс,
- Собственный декодер, если он потребуется,
- Собственные исключения, если они потребуются,
- Модели запроса/отклика,
- Прокси-класс для обработки исключений и вывода.
Сразу видно, насколько сложно всё это сделать. Но, когда библиотека будет готова, нам понадобится добавить к приложению всего две вещи:
- Саму библиотеку – она заносится в список зависимостей,
- Нашу собственную аннотацию для главного класса, чтобы активировать api-клиент.
Начнём
Первым делом создадим пустой проект и проверим его зависимости. Для управления зависимостями я использую Maven, поэтому нам придётся заглянуть в файл
pom.xml
1. POM
В исходном виде проекты spring boot содержат немногочисленные зависимости и spring-boot-starter-parent. Они нам не нужны. Сама по себе библиотека работать не будет, поэтому их можно удалить. В общем, отсекаем всё лишнее. Кроме того, из раздела со сборочными плагинами можно удалить spring-boot-maven-plugin.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.emrekumas</groupId>
<artifactId>marvelous-api-client</artifactId>
<version>${revision}</version>
<name>marvelous-api-client</name>
<description>Demo project for demonstrating Spring Boot Library</description>
<packaging>jar</packaging>
<properties>
<maven.compiler.release>11</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
<!-- Feign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form</artifactId>
<version>3.8.0</version>
</dependency>
<!-- Others -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
<scope>provided</scope>
</dependency>
</dependencies>
<distributionManagement>
<repository>
<id>trendyol-maven-release</id>
<name>Trendyol release</name>
<url>some-url</url>
</repository>
</distributionManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
</plugin>
</plugins>
</build>
</project>
- Версию проекта задаём как “${revision}”. Таким образом, всякий раз, когда мы создаём новую версию и развёртываем её, можно с лёгкостью передать номер версии из нашего инструмента для непрерывной интеграции/непрерывного развёртывания, даже не редактируя сам файл pom. Можете почитать эту статью, в которой мой коллега объясняет, как мы пользовались этой системой при работе с нашими библиотеками.
- Метод упаковки добавляем в формате ‘jar’. Дело в том, что мы развернём библиотеку в менеджере репозиториев именно как jar-файл.
- Включаем две feign-зависимости. Можете погуглить о них дополнительную информацию. Но учитывайте, что исходно эта библиотека не предназначалась для работы с feign, так что в итоге эти две зависимости нам даже не понадобятся. На практике такая ситуация может сложиться с общей библиотекой моделей, в состав которой входят только классы моделей.
- Включаем зависимость Lombok. Она нам очень пригодится, когда будем писать библиотечный проект.
- Далее нужно поработать с менеджером дистрибутивов. Нам нужно выбрать, где мы развернём нашу библиотеку, так, чтобы любое приложение на этапе сборки могло загрузить её. Можете использовать для этого Nexus, Jfrog, т.д. — выбор за вами.
- Наконец, нам понадобится плагин для сборки. Воспользуемся maven-compiler-plugin, который скомпилирует проект и создаст jar-файл. Также сверху включается несколько свойств, в которых мы задаём версию java и кодировки.
2. Файл свойств
В исходном проекте содержится файл со свойствами приложения. Лично я больше люблю работать с файлами свойств, записанными в формате yaml. В таком случае можно создавать множество профилей в одном файле. Поэтому далее я покажу файл свойств именно в формате yaml. Кроме того, для демонстрации выберу произвольный сервис. Поскольку тестового URL у меня нет, я воспользуюсь одним и тем же URL для всех окружений.
ВАЖНО: Здесь обратите внимание: если назвать библиотечный файл свойств “application.yaml”, то окажется, что в приложении, которое станет использовать эту библиотеку, будет собственный файл со свойствами, именуемый “application.yaml”. Между ними возникнет конфликт. Свойство из родительского файла переопределит одноимённое свойство из библиотеки, поэтому прочитать приложение мы не сможем. Полное разочарование.
РЕШЕНИЕ: чтобы такая проблема не возникала, подберите другое название для файла свойств, который будет находиться у вас в библиотеке. Допустим, “marvelousapiclient-application.yaml”. Может быть, длинновато, но какая разница. Оставьте так. Теперь возникает другая проблема: из-за того, что имя файла изменилось, ваша библиотека автоматически этот файл не увидит. Но вскоре я покажу, как это решить.
spring:
application:
name: marvelous-api-client
---
spring:
profiles: dev, stage
feign:
client:
config:
randomjokeapi:
connect-timeout: 2000
read-timeout: 4000
randomjokeapi:
url: https://official-joke-api.appspot.com/random_joke
---
spring:
profiles: prod
feign:
client:
config:
randomjokeapi:
connect-timeout: 1000
read-timeout: 2000
randomjokeapi:
url: https://official-joke-api.appspot.com/random_joke
3. КОНФИГУРАЦИИ
Перед тем, как переходить к конфигурированию свойств, нужно научиться настраивать собственный активный профиль в контейнеризованных окружениях. Это можно сделать несколькими способами. Например, при создании контейнера установить системную переменную или передавать её как параметр, когда запускаете ваши приложения на java. Если требуется работать только на локальной машине, то установить активный профиль можно исключительно силами вашей IDE, так как показано ниже (это нужно делать в том приложении, которое будет использовать библиотеку):
Установка активного профиля в IntelliJ IDEA
Теперь нужно создать конфигурационный файл, при помощи которого мы будем читать файл свойств. Вы могли бы спросить — а зачем? Дело в том, что мы его переименовали, и, поскольку сам spring его теперь автоматически не считывает, мы должны сами рассказать системе, как ей читать
marvelousapiclient-application.yaml
.@Slf4j
public class YamlPropertySourceFactory implements PropertySourceFactory {
@Override
public PropertySource<?> createPropertySource(String name, EncodedResource encodedResource) {
String activeProfile = Optional.ofNullable(System.getenv("SPRING_PROFILES_ACTIVE"))
.orElse(System.getProperty("spring.profiles.active"));
log.info("MarvelousApiClient active profile: " + activeProfile);
assert activeProfile != null;
YamlPropertiesFactoryBean yamlFactory = new YamlPropertiesFactoryBean();
yamlFactory.setDocumentMatchers(properties -> {
String profileProperty = properties.getProperty("spring.profiles");
if (StringUtils.isEmpty(profileProperty)) {
return ABSTAIN;
}
return profileProperty.contains(activeProfile) ? FOUND : NOT_FOUND;
});
yamlFactory.setResources(encodedResource.getResource());
Properties properties = yamlFactory.getObject();
assert properties != null;
return new PropertiesPropertySource(Objects.requireNonNull(encodedResource.getResource().getFilename()), properties);
}
}
Можете сами проанализировать этот код, но, если сказать просто — он первым делом берёт активный профиль из системного окружения. Если находит null, то проверяет то свойство spring.profiles.active, которое мы передали в качестве параметра. Как видите, код выбросит исключение, если активный профиль не установлен. Мы зададим простую проверку логов, чтобы проверить его в нашей системе мониторинга логов.
Давайте создадим конфигурационный файл для библиотеки. В этом файле мы используем пару аннотаций.
@EnableFeignClients(clients = RandomJokeClient.class)
@ComponentScan(basePackageClasses = MarvelousApiClientProxy.class)
@Configuration
@PropertySource(value = "classpath:marvelousapiclient-application.yaml", factory = YamlPropertySourceFactory.class)
public class MarvelousApiClientConfiguration {
}
Мы воспользовались аннотацией EnableFeignClients и указали класс RandomJokeClient, который вскоре создадим. Следовательно, клиентский feign-интерфейс ничего не сможет прочитать из нашего собственного файла со свойствами. Во-вторых, при помощи ComponentScan будем сканировать прокси-файл, который вскоре также создадим. Если этого не сделать, то нужный компонент не будет создан в приложении, использующем эту библиотеку.
Хотя мы и передали файлы классов непосредственно в EnableFeignClients и ComponentScan, мы с тем же успехом могли бы указать только имена тех пакетов, в которых они находятся. Таким образом, не приходится указывать в объявлениях все имена классов. Поскольку здесь мы рассматриваем небольшой учебный проект, поступим именно так.
Далее воспользуемся аннотацией PropertySource, при помощи которой сообщим системе, как ей читать файл свойств. Для этого она будет использовать наш собственный фабричный класс.
4. МОДЕЛИ
Эта часть выглядит с каждым API по-своему, но, в принципе, довольно проста. Можете посмотреть в моём git-репозитории, как я создал их для собственных нужд, оставлю ссылку в конце статьи. Так что, идём дальше.
5. FEIGN-КЛИЕНТ
Теперь переходим к созданию feign-клиента. Это всего лишь простой интерфейс с несколькими аннотациями. Поскольку эта статья — не руководство по feign, просто пойдём дальше.
@FeignClient(name = "randomjokeapi", url = "${randomjokeapi.url}")
public interface RandomJokeClient {
@GetMapping
RandomJokeResponse getRandomJoke();
}
6. ПРОКСИ
При помощи прокси создадим стандартный отклик, по которому сможем судить, произошла ошибка или нет. Таким образом, пользователям нашей библиотеки не придётся предусматривать дополнительные поля. Клиент всегда будет возвращать отклик только одного типа.
7. СОБСТВЕННАЯ АННОТАЦИЯ
В официальной документации по Spring описано два способа, которыми можно импортировать библиотеку в приложение. Один из них — воспользоваться автоконфигурацией, другой — прибегнуть к собственным аннотациям. Можете почитать здесь, как делается автоконфигурация. В нашей библиотеке мы будем работать через собственные аннотации.
Для простоты давайте создадим собственную аннотацию, которая будет импортировать нашу конфигурацию. Следовательно, пользователям нашей библиотеки придётся всего лишь добавить эту аннотацию в их главный класс.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(MarvelousApiClientConfiguration.class)
public @interface EnableMarvelousApiClient {
}
8. РАЗВЁРТЫВАНИЕ
Итак, всё готово. Не забудьте удалить главный класс и связанный с ним тестовый класс – нам они больше не нужны. Кстати, мы оставим дополнительный XML-файл под названием “settings.xml”, в котором содержатся учётные данные для центрального репозитория. Теперь можем развернуть наш jar-файл в центральном репозитории при помощи mvn. Наш вариант я включу в качестве примера, но вы можете дорабатывать его как вам удобно:
mvn clean deploy -Drevision='1.0.0' -f pom.xml --settings settings.xml
ЗАКЛЮЧЕНИЕ
Библиотека завершена и готова к использованию. Как я уже сказал, вам только нужно перейти в ваше приложение, добавить её в ваш список зависимостей и добавить собственную аннотацию в ваш главный класс. Дерзайте!
pom.xml приложения, использующего эту библиотеку
Главный класс того приложения, в которое мы добавили нашу собственную аннотацию
Примерный отклик после того, как мы установили контроллер
Хотя в этом проекте и нет ничего сверхъестественного, наша библиотека, как минимум, работает. Мы вынесли клиентский компонент за пределы приложения и выделили его в собственный модуль. Теперь, как только понадобится внести изменение, нам придётся просто обновить нашу библиотеку и заново развернуть её. Все приложения подхватят эти изменения и, после того, как они будут повторно развёрнуты, логика кода ничуть не изменится.
Библиотека, которую мы написали, выложена для примера здесь:
github.com/EmreKumas/marvelous-api-client?source=post_page-----7064e831b63b--------------------------------
Спасибо, что дочитали. Надеюсь, что вам было интересно.