В этой заметке хочу поделится наблюдениями о некоторых антипаттернах, встречающихся в коде приложений, работающих на Спринге. Все из них так или иначе всплывали в живом коде: либо я натыкался на них в уже существующих классах, либо вылавливал во время вычитки творчества коллег.
Надеюсь, вам будет интересно, а если после прочтения вы осознаете свои "грехи" и встанете на путь исправления, то я буду доволен вдвойне. Также призываю вас поделится своими собственными примерами в комментария, наиболее любопытные и необычные добавим в запись.
Autowired
Великий и ужасный @Autowired — это целая эпоха в Спринге. Без него до сих пор не обойтись при написании тестов, но в основном коде он (ПМСМ) явно лишний. В нескольких моих последних проектах его не было вовсе. Долгое время мы писали вот так:
@Component public class MyService { @Autowired private ServiceDependency; @Autowired private AnotherServiceDependency; //... }
Причины, по которым внедрение зависимостей через поля и сеттеры подвергаются критике, уже подробно описывались, в частности здесь. Альтернатива — внедрение через конструктор. По приведённой ссылке, описан такой пример:
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; }
Он выглядит более-менее прилично, но представьте, что зависимостей у нас штук 10 (да-да, я знаю, что в этом случае их нужно группировать по отдельным классам, но сейчас речь не об этом). Картинка получается уже не столь привлекательной:
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; }
Код прямо скажем, выглядит монструозненько.
И вот тут многие забывают, что и здесь скрипач @Autowired не нужен! Если у класса только один конструктор, то Спринг (>= 4) поймёт, что зависимости нужно внедрять через этот конструктор. А значит мы можем и его выбросить, заменив его на ломбоковский @AllArgsContructor. Или ещё лучше — на @RequiredArgsContructor, не забыв при этом объявить все нужные поля final и получив на сдачу безопасную инициализацию объекта в многопоточной среде (при условии, что все зависимости также безопасно инициализируются):
@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; }
Статиче��кие методы в утилитных классах и enum-функции
В кровавом Э часто стоит задача преобразования объектов-носителей данных из одного слоя приложения, в подобные объекты другого слоя. Для этого до сих пор (напоминаю, на дворе 2019 год) используются утилитные классы со статическими методами вроде такого:
@UtilityClass public class ЧтоТоТамUtils { public static UserDto entityToDto(UserEntity user) { //... } }
Более продвинутые пользователи, читавшие умные книги, знают про волшебные свойства перечислений:
enum ЧтоТоТамFunction implements Function<UserEntity, UserDto> { INST; @Override public UserDto apply(UserEntity user) { //... } }
Правда, и в этом случае обращение всё равно происходит к объекту-одиночке, а не к компоненту, управляемому Спрингом.
Ещё более продвинутые парни (и девушки) знают про MapStruct, позволяющий описать всё в виде одного-единственного интерфейса:
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.ERROR) public interface CriminalRecommendationMapper { UserDto map(UserEntity user); }
Вот теперь мы получает спринговый компонент. Вроде победа. Но дьявол в мелочах, и бывает, что победа становится перемогой. Во-первых, именования полей должны быть одинаковы (иначе начинается геморрой), что не всегда удобно, во-вторых, при наличии каких-либо сложных преобразований полей обрабатываемых объектов возникают дополнительные сложности. Ну и сам mapstruct нужно добавлять в зависимости.
И мало кто вспоминает дедовский, но тем не менее простой и рабочий способ получить управляемый спрингом преобразователь:
import org.springframework.core.convert.converter.Converter; @Component public class UserEntityToDto implements Converter<UserEntity, UserDto> { @Override public UserDto convert(UserEntity user) { //... } }
Преимущество здесь в том, что в другом классе мне достаточно написать
@Component @RequiredArgsConstructor public class DI { private final Converter<UserEntity, UserDto> userEnityToDto; }
и Спринг самостоятельно всё разрулит!
Ненужный Qualifier
Случай из жизни: приложение работает с двумя БД. Соответственно, есть два источника данных (java.sql.DataSource), два менеджера транзакций, две группы репозиториев и т.д. Всё это удобно описать в двух отдельных настройках. Вот это для Постгре:
@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; } }
А это для ДБ2:
@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; } }
Поскольку у меня есть две базы, то для тестов я хочу накатить на них два отдельных DDL/DML-я. Поскольку обе конфигурации загружаются одновременно при поднятии приложения, то если я уберу @Qualifier, то Спринг лишится целеуказания и в лучшем случае свалится с ошибкой. Получается, что @Qualifier-ы громоздки и подвержены очепяткам, а без них не работает. Чтобы выйти из тупика нужно сообразить, что зависимость можно получать не только в виде аргумента, но и в виде возвращаемого значения, переписав код вот так:
@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()); // <----- liquibase.setChangeLog("classpath:liquibase/schema-postgre.xml"); liquibase.setShouldRun(isTest); return liquibase; } } //... @Configuration public class Db2DatasourceConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource.db2") public DataSource db2DataSource() { return DataSourceBuilder.create().build(); } @Bean public SpringLiquibase liquibase(ProfileChecker checker) { boolean isTest = checker.isTest(); SpringLiquibase liquibase = new SpringLiquibase(); liquibase.setDataSource(db2DataSource()); // <----- liquibase.setChangeLog("classpath:liquibase/schema-db2.xml"); liquibase.setShouldRun(isTest); return liquibase; } }
javax.inject.Provider
Как получить бин с областью действия prototype? Часто мне попадалось вот это
@Component @Scope(SCOPE_PROTOTYPE) @RequiredArgsConstructor public class ProjectBuilder { private final ProjectFileConverter converter; private final ProjectRepository projectRepository; //... } @Component @RequiredArgsConstructor public class PrototypeUtilizer { private final Provider<ProjectBuilder> projectBuilderProvider; void method() { ProjectBuilder freshBuilder = projectBuilderProvider.get(); } }
Казалось бы, всё хорошо, код работает. Однако, в этой бочке мёда есть ложка дёгтя. Нам нужно тащить за собой ещё одну зависимость javax.inject:javax.inject:1, которая была добавлена в Мавен Централ ровно 10 лет назад и с тех пор ни разу не обновлялась.
А ведь Спринг давным давно умеет делать то же самое без сторонних зависимостей! Достаточно заменить javax.inject.Provider::get на org.springframework.beans.factory.ObjectFactory::getObject и всё работает точно так же.
@Component @RequiredArgsConstructor public class PrototypeUtilizer { private final ObjectFactory<ProjectBuilder> projectBuilderFactory; void method() { ProjectBuilder freshBuilder = projectBuilderFactory.getObject(); } }
Теперь мы можем с чистой совестью выпилить javax.inject из списка зависимостей.
Использование строк вместо классов в настройках
Часто встречающийся пример подключения Spring Data -репозиториев к проекту:
@Configuration @EnableJpaRepositories("com.smth.repository") public class JpaConfig { //... }
Здесь мы явно прописываем пакет, который будет просматриваться Спрингом. Если мы допустим очепятку в именовании, то приложение упадёт при запуске. Хочется выявлять подобные глупые ошибки на ранних этапах, в пределе — прямо во время правки кода. Фреймворк идёт нам навстречу, поэтому код выше можно переписать:
@Configuration @EnableJpaRepositories(basePackageClasses = AuditRepository.class) public class JpaConfig { //... }
Здесь AuditRepository — один из репозиториев пакета, который мы будем просматривать. Т. к. мы указали класс, то нам потребуется подключить этот класс к нашей конфигурации, и теперь опечатки будут выявляться прямо в редакторе или на худой конец при сборке проекта.
Этот подход можно применить во многих похожих случаях, например:
@ComponentScan(basePackages = "com.smth")
превращается в
import com.smth.Smth; @ComponentScan(basePackageClasses = Smth.class)
Если нам нужно добавить некоторый класс в словарь вида Map<String, Object>, то это можно сделать вот так:
void config(LocalContainerEntityManagerFactoryBean bean) { String property = "hibernate.session_factory.session_scoped_interceptor"; bean.getJpaPropertyMap().put(property, "com.smth.interceptor.AuditInterceptor"); }
но лучше использовать явный тип:
import com.smth.interceptor.AuditInterceptor; void config(LocalContainerEntityManagerFactoryBean bean) { String property = "hibernate.session_factory.session_scoped_interceptor"; bean.getJpaPropertyMap().put(property, AuditInterceptor.class); }
А когда есть нечто вроде
LocalContainerEntityManagerFactoryBean bean = builder .dataSource(dataSource) .packages( //... пакеты с сущностями хибернейта ) .persistenceUnit("psql") .build();
то стоит обратить внимание, что метод packages() перегружен и использовать классы:

Не кладите все бины в один пакет
Думаю, во многих проектах на Спринге/Спринг Буте вы видели подобную структуру:
root-package | \ repository/ entity/ service/ Application.java
Здесь Application.java — это корневой класс приложения:
@SpringBootApplication @EnableJpaRepositories(basePackageClasses = SomeRepository.class) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
Это классический код микросервиса: составляющие разложены по папочкам согласно назначению, в корне лежит класс с настройками. Пока проект небольшой, то всё хорошо. По мере разрастания проекта возникают жирненьки�� пакеты с десятками репозиториев/сервисов. И если проект так и останется монолитом, то б-г с ними. А вот если встанет задача разделить раскабаневшее приложение на части, тут-то и начинаются вопросы. Один раз испытав эту боль я решил применить другой подход, а именно группировать классы по их домену. В итоге получается что-то вроде
root-package/ | \ user/ | \ repository/ domain/ service/ controller/ UserConfig.java billing/ | \ repository/ domain/ service/ BillingConfig.java //... Application.java
Здесь пакет user включает в себя подпакеты с классами, ответственными за логику пользователей:
user/ | \ repository/ UserRepository.java domain/ UserEntity.java service/ UserService.java controller/ UserController.java UserConfig.java
Теперь в UserConfig можно описать все настройки, связанные с данным функционалом:
@Configuration @ComponentScan(basePackageClasses = UserServiceImpl.class) @EnableJpaRepositories(basePackageClasses = UserRepository.class) class UserConfig { }
Преимущество такого подхода в том, что модули можно будет при необходимости легче выделять в отдельные сервисы/приложения. Также это полезно в том случае, если вы собираетесь модуляризировать ваш проект, добавив module-info.java, скрыв от внешнего мира служебные классы.
На этом всё, я надеюсь, моё творчество оказалось для вас полезным. Описывайте в комментариях ваши антипаттерны, будем разбираться вместе :)
