Estrategia de Modularización con Maven
El punto de partida consiste en estructurar el proyecto mediante un modelo padre-hijo. El artefacto principle actúa como gestor de dependencias, mientras que los módulos secundarios heredan su configuración. La cadena de herencia se define de manera lógica: la capa de API depende de la capa de servicios, esta última consume la capa de mapeo, la cual referencia los modelos de datos, y finalmente estos dependen de una biblioteca base compartida.
Herramientas de Modelado de Datos
Para estandarizar el diseño del esquema relacional, se emplean herramientas especializadas como PDMan. Estas utilidades permiten visualizar diagramas entidad-relación, generar scripts DDL automáticos y mantener un registro histórico de versiones sobre las estructuras tabulares.
Integración del Motor Spring Boot
1. Declaración de Dependencias
La coordinación entre el motor de aplicaciones, el compilador y el sistema de bases de datos es crítica. Se recomienda validar la compatibilidad cruzada antes de consolidar el archivo de construcción.
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Núcleo web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Procesador de configuración opcional -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- Persistencia con MyBatis-Plus v3.5.5+ -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
<exclusions>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Controlador JDBC MySQL 8+ -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.2.0</version>
</dependency>
<!-- Simplificador de accesores -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
</dependencies>
2. Archivo de Propiedades
Se definen los parámetros de escucha, el conjunto de conexiones HikariCP y el comportamiento del maper.
server:
port: 8090
tomcat:
uri-encoding: UTF-8
max-http-header-size: 80KB
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/tienda_online_dev?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC&autoReconnect=true
username: admin_db
password: clave_segura
hikari:
connection-timeout: 30000
minimum-idle: 5
maximum-pool-size: 20
auto-commit: true
idle-timeout: 600000
pool-name: PoolConexionesHikari
max-lifetime: 1800000
connection-test-query: SELECT 1
mybatis:
type-aliases-package: com.ecommerce.auth.modelo
mapper-locations: classpath:mappers/*.xml
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: auto
logic-delete-field: eliminado
logic-delete-value: 1
logic-not-delete-value: 0
MyBatis-Plus extiende las capacidades nativas sin alterar el núcleo, por lo que representa una elección sólida. Las capas de entidades y DAO pueden generarse automáticamente mediante plantillas o asistentes inteligentes que interpreten la estructura del DTO y la tabla destino.
3. Clase Principle
package com.ecommerce.auth.configuracion;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan(basePackages = {"com.ecommerce.auth", "com.lib.generadores.id"})
public class AplicacionPrincipal {
public static void main(String[] args) {
SpringApplication.run(AplicacionPrincipal.class, args);
}
}
Definición de Contratos REST
Las rutas siguen convenciones semánticas HTTP. Para facilitar la integración con clientes front-end, se implementa documentación interactiva migrada hacia estándares OpenAPI 3, utilizando Knife4j para mejorar la experiencia visual.
Dependencias Adicionales
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>4.4.0</version>
</dependency>
Configuración Descriptiva
package com.ecommerce.auth.configuracion;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DocumentacionAPI {
@Bean
public OpenAPI configurarEndpoints() {
return new OpenAPI()
.info(new Info()
.title("Gestión de Usuarios API")
.version("2.1.0")
.description("Contratos para operaciones de autenticación y registro")
.contact(new Contact()
.name("Soporte Técnico")
.email("soporte@dominio.dev")));
}
}
Inclusión en application.yml:
springdoc:
api-docs:
enabled: true
path: /v3/api-docs
swagger-ui:
enabled: true
path: /swagger-ui.html
group-configs:
- group: 'default'
paths-to-match: '/**'
packages-to-scan: com.ecommerce.auth.controlador
knife4j:
enable: true
setting:
language: zh_cn
swagger-model-name: Lista de Entidades
Resolución de Restricciones Cruzadas (CORS)
Se habilita la comunicación entre dominios configurando un filtro dedicado:
package com.ecommerce.auth.configuracion;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class SeguridadCruceDominios {
@Bean
public CorsFilter aplicadorCors() {
CorsConfiguration configuracion = new CorsConfiguration();
configuracion.addAllowedOrigin("http://localhost:3000");
configuracion.setAllowCredentials(true);
configuracion.addAllowedMethod("*");
configuracion.addAllowedHeader("*");
UrlBasedCorsConfigurationSource fuente = new UrlBasedCorsConfigurationSource();
fuente.registerCorsConfiguration("/api/**", configuracion);
return new CorsFilter(fuente);
}
}
Implementación de Funcionalidades Críticas
Verificación de Disponibilidad de Identificador
Antes de proceder al alta, se valida si el nombre seleccionado ya está registrado.
@Service
public class ServicioUsuariosImpl extends ServiceImpl<usuariodao usuario=""> implements ServicioUsuarios {
private final UsuarioDao daoUsuario;
public ServicioUsuariosImpl(UsuarioDao daoUsuario) {
this.daoUsuario = daoUsuario;
}
@Override
public boolean verificarDisponibilidadIdentificador(String identificador) {
if (identificador == null || identificador.trim().isEmpty()) {
return false;
}
var consulta = new LambdaQueryWrapper<Usuario>();
consulta.eq(Usuario::getNombreUsuario, identificador);
return daoUsuario.exists(consulta);
}
}</usuariodao>
Gestión de Alta de Cuenta
Se utiliza un Objeto de Transferencia (DTO) para encapsular solo los campos necesarios, evitando exponer atributos internos como la confirmación repetida de contraseña.
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SolicitudRegistro {
@NotBlank(message = "El usuario es requerido")
private String nombreUsuario;
@Size(min = 6, message = "La clave debe tener al menos 6 caracteres")
private String claveSecreta;
private String confirmarClave;
}
Lógica de persistencia con transformación hash y generación distribuida de identificadores:
@Service
public class GestionAlta {
private static final String AVATAR_POR_DEFECTO = "https://cdn.ejemplo.com/avatars/default.png";
public Usuario registrar(SolicitudRegistro solicitud) {
if (!solicitud.getClaveSecreta().equals(solicitud.getConfirmarClave())) {
throw new IllegalArgumentException("Las claves no coinciden");
}
Usuario perfil = new Usuario();
perfil.setId(GeneradorIDDistrito.obtenerTokenCorto());
perfil.setNombreUsuario(solicitud.getNombreUsuario());
perfil.setClave(HashUtil.encriptarMD5(solicitud.getClaveSecreta()));
perfil.setApodo(solicitud.getNombreUsuario());
perfil.setRostro(AVATAR_POR_DEFECTO);
perfil.fechaCreacion(new Date());
perfil.estadoActivo(true);
daoUsuario.insert(perfil);
return perfil;
}
}
Controlador de Operaciones
Respuesta estandarizada y enmascaramiento de datos sensibles antes de serializar hacia Cookies.
@RestController
@RequestMapping("/api/auth")
@Tag(name = "Autenticación y Acceso")
public class ControladorAcceso {
private final ServicioUsuarios servicio;
private final GestionAlta alto;
private final CookieHelper manejadorCookies;
@PostMapping("/verificar-usuario")
public RespuestaOperacion<boolean> comprobarExistencia(@RequestParam String usuario) {
boolean disponible = !servicio.verificarDisponibilidadIdentificador(usuario);
return RespuestaOperacion.exito(disponible ? "Identificador libre" : "Ya registrado", disponible);
}
@PostMapping("/crear-cuenta")
public RespuestaOperacion<object> darDeAlta(@RequestBody SolicitudRegistro payload,
HttpServletRequest req,
HttpServletResponse res) {
alto.registrar(payload);
Usuario perfilRecuperado = servicio.consultarPorNombre(payload.getNombreUsuario());
// Ocultar información sensible
PerfilDTO dtoSeguro = ocultarDatosCriticos(perfilRecuperado);
manejadorCookies.guardarEnCliente(req, res, "token_sesion", Serializador.objetoATexto(dtoSeguro));
return RespuestaOperacion.exito("Cuenta creada exitosamente");
}
private PerfilDTO ocultarDatosCriticos(Usuario u) {
return new PerfilDTO(u.getId(), u.getNombreUsuario(), u.getApodo(), null);
}
}</object></boolean>
Contenedor genérico de retorno:
@Data
public class RespuestaOperacion<T> {
private Integer codigo;
private String mensaje;
private T contenido;
private Long fechaHoraIso;
public RespuestaOperacion(Integer codigo, String mensaje, T contenido) {
this.codigo = codigo;
this.mensaje = mensaje;
this.contenido = contenido;
this.fechaHoraIso = System.currentTimeMillis();
}
public static <T> RespuestaOperacion<T> exito(String msg, T data) {
return new RespuestaOperacion<>(200, msg, data);
}
public static <T> RespuestaOperacion<T> fallo(String msg) {
return new RespuestaOperacion<>(500, msg, null);
}
}
Ingreso al Sistema
Validación de credenciales contra el repositorio cifrado y establecimiento de contexto:
@PostMapping("/entrar")
public RespuestaOperacion<object> iniciarSesion(@RequestBody CredencialesLogin credenciales,
HttpServletRequest req,
HttpServletResponse res) {
if (StringUtils.isEmpty(credenciales.getUsuario()) || StringUtils.isEmpty(credenciales.getClave())) {
return RespuestaOperacion.fallo("Propetario vacío");
}
Usuario valido = servicio.buscarCredencial(credenciales.getUsuario(), HashUtil.encriptarMD5(credenciales.getClave()));
if (valido == null) {
return RespuestaOperacion.fallo("Combinación inválida");
}
PerfilDTO datosPublicos = ocultarDatosCriticos(valido);
manejadorCookies.guardarEnCliente(req, res, "sesion_activa", Serializador.objetoATexto(datosPublicos));
return RespuestaOperacion.exito("Acceso concedido", datosPublicos);
}</object>
Terminación de Sesión
@PostMapping("/salir")
public RespuestaOperacion<void> finalizarSesion(@RequestParam String idUsuario,
HttpServletRequest req,
HttpServletResponse res) {
manejadorCookies.eliminarElemento(req, res, "sesion_activa");
// Pendiente: limpieza de carrito persistente y invalidación en cache distribuido
return RespuestaOperacion.exito("Sesión cerrada", null);
}</void>
Métricas de Ejecución vía Aspect Oriented Programming
Para auditar el rendimiento de las capas de negocio, se aplica un corte transversal que mide el ciclo de vida de los métodos anotados.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
package com.ecommerce.auth.interceptores;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class MonitorDeRendimiento {
private static final Logger traza = LoggerFactory.getLogger(MonitorDeRendimiento.class);
@Around("execution(* com.ecommerce.auth.servicio.impl..*.*(..))")
public Object medirTiempoProcesamiento(ProceedingJoinPoint joinPoint) throws Throwable {
long inicio = System.currentTimeMillis();
String descripcion = joinPoint.getTarget().getClass().getSimpleName() + "." + joinPoint.getSignature().getName();
traza.info("[INICIO OPERACIÓN] {}", descripcion);
Object resultado = joinPoint.proceed();
long duracionMs = System.currentTimeMillis() - inicio;
traza.info("[FIN OPERACIÓN] {} | Tiempo transcurrido: {}ms", descripcion, duracionMs);
if (duracionMs > 3000) {
traza.error("[ADVERTENCIA CRÍTICA] Operación lenta: {}", duracionMs);
} else if (duracionMs > 1500) {
traza.warn("[AVISO] Latencia elevada detectada: {}", duracionMs);
}
return resultado;
}
}