[{"content":"Si nuestros endpoints devuelven String o Map, ya contamos con una API plenamente funcional, pero no es la mejor solución. El problema es que la estructura de la respuesta está demasiado abierta, lo que la hace más frágil y difícil de mantener.\nPara especificar de forma clara el contrato de la API, es conveniente introducir en este punto los DTO (Data Transfer Objects).\nEl problema de devolver un Map Hasta ahora, nuestra API devuelve datos, pero sin estar representados de forma explícita y mantenible. Spring serializa el Map sin problema a JSON, pero presenta una serie de inconvenientes que lo hacen poco adecuado para una API que empieza a evolucionar:\nNo está tipado: No existe una estructura definida que indique qué campos debe contener la respuesta. Sin documentación clara: la forma del JSON no queda reflejada en ningún tipo o clase, dificultando entender qué devuelve realmente el endpoint. Fácil equivocarse en claves: las claves son cadenas de texto, por lo que un error tipográfico puede pasar desapercibido hasta tiempo de ejecución. Un cambio de nombre rompe contratos con clientes fácilmente: modificar una clave puede afectar a clientes que dependen de esa estructura sin que el compilador detecte el problema. Perdemos claridad y el apoyo del compilador durante el desarrollo: no contamos con autocompletado ni comprobaciones de tipos al trabajar con los datos. No es escalable: a medida que la respuesta crece, el Map se vuelve más difícil de leer, mantener y evolucionar. El problema no es que falle, es que la estructura de la respuesta queda implícita, dispersa y demasiado abierta.\nQué es un DTO Un DTO (Data Transfer Object) es un objeto cuyo objetivo es transportar datos entre capas o entre sistemas. En una API, uno de sus usos más habituales es representar de forma explícita los datos que enviamos o recibimos en los diferentes endpoints.\nSon objetos simples cuya función es representar datos con una estructura clara y fija, sin contener ninguna lógica de negocio. De esta forma, la estructura de la respuesta queda definida de manera explícita y controlada.\nPrimer DTO en nuestra API La respuesta que devolvemos actualmente ya está serializada en un JSON con el siguiente contenido:\n{ \u0026#34;message\u0026#34;: \u0026#34;Hello Juan from Spring Boot!\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;Juan\u0026#34;, \u0026#34;timestamp\u0026#34;: \u0026#34;2026-04-04T11:30:38.578535500Z\u0026#34; } Spring la está formando a partir de un Map en el endpoint:\n1 2 3 4 5 6 7 8 9 @GetMapping(\u0026#34;/hello\u0026#34;) public ResponseEntity\u0026lt;Map\u0026lt;String, String\u0026gt;\u0026gt; hello(@RequestParam(defaultValue = \u0026#34;World\u0026#34;) String name) { Map\u0026lt;String, String\u0026gt; content = Map.of( \u0026#34;message\u0026#34;, \u0026#34;Hello \u0026#34; + name + \u0026#34; from Spring Boot!\u0026#34;, \u0026#34;name\u0026#34;, name, \u0026#34;timestamp\u0026#34;, Instant.now().toString() ); return ResponseEntity.ok(content); } Un primer DTO para la respuesta de nuestro endpoint podría ser el siguiente, incluyendo un atributo por cada campo que queremos devolver:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class HelloResponseDto { private String message; private String name; private String timestamp; public HelloResponseDto(String message, String name, String timestamp) { this.message = message; this.name = name; this.timestamp = timestamp; } public String getMessage() { return message; } public String getName() { return name; } public String getTimestamp() { return timestamp; } } Quedando el endpoint de la siguiente manera, sin alterar la respuesta de la petición:\n1 2 3 4 5 6 7 8 9 @GetMapping(\u0026#34;/hello\u0026#34;) public ResponseEntity\u0026lt;HelloResponseDto\u0026gt; hello(@RequestParam(defaultValue = \u0026#34;World\u0026#34;) String name) { HelloResponseDto content = new HelloResponseDto( \u0026#34;Hello \u0026#34; + name + \u0026#34; from Spring Boot!\u0026#34;, name, Instant.now().toString() ); return ResponseEntity.ok(content); } Ahora la estructura de la respuesta queda definida de forma explícita en un tipo Java, facilitando su comprensión y evolución.\nUsando record de Java Como el objeto HelloResponseDto solo transporta datos, Java ofrece una forma más compacta de representarlo: record. Un record define datos inmutables, reduce el código repetitivo y encaja muy bien en objetos simples de transporte.\nPodríamos redefinir la clase HelloResponseDto de la siguiente manera:\n1 public record HelloResponseDto(String message, String name, String timestamp) { } El endpoint no necesita modificarse, ya que el record implementa automáticamente el constructor y los métodos de acceso.\nVentajas frente a Map Un Map describe datos de forma improvisada; un DTO describe datos de forma intencional. Más claridad: el nombre HelloResponseDto expresa qué estamos devolviendo. Estructura explícita: los campos están definidos en un tipo. Menos errores involuntarios en el desarrollo: no dependemos de escribir claves String a mano Mejor mantenimiento: el cambio está centralizado. Mejor evolución: más fácil añadir nuevos campos sin desordenar el controlador Evolución de nuestra API En el endpoint /hello comenzamos devolviendo un String, luego mejoramos la respuesta con ResponseEntity. Después pasamos a devolver un JSON. Y ahora damos un paso más definiendo explícitamente su estructura mediante DTOs. De esta forma, el contrato con los clientes se vuelve más robusto, mantenible y fiable, sentando además una base sólida para seguir evolucionando la API.\n","permalink":"/posts/sustituyendo-map-dto-api/","summary":"\u003cp\u003eSi nuestros endpoints devuelven \u003ccode\u003eString\u003c/code\u003e o \u003ccode\u003eMap\u003c/code\u003e, ya contamos con una API plenamente funcional, pero no es la mejor solución. El problema es que \u003cstrong\u003ela estructura de la respuesta está demasiado abierta\u003c/strong\u003e, lo que la hace más \u003cstrong\u003efrágil y difícil de mantener\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003ePara especificar de forma clara el contrato de la API, es conveniente introducir en este punto los DTO (\u003cem\u003eData Transfer Objects\u003c/em\u003e).\u003c/p\u003e\n\u003ch1 id=\"el-problema-de-devolver-un-map\"\u003eEl problema de devolver un Map\u003c/h1\u003e\n\u003cp\u003eHasta ahora, nuestra API devuelve datos, pero sin estar representados de forma explícita y mantenible. Spring serializa el \u003ccode\u003eMap\u003c/code\u003e sin problema a JSON, pero presenta una serie de inconvenientes que lo hacen poco adecuado para una API que empieza a evolucionar:\u003c/p\u003e","title":"Sustituyendo Map por un DTO en nuestra API"},{"content":"Hasta ahora nuestro código está devolviendo un String de forma directa que Spring Boot convierte automáticamente en una respuesta HTTP completa. Ésta, por defecto, incluye un código de estado (200 OK) y un cuerpo con el mensaje.\nEl siguiente paso para mejorar el diseño de nuestra API es controlar completamente la respuesta y adaptarla a nuestra lógica, para lo que contamos con la clase ResponseEntity.\nQué es una respuesta HTTP Una respuesta HTTP es el mensaje que devuelve el servidor cuando un cliente realiza una petición a una API. Esta respuesta incluye principalmente un código de estado, que indica si la operación ha sido correcta o ha ocurrido algún problema, y un cuerpo con los datos que queremos devolver.\n200 OK Content-Type: application/json { \u0026#34;Hello World from Spring Boot!\u0026#34; } En nuestro caso:\n200 OK: la petición se ha procesado correctamente. Content-Type: indica el tipo de contenido, JSON en este caso. Hello World from Spring Boot!: cuerpo de la respuesta, un String que Spring Boot serializa automáticamente. Este concepto es importante porque, a medida que nuestra API evolucione, no solo devolveremos texto, sino también distintos códigos HTTP y estructuras más completas, y ahí es donde entra ResponseEntity.\nControlando la respuesta con ResponseEntity ResponseEntity es una clase de Spring que permite controlar completamente la respuesta HTTP que devuelve nuestra API. En lugar de devolver solo el contenido, podemos definir también el código de estado, las cabeceras y el cuerpo de la respuesta.\nHasta ahora, nuestro controlador devuelve un String directamente:\n1 2 3 4 @GetMapping(\u0026#34;/hello\u0026#34;) public String hello(@RequestParam(defaultValue = \u0026#34;World\u0026#34;) String name) { return \u0026#34;Hello \u0026#34; + name + \u0026#34; from Spring Boot!\u0026#34;; } Podemos usar ResponseEntity para indicar explícitamente el código HTTP que debe incluir:\n1 2 3 4 5 @GetMapping(\u0026#34;/hello\u0026#34;) public ResponseEntity\u0026lt;String\u0026gt; hello(@RequestParam(defaultValue = \u0026#34;World\u0026#34;) String name) { String content = \u0026#34;Hello \u0026#34; + name + \u0026#34; from Spring Boot!\u0026#34;; return ResponseEntity.ok(content); } En este caso seguimos devolviendo el mismo contenido, pero ahora estamos construyendo explícitamente una respuesta HTTP, algo fundamental cuando queramos devolver errores (códigos 400 o 404), recursos creados ( código 201) o respuestas más complejas.\nCompletando la respuesta con un JSON Otra mejora para conseguir una respuesta más adecuada para una API es devolver el contenido como un objeto JSON bien estructurado.\n1 2 3 4 5 6 7 @GetMapping(\u0026#34;/hello\u0026#34;) public ResponseEntity\u0026lt;Map\u0026lt;String, String\u0026gt;\u0026gt; hello(@RequestParam(defaultValue = \u0026#34;World\u0026#34;) String name) { Map\u0026lt;String, String\u0026gt; content = Map.of( \u0026#34;message\u0026#34;, \u0026#34;Hello \u0026#34; + name + \u0026#34; from Spring Boot!\u0026#34; ); return ResponseEntity.ok(content); } En el caso de un String simple puede parecer de poco interés, pero lo habitual será que la respuesta incluya mucha más información, de forma que un JSON permita estructurar mejor los datos.\n1 2 3 4 5 6 7 8 9 @GetMapping(\u0026#34;/hello\u0026#34;) public ResponseEntity\u0026lt;Map\u0026lt;String, String\u0026gt;\u0026gt; hello(@RequestParam(defaultValue = \u0026#34;World\u0026#34;) String name) { Map\u0026lt;String, String\u0026gt; content = Map.of( \u0026#34;message\u0026#34;, \u0026#34;Hello \u0026#34; + name + \u0026#34; from Spring Boot!\u0026#34;, \u0026#34;name\u0026#34;, name, \u0026#34;timestamp\u0026#34;, Instant.now().toString() ); return ResponseEntity.ok(content); } Si levantamos el servidor y, como venimos haciendo, realizamos una petición desde el navegador, obtendremos la respuesta con el JSON completo.\nValidando la API con los test Una vez más, debemos usar los test para validar los cambios realizados. Hasta ahora, el contrato de nuestra API incluía una respuesta HTTP formada automáticamente con un único String en el cuerpo. Pero ahora, la respuesta es mucho más completa, con otro formato y con el mensaje dentro de un JSON.\nSi ejecutamos los test existentes, éstos fallarán, indicando que hemos roto ese contrato.\nCambio de contrato con los clientes Si nuestra API se encontrase en producción, esto nos indicaría que tendríamos un problema que podríamos afrontar con dos planteamientos:\nObligar a todos nuestros clientes a actualizar sus desarrollos para adaptarse al nuevo formato de respuesta. Versionar nuestra API, manteniendo el endpoint actual sin modificar y publicar el nuevo en una nueva ruta, permitiendo a los clientes migrar de forma progresiva. Evolucionando los tests Para validar el nuevo desarrollo, adaptamos los test a la nueva respuesta del endpoint.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Test void helloWithoutParamReturnsDefaultValue() throws Exception { mockMvc.perform( get(\u0026#34;/hello\u0026#34;) ) .andExpect( status().isOk() ) .andExpect( jsonPath(\u0026#34;$.message\u0026#34;).value(\u0026#34;Hello World from Spring Boot!\u0026#34;) ) .andExpect( jsonPath(\u0026#34;$.name\u0026#34;).value(\u0026#34;World\u0026#34;) ) .andExpect( jsonPath(\u0026#34;$.timestamp\u0026#34;).isString() ); } @Test void helloWithParamReturnsCustomName() throws Exception { mockMvc.perform(get(\u0026#34;/hello\u0026#34;).param(\u0026#34;name\u0026#34;, \u0026#34;Juan\u0026#34;)) .andExpect(status().isOk()) .andExpect(jsonPath(\u0026#34;$.message\u0026#34;).value(\u0026#34;Hello Juan from Spring Boot!\u0026#34;)) .andExpect(jsonPath(\u0026#34;$.name\u0026#34;).value(\u0026#34;Juan\u0026#34;)) .andExpect(jsonPath(\u0026#34;$.timestamp\u0026#34;).isString()); } jsonPath(\u0026quot;$.message\u0026quot;) indica a MockMvc que acceda al campo message del JSON devuelto:\n$: representa la raíz del JSON $.message: accede al campo message .value(...): comprueba que ese campo tiene el valor esperado Por lo tanto, permite validar un campo concreto dentro del JSON que devuelve la API y verifica que:\nLa respuesta tiene formato JSON Existe el campo message Su valor es exactamente el esperado en cada caso Este enfoque permite validar respuestas más complejas de forma precisa, comprobando cada campo individualmente en lugar de comparar todo el contenido como una cadena de texto, lo que hace los tests más robustos y fáciles de mantener.\nConclusiones Mejorando la respuesta de nuestra API, pasando de devolver texto simple a diseñar respuestas HTTP más completas, estamos evolucionándola de forma natural para conseguir un resultado más profesional. Además, con el trabajo continuo con los test, conseguimos validar el contrato que establecemos con nuestros clientes, de forma que podamos realizar esa evolución con seguridad.\n","permalink":"/posts/mejorando-respuesta-api/","summary":"\u003cp\u003eHasta ahora nuestro código está devolviendo un \u003ccode\u003eString\u003c/code\u003e de forma directa que \u003cstrong\u003eSpring Boot convierte automáticamente en una respuesta HTTP completa\u003c/strong\u003e. Ésta, por defecto, incluye un código de estado (\u003ccode\u003e200 OK\u003c/code\u003e) y un cuerpo con el mensaje.\u003c/p\u003e\n\u003cp\u003eEl siguiente paso para mejorar el diseño de nuestra API es \u003cstrong\u003econtrolar completamente la respuesta y adaptarla a nuestra lógica\u003c/strong\u003e, para lo que contamos con la clase \u003ccode\u003eResponseEntity\u003c/code\u003e.\u003c/p\u003e\n\u003ch1 id=\"qué-es-una-respuesta-http\"\u003eQué es una respuesta HTTP\u003c/h1\u003e\n\u003cp\u003eUna \u003cstrong\u003erespuesta HTTP\u003c/strong\u003e es el mensaje que devuelve el servidor cuando un cliente realiza una petición a una API. Esta respuesta incluye principalmente \u003cstrong\u003eun código de estado\u003c/strong\u003e, que indica si la operación ha sido correcta o ha ocurrido algún problema, y \u003cstrong\u003eun cuerpo con los datos\u003c/strong\u003e que queremos devolver.\u003c/p\u003e","title":"Mejorando la respuesta de nuestra API"},{"content":"Una API es la puerta de entrada a nuestra aplicación: recibe peticiones desde el exterior, extrae los datos que le envían, ejecuta la lógica necesaria y devuelve una respuesta. Hasta ahora, en nuestra API HelloWorld solo recibimos peticiones y damos una respuesta. El siguiente paso es permitir que un cliente nos pase información a través de la URL.\nFormas en las que una API puede recibir datos El componente encargado de extraer los datos de la petición es el controlador. En una API HTTP, éstos pueden llegar al endpoint de varias maneras según su finalidad:\nEn la URL como parámetros En la URL como parte de la ruta En el cuerpo de la petición En las cabeceras de la petición En cookies Como formularios Como ficheros Vamos a ver la primera y más sencilla de ellas: recibir datos como parámetros de la URL.\nPrimer dato de entrada en nuestra API Nuestra API de momento, está devolviendo un saludo genérico. A partir de aquí, empezamos a construir APIs que reaccionan a lo que les envía el cliente.\nhttp://localhost:8080/hello?name=Juan Para recibirlo en el controlador y poder utilizarlo, incorporamos un parámetro con el mismo nombre al método del endpoint y lo anotamos con @RequestParam. De esta forma Spring Boot extrae automáticamente el valor de la URL y lo pasa como argumento al método.\n1 2 3 4 @GetMapping(\u0026#34;/hello\u0026#34;) public String hello(@RequestParam String name) { return \u0026#34;Hello \u0026#34; + name + \u0026#34; from Spring Boot!\u0026#34;; } Si desplegamos la aplicación y probamos desde el navegador, vemos el resultado:\nPero, ¿y si no incluimos el parámetro name en la URL?\nParámetros opcionales Con la configuración actual, la aplicación está esperando siempre recibir el parámetro name en la petición, devolviendo un error de no ser así.\nPara evitarlo, podemos marcar los parámetros como opcionales con la propiedad required de la siguiente manera:\n1 2 3 4 5 6 7 @GetMapping(\u0026#34;/hello\u0026#34;) public String hello(@RequestParam(required = false) String name) { if( name == null ) { return \u0026#34;Hello World from Spring Boot!\u0026#34;; } return \u0026#34;Hello \u0026#34; + name + \u0026#34; from Spring Boot!\u0026#34;; } En este caso, si no se envía el parámetro, Spring asigna el valor null.\nValores por defecto La anotación cuenta con la propiedad defaultValue que nos permite simplificar la lógica en el controlador, fijando un valor por defecto si no se informa el parámetro, en cuyo caso Spring asume automáticamente que se trata de un parámetro opcional. Cuando usamos defaultValue, Spring considera automáticamente que el parámetro es opcional, por lo que no es necesario indicar required = false.\n1 2 3 4 @GetMapping(\u0026#34;/hello\u0026#34;) public String hello(@RequestParam(defaultValue = \u0026#34;World\u0026#34;) String name) { return \u0026#34;Hello \u0026#34; + name + \u0026#34; from Spring Boot!\u0026#34;; } Cuando usar parámetros en la URL Los @RequestParam se utilizan cuando queremos recibir datos simples en la URL, normalmente para:\nfiltros búsquedas valores opcionales Los tests como red de seguridad ante cambios Cada vez que cambiamos el comportamiento de un endpoint, los tests nos ayuda a comprobar si de cara al cliente el cambio ha sido transparente. No solo validan el código, también documentan cómo debe comportarse la API.\nSi lanzamos los tests que ya habíamos implementado, vemos que siguen ejecutándose correctamente. Esto ocurre porque hemos mantenido el comportamiento por defecto gracias al uso de defaultValue.\n1 2 3 4 5 6 @Test void helloEndpointReturnsHelloWorld() throws Exception { mockMvc.perform( get(\u0026#34;/hello\u0026#34;) ) .andExpect( status().isOk() ) .andExpect( content().string(\u0026#34;Hello World from Spring Boot!\u0026#34;) ); } Los tests evolucionan con el código Unos test útiles y completos deben cubrir los distintos escenarios posibles, y deben ampliarse junto con el código de la API. Nuestro test cubre el comportamiento por defecto, pero deberemos completarlo ahora para el caso de recibir un parámetro.\n1 2 3 4 5 6 @Test void helloWithParamReturnsCustomName() throws Exception { mockMvc.perform(get(\u0026#34;/hello\u0026#34;).param(\u0026#34;name\u0026#34;, \u0026#34;Juan\u0026#34;)) .andExpect(status().isOk()) .andExpect(content().string(\u0026#34;Hello Juan from Spring Boot!\u0026#34;)); } Elementos nuevos que usa el test Herramientas para simular peticiones HTTP param(): Spring Boot añade este parámetro a la URL como si el cliente lo hubiera enviado en la petición real. Nombres que reflejen intención Adicionalmente, para reflejar mejor qué caso valida cada test, actualizamos también el nombre del test previo:\n1 2 3 4 5 6 @Test void helloWithoutParamReturnsDefaultValue() throws Exception { mockMvc.perform( get(\u0026#34;/hello\u0026#34;) ) .andExpect( status().isOk() ) .andExpect( content().string(\u0026#34;Hello World from Spring Boot!\u0026#34;) ); } Lanzamos de nuevo la ejecución de los tests y comprobamos que finalizan correctamente.\nConclusiones Las APIs son las puertas de entrada a nuestras aplicaciones por lo que será habitual recibir datos en alguno de los formatos y por alguna de las vías que proporciona Spring Boot. Los parámetros en la URL son una de las más sencillas y útiles. A partir de aquí, iremos incorporando nuevas formas de recibir datos en nuestra API a medida que evolucionamos sus funcionalidades.\nTambién, cada vez que cambiamos el comportamiento de nuestra API, los tests nos ayudan a validar y revisar los cambios de forma que podamos detectar si estamos modificando algún caso de uso que no teníamos contemplado, pero para que sigan siendo útiles, deben completarse a medida que ampliamos nuestra API.\n","permalink":"/posts/recibiendo-datos-request-param/","summary":"\u003cp\u003eUna API es \u003cstrong\u003ela puerta de entrada a nuestra aplicación\u003c/strong\u003e: recibe peticiones desde el exterior,\nextrae los datos que le envían, ejecuta la lógica necesaria y devuelve una respuesta. Hasta\nahora, en \u003ca href=\"/posts/spring-boot-hello-world/\"\u003enuestra API \u003cem\u003eHelloWorld\u003c/em\u003e\u003c/a\u003e solo recibimos\npeticiones y damos una respuesta. El siguiente paso es \u003cstrong\u003epermitir que un cliente nos pase\ninformación a través de la URL\u003c/strong\u003e.\u003c/p\u003e\n\u003ch1 id=\"formas-en-las-que-una-api-puede-recibir-datos\"\u003eFormas en las que una API puede recibir datos\u003c/h1\u003e\n\u003cp\u003eEl componente encargado de extraer los datos de la petición es el controlador. En una API HTTP, éstos pueden llegar al endpoint de varias maneras según su finalidad:\u003c/p\u003e","title":"Recibiendo datos en una API con @RequestParam"},{"content":"El Principio de Responsabilidad Única nos dice que clases y funciones deben tener una responsabilidad clara y específica para conseguir código de mayor calidad. Pero podemos ir un paso más allá aplicando este mismo criterio para organizar una aplicación completa, agrupando componentes según su responsabilidad y definiendo cómo se comunican entre sí.\nEste enfoque permite estructurar el sistema de forma que sea más robusto, mantenible y fácil de evolucionar.\nEn qué consiste La arquitectura por capas consiste en organizar una aplicación en módulos que comparten una responsabilidad clara, estableciendo además una dirección definida en sus dependencias. Cada capa se comunica únicamente con las capas adyacentes.\nLos objetivos que persigue son:\nReducir el acoplamiento. Facilitar el mantenimiento. Permitir evolución tecnológica sin afectar al núcleo. Mejorar la testabilidad. Aislar la lógica de negocio. Estructura básica Una división mínima podría representarse así:\n[ Entrada ] - Capa de entrada/transporte/salida (HTTP, API...) ↓ [ Negocio ] - Lógica de negocio ↓ [ Persistencia ] - Acceso a datos ↓ [ Base de datos ] La capa de entrada recibe peticiones y devuelve respuestas. La capa de negocio contiene la lógica que implementa las reglas de la aplicación. La capa de persistencia se encarga del acceso a los datos. Cada capa tiene responsabilidades claras y solo conoce la capa inmediatamente inferior. De esta forma se evita que detalles de infraestructura o transporte se mezclen con la lógica de negocio.\nPor ejemplo:\nValidar que una dirección de correo tiene un formato correcto pertenece a la capa de entrada, ya que depende de cómo llegan los datos (JSON, formulario, etc.). Comprobar si ese correo pertenece a un cliente registrado es una regla de negocio, por lo que debe implementarse en la capa de negocio. De la misma forma, el cálculo del importe total de un pedido debería hacerse en la capa de negocio. Si esta lógica se implementara en la base de datos, cambiar de sistema gestor implicaría rehacer parte de la lógica de la aplicación.\nCómo se rompe la arquitectura por capas Estructurar una aplicación por capas no consiste únicamente en separar el código en carpetas o crear clases llamadas Controller, Service o Repository. Lo importante es respetar las responsabilidades y la dirección de las dependencias.\nUn ejemplo de una API de gestión de pedidos donde aparentemente existen las tres capas puede ser el siguiente:\nControlador 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @RestController @RequestMapping(\u0026#34;/orders\u0026#34;) public class OrderController { @Autowired private OrderService orderService; @Autowired private CouponRepository couponRepository; @PostMapping public ResponseEntity\u0026lt;OrderResponseDto\u0026gt; createOrder(@RequestBody OrderRequestDto request) { Coupon coupon = null; if (request.getCouponCode() != null \u0026amp;\u0026amp; !request.getCouponCode().isBlank()) { coupon = couponRepository.findByCode(request.getCouponCode()) .orElseThrow(() -\u0026gt; new RuntimeException(\u0026#34;Cupón no encontrado\u0026#34;)); } Order order = orderService.createOrder(request, coupon); return ResponseEntity.ok(mapper.toResponse(order)); } } Servicio 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 @Service public class OrderService { @Autowired private UserRepository userRepository; @Autowired private ProductRepository productRepository; @Autowired private OrderRepository orderRepository; public Order createOrder(OrderRequestDto request, Coupon coupon) { User user = userRepository.findById(request.getUserId()) .orElseThrow(() -\u0026gt; new RuntimeException(\u0026#34;Usuario no encontrado\u0026#34;)); BigDecimal totalBeforeDiscount = BigDecimal.ZERO; for (OrderItemRequestDto item : request.getItems()) { Product product = productRepository.findById(item.getProductId()) .orElseThrow(() -\u0026gt; new RuntimeException(\u0026#34;Producto no encontrado\u0026#34;)); BigDecimal lineTotal = product.getPrice() .multiply(BigDecimal.valueOf(item.getQuantity())); totalBeforeDiscount = totalBeforeDiscount.add(lineTotal); } BigDecimal discountApplied = BigDecimal.ZERO; if (coupon != null \u0026amp;\u0026amp; coupon.isActive()) { discountApplied = totalBeforeDiscount .multiply(coupon.getPercentage()) .divide(BigDecimal.valueOf(100)); } Order order = new Order(); order.setUser(user); order.setCreatedAt(LocalDateTime.now()); order.setTotalBeforeDiscount(totalBeforeDiscount); order.setDiscountApplied(discountApplied); order.setTotalFinal(totalBeforeDiscount.subtract(discountApplied)); return orderRepository.save(order); } } Repositorio 1 2 3 4 @Repository public interface CouponRepository extends JpaRepository\u0026lt;Coupon, Long\u0026gt; { Optional\u0026lt;Coupon\u0026gt; findByCode(String code); } A primera vista parece una aplicación bien estructurada: existen controladores, servicios y repositorios. Sin embargo, la arquitectura por capas no se está respetando.\nQué está mal aquí 1. El controlador accede directamente al repositorio El Controller consulta CouponRepository por su cuenta. Esto rompe la dirección natural de comunicación: Controller → Service → Repository; y hace que la capa de entrada dependa directamente de la persistencia.\n2. La lógica de negocio queda repartida El controlador decide si hay que buscar el cupón y pasa el resultado al servicio. Parte del caso de uso empieza en el controlador y continúa en el servicio.\n3. El servicio no controla completamente el caso de uso El método createOrder depende de que el controlador haya resuelto previamente cierta información, por lo que el servicio deja de representar el flujo completo de creación de pedidos.\n4. El servicio recibe estructuras de transporte El servicio recibe OrderRequestDto, que pertenece a la capa de entrada. Esto mezcla el modelo de transporte con la lógica de negocio.\nProblemas que genera Este tipo de diseño puede funcionar, pero genera varios problemas:\nReglas repartidas: cambios en la lógica obligan a modificar varias capas. Menor reutilización: otros puntos de entrada (batch, eventos, etc.) necesitarían replicar parte de la lógica. Servicios menos autónomos: el servicio ya no representa el caso de uso completo. Controladores demasiado complejos: empiezan a conocer detalles de persistencia y reglas de negocio. Versión correcta En una arquitectura por capas el controlador debería limitarse a recibir la petición y delegar el caso de uso en la capa de negocio.\n1 2 3 4 5 6 @PostMapping public ResponseEntity\u0026lt;OrderResponseDto\u0026gt; createOrder(@RequestBody OrderRequestDto request) { CreateOrderDTO orderDto = mapper.toCreateOrderDto(request); Order order = orderService.createOrder(orderDto); return ResponseEntity.ok(mapper.toResponse(order)); } El servicio se encarga entonces de todo el flujo del caso de uso:\n1 2 3 4 5 6 7 8 public Order createOrder(CreateOrderDTO orderDto) { Discount discount = discountService.calculateDiscount(orderDto); // buscar usuario // buscar productos // validar cupón // calcular totales // guardar pedido } De esta forma:\nel Controller gestiona el transporte (HTTP, DTOs, respuesta), el Service implementa la lógica de negocio, los Repositories se encargan únicamente del acceso a datos. Conclusiones Una arquitectura por capas no se rompe solo cuando una clase hace demasiado, sino también cuando las responsabilidades parecen separadas pero el flujo real del caso de uso queda repartido entre capas que no deberían conocer esos detalles.\nTener clases llamadas Controller, Service y Repository no garantiza una arquitectura por capas. Lo que realmente la define es respetar las responsabilidades de cada capa y mantener una dirección clara en las dependencias.\n","permalink":"/posts/arquitectura-capas-aplicaciones/","summary":"\u003cp\u003eEl \u003ca href=\"/posts/principio-responsabilidad-unica/\"\u003ePrincipio de Responsabilidad Única\u003c/a\u003e nos dice que clases y funciones deben tener una\nresponsabilidad clara y específica para conseguir código de mayor calidad. Pero podemos ir un paso más allá aplicando este mismo criterio para \u003cstrong\u003eorganizar una aplicación completa\u003c/strong\u003e, agrupando componentes según su responsabilidad y definiendo cómo se comunican entre sí.\u003c/p\u003e\n\u003cp\u003eEste enfoque permite estructurar el sistema de forma que sea \u003cstrong\u003emás robusto, mantenible y fácil de evolucionar\u003c/strong\u003e.\u003c/p\u003e\n\u003ch1 id=\"en-qué-consiste\"\u003eEn qué consiste\u003c/h1\u003e\n\u003cp\u003eLa arquitectura por capas consiste en \u003cstrong\u003eorganizar una aplicación en módulos que comparten una responsabilidad clara\u003c/strong\u003e, estableciendo además una \u003cstrong\u003edirección definida en sus dependencias\u003c/strong\u003e. Cada capa se comunica únicamente con las capas adyacentes.\u003c/p\u003e","title":"Arquitectura por capas en aplicaciones"},{"content":"Una vez que hemos desacoplado la ejecución de una aplicación de nuestro equipo y su entorno desplegándola en un contenedor, el siguiente paso es independizar también el proceso de compilación y construcción. El objetivo es evitar también problemas de configuraciones y dependencias relacionadas con el entorno de desarrollo, moviendo la responsabilidad a un proceso automatizado. De esta forma nos acercarnos más a un entorno de producción.\nEnfoque de partida El Dockerfile con el que hemos trabajado previamente incluye lo mínimo para desplegar el JAR:\n1 2 3 4 FROM eclipse-temurin:17-jre-alpine WORKDIR /app COPY target/app.jar app.jar ENTRYPOINT [\u0026#34;java\u0026#34;,\u0026#34;-jar\u0026#34;,\u0026#34;app.jar\u0026#34;] Los problemas que presenta son:\nNecesitamos compilar la aplicación fuera del contenedor. Dependencias de Maven instaladas en el entorno de desarrollo. No es reproducible al 100%. El proceso de compilación y despliegue depende de varios pasos manuales. Para completar el proceso, podríamos incorporar en el mismo contenedor lo necesario para realizar la compilación, pero todas las herramientas necesarias para ello convivirían con la aplicación desplegada en el contenedor de producción.\nQué es un multi-stage build Un multi-stage build es una técnica de Docker que consiste en usar un contenedor para construir la aplicación y otro distinto, mucho más pequeño, solo para ejecutarla. Así Docker, compila dentro de un contenedor pesado con las dependencias necesarias para ese proceso, descarta todo lo innecesario y se queda con la aplicación final compilada para desplegarla en otro contenedor mucho más ligero.\nEsquema del proceso Stage 1 (builder) Tiene Maven Compila el proyecto Genera el JAR Stage 2 (runtime) Solo tiene Java Copia el JAR Ejecuta la app De esta forma, el contenedor final no contiene Maven, ni código fuente, ni caché.\nNuevo Dockerfile multi-stage El nuevo Dockerfile completo con las dos etapas quedaría así:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # ---------- STAGE 1: build ---------- FROM maven:3.9.9-eclipse-temurin-21-alpine AS build WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests # ---------- STAGE 2: runtime ---------- FROM eclipse-temurin:21-jre-alpine-3.23 WORKDIR /app COPY --from=build /app/target/*-SNAPSHOT.jar app.jar EXPOSE 8080 ENTRYPOINT [\u0026#34;java\u0026#34;,\u0026#34;-jar\u0026#34;,\u0026#34;app.jar\u0026#34;] Stage 1 - Build con Maven FROM maven:3.9.9-eclipse-temurin-21-alpine AS build\nImagen base con Maven + JDK 21. AS build: le ponemos nombre a esta fase para poder referenciarla después. WORKDIR /app\nEstablece el directorio de trabajo dentro del contenedor. A partir de aquí, todos los comandos se ejecutan desde /app. (equivalente a hacer cd /app) COPY pom.xml .\nCopia únicamente el pom.xml al contenedor. Se hace así para aprovechar la caché de Docker y no tener que descargar dependencias cada vez que cambia el código fuente. RUN mvn dependency:go-offline\nDescarga todas las dependencias declaradas en el pom.xml. Si después cambia el código pero no las dependencias, Docker reutiliza esta capa y el build es mucho más rápido. go-offline es un goal del plugin de Maven, indica que se deben descargar todas las dependencias necesarias para que el proyecto pueda compilarse sin conexión a internet. Es útil para mantener las descargas en la caché de Docker. COPY src ./src\nCopia el código fuente de la aplicación dentro del contenedor. Ya tenemos todo lo necesario para compilar. RUN mvn clean package -DskipTests\nclean elimina compilaciones anteriores. package compila y genera el JAR ejecutable. -DskipTests evita ejecutar los tests durante la construcción de la imagen. En entornos reales, los tests suelen ejecutarse previamente en un pipeline de integración continua. Separar la fase de validación de la fase de construcción permite builds más rápidos y predecibles. Aquí se genera el mismo JAR que antes creábamos en local, pero ahora dentro del contenedor. Stage 2 - Runtime limpio Comienza una nueva fase completamente independiente.\nFROM eclipse-temurin:21-jre-alpine-3.23\nImagen ligera con solo el entorno de ejecución Java. No incluye Maven ni herramientas de desarrollo. WORKDIR /app De nuevo, definimos el directorio de trabajo. Cada FROM reinicia el contexto, por eso hay que volver a declararlo. COPY --from=build /app/target/*-SNAPSHOT.jar app.jar\n--from=build indica que copiamos desde el stage anterior. Copiamos el JAR generado dentro del contenedor de build. EXPOSE 8080\nIndica que la aplicación escucha en el puerto 8080. No abre el puerto, solo lo documenta a nivel de imagen. ENTRYPOINT [\u0026quot;java\u0026quot;,\u0026quot;-jar\u0026quot;,\u0026quot;app.jar\u0026quot;]\nDefine el comando que se ejecutará cuando el contenedor arranque. Lanza la aplicación Spring Boot. Es equivalente a ejecutar java -jar app.jar Con esto, hemos separado el proceso de construcción del proceso de ejecución. El contenedor ya no depende del entorno del desarrollador, sino de un proceso reproducible y aislado. Igual que separamos responsabilidades en el código, aquí separamos responsabilidades en el contenedor.\nConstrucción de los contenedores De forma similar a cómo construíamos la imagen anterior, lanzamos el siguiente comando en consola que, ahora realizará la compilación de la aplicación y generará la imagen para desplegarla (etiquetada como version 2.0).\ndocker build -t hello-world-api:2.0 . En este caso el tiempo para finalizar será superior, aunque será más evidente solo en la primera ejecución. Si el proceso finaliza correctamente, tendremos la nueva imagen disponible.\nPara desplegar el contenedor con la aplicación, ejecutamos el siguiente comando:\ndocker run -p 8080:8080 hello-world-api:2.0 De esta forma, tendremos la aplicación compilada y desplegada, sin haber ejecutado Maven en nuestro equipo.\nComparativa con el enfoque anterior Hemos conseguido desacoplar la compilación del entorno del desarrollador y mover esa responsabilidad al contenedor. Pero además usando multi-stage conseguimos que esa imagen final:\nNo incluye herramientas de construcción innecesarias. Mantiene una imagen final ligera. Evita inflar la imagen con Maven y código fuente. Reproducible totalmente en otros entornos. Lista para CI/CD. Más segura (al incluir solo los elementos mínimos para su ejecución). Reflexión Existe una premisa fundamental a la hora de trabajar con contenedores: Una imagen debe contener solo lo necesario para ejecutarse. Nada más. Debido a ello, multi-stage es el planteamiento recomendado en entornos reales, salvo para la construcción de prototipos ultra rápidos.\nPor otro lado, desacoplando la compilación del entorno del desarrollador ya no importa que versión de Maven tiene, si está instalado, si otro desarrollador usa otro sistema operativo, si el servidor se actualiza, etc. El contenedor se convierte en el entorno estándar de construcción de la aplicación. Esto es el enfoque DevOps.\n","permalink":"/posts/compilar-app-maven-docker/","summary":"\u003cp\u003eUna vez que hemos desacoplado la ejecución de una aplicación de nuestro equipo y su entorno desplegándola en un contenedor, el siguiente paso es \u003cstrong\u003eindependizar también el proceso de compilación y construcción\u003c/strong\u003e. El objetivo es evitar también problemas de configuraciones y dependencias relacionadas con el entorno de desarrollo, \u003cstrong\u003emoviendo la responsabilidad a un proceso automatizado\u003c/strong\u003e. De esta forma nos acercarnos más a un entorno de producción.\u003c/p\u003e\n\u003ch1 id=\"enfoque-de-partida\"\u003eEnfoque de partida\u003c/h1\u003e\n\u003cp\u003eEl Dockerfile con el que hemos trabajado previamente incluye lo mínimo para desplegar el JAR:\u003c/p\u003e","title":"Compilar con Maven dentro de Docker - multi-stage build"},{"content":"Hoy día usamos la palabra bug de forma cotidiana para referirnos a cualquier error de software. Pero lo curioso es que este término, tan ligado a la informática moderna, nació de un fallo completamente literal.\nEn el año 1947, un equipo de ingenieros trabajaba con el ordenador electromecánico Harvard Mark II. Durante una sesión de diagnóstico, el sistema empezó a comportarse de forma errática. Tras investigar el problema, encontraron la causa: una polilla atrapada en uno de los relés del equipo, impidiendo su correcto funcionamiento.\nEl insecto fue retirado y pegado en el cuaderno de incidencias, acompañado de la anotación: “First actual case of bug being found” (“Primer caso real de un bug encontrado”).\nRegistro del \u0026lsquo;primer bug\u0026rsquo; encontrado en el Harvard Mark II (1947). Imagen de dominio público, cortesía del Naval Surface Warfare Center, vía Wikimedia Commons.\nAunque el término bug ya se utilizaba de forma informal en ingeniería para referirse a fallos mecánicos, este episodio popularizado por Grace Hopper, ayudó a consolidarlo definitivamente en el vocabulario informático.\nMás allá de la anécdota, la historia es interesante por lo que representa. En aquellos primeros sistemas, hardware y software estaban íntimamente ligados. Un fallo podía ser causado por un problema eléctrico, mecánico… o incluso biológico. Hoy los errores son menos visibles, pero el concepto sigue siendo el mismo: algo rompe un supuesto que dábamos por válido.\nTambién resulta curioso que el equipo documentara el incidente con tanto cuidado. Desde el inicio, la informática entendió que registrar errores es tan importante como resolverlos. Los actuales sistemas de logging, trazas y monitorización son herederos directos de ese cuaderno donde acabó pegada una polilla.\nEn la actualidad, aunque el software se haya vuelto abstracto y complejo, los errores siguen siendo inevitables. Cambian de forma, de escala y de impacto, pero siempre están ahí. Y a veces, entender su origen, aunque sea tan humilde como un insecto, nos ayuda a diseñar sistemas más robustos y a asumir que fallar es parte del proceso.\n","permalink":"/posts/primer-bug-informatico/","summary":"\u003cp\u003eHoy día usamos la palabra \u003cem\u003ebug\u003c/em\u003e de forma cotidiana para referirnos a cualquier error de software. Pero lo curioso es que este término, tan ligado a la informática moderna, \u003cstrong\u003enació de un fallo completamente literal\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eEn el año 1947, un equipo de ingenieros trabajaba con el ordenador electromecánico \u003ca href=\"https://es.wikipedia.org/wiki/Harvard_Mark_II\"\u003eHarvard Mark II\u003c/a\u003e. Durante una sesión de diagnóstico, el sistema empezó a comportarse de forma errática. Tras investigar el problema, encontraron la causa: \u003cstrong\u003euna polilla atrapada en uno de los relés del equipo\u003c/strong\u003e, impidiendo su correcto funcionamiento.\u003c/p\u003e","title":"El primer bug informático fue literalmente un insecto"},{"content":"El principio de responsabilidad única es uno de los principios SOLID. Defiende los beneficios de que clases y funciones tengan una responsabilidad clara y específica, de forma que solo se tengan que modificar por un motivo. Aplicándolo conseguimos que nuestro código sea más claro y mantenible.\nEl problema real Seguro que en algún momento de tu vida como desarrollador te has cruzado con clases que validan datos, acceden a base de datos, construyen respuestas, escriben logs, aplican lógica de negocio\u0026hellip; todo en un mismo sitio.\nAplicar una modificación en estas clases, por muy pequeña y sencilla que parezca, además de la dificultad de ubicarla genera una alta probabilidad de generar problemas.\nUn ejemplo reducido de una clase que viola el principio podría ser el siguiente UserService.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class UserService { public void createUser(User user) { // Validacion de los datos de entrada if (user.getEmail() == null) { throw new IllegalArgumentException(\u0026#34;Email obligatorio\u0026#34;); } // Guardar en base de datos database.save(user); // Enviar email emailSender.sendWelcomeEmail(user); // Log logger.info(\u0026#34;Usuario creado: \u0026#34; + user.getEmail()); } } Esta clase tiene las responsabilidades de: validar datos, persistencia, comunicación externa, logging y lógica de negocio. Demasiadas razones para cambiar.\nResponsabilidad única en clases Si aplicamos el principio de responsabilidad única a esa misma clase, podría quedar de la siguiente manera.\n1 2 3 4 5 6 7 8 9 10 11 12 public class UserService { private final UserValidator validator; private final UserRepository repository; private final NotificationService notificationService; public void createUser(User user) { validator.validate(user); repository.save(user); notificationService.sendWelcome(user); } } De esta forma obtenemos:\nCódigo más fácil de leer Cambios más seguros Test más simples Menos miedo a tocar código antiguo Menos clases Dios Responsabilidad única en funciones Este mismo problema surge a nivel de funciones. Una función debería hacer solo una cosa y hacerla bien. Y en este caso, una sola cosa no significa una sola línea, sino un único nivel de abstracción (qué hace frente al cómo lo hace).\nDe forma similar al ejemplo anterior de la clase, podríamos encontrarnos la siguiente función.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public void processOrder(Order order) { // Validación if (order == null || order.getItems().isEmpty()) { throw new IllegalArgumentException(\u0026#34;Pedido inválido\u0026#34;); } // Cálculo del total BigDecimal total = BigDecimal.ZERO; for (OrderItem item : order.getItems()) { total = total.add(item.getPrice().multiply( BigDecimal.valueOf(item.getQuantity()))); } // Persistencia order.setTotal(total); orderRepository.save(order); // Comunicación externa emailService.sendConfirmation(order); } ¿Cuántos motivos distintos podrían obligarme a modificar este código?\nCambio en las reglas de validación de los datos del pedido (dirección de entrega obligatoria). Cambio en el cálculo del importe total del pedido (incluir gastos de envío). Cambia la forma de almacenar los datos por infraestructura (se migra a otro sistema gestor de base de datos). Cambia el canal de notificación (se envía un sms). Aplicando SRP a la función, separando responsabilidades y manteniendo el comportamiento, podríamos hacer la siguiente división.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public void processOrder(Order order) { validate(order); BigDecimal total = calculateTotal(order); saveOrder(order, total); sendConfirmation(order); } private void validate(Order order) { if (order == null || order.getItems().isEmpty()) { throw new IllegalArgumentException(\u0026#34;Pedido inválido\u0026#34;); } } private BigDecimal calculateTotal(Order order) { BigDecimal total = BigDecimal.ZERO; for (OrderItem item : order.getItems()) { total = total.add( item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())) ); } return total; } private void saveOrder(Order order, BigDecimal total) { order.setTotal(total); orderRepository.save(order); } private void sendConfirmation(Order order) { emailService.sendConfirmation(order); } Aunque ahora hay más funciones, el código se lee como una historia. Permite cambiar una responsabilidad sin tocar las otras y facilita los test.\nEs importante no confundir responsabilidad única con hacer una sola acción. La función processOrder sigue realizando varias acciones (validar, calcular, guardar y notificar), pero todas forman parte de una misma responsabilidad: gestionar el proceso completo de un pedido.\nLa función solo cambiará si cambia el flujo del proceso, no si cambian los detalles internos de validación, cálculo o persistencia.\nNo lo lleves al extremo Algunos de los errores más comunes al intentar aplicar SRP son:\nReducir tanto como para llegar a una clase = un método Crear abstracciones innecesarias, llegando a niveles muy profundos Aplicarlo solo a clases y funciones grandes Pensar ya lo simplificaremos (no se hará) En aplicaciones pequeñas o scripts simples, dividir responsabilidades puede introducir más complejidad que beneficio. El diseño debe responder al contexto, no a principios aplicados de forma automática.\nConclusiones El principio de responsabilidad única no es una regla estricta, es una alarma. Cuando una clase o función empieza a crecer sin control, probablemente no esté siendo “productiva”, sino acumulando responsabilidades que no le corresponden.\n","permalink":"/posts/principio-responsabilidad-unica/","summary":"\u003cp\u003eEl principio de responsabilidad única \u003cstrong\u003ees uno de los principios SOLID\u003c/strong\u003e. Defiende los beneficios de que clases y funciones tengan una \u003cstrong\u003eresponsabilidad clara y específica\u003c/strong\u003e, de forma que \u003cstrong\u003esolo se tengan que modificar por un motivo\u003c/strong\u003e. Aplicándolo conseguimos que nuestro código sea más claro y mantenible.\u003c/p\u003e\n\u003ch1 id=\"el-problema-real\"\u003eEl problema real\u003c/h1\u003e\n\u003cp\u003eSeguro que en algún momento de tu vida como desarrollador te has cruzado con clases que validan datos, acceden a base de datos, construyen respuestas, escriben logs, aplican lógica de negocio\u0026hellip; todo en un mismo sitio.\u003c/p\u003e","title":"El principio de responsabilidad única"},{"content":"Alrededor de la programación hay una serie de tareas menos agradecidas y poco atractivas para la mayoría de los desarrolladores: el análisis, la documentación… los tests y las pruebas. Con cada nuevo evolutivo solemos lanzarnos directamente al teclado, abrir nuestro IDE y empezar a picar código, cuando muchas veces lo más efectivo a largo plazo es comenzar con papel y bolígrafo, desgranando qué queremos hacer realmente.\nCon las pruebas y los tests sucede algo similar. Durante el desarrollo vamos lanzando comprobaciones manuales y, cuando vemos que todo más o menos funciona, lo damos por válido. Puede que incluso documentemos alguna de ellas, pero suele percibirse como otra tarea pesada que no siempre motiva y que intentamos quitarnos de encima cuanto antes.\nSin embargo, al igual que un buen análisis, diseño y planificación nos ahorra tiempo y evita errores a largo plazo, un buen diseño de los casos de prueba y su automatización contribuye a obtener un mejor resultado final. Además, nos garantiza que podremos detectar rápidamente si un nuevo evolutivo introduce errores que no habíamos contemplado.\nTesting en Spring Boot Spring Boot incorpora de serie varias herramientas que facilitan enormemente la implementación de los primeros tests. Entre ellas destacan:\nJUnit 5: es un framework de testing que nos permite definir y ejecutar test en Java mediante anotaciones como @Test. Spring Boot Test: conjunto de utilidades que facilita probar aplicaciones Spring cargando el contexto, inyectando dependencias y simulando peticiones HTTP. Maven configurado para ejecutar test: Maven ejecuta automáticamente los test durante el ciclo de compilación, fallando el proceso si alguno no pasa y evitando la generación de artefactos incorrectos. Primer test de Hello World Para implementar los primeros tests nos vamos a basar en el Hello World que desarrollamos en el artículo anterior y que ya hemos compilado con Maven.\nAl crear la estructura básica del proyecto con Spring Initializr e incluir la dependencia Spring Web, ya deberíamos tener en el pom.xml la dependencia spring-boot-starter-webmvc-test, que nos proporciona todo lo necesario para estos primeros tests.\nLa aplicación arranca Se trata de un test de contexto, anotado con @SpringBootTest, que ya tendremos creado automáticamente en la estructura del proyecto dentro de la carpeta src/test. En nuestro caso se denomina SpringBootHelloWorldApplicationTests.\n1 2 3 4 5 6 7 8 9 10 11 12 13 package dev.juanfbermejo.SpringBootHelloWorld; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class SpringBootHelloWorldApplicationTests { @Test void contextLoads() { } } Este test no contiene lógica alguna, pero precisamente ahí reside su valor. Comprueba que el contexto de Spring arranca correctamente y, si falla, indica que existe un problema grave en la configuración de la aplicación.\nTestear un endpoint sencillo: Hello World El siguiente paso es diseñar e implementar un test para nuestro endpoint /hello. Aunque se trata de un endpoint muy sencillo, ya podemos validar tres aspectos básicos:\nQue el endpoint existe bajo la URL Que responde con código HTTP 200 Que devuelve el contenido esperado El código del test es el siguiente:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package dev.juanfbermejo.SpringBootHelloWorld; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; import org.springframework.test.web.servlet.MockMvc; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest @AutoConfigureMockMvc public class HelloControllerTest { @Autowired private MockMvc mockMvc; @Test void helloEndpointReturnsHelloWorld() throws Exception { mockMvc.perform( get(\u0026#34;/hello\u0026#34;) ) .andExpect( status().isOk() ) .andExpect( content().string(\u0026#34;Hello World from Spring Boot!\u0026#34;) ); } } Elementos que se usan en el test Anotaciones de Spring y JUnit: @Autowired: le indica a Spring que inyecte automáticamente una instancia de MockMvc en el test. @Test: anotación de JUnit 5 que marca un método como test y permite que el framework lo ejecute durante la fase de testing. @AutoConfigureMockMvc: indica a Spring Boot que configure automáticamente MockMvc, permitiéndonos simular peticiones HTTP a los endpoints sin arrancar un servidor real. Herramientas para simular peticiones HTTP: MockMvc: utilidad de Spring para simular llamadas HTTP (GET, POST, etc.) contra los controladores y verificar sus respuestas. get(): construye una petición HTTP de tipo GET contra la ruta indicada, en nuestro ejemplo /hello. perform(): ejecuta la petición HTTP simulada y definida previamente. Devuelve el resultado para poder hacer comprobaciones sobre la respuesta. Validación de la respuesta: andExpect(): define una expectativa sobre la respuesta obtenida; si no se cumple, el test falla. status() e isOk(): comprueban que el código de estado HTTP de la respuesta sea 200 (OK). content(): permite validar el contenido del cuerpo de la respuesta. Ejecutar los test con Maven Una vez configurados los tests, podemos ejecutarlos desde el propio IDE o, al igual que para la construcción del artefacto, desde la línea de comandos utilizando Maven.\nDesde un terminal, situados en la carpeta raíz del proyecto, ejecutamos:\nmvn test Maven compilará el código y lanzará todos los tests definidos en el proyecto, mostrando el resultado en la consola.\nSi forzamos un error -por ejemplo, cambiando el texto esperado en el cuerpo de la respuesta- y ejecutamos de nuevo el comando, veremos cómo el test falla. La salida por consola indicará qué test ha fallado, qué se esperaba que ocurriera y cuál ha sido el resultado real, proporcionando información muy útil para entender el problema y corregirlo.\nConclusiones En este artículo hemos dado el primer paso en la incorporación de tests automatizados a nuestra API Spring Boot. Partiendo de un ejemplo muy sencillo, hemos visto cómo validar que la aplicación arranca correctamente y cómo comprobar el comportamiento de un endpoint HTTP mediante tests automatizados.\nHasta ahora, en la serie hemos creado una API básica, la hemos compilado con Maven y la hemos preparado para su ejecución. Con estos primeros tests añadimos una capa fundamental de seguridad que nos permitirá evolucionar el código con mayor confianza.\nEn los siguientes artículos iremos ampliando este enfoque, incorporando tests más completos y sentando las bases para su integración en un proceso de integración continua (CI), donde estos tests se ejecutarán automáticamente en cada cambio del código.\n","permalink":"/posts/primer-test-spring-boot/","summary":"\u003cp\u003eAlrededor de la programación hay una serie de tareas menos agradecidas y poco atractivas para la mayoría de los desarrolladores: \u003cstrong\u003eel análisis, la documentación… los tests y las pruebas\u003c/strong\u003e. Con cada nuevo evolutivo solemos lanzarnos directamente al teclado, abrir nuestro IDE  y empezar a \u003cem\u003epicar código\u003c/em\u003e, cuando muchas veces \u003cstrong\u003elo más efectivo a largo plazo es comenzar con papel y bolígrafo, desgranando qué queremos hacer realmente\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eCon las pruebas y los tests sucede algo similar. Durante el desarrollo vamos lanzando comprobaciones manuales y, cuando vemos que todo más o menos funciona, lo damos por válido. Puede que incluso documentemos alguna de ellas, pero suele percibirse como otra tarea pesada que no siempre motiva y que intentamos quitarnos de encima cuanto antes.\u003c/p\u003e","title":"Primer test en Spring Boot"},{"content":"Hasta ahora, hemos creado una aplicación Hello World con Spring Boot, la hemos compilado con Maven y la hemos ejecutado correctamente en nuestro equipo. Sin embargo, ejecutar una aplicación en local es solo el primer paso.\nEn cuanto queremos compartirla, desplegarla en un servidor o moverla entre distintos entornos (desarrollo, pruebas, producción), empiezan a aparecer los problemas clásicos: versiones diferentes de Java, dependencias que no coinciden, configuraciones específicas de la máquina o incluso sistemas operativos diferentes.\nAquí es donde encapsular la aplicación en un contenedor se convierte en el siguiente paso natural.\nQué es un contenedor Un contenedor nos permite empaquetar la aplicación junto con todo lo que necesita para ejecutarse: la versión de Java, las dependencias y la configuración básica del entorno. El resultado es un paquete autocontenido que se comporta igual en cualquier máquina donde se ejecute.\nDocker es una de las herramientas más extendidas para crear, ejecutar y gestionar contenedores de forma sencilla.\nConfiguración del contenedor De forma similar al fichero pom.xml de Maven, para configurar un contenedor Docker usaremos un fichero Dockerfile (sin extensión) que se debe ubicar en la misma ruta del proyecto.\nCon el siguiente Dockerfile mínimo se está creando de la forma más simple y limpia un contenedor Docker para ejecutar una aplicación Spring Boot:\n1 2 3 4 5 6 7 8 9 FROM eclipse-temurin:21-jre-alpine-3.23 WORKDIR /app COPY target/SpringBootHelloWorld-0.0.1-SNAPSHOT.jar app.jar EXPOSE 8080 ENTRYPOINT [\u0026#34;java\u0026#34;,\u0026#34;-jar\u0026#34;,\u0026#34;app.jar\u0026#34;] En primer lugar se indica una imagen base para el contenedor. En este caso se trata de una distribución ligera de Linux con Java 21 (solo el entorno de ejecución), suficiente para ejecutar una aplicación Spring Boot. Después se indica el directorio de trabajo dentro del contenedor. A partir de aquí, todos los comandos se ejecutarán desde /app. Se copia el JAR generado por Maven desde el equipo anfitrión al contenedor, renombrándolo como app.jar. El nombre del JAR debe coincidir exactamente con el generado. Se indica el puerto en el que escucha la aplicación, que en Spring Boot suele ser el 8080 por defecto, pero a nivel informativo, no se abre el puerto. Por último, se define el comando que se ejecutará al arrancar el contenedor, lanzando la aplicación Spring Boot. Construcción de la imagen El primer paso es construir una imagen Docker a partir de la cual se podrán crear instancias del contenedor y desplegarlas.\nCon Docker ejecutándose, y desde la raíz del proyecto, se lanza el siguiente comando:\n1 docker build -t hello-world-api:1.0 . Qué significa:\n-t helloWorld-api:1.0 es el nombre y versión de la imagen. . indica que se trabaja en el directorio actual, donde está el Dockerfile sobre el que generar la imagen. Si todo va bien, podemos listar las imágenes del sistema con el comando docker images.\nEjecutar el contenedor Para crear y ejecutar un contenedor a partir de la imagen generada, se lanza el siguiente comando:\n1 docker run -p 8080:8080 --name hello-world-api-container hello-world-api:1.0 Qué significa:\n-p 8080:8080 indica el puerto del equipo donde se quieren recibir las peticiones del contenedor, seguido del puerto del contenedor a donde deben dirigirse. En este caso, usa el mismo. --name helloWorld-api-container indica un nombre para identificar al contenedor. De nuevo, si todo va bien, podremos abrir un navegador y acceder a la url http://localhost:8080 y ver el mensaje de \u0026lsquo;Hello World\u0026rsquo;, esta vez lanzado desde la API en el contenedor.\nPara parar el contenedor, que se habrá quedado en la terminal, pulsamos la combinación de teclas Ctrl + C.\nOtros comandos útiles Ejecución del contenedor en segundo plano Para no tener que mantener la ventana de terminal con el contenedor en ejecución, se puede lanzar en segundo plano añadiendo -d al comando run de la siguiente manera:\n1 docker run -d -p 8080:8080 --name helloWorld-api-container helloWorld-api:1.0 Ver logs del contenedor Para consultar los logs en la consola de Spring Boot dentro del contenedor, lanzamos el siguiente comando:\n1 docker logs helloWorld-api-container Detener el contenedor Al no estar ahora ligada la ejecución del contenedor a la sesión de terminal, para detenerlo, se debe lanzar el siguiente comando:\n1 docker stop helloWorld-api-container Conclusiones Ya hemos dado un paso clave: llevar nuestra aplicación Spring Boot desde un simple JAR hasta un contenedor ejecutable, garantizando que se comporta igual independientemente del entorno donde se despliegue.\nEl flujo que hemos seguido hasta ahora es muy sencillo, pero extremadamente importante:\nHemos desarrollado una API sencilla en Spring Boot Con Apache Maven hemos generado un artefacto ejecutable (el JAR) Ese JAR lo hemos encapsulado en un contenedor usando Docker, resolviendo de un plumazo los problemas de dependencias, versiones y entornos. Hemos visto que desplegar la aplicación se reduce a construir una imagen y arrancar un contenedor. A partir de aquí, se abre un abanico de mejoras naturales como optimizar el proceso, orquestar varios servicios y llegar a automatizar todo el flujo de construcción y despliegue. Todo ello de forma cercana a un entorno real de producción.\n","permalink":"/posts/desplegar-aplicacion-contenedor/","summary":"\u003cp\u003eHasta ahora, \u003ca href=\"/posts/spring-boot-hello-world/\"\u003ehemos creado una aplicación Hello World con Spring Boot\u003c/a\u003e, la hemos \u003ca href=\"/posts/compilar-app-maven/\"\u003ecompilado con Maven y la hemos ejecutado correctamente en nuestro equipo\u003c/a\u003e. Sin embargo, \u003cstrong\u003eejecutar una aplicación en local es solo el primer paso\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eEn cuanto queremos compartirla, desplegarla en un servidor o moverla entre distintos entornos (desarrollo, pruebas, producción), empiezan a aparecer los problemas clásicos: \u003cstrong\u003eversiones diferentes de Java, dependencias que no coinciden, configuraciones específicas de la máquina o incluso sistemas operativos diferentes\u003c/strong\u003e.\u003c/p\u003e","title":"Desplegar una aplicación Spring Boot en un contenedor"},{"content":"¿Qué es Docker? Docker permite crear y ejecutar aplicaciones dentro de contenedores, facilitando un entorno consistente y reproducible tanto en desarrollo y despliegue. Esto evita el clásico “en mi máquina funciona” y lo convierte en una herramienta clave en flujos modernos de CI/CD.\nInstalación de Docker en Windows En Windows, la forma recomendada de trabajar con Docker es mediante Docker Desktop, ya que su instalación es muy sencilla e incluye todo lo necesario para empezar.\nRequisitos previos En la actualidad, para poder instalar Docker en Windows, el sistema debe cumplir los siguientes requisitos:\nContar con Windows 10 u 11 de 64 bits (Home o Pro) Tener habilitada la virtualización en la BIOS Tener activo WSL 2 (Windows Subsystem for Linux) Virtualización habilitada Para poder ejecutar Docker y contenedores en un equipo, no solo debe contar con un procesador que lo soporte, también debe tener habilitada la opción de virtualización en la BIOS.\nPara verificarlo, se puede acudir al Administrador de tareas de Windows, sección CPU, donde en la parte inferior aparece una etiqueta que indica si la virtualización está habilitada.\nSi la configuración indica que está deshabilitada, se debe cambiar desde la BIOS del propio equipo siguiendo las indicaciones específicas del fabricante.\nInstalación de WSL 2 WSL (Windows Subsystem for Linux) permite ejecutar un entorno Linux directamente en Windows. Su versión más moderna, WSL 2, ofrece mejor rendimiento y compatibilidad, y es la opción recomendada para trabajar con Docker. Docker Desktop utiliza WSL 2 como backend para ejecutar los contenedores Linux de forma eficiente dentro de Windows.\nLa instalación es muy sencilla, solo habrá que lanzar el comando siguiente en una terminal.\n1 wsl --install Esto, automáticamente:\nHabilita las características necesarias de Windows. Instala WSL 2 (la versión recomendada). Descarga e instala Ubuntu como distribución por defecto. Tras un reinicio, nos pedirá crear un usuario de Linux con una contraseña (no es necesario que coincida con los de Windows.)\nPara usar Linux desde Windows, basta con abrir una terminal y lanzar el siguiente comando.\n1 wsl Descarga e instalación de Docker Desktop Docker Desktop para Windows está disponible para su descarga desde la web oficial de Docker. Ahí está disponible un instalador ejecutable.\nUna vez descargado, basta con ejecutarlo siguiendo los pasos marcados, y marcando la opción de \u0026ldquo;Use WSL 2 instead of Hyper-V\u0026rdquo; cuando nos lo solicite.\nAl finalizar, es posible que sea necesario reiniciar el equipo. Tras ello, ejecutamos Docker Desktop desde el menú Inicio. La primera vez puede tardar unos minutos mientras se configuran los componentes necesarios, llegando a una pantalla similar a la siguiente si todo ha ido bien.\nTambién se puede verificar que todo está funcionando desde la terminal, lanzado el siguiente comando que mostrará la versión en ejecución.\n1 docker --version Opcionalmente, se puede lanzar un contenedor de prueba que mostrará un mensaje de bienvenida con el siguiente comando.\n1 docker run hello-world Conclusión Con Docker Desktop instalado en Windows ya tenemos un entorno preparado para trabajar con contenedores de forma local. A partir de ahí ya se pueden crear y ejecutar contenedores e integrar Docker en proyectos de desarrollo. Las posibilidades son infinitas y con cierta complejidad si queremos especializarnos, pero la potencia que ofrecen los contenedores, aun con configuraciones básicas, ya son muy grandes.\n","permalink":"/posts/instalar-docker-windows/","summary":"\u003ch1 id=\"qué-es-docker\"\u003e¿Qué es Docker?\u003c/h1\u003e\n\u003cp\u003eDocker permite \u003cstrong\u003ecrear y ejecutar aplicaciones dentro de contenedores\u003c/strong\u003e, facilitando un \u003cstrong\u003eentorno consistente y reproducible tanto en desarrollo y despliegue\u003c/strong\u003e. Esto evita el clásico \u003cem\u003e“en mi máquina funciona”\u003c/em\u003e y lo convierte en una herramienta clave en flujos modernos de CI/CD.\u003c/p\u003e\n\u003ch1 id=\"instalación-de-docker-en-windows\"\u003eInstalación de Docker en Windows\u003c/h1\u003e\n\u003cp\u003eEn Windows, la forma recomendada de trabajar con Docker es mediante \u003ca href=\"https://www.docker.com/products/docker-desktop/\"\u003eDocker Desktop\u003c/a\u003e, ya que su instalación es muy sencilla e incluye todo lo necesario para empezar.\u003c/p\u003e","title":"Pasos para instalar Docker en Windows"},{"content":"Una de las fuentes habituales de errores a la hora de desarrollar software y administrar sistemas informáticos es el tiempo. Formatos, husos horarios, conversiones\u0026hellip; suelen generar problemas cuando diferentes aplicaciones o sistemas no están alineados.\nEn sistemas tipo Unix, el tiempo se representa como el número de segundos transcurridos desde el 1 de enero de 1970. Este valor se conoce como Unix time. El problema aparece cuando ese valor se almacena en un entero de 32 bits con signo. Ese tipo de dato tiene un valor máximo que, cuando se alcanza, el contador se desborda y pasa a valores negativos.\nEl efecto 2000 A finales de los años 90, el mundo de la tecnología vivió una cuenta atrás peculiar que se denominó efecto 2000. En etapas iniciales de desarrollo de los sistemas informáticos la memoria era un elemento con un coste elevado, por lo que muchos almacenaban el año con sólo dos dígitos. Asumían implícitamente que siempre sería “19xx”, lo cual iba a provocar que al pasar de \u0026ldquo;99\u0026rdquo; a \u0026ldquo;00\u0026rdquo; se interpretase el año 2000 como 1900.\nEl problema se mitigó sin incidentes relevantes y dejó una lección: las decisiones técnicas aparentemente inocentes, en sistemas destinados a perdurar en el tiempo, pueden tener consecuencias mucho tiempo después.\nEl tiempo se romperá de nuevo en 2038 El 19 de enero de 2038 a las 03:14:07 UTC los segundos transcurridos desde la fecha base en Unix alcanzarán el valor máximo que pueden almacenar los sistemas de 32 bits. En el siguiente, el valor se desbordará y el tiempo \u0026ldquo;volverá atrás\u0026rdquo;, provocando comportamientos impredecibles.\nA diferencia del año 2000, esta vez, el problema no afecta tanto a sistemas de escritorio modernos, sino a pequeños dispositivos de infraestructura. Esto son: sistemas embebidos, dispositivos IoT, maquinaria industrial o software legado que sigue funcionando años después de haber sido escrito. Muchos de estos sistemas no se actualizan con facilidad, o directamente no se actualizan, y podrían seguir funcionando dentro de 10 o 15 años y romperse de golpe.\n1 2 3 4 5 6 7 public class Year2038Example { public static void main(String[] args) { int maxSeconds = Integer.MAX_VALUE; System.out.println(maxSeconds); // 2147483647 System.out.println(maxSeconds + 1); // -2147483648 } } No es un fallo del futuro, sino una deuda del pasado. Una decisión técnica antigua que sigue vigente. Diseñar pensando a largo plazo, así como tener en cuenta los supuestos ocultos o extremos, es una de las responsabilidades de los ingenieros de software.\n","permalink":"/posts/bug-tiempo-desborda/","summary":"\u003cp\u003eUna de las fuentes habituales de errores a la hora de desarrollar software y administrar sistemas informáticos es el tiempo. Formatos, husos horarios, conversiones\u0026hellip; suelen generar problemas cuando diferentes aplicaciones o sistemas no están alineados.\u003c/p\u003e\n\u003cp\u003eEn sistemas tipo Unix, el tiempo se representa como \u003cstrong\u003eel número de segundos transcurridos desde el 1 de enero de 1970\u003c/strong\u003e. Este valor se conoce como \u003cem\u003eUnix time\u003c/em\u003e. El problema aparece cuando ese valor se almacena en un \u003cstrong\u003eentero de 32 bits con signo\u003c/strong\u003e. Ese tipo de dato tiene un valor máximo que, cuando se alcanza, el contador se desborda y pasa a valores negativos.\u003c/p\u003e","title":"El bug del año 2038: cuando el tiempo se desborda"},{"content":"Aunque hoy día existen muy buenos IDEs con numerosos plugins que permiten centrarse en el código y en el desarrollo de la propia aplicación, es útil también conocer qué hacen internamente y poder lanzar tareas de forma más cercana a cómo se haría en un entorno de producción.\nObjetivo Se trata de compilar y desplegar una aplicación Spring Boot con Maven utilizando los comandos correspondientes a Maven y Java.\nPrerrequisitos 1. Tener instalado y configurado Java. Para comprobarlo, lanzamos el siguiente comando en una terminal:\n1 java -version Que debe devolver una salida similar a la siguiente:\n1 2 3 openjdk version \u0026#34;25.0.1\u0026#34; 2025-10-21 LTS OpenJDK Runtime Environment Temurin-25.0.1+8 (build 25.0.1+8-LTS) OpenJDK 64-Bit Server VM Temurin-25.0.1+8 (build 25.0.1+8-LTS, mixed mode, sharing) 2. Tener instalado y configurado Maven. Para ello, se puede seguir el siguiente artículo: Pasos para instalar Maven en Windows\n3. Tener una aplicación Java que haga uso de Maven. Se puede usar la desarrollada en el siguiente artículo: Spring Boot Hello World\nCompilar el proyecto Navegar hasta la ruta del proyecto donde está el fichero pom.xml 1 cd ruta/a/tu/proyecto Limpiar y empaquetar usando el comando mvn 1 mvn clean package clean → borra compilaciones previas package → compila + ejecuta tests + genera el JAR Si todo va bien, la consola mostrará un mensaje de \u0026lsquo;BUILD SUCCESS\u0026rsquo;.\nLocalización del artefacto Si la compilación ha finalizado correctamente, se habrá generado el fichero jar dentro de la carpeta target dentro de la estructura de carpetas del proyecto. Pero en el caso de una aplicación Spring Boot, nos encontraremos con dos ficheros, un .jar y adicionalmente un .jar.original.\nEl .jar es el que debemos usar para desplegar, ya que contiene:\nEl código de la aplicación compilado TODAS las dependencias El cargador de Spring Boot Un Main-Class ejecutable El .jar.original por contra, no se usa para desplegar, ya que:\nEs el JAR “normal” de Maven Solo contiene el código compilado No incluye dependencias No es ejecutable Lo que ha sucedido es Maven ha creado inicialmente el jar estándar SpringBootHelloWorld-0.0.1-SNAPSHOT.jar. Posteriormente, el plugin de Spring Boot lo renombra a SpringBootHelloWorld-0.0.1-SNAPSHOT.jar.original dejándolo como respaldo interno del proceso y realiza un reempaquetado generando el .jar que contiene todo lo necesario para que sea ejecutable.\nDespliegue de la aplicación Una vez localizado el fichero .jar se ejecuta mediante el comando java de la siguiente manera:\n1 java -jar target/SpringBootHelloWorld-0.0.1-SNAPSHOT.jar Mostrando la consola algo similar a lo que se ve en la siguiente imagen.\nY en este caso, se podrá probar que la aplicación funciona accediendo a un navegador.\nConclusiones Este enfoque permite comprender mejor el ciclo de construcción de una aplicación Java, identificar el artefacto correcto para su despliegue y ganar autonomía a la hora de ejecutar la aplicación en otros entornos.\nEntender qué genera Maven y cómo Spring Boot reempaqueta el artefacto final es un paso fundamental antes de abordar escenarios más avanzados como el despliegue en contenedores Docker o en servidores de producción.\n","permalink":"/posts/compilar-app-maven/","summary":"\u003cp\u003eAunque hoy día existen muy buenos IDEs con numerosos plugins que permiten centrarse en el código y en el desarrollo de la propia aplicación, es útil también \u003cstrong\u003econocer qué hacen internamente y poder lanzar tareas de forma más cercana a cómo se haría en un entorno de producción\u003c/strong\u003e.\u003c/p\u003e\n\u003ch1 id=\"objetivo\"\u003eObjetivo\u003c/h1\u003e\n\u003cp\u003eSe trata de \u003cstrong\u003ecompilar y desplegar una aplicación Spring Boot con Maven\u003c/strong\u003e utilizando los comandos correspondientes a Maven y Java.\u003c/p\u003e\n\u003ch1 id=\"prerrequisitos\"\u003ePrerrequisitos\u003c/h1\u003e\n\u003cp\u003e\u003cstrong\u003e1. Tener instalado y configurado Java\u003c/strong\u003e. Para comprobarlo, lanzamos el siguiente comando en una terminal:\u003c/p\u003e","title":"Compilar una aplicación Java usando Maven"},{"content":"Spring Boot es en la actualidad un estándar en el desarrollo de aplicaciones Java, con mucho protagonismo en el mundo empresarial. Con una configuración inicial simplificada e inteligente permite levantar servicios en minutos. Incluye un servidor embebido y un ecosistema muy completo de starters que simplifican las dependencias necesarias para el desarrollo de muchos tipos de aplicaciones.\nObjetivo Para ejemplificar esta simplicidad, se desarrolla un servicio web con un solo endpoint que devuelva un mensaje de Hello World.\nCreación del proyecto base El ecosistema Spring cuenta con una herramienta que facilita la creación de la estructura base del proyecto, se trata de Spring initializr.\nÉsta genera la estructura básica de un proyecto Spring Boot en segundos. Permite seleccionar la versión de Spring, el lenguaje, el build system (Maven o Gradle) y las dependencias necesarias, creando automáticamente un proyecto listo para abrir en cualquier IDE.\nPara el Hello World solo es necesario incluir la dependencia de Spring Web.\nAl pulsar en GENERATE se descarga un fichero comprimido con el proyecto que cuenta con la estructura y componentes mínimos para importar en cualquier IDE y desplegar la aplicación.\nEste proyecto se puede importar o abrir desde cualquier IDE que cuente con soporte para desarrollo de aplicaciones Spring Boot. Para el ejemplo, se utiliza IntelliJ.\nCreación del controlador Un controlador en Spring es el componente encargado de recibir las peticiones HTTP, procesarlas y devolver una respuesta. Actúa como punto de entrada de la aplicación, mapeando URLs y métodos HTTP (GET, POST, etc.) a métodos Java. Separa la lógica de exposición de la API del resto de la aplicación y su lógica, que se conformará con otro tipo de componentes que se utilizarán para obtener respuestas más complejas.\nAnotaciones RestController y GetMapping Las anotaciones en Spring son etiquetas que se colocan sobre clases y métodos para decirle al framework qué papel cumple cada uno y cómo deben actuar, sin necesidad de escribir código adicional de configuración.\nRestController Es la anotación que marca una clase como un componente que expone endpoints REST. Todo lo que devuelvan sus métodos será serializado automáticamente (Strings, JSONs, etc.).\nGetMapping Asocia un método a una petición HTTP GET y a una ruta concreta que se debe especificar como un parámetro de la propia anotación.\nEjemplo completo Para disponer de un endpoint bajo la ruta /hello que responda a una petición GET, bastaría con completar el Controlador HelloController de la siguiente forma:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 package dev.juanfbermejo.SpringBootHelloWorld; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { @GetMapping(\u0026#34;/hello\u0026#34;) public String hello() { return \u0026#34;Hello World from Spring Boot!\u0026#34;; } } Prueba de la aplicación Para probar la aplicación y ver el HelloWorld bastaría con ejecutar en nuestro IDE la clase anotada con SpringBootApplication.\nEn la consola veremos aparecer varios logs que nos indicarán:\nInicialización del servidor web (Tomcat por defecto) y el puerto en el que estará escuchando para recibir las peticiones (8080 por defecto) Inicialización correcta de la propia aplicación. En ese momento, podremos abrir un navegador web e intentar cargar la url http://localhost:8080/hello lo cual realiza por defecto una petición GET a la ruta, devolviendo en la pantalla el texto de HelloWorld.\nConclusiones Montar un Hello World en Spring Boot es rápido y sencillo, y un buen punto de partida para entender las piezas que lo hacen posible. Las anotaciones son uno de los pilares del framework, y dominar su significado permite construir aplicaciones más limpias, expresivas y mantenibles.\n","permalink":"/posts/spring-boot-hello-world/","summary":"\u003cp\u003e\u003ca href=\"https://spring.io/projects/spring-boot\"\u003eSpring Boot\u003c/a\u003e es en la actualidad un \u003cstrong\u003eestándar en el desarrollo de aplicaciones Java\u003c/strong\u003e, con mucho protagonismo en el mundo empresarial. Con una \u003cstrong\u003econfiguración inicial simplificada e inteligente\u003c/strong\u003e permite levantar servicios en minutos. Incluye un servidor embebido y un ecosistema muy completo de \u003cem\u003estarters\u003c/em\u003e que simplifican las dependencias necesarias para el desarrollo de muchos tipos de aplicaciones.\u003c/p\u003e\n\u003ch1 id=\"objetivo\"\u003eObjetivo\u003c/h1\u003e\n\u003cp\u003ePara ejemplificar esta simplicidad, se desarrolla un servicio web con un solo endpoint que devuelva un mensaje de \u003cem\u003eHello World\u003c/em\u003e.\u003c/p\u003e","title":"Spring Boot Hello World"},{"content":"¿Qué es Maven? Apache Maven es una herramienta de gestión y automatización de proyectos Java. Su objetivo principal es simplificar ciertas tareas de desarrollo como la compilación, la gestión de dependencias, ejecución de test y generación de artefactos (como pueden ser los archivos JAR).\nLa configuración del proyecto se centraliza en el fichero pom.xml (Project Object Model) a partir de la cual, Maven se encarga de descargar librerías y ejecutar las tareas correspondientes.\nMaven se ha convertido en una herramienta esencial en el desarrollo de aplicaciones Java.\nInstalación de Maven en Windows Aunque Windows quizás no es el mejor sistema para utilizarlo en desarrollo de software, y la instalación de algunas herramientas implica cierta dificultad, en el caso de Maven basta con seguir un poco pasos para tenerlo funcionando.\nComprobación previa a la instalación En primer lugar, se puede verificar si el equipo ya cuenta con una instalación de Maven correctamente configurada. Para ello, basta con abrir una consola de terminal y ejecutar el siguiente comando:\n1 mvn -version Si no está instalado o configurado, obtendremos un mensaje que nos avisa de que el sistema no reconoce el comando:\nDescarga de Maven Desde la web https://maven.apache.org/download.cgi descargamos un \u0026lsquo;Binary zip archive\u0026rsquo;, que es un fichero zip que contiene lo necesario para ejecutar Maven.\nLo descomprimimos en una ruta de nuestro equipo donde queramos dejarlo instalado. Por ejemplo en C:\\user\\jfber\\Maven\\apache-maven-3.9.12\nConfiguración de sistema Ahora hay que configurar el sistema para que sepa donde localizar el ejecutable de Maven y que se pueda invocar desde cualquier ruta del sistema.\nEn primer lugar, hay que configurar una variable de entorno llamada MAVEN_HOME cuyo valor será la ruta completa de la carpeta de Maven creada en el paso anterior.\nEn el sistema Windows, en Panel de control → Sistema → Editar las variables de entorno del Sistema se crea la nueva variable:\nUna vez configurada la variable que almacena la ruta donde se encuentra instalado Maven, se incluye la ruta del ejecutable en la variable Path de forma que se pueda ejecutar Maven desde cualquier ubicación del sistema.\nEn la variable Path se incluye el siguiente valor, que hace uso de la variable declarada anteriormente:\n1 %MAVEN_HOME%\\bin Comprobación de la instalación Una vez completada la configuración, se debe reiniciar la terminal y se vuelve a lanzar el comando:\n1 mvn -version Si todo está correcto, ahora muestra la información sobre la versión de Maven instalada y el sistema está listo para comenzar a usar la herramienta.\n","permalink":"/posts/instalar-maven-windows/","summary":"\u003ch1 id=\"qué-es-maven\"\u003e¿Qué es Maven?\u003c/h1\u003e\n\u003cp\u003eApache Maven es una \u003cstrong\u003eherramienta de gestión y automatización de proyectos Java.\u003c/strong\u003e Su objetivo principal es \u003cstrong\u003esimplificar ciertas tareas de desarrollo\u003c/strong\u003e como la compilación, la gestión de dependencias, ejecución de test y generación de artefactos (como pueden ser los archivos JAR).\u003c/p\u003e\n\u003cp\u003eLa configuración del proyecto se centraliza en el fichero \u003ccode\u003epom.xml\u003c/code\u003e (\u003cem\u003eProject Object Model\u003c/em\u003e) a partir de la cual, Maven se encarga de descargar librerías y ejecutar las tareas correspondientes.\u003c/p\u003e","title":"Pasos para instalar Maven en Windows"},{"content":"Cuando hablamos de Clean Code, una de las reglas básicas es utilizar nombres descriptivos que declaren la intención. Esto es trivial en métodos y variables, pero los constructores tienen un nombre fijo y único, y cuando empiezan a aparecer sobrecargas, esa imposibilidad de nombrarlos de forma expresiva penaliza la claridad y legibilidad del código. Es ahí donde Robert C. Martin sugiere en su libro Clean Code el uso de métodos de factoría y constructores privados.\nEl problema con la sobrecarga de constructores A priori, la sobrecarga de constructores parece una solución práctica que nos permite declarar tantos como necesitemos y haciendo cualquier combinación de atributos, según las necesidades del código.\n1 2 3 new User(\u0026#34;Juan\u0026#34;); new User(\u0026#34;Juan\u0026#34;, 35); new User(\u0026#34;Juan\u0026#34;, 35, true); Pero esta sobrecarga escala muy mal y penaliza la legibilidad y comprensión. No sabemos qué representa cada parámetro, son ambiguos y amplían la posibilidad de error aunque el programador invierta tiempo en repasar su definición antes de cada uso.\n1 2 3 4 5 6 public class User { public User(String name) { ... } public User(String name, int age) { ... } public User(String name, int age, boolean isAdmin) { ... } public User(String name, int age, boolean isAdmin, LocalDate createdAt) { ... } } Uso de métodos factoría Los métodos de factoría (factory methods) son métodos estáticos con nombre, ayudan a resolver esta ambigüedad y expresar y definir de manera más clara qué se está creando. Para evitar totalmente la ambigüedad, los constructores pueden ser privados.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class User { private final String name; private final int age; private User(String name, int age) { this.name = name; this.age = age; } public static User withName(String name) { return new User(name, 0); } public static User withNameAndAge(String name, int age) { return new User(name, age); } } 1 2 User u1 = User.withName(\u0026#34;Juan\u0026#34;); User u2 = User.withNameAndAge(\u0026#34;Juan\u0026#34;, 35); Ventajas clave Claridad: los nombres expresan la intención. Flexibilidad: se pueden devolver subtipos, instancias cacheadas, objetos inmutables, etc. Ocultan la complejidad: se centraliza la lógica de creación en un único punto. Evolución más sencilla: se puede ampliar el comportamiento sin romper constructores existentes. Cuando es mejor usar un Builder Si la clase cuenta con muchos parámetros, y/o muchos de ellos son opcionales y, además, queremos incluir una lógica de validación compleja y configuraciones combinatorias (valores de un atributo condicionados al valor de otro), los Builder pasan a ser la solución más potente e indicada.\nConclusiones Para casos simples, un constructor normal está bien. Pero cuando tenemos varias formas de crear el mismo objeto o una lógica de inicialización con cierta complejidad, el uso de métodos de factoría o Builders se posicionan como una solución más clara. El objetivo final es que el código sea fácil de leer, más difícil de romper y más sencillo de extender.\n","permalink":"/posts/constructores-metodos-factoria/","summary":"\u003cp\u003eCuando hablamos de \u003ccode\u003eClean Code\u003c/code\u003e, una de las reglas básicas es \u003cstrong\u003eutilizar nombres descriptivos que declaren la intención\u003c/strong\u003e. Esto es trivial en métodos y variables, pero los constructores tienen un nombre fijo y único, y cuando empiezan a aparecer sobrecargas, esa imposibilidad de nombrarlos de forma expresiva \u003cstrong\u003epenaliza la claridad y legibilidad del código\u003c/strong\u003e. Es ahí donde \u003ca href=\"https://es.wikipedia.org/wiki/Robert_C._Martin\" target=\"_blank\" rel=\"noopener noreferrer\"\u003eRobert C. Martin\u003c/a\u003e sugiere en su libro \u003ca href=\"https://archive.org/details/cleancodehandboo0000unse\" target=\"_blank\" rel=\"noopener noreferrer\"\u003eClean Code\u003c/a\u003e el uso de \u003cstrong\u003emétodos de factoría y constructores privados\u003c/strong\u003e.\u003c/p\u003e","title":"Sobrecarga de constructores con métodos de factoría"},{"content":"Aunque realmente las nuevas switch expressions no son una novedad reciente, ya que están disponibles desde la versión 14 de Java, es otra de esas mejoras que pasan desapercibidas aunque, con un pequeño cambio de sintaxis, proporcionan una versión moderna, limpia, segura y expresiva de los tradicionales switch.\n¿Qué aporta el switch moderno? 1. Es una expresión, no solo una sentencia El nuevo switch puede devolver valores directamente a una variable haciendo uso del operador flecha.\n1 2 3 4 5 6 7 8 9 10 String day = switch (dayOfWeek) { case 1 -\u0026gt; \u0026#34;Monday\u0026#34;; case 2 -\u0026gt; \u0026#34;Tuesday\u0026#34;; case 3 -\u0026gt; \u0026#34;Wednesday\u0026#34;; case 4 -\u0026gt; \u0026#34;Thursday\u0026#34;; case 5 -\u0026gt; \u0026#34;Friday\u0026#34;; case 6 -\u0026gt; \u0026#34;Saturday\u0026#34;; case 7 -\u0026gt; \u0026#34;Sunday\u0026#34;; default -\u0026gt; \u0026#34;Invalid day\u0026#34;; }; 2. Evita el \u0026lsquo;fall-through\u0026rsquo; En los tradicionales switch, si te olvidabas un break provocaba la ejecución de varios case. Ahora ya no son necesarios, eliminado verbosidad y aportando robustez y permitiendo igualmente agrupar casos.\n1 2 3 4 5 String dayType = switch (dayOfWeek) { case 1, 2, 3, 4, 5 -\u0026gt; \u0026#34;Weekday\u0026#34;; case 6, 7 -\u0026gt; \u0026#34;Weekend\u0026#34;; default -\u0026gt; \u0026#34;Invalid\u0026#34;; }; 3. Menos código y más claro En el switch clásico, había que declarar la variable, abrir el switch, asignar valor en cada case, acordarte de los break y cerrar la expresión correctamente.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 String day; switch (dayOfWeek) { case 1: day = \u0026#34;Monday\u0026#34;; break; case 2: day = \u0026#34;Tuesday\u0026#34;; break; case 3: day = \u0026#34;Wednesday\u0026#34;; break; case 4: day = \u0026#34;Thursday\u0026#34;; break; case 5: day = \u0026#34;Friday\u0026#34;; break; case 6: day = \u0026#34;Saturday\u0026#34;; break; case 7: day = \u0026#34;Sunday\u0026#34;; break; default: day = \u0026#34;Invalid day\u0026#34;; } 4. Uso de \u0026lsquo;yield\u0026rsquo; para casos más complejos Se trata de un nuevo operador, un return que sólo existe dentro de las nuevas switch expressions, y que no finaliza la ejecución del método. Permite desarrollar un case más complejo y devolver un valor en el mismo.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 double price = switch (customerType) { case STANDARD -\u0026gt; basePrice; case PREMIUM -\u0026gt; { double discount = basePrice * 0.10; if (discount \u0026gt; 30) { System.out.println(\u0026#34;Aplicando tope de descuento\u0026#34;); discount = 30; } yield basePrice - discount; } case STUDENT -\u0026gt; basePrice * 0.85; default -\u0026gt; basePrice; }; 5. Switch más exhaustivos usando Enums Otra mejora interesante, disponible desde Java 17, es el uso de Enums para definir los case. De esta forma el desarrollador está obligado a usar todos sus valores, obteniendo un aviso del compilador en caso contrario.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public enum CustomerType { REGULAR, SILVER, GOLD, PLATINUM, DIAMOND; } public double calculateDiscount(CustomerType type, double amount) { return switch (type) { case REGULAR -\u0026gt; 0.0; case SILVER -\u0026gt; amount * 0.05; case GOLD -\u0026gt; amount * 0.10; case PLATINUM -\u0026gt; { System.out.println(\u0026#34;Aplicando descuento especial\u0026#34;); double bonus = amount \u0026gt; 1000 ? 0.02 : 0.0; yield amount * (0.15 + bonus); } }; } Conclusiones Las nuevas switch expressions no cambian la forma de pensar en condiciones, pero sí nos permite expresarlas con menos ruido y más seguridad. Es una pieza pequeña, pero encaja muy bien en el Java moderno: más conciso, más claro y más difícil de romper por descuido.\n","permalink":"/posts/nuevo-switch-expression-java/","summary":"\u003cp\u003eAunque realmente las nuevas \u003ccode\u003eswitch expressions\u003c/code\u003e no son una novedad reciente, ya que están disponibles desde la versión 14 de Java, es otra de esas mejoras que pasan desapercibidas aunque, con un pequeño cambio de sintaxis, proporcionan una versión moderna, limpia, segura y expresiva de los tradicionales \u003ccode\u003eswitch\u003c/code\u003e.\u003c/p\u003e\n\u003ch1 id=\"qué-aporta-el-switch-moderno\"\u003e¿Qué aporta el switch moderno?\u003c/h1\u003e\n\u003ch2 id=\"1-es-una-expresión-no-solo-una-sentencia\"\u003e1. Es una expresión, no solo una sentencia\u003c/h2\u003e\n\u003cp\u003eEl nuevo \u003ccode\u003eswitch\u003c/code\u003e puede devolver valores directamente a una variable haciendo uso del operador \u003ccode\u003eflecha\u003c/code\u003e.\u003c/p\u003e","title":"Nuevo switch expression de Java. Menos ruido, más claridad"},{"content":"Una de las primeras lecciones que aprendemos cuando empezamos con Java es que \u0026ldquo;en Java, todo son objetos\u0026rdquo;. Por eso, a la hora de comparar dos Strings, no debemos hacerlo con el operador ==, ya que estaríamos comparando sus referencias de memoria y no su contenido.\nPero otro problema relacionado con la comparación de Strings puede surgir al usar la función equals y que, aunque sea menos evidente, puede provocar errores difíciles de detectar.\nLa mayoría de desarrolladores tenderíamos a montar la comparación en el siguiente orden (quizás porque nos centramos en nuestra variable), lo cual, si en algún momento diaSemana es nulo, genera una NullPointerException:\n1 2 3 if( diaSemana.equals(\u0026#34;SUNDAY\u0026#34;) ){ hacemosAlgo(); } Para evitarlo, basta con invertir el orden, ya que SUNDAY nunca será nulo:\n1 2 3 if( \u0026#34;SUNDAY\u0026#34;.equals(diaSemana) ){ hacemosAlgo(); } ¿Por qué este detalle importa en proyectos grandes? Imaginemos que en nuestro desarrollo original, el valor de la variable diaSemana lo obtenemos mediante algún mecanismo que nos garantice que en ningún caso sea nulo. Por ejemplo:\n1 2 3 public String obtenerDiaSemana(){ return LocalDate.now().getDayOfWeek().name(); } Por lo que la primera implementación de la comparativa de la variable con la cadena SUNDAY funcionaría correctamente y nuestro desarrollo iría a producción pasando los test.\nPero también podría pasar, que en una aplicación grande y donde tocan varios equipos, se realiza un desarrollo nuevo que podría consistir en que un usuario puede configurar en su perfil con una zona horaria específica.\nDe esta forma, ese segundo equipo cambia la implementación de la función obtenerDiaSemana para hacerlo de la siguiente manera (asumiendo que el contexto es la sesión en la aplicación):\n1 2 3 4 5 public String obtenerDiaSemana() { Usuario usuario = contexto.getUsuarioActual(); ZonaHoraria zona = usuario.getPreferencias().getZonaHoraria(); return LocalDate.now(zona).getDayOfWeek().name(); } De esta forma, una implementación determinista y segura, \u0026ldquo;imposible de romper\u0026rdquo;, pasa a depender de otros factores que podrían desencadenar en que la variable diaSemana en el punto en que se compara con un valor, pueda ser nula.\nAunque el segundo equipo podría haber detectado el fallo en pruebas, el primero habría evitado el problema desde el principio escribiendo código más robusto. Un pequeño detalle en apariencia, pero de los que separan código que funciona de código que sobrevive.\n","permalink":"/posts/comparar-strings-java-excep/","summary":"\u003cp\u003eUna de las primeras lecciones que aprendemos cuando empezamos con Java es que \u0026ldquo;en Java, todo son objetos\u0026rdquo;. Por eso, a la hora de comparar dos Strings, no debemos hacerlo con el operador \u003ccode\u003e==\u003c/code\u003e, ya que estaríamos comparando sus referencias de memoria y no su contenido.\u003c/p\u003e\n\u003cp\u003ePero otro problema relacionado con la comparación de Strings puede surgir al usar la función \u003ccode\u003eequals\u003c/code\u003e y que, aunque sea menos evidente, puede provocar errores difíciles de detectar.\u003c/p\u003e","title":"Comparar Strings en Java evitando excepciones"}]