Reference
JxMVC 3.1 Docs
Jakarta EE 11 · Java 17+ · Cero dependencias en runtime · WAR ~205 KB
Routing
Convención URL
/controlador/accion/arg0/arg1 — cero configuración.
@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}.
@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.
@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.
@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() + "\"}");
}
Controladores
ActionResult — todos los tipos
// 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)
// 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
@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
@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);
}
}
Base de datos
JxModel — Active Record (v3.1)
// 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
// 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
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
Validación
Anotaciones sobre entidades
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
@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
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
// 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)
Seguridad
Autenticación y roles
// 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
// 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
// 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
# 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
Filtros globales
Registrar filtros
@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
// 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");
}
});
Cron & Async
@JxAsync — ejecución 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
// 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
@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
// 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)
Inyección de dependencias
@JxService · @JxInject
// 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
// 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.
Configuración completa
# ── 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