Возможно, одна из самых популярных сред Java, Spring — могучий зверь, которого нужно приручить. Хотя его основные концепции довольно просты для понимания, все же требуется много времени и усилий, чтобы стать сильным разработчиком Spring.
В этой статье мы рассмотрим некоторые распространенные ошибки в Spring, особенно для веб-приложений и Spring Boot. так какОфициальный сайт Spring BootСказав это, Spring Boot отслеживает, как создавать приложения, готовые к производству.довольно упрямое мнение, в этой статье мы попытаемся подражать этой точке зрения и предоставить обзор некоторых методов, которые прекрасно впишутся в стандартную разработку веб-приложений Spring Boot.
Если вы новичок в Spring Boot, но все еще хотите попробовать некоторые вещи, упомянутые далее, я также создал эту статью.репозиторий GitHub. Если вы запутались при чтении, я предлагаю клонировать код и использовать его на своем локальном компьютере.
1. Распространенная ошибка 1: уделять слишком много внимания нижнему слою
Мы устраняем эту распространенную ошибку, потому что"Сделано не мной"Синдром распространен в разработке программного обеспечения. Симптомы включают частое переписывание некоторого общего кода, которое есть у многих разработчиков.
Хотя понимание внутреннего устройства конкретной библиотеки и ее реализации в значительной степени полезно и необходимо (а также может быть хорошим процессом обучения), для инженера-программиста постоянно иметь дело с одними и теми же низкоуровневыми деталями реализации очень важно для карьеры личного развития. вредный. Абстрактные фреймворки, такие как Spring, существуют не просто так: они освобождают вас от повторяющейся ручной работы и позволяют сосредоточиться на деталях более высокого уровня — объектах предметной области и бизнес-логике.
Так что прими абстракцию. В следующий раз, когда вы столкнетесь с конкретной проблемой, начните с быстрого поиска, чтобы узнать, была ли библиотека, решающая эту проблему, интегрирована в Spring; теперь вы можете найти подходящее готовое решение. Например, полезная библиотека, которую я буду использовать в примерах в оставшейся части этой статьи.Project Lombokаннотация. Lombok используется как генератор шаблонного кода в надежде, что у ленивых разработчиков не будет проблем с освоением библиотеки. В качестве примера посмотрите на использование Lombok's "Стандартный Java-бин" На что это похоже:
@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. Распространенная ошибка 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
данные.
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class TopTalentData {
private String name;
}
Таким образом, внесение изменений в серверную часть базы данных не потребует дополнительных изменений на сервисном уровне. Учтите, вTopTalentEntity
Добавьте поле «пароль» для хранения хэша пароля пользователя в базе данных — если его нетTopTalentData
Соединитель, например, если вы забыли изменить внешний интерфейс службы, случайно раскроет некоторую ненужную секретную информацию.
3. Распространенная ошибка № 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());
}
}
На первый взгляд этот код не кажется особенно проблематичным, он обеспечиваетTopTalentEntity
экземпляр полученTopTalentData
Список . Однако при ближайшем рассмотрении мы видим, что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. Распространенная ошибка 4: отсутствие обработки исключений или неправильная обработка
Тема согласованности не уникальна для Spring (или Java), но по-прежнему является важным аспектом, который следует учитывать при работе с проектами Spring. Хотя стили кодирования могут быть противоречивыми (часто согласовываются внутри команды или всей компании), наличие общего стандарта в конечном итоге значительно повышает производительность. Это особенно верно для команд, состоящих из нескольких человек: согласованность позволяет общаться без необходимости тратить много ресурсов на передачу или предоставление длинных объяснений для разных классов обязанностей.
Рассмотрим проект Spring с различными файлами конфигурации, службами и контроллерами. Поддержание семантической согласованности в именовании создает удобную для поиска структуру, в которой любой новый разработчик может управлять кодом по-своему, например, добавляя суффикс Config к классу конфигурации, уровень службы, оканчивающийся на Service, и элемент управления Контроллер заканчивается на Controller .
Обработка ошибок на стороне сервера, тесно связанная с темой согласованности, заслуживает особого внимания. Если вам когда-либо приходилось иметь дело с ответами об исключениях от плохо написанного API, вы, вероятно, знаете, почему - правильный анализ исключений может быть проблемой, а определение того, почему эти исключения произошли, еще более болезненно.
Как разработчик API, вы в идеале хотите охватить все конечные точки, с которыми сталкивается пользователь, и преобразовать их в распространенные форматы ошибок. Обычно это означает наличие общего кода ошибки и описания, а не уклонение от проблемы: а) возврат сообщения «500 Internal Server Error». b) Вернуть информацию о стеке исключения непосредственно пользователю. (На самом деле, этого следует избегать любой ценой, потому что это не только сложно для клиента, но и раскрывает вашу внутреннюю информацию).
Например, общий формат ответа об ошибке может выглядеть так:
@Value
public class ErrorResponse {
private Integer errorCode;
private String errorMessage;
}
Нечто подобное часто встречается в большинстве популярных API и часто хорошо работает, потому что его можно легко и систематически задокументировать. Преобразование исключений в этот формат можно выполнить, предоставив метод@ExceptionHandler
Аннотации сделаны (примеры аннотаций можно найти в главе 6).
5. Распространенная ошибка 5: неправильная многопоточность
Будь то настольное приложение или веб-приложение, будь то Spring или No Spring, многопоточность очень сложно взломать. Проблемы, вызванные параллельным выполнением программ, жуткие и неуловимые, и часто их трудно отлаживать — на самом деле, из-за характера проблемы, когда вы понимаете, что имеете дело с проблемой параллельного выполнения, вам, возможно, придется полностью отказаться от отладчика. и «вручную» проверять код, пока не будет найдена основная причина ошибки. К сожалению, не существует универсального решения таких проблем; оценивайте ситуацию в каждом конкретном случае и подходите к проблеме исходя из того, что вы считаете лучшим.
Конечно, в идеале вы также хотите полностью избежать ошибок многопоточности. Опять же, такого универсального подхода не существует, но у него есть некоторые практические соображения для отладки и предотвращения ошибок многопоточности:
5.1 Избегание глобального состояния
Во-первых, не забывайте о проблеме «глобального состояния». Если вы создаете многопоточное приложение, вы должны следить за всем, что может быть изменено глобально, и удалять их все, если это возможно. Если есть причина, по которой глобальная переменная должна оставаться модифицируемой, используйте ее осторожно.synchronization, и отслеживать производительность программы, чтобы убедиться, что новая задержка не приводит к снижению производительности системы.
5.2 Избегайте изменчивости
Это происходит непосредственно изфункциональное программирование, а для ООП объявление должно избегать изменений класса и состояния. В двух словах, это означает отказ от методов установки и наличие закрытых конечных полей во всех классах моделей. Единственный раз, когда их значения меняются, это во время строительства. Таким образом, вы можете быть уверены, что не возникнет конфликтов и что доступ к свойствам объекта всегда будет предоставлять правильное значение.
5.3 Запись ключевых данных
Оцените, где ваша программа может быть аномальной, и предварительно запишите все ключевые данные. В случае возникновения ошибки вы будете рады получить информацию о том, какие запросы были получены, и лучше понять, почему ваше приложение не работает. Еще раз обратите внимание, что ведение журнала вводит дополнительный файловый ввод-вывод, который может серьезно повлиять на производительность вашего приложения, поэтому не злоупотребляйте ведением журнала.
5.4 Повторное использование существующих реализаций
Всякий раз, когда вам нужно создать свой собственный поток (например, сделать асинхронные запросы к другой службе), повторно используйте существующую реализацию безопасности вместо создания собственного решения. Во многом это означает использованиеExecutorServicesи краткий функционал Java 8CompletableFuturesчтобы создать нить. Весна также позволяетDeferredResultкласс для асинхронной обработки запросов.
6. Распространенная ошибка № 6: не использовать проверку на основе аннотаций
Предположим, нашему предыдущему сервису TopTalent нужна конечная точка для добавления нового 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
не должно быть пустым,TopTalentData.name
не должно быть пустым иTopTalentData.name
имеет длину 10 символов) и выдает исключение, если данные недействительны.
Интеграция в SpringHibernate validator, проверка данных может быть выполнена более аккуратно. Давайте сначала рефакторим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, оно помечалось как действительное (isValid
в методеs == null
), если это дополнительное требование к свойству, используйте@NotNull
аннотация.
public class TopTalentData {
@MyAnnotation(value = 10)
@NotNull
private String name;
}
7. Распространенная ошибка № 7: (по-прежнему) использование конфигурации на основе XML
В то время как предыдущие версии Spring требовали XML, сегодня большая часть конфигурации может быть выполнена с помощью кода Java или аннотаций; конфигурация XML — это просто дополнительный шаблонный код, который не нужен.
В этой статье (и сопроводительном репозитории GitHub) используются аннотации для настройки Spring, и Spring знает, какие bean-компоненты подключать, поскольку каталог пакета верхнего уровня для сканирования уже находится в@SpringBootApplication
Объявление делается в составной аннотации следующим образом:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Составные аннотации (доступны черезВесенняя документацияУзнайте больше) просто намекните Spring, какие пакеты следует сканировать для получения bean-компонентов. В нашем случае это означает, что этот пакет верхнего уровня (co.kukurin) будет использоваться для получения:
-
@Component
(TopTalentConverter
,MyAnnotationValidator
) -
@RestController
(TopTalentController
) -
@Repository
(TopTalentRepository
) -
@Service
(TopTalentService
) Добрый
если у нас есть дополнительные@Configuration
Аннотируйте классы, которые также проверяют конфигурацию на основе Java.
8. Распространенная ошибка 8: игнорирование профиля
При разработке на стороне сервера распространенной проблемой является различие между различными типами конфигурации, обычно рабочей конфигурацией и конфигурацией разработки. Вместо того, чтобы вручную заменять различные элементы конфигурации каждый раз, когда вы переключаетесь с тестирования на развертывание приложения, более эффективно использовать профили.
Рассмотрим ситуацию, когда вы используете базу данных в памяти для локальной разработки и базу данных MySQL в рабочей среде. По сути, это означает, что вам нужно использовать разные URL-адреса и (надеюсь) разные учетные данные для доступа к обоим. Давайте посмотрим, как это можно сделать с этими двумя разными профилями:
8.1 Файл 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:
8.2 Файл APPLICATION-DEV.YAML
spring.datasource.url: 'jdbc:h2:mem:'
spring.datasource.platform: h2
Предполагая, что вы не хотите случайно что-либо делать с производственной базой данных при изменении кода, имеет смысл сделать файл конфигурации по умолчанию dev. Затем на сервере вы можете предоставить-Dspring.profiles.active=prod
Аргументы JVM для ручного переопределения файлов конфигурации. Кроме того, для переменных среды операционной системы можно установить желаемый профиль по умолчанию.
9. Распространенная ошибка 9: Недопустимое внедрение зависимостей
Правильное использование впрыска зависимостей весны, позволяющее разрешать его проволоку все объекты вместе, сканируя все необходимые классы конфигурации; это полезно для развязки отношений и облегчает тестирование, а не благодаря герметичности между классами муфта, чтобы сделать такие вещи:
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;
}
}
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. Распространенная ошибка № 10: отсутствие тестирования или неправильное тестирование
Хотя концепция модульного тестирования существует уже давно, многие разработчики, похоже, либо «забывают» об этом (особенно если это «не требуется»), либо просто добавляют его постфактум. Это явно нежелательно, так как тесты должны не только проверять правильность кода, но и служить документацией того, как программа должна вести себя в разных сценариях.
При тестировании веб-служб редко выполняются «чистые» модульные тесты, потому что для связи по HTTP часто требуется вызов SpringDispatcherServlet
, и посмотреть, когда фактическое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
, в то время как все остальные классы являются стандартными конфигурациями, выведенными путем сканирования каталогов подпакетов пакета, в котором находится класс приложения.RestAssuredMockMvc
просто используется для настройки облегченной среды, и/toptal/get
Конечная точка отправляетGET
просить.
11. Станьте мастером весны
Spring — это мощный фреймворк, который легко освоить, но для полного освоения требуются определенные инвестиции и время. Если вы потратите время на ознакомление с фреймворком, это определенно повысит вашу продуктивность в долгосрочной перспективе и, в конечном счете, поможет вам писать более чистый код и стать лучшим разработчиком.
Чтобы найти больше ресурсов,Spring In ActionЭто отличная практическая книга, охватывающая многие основные темы Spring.
Оригинал: https://www.toptal.com/spring/top-10-most-common-spring-framework-mistakes
автор:Toni Kukurin
Переводчик: Ваньсян