10 самых распространенных ошибок при работе с платформой Spring. Часть 1 +8



Всем привет. Сегодня делимся первой частью статьи, перевод которой подготовлен специально для студентов курса «Разработчик на Spring Framework». Начнём!





Spring — пожалуй, одна из самых популярных платформ разработки на языке Java. Это мощный, но довольно сложный в освоении инструмент. Его базовые концепции довольно легко понять и усвоить, но для того чтобы стать опытным разработчиком на Spring, потребуются время и определенные усилия.

В этой статье мы рассмотрим некоторые из самых распространенных ошибок, совершаемых при работе в Spring и связанных, в частности, с разработкой веб-приложений и использованием платформы Spring Boot. Как отмечается на веб-сайте Spring Boot, в Spring Boot используется стандартизованный подход к созданию готовых к эксплуатации приложений, и данная статья будет придерживаться этого подхода. В ней будет дан ряд рекомендаций, которые можно эффективно использовать при разработке стандартных веб-приложений на базе Spring Boot.

На тот случай, если вы не очень хорошо знакомы с платформой Spring Boot, но хотите поэкспериментировать с примерами, приведенными в статье, я создал GitHub-репозиторий с дополнительными материалами для этой статьи. Если в какой-то момент вы немного запутались, читая эту статью, я бы посоветовал вам создать клон этого репозитория и поэкспериментировать с кодом на своем компьютере.

Распространенная ошибка № 1. Переход к слишком низкоуровневому программированию


Мы легко поддаемся этой распространенной ошибке, поскольку «синдром неприятия чужой разработки» довольно характерен для программистской среды. Один из его симптомов — постоянное переписывание кусков широко применяемого кода, и этот симптом наблюдается у многих программистов.
Изучение внутреннего устройства и особенностей реализации определенной библиотеки — зачастую полезный, необходимый и интересный процесс, но если вы будете постоянно писать однотипный код, занимаясь низкоуровневой реализацией функций, это может оказаться вредным для вас как для разработчика ПО. Именно поэтому существуют и используются абстракции и такие платформы, как Spring — они избавляют вас от повторяющейся ручной работы и позволяют на более высоком уровне сконцентрироваться на объектах вашей предметной области и логике программного кода.

Поэтому не следует избегать абстракций. Когда в следующий раз вы столкнетесь с необходимостью решить определенную задачу, сначала проведите быстрый поиск и выясните, не встроена ли уже в Spring библиотека, которая решает эту задачу, — вполне вероятно, вы найдете подходящее готовое решение. Одна из таких полезных библиотек — Project Lombok, аннотации из которой я буду использовать в примерах в этой статье. Lombok используется в качестве генератора шаблонного кода, так что ленивый разработчик, который живет в каждом из нас, будет очень рад познакомиться с этой библиотекой. Посмотрите, например, как в Lombok выглядит стандартный компонент JavaBean:

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

Как вы, возможно, уже поняли, приведенный выше код преобразуется в следующий вид:

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() {
    }
}

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

Распространенная ошибка № 2. «Утечка» внутренних структур


Предоставление доступа к внутренним структурам никогда не было хорошей идеей, так как оно ухудшает гибкость модели службы и, как следствие, способствует формированию плохого стиля программирования. «Утечка» внутренних структур проявляется в том, что структура базы данных становится доступна из определенных конечных точек API. Допустим, например, что следующий «старый добрый Java-объект» (POJO) представляет таблицу в вашей базе данных:

@Entity
@NoArgsConstructor
@Getter
public class TopTalentEntity {

    @Id
    @GeneratedValue
    private Integer id;

    @Column
    private String name;

    public TopTalentEntity(String name) {
        this.name = name;
    }

}

Допустим, что существует конечная точка, которой необходимо обратиться к данным объекта TopTalentEntity. Вернуть экземпляры TopTalentEntity выглядит заманчивой идеей, но более гибким решением будет создание нового класса, представляющего данные TopTalentEntity для конечной точки API:

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

Таким образом, внесение изменений во внутренние компоненты базы данных не потребует дополнительных изменений на уровне служб. Посмотрим, что произойдет, если в класс TopTalentEntity будет добавлено поле password для хранения хешей пользовательских паролей в базе данных: если будет отсутствовать коннектор, такой как TopTalentData, и разработчик забудет изменить интерфейсную часть службы, это может привести к очень нежелательному раскрытию секретной информации!

Распространенная ошибка № 3. Объединение в коде функций, которые лучше было бы разнести


Организация кода приложения по мере его разрастания становится все более важной задачей. Как ни странно, большая часть принципов эффективного программирования перестает работать при достижении определенного масштаба разработки, особенно если архитектура приложения не была достаточно хорошо продумана. И одной из самых часто совершаемых ошибок становится объединение в коде функций, которые разумнее реализовать отдельно.

Причиной нарушения принципа разделения ответственности обычно является добавление новой функциональности в существующие классы. Возможно, это хорошее сиюминутное решение (в частности, оно требует написания меньшего объема кода), но в дальнейшем оно неизбежно становится проблемой, в том числе на этапах тестирования и поддержки кода и между ними. Рассмотрим следующий контроллер, возвращающий TopTalentData из репозитория:

@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());
    }

}

На первый взгляд кажется, что в этом фрагменте кода нет каких-то очевидных ошибок. Он предоставляет список объектов TopTalentData, который берется из экземпляров класса TopTalentEntity. Но если посмотреть на код более внимательно, мы увидим, что в действительности TopTalentController выполняет здесь несколько действий, а именно — соотносит запросы с определенной конечной точкой, извлекает данные из репозитория и преобразует объекты, полученные из TopTalentRepository, в другой формат. Более «чистое» решение должно разделять эти функции по отдельным классам. Например, это может выглядеть следующим образом:

@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());
    }
}

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

Распространенная ошибка № 4. Неединообразный код и плохая обработка ошибок


Тема единообразия кода не является уникальной именно для Spring (или вообще для Java), но, тем не менее, является важным аспектом, который необходимо учитывать, работая с проектами в Spring. Выбор конкретного стиля программирования может быть темой для обсуждения (и обычно согласуется внутри команды разработчиков или в пределах всей компании), но так или иначе наличие общего стандарта написания кода способствует повышению эффективности работы. Это особенно актуально, если над кодом работают несколько человек. Единообразный код можно передавать от разработчика к разработчику, не тратя много усилий на его сопровождение или на длительные объяснения по поводу того, для чего нужны те или иные классы.

Представим, что есть проект Spring, в котором имеются различные конфигурационные файлы, службы и контроллеры. Соблюдая семантическое единообразие при их именовании, мы создаем структуру, по которой можно легко осуществлять поиск и в которой любой разработчик может легко разобраться с кодом. Например, к именам конфигурационных классов можно добавлять суффикс Config, к именам служб — суффикс Service, а к именам контроллеров — суффикс Controller.

Тема обработки ошибок на стороне сервера тесно связана с темой единообразия кода и заслуживает особого внимания. Если вы когда-либо обрабатывали исключения, поступающие из плохо написанного API-интерфейса, вы, вероятно, знаете, как трудно правильно понять смысл этих исключений, а еще труднее определить, почему, собственно, они возникли.
Как разработчик API-интерфейса в идеале вы бы хотели охватить все конечные точки, с которыми работает пользователь, и привести их к использованию единого формата сообщений об ошибках. Обычно это означает, что необходимо использовать стандартные коды ошибок и их описание и отказываться от сомнительных решений типа выдачи пользователю сообщения «500 Internal Server Error» или результатов трассировки стека (последнего варианта, кстати, нужно избегать всеми силами, так как вы раскрываете внутренние данные, а кроме того, такие результаты трудно обрабатывать на стороне клиента).
Вот, к примеру, как может выглядеть общий формат сообщения об ошибке:

@Value
public class ErrorResponse {

    private Integer errorCode;
    private String errorMessage;

}

Формат, похожий на этот, часто встречается в самых популярных API и, как правило, отлично работает, поскольку может быть легко и планомерно задокументирован. Преобразовать исключение в этот формат можно путем добавления аннотации @ExceptionHandler к методу (пример аннотации см. в разделе «Распространенная ошибка № 6»).

Распространенная ошибка № 5. Неправильная работа с многопоточностью


Реализация многопоточности может оказаться сложной задачей вне зависимости от того, применяется она в приложениях для настольных систем или в веб-приложениях, в Spring-проектах или проектах для других платформ. Проблемы, вызываемые параллельным выполнением программ, трудно отследить, и разбираться с ними с помощью отладчика зачастую бывает очень тяжело. Если вы поняли, что имеете дело с ошибкой, связанной с параллельным выполнением, то, скорее всего, вам придется отказаться от отладчика и исследовать код вручную, пока не будет найдена первопричина ошибки. К сожалению, стандартного способа решения таких проблем не существует. В каждом случае необходимо оценивать ситуацию и «атаковать» проблему тем методом, который представляется лучшим в данных условиях.

В идеале вы бы, конечно, хотели полностью избежать ошибок, связанных с многопоточностью. И хотя универсального рецепта здесь нет, некоторые практические рекомендации я все-таки могу предложить.

Избегайте использования глобальных состояний


Во-первых, всегда помните о проблеме «глобальных состояний». Если вы разрабатываете многопоточное приложение, нужно внимательно отслеживать абсолютно все глобально модифицируемые переменные, а по возможности — избавляться от них вовсе. Если у вас все же есть причина, по которой глобальная переменная должна быть модифицируемой, должным образом реализуйте синхронизацию и следите за производительностью вашего приложения — следует убедиться, что не происходит замедления его работы из-за добавленных периодов ожидания.

Избегайте изменяемых объектов


Эта идея исходит прямо из принципов функционального программирования и, будучи адаптированной под принципы ООП, гласит о том, что следует избегать изменяемых классов и изменяемых состояний. Говоря короче, это означает, что следует воздерживаться от устанавливающих методов (сеттеров) и иметь закрытые поля с модификатором final во всех классах моделей. Единственный момент, когда их значения меняются, — при создании объекта. В этом случае можно быть уверенным, что не возникнут проблемы, связанные с состязанием за ресурсы, и при обращении к свойствам объекта всегда будут получены правильные значения.

Фиксируйте в журнале важные данные


Оцените, где в приложении могут возникнуть проблемы, и заблаговременно настройте запись в журнал всех важных данных. При возникновении ошибки вы будете рады иметь информацию о том, какие запросы были получены, и сможете лучше понять причины неправильной работы вашего приложения. Однако учтите, что ведение журнала подразумевает дополнительные файловые операции ввода-вывода и им не следует злоупотреблять, так как это может негативно сказаться на производительности приложения.

Используйте готовые реализации потоков


Когда вам необходимо породить свои потоки (например, для создания асинхронных запросов к различным службам), используйте готовые безопасные реализации потоков вместо создания собственных. В большинстве случаев для создания потоков можно использовать абстракции ExecutorService и эффектные функциональные абстракции CompletableFuture для Java 8. Платформа Spring также позволяет обрабатывать асинхронные запросы с помощью класса DeferredResult.

Конец первой части.

Вы можете помочь и перевести немного средств на развитие сайта

Теги:



Комментарии (8):

  1. nicholas_k
    /#20528555 / +1

    Я так и не понял, при чем здесь Spring.

  2. AstarothAst
    /#20530297

    Не вводите людей в заблуждение заголовком — слово «Spring» в нем лишнее. Озаглавьте так, что бы заголовок соответствовал содержанию, например так: «Несколько прописных истин, которые вы точно уже где-то слышали».

  3. amarkevich
    /#20531681

    в п.2 гармонично вписывается MapStruct, который, как и Lombok, работает на этапе кодогенерации:

    @Mapper
    public interface UserMapper {
    
        UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
    
        UserModel fromEntity(UserEntity user);
    
    }
    

    @RestController
    ...
        @Override
        public UserModel getUser(UUID userId) {
            return UserMapper.INSTANCE.fromEntity(userService.findById(userId));
        }
    

  4. usharik
    /#20532361

    Вопрос немного не по теме поста. Lombok позволяет убирать boilerplate код, но на сколько он при этом сажает производительность? Создает ли он какие-то проблемы при практическом использовании?

    • mayorovp
      /#20532525

      А каким образом кодогенератор может просадить производительность?

      • usharik
        /#20532549

        А разве Lombok работает на кодогенерации? Возможно я отстал от жизни, но там все как-то на рефлексии было построено. Или уже нет?

        • mayorovp
          /#20532579 / +1

          Оно работает как плагин к компилятору, дописывающий AST. Но на всякий случай есть и утилита-кодогенератор (Delombok).