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