En este artículo quiero compartir observaciones sobre algunos antipatrones encontrados en el código de aplicaciones que se ejecutan en Spring. Todos ellos aparecieron de una forma u otra en el código viviente: o los encontré en las clases existentes o los descubrí mientras leía el trabajo de colegas.
Espero que sea interesante para usted, y si después de leer reconoce sus "pecados" y se embarca en el camino de la corrección, estaré doblemente complacido. También te insto a que compartas tus propios ejemplos en el comentario, agregaremos los más curiosos e inusuales a la publicación.
Autowired
El genial y terrible @Autowired
es toda una era en primavera. Todavía no puede prescindir de él cuando escribe pruebas, pero en el código principal (PMSM) es claramente superfluo. En varios de mis proyectos recientes, no estaba en absoluto. Durante mucho tiempo escribimos así:
@Component public class MyService { @Autowired private ServiceDependency; @Autowired private AnotherServiceDependency;
Las razones por las que se critica la inyección de dependencia a través de campos y establecedores ya se han descrito en detalle, en particular aquí . Una alternativa es la implementación a través del constructor. Siguiendo el enlace, se describe un ejemplo:
private DependencyA dependencyA; private DependencyB dependencyB; private DependencyC dependencyC; @Autowired public DI(DependencyA dependencyA, DependencyB dependencyB, DependencyC dependencyC) { this.dependencyA = dependencyA; this.dependencyB = dependencyB; this.dependencyC = dependencyC; }
Parece más o menos decente, pero imagina que tenemos 10 dependencias (sí, sí, sé que en este caso deben agruparse en clases separadas, pero ahora no se trata de eso). La imagen ya no es tan atractiva:
private DependencyA dependencyA; private DependencyB dependencyB; private DependencyC dependencyC; private DependencyD dependencyD; private DependencyE dependencyE; private DependencyF dependencyF; private DependencyG dependencyG; private DependencyH dependencyH; private DependencyI dependencyI; private DependencyJ dependencyJ; @Autowired public DI() { this.dependencyA = dependencyA; this.dependencyB = dependencyB; this.dependencyC = dependencyC; this.dependencyD = dependencyD; this.dependencyE = dependencyE; this.dependencyF = dependencyF; this.dependencyG = dependencyG; this.dependencyH = dependencyH; this.dependencyI = dependencyI; this.dependencyJ = dependencyJ; }
Francamente, el código parece monstruoso.
Y aquí, muchos olvidan que aquí también violinista @Autowired
no @Autowired
necesario! Si una clase tiene solo un constructor, Spring (> = 4) comprenderá que las dependencias deben implementarse a través de este constructor. Entonces podemos tirarlo, reemplazándolo con el Lombok @AllArgsContructor
. O incluso mejor: en @RequiredArgsContructor
, sin olvidar declarar todos los campos necesarios como final
y recibir una inicialización segura del objeto en un entorno de subprocesos múltiples (siempre que todas las dependencias también se inicialicen de forma segura):
@RequiredArgsConstructor public class DI { private final DependencyA dependencyA; private final DependencyB dependencyB; private final DependencyC dependencyC; private final DependencyD dependencyD; private final DependencyE dependencyE; private final DependencyF dependencyF; private final DependencyG dependencyG; private final DependencyH dependencyH; private final DependencyI dependencyI; private final DependencyJ dependencyJ; }
Métodos estáticos en clases de utilidad y funciones enum.
Bloody E a menudo tiene la tarea de convertir objetos de soporte de datos de una capa de aplicación a objetos similares de otra capa. Para esto, todavía se usan clases de utilidad con métodos estáticos como este (recuerde, en el año 2019):
@UtilityClass public class Utils { public static UserDto entityToDto(UserEntity user) {
Los usuarios más avanzados que leen libros inteligentes son conscientes de las propiedades mágicas de las enumeraciones:
enum Function implements Function<UserEntity, UserDto> { INST; @Override public UserDto apply(UserEntity user) {
Es cierto que, en este caso, la llamada aún se produce a un solo objeto, y no a un componente controlado por Spring.
Incluso los chicos (y las chicas) más avanzados conocen MapStruct , que te permite describir todo en una sola interfaz:
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.ERROR) public interface CriminalRecommendationMapper { UserDto map(UserEntity user); }
Ahora tenemos el componente de resorte. Parece una victoria Pero el diablo está en los detalles, y sucede que la victoria se vuelve abrumadora. En primer lugar, los nombres de los campos deben ser los mismos (de lo contrario, comienzan las hemorroides), lo que no siempre es conveniente, y en segundo lugar, si hay transformaciones complejas de los objetos procesados, surgen dificultades adicionales. Bueno, es necesario agregar Mapstruct en sí.
Y pocas personas recuerdan la forma anticuada, pero sin embargo simple y funcional de obtener un convertidor accionado por resorte:
import org.springframework.core.convert.converter.Converter; @Component public class UserEntityToDto implements Converter<UserEntity, UserDto> { @Override public UserDto convert(UserEntity user) {
La ventaja aquí es que en otra clase, solo necesito escribir
@Component @RequiredArgsConstructor public class DI { private final Converter<UserEntity, UserDto> userEnityToDto; }
¡y Spring resolverá todo por sí solo!
Calificador de residuos
Caso de vida: la aplicación funciona con dos bases de datos. En consecuencia, hay dos fuentes de datos ( java.sql.DataSource
), dos administradores de transacciones, dos grupos de repositorios, etc. Todo esto se describe convenientemente en dos configuraciones separadas. Esto es para Postgre:
@Configuration public class PsqlDatasourceConfig { @Bean @Primary @ConfigurationProperties(prefix = "spring.datasource.psql") public DataSource psqlDataSource() { return DataSourceBuilder.create().build(); } @Bean public SpringLiquibase primaryLiquibase( ProfileChecker checker, @Qualifier("psqlDataSource") DataSource dataSource ) { boolean isTest = checker.isTest(); SpringLiquibase liquibase = new SpringLiquibase(); liquibase.setDataSource(dataSource); liquibase.setChangeLog("classpath:liquibase/schema-postgre.xml"); liquibase.setShouldRun(isTest); return liquibase; } }
Y esto es para DB2:
@Configuration public class Db2DatasourceConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource.db2") public DataSource db2DataSource() { return DataSourceBuilder.create().build(); } @Bean public SpringLiquibase liquibase( ProfileChecker checker, @Qualifier("db2DataSource") DataSource dataSource ) { boolean isTest = checker.isTest(); SpringLiquibase liquibase = new SpringLiquibase(); liquibase.setDataSource(dataSource); liquibase.setChangeLog("classpath:liquibase/schema-db2.xml"); liquibase.setShouldRun(isTest); return liquibase; } }
Como tengo dos bases de datos, para las pruebas quiero rodar dos DDL / DML separados en ellas. Dado que ambas configuraciones se cargan al mismo tiempo cuando la aplicación está @Qualifier
, si @Qualifier
, Spring perderá su designación de destino y, en el mejor de los casos, fallará. Resulta que los @Qualifier
engorrosos y propensos a los arañazos, y sin ellos no funciona. Para romper el punto muerto, debe darse cuenta de que la dependencia se puede obtener no solo como un argumento, sino también como un valor de retorno, reescribiendo el código de esta manera:
@Configuration public class PsqlDatasourceConfig { @Bean @Primary @ConfigurationProperties(prefix = "spring.datasource.psql") public DataSource psqlDataSource() { return DataSourceBuilder.create().build(); } @Bean public SpringLiquibase primaryLiquibase(ProfileChecker checker) { boolean isTest = checker.isTest(); SpringLiquibase liquibase = new SpringLiquibase(); liquibase.setDataSource(psqlDataSource());
javax.inject.Provider
¿Cómo obtener un bean con prototipo de alcance? A menudo me encontré con esto
@Component @Scope(SCOPE_PROTOTYPE) @RequiredArgsConstructor public class ProjectBuilder { private final ProjectFileConverter converter; private final ProjectRepository projectRepository;
Parece que todo está bien, el código funciona. Sin embargo, en este barril de miel hay una mosca en la pomada. Necesitamos arrastrar una javax.inject:javax.inject:1
más de javax.inject:javax.inject:1
, que se agregó a Maven Central exactamente hace 10 años y nunca se ha actualizado desde entonces.
¡Pero Spring ha sido capaz de hacer lo mismo sin adicciones de terceros! Simplemente reemplace javax.inject.Provider::get
con org.springframework.beans.factory.ObjectFactory::getObject
y todo funciona de la misma manera.
@Component @RequiredArgsConstructor public class PrototypeUtilizer { private final ObjectFactory<ProjectBuilder> projectBuilderFactory; void method() { ProjectBuilder freshBuilder = projectBuilderFactory.getObject(); } }
Ahora podemos, con la conciencia tranquila, eliminar javax.inject
de la lista de dependencias.
Usar cadenas en lugar de clases en la configuración
Un ejemplo común de conectar repositorios Spring Data a un proyecto:
@Configuration @EnableJpaRepositories("com.smth.repository") public class JpaConfig {
Aquí prescribimos explícitamente el paquete que será visto por Spring. Si permitimos un poco de nomenclatura adicional, la aplicación se bloqueará al inicio. Me gustaría detectar tales errores estúpidos en las primeras etapas, en el límite, justo durante la edición del código. El marco se dirige hacia nosotros, por lo que el código anterior se puede reescribir:
@Configuration @EnableJpaRepositories(basePackageClasses = AuditRepository.class) public class JpaConfig {
Aquí AuditRepository
es uno de los repositorios de paquetes que veremos. Como indicamos la clase, necesitaremos conectar esta clase a nuestra configuración, y ahora los errores tipográficos se detectarán directamente en el editor o, en el peor de los casos, al compilar el proyecto.
Este enfoque se puede aplicar en muchos casos similares, por ejemplo:
@ComponentScan(basePackages = "com.smth")
se convierte en
import com.smth.Smth; @ComponentScan(basePackageClasses = Smth.class)
Si necesitamos agregar alguna clase a un diccionario de la forma Map<String, Object>
, esto se puede hacer así:
void config(LocalContainerEntityManagerFactoryBean bean) { String property = "hibernate.session_factory.session_scoped_interceptor"; bean.getJpaPropertyMap().put(property, "com.smth.interceptor.AuditInterceptor"); }
pero es mejor usar un tipo explícito:
import com.smth.interceptor.AuditInterceptor; void config(LocalContainerEntityManagerFactoryBean bean) { String property = "hibernate.session_factory.session_scoped_interceptor"; bean.getJpaPropertyMap().put(property, AuditInterceptor.class); }
Y cuando hay algo como
LocalContainerEntityManagerFactoryBean bean = builder .dataSource(dataSource) .packages(
Vale la pena señalar que el método packages()
está sobrecargado y utiliza las clases:

No ponga todos los frijoles en un paquete
Creo que en muchos proyectos en Spring / Spring Booth viste una estructura similar:
root-package | \ repository/ entity/ service/ Application.java
Aquí Application.java
es la clase raíz de la aplicación:
@SpringBootApplication @EnableJpaRepositories(basePackageClasses = SomeRepository.class) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
Este es el código clásico de microservicio: los componentes se organizan en carpetas según el propósito, la clase con la configuración está en la raíz. Si bien el proyecto es pequeño, entonces todo está bien. A medida que el proyecto crece, aparecen paquetes gordos con docenas de repositorios / servicios. Y si el proyecto sigue siendo un monolito, entonces Di-s con ellos. Pero si surge la tarea de dividir la aplicación irregular en partes, entonces comienzan las preguntas. Habiendo experimentado este dolor una vez, decidí adoptar un enfoque diferente, es decir, agrupar las clases por su dominio. El resultado es algo como
root-package/ | \ user/ | \ repository/ domain/ service/ controller/ UserConfig.java billing/ | \ repository/ domain/ service/ BillingConfig.java //... Application.java
Aquí, el paquete del user
incluye subpaquetes con clases responsables de la lógica del usuario:
user/ | \ repository/ UserRepository.java domain/ UserEntity.java service/ UserService.java controller/ UserController.java UserConfig.java
Ahora en UserConfig
puede describir todas las configuraciones asociadas con esta funcionalidad:
@Configuration @ComponentScan(basePackageClasses = UserServiceImpl.class) @EnableJpaRepositories(basePackageClasses = UserRepository.class) class UserConfig { }
La ventaja de este enfoque es que, si es necesario, los módulos se pueden asignar más fácilmente a servicios / aplicaciones separados. También es útil si tiene la intención de modularizar su proyecto agregando module-info.java
, ocultando clases de utilidad del mundo exterior.
Eso es todo, espero, mi trabajo te ha sido útil. Describa sus antipatrones en los comentarios, los resolveremos juntos :)