Ejemplo de manejo centralizado de excepciones en Java Spring

 🎯 Objetivo

✔ No usar try-catch en cada controlador
✔ Manejar errores desde un solo lugar (@ControllerAdvice)
✔ Mostrar páginas amigables (Thymeleaf + Bootstrap)
✔ Mantener separación limpia (MVC)


🧩 1. Escenario de ejemplo

Supongamos:

👉 Tienes un módulo de usuarios
👉 Buscas un usuario por ID
👉 Si no existe → lanzar excepción


🧱 2. Excepción personalizada

package com.tuempresa.login.exception;

public class RecursoNoEncontradoException extends RuntimeException {

    public RecursoNoEncontradoException(String mensaje) {
        super(mensaje);
    }
}

🧠 3. Servicio (donde se lanza la excepción)

@Service
public class UsuarioService {

    @Autowired
    private UsuarioRepository repo;

    public Usuario obtenerPorId(Long id) {
        return repo.findById(id)
                .orElseThrow(() -> new RecursoNoEncontradoException("Usuario no encontrado con ID: " + id));
    }
}

🎮 4. Controlador (SIN try-catch)

@Controller
@RequestMapping("/usuarios")
public class UsuarioController {

    @Autowired
    private UsuarioService service;

    @GetMapping("/{id}")
    public String verUsuario(@PathVariable Long id, Model model) {

        Usuario usuario = service.obtenerPorId(id);

        model.addAttribute("usuario", usuario);
        return "usuarios/usuario-detalle";
    }
}

👉 Nota: aquí NO hay manejo de errores


🌍 5. Manejo centralizado de Excepciones

GlobalExceptionHandler.java

package com.tuempresa.login.exception;

import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.security.access.AccessDeniedException;

@ControllerAdvice
public class GlobalExceptionHandler {

    // 404 - recurso no encontrado
    @ExceptionHandler(RecursoNoEncontradoException.class)
    public ModelAndView handleNotFound(RecursoNoEncontradoException ex) {

        ModelAndView mav = new ModelAndView("error/404");
        mav.addObject("mensaje", ex.getMessage());

        return mav;
    }

    // 403 - acceso denegado
    @ExceptionHandler(AccessDeniedException.class)
    public ModelAndView handleAccessDenied() {

        ModelAndView mav = new ModelAndView("error/403");
        mav.addObject("mensaje", "No tienes permisos para acceder");

        return mav;
    }

    // 500 - error general
    @ExceptionHandler(Exception.class)
    public ModelAndView handleGeneral(Exception ex) {

        ModelAndView mav = new ModelAndView("error/500");
        mav.addObject("mensaje", "Error interno del sistema");

        return mav;
    }
}

🎨 6. Vistas (Thymeleaf + Bootstrap)

📄 templates/error/404.html

<!DOCTYPE html>
<html>
<head>
    <title>No encontrado</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">

<div class="container text-center mt-5">
    <h1 class="text-warning">404</h1>
    <p th:text="${mensaje}"></p>
    <a href="/home" class="btn btn-primary">Volver</a>
</div>

</body>
</html>

📄 templates/error/403.html

<h1>403 - Acceso denegado</h1>
<p th:text="${mensaje}"></p>

📄 templates/error/500.html

<h1>500 - Error interno</h1>
<p th:text="${mensaje}"></p>

🎨 vista usuario-detalle.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Detalle de Usuario</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">

<div class="container mt-5">

    <div class="card shadow">
        
        <div class="card-header bg-primary text-white">
            <h4>Detalle de Usuario</h4>
        </div>

        <div class="card-body">

            <table class="table table-bordered">
                <tr>
                    <th>ID</th>
                    <td th:text="${usuario.id}"></td>
                </tr>

                <tr>
                    <th>Usuario</th>
                    <td th:text="${usuario.username}"></td>
                </tr>

                <tr>
                    <th>Estado</th>
                    <td>
                        <span th:if="${usuario.enabled}" class="badge bg-success">Activo</span>
                        <span th:unless="${usuario.enabled}" class="badge bg-danger">Inactivo</span>
                    </td>
                </tr>

                <tr>
                    <th>Roles</th>
                    <td>
                        <span th:each="rol : ${usuario.roles}" 
                              th:text="${rol.nombre}" 
                              class="badge bg-info me-1">
                        </span>
                    </td>
                </tr>
            </table>

            <div class="mt-3">
                <a href="/home" class="btn btn-secondary">Volver</a>
            </div>

        </div>

    </div>

</div>

</body>
</html>

Probar desde un boton x
<a th:href="@{/usuarios/1}" class="btn btn-primary">Ver</a>

🧠 Flujo completo

  1. Usuario accede a /usuarios/10

  2. ❌ No existe → RecursoNoEncontradoException

  3. 👉 @ControllerAdvice la captura

  4. 👉 retorna vista error/404

  5. ✔ Usuario ve página bonita


🚀 Resultado final

✔ Código limpio
✔ Sin try-catch repetitivos
✔ Manejo centralizado real
✔ Vistas profesionales
✔ Escalable para APIs o MVC


Comentarios

Entradas populares de este blog

Aprende a Armar tu PC con el Simulador de Cisco 🖥️ Guía Paso a Paso

¿Qué es XAMPP y cómo descargarlo e instalarlo en Windows?

🗄️ Cómo Instalar y usar SQLite en Windows [Guía Paso a Paso]