Блог создается при поддержке Axiom JDK и Failover Bar

Гайд по Spring Boot — Твоё первое приложение

October 25, 2023 . 10 минут на чтение статьи

Spring Boot - самое популярное в мире дополнение к платформе Spring. Больше не нужно самому конфигурировать миллионы XML и аннотаций, и вместо этого довериться архитектурным решениям и значениям по умолчанию, которые придумали за тебя авторы Spring Boot. Она отлично подходит, чтобы начать новый Java-проект с минимальными усилиями и при этом сразу получить крутое приложение, готовое к работе.

Этот урок можно использовать как стартовую точка для того, чтобы разобраться со Spring Boot. Иначе говоря, это способ начать делать простое веб-приложение без лишних сложностей и заморочек.

Когда вы в первый раз изучаете Spring, вам нужно знать множество хитрых архитектурных паттернов, и новичку узнать о них негде. Не существует никакой книги или YouTube-ролика, в котором бы написали, как писать все приложения вообще. Spring Boot позволяет новичку быстро вкатиться в Spring: достаточно повторять за авторами Spring Boot, и вы уже сможете программировать на Java на довольно достойном уровне, за который платят деньги.

Мы поговорим о базовых настройках, оформлении интерфейса, манипуляциях с данными и даже о том, как ловить ошибки, если что-то пойдет не так. Мы не будем говорить о том, как установить Java, Maven, IDE, не будем учить синтаксису языка Java и формату файлов pom.xml. Только концентрированный смысл по одной теме: Spring Boot.

С чего начать

Часто туториалы по Java начинаются с Hello World в черной консоли, с ручного написания pom.xml, и тому подобной ненужной фигни. Обычно такие советы пишут преподаватели в вузах, которые сами не работали разработчиком ни дня.

Настоящие хакеры делают по-другому. Они идут в Spring Initializr и создают проект там. Проделайте это сейчас.

В сгенерированном коде мы увидим, что pom.xml полностью построен поверх специального стартера:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <relativePath />
</parent>

Самый простой набор зависимостей выглядит как-то так:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
</dependency>

Конечно, в зависимости от того, какие Dependencies вы натыкали в Spring Initializr, результаты могут разительно различаться. Изучать этот инструмент и играть с ним - сплошное удовольствие. Представьте, сколько времени вы бы потратили, самостоятельно вписывая этот мусор в pom.xml.

Что почитать?

Наш аналог HelloWorld

Простейшее приложение для Spring Boot похоже на обычный HelloWorld:

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Самое важное здесь - аннотация @SpringBootApplication. Именно с помощью аннотаций авторы Spring Boot доносят нам, какие именно архитектурные решения они считают правильными.

В данном случае, авторы Spring Boot показывают нам полезную практику группировки аннотаций: @SpringBootApplication - это всего лишь группа из @Configuration, @EnableAutoConfiguration, и @ComponentScan. Если каждый раз писать все эти аннотации вручную, код будет выглядеть заваленным мусором.

Дальше нужно создать текстовый файл application.properties, и вписать в него порт, на котором будет слушать наше приложение. Именно по этому порту можно будет открыть в браузере веб-интерфейс. Например, давайте поменяем обычный для Java порт 8080 на специальный 8082, чтобы школьники в интернете не догадались, что мы что-то запустили.

server.port=8082

Что почитать?

Отображаем HTML в браузере

Самостоятельно склеивать HTML из строчек - довольно нудная задача. Каждый, кто пытался заниматься вебом знает, что все в конце концов приходят к примерно одним и тем же идеям, поэтому писать свой шаблонизатор совершенно бессмысленно. Благо, в Java таких шаблонизаторов как у дурака - фантиков.

В Spring Boot у нас есть возможность взять один из готовых шаблонизаторов для HTML. Thymeleaf выгодно отличается тем, что специально разрабатывался для работы вместе со Spring.

Для включения Thymeleaf в проект, нужно добавить в pom.xml зависимость на стартер.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

Теоретически, подключить его можно и другими способами, но конкретно этот способ не требует никакой дополнительной конфигурации. Скопипастил себе в pom.xml и забыл.

Тем не менее мы можем кое-что поднастроить из application.properties:

spring.thymeleaf.cache=false
spring.thymeleaf.enabled=true
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html

spring.application.name=Spring Boot Hello World

Теперь мы можем вывести HTML, создав так называемый Контроллер (это термин Spring и архитектурный паттерн в MVC):

@Controller
public class SimpleController {
    @Value("${spring.application.name}")
    String appName;

    @GetMapping("/")
    public String homePage(Model model) {
        model.addAttribute("appName", appName);
        return "home";
    }
}

@GetMapping показывает, по какому пути в браузере мы будем показывать эту страничку ("/" означает корень сайта). С помощью model.addAttribute мы накидываем данные, которые потом можно будет отображать на странице. Возвращаемая строка - название шаблона, из которого будет рисоваться страничка. Создадим этот шаблон.

<html>
<head>
    <title>Превед</title>
</head>
<body>

<h1>Превед, медвед!</h1>
<p>
    Welcome to <span th:text="${appName}">Имя приложения</span>
</p>

</body>
</html>

Что почитать?

Безопасность

В Spring есть одна главная библиотека, делающая нам безопасно: Spring Security.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Spring Security начинает работать мгновенно после подключения зависимости, не надо писать никакого кода.

Мы знаем, что по-умолчанию используются стратегии httpBasic и formLogin. Тем не менее, безопасность требует вдумчивого отношения, и использовать Spring Security с настройками по-умолчанию имеет мало смысла. Вот как можно настроить его самостоятельно:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http)
throws Exception {

http.authorizeHttpRequests(expressionInterceptUrlRegistry ->
    expressionInterceptUrlRegistry
    .anyRequest()
    .permitAll())
.csrf(AbstractHttpConfigurer::disable);


return http.build();

}}

Это плохой пример (люблю давать плохие примеры!), который позволяет соединиться со всеми эндпоинтами без настройки юзернеймов и паролей. Настраивать безопасность по-нормальному - это не на один час работа, и дальше поиск вам в помощь. Когда-нибудь я напишу про это отдельные статьи.

Что почитать?

Сохраняем данные в базе данных

У любой уважающей себя хозяйки есть пакет с пакетами. Кроме того, я из Санкт-Петербурга, если вы понимаете, о чём я. Иногда пакетов становится так много, что для управления ими нужен отдельный софт. Давайте заведем сущность PlasticBag, которую будем сохранять в базе.

@Entity
public class PlasticBag {

    // Идентификатор для хранения в базе
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    // Красивое индивидуальное название,
    // которое можно написать на лейкопластыре и наклеить на пакет
    @Column(nullable = false, unique = true)
    private String title;

    // Если дома живет несколько человек - подписать хозяина
    @Column(nullable = false)
    private String author;
}

Чтобы сущность начала сохраняться в базе, недостаточно ее создать. Нужно сделать класс, который будет ей управлять.

И тут случатся волшебство. Spring Data позволяет нам не писать этот класс самостоятельно. Достаточно создать интерфейс с методами, у которых названия как бы намекают на то, что мы хотим сделать. Дальше Spring Data сама "напишет" реализацию этого интерфейса.

public interface PlasticBagRepository extends CrudRepository<PlasticBag, Long> {
    List<PlasticBag> findByTitle(String title);
}

Теперь нужно включить этот механизм в настройках приложения:

@EnableJpaRepositories("guru.oleg.spring.repo")
@EntityScan("guru.oleg.spring.model")
@SpringBootApplication
public class Application {
   // Муть, которую вы уже успели сюда написать...
}

Нужно положить разработанные нами классы по тому пути, который указан в аннотациях. Аннотация @EnableJpaRepositories указывает на место, где лежат репозитории. @EntityScan указывает на расположение сущностей.

Чтобы не разворачивать PostgreSQL и не тратить час на изучение, как создавать там пользователей и назначать им права, данные наших учебных приложений можно хранить прямо в оперативной памяти. Положите в application.properties следующую телегу:

spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:bootapp;DB_CLOSE_DELAY=-1
spring.datasource.username=sa
spring.datasource.password=

Что почитать?

Пишем API для REST

Большую часть жизни веб-разработчики пишут всевозможные круды. Это наиболее анскильная, бессмысленная и беспощадная часть работы... но тем не менее, абсолютно необходимая. Spring Boot позволяет нам мучиться с пробросом API чуть меньше, чем обычно.

@RestController
@RequestMapping("/api/bag")
public class PlasticBagController {

    @Autowired
    private PlasticBagRepository bagRepository;

    @GetMapping
    public Iterable findAll() {
        return bagRepository.findAll();
    }

    @GetMapping("/title/{bagTitle}")
    public List findByTitle(@PathVariable String bagTitle) {
        return bagRepository.findByTitle(bagTitle);
    }

    @GetMapping("/{id}")
    public PlasticBag findOne(@PathVariable Long id) {
        return bagRepository.findById(id)
          .orElseThrow(PlasticBagNotFoundException::new);
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public PlasticBag create(@RequestBody PlasticBag bag) {
        return bagRepository.save(bag);
    }

    @DeleteMapping("/{id}")
    public void delete(@PathVariable Long id) {
        bagRepository.findById(id)
          .orElseThrow(PlasticBagNotFoundException::new);
        bagRepository.deleteById(id);
    }

    @PutMapping("/{id}")
    public PlasticBag updateBag(@RequestBody PlasticBag bag, @PathVariable Long id) {
        if (bag.getId() != id) {
          throw new PlasticBagIdMismatchException();
        }
        bagRepository.findById(id)
          .orElseThrow(PlasticBagNotFoundException::new);
        return bagRepository.save(bag);
    }
}

В мудрости своей, авторы Spring Boot показывают, что чем меньше в коде мусора — тем лучше. В частности, @RestController — всего лишь синоним для @Controller + @ResponseBody. Можно было бы написать это самостоятельно, но не нужно. Кроме того, само название "RestController" имеет смысл, потому что мы с первых же строчек класса понимаем замысел его создателя. Если бы там была просто аннотация @Controller, для выяснения замысла пришлось бы читать код.

Что почитать?

Обрабатываем ошибки

Аннотация @ControllerAdvice позволяет обработать все проблемы в одном месте:

@ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler({ PlasticBagNotFoundException.class })
    protected ResponseEntity<Object> handleNotFound(
      Exception ex, WebRequest request) {
        return handleExceptionInternal(ex,
            "Пакет не найден. Возможно, ты в них не помещаешься, жиробас.",
          new HttpHeaders(), HttpStatus.NOT_FOUND, request);
    }

    @ExceptionHandler({ PlasticBagIdMismatchException.class,
      ConstraintViolationException.class,
      DataIntegrityViolationException.class })
    public ResponseEntity<Object> handleBadRequest(
      Exception ex, WebRequest request) {
        return handleExceptionInternal(ex, ex.getLocalizedMessage(),
          new HttpHeaders(), HttpStatus.BAD_REQUEST, request);
    }
}

Такая конфигурация позволяет нам иметь свои собственные исключения. Такие исключения не только индивидуально обрабатываются и красиво выглядят, но и внутри их классов можно творить грязь.

public class PlasticBagNotFoundException extends RuntimeException {
    public PlasticBagNotFoundException(String message, Throwable cause) {
        super(message, cause);
    }
}

Если хочется переназначить стандартный HTML и положить его на нестандартный URL (/error), то можно написать вот такой файл:

<html lang="en">
<head><title>Всё пропало</title></head>
<body>
    <h1>Ваше приложение помножено на ноль</h1>
    <b>[<span th:text="${status}">Статус</span>]
        <span th:text="${error}">Проблема</span>
    </b>
    <p th:text="${message}">Сообщение</p>
</body>
</html>

И прописать его в application.properties:

server.error.path=/error2

Что почитать?

Тестирование

А что, кто-то вообще тестирует? Эммм. Тем не менее, даже если вы не написали в жизни ни одного автотеста, эта информация может оказаться полезной в будущем.

Аннотация @SpringBootTest запускает приложение и проверяет, что оно хотя бы не упало.

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringContextTest {

    @Test
    public void contextLoads() {
    }
}

В Java есть проблема, что REST обычно отдает произвольные данные, а Java ни разу не динамический язык (типа Java Script). REST-assured — популярная либа, которая борется с этой проблемой, добавляя вкуса грязного JS прямо в нашу чистую святую Джаву.

Вначале эту либу нужно добавить в проект:

<dependency>
    <groupId>io.rest-assured</groupId>
    <artifactId>rest-assured</artifactId>
    <scope>test</scope>
</dependency>

Напишем тест, который честно подсоединяется к порту 8081 и что-то делает с приложением:

public class SpringBootHelloWorldTest {

    private static final String API_ROOT
      = "http://localhost:8081/api/bag";

    private PlasticBag createRandomBag() {
        PlasticBag bag = new PlasticBag();
        bag.setTitle(randomAlphabetic(15));
        bag.setAuthor(randomAlphabetic(20));
        return bag;
    }

    private String createBagAsUri(PlasticBag bag) {
        Response response = RestAssured.given()
          .contentType(MediaType.APPLICATION_JSON_VALUE)
          .body(bag)
          .post(API_ROOT);
        return API_ROOT + "/" + response.jsonPath().get("id");
    }
}

Вот как можно протестировать поиск:

@Test
public void whenGetAllBags_thenOK() {
    Response response = RestAssured.get(API_ROOT);

    assertEquals(HttpStatus.OK.value(), response.getStatusCode());
}

@Test
public void whenGetBagsByTitle_thenOK() {
    Bag bag = createRandomBag();
    createBagAsUri(bag);
    Response response = RestAssured.get(
      API_ROOT + "/title/" + bag.getTitle());

    assertEquals(HttpStatus.OK.value(), response.getStatusCode());
    assertTrue(response.as(List.class)
      .size() > 0);
}
@Test
public void whenGetCreatedBagById_thenOK() {
    Bag bag = createRandomBag();
    String location = createBagAsUri(bag);
    Response response = RestAssured.get(location);

    assertEquals(HttpStatus.OK.value(), response.getStatusCode());
    assertEquals(bag.getTitle(), response.jsonPath()
      .get("title"));
}

@Test
public void whenGetNotExistBagById_thenNotFound() {
    Response response = RestAssured.get(API_ROOT + "/" + randomNumeric(4));

    assertEquals(HttpStatus.NOT_FOUND.value(), response.getStatusCode());
}

Дальше такие тесты нужно написать на методы создания, удаления, обновления и всевозможные их комбинации. Если бы мы хотели написать здесь все тесты для круда, статья превратилась бы в сплошную стену текста из однотипных операций. Возможно, это тема для какого-то отдельного гайда, который мы напишем потом.

Что почитать?

Резюме

Это было краткое введение в Spring Boot, предназначенное для чтения по диагонали. Дальше вам нужно углубиться в подробности по каждой из этих тем, на что могут уйти дни или даже месяцы. Тем не менее, основа для этого большого путешествия - заложена.



Не забывайте подписаться на наши ресурсы, там есть ништяки:

  • CodCraft - Youtube-канал от автора этого гайда
  • Оправдания от Олега - Telegram-чат автора (общий, про всё на свете)
  • Javawatch - Telegram-канал про Java
  • Telegram-канал Failover Bar - единственный в Санкт-Петербурге (а может, и в России вообще) бар для разработчиков. Мы здесь постоянно встречаемся и разговариваем про Java.