Jx MVC JxMVC

Reference

JxMVC 2.7 Docs

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

Routing Controladores Base de datos Validacion Seguridad Filtros Async & Retry DI Config Sistema

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() {
        long id = Long.parseLong(model.pathVar("id"));
        return json(repo.findById(id));
    }

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

Profile por ruta

Activa endpoints solo en ciertos perfiles de ejecución.

AdminController.java
@JxProfile("dev")              // solo perfil dev
@JxGetMapping("debug")
public ActionResult debug() {
    return json(JxMetrics.snapshot());
}

@JxProfile({"prod","staging"}) // varios perfiles
@JxGetMapping("status")
public ActionResult status() {
    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
String uid = model.pathVar("id");  // /users/{id}

// Session
model.session().setAttribute("user", dto);

// 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 {

    // Se ejecuta antes de cada acción (o solo las indicadas)
    @JxBeforeAction(only = {"create","update"})
    public void requireAdmin() {
        String role = (String) model.session().getAttribute("role");
        if (!"ADMIN".equals(role))
            throw JxException.forbidden("Solo administradores");
    }

    // Inyecta variables en el modelo antes de renderizar
    @JxModelAttr
    public void commonAttrs() {
        model.setVar("appVersion", "2.7.0");
        model.setVar("usuario", model.session().getAttribute("user"));
    }

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

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

@JxAdvice — manejador global de excepciones

GlobalAdvice.java
@JxAdvice
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

JxRepository — CRUD genérico

ProductRepository.java
@JxTable("productos")
public class Producto {
    @JxId public long id;
    @JxRequired @JxMinLength(2) public String nombre;
    public double precio;
    public boolean activo;          // soft delete column
}

@JxService
public class ProductoRepo extends JxRepository<Producto, Long> {

    public ProductoRepo() {
        super("productos", Producto.class);
        enableSoftDelete("activo", "true");  // soft delete
    }

    // Query personalizado — ? = parámetros posicionales
    @JxQuery("SELECT * FROM productos WHERE precio < ?")
    public List<Producto> baratos(double max) {
        return executeQuery(max);
    }

    // Paginación
    public DBRowSet pagina(int page) {
        return findAllPaged(page, 20);  // 20 por página
    }
}

JxDB — acceso JDBC directo

uso en controlador
public ActionResult reportes() {
    try (JxDB db = db()) {           // AutoCloseable — devuelve al pool

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

        // Fila individual
        DBRow row = db.queryOne(
            "SELECT * FROM config WHERE clave = ?", "max_items"
        );

        // Mutación
        int affected = db.execute(
            "UPDATE productos SET stock = stock - ? WHERE id = ?",
            1, productId
        );

        model.setVar("pedidos", rows.asList());
        model.setVar("config",  row);
    }
    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");
}

05 Seguridad

Autenticación y roles

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

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

// Por controlador completo
@JxRequireAuth
@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.before(ctx -> {
    ctx.response().header("X-App", "JxMVC/2.7");
    return true;
});

JxFilters.after(ctx -> {
    long ms = ctx.elapsed();
    ctx.response().header("X-Response-Time", ms + "ms");
});

07 Async & Retry

@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(times = 3, delay = 500)
@JxGetMapping("externo")
public ActionResult externo() {
    // Llama a una API externa que puede fallar
    String data = httpClient.get("https://api.externa.com/data");
    return json(data);
}

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

JxScheduler — tareas programadas

background jobs
// En el init() del servlet, o en un @JxService:
JxScheduler.scheduleAtFixedRate(() -> {
    log.info("Limpiando sesiones expiradas...");
    sesionRepo.eliminarExpiradas();
}, 0, 3_600_000);          // cada hora (ms)

// Keepalive del pool (ya incluido por defecto):
JxScheduler.scheduleAtFixedRate(() -> {
    JxPool pool = JxPool.global();
    if (pool != null) pool.keepAlive();
}, 180_000, 180_000);      // cada 3 minutos

JxJson — serialización propia

sin Jackson ni Gson
// Objeto → JSON string
String json = JxJson.toJson(miObjeto);
// Serializa campos públicos, listas, mapas y primitivos.

// En un controlador:
public ActionResult datos() {
    List<Producto> lista = repo.findAll();
    return json(lista);   // usa JxJson internamente
}

// En BaseController (convención recomendada):
protected ActionResult jsonOk(Object data) {
    return json("{\"ok\":true,\"data\":" + JxJson.toJson(data) + "}");
}
protected ActionResult jsonError(int status, String msg) {
    view.status(status);
    return json("{\"ok\":false,\"error\":" + JxJson.toJson(msg) + "}");
}

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 →