Jx MVC JxMVC

Reference

JxMVC 3.1 Docs

Jakarta EE 11 · Java 17+ · Cero dependencias en runtime · WAR ~205 KB

01

Routing

Convención URL

/controlador/accion/arg0/arg1 — cero configuración.

HomeController.java
@JxControllerMain              // ruta raíz "/"
@JxControllerMapping("home")
public class HomeController extends JxController {

    public ActionResult index() {      // GET /home/index
        return view("home/index");
    }

    public ActionResult list() {
        String page = model.arg(0);    // GET /home/list/2
        return text("page=" + page);
    }
}

Anotaciones

GET, POST, PUT, DELETE, PATCH, ANY. Plantillas {var}.

ApiController.java
@JxControllerMapping("api")
public class ApiController extends JxController {

    @JxGetMapping("/users/{id}")       // GET /users/42
    public ActionResult getUser() {
        String id = model.pathVar("id");
        return json("{\"id\":" + id + "}");
    }

    @JxPostMapping("save")             // POST /api/save
    public ActionResult save() { ... }

    @JxAnyMapping("ping")              // cualquier verbo
    public ActionResult ping() {
        return text("pong");
    }
}

REST Controller

Alias rápido para APIs — Content-Type JSON por defecto.

ProductApi.java
@JxRestController("/products")
public class ProductApi extends JxController {

    @JxGetMapping("")            // GET /products
    public ActionResult list() {
        return json(repo.findAll());
    }

    @JxGetMapping("{id}")        // GET /products/5
    public ActionResult get(@JxPathVar long id) {
        return json(repo.findById(id));
    }

    @JxDeleteMapping("{id}")     // DELETE /products/5
    public ActionResult delete(@JxPathVar long id) {
        repo.deleteById(id);
        return json("{\"ok\":true}");
    }
}

Profile por ruta

Activa endpoints solo en ciertos perfiles de ejecución.

AdminController.java
@JxGetMapping("debug")
public ActionResult debug() {
    if (!JxProfile.is("dev")) throw JxException.forbidden("solo dev");
    return json(JxMetrics.summary());
}

@JxGetMapping("status")
public ActionResult status() {
    if (!JxProfile.isAny("prod", "staging")) throw JxException.forbidden("acceso restringido");
    return json("{\"env\":\"" + JxProfile.active() + "\"}");
}
02

Controladores

ActionResult — todos los tipos

ejemplos de respuesta
// Vista JSP
return view("home/dashboard");

// Texto plano
return text("pong!");

// JSON raw
return json("{\"ok\":true,\"id\":42}");

// JSON de objeto (usa JxJson)
return json(miObjeto);

// Redirect
return redirect("/home/index");

// Status personalizado
return view("home/created").status(201);

// Archivo binario
byte[] pdf = generarPdf();
view.raw(pdf, "application/pdf", "reporte.pdf");
return null;

model (JxRequest) · view (JxResponse)

acceso a request y response
// Parámetros de formulario / query string
String nombre = model.param("nombre");

// Argumentos posicionales en la URL
String id   = model.arg(0);           // /ctrl/action/42  → "42"
String slug = model.argRaw(1);        // sin sanitizar

// Variables de plantilla (2 formas equivalentes)
String uid  = model.pathVar("id");    // /users/{id}  → dinámico
long   uid2 = model.pathVarLong("id"); // como long directo

// Session — métodos en JxController
sessionSet("user", dto);
Object user = sessionGet("user");

// Cabeceras y cookies
String token = model.header("Authorization");

// Pasar variables a la vista
model.setVar("title", "Dashboard");
model.setVar("items", lista);

// Cabecera / status en respuesta
view.status(201);
view.header("X-Token", jwt);
view.contentType("application/json");

@JxBeforeAction · @JxAfterAction

interceptores de ciclo de vida
@JxControllerMapping("admin")
public class AdminController extends JxController {

    private static final JxLogger log = JxLogger.getLogger(AdminController.class);

    @JxBeforeAction(only = {"create","update"})
    public void requireAdmin() {
        String role = (String) sessionGet("role");
        if (!"ADMIN".equals(role))
            throw JxException.forbidden("Solo administradores");
    }

    @JxModelAttr
    public void commonAttrs() {
        model.setVar("appVersion", "3.1.1");
        model.setVar("usuario", sessionGet("user"));
    }

    @JxAfterAction
    public void audit() {
        log.info("Admin action: {}", model.path());
    }

    @JxGetMapping("create")
    public ActionResult create() { return view("admin/create"); }
}

@JxControllerAdvice — manejador global de excepciones

GlobalAdvice.java
@JxControllerAdvice
public class GlobalAdvice {

    // Captura cualquier JxException no manejada en el controlador
    @JxExceptionHandler(JxException.class)
    public ActionResult onJxError(JxException ex) {
        return ActionResult.json(
            "{\"ok\":false,\"code\":" + ex.getStatus() +
            ",\"error\":\"" + ex.getMessage() + "\"}"
        ).status(ex.getStatus());
    }

    // Captura excepciones de runtime genéricas
    @JxExceptionHandler(RuntimeException.class)
    public ActionResult onRuntime(RuntimeException ex) {
        return ActionResult.json("{\"ok\":false,\"error\":\"Error interno\"}")
                           .status(500);
    }
}
03

Base de datos

JxModel — Active Record (v3.1)

Producto.java
// Un solo archivo — sin re-declarar campos (la BD es la fuente de verdad)
public class Producto extends JxModel {
    private static final String T = "productos";

    public static DBRowSet todos() {
        try (JxDB db = db()) {
            return db.query("SELECT * FROM " + T);
        } catch (Exception e) { return new DBRowSet(); }
    }

    public static DBRow porId(Object id) {
        try (JxDB db = db()) {
            return db.queryRow(
                "SELECT * FROM " + T + " WHERE id = ?", id);
        } catch (Exception e) { return null; }
    }

    public static long guardar(DBRow datos) {
        try (JxDB db = db()) { return db.insert(T, datos); }
        catch (Exception e) { return -1; }
    }

    public static int eliminar(Object id) {
        try (JxDB db = db()) {
            return db.exec(
                "DELETE FROM " + T + " WHERE id = ?", id);
        } catch (Exception e) { return 0; }
    }
}

// En el controlador — sin @JxInject, sin Repository separado
DBRow p      = Producto.porId(42);
String nombre = p.GetString("nombre");
double precio  = p.GetDouble("precio");

JxDB — acceso JDBC directo

uso en controlador
// db() disponible directo en JxController — sin extender JxModel
public ActionResult reportes() {
    try (JxDB db = db()) {

        DBRowSet rows = db.query(
            "SELECT id, nombre, total FROM pedidos WHERE fecha = ?",
            LocalDate.now()
        );

        DBRow cfg = db.queryRow(
            "SELECT * FROM config WHERE clave = ?", "max_items"
        );

        db.exec(
            "UPDATE productos SET stock = stock - ? WHERE id = ?",
            1, productId
        );

        model.setVar("pedidos", rows.asList());
        model.setVar("config",  cfg);
    }
    return view("home/reportes");
}

application.properties — configuración de BD

src/main/resources/application.properties
jxmvc.controllers.package = com.miapp.controllers

# PostgreSQL
jxmvc.db.url     = jdbc:postgresql://localhost:5432/mi_db
jxmvc.db.user    = postgres
jxmvc.db.pass    = secret

# Pool de conexiones
jxmvc.pool.enabled  = true
jxmvc.pool.size     = 10
jxmvc.pool.timeout  = 5       # segundos para obtener conexión del pool
04

Validación

Anotaciones sobre entidades

Usuario.java
public class Usuario {

    @JxRequired
    @JxMinLength(3)
    @JxMaxLength(50)
    public String nombre;

    @JxRequired
    @JxEmail                       // valida formato email
    public String email;

    @JxPattern("[0-9]{8}")         // regex personalizado
    public String dni;

    @JxMin(0)
    @JxMax(120)
    public int edad;
}

Validar en el controlador

uso en acción
@JxPostMapping("registro")
public ActionResult registro() {
    Usuario u = new Usuario();
    u.nombre = model.param("nombre");
    u.email  = model.param("email");
    u.dni    = model.param("dni");

    // JxValidation lanza JxException(400) si falla
    JxValidation.validate(u);

    repo.save(u);
    return redirect("/home/index");
}

// O capturar los errores manualmente
JxValidation.Result result = JxValidation.check(u);
if (!result.isValid()) {
    model.setVar("errors", result.getErrors());
    return view("home/registro");
}

Fechas, URLs y validadores custom v3.1

nuevas anotaciones
import java.time.LocalDate;

public class ReservaDto {

    @JxRequired @JxFuture          // debe ser fecha futura
    public LocalDate fechaReserva;

    @JxPast                        // debe ser fecha pasada
    public LocalDate fechaNacimiento;

    @JxUrl                         // http:// o https:// valido
    public String website;

    @JxCheck(RucPeruano.class)     // validador personalizado
    public String ruc;
}

// Implementar JxConstraint<T> para validadores propios:
public class RucPeruano implements JxValidation.JxConstraint<String> {
    public boolean isValid(String v) {
        return v != null && v.matches("\\d{11}");
    }
    public String message() { return "RUC debe tener 11 digitos"; }
}
// Las instancias se cachean — se crean una sola vez.

Todas las anotaciones disponibles

catalogo — 21 anotaciones
// Strings
@JxRequired  @JxNotNull  @JxNotEmpty
@JxMinLength(n)  @JxMaxLength(n)  @JxLength(n)
@JxEmail  @JxPhone  @JxUrl  @JxPattern(regex)
@JxSafe  @JxDigits(n)  @JxIn({"a","b","c"})

// Numeros
@JxMin(n)  @JxMax(n)  @JxRange(min,max)  @JxPositive

// Fechas (java.time)
@JxFuture  @JxPast

// Custom
@JxCheck(MiValidador.class)
05

Seguridad

Autenticación y roles

control de acceso
// Requiere sesión activa
@JxRequireAuth
@JxGetMapping("perfil")
public ActionResult perfil() { ... }

// Requiere rol específico
@JxRequireRole("ADMIN")
@JxGetMapping("panel")
public ActionResult panel() { ... }

// Por controlador completo + roles múltiples
@JxRequireRole({"ADMIN", "SUPERUSER"})
@JxControllerMapping("admin")
public class AdminController extends JxController { ... }

Rate limiting

@JxRateLimit
// Máximo 5 requests por IP en 60 segundos
@JxRateLimit(requests = 5, window = 60)
@JxPostMapping("login")
public ActionResult login() {
    String user = model.param("username");
    String pass = model.param("password");
    // ... autenticar
    return redirect("/home/index");
}

// Rate limit a nivel de controlador
@JxRateLimit(requests = 100, window = 3600)
@JxControllerMapping("api")
public class ApiController extends JxController { ... }

CORS

@JxCors
// Por controlador
@JxCors(origins = {"https://app.midominio.com"})
@JxControllerMapping("api")
public class ApiController extends JxController { ... }

// Por acción (sobreescribe al controlador)
@JxCors(origins = {"*"}, methods = {"GET","POST"})
@JxGetMapping("public")
public ActionResult publicData() { ... }

Headers de seguridad — automáticos

application.properties
# Siempre activos en todas las respuestas:
# X-Content-Type-Options: nosniff
# Referrer-Policy: strict-origin-when-cross-origin

# Configurables:
jxmvc.security.frame-options = SAMEORIGIN   # o DENY / false
jxmvc.security.hsts          = false        # true solo con HTTPS
jxmvc.security.hsts.maxage   = 31536000
06

Filtros globales

Registrar filtros

AppFilters.java · @JxFilter
@JxFilter
public class AuthFilter implements JxFilterChain {

    @Override
    public boolean before(JxFilterContext ctx) {
        String path = ctx.path();
        // Permitir rutas públicas
        if (path.startsWith("/home") || path.startsWith("/assets"))
            return true;

        Object user = ctx.request().getSession()
                         .getAttribute("user");
        if (user == null) {
            ctx.response().redirect("/login");
            return false;   // corta la cadena
        }
        return true;
    }

    @Override
    public void after(JxFilterContext ctx) {
        ctx.response().header("X-Frame-Options", "DENY");
    }
}

Registro programático

alternativa sin anotación
// En el servlet de inicio o AppConfig:
JxFilters.add(ctx -> {
    ctx.response().header("X-App", "JxMVC/3.1");
    return true;
});

JxFilters.add(new JxFilter() {
    public boolean before(JxFilterContext ctx) { return true; }
    public void after(JxFilterContext ctx) {
        ctx.response().header("X-Frame-Options", "DENY");
    }
});
07

Cron & Async

@JxAsync — ejecución en background

respuesta inmediata, proceso en background
// Responde 202 Accepted al instante.
// La acción corre en el pool de threads async.
@JxAsync
@JxPostMapping("exportar")
public ActionResult exportar() {
    // Este bloque se ejecuta en background
    byte[] csv = reportService.generarCSV();
    emailService.enviar(csv, destinatario);
    return null;   // respuesta ya fue enviada (202)
}

// Configurar el pool en application.properties:
// jxmvc.async.threads = 8

@JxRetry — reintentos automáticos

resiliencia ante fallos transitorios
// Hasta 3 intentos con 500ms entre ellos.
// Si todos fallan, propaga la última excepción.
@JxRetry(attempts = 3, backoff = 500)
@JxGetMapping("externo")
public ActionResult externo() {
    String data = httpClient.get("https://api.externa.com/data");
    return json(data);
}

// Combinable con @JxAsync
@JxAsync
@JxRetry(attempts = 2, backoff = 1000)
@JxPostMapping("sync")
public ActionResult sync() { ... }

@JxScheduled — cron y fixed rate v3.1

tareas programadas con cron
@JxService
public class TareasService {

    // Cron 5 campos: min hora dom mes dow (0=domingo)
    @JxScheduled(cron = "0 3 * * *")    // cada dia a las 3 AM
    public void backupDiario() { ... }

    @JxScheduled(cron = "0 9 * * 1")    // cada lunes 9 AM
    public void reporteSemanal() { ... }

    @JxScheduled(cron = "0 0 1 * *")    // primer dia del mes
    public void facturacion() { ... }

    // Fixed rate/delay en milisegundos (sigue disponible)
    @JxScheduled(fixedDelay = 60_000, initialDelay = 5_000)
    public void purgeTokens() { ... }

    @JxScheduled(fixedRate = 30_000)
    public void refreshCache() { ... }
}

// Programático — sin @JxService:
JxScheduler.scheduleAtFixedRate(() -> tarea(), 0, 3_600_000);
JxScheduler.scheduleCron(() -> backup(), "0 2 * * *");
JxScheduler.runOnce(() -> notificar(), 5_000);

JxJson — serialización propia

sin Jackson ni Gson
// Soporta: String, Number, Boolean, List, Map,
// DBRow, DBRowSet, POJO con getters, java.time.*

// Tipos java.time — serializacion automatica (v3.1):
LocalDate.of(2026, 5, 17)     // → "2026-05-17"
LocalDateTime.now()            // → "2026-05-17T10:30:00"
LocalTime.of(10, 30)           // → "10:30"
java.sql.Date / Timestamp      // → formato ISO tambien

// POJO con campos de fecha — funciona directo:
public class PedidoDto {
    public String nombre;
    public LocalDate fecha;        // serializado como "2026-05-17"
    public LocalDateTime creado;   // serializado como "2026-05-17T..."
}
return json(pedidoDto);  // {"nombre":"...","fecha":"2026-05-17",...}

// Deserializacion — tambien automatica:
PedidoDto dto = JxJson.fromJson(body, PedidoDto.class);
// dto.fecha es LocalDate.of(2026, 5, 17)
08

Inyección de dependencias

@JxService · @JxInject

registro automático + inyección
// Marcar como servicio singleton
@JxService
public class EmailService {
    public void enviar(String to, String body) { ... }
}

@JxService
public class UserService {

    @JxInject
    private EmailService emailService;   // inyectado automáticamente

    public void registrar(Usuario u) {
        userRepo.save(u);
        emailService.enviar(u.email, "Bienvenido");
    }
}

// En el controlador:
@JxControllerMapping("user")
public class UserController extends JxController {

    @JxInject
    private UserService userService;     // inyectado

    @JxPostMapping("registro")
    public ActionResult registro() {
        userService.registrar(buildUsuario());
        return redirect("/home/index");
    }
}

Acceso al registro

JxServiceRegistry
// Obtener instancia singleton por clase
EmailService svc = JxServiceRegistry.get(EmailService.class);

// Registrar manualmente (útil en tests)
JxServiceRegistry.register(new MockEmailService());

// Todos los @JxService se escanean y registran
// automáticamente al arrancar el framework.
09

Configuración completa

src/main/resources/application.properties
# ── Núcleo ─────────────────────────────────────────────────────────
jxmvc.controllers.package = com.miapp.controllers
jxmvc.profile             = dev           # dev | prod | staging
jxmvc.log.level           = INFO          # DEBUG | INFO | WARN | ERROR

# ── Base de datos ───────────────────────────────────────────────────
jxmvc.db.url              = jdbc:postgresql://localhost:5432/mi_db
jxmvc.db.user             = postgres
jxmvc.db.pass             = secret

# ── Pool de conexiones ──────────────────────────────────────────────
jxmvc.pool.enabled        = true
jxmvc.pool.size           = 10            # máximo de conexiones
jxmvc.pool.timeout        = 5             # segundos para obtener del pool

# ── Async ───────────────────────────────────────────────────────────
jxmvc.async.threads       = 8

# ── Seguridad ───────────────────────────────────────────────────────
jxmvc.security.frame-options = SAMEORIGIN # DENY | false
jxmvc.security.hsts          = false      # true solo con HTTPS
jxmvc.security.hsts.maxage   = 31536000   # 1 año en segundos
10

Endpoints del sistema

GET
/jx/health

Estado del pool, uptime, threads activos

probar →
GET
/jx/info

Versión, perfil activo, Java, servidor

probar →
GET
/jx/metrics

Peticiones por ruta: total, errores, latencia media

probar →
GET
/jx/openapi

Spec OpenAPI 3.0 generada automáticamente de las anotaciones

probar →