[Перевод] 10 самых распространенных ошибок при работе с платформой Spring. Часть 2

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

-iva66o8uvrummnptqda3getv_i.png

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[] payload() default {};

    int value();

}

@Component
public class MyAnnotationValidator implements ConstraintValidator {

    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.

На этом перевод данной статьи подошел к концу.
Читать первую часть.

© Habrahabr.ru