Топ-10 самых распространенных ошибок Spring Framework
Опубликовано: 2022-03-11Spring, возможно, является одной из самых популярных сред Java, а также могучим зверем, которого нужно приручить. Хотя его основные концепции довольно легко понять, чтобы стать сильным разработчиком Spring, требуется некоторое время и усилия.
В этой статье мы рассмотрим некоторые из наиболее распространенных ошибок в Spring, специально ориентированных на веб-приложения и Spring Boot. Как указано на веб-сайте Spring Boot, Spring Boot придерживается определенного мнения о том, как должны создаваться готовые к работе приложения, поэтому в этой статье мы попытаемся подражать этому мнению и предоставим обзор некоторых советов, которые хорошо впишутся в стандартную разработку веб-приложений Spring Boot.
Если вы не очень хорошо знакомы с Spring Boot, но все же хотели бы попробовать некоторые из упомянутых вещей, я создал репозиторий GitHub, сопровождающий эту статью. Если вы почувствуете себя потерянным в какой-либо момент статьи, я бы порекомендовал клонировать репозиторий и поэкспериментировать с кодом на вашем локальном компьютере.
Распространенная ошибка № 1: Слишком низкий уровень
Мы нашли общий язык с этой распространенной ошибкой, потому что синдром «изобретено не здесь» довольно распространен в мире разработки программного обеспечения. Симптомы включают в себя регулярное переписывание фрагментов часто используемого кода, и многие разработчики, похоже, страдают от этого.
Хотя понимание внутреннего устройства конкретной библиотеки и ее реализации по большей части полезно и необходимо (а также может быть отличным процессом обучения), для вашего развития как инженера-программиста вредно постоянно заниматься одной и той же низкоуровневой реализацией. подробности. Существует причина, по которой существуют абстракции и фреймворки, такие как Spring, которая заключается именно в том, чтобы отделить вас от повторяющейся ручной работы и позволить вам сосредоточиться на деталях более высокого уровня — объектах предметной области и бизнес-логике.
Так что используйте абстракции — в следующий раз, когда вы столкнетесь с конкретной проблемой, сначала выполните быстрый поиск и определите, интегрирована ли библиотека, решающая эту проблему, в Spring; в настоящее время есть вероятность, что вы найдете подходящее существующее решение. В качестве примера полезной библиотеки я буду использовать аннотации Project Lombok в оставшейся части этой статьи. Lombok используется как генератор стандартного кода, и, надеюсь, у ленивого разработчика внутри вас не должно возникнуть проблем с ознакомлением с библиотекой. В качестве примера посмотрите, как выглядит «стандартный Java-бин» с Lombok:
@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 со своей IDE. Версию плагина IntelliJ IDEA можно найти здесь.
Распространенная ошибка № 2: «протекание» внутренних органов
Открытие вашей внутренней структуры никогда не является хорошей идеей, потому что это создает негибкость в сервис-дизайне и, следовательно, продвигает неправильные методы кодирования. «Утечка» внутренних компонентов проявляется в том, что структура базы данных становится доступной из определенных конечных точек API. В качестве примера предположим, что следующий POJO («Обычный старый объект Java») представляет таблицу в вашей базе данных:
@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 для хранения хэшей паролей ваших пользователей в базе данных — без соединителя, такого как 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 или без Spring, многопоточность может оказаться крепким орешком. Проблемы, вызванные параллельным выполнением программ, мучительно неуловимы, и часто их чрезвычайно трудно отладить — на самом деле, из-за характера проблемы, когда вы понимаете, что имеете дело с проблемой параллельного выполнения, вы, вероятно, собираетесь вам придется полностью отказаться от отладчика и проверять код «вручную», пока не найдете основную причину ошибки. К сожалению, универсального решения для таких проблем не существует; в зависимости от вашего конкретного случая вам придется оценить ситуацию, а затем решить проблему с той точки зрения, которую вы считаете лучшей.
В идеале вы, конечно, хотели бы вообще избежать ошибок многопоточности. Опять же, универсального подхода для этого не существует, но вот несколько практических соображений по отладке и предотвращению ошибок многопоточности:
Избегайте глобального состояния
Во-первых, всегда помните о проблеме «глобального состояния». Если вы создаете многопоточное приложение, абсолютно все, что можно изменить глобально, должно внимательно отслеживаться и, если возможно, полностью удаляться. Если есть причина, по которой глобальная переменная должна оставаться модифицируемой, тщательно используйте синхронизацию и отслеживайте производительность вашего приложения, чтобы убедиться, что оно не тормозит из-за вновь введенных периодов ожидания.
Избегайте изменчивости
Этот исходит прямо из функционального программирования и, адаптированный к ООП, утверждает, что следует избегать изменчивости класса и изменения состояния. Короче говоря, это означает отказ от методов установки и наличие закрытых конечных полей во всех классах вашей модели. Единственный раз, когда их значения мутируют, это во время строительства. Таким образом, вы можете быть уверены, что не возникнет проблем с конкуренцией и что доступ к свойствам объекта всегда будет предоставлять правильные значения.

Регистрация важных данных
Оцените, где ваше приложение может вызвать проблемы, и заблаговременно запишите все важные данные. Если произойдет ошибка, вы будете благодарны за информацию о том, какие запросы были получены, и лучше поймете, почему ваше приложение работает неправильно. Еще раз необходимо отметить, что ведение журнала вводит дополнительный файловый ввод-вывод, и поэтому им не следует злоупотреблять, поскольку это может серьезно повлиять на производительность вашего приложения.
Повторное использование существующих реализаций
Всякий раз, когда вам нужно создавать собственные потоки (например, для выполнения асинхронных запросов к различным службам), повторно используйте существующие безопасные реализации, а не создавайте свои собственные решения. По большей части это будет означать использование ExecutorServices и CompletableFutures в функциональном стиле Java 8 для создания потоков. Spring также позволяет асинхронную обработку запросов через класс DeferredResult.
Распространенная ошибка № 6: не использовать проверку на основе аннотаций
Давайте представим, что нашей предыдущей службе TopTalent требуется конечная точка для добавления новых лучших талантов. Кроме того, предположим, что по какой-то действительно уважительной причине каждое новое имя должно иметь длину ровно 10 символов. Один из способов сделать это может быть следующим:
@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); } Однако вышеизложенное (в дополнение к тому, что оно плохо построено) на самом деле не является «чистым» решением. Мы проверяем более одного типа достоверности (а именно, что TopTalentData не равно null, и что TopTalentData.name не равно null, и что TopTalentData.name имеет длину 10 символов), а также выбрасываем исключение, если данные недействительны. .
Это можно выполнить намного чище, используя валидатор Hibernate с Spring. Давайте сначала рефакторим метод addTopTalent для поддержки проверки:
@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 } Кроме того, нам нужно будет указать, какое свойство мы хотим проверить в классе TopTalentData :
public class TopTalentData { @Length(min = 10, max = 10) @NotNull private String name; }Теперь Spring перехватит запрос и проверит его перед вызовом метода — нет необходимости использовать дополнительные ручные тесты.
Другой способ добиться того же — создать собственные аннотации. Хотя вы обычно будете использовать пользовательские аннотации только тогда, когда ваши потребности превышают встроенный набор ограничений Hibernate, для этого примера давайте притворимся, что @Length не существует. Вы бы сделали валидатор, который проверяет длину строки, создав два дополнительных класса, один для проверки, а другой для аннотирования свойств:
@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; } } Обратите внимание, что в этих случаях лучшие практики по разделению проблем требуют, чтобы вы помечали свойство как допустимое, если оно равно null ( s == null в методе isValid ), а затем использовали аннотацию @NotNull , если это является дополнительным требованием для имущество:
public class TopTalentData { @MyAnnotation(value = 10) @NotNull private String name; }Распространенная ошибка № 7: (по-прежнему) использование конфигурации на основе XML
Хотя XML был необходим для предыдущих версий Spring, в настоящее время большая часть конфигурации может быть выполнена исключительно с помощью Java-кода/аннотаций; Конфигурации XML просто представляют собой дополнительный и ненужный шаблонный код.
В этой статье (а также в сопровождающем ее репозитории GitHub) используются аннотации для настройки Spring, а Spring знает, какие bean-компоненты следует связать, поскольку корневой пакет снабжен составной аннотацией @SpringBootApplication , например:
@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }Составная аннотация (вы можете узнать больше об этом в документации Spring) просто дает Spring подсказку о том, какие пакеты следует сканировать для извлечения bean-компонентов. В нашем конкретном случае это означает, что будет использоваться следующий пакет под верхним (co.kukurin) для проводки:
-
@Component(TopTalentConverter,MyAnnotationValidator) -
@RestController(TopTalentController) - @
TopTalentRepository@Repository -
@Service(TopTalentService)
Если бы у нас были какие-либо дополнительные аннотированные классы @Configuration , они также были бы проверены на наличие конфигурации на основе Java.
Распространенная ошибка № 8: Забыть о профилях
Проблема, с которой часто приходится сталкиваться при разработке серверов, заключается в различении разных типов конфигураций, обычно производственных и разрабатываемых конфигураций. Вместо того, чтобы вручную заменять различные записи конфигурации каждый раз, когда вы переключаетесь с тестирования на развертывание приложения, более эффективным способом было бы использование профилей.
Рассмотрим случай, когда вы используете базу данных в памяти для локальной разработки с базой данных MySQL в рабочей среде. По сути, это будет означать, что вы будете использовать другой URL-адрес и (надеюсь) разные учетные данные для доступа к каждому из них. Давайте посмотрим, как это можно сделать с двумя разными файлами конфигурации:
файл 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:файл приложения-dev.yaml
spring.datasource.url: 'jdbc:h2:mem:' spring.datasource.platform: h2 Предположительно, вы не хотели бы случайно выполнять какие-либо действия в своей производственной базе данных во время работы с кодом, поэтому имеет смысл установить профиль по умолчанию на dev. Затем на сервере вы можете вручную переопределить профиль конфигурации, указав параметр -Dspring.profiles.active=prod для JVM. Кроме того, вы также можете установить переменную среды вашей ОС в желаемый профиль по умолчанию.
Распространенная ошибка № 9: отказ от внедрения зависимостей
Правильное использование внедрения зависимостей с помощью Spring означает возможность связывания всех ваших объектов вместе путем сканирования всех желаемых классов конфигурации; это оказывается полезным для разделения отношений, а также значительно упрощает тестирование. Вместо классов с жесткой связью, выполнив что-то вроде этого:
public class TopTalentController { private final TopTalentService topTalentService; public TopTalentController() { this.topTalentService = new TopTalentService(); } }Мы позволяем Spring сделать проводку за нас:
public class TopTalentController { private final TopTalentService topTalentService; public TopTalentController(TopTalentService topTalentService) { this.topTalentService = topTalentService; } } Misko Hevery в Google talk подробно объясняет «почему» внедрение зависимостей, поэтому давайте вместо этого посмотрим, как это используется на практике. В разделе о разделении ответственности (Распространенные ошибки № 3) мы создали класс службы и контроллера. Допустим, мы хотим протестировать контроллер в предположении, что TopTalentService ведет себя корректно. Мы можем вставить фиктивный объект вместо фактической реализации службы, предоставив отдельный класс конфигурации:
@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; } } Затем мы можем внедрить фиктивный объект, сказав Spring использовать SampleUnitTestConfig в качестве поставщика конфигурации:
@ContextConfiguration(classes = { SampleUnitTestConfig.class })Затем это позволяет нам использовать конфигурацию контекста для внедрения пользовательского компонента в модульный тест.
Распространенная ошибка № 10: отсутствие тестирования или неправильное тестирование
Несмотря на то, что идея юнит-тестирования была с нами уже давно, многие разработчики, похоже, либо «забывали» это делать (особенно если это не требуется ), либо просто добавляли это как запоздалую мысль. Это явно нежелательно, так как тесты должны не только проверять правильность вашего кода, но и служить документацией того, как приложение должно вести себя в разных ситуациях.
При тестировании веб-сервисов вы редко выполняете исключительно «чистые» модульные тесты, поскольку для связи по HTTP обычно требуется вызвать DispatcherServlet Spring и посмотреть, что произойдет, когда будет получен фактический HttpServletRequest (что делает его интеграционным тестом, имеющим дело с проверкой, сериализацией). , так далее). REST Assured, Java DSL для простого тестирования сервисов REST поверх MockMVC, оказался очень элегантным решением. Рассмотрим следующий фрагмент кода с внедрением зависимостей:
@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 реализацию TopTalentService с TopTalentController , в то время как все остальные классы подключаются с использованием стандартной конфигурации, полученной в результате сканирования пакетов, находящихся в корневом пакете класса Application. RestAssuredMockMvc просто используется для настройки облегченной среды и отправки запроса GET на конечную точку /toptal/get .
Стать мастером весны
Spring — это мощный фреймворк, с которым легко начать работу, но для достижения полного мастерства требуется определенная самоотверженность и время. Если вы потратите время на ознакомление с фреймворком, это определенно повысит вашу производительность в долгосрочной перспективе и, в конечном итоге, поможет вам писать более чистый код и стать лучшим разработчиком.
Если вам нужны дополнительные ресурсы, Spring In Action — это хорошая практическая книга, охватывающая многие основные темы Spring.
