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 independientemente 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 automatizació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, Java Spring Boot: A Pro-Level Guide to Java Spring Boot: Advanced Patterns and Best Practices está orientado a aplicar Spring Bot en el contexto de microservicios aunque muchos conceptos aplicables en una aplicación de monolito modular.
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/.
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.