1. Introducción

En este post analizaremos como funcionan las clases auto configurables en Spring, cómo crearlas y hacer uso de ellas.

Os podéis descargar el código de ejemplo de mi GitHub aquí.

Tecnologías empleadas:

  • Java 8
  • Gradle 3.1
  • SpringBoot 1.5.2.RELEASE
  • Spring 4.3.7.RELEASE

2. ¿Qué son las clases auto configurables?

Son clases con anotación @Configuration que no se encuentran en subpaquetes del main de la aplicación por lo que a priori no serán escaneadas y por tanto consideradas al levantar el contexto de Spring. Sin embargo en muchas ocasiones podemos ver como Spring crea en el contexto clases que se encuentran en dependencias que incluimos sin que nosotros realicemos ninguna configuración especial. ¿Cómo consigue esto Spring?

La clave está en la dependencia spring-boot-autoconfiguration que es añadida al incluir spring-boot-starter-web

Dentro de ésta podemos encontrar el fichero META-INF/spring.factories en donde se especifican bajo la propiedad org.springframework.boot.autoconfigure.EnableAutoConfiguration un conjunto de clases autoconfigurables que serán cargadas cuando la anotación @EnableAutoConfiguration sea especificada.

META-INF/spring.factories en spring-boot-autoconfigure

...
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.cloud.CloudAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\
org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\
org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.ldap.LdapDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.ldap.LdapRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.solr.SolrRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration,\
org.springframework.boot.autoconfigure.elasticsearch.jest.JestAutoConfiguration,\
org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\
org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\
org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,\
org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration,\
org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration,\
org.springframework.boot.autoconfigure.hazelcast.HazelcastJpaDependencyAutoConfiguration,\
org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration,\
org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration,\
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration,\
org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration,\
org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration,\
org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration,\
org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration,\
org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderValidatorAutoConfiguration,\
org.springframework.boot.autoconfigure.mobile.DeviceResolverAutoConfiguration,\
org.springframework.boot.autoconfigure.mobile.DeviceDelegatingViewResolverAutoConfiguration,\
org.springframework.boot.autoconfigure.mobile.SitePreferenceAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\
org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\
org.springframework.boot.autoconfigure.reactor.ReactorAutoConfiguration,\
org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.SecurityFilterAutoConfiguration,\
org.springframework.boot.autoconfigure.security.FallbackWebSecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.OAuth2AutoConfiguration,\
org.springframework.boot.autoconfigure.sendgrid.SendGridAutoConfiguration,\
org.springframework.boot.autoconfigure.session.SessionAutoConfiguration,\
org.springframework.boot.autoconfigure.social.SocialWebAutoConfiguration,\
org.springframework.boot.autoconfigure.social.FacebookAutoConfiguration,\
org.springframework.boot.autoconfigure.social.LinkedInAutoConfiguration,\
org.springframework.boot.autoconfigure.social.TwitterAutoConfiguration,\
org.springframework.boot.autoconfigure.solr.SolrAutoConfiguration,\
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration,\
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,\
org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration,\
org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration,\
org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration,\
org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration,\
org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.web.HttpEncodingAutoConfiguration,\
org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration,\
org.springframework.boot.autoconfigure.web.MultipartAutoConfiguration,\
org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.web.WebClientAutoConfiguration,\
org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.WebSocketAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.WebSocketMessagingAutoConfiguration,\
org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration
...
Un caso que me pasó hace poco ocurrió cuando estaba configurando una conexión a elasticsearch utilizando Spring Data. Lo que ocurria era que cuando añadia la dependencia org.springframework.boot:spring-boot-starter-data-elasticsearch se configuraban de manera automática objetos relacionados con la conexión como ElasticsearchTemplate. Esto ocurre porque estas dependencias hacen uso de las clases Autoconfiguration. Veremos como crearlas y como configurarlas para que se tengan en cuenta en el arranque del contexto.

Si analizamos por ejemplo la clase ElasticsearchAutoConfiguration veremos que solamente se creará un objeto de la clase Client siempre y cuando encuentre en el classpath las clases Client, TransportClientFactoryBean y NodeClientFactoryBean. Esto se indica a través de la anotación @ConditionalOnClass

@Configuration
@ConditionalOnClass({ Client.class, TransportClientFactoryBean.class,
		NodeClientFactoryBean.class })
@EnableConfigurationProperties(ElasticsearchProperties.class)
public class ElasticsearchAutoConfiguration implements DisposableBean {

        ...

	@Bean
	@ConditionalOnMissingBean
	public Client elasticsearchClient() {
		try {
			return createClient();
		}
		catch (Exception ex) {
			throw new IllegalStateException(ex);
		}
	}

        ...
}

Esto ocurrirá si añadimos la dependencia de Spring data de Elasticsearch ya que proveeremos las clases Client, TransportClientFactoryBean y NodeClientFactoryBean mencionadas anteriormente al classpath.

...
dependencies {
   ...
   compile("org.springframework.boot:spring-boot-starter-data-elasticsearch")
   ...
}
...

3. Creación y uso de clases auto configurables

group 'org.hernandez.ramirez.jorge.pruebaconcepto'
version '1.0-SNAPSHOT'

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.2.RELEASE")
    }
}

apply plugin: 'java'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'

sourceCompatibility = 1.8

springBoot {
    mainClass = "com.jorgehernandezramirez.spring.springboot.autoconfiguration.application.Application"
}

repositories {
    mavenCentral()
}

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
    testCompile("org.springframework.boot:spring-boot-starter-test")
}

Las clases que forman parte de la capa de servicios

package com.jorgehernandezramirez.spring.springboot.autoconfiguration.application.service.dto;

public class UserDto {

    private String username;

    private String password;

    public UserDto(){
        super();
    }

    public UserDto(String username, String password) {
        this.username = username;
        this.password = password;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        UserDto userDto = (UserDto) o;

        if (username != null ? !username.equals(userDto.username) : userDto.username != null) return false;
        return password != null ? password.equals(userDto.password) : userDto.password == null;
    }

    @Override
    public int hashCode() {
        int result = username != null ? username.hashCode() : 0;
        result = 31 * result + (password != null ? password.hashCode() : 0);
        return result;
    }

    @Override
    public String toString() {
        return "UserDto{" +
                "username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

package com.jorgehernandezramirez.spring.springboot.autoconfiguration.application.service.api;

import com.jorgehernandezramirez.spring.springboot.autoconfiguration.application.service.dto.UserDto;

import java.util.List;

/**
 * Api de usuarios
 */
public interface IUserService {

    /**
     * Método que debe devolver la lista de usuarios en el sistema
     * @return
     */
    List<UserDto> getUsers();
}
package com.jorgehernandezramirez.spring.springboot.autoconfiguration.application.service.impl;

import com.jorgehernandezramirez.spring.springboot.autoconfiguration.application.service.api.IUserService;
import com.jorgehernandezramirez.spring.springboot.autoconfiguration.application.service.dto.UserDto;

import java.util.Arrays;
import java.util.List;

public class UserServiceDummyImpl implements IUserService {

    public UserServiceDummyImpl(){
        //For Spring
    }

    @Override
    public List<UserDto> getUsers() {
        return Arrays.asList(new UserDto("1", "Jorge"),
                new UserDto("2", "Jose"));
    }
}

Controlador donde exponemos el servicio de usuarios vía rest.

package com.jorgehernandezramirez.spring.springboot.autoconfiguration.application.controller;

import com.jorgehernandezramirez.spring.springboot.autoconfiguration.application.service.api.IUserService;
import com.jorgehernandezramirez.spring.springboot.autoconfiguration.application.service.dto.UserDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping(value = "/user")
public class UserController {

    @Autowired
    private IUserService userService;

    public UserController(){
        //For Java
    }

    @RequestMapping
    public List<UserDto> getUsers(){
        return userService.getUsers();
    }
}

Main que arranca SpringBoot. Hacemos uso de la anotación @SpringBootApplication que contiene a su vez la anotación @EnableAutoConfiguration

package com.jorgehernandezramirez.spring.springboot.autoconfiguration.application;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

    public Application(){
        //For Spring
    }

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

Creamos nuestro fichero @Configuration en un package distinto al que se encuentra nuestro main

  • Application.java -> com.jorgehernandezramirez.spring.springboot.autoconfiguration.configuration
  • UserAutoConfiguration -> com.jorgehernandezramirez.spring.springboot.autoconfiguration.applicaiton
package com.jorgehernandezramirez.spring.springboot.autoconfiguration.configuration;

import com.jorgehernandezramirez.spring.springboot.autoconfiguration.application.service.api.IUserService;
import com.jorgehernandezramirez.spring.springboot.autoconfiguration.application.service.impl.UserServiceDummyImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class UserAutoConfiguration {

    private static final Logger LOGGER = LoggerFactory.getLogger(UserConfiguration.class);

    @Bean
    public IUserService buildUserService(){
        LOGGER.info("Building user service dummy impl...");
        return new UserServiceDummyImpl();
    }
}

Ahora viene lo verdaderamente importante!. Como ya hemos comentado spring considera las clases con anotación @Configuration que se encuentran en subpaquetes del main que arranca SpringBoot. Sin embargo si tenemos dichos ficheros en otros paquetes y queremos que se carguen en el arranque del contexto deberemos:

1. Utilizar @EnableAutoConfiguration
Esta anotación forma parte de la definición de @SpringBootApplication por lo que no tendremos que añadir ninguna anotación adicional

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
		@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
   ...
}

2. Crear fichero META-INF/spring.factories
Indicar en el fichero META-INF/spring.factories que nuestra clase de configuración se cargue cuando hacemos uso de la anotación @EnableAutoConfiguration

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.jorgehernandezramirez.spring.springboot.autoconfiguration.configuration.UserAutoConfiguration

Si, a pesar de tener nuestra clase en el fichero spring.factories, no queremos cargarla en el contexto deberemos hacer la propiedad exclude en la anotación @EnabledAutoConfiguration

@EnableAutoConfiguration(exclude = UserAutoConfiguration.class)
@SpringBootApplication
public class Application {
   ...
}

3. Testeando la aplicación

package com.jorgehernandezramirez.spring.springboot.autoconfiguration;

import com.jorgehernandezramirez.spring.springboot.autoconfiguration.application.Application;
import com.jorgehernandezramirez.spring.springboot.autoconfiguration.application.service.dto.UserDto;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.List;

import static org.junit.Assert.assertEquals;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class,
        webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UserControllerTest {

    @Autowired
    private TestRestTemplate testRestTemplate;

    @Test
    public void shouldReturnDummyUserImpl() throws Exception {
        final ResponseEntity<List<UserDto>> userResponse = testRestTemplate.exchange("/user",
                HttpMethod.GET, null, new ParameterizedTypeReference<List<UserDto>>() {
                });
        assertEquals(2, userResponse.getBody().size());
    }
}

4. @ConditionalOnClass

La anotación @ConditionalOnClass permite indicarle a Spring que considere la clase con la anotación @Configuration siempre y cuando encuentre, en el classpath, la lista de clases que se le especifica.

Una estrategia común para cargar beans en el contexto es hacer uso de las clases AutoConfiguration. Para ello se deberá

  • Asignar el valor de la clase de configuración a la propiedad org.springframework.boot.autoconfigure.EnableAutoConfiguration en el fichero spring.factories
  • Usar la anotación @ConditionalOnClass para indicar que se cargue la clase de configuración cuando se encuentren ciertas clases en el classpath

Ejemplo

Cuando añadimos las siguientes dependencias a nuestro build.gradle

...
dependencies {
   compile("org.springframework.boot:spring-boot-starter")
   compile("org.springframework.boot:spring-boot-starter-data-elasticsearch")
}
...

la clase ElasticsearchAutoConfiguration es considerada por Spring ya que encuentra en el classpath las clases Client, TransportClientFactoryBean y NodeClientFactoryBean provistas por la dependencia org.springframework.boot:spring-boot-starter-data-elasticsearch

@Configuration
@ConditionalOnClass({ Client.class, TransportClientFactoryBean.class,
		NodeClientFactoryBean.class })
@EnableConfigurationProperties(ElasticsearchProperties.class)
public class ElasticsearchAutoConfiguration implements DisposableBean {
...
}