Los 10 errores más comunes de Spring Framework

Publicado: 2022-03-11

Spring es posiblemente uno de los marcos de Java más populares, y también una bestia poderosa para domar. Si bien sus conceptos básicos son bastante fáciles de comprender, convertirse en un desarrollador de Spring fuerte requiere algo de tiempo y esfuerzo.

En este artículo cubriremos algunos de los errores más comunes en Spring, específicamente orientados hacia las aplicaciones web y Spring Boot. Como dice el sitio web de Spring Boot, Spring Boot tiene una visión obstinada sobre cómo deben construirse las aplicaciones listas para producción, por lo que este artículo intentará imitar esa visión y brindar una descripción general de algunos consejos que se incorporarán bien en el desarrollo estándar de aplicaciones web de Spring Boot.

En caso de que no esté muy familiarizado con Spring Boot pero le gustaría probar algunas de las cosas mencionadas, he creado un repositorio de GitHub que acompaña a este artículo. Si se siente perdido en algún momento durante el artículo, le recomiendo que clone el repositorio y juegue con el código en su máquina local.

Error común n.º 1: ir a un nivel demasiado bajo

Nos llevamos bien con este error común porque el síndrome de "no inventado aquí" es bastante común en el mundo del desarrollo de software. Los síntomas incluyen la reescritura regular de piezas de código de uso común y muchos desarrolladores parecen sufrirlo.

Si bien comprender los aspectos internos de una biblioteca en particular y su implementación es en su mayor parte bueno y necesario (y también puede ser un gran proceso de aprendizaje), es perjudicial para su desarrollo como ingeniero de software abordar constantemente la misma implementación de bajo nivel. detalles. Hay una razón por la que existen abstracciones y marcos como Spring, que es precisamente para separarlo del trabajo manual repetitivo y permitirle concentrarse en detalles de nivel superior: los objetos de su dominio y la lógica comercial.

Así que adopte las abstracciones: la próxima vez que se enfrente a un problema en particular, primero haga una búsqueda rápida y determine si una biblioteca que resuelve ese problema ya está integrada en Spring; hoy en día, es probable que encuentre una solución existente adecuada. Como ejemplo de una biblioteca útil, usaré las anotaciones de Project Lombok en ejemplos para el resto de este artículo. Lombok se usa como un generador de código repetitivo y, con suerte, el desarrollador perezoso dentro de usted no debería tener problemas para familiarizarse con la biblioteca. Como ejemplo, vea cómo se ve un "bean de Java estándar" con Lombok:

 @Getter @Setter @NoArgsConstructor public class Bean implements Serializable { int firstBeanProperty; String secondBeanProperty; }

Como puede imaginar, el código anterior se compila para:

 public class Bean implements Serializable { private int firstBeanProperty; private String secondBeanProperty; public int getFirstBeanProperty() { return this.firstBeanProperty; } public String getSecondBeanProperty() { return this.secondBeanProperty; } public void setFirstBeanProperty(int firstBeanProperty) { this.firstBeanProperty = firstBeanProperty; } public void setSecondBeanProperty(String secondBeanProperty) { this.secondBeanProperty = secondBeanProperty; } public Bean() { } }

Tenga en cuenta, sin embargo, que lo más probable es que tenga que instalar un complemento en caso de que tenga la intención de usar Lombok con su IDE. La versión del complemento de IntelliJ IDEA se puede encontrar aquí.

Error común n.º 2: partes internas con "fugas"

Exponer su estructura interna nunca es una buena idea porque crea rigidez en el diseño del servicio y, en consecuencia, promueve malas prácticas de codificación. Las 'fugas' internas se manifiestan al hacer que la estructura de la base de datos sea accesible desde ciertos puntos finales de la API. Como ejemplo, digamos que el siguiente POJO ("Plain Old Java Object") representa una tabla en su base de datos:

 @Entity @NoArgsConstructor @Getter public class TopTalentEntity { @Id @GeneratedValue private Integer id; @Column private String name; public TopTalentEntity(String name) { this.name = name; } }

Digamos que existe un punto final que necesita acceder a los datos de TopTalentEntity . Por muy tentador que sea devolver instancias de TopTalentEntity , una solución más flexible sería crear una nueva clase para representar los datos de TopTalentEntity en el extremo de la API:

 @AllArgsConstructor @NoArgsConstructor @Getter public class TopTalentData { private String name; }

De esa manera, realizar cambios en el back-end de su base de datos no requerirá ningún cambio adicional en la capa de servicio. Considere lo que sucedería en el caso de agregar un campo de 'contraseña' a TopTalentEntity para almacenar los hashes de contraseña de sus usuarios en la base de datos; sin un conector como TopTalentData , olvidar cambiar el front-end del servicio expondría accidentalmente información secreta muy indeseable. !

Error común #3: Falta de separación de preocupaciones

A medida que crece su aplicación, la organización del código comienza a convertirse cada vez más en un asunto cada vez más importante. Irónicamente, la mayoría de los buenos principios de ingeniería de software comienzan a desmoronarse a escala, especialmente en los casos en los que no se ha pensado mucho en el diseño de la arquitectura de la aplicación. Uno de los errores más comunes a los que los desarrolladores tienden a sucumbir es mezclar problemas de código, ¡y es extremadamente fácil de hacer!

Lo que generalmente rompe la separación de preocupaciones es simplemente "volcar" nueva funcionalidad en las clases existentes. Esta es, por supuesto, una excelente solución a corto plazo (para empezar, requiere menos tipeo), pero inevitablemente se convierte en un problema más adelante, ya sea durante las pruebas, el mantenimiento o en algún punto intermedio. Considere el siguiente controlador, que devuelve TopTalentData desde su repositorio:

 @RestController public class TopTalentController { private final TopTalentRepository topTalentRepository; @RequestMapping("/toptal/get") public List<TopTalentData> getTopTalent() { return topTalentRepository.findAll() .stream() .map(this::entityToData) .collect(Collectors.toList()); } private TopTalentData entityToData(TopTalentEntity topTalentEntity) { return new TopTalentData(topTalentEntity.getName()); } }

Al principio, puede parecer que no hay nada particularmente malo con este fragmento de código; proporciona una lista de TopTalentData que se recupera de las instancias de TopTalentEntity . Sin embargo, al mirar más de cerca, podemos ver que en realidad hay algunas cosas que TopTalentController está realizando aquí; es decir, está asignando solicitudes a un punto final en particular, recuperando datos de un repositorio y convirtiendo entidades recibidas de TopTalentRepository en un formato diferente. Una solución 'más limpia' sería separar esas preocupaciones en sus propias clases. Podría verse algo como esto:

 @RestController @RequestMapping("/toptal") @AllArgsConstructor public class TopTalentController { private final TopTalentService topTalentService; @RequestMapping("/get") public List<TopTalentData> getTopTalent() { return topTalentService.getTopTalent(); } } @AllArgsConstructor @Service public class TopTalentService { private final TopTalentRepository topTalentRepository; private final TopTalentEntityConverter topTalentEntityConverter; public List<TopTalentData> getTopTalent() { return topTalentRepository.findAll() .stream() .map(topTalentEntityConverter::toResponse) .collect(Collectors.toList()); } } @Component public class TopTalentEntityConverter { public TopTalentData toResponse(TopTalentEntity topTalentEntity) { return new TopTalentData(topTalentEntity.getName()); } }

Una ventaja adicional de esta jerarquía es que nos permite determinar dónde reside la funcionalidad simplemente inspeccionando el nombre de la clase. Además, durante las pruebas, podemos sustituir fácilmente cualquiera de las clases con una implementación simulada si surge la necesidad.

Error común n.° 4: inconsistencia y manejo deficiente de errores

El tema de la consistencia no es necesariamente exclusivo de Spring (o Java, para el caso), pero sigue siendo una faceta importante a considerar cuando se trabaja en proyectos de Spring. Si bien el estilo de codificación puede ser objeto de debate (y generalmente es una cuestión de acuerdo dentro de un equipo o dentro de toda una empresa), tener un estándar común resulta ser una gran ayuda para la productividad. Esto es especialmente cierto con equipos de varias personas; la coherencia permite que se produzca el traspaso sin que se gasten muchos recursos en tomar de la mano o proporcionar largas explicaciones sobre las responsabilidades de las diferentes clases

Considere un proyecto Spring con sus diversos archivos de configuración, servicios y controladores. Ser semánticamente coherente al nombrarlos crea una estructura de fácil búsqueda en la que cualquier desarrollador nuevo puede manejar el código; agregar sufijos de configuración a sus clases de configuración, sufijos de servicio a sus servicios y sufijos de controlador a sus controladores, por ejemplo.

Estrechamente relacionado con el tema de la consistencia, el manejo de errores en el lado del servidor merece un énfasis específico. Si alguna vez tuvo que manejar respuestas de excepción de una API mal escrita, probablemente sepa por qué: puede ser una molestia analizar adecuadamente las excepciones, y aún más doloroso determinar la razón por la que ocurrieron esas excepciones en primer lugar.

Como desarrollador de API, lo ideal sería que cubriera todos los puntos de enlace de cara al usuario y los tradujera a un formato de error común. Esto generalmente significa tener un código de error genérico y una descripción en lugar de la solución de a) devolver un mensaje de "Error interno del servidor 500", o b) simplemente descargar el seguimiento de la pila al usuario (que en realidad debe evitarse a toda costa). ya que expone sus partes internas además de ser difícil de manejar del lado del cliente).

Un ejemplo de un formato de respuesta de error común podría ser:

 @Value public class ErrorResponse { private Integer errorCode; private String errorMessage; }

Algo similar a esto se encuentra comúnmente en las API más populares y tiende a funcionar bien, ya que se puede documentar fácil y sistemáticamente. La traducción de las excepciones a este formato se puede realizar proporcionando la anotación @ExceptionHandler a un método (un ejemplo de una anotación se encuentra en Error común n.º 6).

Error común n.º 5: tratar incorrectamente los subprocesos múltiples

Independientemente de si se encuentra en aplicaciones web o de escritorio, con Spring o sin Spring, los subprocesos múltiples pueden ser un hueso duro de roer. Los problemas causados ​​por la ejecución en paralelo de programas son estresantemente elusivos y, a menudo, extremadamente difíciles de depurar; de hecho, debido a la naturaleza del problema, una vez que se da cuenta de que está lidiando con un problema de ejecución en paralelo, es probable que tiene que renunciar al depurador por completo e inspeccionar su código "a mano" hasta que encuentre la causa raíz del error. Desafortunadamente, no existe una solución estándar para resolver tales problemas; dependiendo de su caso específico, tendrá que evaluar la situación y luego atacar el problema desde el ángulo que considere mejor.

Idealmente, por supuesto, desearía evitar los errores de subprocesos múltiples por completo. Nuevamente, no existe un enfoque único para hacerlo, pero aquí hay algunas consideraciones prácticas para depurar y prevenir errores de subprocesos múltiples:

Evitar estado global

Primero, recuerde siempre el tema del “estado global”. Si está creando una aplicación de subprocesos múltiples, absolutamente cualquier cosa que sea modificable globalmente debe monitorearse de cerca y, si es posible, eliminarse por completo. Si hay una razón por la cual la variable global debe permanecer modificable, emplee con cuidado la sincronización y realice un seguimiento del rendimiento de su aplicación para confirmar que no sea lenta debido a los períodos de espera recientemente introducidos.

Evite la mutabilidad

Este proviene directamente de la programación funcional y, adaptado a OOP, establece que se debe evitar la mutabilidad de clase y el cambio de estado. En resumen, esto significa renunciar a los métodos de establecimiento y tener campos finales privados en todas las clases de su modelo. La única vez que se mutan sus valores es durante la construcción. De esta manera, puede estar seguro de que no surgirán problemas de contención y que el acceso a las propiedades del objeto proporcionará los valores correctos en todo momento.

Registrar datos cruciales

Evalúe dónde su aplicación podría causar problemas y registre de forma preventiva todos los datos cruciales. Si ocurre un error, le agradecerá tener información que indique qué solicitudes se recibieron y tener una mejor idea de por qué su aplicación se comportó mal. Nuevamente, es necesario tener en cuenta que el registro introduce E/S de archivos adicionales y, por lo tanto, no debe abusarse, ya que puede afectar gravemente el rendimiento de su aplicación.

Reutilizar implementaciones existentes

Siempre que necesite generar sus propios subprocesos (por ejemplo, para realizar solicitudes asíncronas a diferentes servicios), reutilice las implementaciones seguras existentes en lugar de crear sus propias soluciones. Esto significará, en su mayor parte, utilizar ExecutorServices y CompletableFutures de estilo funcional de Java 8 para la creación de subprocesos. Spring también permite el procesamiento de solicitudes asincrónicas a través de la clase DeferredResult.

Error común n.º 6: no emplear la validación basada en anotaciones

Imaginemos que nuestro servicio TopTalent de antes requiere un punto final para agregar nuevos Top Talents. Además, digamos que, por alguna razón realmente válida, cada nuevo nombre debe tener exactamente 10 caracteres. Una forma de hacerlo podría ser la siguiente:

 @RequestMapping("/put") public void addTopTalent(@RequestBody TopTalentData topTalentData) { boolean nameNonExistentOrHasInvalidLength = Optional.ofNullable(topTalentData) .map(TopTalentData::getName) .map(name -> name.length() == 10) .orElse(true); if (nameNonExistentOrInvalidLength) { // throw some exception } topTalentService.addTopTalent(topTalentData); }

Sin embargo, lo anterior (además de estar mal construido) no es realmente una solución 'limpia'. Estamos verificando más de un tipo de validez (a saber, que TopTalentData no sea nulo, y que TopTalentData.name no sea nulo, y que TopTalentData.name tenga 10 caracteres), así como lanzar una excepción si los datos no son válidos .

Esto se puede ejecutar de manera mucho más limpia empleando el validador de Hibernate con Spring. Primero refactoricemos el método addTopTalent para admitir la validación:

 @RequestMapping("/put") public void addTopTalent(@Valid @NotNull @RequestBody TopTalentData topTalentData) { topTalentService.addTopTalent(topTalentData); } @ExceptionHandler @ResponseStatus(HttpStatus.BAD_REQUEST) public ErrorResponse handleInvalidTopTalentDataException(MethodArgumentNotValidException methodArgumentNotValidException) { // handle validation exception }

Además, vamos a tener que indicar qué propiedad queremos validar en la clase TopTalentData :

 public class TopTalentData { @Length(min = 10, max = 10) @NotNull private String name; }

Ahora Spring interceptará la solicitud y la validará antes de que se invoque el método; no es necesario emplear pruebas manuales adicionales.

Otra forma en que podríamos haber logrado lo mismo es creando nuestras propias anotaciones. Aunque normalmente solo empleará anotaciones personalizadas cuando sus necesidades excedan el conjunto de restricciones integrado de Hibernate, para este ejemplo supongamos que @Length no existe. Haría un validador que verifique la longitud de la cadena creando dos clases adicionales, una para validar y otra para anotar propiedades:

 @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented @Constraint(validatedBy = { MyAnnotationValidator.class }) public @interface MyAnnotation { String message() default "String length does not match expected"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; int value(); } @Component public class MyAnnotationValidator implements ConstraintValidator<MyAnnotation, String> { private int expectedLength; @Override public void initialize(MyAnnotation myAnnotation) { this.expectedLength = myAnnotation.value(); } @Override public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) { return s == null || s.length() == this.expectedLength; } }

Tenga en cuenta que, en estos casos, las mejores prácticas sobre la separación de preocupaciones requieren que marque una propiedad como válida si es nula ( s == null dentro del método isValid ) y luego use una anotación @NotNull si ese es un requisito adicional para el propiedad:

 public class TopTalentData { @MyAnnotation(value = 10) @NotNull private String name; }

Error común n.º 7: (todavía) usar una configuración basada en XML

Si bien XML era una necesidad para las versiones anteriores de Spring, hoy en día la mayor parte de la configuración se puede realizar exclusivamente a través de código/anotaciones de Java; Las configuraciones XML simplemente se hacen pasar por un código repetitivo adicional e innecesario.

Este artículo (así como el repositorio de GitHub que lo acompaña) usa anotaciones para configurar Spring y Spring sabe qué beans debe conectar porque el paquete raíz se anotó con una anotación compuesta @SpringBootApplication , así:

 @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }

La anotación compuesta (puede obtener más información al respecto en la documentación de Spring simplemente le da a Spring una pista sobre qué paquetes deben escanearse para recuperar beans. En nuestro caso concreto, esto significa que se usará lo siguiente debajo del paquete superior (co.kukurin) para cableado:

  • @Component ( TopTalentConverter , MyAnnotationValidator )
  • @RestController ( TopTalentController )
  • @Repository ( TopTalentRepository )
  • @Service ( TopTalentService )

Si tuviéramos clases anotadas @Configuration adicionales, también se verificarían para la configuración basada en Java.

Error común n.º 8: olvidarse de los perfiles

Un problema que se encuentra a menudo en el desarrollo de servidores es distinguir entre diferentes tipos de configuración, generalmente sus configuraciones de producción y desarrollo. En lugar de reemplazar manualmente varias entradas de configuración cada vez que cambia de prueba a implementación de su aplicación, una forma más eficiente sería emplear perfiles.

Considere el caso en el que está utilizando una base de datos en memoria para el desarrollo local, con una base de datos MySQL en producción. En esencia, esto significaría que utilizará una URL diferente y (con suerte) credenciales diferentes para acceder a cada uno de los dos. Veamos cómo se podría hacer esto con dos archivos de configuración diferentes:

archivo application.yaml

 # set default profile to 'dev' spring.profiles.active: dev # production database details spring.datasource.url: 'jdbc:mysql://localhost:3306/toptal' spring.datasource.username: root spring.datasource.password:

archivo application-dev.yaml

 spring.datasource.url: 'jdbc:h2:mem:' spring.datasource.platform: h2

Presumiblemente, no querrá realizar accidentalmente ninguna acción en su base de datos de producción mientras juega con el código, por lo que tiene sentido establecer el perfil predeterminado en dev. En el servidor, puede anular manualmente el perfil de configuración proporcionando un parámetro -Dspring.profiles.active=prod a la JVM. Alternativamente, también puede configurar la variable de entorno de su sistema operativo en el perfil predeterminado deseado.

Error común n.º 9: no adoptar la inyección de dependencia

Usar adecuadamente la inyección de dependencia con Spring significa permitirle conectar todos sus objetos juntos escaneando todas las clases de configuración deseadas; esto demuestra ser útil para desacoplar relaciones y también hace que las pruebas sean mucho más fáciles. En lugar de clases de acoplamiento estrecho haciendo algo como esto:

 public class TopTalentController { private final TopTalentService topTalentService; public TopTalentController() { this.topTalentService = new TopTalentService(); } }

Estamos permitiendo que Spring haga el cableado por nosotros:

 public class TopTalentController { private final TopTalentService topTalentService; public TopTalentController(TopTalentService topTalentService) { this.topTalentService = topTalentService; } }

La charla de Google de Misko Hevery explica en profundidad los "por qué" de la inyección de dependencia, así que veamos cómo se usa en la práctica. En la sección sobre la separación de preocupaciones (Errores comunes #3), creamos una clase de servicio y controlador. Digamos que queremos probar el controlador bajo el supuesto de que TopTalentService se comporta correctamente. Podemos insertar un objeto simulado en lugar de la implementación del servicio real al proporcionar una clase de configuración separada:

 @Configuration public class SampleUnitTestConfig { @Bean public TopTalentService topTalentService() { TopTalentService topTalentService = Mockito.mock(TopTalentService.class); Mockito.when(topTalentService.getTopTalent()).thenReturn( Stream.of("Mary", "Joel").map(TopTalentData::new).collect(Collectors.toList())); return topTalentService; } }

Luego, podemos inyectar el objeto simulado diciéndole a Spring que use SampleUnitTestConfig como su proveedor de configuración:

 @ContextConfiguration(classes = { SampleUnitTestConfig.class })

Esto nos permite usar la configuración de contexto para inyectar el bean personalizado en una prueba unitaria.

Error común n.º 10: falta de pruebas o pruebas inadecuadas

Aunque la idea de las pruebas unitarias ha estado con nosotros durante mucho tiempo, muchos desarrolladores parecen "olvidarse" de hacer esto (especialmente si no es necesario ), o simplemente lo agregan como una ocurrencia tardía. Obviamente, esto no es deseable ya que las pruebas no solo deben verificar la corrección de su código, sino que también sirven como documentación sobre cómo debe comportarse la aplicación en diferentes situaciones.

Al probar servicios web, rara vez se realizan exclusivamente pruebas unitarias 'puras', ya que la comunicación a través de HTTP generalmente requiere que invoque DispatcherServlet de Spring y vea qué sucede cuando se recibe una HttpServletRequest real (lo que la convierte en una prueba de integración , que se ocupa de la validación, serialización , etc.). REST Assured, un DSL de Java para probar fácilmente los servicios REST, además de MockMVC, ha demostrado ser una solución muy elegante. Considere el siguiente fragmento de código con inyección de dependencia:

 @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = { Application.class, SampleUnitTestConfig.class }) public class RestAssuredTestDemonstration { @Autowired private TopTalentController topTalentController; @Test public void shouldGetMaryAndJoel() throws Exception { // given MockMvcRequestSpecification givenRestAssuredSpecification = RestAssuredMockMvc.given() .standaloneSetup(topTalentController); // when MockMvcResponse response = givenRestAssuredSpecification.when().get("/toptal/get"); // then response.then().statusCode(200); response.then().body("name", hasItems("Mary", "Joel")); } }

SampleUnitTestConfig una implementación simulada de TopTalentService en TopTalentController , mientras que todas las demás clases están conectadas utilizando la configuración estándar inferida del escaneo de paquetes enraizados en el paquete de la clase de aplicación. RestAssuredMockMvc simplemente se usa para configurar un entorno ligero y enviar una solicitud GET al punto final /toptal/get .

Convertirse en un maestro de la primavera

Spring es un marco poderoso con el que es fácil comenzar, pero requiere algo de dedicación y tiempo para lograr un dominio completo. Tomarse el tiempo para familiarizarse con el marco definitivamente mejorará su productividad a largo plazo y, en última instancia, lo ayudará a escribir un código más limpio y convertirse en un mejor desarrollador.

Si está buscando más recursos, Spring In Action es un buen libro práctico que cubre muchos temas centrales de Spring.