Aplicación Java autocontenida con Spring Boot

Escrito por picodotdev el , actualizado el .
java planeta-codigo programacion spring
Enlace permanente Comentarios

Si queremos una aplicación Java autocontenida ya sea una aplicación de linea de comandos, de escritorio o aplicación web que use el contenedor de dependencias de Spring podemos usar Spring Boot. Además de inicializar el contenedor IoC de Spring, Spring Boot proporciona en una aplicación web elegir el servidor de aplicaciones de entre el por defecto Tomcat y los seleccionables Jetty y Undertow junto con algunas funcionalidades más.

Tradicionalmente las aplicaciones Java web han sido instaladas en un contenedor de servlets como Tomcat o Jetty y Wildfly, JBoss o Weblogic si necesita más servicios que son ofrecidos por la plataforma Java EE completa como JMS, JPA, JTA o EJB. Aunque las aplicaciones se ejecutan independientemente unas de otras comparten el entorno de ejecución del servidor de aplicaciones, algunas aplicaciones no necesitarán todos los servicios que ofrecen los servidores de aplicaciones en su implementación del perfil completo Java EE y algunas nuevas aplicaciones pueden necesitar hacer uso de una nueva versión de un servicio como JMS con funcionalidades mejoradas. En el primer caso algunos servicios son innecesarios y en el segundo la actualización del servidor de aplicaciones se ha de producir para todas las aplicaciones que en él se ejecuten o tener varias versiones del mismo servidor de aplicaciones e ir instalando las aplicaciones en la versión del servidor según las versiones de los servicios para las que se desarrolló la aplicación.

Los microservicios proponen una aproximación diferente al despliegue de las aplicaciones prefiriendo entre otros aspectos que sean autocontenidos de tal forma que puedan evolucionar independieintemente unas de otras. Se puede ejecutar una aplicación web Java de forma autocontenida con la versión embebida de Tomcat, Jetty también ofrece una versión embebible que puede usarse de forma similar de tal modo que ya no necesitemos instalar previamente además del JDK la versión del servidor de aplicaciones que necesite.

Otra forma de poder hacer la aplicación autocontenida es con Spring Boot, internamente usa una versión embebible del servidor de aplicaciones de la misma forma que lo podemos usar directamente, la ventaja al usar Spring Boot es que soporta Tomcat, Jetty o Undertow y pasar de usar uno a otro es muy sencillo y prácticamente transparente para la aplicación, además proporciona algunas características adicionales como inicializar el contenedor IoC de Spring, configuración, perfiles para diferentes entornos (desarrollo, pruebas, producción), monitorización y métricas del servidor de aplicaciones y soporte para la herramienta de automaticación Gradle entre algunas más. En el siguiente ejemplo mostraré como ejecutar una aplicación Java y una aplicación web Java con Spring Boot que usa jOOQ como alternativa a Hibernate, Apache Tapestry como framework web, Liquibase para crear el esquema y tablas de la base de datos y por simplicidad H2 como base de datos.

Los mostrado en este artículo es solo una pequeña parte de lo que ofrece Sring Boot, en el libro Spring Boot in Action se comenta en mucho más detalle y de forma didáctica, un libro muy recomendable para adentrarse rápidamente en ste nuevo mundo de posibilidades.

Spring Boot proporciona un plugin, spring-boot, para Gradle que deberemos añadir al archivo build.gradle, a partir de este momento dispondremos algunas tareas adicionales en el proyecto como bootRun para ejecutar la aplicación desde Gradle (similar a la opción run y el parámetro mainClassName que añade el plugin application) y bootRepackage para poder ejecutar la aplicación con el comando java -jar.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import org.jooq.util.GenerationTool
import org.jooq.util.jaxb.Configuration
import org.jooq.util.jaxb.CustomType
import org.jooq.util.jaxb.Database
import org.jooq.util.jaxb.ForcedType
import org.jooq.util.jaxb.Generate
import org.jooq.util.jaxb.Generator
import org.jooq.util.jaxb.Jdbc
import org.jooq.util.jaxb.Target

apply plugin: 'eclipse'
apply plugin: 'java'
apply plugin: 'application'
apply plugin: 'spring-boot'

mainClassName = 'io.github.picodotdev.blogbitix.springboot.Main'

def versions = [
    'gradle':              '2.9',
    'tapestry':            '5.4-rc-1',
    'spring':              '4.2.3.RELEASE',
    'spring_boot':         '1.3.0.RELEASE',
    'hibernate_validator': '5.2.2.Final',
    'jooq':                '3.7.1',
    'guava':               '18.0',
    'h2':                  '1.4.190',
    'slf4j':               '1.7.13',
    'log4j2':              '2.4.1',
    'servlet_api':         '3.1.0'
]

...

repositories {
    mavenCentral()
    
    // For access to Apache Staging (Preview) packages
    maven {
        name 'Apache Staging'
        url 'https://repository.apache.org/content/groups/staging'
    }
}

dependencies {
    // Spring
    compile "org.springframework:spring-context:$versions.spring"
    compile "org.springframework:spring-jdbc:$versions.spring"
    compile "org.springframework:spring-tx:$versions.spring"
    compile("org.springframework.boot:spring-boot-starter:$versions.spring_boot") { exclude(group: 'ch.qos.logback') }
    compile("org.springframework.boot:spring-boot-starter-web:$versions.spring_boot") { exclude(group: 'ch.qos.logback') }
    compile("org.springframework.boot:spring-boot-starter-actuator:$versions.spring_boot") { exclude(group: 'ch.qos.logback') }
    compile "org.hibernate:hibernate-validator:$versions.hibernate_validator"
    
    // Tapestry
    compile "org.apache.tapestry:tapestry-core:$versions.tapestry"
    compile "org.apache.tapestry:tapestry-webresources:$versions.tapestry"
    compile "org.apache.tapestry:tapestry-javadoc:$versions.tapestry"
    compile "org.apache.tapestry:tapestry-beanvalidator:$versions.tapestry"
    compile("org.apache.tapestry:tapestry-spring:$versions.tapestry") { exclude(group: 'org.springframework') }
    
    // Database	
    compile "org.jooq:jooq:$versions.jooq"
    compile "org.jooq:jooq-meta:$versions.jooq"
    compile "commons-dbcp:commons-dbcp:1.4"
    runtime "com.h2database:h2:$versions.h2"
    
    // Logging
    compile "org.slf4j:slf4j-api:$versions.slf4j"
    compile "org.apache.logging.log4j:log4j-slf4j-impl:$versions.log4j2"
    compile "org.apache.logging.log4j:log4j-api:$versions.log4j2"
    compile "org.apache.logging.log4j:log4j-core:$versions.log4j2"
    
    // Misc
    compile "org.apache.commons:commons-lang3:3.4"
    compile "javax.servlet:javax.servlet-api:$versions.servlet_api"
}

...
build.gradle

El punto de inicio de una aplicación de Spring Boot es una clase Java con su tradicional método main, en el ejemplo la clase Main. Bastan tres lineas para iniciar la aplicación y una anotación. Anotando con @SpringBootApplication la clase que contiene el método main activaremos Spring Boot y el procesado de las anotaciones de Spring. En el método main estableciendo la clase contexto de la aplicación variaremos el tipo de aplicación AnnotationConfigApplicationContext para una aplicación de linea de comandos o de escritorio y AnnotationConfigWebApplicationContext para las aplicaciones web que inicializará el servidor de aplicaciones embebido. Implementando la interfaz CommandLineRunner en la clase que contiene la anotación SpringBootApplication y su método run será el punto de entrada de la aplicación, en el método recibiremos los parámetros de la linea de comandos. Implementar esta interfaz es opcional en las aplicaciones web.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package io.github.picodotdev.blogbitix.springboot;

import java.time.LocalDateTime;
import java.util.List;

import org.jooq.DSLContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.validation.Errors;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;

import io.github.picodotdev.blogbitix.springboot.jooq.Keys;
import io.github.picodotdev.blogbitix.springboot.jooq.Tables;
import io.github.picodotdev.blogbitix.springboot.jooq.tables.records.DepartmentRecord;
import io.github.picodotdev.blogbitix.springboot.jooq.tables.records.EmployeeDepartmentRecord;
import io.github.picodotdev.blogbitix.springboot.jooq.tables.records.EmployeeRecord;
import io.github.picodotdev.blogbitix.springboot.misc.RecordContainer;
import io.github.picodotdev.blogbitix.springboot.service.AppService;

@SpringBootApplication
public class Main implements CommandLineRunner {

	@Autowired
	private DSLContext context;
	
	@Autowired
	private AppService service;

	@Override
	public void run(String... args) {
        System.out.printf("Number employees: %d%n", service.countEmployees());
        System.out.printf("Number departments: %d%n", service.countDepartments());

        System.out.println();
        System.out.println("# Relations (with 1+N problem)");
        DepartmentRecord department = service.findDepartment(1l);
        List<EmployeeDepartmentRecord> eds = department.fetchChildren(Keys.DEPARTMENT_ID);
        for (EmployeeDepartmentRecord ed : eds) {
            EmployeeRecord employee = ed.fetchParent(Keys.EMPLOYEE_ID);    
            System.out.printf("%s %s%n", employee.getName(), department.getName());
        }

        System.out.println();
        System.out.println("# Multipletables (no 1+N)");
        List<RecordContainer> data = service.findDepartmentEmployees(1l);
        data.stream().forEach((RecordContainer c) -> {
            System.out.printf("%s %s%n", c.getEmployee().getName(), c.getDepartment().getName());
        });

        System.out.println();
        System.out.println("# Validation");
        EmployeeRecord employee = context.newRecord(Tables.EMPLOYEE);
        employee.setBirthday(LocalDateTime.now().plusDays(1));
        Errors errors = service.validate(employee);
        errors.getFieldErrors().stream().forEach(error -> {
            System.out.printf("%s, %s, %s%n", error.getField(), error.getCode(), error.getArguments(), error.getRejectedValue());
        });
        errors.getGlobalErrors().stream().forEach(error -> {
            System.out.printf("%s, %s, %s%n", error.getObjectName(), error.getCode(), error.getArguments());
        });
	}

	public static void main(String[] args) throws Exception {
		SpringApplication application = new SpringApplication(Main.class);
		//application.setApplicationContextClass(AnnotationConfigApplicationContext.class);
		application.setApplicationContextClass(AnnotationConfigWebApplicationContext.class);
		SpringApplication.run(Main.class, args);
	}
}
Main.java

La clase AppConfiguration contiene la definición de beans propios del contenedor de inversión de control de las aplicaciones que serán inyectados en las clases donde se indiquen. Pero además definiendo algunos beans podremos configurar el servidor de aplicaciones embebido y la aplicación. Con el bean ServletContextInitializer podemos definir parámetros de inicialización, filtros, servlets, listeners, propiedades de cookies y obtener información del entorno. Con el bean EmbeddedServletContainerCustomizer podemos añadir páginas de error para estados como 404 o 500, configurar el puerto de servicio, establecer la dirección IP, el contexto de la aplicación, directorio raíz de archivos del servidor web, SSL/TLS y tiempo de vida de las sesiones. Con el bean TomcatConnectorCustomizer se pueden personalizar diferentes parámetros del conector y con el bean TomcatContextCustomizer varios parámetros del contexto que en un Tomcat instalado como paquete de software configuraríamos mediante el archivo de configuración server.xml o context.xml. Para que las peticiones se procesen por el framework web Tapestry se define su filtro en el ejemplo o si fuese el caso un servlet. Toda esta configuración es similar a lo que definimos en el archivo web.xml, pero en código Java al ser validado por el compilador es menos propenso a errores que los archivos de texto xml.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
package io.github.picodotdev.blogbitix.springboot.spring;

import java.io.IOException;
import java.util.EnumSet;

import javax.servlet.DispatcherType;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.SessionTrackingMode;
import javax.sql.DataSource;
import javax.validation.Validator;

import org.apache.catalina.Context;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase;
import org.apache.commons.dbcp.BasicDataSource;
import org.apache.tapestry5.spring.TapestrySpringFilter;
import org.jooq.ConnectionProvider;
import org.jooq.DSLContext;
import org.jooq.SQLDialect;
import org.jooq.impl.DSL;
import org.jooq.impl.DataSourceConnectionProvider;
import org.jooq.impl.DefaultConfiguration;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.context.embedded.ErrorPage;
import org.springframework.boot.context.embedded.ServletContextInitializer;
import org.springframework.boot.context.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatContextCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.support.ResourceTransactionManager;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

import io.github.picodotdev.blogbitix.springboot.service.AppService;
import io.github.picodotdev.blogbitix.springboot.service.DefaultAppService;
import io.github.picodotdev.blogbitix.springboot.validator.EmployeeValidator;

@Configuration
@ComponentScan({ "io.github.picodotdev.blogbitix.springboot" })
@EnableTransactionManagement
public class AppConfiguration {

    @Bean(destroyMethod = "close")
    public DataSource dataSource() {
        BasicDataSource ds = new BasicDataSource();
        ds.setDriverClassName("org.h2.Driver");
        ds.setUrl("jdbc:h2:./misc/database/app");
        ds.setUsername("sa");
        ds.setPassword("sa");
        return ds;
    }

    @Bean
    public ResourceTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean
    public ConnectionProvider connectionProvider(DataSource dataSource) {
        return new DataSourceConnectionProvider(dataSource);
    }

    @Bean
    public org.jooq.Configuration config(ConnectionProvider connectionProvider) {
        DefaultConfiguration config = new DefaultConfiguration();
        config.set(connectionProvider);
        config.set(SQLDialect.H2);
        return config;
    }

    @Bean
    public Validator validator() {
        return new LocalValidatorFactoryBean();
    }

    @Bean
    public DSLContext dsl(org.jooq.Configuration config) {
        return DSL.using(config);
    }

    @Bean
    public ServletContextInitializer initializer() {
        return new ServletContextInitializer() {
            @Override
            public void onStartup(ServletContext servletContext) throws ServletException {
                servletContext.setInitParameter("tapestry.app-package", "io.github.picodotdev.blogbitix.springboot.tapestry");
                servletContext.setInitParameter("tapestry.use-external-spring-context", "true");
                servletContext.addFilter("app", TapestrySpringFilter.class).addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST, DispatcherType.ERROR), false, "/*");
                servletContext.setSessionTrackingModes(EnumSet.of(SessionTrackingMode.COOKIE));
            }
        };
    }

    @Bean
    public EmbeddedServletContainerCustomizer containerCustomizer() {
        return new EmbeddedServletContainerCustomizer() {
            @Override
            public void customize(ConfigurableEmbeddedServletContainer container) {
                ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, "/error404");
                ErrorPage error500Page = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error500");
                container.addErrorPages(error404Page, error500Page);
            }
        };
    }

    @Bean
    public TomcatConnectorCustomizer connectorCustomizer() {
        return new TomcatConnectorCustomizer() {
            @Override
            public void customize(Connector connector) {
            }
        };
    }

    @Bean
    public TomcatContextCustomizer contextCustomizer() {
        return new TomcatContextCustomizer() {
            @Override
            public void customize(Context context) {
            }
        };
    }

    @Bean
    public TomcatEmbeddedServletContainerFactory containerFactory() {
        TomcatEmbeddedServletContainerFactory factory = new TomcatEmbeddedServletContainerFactory();
        factory.addContextValves(new ValveBase() {
            @Override
            public void invoke(Request request, Response response) throws IOException, ServletException {
                getNext().invoke(request, response);
            }
        });
        return factory;
    }

    @Bean
    public AppService appService(DSLContext context, Validator validator, EmployeeValidator employeeValidator) {
        return new DefaultAppService(context, validator, employeeValidator);
    }
}
AppConfiguration.java

No será muy común pero si queremos configurar algunas propiedades internas como las válvulas de Tomcat que funcionalmente es similar a un filtro de una aplicación web Java podemos definir un bean del tipo TomcatEmbeddedServletContainerFactory, con esta factoría además podremos configurar muchas de las propiedades que podemos configurar con ServletContextInitializer y EmbeddedServletContainerCustomizer pero salvo por las válvulas que es específico de Tomcat la forma preferida hacer la configuración es con estas últimas clases.

Si en vez de usar Tomcat queremos usar Jetty o Undertow debemos cambiar las dependencias de la aplicación, excluimos la dependencia transitiva Tomcat y por defecto de spring-boot-starter-web e incluimos la propia del servidor que deseemos. spring-boot-starter-jetty para Jetty y spring-boot-starter-undertow para Undertow. En el siguiente código la configuración a modificar en el archivo build.gradle para ambas.

1
2
3
4
5
6
7
8
9
configurations {
    compile.exclude module: "spring-boot-starter-tomcat"
}

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web:$versions.spring_boot")
    compile("org.springframework.boot:spring-boot-starter-jetty:$versions.spring_boot")
    // ...
}
build-jetty.gradle
1
2
3
4
5
6
7
8
9
configurations {
    compile.exclude module: "spring-boot-starter-tomcat"
}

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web:$versions.spring_boot")
    compile("org.springframework.boot:spring-boot-starter-undertow:$versions.spring_boot")
    // ...
}
build-undertow.gradle

El resto de esta aplicación de ejemplo es propio de jOOQ y de Apache Tapestry. Para inicializar la base de datos H2 antes de ejecutar la aplicación debemos ejecutar la tarea de Gradle updataDatabase que creará las base de datos, esquema y tablas con la herramienta Liquibase.

1
2
$ ./gradlew updateDatabase

update-database.sh

El código fuente del ejemplo completo puedes encontrarlo en el repositorio de ejemplos de Blog Bitix, arrancarlo y acceder con el navegador a la dirección http://localhost:8080/.

1
2
$ ./gradlew run

run.sh

En el siguiente vídeo puede verse como es la salida en la terminal cuando la aplicación se arranca con Gradle y con el comando java -jar.

Añadiendo la dependencia Spring Boot Actuator podemos obtener información de estado y métricas en las aplicaciones Spring Boot.

En un repositorio de GitHub de Spring hay muchos más ejemplos sobre Spring Boot.

Este artículo forma parte de la serie spring-cloud:

  1. Datos de sesión externalizados con Spring Session
  2. Aplicación Java autocontenida con Spring Boot
  3. Configuración de una aplicación en diferentes entornos con Spring Cloud Config
  4. Información y métricas de la aplicación con Spring Boot Actuator
  5. Registro y descubrimiento de servicios con Spring Cloud y Consul
  6. Aplicaciones basadas en microservicios
  7. Registro y descubrimiento de servicios con Spring Cloud Netflix
  8. Servicio de configuración para microservicios con Spring Cloud Config
  9. Recargar sin reiniciar la configuración de una aplicación Spring Boot con Spring Cloud Config
  10. Almacenar cifrados los valores de configuración sensibles en Spring Cloud Config
  11. Tolerancia a fallos en un cliente de microservicio con Spring Cloud Netflix y Hystrix
  12. Balanceo de carga y resilencia en un microservicio con Spring Cloud Netflix y Ribbon
  13. Proxy para microservicios con Spring Cloud Netflix y Zuul
  14. Monitorizar una aplicación Java de Spring Boot con Micrometer, Prometheus y Grafana
  15. Exponer las métricas de Hystrix en Grafana con Prometheus de una aplicación Spring Boot
  16. Servidor OAuth, gateway y servicio REST utilizando tokens JWT con Spring
  17. Trazabilidad en microservicios con Spring Cloud Sleuth
  18. Implementar tolerancia a fallos con Resilience4j
  19. Iniciar una aplicación de Spring Boot en un puerto aleatorio
  20. Utilizar credenciales de conexión a la base de datos generadas por Vault en una aplicación de Spring
  21. Microservicios con Spring Cloud, Consul, Nomad y Traefik
  22. Trazabilidad en servicios distribuidos con Sleuth y Zipkin
Comparte el artículo: