Расширение и настройка классов автоконфигурации, предоставляемых Spring Boot


Постановка проблемы

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

Не все автоконфигурации spring boot поддерживают отключение их с помощью свойства. Мы можем попробовать исключить автоконфигурации, объявив их в свойстве exclusions в @SpringBootApplication и позже импортировать эти автоконфигурации с помощью @Import или @ImportAutoConfiguration в конфигурации конкретного профиля. К сожалению, этот подход часто не работает. Смешивание автоконфигурации и ручного импорта часто приводит к трудно диагностируемым ошибкам конфигурации. К счастью, есть лучший способ, основанный исключительно на автоконфигурации.

Группы профилей

В Spring boot 2.4 появилась концепция «групп профилей», которая позволяет расширить один профиль на несколько подпрофилей.
Мы можем использовать группы профилей для сопоставления одного профиля, определяющего среду, в которой работает приложение (dev / stage / prod), с набором функций, включенных в каждой среде.
Для использования групп профилей необходимо определить раздел spring.profiles.group в application.yml.

spring:
  profiles:
    group:
      dev: bravo, halo
      prod: bravo
Вход в полноэкранный режим Выйти из полноэкранного режима

Такая настройка приведет к появлению активных профилей: dev, bravo, halo в среде dev. В среде prod будут активны только профили prod и bravo.

Автоконфигурация для конкретной функции

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

Для начала нам нужно определить аннотацию, которая позволит связать специфические компоненты & конфигурации с выделенным spring-профилем.

@Profile("halo")
@Retention(RetentionPolicy.RUNTIME)
public @interface HaloFeature {
}
Вход в полноэкранный режим Выход из полноэкранного режима

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

class HaloProfileCondition implements Condition {

    private static final Profiles HALO_PROFILE = Profiles.of("halo");

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return context.getEnvironment().acceptsProfiles(HALO_PROFILE);
    }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Есть три автоконфигурации, связанные с mongo, которые используются нашим приложением. Их нужно сделать условными. Давайте создадим новые классы автоконфигураций, которые являются подклассами исходных автоконфигураций, и аннотируем их @Conditional, используя условие функции:

@Configuration
@Conditional(HaloProfileCondition.class)
class HaloMongoAutoConfiguration extends MongoAutoConfiguration {
}
Войти в полноэкранный режим Выйти из полноэкранного режима
@Configuration
@Conditional(HaloProfileCondition.class)
class HaloMongoDataAutoConfiguration extends MongoDataAutoConfiguration {
}
Войти в полноэкранный режим Выйти из полноэкранного режима
@Configuration
@Conditional(HaloProfileCondition.class)
class HaloMongoRepositoriesAutoConfiguration extends MongoRepositoriesAutoConfiguration {
}
Ввести полноэкранный режим Выйти из полноэкранного режима

К сожалению, этого недостаточно, поскольку условные правила spring boot не наследуются подклассами. Следовательно, условные правила должны быть скопированы из MongoAutoConfiguration, MongoDataAutoConfiguration и MongoRepositoriesAutoConfiguration в их подклассы.

Другое дело, что зависимости, объявленные в @AutoConfigureAfter / @AutoConfigureBefore должны ссылаться на классы автоконфигурации, а не на их подклассы. В противном случае они не будут работать. Поэтому эти аннотации должны быть скопированы из суперкласса в подклассы, но на этот раз значения внутри аннотаций должны быть заменены на соответствующие классы Halo*AutoConfiguration.

Другие аннотации Spring, используемые в подклассах автоконфигурации, например, @Import или @EnableConfigurationProperties будут работать так же, как если бы они были частью автоконфигурации подкласса, поэтому нет необходимости копировать их из подкласса.

После применения этих изменений мы получим следующие классы автоконфигурации:

@Configuration
@Conditional(HaloProfileCondition.class)
@ConditionalOnClass(MongoClient.class)
@ConditionalOnMissingBean(type = "org.springframework.data.mongodb.MongoDatabaseFactory")
class HaloMongoAutoConfiguration extends MongoAutoConfiguration {
}
Вход в полноэкранный режим Выход из полноэкранного режима
@Configuration
@Conditional(HaloProfileCondition.class)
@ConditionalOnClass({ MongoClient.class, MongoTemplate.class })
@AutoConfigureAfter(HaloMongoAutoConfiguration.class)
class HaloMongoDataAutoConfiguration extends MongoDataAutoConfiguration {
}
Вход в полноэкранный режим Выход из полноэкранного режима
@Configuration
@Conditional(HaloProfileCondition.class)
@ConditionalOnClass({ MongoClient.class, MongoRepository.class })
@ConditionalOnMissingBean({ MongoRepositoryFactoryBean.class, MongoRepositoryConfigurationExtension.class })
@ConditionalOnRepositoryType(store = "mongodb", type = RepositoryType.IMPERATIVE)
@AutoConfigureAfter(HaloMongoDataAutoConfiguration.class)
class HaloMongoRepositoriesAutoConfiguration extends MongoRepositoriesAutoConfiguration {
}
Ввести полноэкранный режим Выход из полноэкранного режима

Новые автоконфигурации должны быть зарегистрированы в файле META-INF/spring.factories:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=
  com.adgadev.examples.featureflags.infrastructure.halo.HaloMongoAutoConfiguration,
  com.adgadev.examples.featureflags.infrastructure.halo.HaloMongoDataAutoConfiguration,
  com.adgadev.examples.featureflags.infrastructure.halo.HaloMongoRepositoriesAutoConfiguration
Войти в полноэкранный режим Выход из полноэкранного режима

Теперь давайте исключим оригинальные автоконфигурации mongo:

@SpringBootApplication(exclude = {
        MongoAutoConfiguration.class,
        MongoDataAutoConfiguration.class,
        MongoRepositoriesAutoConfiguration.class
})
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Применение решения для примера функции

Установив условную настройку mongo, мы можем добавить простую зависимую от mongo функцию под названием halo. Функция состоит из одного документа, хранилища mongo и сервиса. Бобы хранилища и сервиса создаются только в том случае, если функция halo включена.

@Getter
@Document("halo")
@AllArgsConstructor
public class HaloEntity {

    @Id
    private String id;

    private String name;
}
Вход в полноэкранный режим Выход из полноэкранного режима
public interface HaloRepository {

    HaloEntity save(HaloEntity haloEntity);

    Optional<HaloEntity> findById(String id);
}
Войти в полноэкранный режим Выход из полноэкранного режима
@HaloFeature
interface MongoHaloRepository extends HaloRepository, MongoRepository<HaloEntity, String> {
}
Войти в полноэкранный режим Выход из полноэкранного режима
@Service
@HaloFeature
@RequiredArgsConstructor
public class HaloService {

    private final HaloRepository haloRepository;

    public HaloEntity addHalo(String name) {
        var haloEntity = new HaloEntity(UUID.randomUUID().toString(), name);
        return haloRepository.save(haloEntity);
    }

    public HaloEntity getHalo(String id) {
        return haloRepository.findById(id).orElseThrow();
    }

}
Войти в полноэкранный режим Выход из полноэкранного режима

Существует также конфигурация spring, которая включает фреймворк mongock для миграции документов и явно определяет пакет mongo repositories:

@HaloFeature
@Configuration
@EnableMongock
@EnableMongoRepositories(basePackageClasses = MongoHaloRepository.class)
class MongoCustomisationsConfig {
}
Войти в полноэкранный режим Выход из полноэкранного режима

Теперь давайте проверим, как работает эта функция, если предположить, что она включена только на dev и база данных mongo присутствует только там.

spring:
  profiles:
    group:
      dev: halo
      prod:
Вход в полноэкранный режим Выйти из полноэкранного режима

Тест очень прост. Он создает базу данных mongo с помощью testcontainers, запускает spring контекст и тестирует haloService в этой среде. При выполнении тест окрашивается в зеленый цвет.

@Testcontainers
@SpringBootTest
@ActiveProfiles("dev")
class DevProfileDemoApplicationTest {

    @Container
    static MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:4.4.2");

    @DynamicPropertySource
    static void setProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.data.mongodb.uri", mongoDBContainer::getReplicaSetUrl);
    }

    @Autowired
    private HaloService haloService;

    @Test
    void shouldExecuteOperationOnMongo() {
        HaloEntity haloEntity = haloService.addHalo("some name");
        assertNotNull(haloService.getHalo(haloEntity.getId()));
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Тест для prod env показывает, что контекст приложения запускается успешно, несмотря на то, что не настроена база данных mongo. Не создано ничего связанного с mongo или halo.

@SpringBootTest
@ActiveProfiles("prod")
class ProdProfileDemoApplicationTest {

    @Test
    void contextLoads() {
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Тот же тест, но с включенной функцией halo не работает при создании контекста spring, из-за проблем с подключением к базе данных mongo при инстанцировании бобов mongock.

@SpringBootTest(properties = "spring.profiles.group.prod=halo")
@ActiveProfiles("prod")
class ProdProfileDemoApplicationTest {

    @Test
    void contextLoads() {
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Резюме

Полный исходный код примеров доступен здесь.

Оцените статью
devanswers.ru
Добавить комментарий