Гайд по 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
.
Что почитать?
- Учитесь пользоваться Spring Initializr
- Читайте более подробный гайд на английском
Наш аналог 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
Что почитать?
- Определение аннотации @SpringBootApplication
- Список того, что можно писать в application.properties
Отображаем 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>
Что почитать?
- Википедия про архитектурный паттерн MVC
- Документация на Spring MVC
- Документация на Thymeleaf
Безопасность
В 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();
}}
Это плохой пример (люблю давать плохие примеры!), который позволяет соединиться со всеми эндпоинтами без настройки юзернеймов и паролей. Настраивать безопасность по-нормальному - это не на один час работа, и дальше поиск вам в помощь. Когда-нибудь я напишу про это отдельные статьи.
Что почитать?
- Документация на Spring Security
Сохраняем данные в базе данных
У любой уважающей себя хозяйки есть пакет с пакетами. Кроме того, я из Санкт-Петербурга, если вы понимаете, о чём я. Иногда пакетов становится так много, что для управления ими нужен отдельный софт. Давайте заведем сущность 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=
Что почитать?
- Документация на Spring Data
Пишем 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
, для выяснения замысла пришлось бы читать код.
Что почитать?
- Подробный официальный гайд про создание REST API
Обрабатываем ошибки
Аннотация @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
Что почитать?
- Гайд про обработку ошибок
- Описание аннотации @ControllerAdvice
Тестирование
А что, кто-то вообще тестирует? Эммм. Тем не менее, даже если вы не написали в жизни ни одного автотеста, эта информация может оказаться полезной в будущем.
Аннотация @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());
}
Дальше такие тесты нужно написать на методы создания, удаления, обновления и всевозможные их комбинации. Если бы мы хотели написать здесь все тесты для круда, статья превратилась бы в сплошную стену текста из однотипных операций. Возможно, это тема для какого-то отдельного гайда, который мы напишем потом.
Что почитать?
- Сайт проекта REST-assured
- Официальный гайд по тестированию веба в Spring
Резюме
Это было краткое введение в Spring Boot, предназначенное для чтения по диагонали. Дальше вам нужно углубиться в подробности по каждой из этих тем, на что могут уйти дни или даже месяцы. Тем не менее, основа для этого большого путешествия - заложена.
Не забывайте подписаться на наши ресурсы, там есть ништяки:
- CodCraft - Youtube-канал от автора этого гайда
- Оправдания от Олега - Telegram-чат автора (общий, про всё на свете)
- Javawatch - Telegram-канал про Java
- Telegram-канал Failover Bar - единственный в Санкт-Петербурге (а может, и в России вообще) бар для разработчиков. Мы здесь постоянно встречаемся и разговариваем про Java.