Салют. Вот и подоспел перевод второй части статьи, подготовленной специально для студентов курса «Разработчик на 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, но для этого примера давайте представим, что аннотации не существует. Вы можете создать средство проверки данных, проверяющее длину строки, создав два дополнительных класса: один — для валидации, а другой — для аннотирования свойств:
@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
), а затем использовать аннотацию , если это дополнительно требуется для данного свойства:
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
Если у нас есть дополнительные классы, имеющие аннотацию , они также будут проверены на наличие 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
конечной точке . get
Используйте Spring на профессиональном уровне
Spring — мощная платформа, с которой легко начать работу, но для ее полного освоения требуются время и некоторые усилия. Если вы потратите время на то, чтобы хорошо ознакомиться с этой платформой, в конечном итоге это, несомненно, поднимет эффективность вашей работы, поможет вам создавать более чистый код и повысит вашу квалификацию как разработчика.
Рекомендую обратить внимание на Spring In Action — это хорошая, ориентированная на практическое применение книга, в которой рассмотрено множество важных тем, связанных с платформой Spring.
На этом перевод данной статьи подошел к концу.
Читать первую часть.
Автор: MaxRokatansky