Crie uma configuração automática personalizada com o Spring Boot

Crie uma configuração automática personalizada com o Spring Boot

1. Visão geral

Simplificando, a configuração automática do Spring Boot representa uma maneira de configurar automaticamente um aplicativo Spring com base nas dependências presentes no caminho de classe.

Isso pode tornar o desenvolvimento mais rápido e fácil, eliminando a necessidade de definir determinados beans incluídos nas classes de configuração automática.

Na seção seguinte, daremos uma olhada emcreating our custom Spring Boot auto-configuration.

2. Dependências do Maven

Vamos começar com as dependências de que precisamos:


    org.springframework.boot
    spring-boot-starter-data-jpa
    2.0.1.RELEASE


    mysql
    mysql-connector-java
    8.0.11

As versões mais recentes despring-boot-starter-data-jpa emysql-connector-java podem ser baixadas do Maven Central.

3. Criação de uma configuração automática personalizada

Para criar uma autoconfiguração personalizada, precisamos criar uma classe anotada como@Configuratione registrá-la.

Vamos criar uma configuração personalizada para uma fonte de dadosMySQL:

@Configuration
public class MySQLAutoconfiguration {
    //...
}

A próxima etapa obrigatória é registrar a classe como candidata à configuração automática, adicionando o nome da classe sob a chaveorg.springframework.boot.autoconfigure.EnableAutoConfiguration no arquivo padrãoresources/META-INF/spring.factories:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.autoconfiguration.MySQLAutoconfiguration

Se quisermos que nossa classe de configuração automática tenha prioridade sobre outros candidatos de configuração automática, podemos adicionar a anotação@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE).

A configuração automática é projetada usando classes e beans marcados com anotações@Conditional para que a configuração automática ou partes específicas dela possam ser substituídas.

Observe que a configuração automática só tem efeito se os beans configurados automaticamente não estiverem definidos no aplicativo. Se você definir seu bean, o padrão será substituído.

3.1. Condições de aula

As condições de classe nos permitemspecify that a configuration bean will be included if a specified class is present usando a anotação@ConditionalOnClass,or if a class is absent usando a anotação@ConditionalOnMissingClass.

Vamos especificar que nossoMySQLConfiguration só será carregado se a classeDataSource estiver presente, caso em que podemos assumir que o aplicativo usará um banco de dados:

@Configuration
@ConditionalOnClass(DataSource.class)
public class MySQLAutoconfiguration {
    //...
}

3.2. Condições do feijão

Se quisermosinclude a bean only if a specified bean is present or not, podemos usar as anotações@ConditionalOnBeane@ConditionalOnMissingBean.

Para exemplificar isso, vamos adicionar um beanentityManagerFactory à nossa classe de configuração e especificar que só queremos que este bean seja criado se um bean chamadodataSource estiver presente e se um bean chamadoentityManagerFactory estiver ainda não definido:

@Bean
@ConditionalOnBean(name = "dataSource")
@ConditionalOnMissingBean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
    LocalContainerEntityManagerFactoryBean em
      = new LocalContainerEntityManagerFactoryBean();
    em.setDataSource(dataSource());
    em.setPackagesToScan("com.example.autoconfiguration.example");
    em.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
    if (additionalProperties() != null) {
        em.setJpaProperties(additionalProperties());
    }
    return em;
}

Vamos também configurar um beantransactionManager que só será carregado se um bean do tipoJpaTransactionManager ainda não estiver definido:

@Bean
@ConditionalOnMissingBean(type = "JpaTransactionManager")
JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
    JpaTransactionManager transactionManager = new JpaTransactionManager();
    transactionManager.setEntityManagerFactory(entityManagerFactory);
    return transactionManager;
}

3.3. Condições de Propriedade

A anotação@ConditionalOnProperty é usada paraspecify if a configuration will be loaded based on the presence and value of a Spring Environment property.

Primeiro, vamos adicionar um arquivo de origem de propriedade para nossa configuração que determinará de onde as propriedades serão lidas:

@PropertySource("classpath:mysql.properties")
public class MySQLAutoconfiguration {
    //...
}

Podemos configurar o beanDataSource principal que será usado para criar conexões com o banco de dados de forma que só seja carregado se uma propriedade chamadausemysql estiver presente.

Podemos usar o atributohavingValue para especificar certos valores da propriedadeusemysql que devem ser correspondidos.

Vamos definir o beandataSource com valores padrão que se conectam a um banco de dados local chamadomyDb se a propriedadeusemysql for definida comolocal:

@Bean
@ConditionalOnProperty(
  name = "usemysql",
  havingValue = "local")
@ConditionalOnMissingBean
public DataSource dataSource() {
    DriverManagerDataSource dataSource = new DriverManagerDataSource();

    dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
    dataSource.setUrl("jdbc:mysql://localhost:3306/myDb?createDatabaseIfNotExist=true");
    dataSource.setUsername("mysqluser");
    dataSource.setPassword("mysqlpass");

    return dataSource;
}

Se a propriedadeusemysql for definida comocustom,, o beandataSource será configurado usando valores de propriedades personalizadas para o URL do banco de dados, usuário e senha:

@Bean(name = "dataSource")
@ConditionalOnProperty(
  name = "usemysql",
  havingValue = "custom")
@ConditionalOnMissingBean
public DataSource dataSource2() {
    DriverManagerDataSource dataSource = new DriverManagerDataSource();

    dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
    dataSource.setUrl(env.getProperty("mysql.url"));
    dataSource.setUsername(env.getProperty("mysql.user") != null
      ? env.getProperty("mysql.user") : "");
    dataSource.setPassword(env.getProperty("mysql.pass") != null
      ? env.getProperty("mysql.pass") : "");

    return dataSource;
}

O arquivomysql.properties conterá a propriedadeusemysql:

usemysql=local

Se um aplicativo que usaMySQLAutoconfiguration deseja substituir as propriedades padrão, tudo o que precisa fazer é adicionar valores diferentes para as propriedadesmysql.url,mysql.useremysql.pass eusemysql=custom linha no arquivomysql.properties.

3.4. Condições de recursos

Adicionar a anotação@ConditionalOnResource significa queconfiguration will only be loaded when a specified resource is present.

Vamos definir um método chamadoadditionalProperties() que retornará um objetoProperties contendo propriedades específicas do Hibernate a serem usadas pelo beanentityManagerFactory, apenas se o arquivo de recursomysql.properties estiver presente:

@ConditionalOnResource(
  resources = "classpath:mysql.properties")
@Conditional(HibernateCondition.class)
Properties additionalProperties() {
    Properties hibernateProperties = new Properties();

    hibernateProperties.setProperty("hibernate.hbm2ddl.auto",
      env.getProperty("mysql-hibernate.hbm2ddl.auto"));
    hibernateProperties.setProperty("hibernate.dialect",
      env.getProperty("mysql-hibernate.dialect"));
    hibernateProperties.setProperty("hibernate.show_sql",
      env.getProperty("mysql-hibernate.show_sql") != null
      ? env.getProperty("mysql-hibernate.show_sql") : "false");
    return hibernateProperties;
}

Podemos adicionar as propriedades específicas do Hibernate ao arquivomysql.properties:

mysql-hibernate.dialect=org.hibernate.dialect.MySQLDialect
mysql-hibernate.show_sql=true
mysql-hibernate.hbm2ddl.auto=create-drop

3.5. Condições Personalizadas

Se não quisermos usar nenhuma das condições disponíveis no Spring Boot, também podemosdefine custom conditions by extending the SpringBootCondition class and overriding the getMatchOutcome() method.

Vamos criar uma condição chamadaHibernateCondition para nosso métodoadditionalProperties() que verificará se uma classeHibernateEntityManager está presente no caminho de classe:

static class HibernateCondition extends SpringBootCondition {

    private static String[] CLASS_NAMES
      = { "org.hibernate.ejb.HibernateEntityManager",
          "org.hibernate.jpa.HibernateEntityManager" };

    @Override
    public ConditionOutcome getMatchOutcome(ConditionContext context,
      AnnotatedTypeMetadata metadata) {

        ConditionMessage.Builder message
          = ConditionMessage.forCondition("Hibernate");
        return Arrays.stream(CLASS_NAMES)
          .filter(className -> ClassUtils.isPresent(className, context.getClassLoader()))
          .map(className -> ConditionOutcome
            .match(message.found("class")
            .items(Style.NORMAL, className)))
          .findAny()
          .orElseGet(() -> ConditionOutcome
            .noMatch(message.didNotFind("class", "classes")
            .items(Style.NORMAL, Arrays.asList(CLASS_NAMES))));
    }
}

Então podemos adicionar a condição ao métodoadditionalProperties():

@Conditional(HibernateCondition.class)
Properties additionalProperties() {
  //...
}

3.6. Condições de aplicação

Também podemosspecify that the configuration can be loaded only inside/outside a web context, adicionando a anotação@ConditionalOnWebApplication ou@ConditionalOnNotWebApplication.

4. Testando a configuração automática

Vamos criar um exemplo muito simples para testar nossa configuração automática. Vamos criar uma classe de entidade chamadaMyUser, e uma interfaceMyUserRepository usando Spring Data:

@Entity
public class MyUser {
    @Id
    private String email;

    // standard constructor, getters, setters
}
public interface MyUserRepository
  extends JpaRepository { }

Para habilitar a configuração automática, podemos usar uma das anotações@SpringBootApplication ou@EnableAutoConfiguration:

@SpringBootApplication
public class AutoconfigurationApplication {
    public static void main(String[] args) {
        SpringApplication.run(AutoconfigurationApplication.class, args);
    }
}

A seguir, vamos escrever um testeJUnit que salva uma entidadeMyUser:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(
  classes = AutoconfigurationApplication.class)
@EnableJpaRepositories(
  basePackages = { "com.example.autoconfiguration.example" })
public class AutoconfigurationTest {

    @Autowired
    private MyUserRepository userRepository;

    @Test
    public void whenSaveUser_thenOk() {
        MyUser user = new MyUser("[email protected]");
        userRepository.save(user);
    }
}

Como não definimos nossa configuraçãoDataSource, o aplicativo usará a configuração automática que criamos para se conectar a um banco de dadosMySQL chamadomyDb.

A string de conexão contém a propriedadecreateDatabaseIfNotExist=true, portanto, o banco de dados não precisa existir. No entanto, o usuáriomysqluser ou aquele especificado por meio da propriedademysql.user se estiver presente, precisa ser criado.

Podemos verificar o log do aplicativo para ver se a fonte de dadosMySQL está sendo usada:

web - 2017-04-12 00:01:33,956 [main] INFO  o.s.j.d.DriverManagerDataSource - Loaded JDBC driver: com.mysql.cj.jdbc.Driver

5. Desativando classes de configuração automática

Se quiséssemosexclude the auto-configuration from being loaded, poderíamos adicionar a anotação@EnableAutoConfiguration com o atributoexclude ouexcludeName a uma classe de configuração:

@Configuration
@EnableAutoConfiguration(
  exclude={MySQLAutoconfiguration.class})
public class AutoconfigurationApplication {
    //...
}

Outra opção para desativar as configurações automáticas específicas é definir a propriedadespring.autoconfigure.exclude:

spring.autoconfigure.exclude=com.example.autoconfiguration.MySQLAutoconfiguration

6. Conclusões

Neste tutorial, mostramos como criar uma configuração automática Spring Boot personalizada. O código-fonte completo do exemplo pode ser encontradoover on GitHub.

O teste JUnit pode ser executado usando o perfilautoconfiguration:mvn clean install -Pautoconfiguration.