Салют. Вот и подоспел перевод второй части статьи, подготовленной специально для студентов курса «Разработчик на Spring Framework». Первую часть можно прочитать тут.
Spring — пожалуй, одна из самых популярных платформ разработки на языке Java. Это мощный, но довольно сложный в освоении инструмент. Его базовые концепции довольно легко понять и усвоить, но для того чтобы стать опытным разработчиком на Spring, потребуются время и определенные усилия.
В этой статье мы рассмотрим некоторые из самых распространенных ошибок, совершаемых при работе в Spring и связанных, в частности, с разработкой веб-приложений и использованием платформы Spring Boot. Как отмечается на веб-сайте Spring Boot, в Spring Boot используется стандартизованный подход к созданию готовых к эксплуатации приложений, и данная статья будет придерживаться этого подхода. В ней будет дан ряд рекомендаций, которые можно эффективно использовать при разработке стандартных веб-приложений на базе Spring Boot. На тот случай, если вы не очень хорошо знакомы с платформой Spring Boot, но хотите поэкспериментировать с примерами, приведенными в статье, я создал GitHub-репозиторий с дополнительными материалами для этой статьи. Если в какой-то момент вы немного запутались, читая эту статью, я бы посоветовал вам создать клон этого репозитория и поэкспериментировать с кодом на своем компьютере.
Распространенная ошибка № 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) {
// выдача исключения
}
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) {
// обработка исключения валидации
}
Кроме того, нам нужно указать, валидацию какого свойства мы хотим выполнить в классе 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 знает, какие компоненты JavaBean необходимо привязать, так как корневой пакет аннотируется с помощью составной аннотации @SpringBootApplication — вот так:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Эта составная аннотация (подробнее о ней см. в документации по Spring) просто указывает платформе Spring, какие пакеты нужно просканировать для извлечения компонентов JavaBean. В нашем конкретном случае это означает, что для привязки будут использоваться следующие подпакеты co.kukurin:
- Component (TopTalentConverter, MyAnnotationValidator)
- @RestController (TopTalentController)
- Repository (TopTalentRepository)
- Service (TopTalentService) classes
Если у нас есть дополнительные классы, имеющие аннотацию @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:
ФАЙЛ APPLICATION-DEV.YAML
spring.datasource.url: 'jdbc:h2:mem:'
spring.datasource.platform: h2
Надо полагать, что, работая с кодом, вы бы не хотели совершить некие случайные действия с базой данных, предназначенной для среды эксплуатации, поэтому имеет смысл в качестве профиля по умолчанию выбрать профиль для среды разработки (DEV). Впоследствии на сервере можно вручную переопределить профиль конфигурации, указав для JVM параметр -Dspring.profiles.active=prod
. Кроме того, профиль конфигурации, который следует использовать по умолчанию, можно указать в переменной среды операционной системы.
Распространенная ошибка № 9. Неиспользование механизма внедрения зависимостей
Правильное использование механизма внедрения зависимостей в Spring означает, что 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;
}
}
В лекции Мишко Хевери на канале Google Tech Talks подробно объясняется, зачем следует использовать внедрение зависимостей, а здесь мы посмотрим, как этот механизм используется на практике. В разделе, посвященном разделению ответственности («Распространенная ошибка № 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 })
Впоследствии это позволит нам использовать контекстную конфигурацию для внедрения пользовательского компонента JavaBean в модульный тест.
Распространенная ошибка № 10. Недостаток тестирования или неправильное тестирование
Несмотря на то что идея модульного тестирования отнюдь не нова, кажется, что многие разработчики либо «забывают» о нем (особенно если оно не является обязательным), либо проводят его слишком поздно. Очевидно, что это неправильно, потому что тесты не только позволяют проверить правильность работы кода, но и служат в качестве документации, показывающей, как приложение должно вести себя в различных ситуациях.
При тестировании веб-служб вы редко проводите исключительно «чистые» модульные тесты, так как для соединения по HTTP-протоколу обычно требуется задействовать сервлет Spring DispatcherServlet
и посмотреть, что происходит при получении реального запроса HttpServletRequest
(то есть это получается интеграционный тест, в котором используются валидация, сериализация и пр.). Элегантное и проверенное решение — использование REST Assured, Java-библиотеки для удобного тестирования 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 — мощная платформа, с которой легко начать работу, но для ее полного освоения требуются время и некоторые усилия. Если вы потратите время на то, чтобы хорошо ознакомиться с этой платформой, в конечном итоге это, несомненно, поднимет эффективность вашей работы, поможет вам создавать более чистый код и повысит вашу квалификацию как разработчика.
Рекомендую обратить внимание на Spring In Action — это хорошая, ориентированная на практическое применение книга, в которой рассмотрено множество важных тем, связанных с платформой Spring.
На этом перевод данной статьи подошел к концу.
Читать первую часть.
Автор: MaxRokatansky