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 ...
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());
}
}