Api con tomcat 11, java 17, jersey 3.x


Una de las sorpresas que uno se lleva cuando migra a una versión superior de tomcat por ejemplo 10.x o tomcat 11.x , que claro es evidente que se requieren ajustes eso no es nada nuevo, lo que si es nuevo es que Jersey 3.x  ya cambio nombre de espacio de javax a jakarta, eso no es tan significativo lo que si lo es , es que Jersey 2.x , en general necesitaba algunas pocas librerías , pero en Jersey 3.x , por ser modular para "facilitar" la vida del programador , necesita una cantidad interesante de librerías jar para funcionar, la solución que dan los expertos, es lo mas normal del mundo usar Maven , para que instale el repositorio de librería adecuado, y sugieren este pom.xml para maven.

Jersey 3.1.3 ya no es compatible directamente con Java EE/Jakarta EE clásicos, porque todo fue migrado al espacio de nombres jakarta.* (en lugar de javax.*). Este cambio obligó a Jersey a modificar su arquitectura interna, y eso cambió las dependencias necesarias para que funciones como @Consumes(MediaType.APPLICATION_JSON) trabajen correctamente.

 <dependencies>

  <dependency>

    <groupId>org.glassfish.jersey.containers</groupId>

    <artifactId>jersey-container-servlet</artifactId>

    <version>3.1.3</version>

  </dependency>

  <dependency>

    <groupId>org.glassfish.jersey.media</groupId>

    <artifactId>jersey-media-json-jackson</artifactId>

    <version>3.1.3</version>

  </dependency>

  <dependency>

    <groupId>com.fasterxml.jackson.module</groupId>

    <artifactId>jackson-module-jakarta-xmlbind-annotations</artifactId>

    <version>2.17.0</version>

  </dependency>

</dependencies>


Nada especial caso del día de un programador, más si esta migrando, hasta hay bien, lo que si es del otro mundo, es que no es cierto que instala todas las dependencias que se necesita, y cuando inicia tu flamante tomcat 11 con 17 y jersey 3.x no encuentras ningún error ni de compilaciones en el registro de /log/catalina.out , y levanta el stack jersey, entonces supones que todo es bien, pero eso no es cierto, cuando intentas ejecutar tu aplicación empinan a aparecer errores del tipo 309, 403, 415, 500 y un largo etc, que piensas ?, lo lógico que tu programa tiene problemas, y empezar actualizar , revisar, mejorar , pero después de frustrarte y terminar sin ver ningún error que te ayude, puede que estés de buenas y aparezca un error 500, o 415 , y eso ya es un indicio que tu jersey 3.x tiene problemas, y problemas serios de dependencias, luego de batallar puedes llegar que todas las dependencias necesarias son las siguientes por lo menos en mi caso fueron esas y al final funciono:

/webapps/miApi/lib

-rw-r--r--. 1 tomcat tomcat   27415   May  7 14:37 aopalliance-repackaged-3.0.4.jar
-rw-r--r--. 1 tomcat tomcat 4210756 May  7 14:37 byte-buddy-1.14.9.jar
-rw-r--r--. 1 tomcat tomcat   67815   May  7 14:37 classmate-1.5.1.jar
-rw-r--r--. 1 tomcat tomcat 1328694 May  7 14:37 hibernate-validator-7.0.5.Final.jar
-rw-r--r--. 1 tomcat tomcat  207249  May  7 14:37 hk2-api-3.0.4.jar
-rw-r--r--. 1 tomcat tomcat  205291  May  7 14:37 hk2-locator-3.0.4.jar
-rw-r--r--. 1 tomcat tomcat  132025  May  7 14:37 hk2-utils-3.0.4.jar
-rw-r--r--. 1 tomcat tomcat   26147   May  7 14:37 istack-commons-runtime-4.1.1.jar
-rw-r--r--. 1 tomcat tomcat   78488   May  7 14:37 jackson-annotations-2.17.0.jar
-rw-r--r--. 1 tomcat tomcat  581556  May  7 14:37 jackson-core-2.17.0.jar
-rw-r--r--. 1 tomcat tomcat 1649184 May  7 14:37 jackson-databind-2.17.0.jar
-rw-r--r--. 1 tomcat  tomcat    32407 May 7  14:37 jackson-module-jakarta-xmlbind-annotations-2.17.0.jar
-rw-r--r--. 1 tomcat tomcat   66651   May  7 14:37 jakarta.activation-api-2.1.1.jar
-rw-r--r--. 1 tomcat tomcat   26141   May  7 14:37 jakarta.annotation-api-2.1.1.jar
-rw-r--r--. 1 tomcat tomcat  173677  May  7 14:37 jakarta.el-4.0.2.jar
-rw-r--r--. 1 tomcat tomcat   80563   May  7 14:37 jakarta.el-api-4.0.0.jar
-rw-r--r--. 1 tomcat tomcat   10681   May  7 14:37 jakarta.inject-api-2.0.1.jar
-rw-r--r--. 1 tomcat tomcat   92678   May  7 14:37 jakarta.validation-api-3.0.0.jar
-rw-r--r--. 1 tomcat tomcat  154815  May  7 14:37 jakarta.ws.rs-api-3.1.0.jar
-rw-r--r--. 1 tomcat tomcat  127111  May  7 14:37 jakarta.xml.bind-api-4.0.0.jar
-rw-r--r--. 1 tomcat tomcat  794137  May  7 14:37 javassist-3.29.2-GA.jar
-rw-r--r--. 1 tomcat tomcat  138671  May  7 14:37 jaxb-core-4.0.3.jar
-rw-r--r--. 1 tomcat tomcat  920315  May  7 14:37 jaxb-runtime-4.0.3.jar
-rw-r--r--. 1 tomcat tomcat   60790   May  7 14:37 jboss-logging-3.4.1.Final.jar
-rw-r--r--. 1 tomcat tomcat  305942  May  7 14:37 jersey-client-3.1.3.jar
-rw-r--r--. 1 tomcat tomcat 1213519 May  7 14:37 jersey-common-3.1.3.jar
-rw-r--r--. 1 tomcat tomcat   32920   May  7 14:37 jersey-container-servlet-3.1.3.jar
-rw-r--r--. 1 tomcat tomcat   74544   May  7 14:37 jersey-container-servlet-core-3.1.3.jar
-rw-r--r--. 1 tomcat tomcat     84885 May  7 14:37 jersey-entity-filtering-3.1.3.jar
-rw-r--r--. 1 tomcat tomcat   78428   May  7 14:37 jersey-hk2-3.1.3.jar
-rw-r--r--. 1 tomcat tomcat     80721 May  7 14:37 jersey-media-json-jackson-3.1.3.jar
-rw-r--r--. 1 tomcat tomcat  985504  May  7 14:37 jersey-server-3.1.3.jar
-rw-r--r--. 1 tomcat tomcat   19479   May  7 14:37 osgi-resource-locator-1.0.3.jar
-rw-r--r--. 1 tomcat tomcat     817     May  7 14:37 test-resource.jar
-rw-r--r--. 1 tomcat tomcat   73269   May  7 14:37 txw2-4.0.3.jar


Casi la tercera parte me tocó agregarlas mediante el comando wget), a medida que iba encontrando problemas, esto después de haber importando "todas" las dependencias "necesarias" con Maven supuestamente.

También hay que tener en cuenta que debes registrar algunas dependencias en tu backend , eso no es nada nuevo pero hay que tenerlo en cuenta

en mi caso fueron.


miApplication.java
=====================================================
package miPaquete;

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;
import java.util.HashSet;
import java.util.Set;
import org.glassfish.jersey.jackson.JacksonFeature;


@ApplicationPath("api")
public class ZephyrApplication extends Application {

    @Override
    public Set<Class<?>> getClasses() {
        Set<Class<?>> classes = new HashSet<>();
        // Registrar recursos                                                                                                               
        classes.add(ZephyrResource.class); // Registra tu recurso principal                                                                 

        // Registrar filtros                                                                                                                
        classes.add(CORSFilter.class); // Registra el filtro CORS                                                                           

         // Proveedor JSON                                                                                                                  
         classes.add(JacksonFeature.class);

        // Puedes agregar más filtros o proveedores si es necesario                                                                         
        // classes.add(MiFiltro.class); // Filtro personalizado                                                                             
        // classes.add(MiProveedor.class); // Proveedor personalizado                                                                       

        return classes;
    }
}


y por su puesto el CORSFilter.java , yo lo hice asi, pero se pudo haber hecho en el archivo web.xml , sin problema, yo paso una variable de interes para mi. USUARIO_DB. que me permite algunos manejos de mi database.

CORSFilter.kava
===========================================================
package byb.zephyr;

import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerResponseContext;
import jakarta.ws.rs.container.ContainerResponseFilter;
import jakarta.ws.rs.ext.Provider;

import java.io.IOException;

@Provider
public class CORSFilter implements ContainerResponseFilter {

    @Override
    public void filter(ContainerRequestContext requestContext,
                       ContainerResponseContext responseContext) throws IOException {

        responseContext.getHeaders().putSingle("Access-Control-Allow-Origin", "*");
        responseContext.getHeaders().putSingle("Access-Control-Allow-Headers",
                                               "origin, content-type, accept, authorization, X-API-KEY, X-USUARIO-DB");
        responseContext.getHeaders().putSingle("Access-Control-Allow-Methods",
                                               "GET, POST, PUT, DELETE, OPTIONS, HEAD");
        responseContext.getHeaders().putSingle("Access-Control-Max-Age", "1728000");

    }
}


y luego claro el API como tal en este caso tengo una entrada para extrae mis agentes comerciales:

miResource.java
==================================================
package mipackage;

import org.slf4j.Logger                       ;
import org.slf4j.LoggerFactory           ;

import java.util.*                                 ;

import jakarta.ws.rs.Path                     ;
import jakarta.ws.rs.Produces             ;
import jakarta.ws.rs.core.MediaType  ;

import jakarta.ws.rs.*                          ;
import jakarta.ws.rs.core.Context       ;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response    ;

import com.mongodb.client.*            ;

import com.mongodb.client.*            ;

@Path("/")
public class miResource {

    private static final Logger logger = LoggerFactory.getLogger(ZephyrResource.class);

  private boolean ValidarApiKey(HttpHeaders headers) {
        String apiKey = headers.getHeaderString("X-API-KEY")               ;
        return "miKey".equals(apiKey); // Reemplazar "your-api-key" con tu API key real                                                
    }
    @POST
    @Path("/AgentesConsultarID")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response AgentesID(@Context HttpHeaders headers, AgentesDTO agente) {
                logger.info("acceso");
                if (! ValidarApiKey(headers)) {
                    logger.error("API key inválida. Acceso denegado.");
            return Response.status(Response.Status.UNAUTHORIZED).entity("Invalida API key").build();
        }
                logger.info("Conectando a la base de datos...");
                ConectarDB(headers)                               ;
                logger.debug("Datos del agente recibido: {}", agente);

        AgentesDAO dao = new AgentesDAO();
        Vector<AgentesDTO> resultados = dao.AgentesID(mgDB,agente);
        logger.info("Consultados {} resultados de agentes", resultados.size());
        return Response.ok(resultados).build();
    }
}

Recordar colocar un par de entradas al path de tu script de compilación
los siguientes jar qe debes tener en un lugar accesible por javac, yo las coloco en /lib por facilidad.
  •  jersey-media-json-jackson-3.1.3.jar
  • jersey-server-3.1.3.jar
  •  jakarta.ws.rs-api-3.1.0.jar

y por ultimo generar el web.xml en la carpeta de tu api, webapps/ miApi/WEB-INF/web.xml

yo lo tengo minimalista porque deje todo el peso de la gestión en los archivos CORSFilter.java y
miApplication.java , buneo en eso si hay gustos. mi web.xml qudó.

web.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="https://jakarta.ee/xml/ns/jakartaee" xsi:schemaLocation="https://jaka\
rta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd" version="5.0">

  <display-name>bybapi</display-name>

  <servlet>
    <servlet-name>Jersey Web Application</servlet-name>
    <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
    <init-param>
      <param-name>jakarta.ws.rs.Application</param-name>
      <param-value>miApi.mi.miApplication</param-value>
      <!-- O byb.zephyr.ApplicationConfig, dependiendo de cuál quieras usar -->
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>Jersey Web Application</servlet-name>
    <url-pattern>/api/*</url-pattern>
  </servlet-mapping>

</web-app>

Adicionalmente generé este script para ejecutar en node, solamente por facilidad y evitar meter
mas variables en la depuración e ir directamente a la fuente del problema en este sistema :

prueba.mjs para node
================
Tener en cuenta que se debe usar (-) y no (_) por si usa algún sistema como Nginx o Apache
estos campos en la cabecera son bloqueados , recomienda el prefijo X- , para que cumpla con
las recomendaciones W3C. Este script es una herramienta de prueba que ayuda a drpurar
problemas de las librerias faltantes y logica de la API, pero no es apta para depurar errores de
CORS. Porque node no es un navegador

import fetch from 'node-fetch'; // Asegúrate de tener instalada la librería 'node-fetch'

const apiUrl = 'https:miapi.misitio.com/api/AgentesConsultarID';

const headers = {
  'Content-Type'       : 'application/json',
  'X-API-KEY'           : 'miclave',
  'X-USUARIO-DB'  : 'usuario001',
  'Authorization'         : 'Bearer token'
};

const data = {
      agenteLimit  : "25",
      agenteOffSet : "0",
      agenteBuscar : "",
      agenteSort   : "NOMBRE",
      agenteEntorno: "usuario001"
};

fetch(apiUrl, {
  method: 'POST',
  headers: headers,
  body: JSON.stringify(data)  //pasar datos JSON 
})
  .then(async res => {
    const text = await res.text(); // Obtener la respuesta cruda
    console.log('Respuesta cruda:\n', text); // Mostrar la respuesta

    try {
      const json = JSON.parse(text);
      console.log('Respuesta JSON:', json);
    } catch (err) {
      console.error('⚠️ No se pudo parsear como JSON:', err.message);
    }
  })
  .catch(err => console.error('Error de red:', err));


script NGIX para proxy inverso
========================
server {
    listen 80;
    server_name bybapi.burgosyburgossas.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name miSitiocom;

    ssl_certificate /etc/letsencrypt/live/bybapi.burgosyburgossas.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/bybapi.burgosyburgossas.com/privkey.pem;

              location / {

    if ($request_method = OPTIONS) {
            add_header 'Access-Control-Allow-Origin' '*';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-API-KEY, X-USUARIO-DB';
            add_header 'Access-Control-Max-Age' 1728000;
            add_header 'Content-Length' 0;
            add_header 'Content-Type' 'text/plain charset=UTF-8';
            return 204;
        }

        proxy_pass http://localhost:8080/bybapi/;
        proxy_set_header Host $host;
        proxy_set_header X-USUARIO-DB $http_x_usuario_db;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

    }
}

Caso de uso:
Ejemplo.html
=========
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <title>Ejemplo de consulta API</title>
</head>
<body>

  <h2>Consultar Agentes</h2>

  <button onclick="consultarAgentes()">Consultar</button>

  <h3>Respuesta del servidor:</h3>
  <pre id="resultado" style="background: #eee; padding: 1em;"></pre>

  <script>
    function consultarAgentes() {

      const datos = {
        agenteLimit      : "25",
        agenteOffSet    : "0",
        agenteBuscar   : "",
        agenteSort        : "NOMBRE",
        agenteEntorno  : "mi001"
      };

      const headers = {
                        "X-API-KEY"         : "miKey",
                        "X-USUARIO-DB" : "mi001",
                        "Authorization"       : "Bearer token",
                        "Accept"                  : "application/json",
                        "Content-Type"       : "application/json"
                      };

      fetch("https://miSitio.com/api/AgentesConsultarID", {
        method : 'POST',
        headers: headers,
        body: JSON.stringify(datos)
      })
      .then(response => {
        if (!response.ok) {
          throw new Error("Error en la respuesta: " + response.status);
        }
        return response.json();
      })
      .then(data => {
        console.log("Respuesta:", data); 
        document.getElementById("resultado").innerText = JSON.stringify(data, null, 2);
      })
      .catch(error => {
        console.error("Error:", error);
        document.getElementById("resultado error").innerText = "Error: " + error.message;
      });
    }
  </script>

</body>
</html>



Al final después de hacer una análisis al proceso es que todo se resumen a que los importadores de jar en en este caso en MAVEN y los pom están incompletos, pero para llegar a eso toco pasar por proxy inverso, Nginx. certificados de seguridad LetsEncript, el navegador, permisos, logger, la lógica de los programas, tomcat, java , como no se ven errores, pues se sospecha de todo porque no funciona. 







Comentarios

Entradas populares