Глубокое погружение в мир аннотации Spring Boot @Conditional с проработанными примерами реализаций классов доступа к БД Mongo и MySQL.
В моем посте «Почему Spring Boot?» было рассмотрено создание Spring Boot приложения, из которого вы едва ли сможете понять, что происходит за кулисами. Возможно, вы хотите понять магию автоконфигурации Spring Boot.
Перед этим вы должны узнать о Spring аннотации @Conditional, от которой зависит вся магия автоконфигурации Spring Boot.
Изучение возможностей @Conditional
При разработке приложений на основе Spring может возникнуть необходимость условной регистрации bean-компонентов.
Например, вы можете зарегистрировать bean-компонент DataSource, указывающий на базу данных DEV при запуске приложения локально, и указывать на другую базу данных PRODUCTION при работе в рабочей среде.
Вы можете перенести параметры подключения к базе данных в файл properties и использовать файл, соответствующий среде, но вам нужно изменить конфигурацию всякий раз, когда вам нужно указать другую среду и создать приложение.
Для решения этой проблемы в Spring 3.1 была введена концепция Profiles (профиль). Вы можете зарегистрировать несколько бинов одного типа и связать их с одним или несколькими профилями. При запуске приложения вы можете активировать нужные профили и компоненты, связанные с активированными профилями, и только эти профили будут зарегистрированы.
@Configuration
public class AppConfig
{
@Bean
@Profile("DEV")
public DataSource devDataSource() {
...
}
@Bean
@Profile("PROD")
public DataSource prodDataSource() {
...
}
}
Затем вы можете указать активный профиль, используя System Property -Dspring.profiles.active = DEV.
Этот подход работает для простых случаев, таких как включение или отключение регистрации компонентов на основе активированных профилей. Но если вы хотите зарегистрировать bean-компоненты, основанные на некоторой условной логике, тогда сам подход профилей недостаточен.
Чтобы обеспечить большую гибкость условной регистрации bean-компонентов Spring, Spring 4 ввел концепцию @Conditional. Используя подход @Conditional, вы можете зарегистрировать компонент, условно основанный на любом произвольном условии.
Например, вы можете зарегистрировать компонент, когда:
- Определенный класс присутствует в classpath
- Spring bean определенного типа еще не зарегистрирован в ApplicationContext
- Определенный файл существует в местоположении
- Конкретное значение свойства настраивается в файле конфигурации
- Определенное системное свойство присутствует / отсутствует
Это всего лишь несколько примеров, и вы можете использовать любое условие, которое захотите.
Давайте посмотрим, как работает Spring's @Conditional.
Предположим, у нас есть интерфейс UserDAO с методами для получения данных из хранилища данных. У нас есть две реализации интерфейса UserDAO, а именно JdbcUserDAO, который общается с базой данных MySQL, и MongoUserDAO, который общается с MongoDB.
Мы можем захотеть включить только один интерфейс JdbcUserDAO и MongoUserDAO на основе System Property, скажем, dbType.
Если приложение запускается с использованием команды: java -jar myapp.jar -DdbType = MySQL, то мы хотим включить JdbcUserDAO. В противном случае, если приложение запускается с использованием команды: java -jar myapp.jar -DdbType = MONGO, мы хотим включить MongoUserDAO.
Пусть реализации JdbcUserDAO и MongoUserDAO интерфейса UserDAO выглядят следующим образом:
public interface UserDAO
{
List<String> getAllUserNames();
}
public class JdbcUserDAO implements UserDAO
{
@Override
public List<String> getAllUserNames()
{
System.out.println("**** Getting usernames from RDBMS *****");
return Arrays.asList("Siva","Prasad","Reddy");
}
}
public class MongoUserDAO implements UserDAO
{
@Override
public List<String> getAllUserNames()
{
System.out.println("**** Getting usernames from MongoDB *****");
return Arrays.asList("Bond","James","Bond");
}
}
Мы можем реализовать Condition MySQLDatabaseTypeCondition, позволяющий проверить, что System Property dbType равно «MYSQL» следующим образом:
public class MySQLDatabaseTypeCondition implements Condition
{
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata)
{
String enabledDBType = System.getProperty("dbType");
return (enabledDBType != null && enabledDBType.equalsIgnoreCase("MYSQL"));
}
}
Мы можем реализовать Condition MongoDBDatabaseTypeCondition, чтобы проверить, что System Property dbType равно «MONGODB» следующим образом:
public class MongoDBDatabaseTypeCondition implements Condition
{
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata)
{
String enabledDBType = System.getProperty("dbType");
return (enabledDBType != null && enabledDBType.equalsIgnoreCase("MONGODB"));
}
}
Теперь мы можем условно настроить bean-компоненты JdbcUserDAO и MongoUserDAO, используя @Conditional следующим образом:
@Configuration
public class AppConfig
{
@Bean
@Conditional(MySQLDatabaseTypeCondition.class)
public UserDAO jdbcUserDAO(){
return new JdbcUserDAO();
}
@Bean
@Conditional(MongoDBDatabaseTypeCondition.class)
public UserDAO mongoUserDAO(){
return new MongoUserDAO();
}
}
Если мы запустим приложение следующим образом: java -jar myapp.jar -DdbType = MYSQL, тогда будет зарегистрирован только bean-компонент JdbcUserDAO.
Но если вы установите System property: -DdbType = MONGODB, то будет зарегистрирован только bean-компонент MongoUserDAO.
Теперь мы увидели, как условно зарегистрировать бин на основе System Property.
Предположим, что мы хотим зарегистрировать bean-компонент MongoUserDAO только в том случае, если в classpath доступен Java класс MongoDB драйвера "com.mongodb.Server", если нет, мы хотим зарегистрировать bean-компонент JdbcUserDAO.
Для этого мы можем реализовать Condition для проверки наличия или отсутствия класса драйвера MongoDB "com.mongodb.Server" следующим образом:
public class MongoDriverPresentsCondition implements Condition
{
@Override
public boolean matches(ConditionContext conditionContext,AnnotatedTypeMetadata metadata)
{
try {
Class.forName("com.mongodb.Server");
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
}
public class MongoDriverNotPresentsCondition implements Condition
{
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata)
{
try {
Class.forName("com.mongodb.Server");
return false;
} catch (ClassNotFoundException e) {
return true;
}
}
}
Мы только что увидели, как зарегистрировать bean-компоненты условно на основании наличия или отсутствия класса в classpath.
Что если мы хотим зарегистрировать bean-компонент MongoUserDAO, только если другой Spring-компонент типа UserDAO уже не зарегистрирован.
Мы можем реализовать Condition, чтобы проверить, существует ли какой-либо bean-компонент определенного типа, следующим образом:
public class UserDAOBeanNotPresentsCondition implements Condition
{
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata)
{
UserDAO userDAO = conditionContext.getBeanFactory().getBean(UserDAO.class);
return (userDAO == null);
}
}
Что если мы хотим зарегистрировать bean-компонент MongoUserDAO, только если в файле конфигурации установлено свойство app.dbType = MONGO?
Мы можем реализовать интерфейс Condition следующим образом:
public class MongoDbTypePropertyCondition implements Condition
{
@Override
public boolean matches(ConditionContext conditionContext,
AnnotatedTypeMetadata metadata)
{
String dbType = conditionContext.getEnvironment()
.getProperty("app.dbType");
return "MONGO".equalsIgnoreCase(dbType);
}
}
Мы только что увидели различные варианты реализации интерфейса Condition. Но есть еще более элегантный способ реализовать Condition с использованием Аннотаций. Вместо создания реализации Condition для MYSQL и MongoDB мы можем создать аннотацию DatabaseType следующим образом:
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Conditional(DatabaseTypeCondition.class)
public @interface DatabaseType
{
String value();
}
Затем мы можем реализовать класс DatabaseTypeCondition, чтобы использовать значение DatabaseType для определения включить или отключить регистрацию компонента следующим образом:
public class DatabaseTypeCondition implements Condition
{
@Override
public boolean matches(ConditionContext conditionContext,
AnnotatedTypeMetadata metadata)
{
Map<String, Object> attributes = metadata.getAnnotationAttributes(DatabaseType.class.getName());
String type = (String) attributes.get("value");
String enabledDBType = System.getProperty("dbType","MYSQL");
return (enabledDBType != null && type != null && enabledDBType.equalsIgnoreCase(type));
}
}
Теперь мы можем использовать аннотацию @DatabaseType в наших определениях компонентов следующим образом:
@Configuration
@ComponentScan
public class AppConfig
{
@Bean
@DatabaseType("MYSQL")
public UserDAO jdbcUserDAO(){
return new JdbcUserDAO();
}
@Bean
@DatabaseType("MONGO")
public UserDAO mongoUserDAO(){
return new MongoUserDAO();
}
}
Здесь мы получаем метаданные из аннотации DatabaseType и проверяем значение dbType свойства системы, чтобы определить, включить или отключить регистрацию компонента.
Мы увидели множество примеров, позволяющих понять, как мы можем зарегистрировать бины условно, используя аннотацию @Conditional.
Spring Boot широко использует функцию @Conditional для регистрации bean-компонентов условно на основе различных критериев.
Вы можете найти различные реализации интерфейса Condition, которые SpringBoot использует в классе org.springframework.boot.autoconfigure пакета spring-boot-autoconfigure-{version}.jar.
Мы узнали как Spring Boot использует функцию @Conditional для условной проверки, регистрировать бин или нет.
Но что именно запускает механизм автоконфигурации?
Это то, что мы рассмотрим в следующем разделе.
Spring Boot AutoConfiguration
Ключ к волшебству автоконфигурации Spring Boot — аннотация @EnableAutoConfiguration. Обычно мы аннотируем наш класс, являющейся точкой входа в приложение, либо с помощью @SpringBootApplication, либо, если мы хотим настроить значения по умолчанию, мы можем использовать следующие аннотации:
@Configuration
@EnableAutoConfiguration
@ComponentScan
public class Application
{
}
Аннотация @EnableAutoConfiguration включает автоматическую настройку Spring ApplicationContext путем сканирования компонентов пути к классам и регистрации бинов, соответствующих различным условиям.
Spring Boot предоставляет различные классы AutoConfiguration в spring-boot-autoconfigure-{version}.jar, которые отвечают за регистрацию различных компонентов.
Обычно классы AutoConfiguration аннотируются @Configuration, чтобы пометить его как класс конфигурации Spring, и аннотируются @EnableConfigurationProperties для привязки свойств настройки и одного или нескольких методов регистрации условного компонента.
Например, рассмотрим класс org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration.
@Configuration
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ Registrar.class, DataSourcePoolMetadataProvidersConfiguration.class })
public class DataSourceAutoConfiguration
{
...
...
@Conditional(DataSourceAutoConfiguration.EmbeddedDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import(EmbeddedDataSourceConfiguration.class)
protected static class EmbeddedConfiguration {
}
@Configuration
@ConditionalOnMissingBean(DataSourceInitializer.class)
protected static class DataSourceInitializerConfiguration {
@Bean
public DataSourceInitializer dataSourceInitializer() {
return new DataSourceInitializer();
}
}
@Conditional(DataSourceAutoConfiguration.NonEmbeddedDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
protected static class NonEmbeddedConfiguration {
@Autowired
private DataSourceProperties properties;
@Bean
@ConfigurationProperties(prefix = DataSourceProperties.PREFIX)
public DataSource dataSource() {
DataSourceBuilder factory = DataSourceBuilder
.create(this.properties.getClassLoader())
.driverClassName(this.properties.getDriverClassName())
.url(this.properties.getUrl()).username(this.properties.getUsername())
.password(this.properties.getPassword());
if (this.properties.getType() != null) {
factory.type(this.properties.getType());
}
return factory.build();
}
}
...
...
@Configuration
@ConditionalOnProperty(prefix = "spring.datasource", name = "jmx-enabled")
@ConditionalOnClass(name = "org.apache.tomcat.jdbc.pool.DataSourceProxy")
@Conditional(DataSourceAutoConfiguration.DataSourceAvailableCondition.class)
@ConditionalOnMissingBean(name = "dataSourceMBean")
protected static class TomcatDataSourceJmxConfiguration {
@Bean
public Object dataSourceMBean(DataSource dataSource) {
....
....
}
}
...
...
}
Здесь DataSourceAutoConfiguration аннотируется @ConditionalOnClass({DataSource.class,EmbeddedDatabaseType.class}), что означает, что автоконфигурация bean-компонентов в DataSourceAutoConfiguration будет рассматриваться, только если классы DataSource.class и EmbeddedDatabaseType.class доступны в classpath.
Класс также аннотируется @EnableConfigurationProperties(DataSourceProperties.class), который позволяет автоматически связывать свойства в application.properties со свойствами класса DataSourceProperties.
@ConfigurationProperties(prefix = DataSourceProperties.PREFIX)
public class DataSourceProperties implements BeanClassLoaderAware, EnvironmentAware, InitializingBean {
public static final String PREFIX = "spring.datasource";
...
...
private String driverClassName;
private String url;
private String username;
private String password;
...
//setters and getters
}
При такой конфигурации все свойства, которые начинаются со spring.datasource.*, будут автоматически привязаны к объекту DataSourceProperties.
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=secret
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
Вы также можете увидеть некоторые внутренние классы и методы определения bean-компонентов, которые аннотируются условными аннотациями SpringBoot, такими как @ConditionalOnMissingBean, @ConditionalOnClass и @ConditionalOnProperty и т.д.
Эти определения bean-компонентов будут зарегистрированы в ApplicationContext, только если эти условия будут выполнены.
Вы также можете изучить многие другие классы автоконфигурации в spring-boot-autoconfigure-{version}.jar, такие как:
- org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration
- org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
- org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration
- org.springframework.boot.autoconfigure.jackson.JacksonAutoConfigurationetc и т. д.
Я надеюсь, что теперь у вас есть понимание того, как работает автоконфигурация Spring Boot используя различные классы автоконфигурации вместе с функциями @Conditional.