Reference
JxMVC 2.7 Docs
Jakarta EE 11 · Java 17+ · Cero dependencias en runtime · WAR ~177 KB
01 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() {
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.
@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
// 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
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
@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
@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
@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
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
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
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");
}
05 Seguridad
Autenticación y roles
// 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
// 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
06 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.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
// 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(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
// 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
// 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
// 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.
09 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