Poniendo prueba la compatibilidad hacia atrás y hacia adelante de Java EE

Por Victor Orozco
Publicado en Septiembre 2018


Revisado por Elder Moraes




Uno de los conceptos más interesantes que hicieron y hacen a Java EE atractivo para entornos empresariales es su gran compatibilidad hacia atras, lo cual permite asegurarse que años de investigación y desarrollo pueden ser utilizados en posteriores proyectos.

No entanto, uno de los hechos menos entendidos es que Java EE no es un framework, sino un conjunto de APIs pre-seleccionadas que pueden ser utilizadas como base y extendidas con nuevas APIs basadas en EE -e.g Eclipse MicroProfile, DeltaSpike- asi como mejoras especificas de distintos proveedores (implementadores) de servidores de aplicaciones Java EE -e.g. Hazelcast en Payara, Coherence en Weblogic, Infinispan en Wildfly-.

En esta linea, este articulo fue motivado por una serie de consultas de mi equipo de desarrollo, específicamente acerca de la compatibilidad de APIs "viejas" con extensiones "nuevas" en el mundo Java EE:

¿Es posible implementar un artefacto que utiliza MicroProfile en Java EE 7? ¿Sera un artefacto compatible con Java EE 8? ¿Que tan compatible es un programa de Java EE 7 con un servidor de aplicaciones Java EE 8?

Para la respuesta, se ha preparado una prueba de concepto que demuestra las características de compatibilidad inherentes a Java EE.


¿Es Java EE compatible hacia atras? ¿Es posible asumir una migración segura desde EE 7 hacia EE 8?



Una de las reglas pervasivas en el mundo de TI es "si no esta roto, no lo arregle", sin embargo el roto es bastante relativo en relación a la seguridad, errores y caracteristicas.

En el caso de Java EE, las vulnerabilidades de seguridad y errores son actualizados a través de canales especificos de los implementadores de Java EE, manteniendo la compatibilidad de caracteristicas a través del API EE, siendo este tipo de actualizaciones las que se consideran con mayor seguridad y deben ser aplicadas proactivamente.

Sin embargo, una vez que una nueva versión de Java EE es publicada, cada uno de los implementadores publica su propio calendario y mapa de ruta para sus productos, siendo los responsables directos de futuras actualizaciones para sus usuarios y esperandose que eventualmente todos los creadores de software actualicen su stack técnologico hacia una nueva versión de Java EE.

Para esto, Java EE tiene un conjunto completo de requerimientos de compatibilidad hacia atras, para implementadores, lideres de especificaciones y contribuyentes, siendo especialmente utiles ya que en cada nueva versión de Java EE se entregan:

  • Nuevas APIs (Batch en EE 7 o JSON-B en EE 8)
  • APIs que simplemente no cambian y son incluidas en la siguiente versión de Java EE (Batch en EE 8)
  • APIs con actualizaciones menores (Bean Validation en EE 8)
  • APIS con nuevas caracteristicas e interfaces/metodos (clientes reactivos para JAX-RS en EE 8)

De acuerdo a los requerimientos de compatibilidad, si el código de un proyecto implementa unicamente caracteristicas a través de APIs estandar de Java EE, se garantiza la comptaibilidad completa a nivel de código fuente, compatibilidad binaria y compatibilidad de comportamiento, para cualquier aplicación que utilice una versión de la especificación (o al menos esa es la idea).



Probando la compatibilidad con una implementación "compleja"


Para probar estas asumpciones, he preparado una prueba de concepto que implementa:

  • Servlets (actualizados en EE 8)
  • JAX-RS (actualizados en EE 8)
  • JPA (cambios minimos en EE 8)
  • Batch (sin cambios en EE 8)
  • MicroProfile Config (extensión)
  • DeltaSpike Data (extensión)
Batch Structure

Batch Structure



El objetivo de la aplicación es cargar un conjunto de datos de IMDB desde un archivo separado por comas utilizando operaciones en background para almacenar los registros en Derby(Payara 4) y H2(Payara 5) utilizando el datasource predeterminado jdbc/__default.

Como referencia, el proyecto Maven completo esta disponible en GitHub.



Primera parte: Carga de archivo

La prueba de concepto a) Implementa un servlet multipart que recibe el archivo con registros desde un formulario HTML plano, b) Almacena el archivo utilizando MicroProfile para leer la URL de escritura para el archivo cargado e c) Invoca un Batch Job denominado csvJob:

@WebServlet(name = "FileUploadServlet", urlPatterns = "/upload")
@MultipartConfig
public class FileUploadServlet extends HttpServlet {
    @Inject
    @ConfigProperty(name = "file.destination", defaultValue = "/tmp/")
    private String destinationPath;

    @Inject
    private Logger logger;

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws 
	 ServletException, IOException {
        String description = request.getParameter("description");
        Part filePart = request.getPart("file");
        String fileName = Paths.get(filePart.getSubmittedFileName()).getFileName().toString();


        //Almacenamos mediante buffered streams para acelerar el proceso de escritura
        try(InputStream fin = new BufferedInputStream(filePart.getInputStream());
                OutputStream fout = new BufferedOutputStream(new FileOutputStream
				 (destinationPath.concat(fileName)))){

            byte[] buffer = new byte[1024*100];//100kb per chunk
            int lengthRead;
            while ((lengthRead = fin.read(buffer)) > 0) {
                fout.write(buffer,0,lengthRead);
                fout.flush();
            }

            response.getWriter().write("File written: " + fileName);

            //Carga de archivo
            JobOperator jobOperator = BatchRuntime.getJobOperator();
            Properties props = new Properties();
            props.setProperty("csvFileName", destinationPath.concat(fileName));
            response.getWriter().write("Batch job " + jobOperator.start("csvJob", props));
            logger.log(Level.WARNING, "Firing csv bulk load job - " + description );

        }catch (IOException ex){
            logger.log(Level.SEVERE, ex.toString());

            response.getWriter().write("The error");
            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
        }


    }

}



El formulario por otra parte es bastante simple, aprovechando los atributos enctype y method en HTML5.

<h1>CSV Batchee Demo</h1>
<form action="upload" method="post" enctype="multipart/form-data">
    <div class="form-group">
        <label for="description">Description</label>
        <input type="text" id="description" name="description" />
    </div>
    <div class="form-group">
        <label for="file">File</label>
        <input type="file" name="file" id="file"/>
    </div>

    <button type="submit" class="btn btn-default">Submit</button>
</form>




Segunda parte: Batch Job, JTA y JPA

Como se describe en el tutorial de Java EE, los jobs(procesos) Batch tipicos estan compuestos por pasos, y estos a su vez implementan un proceso de tres fases que involucran un reader, un processor y un writer que trabaja por chunks (bloques).

Las tareas Batch son definidas utilizando descriptores XML, cuya ruta debe ser resources/META-INF/batch-jobs/[nombre tarea].xml, los elementos de la triada reader-writer-processor a su vez deben ser implementados utilizando beans Named con CDI.

<?xml version="1.0" encoding="UTF-8"?>
<job id="csvJob" xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/jobXML_1_0.xsd"
    version="1.0">
    
    <step id="loadAndSave" >
        <chunk item-count="5">
            <reader ref="movieItemReader"/>
            <processor ref="movieItemProcessor"/>
            <writer ref="movieItemWriter"/>
        </chunk>
    </step>
</job>



MovieItemReader leé el archivo csv linea por linea, y al mismo tiempo prepara los resultados en instancias del objeto Movie para la siguiente etapa. Notese tambien que los métodos readItem y checkpointInfo son sobreescritos para asegurarse que la tarea reinicie apropiadamente.

@Named
public class MovieItemReader extends AbstractItemReader {

    @Inject
    private JobContext jobContext;

    @Inject
    private Logger logger;

    private FileInputStream is;
    private BufferedReader br;
    private Long recordNumber;

    @Override
    public void open(Serializable prevCheckpointInfo) throws Exception {
        recordNumber = 1L;
        JobOperator jobOperator = BatchRuntime.getJobOperator();
        Properties jobParameters = jobOperator.getParameters(jobContext.getExecutionId());
        String resourceName = (String) jobParameters.get("csvFileName");
        is = new FileInputStream(resourceName);
        br = new BufferedReader(new InputStreamReader(is));

        if (prevCheckpointInfo != null)
            recordNumber = (Long) prevCheckpointInfo;
        for (int i = 0; i < recordNumber; i++) {
            br.readLine();
        }
        logger.log(Level.WARNING, "Reading started on record " + recordNumber);
    }

    @Override
    public Object readItem() throws Exception {

        String line = br.readLine();

        if (line != null) {
            String[] movieValues = line.split(",");
            Movie movie = new Movie();
            movie.setName(movieValues[0]);
            movie.setReleaseYear(movieValues[1]);
            
            // Incrementamos el record para avanzar una linea
            recordNumber++;
            return movie;
        }
        return null;
    }

    @Override
    public Serializable checkpointInfo() throws Exception {
            return recordNumber;
    }
}



Dado que esta es una prueba de concepto, la parte processor simplemente convierte el titulo de la pelicula a mayusculas y para simular demora en las tareas, el thread de ejecución se suspende medio segundo:

@Named
public class MovieItemProcessor implements ItemProcessor {

  @Inject
  private JobContext jobContext;

    @Override
  public Object processItem(Object obj) 
          throws Exception {
      Movie inputRecord =
              (Movie) obj;
      
      //"Procesamient complejo"
      inputRecord.setName(inputRecord.getName().toUpperCase());
      Thread.sleep(500);
        
      return inputRecord;
  } 
}



Como fase final, cada chunk es escrito mediante MovieItemWriter utilizando para ello un repositorio con DeltaSpike.

@Named
public class MovieItemWriter extends AbstractItemWriter {

    @Inject
    MovieRepository movieService;
    
    @Inject
    Logger logger;

    public void writeItems(List list) throws Exception {
        for (Object obj : list) {
            logger.log(Level.INFO, "Writing " + obj);
            movieService.save((Movie)obj);
        }
    }
}



Para referencia, este es el objeto Movie.

@Entity
@Table(name="movie")
public class Movie implements Serializable {

    @Override
    public String toString() {
        return "Movie [name=" + name + ", releaseYear=" + releaseYear + "]";
    }

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy= GenerationType.AUTO)
    @Column(name="movie_id")
    private int id;
    
    @Column(name="name")
    private String name;
    
    @Column(name="release_year")
    private String releaseYear;

    //Getters and setters



El Datasource por defecto es configurado en resources/META-INF/persistence.xml, notese que se utiliza un Data Source transaccional mediante JTA:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence 
			http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
             version="2.1">

    <persistence-unit name="batchee-persistence-unit" transaction-type="JTA">
        <description>BatchEE Persistence Unit</description>
        <jta-data-source>jdbc/__default</jta-data-source>
        <exclude-unlisted-classes>false</exclude-unlisted-classes>
         <properties>
              <property name="javax.persistence.schema-generation.database.action" 
				value="drop-and-create"/>
              <property name="javax.persistence.schema-generation.scripts.action" 
				value="drop-and-create"/>
              <property name="javax.persistence.schema-generation.scripts.create-target" 
				value="sampleCreate.ddl"/>
              <property name="javax.persistence.schema-generation.scripts.drop-target" 
				value="sampleDrop.ddl"/>
            </properties>
    </persistence-unit>
</persistence>



Para probar las operaciones de marshalling hacia JSON, se implementa tambien un endpoint con JAX-RS y el metodo GET, el repositorio es definido mediante DeltaSpike.

@Path("/movies")
@Produces({ "application/xml", "application/json" })
@Consumes({ "application/xml", "application/json" })
public class MovieEndpoint {
    
    @Inject
    MovieRepository movieService;

    @GET
    public List listAll(@QueryParam("start") final Integer startPosition,
            @QueryParam("max") final Integer maxResult) {
        final List movies = movieService.findAll();
        return movies;
    }

}



El repositorio:

@Repository(forEntity = Movie.class)
public abstract class MovieRepository extends AbstractEntityRepository<Movie, Long> {
    
    @Inject
    public EntityManager em;
}




Prueba 1: Servidor Java EE 7 con proyecto Java EE 7


Dado que el objetivo es probar la compatibilidad real hacia atras (nivel de proyecto menor que el servidor) y hacia adelante (extensiones), como primera prueba se despliega el proyecto con las siguientes dependencias en el archivo pom.xml, la prueba POM EE 7 vs Servidor EE 7 se ejecuta unicamente para verificar que el proyecto funcione adecuadamente:

<dependencies>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>7.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.eclipse.microprofile</groupId>
            <artifactId>microprofile</artifactId>
            <version>1.3</version>
            <type>pom</type>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.deltaspike.modules</groupId>
            <artifactId>deltaspike-data-module-api</artifactId>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.deltaspike.modules</groupId>
            <artifactId>deltaspike-data-module-impl</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.deltaspike.core</groupId>
            <artifactId>deltaspike-core-api</artifactId>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.deltaspike.core</groupId>
            <artifactId>deltaspike-core-impl</artifactId>
            <scope>runtime</scope>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.apache.deltaspike.distribution</groupId>
                <artifactId>distributions-bom</artifactId>
                <version>${deltaspike.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <build>
        <finalName>batchee-demo</finalName>
    </build>
    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <deltaspike.version>1.8.2</deltaspike.version>
        <failOnMissingWebXml>false</failOnMissingWebXml>
    </properties>



Luego de desplegar la aplicación en un servidor Payara 4, la misma se ejecuta apropiadamente, permitiendo cargar los datos. Como se demuestra en las siguientes capturas de pantalla durante la ejecución del batch Job:

Payara 4 Demo 1

Payara 4 Demo 1




Payara 4 Demo 2

Payara 4 Demo 2





Prueba 2: Servidor Java EE 8 con POM Java EE 7


Para probar la compabilidad binaria real, la aplicación se despliega sin cambios en Payara 5 (Java EE 8)en este lanzamiento de Payara, tambien se remeplaza la base de datos en memoria, pasando desde Apache Derby hacia H2.

Como esperado y de acuerdo a las lineas de compatibilidad de Java EE, la aplicación funciona sin inconvenientes:

Payara 5 Demo 1

Payara 5 Demo 1




Payara 5 Demo 2


Payara 5 Demo 2




Para verificar, lanzamos una consulta a través de SQuirrel SQL:

 
SQuirrel SQL Demo




Test 3: Servidor Java EE 8 con POM Java EE 8


Finalmente, habilitamos y actualizamos nuestro proyecto hacia Java EE 8, basicamente cambiaremos la versión de Java EE en el archivo pom.xml

<dependency>
    <groupId>javax</groupId>
    <artifactId>javaee-api</artifactId>
    <version>8.0</version>
    <scope>provided</scope>
</dependency>


De nuevo, la aplicación simplemente funciona:

Payara 5 Java EE 8


Payara 5 Java EE 8


Todo esto es posible gracias a la definición e implementación de estandares en Java EE.




Víctor Orozco es un ingeniero de software Guatemalteco. Ex becario de la OEA, Msc. en ciencias de la computación, OCP Java SE 8 y actual director de tecnología en Nabenik. Con 10 años de experiencia en el sector Enterprise, como profesor universitario, desarrollador y arquitecto de software es uno de los fundadores y actual líder del grupo de usuarios Java de Guatemala, ganadores del premio Duke's Choice Award en 2016.

Este artículo ha sido revisado por el equipo de productos Oracle y se encuentra en cumplimiento de las normas y prácticas para el uso de los productos Oracle.