Como hacer una librería Java de forma sencilla

Publicado el 12-04-2023 |

Autor: Pablo Caamaño

En la cotidianidad un desarrollador suele definir constantemente modelos, estructuras y clases que terminan siendo requeridas en otros proyectos, servicios y/o aplicaciones. Esto normalmente se resuelve de forma sencilla, pero incorrecta, haciendo el siempre conocido y querido copy - paste. Lamentablemente, aunque sea una salida rápida, esta práctica termina siendo un problema.

Tener código duplicado por doquier es un dolor de cabeza de cara al mantenimiento, ya que si se requiere hacer un cambio o se encuentra un bug, se tiene que realizar la corrección en todos los lugares donde se duplicó el código. Una de las mejores formas de evitar estos problemas es mediante la definición, disposición e implementación de librerías. Por esto, considero importante compartir la forma más sencilla que conozco de construir una librería. En este caso será realizada en Java, ya que es el lenguaje con el que más a gusto me siento. Sin embargo, salvando las formas propias del lenguaje, esto se suele aplicar en la mayoría de proyectos, por lo cual si te interesa conocer el concepto te invito a seguir leyendo.

¿Qué es una librería?

Una librería o biblioteca, ya que su nombre en inglés es "library", es una artefacto que contiene firmas y clases comunes para múltiples usos. Es decir, que provee interfaces que luego son implementadas, u objetos planos de uso común para diferentes implementaciones.

Si se analiza desde una implementación Java, nos encontraremos con que una librería puede contener interfaces de servicios, clases utilitarias y POJOs (como por ejemplo modelos o dto’s). Lo más importante es entender que una librería ayuda a definir por única vez cierta clase y todos los proyectos que requieran alguno de sus componentes, simplemente tienen que importarla y referenciar la clase requerida.

En la siguiente imagen se ve un ejemplo de librería con todo tipo de componentes. Esa es referenciada por diferentes tipos de proyectos. Con esto se evita repetir modelos (Request y Response), por ejemplo en un servicio proveedor de datos y en su consumidor. También permite mejorar el manejo de errores aprovechando la herencia. Se define una excepción de negocio (BussinesException) y cada proyecto extienda de ella según el caso lo requiera. Al momento de realizar un manejo de errores va a ser suficiente con especificar la clase padre para realizar la captura y distinciones de errores de negocio contra los que no lo son.

Cabe destacar que las importación y referenciación de una librería se realiza mediante un gestor de dependencias o paquetes. En caso de Java puede ser Maven o Gradle, en .Net se suele usar NuGet o NPM en NodeJS entre otros.

¿Qué se necesita para hacer una librería en Java?

Para realizar la implementación basta con tener instalado el JDK, en mi caso trabajo con la versión 17, sin embargo sirven versiones anteriores como la 11 u 8. Y lo más importante es contar con un gestor de dependencias, en este caso voy a utilizar Maven. En cuanto a requerimientos teóricos, basta con entender la bases de POO para las implementaciones en Java. Lo que es Maven ayuda tener conocimientos, sin embargo si no se sabe nada no es problema, ya que solamente basta con copiar y pegar los comandos.

Generación del proyecto

Para generar una librería primero hay que entender cual va a ser su alcance, quienes van a ser los que la utilicen y cuales son los requerimientos. Es decir, si se requieren componentes muy diferentes es mejor realizar una separación en dos o mas artefactos en vez de englobar todo en uno. Después de todo, cada librería va a ir creciendo en su versionado y cuanto más contenido variado y sin relación se meta en una misma, más difícil será el mantenimiento. Por ejemplo, se podrían organizar de forma separada clases comunes como excepciones de modelos y firmas para invocaciones.

La implementación de muestra se va a realizar partiendo de un arquetipo básico de maven. Sin embargo, siéntanse libre partir desde otro template (como por ejemplo un proyecto spring) si así se requiere. Para validar que funcione el ejemplo puntual, se va iniciar abriendo una consola cmd, bash o zsh según el so, y recomiendo ejecutar el comando mvn -version. Si la respuesta que se obtiene es que no se reconoce el comando, hay que verificar la instalación y variables de entorno de maven. En caso que se visualice la versión instalada, se pude continuar con los siguientes comando, la primer línea es para posicionarse en el directorio donde se quiere generar el proyecto, la segunda línea invoca el arquetipo el "setup interactivo":

bash
$ cd ./mi/directorio

$ mvn archetype:generate -DarchetypeGroupId=org.apache.maven.archetypes -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4

Una vez ejecutado el arquetipo el primer paso es definir un groupId, el cual generalmente suele ser el dominio invertido del creador, en este caso ingresar ar.com.pablocaamano o el que se prefiera y luego enter. Un segundo paso pedirá definir el artifactId, para el ejemplo se tipeará example-library y luego enter. A continuación, se preguntará si se quiere definir un numero de versión, si se quiere usar la que genera por defecto (1.0-SNAPSHOT) presionar enter, sino ingresar la deseada y luego enter. Un cuarto paso nos permitirá definir un paquete para el proyecto, pero por defecto genera el mismo que el groupId, se recomienda presionar enter y dejar el sugerido.

Por último, se mostrará un resumen de los datos definidor y se pedirá confirmación para terminar. Si se ingresa "y" y luego enter finaliza el proceso, si por el contrario se desea corregir algo presionar "n" y luego intro para repetir el setup. Luego de esto ya se encontrará con un proyecto maven base desde el cual iniciar.

Antes de empezar a codear se recomienda probar que se realice un build e instalación correcta del proyecto. Para esto ejecutar los siguientes comandos, donde la primer línea es para ingresar al directorio del proyecto y la segunda para ejecutar la construcción de maven:

bash
$ cd example-library

$ mvn install

Este comando va a descargar una serie de dependencias y va a intentar buildear el proyecto tal como está. Si todo salé bien, el proceso finalizará mostrando el mensaje "BUILD SUCESS".

Estructura del proyecto

Una vez realizado el item anterior, simplemente basta con abrir el proyecto desde el IDE preferido para desarrollar. Para este ejemplo se utilizará IntelliJ CE, pero se puede utilizar Eclipse o incluso Visual Code si así se desea. A abrir el proyecto se va a contar con una estructura de directorios general de maven. Por convención, las clases que se desarrollaran se suelen generar dentro de la estructura de paquetes src/main/java/…, según corresponda con la definición generada en el setup del proyecto. El directorio src/test/java/… contendrá las clases de testing, que en este caso no se implementaran para no hacer tan largo el articulo. Por defecto, este arquetipo genera una clase de test, la cual se puede borrar para este caso.

El directorio target/ contendrá todos los archivos .class compilados. Por ultimo y lo más importante para iniciar, en la base del proyecto se encuentra el archivo pom.xml el cual permite definir el proyecto y las dependencias que se usa. Si se analiza entre las líneas 7 y 11, se verá que se encuentran definidos el groupId, artifact y version que se ingresó en el setup. Si se quiere hacer alguna corrección, aquí es donde se debe realizar. En el mismo archivo, a partir de la línea 21 se encuentra el tag "dependencies", donde se referencian otras librerías. Se podrá apreciar que se encuentra importado la dependencia "junit" que es la que se utiliza para la realización de test unitarios, la cual para este ejemplo se va quitar. De una forma similar a esta es como referenciarán la "example-library" en otros proyectos.

El arquetipo utilizado genera por defecto una clase App.java dentro del paquete "src". Si se abre esta clase se podrá ver que tiene implementado un método main, el cual no es necesario, ya que no es un proyecto que necesite una ejecución de arranque. Se recomienda eliminar esta clase.

Implementar librerías externas

A fines de simplificar la implementación y para poner en practica el concepto, se va a implementar una librería a la del ejemplo. En este caso se trata de Lombok la cual facilita la generación de métodos como Getters y Setters con simples anotaciones. Como en el ejemplo se trata de un proyecto maven, basta con buscar en Maven Repository o en la documentación de Lombok como es la referencia. Una vez, obtenido esta información hay que pegar la referencia dentro de la sección dependencies del pom.xml.

xml

  <dependencies>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.26</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>

Generar modelos

Para comenzar la implementación, se recomienda generar directorios para cada tipo de clases. Como en este caso se van a crear modelos, se creara un package "model" dentro del "src", quedando la siguiente estructura src/main/java/ar/com/pablocaamano/model/. Dentro de este nuevo paquete se crearan los modelos, para el ejemplo se crearan una clase BaseRequest.java de referencia. Este contendrá un UUID como atributo, el cual es un identificador, de forma que todas las request que extiendan de esta base contengan este atributo. Se agregan las anotaciones @Getter y @Setter para que lombok generé los métodos en cuestión.

java
package ar.com.pablocaamano.model;

import lombok.*;

import java.util.UUID;

@Setter
@Getter
public class BaseRequest {
    private UUID traceId;
}

También se generará un modelo de respuesta BaseResponse.java, un MetaData.java y un modelo Error.java. De esta forma, MetaData contendrá a Error como atributo, además de otros dos: un enum conteniendo el código del resultado (OK o Error) y un string con la descripción. La clase Error tendrá un atributo descripción que contiene el mensaje plano explicando el mismo y un atributo Throwable que almacene la excepción cruda.

Arriba de todo, en la jerarquía de referencias, BaseResponse contendrá a MetaData como atributo de la clase, junto con un Object que será el cuerpo de datos de la respuesta. Al igual que las clases del parrado anterior, se agregan las anotaciones @Getter y @Setter para que lombok generé los métodos en cuestión.

El objetivo de esto, es que estos modelos sean extendidos en un servicio http, de forma que se generen respuestas heredando de este formato base de request y response.

java
package ar.com.pablocaamano.model;

import lombok.*;

@Setter
@Getter
public class Error {
    private String message;
    private Throwable exception;
}
java
package ar.com.pablocaamano.model;

import ar.com.pablocaamano.enums.ResultCode;
import lombok.*;

@Setter
@Getter
public class MetaData {
    private ResultCode result;
    private String details;
    private Error error;
}
java
package ar.com.pablocaamano.model;

import lombok.*;

@Setter
@Getter
public class BaseResponse {
    private MetaData metaData;
    private Object data;
}

Generar excepciones de negocio

Para garantizar la organización del proyecto, lo primero que se recomienda hacer es generar un directorio que contenga las excepciones que se generen en este proyecto. Para el ejemplo, se generará el directorio "exception" dentro de "src", quedando la estructura de esta forma: src/main/java/ar/com/pablocaamano/exception/.

En este ejemplo se va a generar una sola excepción, sin embargo según sea el caso y el alcance que se desee, se pueden implementar varias. Por ejemplo, una para cada caso de error (Error interno, bad request, Not found, etc). La excepción la llamé BaseException y extenderá de RuntimeException. Se le definieron dos constructores, uno que solo recibe un mensaje y otro que recibe un mensaje, así como un Throwable para poder tener el error y el stack de excepciones previas. Para ambos casos se invoca el constructor de la clase padre, mediante super.

java
package ar.com.pablocaamano.exception;

import lombok.Getter;

@Getter
public class BaseException extends RuntimeException {

    public BaseException(String message) {
        super(message);
    }

    public BaseException(String message, Throwable cause) {
        super(message,cause);
    }
}

Compilar Librería

Para buildear y disponibilizar la librería en el repositorio local de maven, basta con abrir una consola parados en el directorio donde se encuentra el proyecto y ejecutar el comando mvn clean install. Si todo sale bien, el proceso de maven debe finalizar con el mensaje BUILD SUCCESS. En caso contrario, se suele mostrar en la consola el error puntual en la clase y línea donde se produce, en un color rojo.

A la izquierda se muestra como <em>maven compiler</em> muestra muy claramente que el error es un ";" faltante. Del lado derecho se ve el mensaje cuando se realizó la compilación exitosamente. A la izquierda se muestra como maven compiler muestra muy claramente que el error es un ";" faltante. Del lado derecho se ve el mensaje cuando se realizó la compilación exitosamente.

Una vez realizado este proceso, la librería ya se podría importar en otro proyecto maven. Si por alguna razón requieren el .jar, ya sea para subir a algún repositorio propio o para importar en un proyecto que no sea maven, este se encuentra dentro del directorio target.

Implementación de librería en servicio

Con el objetivo de mostrar como se realizar la importación de la librería generada, y con el fin de hacerlo en un entorno un poco diferente, se va referenciar en un servicio web Spring Boot. Más queda nada para aprovechar que gran parte de la logica implementada estaba dirigida a un servicio web.

Para este caso voy a usar un proyecto vacío, generado con Spring initializer, el cual solamente va a contar con Spring Boot y el modulo Spring web. Cabe destacar que hay que especificar que es un proyecto java y que utiliza maven como gestor de dependencias.

Aquí se muestra la configuración inicial que se definió para el proyecto de prueba. Aquí se muestra la configuración inicial que se definió para el proyecto de prueba.

Una vez generado el proyecto de prueba y abierto con el IDE de preferencia de cada uno, lo primero que hay que hacer es abrir el pom.xml y referenciar la librería generada dentro de los tags "dependecies". Cada dependencia esta compuesta por un tag xml padre "dependecy" y en un nivel inferior se encuentran "groupId", "artifactId" y "version". Dentro de cada uno hay que insertar los nombres que se hayan definido en el inicio de este articulo. Si se respetaron los pasos que se hicieron acá, sería algo similar a esto:

xml

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.7.10</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<groupId>ar.com.pablocaamano</groupId>
	<artifactId>test-api</artifactId>
	<version>0.0.1-SNAPSHOT</version>

	<name>test-api</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>17</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
		</dependency>

		<dependency>
			<groupId>ar.com.pablocaamano</groupId>
			<artifactId>example-library</artifactId>
			<version>1.0-SNAPSHOT</version>
		</dependency>
	</dependencies>

Antes de empezar también se va a agregar la dependencia de lombok, que se salteó por error en el setup del proyecto. Una vez referenciadas ambas dependencias y luego de dejar que maven importe todo lo necesario, se puede empezar a desarrollar. Para este caso se va a generar un controller PersonController.java dentro de un paquete también llamado controller. Dentro de este se va a definir un endpoint "/persons" de juguete, del tipo GET, en el que se va a utilizar el modelo de respuesta definido en la librería. Para que no quede vacío el atributo data de la respuesta, se a genera un modelo "PersonDTO" propio de la API. También se va a setear el atributo metadata con código "ok" y un mensaje. Los fragmentos de código quedan a continuación.

java
package ar.com.pablocaamano.testapi.controller;

import ar.com.pablocaamano.enums.ResultCode;
import ar.com.pablocaamano.model.BaseResponse;
import ar.com.pablocaamano.model.MetaData;
import ar.com.pablocaamano.testapi.model.PersonDTO;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping(value = "/persons")
public class PersonController {

    @GetMapping
    public BaseResponse getPerson() {
        PersonDTO p = PersonDTO.builder()
                .name("Pablo")
                .surname("Caa")
                .email("contacto@pablocaamano.com.ar")
                .website("blog.pablocaamano.com.ar")
                .build();

        MetaData metaData = new MetaData();
        metaData.setResult(ResultCode.OK);
        metaData.setDetails("Personas listadas.");


        BaseResponse response = new BaseResponse();
        response.setMetaData(metaData);
        response.setData(List.of(p));

        return response;
    }
}
java
package ar.com.pablocaamano.testapi.model;

import lombok.*;

@Getter
@Setter
@Builder
public class PersonDTO {
    private String name;
    private String surname;
    private String email;
    private String website;
}

Si se ejecuta esta API y se invoca el endpoint se va a poder apreciar el formato de la respuesta definido en la librería. De esta forma, se puede hacer que todas las APIs usen está librería y sus endpoints que se definan retornen este modelo. Esto presenta una ventaja para documentar y consumir la API, ya que se tiene un formato de respuesta estandarizado. Lo mismo se podría aplicar para un método POST, donde las Request serían herederas de la BaseRequest generada y por consecuencia tendrían el formato base definido.

Actualización de la librería

Este tema es fundamental, ya que implementar librerías aporta beneficios y facilidades para el manejo de cambios. En caso de tener que modificar una clase de la librería, o requerir agregar nuevas clases, simplemente hay que realizar el cambio necesario, incrementar la versión de la misma en el pom, por ejemplo a "1.1". Después de eso solo resta compilar la librería con sus cambios y actualizar el pom del o los servicios donde se importa la misma. Es más, el beneficio de esto es que si en algún servicio no se requieren los cambios se puede mantener la versión "1.0" y en los que si se necesiten realizar la actualización a la versión "1.1".

Aquí se puede ver el proceso de actualización e importación en la api de pruebas. En el modelo de respuesta se borra la referencia a metada, al actualizar en la api se puede ver que marca en rojo el set del atributo borrado. Aquí se puede ver el proceso de actualización e importación en la api de pruebas. En el modelo de respuesta se borra la referencia a metada, al actualizar en la api se puede ver que marca en rojo el set del atributo borrado.

Bonus track

Para los que llegaron hasta acá, quiero dejar un tip que es muy útil para muchos casos. Es un comando de Maven para importar al repositorio local una librería de la que solo consiguen su .jar. Esto sucede generalmente con librería muy antiguas o casos raros.

Los pasos son muy sencillos, hay que abrir una consola y ubicarse en el path donde se encuentre el archivo jar. una vez ubicados ejecutar el siguiente comando (reemplazando lo que se encuentre entre llaves según corresponda):

bash
$ mvn install:install-file -Dfile={filename}.jar -DgroupId={groupId} -DartifactId={artifactId} -Dversion={version} -Dpackaging=jar

Aclarar que si no se sabe el groupId, artifactId y version original, ingresen alguno inventado. Simplemente tengan tengan presente que lo ingresado en el comando son los datos que se van a tener que insertar en los tags hijos de denpendecy para importar la librería en un proyecto. Por ejemplo, a continuación se muestra como quedaría el comando y su tag de importación:

bash
$ mvn install:install-file -Dfile=libreria-desconocida.jar -DgroupId=com.unknown -DartifactId=unknown-lib -Dversion=1.0.0 -Dpackaging=jar
xml

<dependency>
	<groupId>com.unknown</groupId>
	<artifactId>unknown-lib</artifactId>
	<version>1.0.0</version>
</dependency>

Conclusión

Implementar librerías en los proyectos, como se vio en este articulo, requiere un esfuerzo extra al inicio. Sin embargo, una vez armado e implementado ahorra muchos dolores de cabeza. Gestionar un cambio o mejora es bastante transparente, aunque requiere ser prolijos con la gestión de versiones. Es decir, si se introducen varios cambios en una misma versión, o no se documenta bien el número de versión que posee cada cambio ("changelog") se vuelve caótico.

Cabe destacar que en este ejemplo se realizó una librería usando un arquetipo de java “pelado”, pero se puede realizar utilizando Spring Boot u otros módulos necesarios. Lo que se debe tener en claro es que el scope al pensar en su desarrollo.

Así que con todo lo dicho, te invito a implementar esta forma de trabajo si tenes un entorno donde hay mucha duplicidad de código. Espero que haya sido clara la explicación y como siempre, si hay alguna consulta, referencia o notás que se puede agregar o corregir algo de este articulo no dudes en escribirme por correo electrónico o también desde Telegram. Nos leemos la próxima, saludos!

Contribuir con el Blog

Dejame tú comentario:

Comentarios anteriores

No se registraron comentarios