Pull to refresh

Что мы делаем не так со Спрингом

Reading time9 min
Views12K

В этой заметке хочу поделится наблюдениями о некоторых антипаттернах, встречающихся в коде приложений, работающих на Спринге. Все из них так или иначе всплывали в живом коде: либо я натыкался на них в уже существующих классах, либо вылавливал во время вычитки творчества коллег.


Надеюсь, вам будет интересно, а если после прочтения вы осознаете свои "грехи" и встанете на путь исправления, то я буду доволен вдвойне. Также призываю вас поделится своими собственными примерами в комментария, наиболее любопытные и необычные добавим в запись.


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, скрыв от внешнего мира служебные классы.


На этом всё, я надеюсь, моё творчество оказалось для вас полезным. Описывайте в комментариях ваши антипаттерны, будем разбираться вместе :)

Tags:
Hubs:
+11
Comments81

Articles