И снова доброго времени суток! Совсем скоро у нас стартует обучение очередной группы «Разработчик на Spring Framework», в связи с чем мы провели открытый урок, что стало уже традицией в преддверии запуска. На этом вебинаре говорили о разработке REST-клиентов с помощью Spring, а также детально узнали о таких технологиях, как Spring Cache, Spring Retry и Hystrix.
Преподаватель: Юрий Дворжецкий — тренер в Luxoft Training Center, ведущий разработчик, кандидат физико-математических наук.
Вебинар посетила совершенно разная аудитория, оценившая свои знания по Spring в пределах 0-6 баллов по 10-бальной шкале, однако, судя по отзывам, открытый урок показался полезным даже опытным пользователям.
Пару слов о Spring 5
Как известно, Spring Framework является универсальным и довольно популярным фреймворком для Java-платформы. Spring состоит из массы подпроектов или модулей, что позволяет решать множество задач. По сути, это большая коллекция «фреймворков во фреймворке», вот, например, лишь некоторые из них:
- Spring IoC + AOP = Context,
- Spring JDBC,
- Spring ORM,
- Spring Data (это целый набор подпроектов),
- Spring MVC, Spring WebFlux,
- Spring Security,
- Spring Cloud (это ещё более огромный набор подпроектов),
- Spring Batch,
- Spring Boot.
Spring заменяет конфигурированием программирование некоторых задач, однако конфигурирование иногда превращается просто в кошмар. Для быстрого создания production-grade приложений как раз и используют Spring Boot. Это специальный фреймворк, который содержит набор стартеров (‘starter’), упрощающих настройку Spring-фреймворков и других технологий.
Чтобы показать некоторые особенности работы Spring, прекрасно подходит тема блокировки сайтов, так как это сейчас модно)). Если хотите активно поучаствовать в уроке и попрактиковаться, рекомендуется скачать репозиторий с кодом сервера, который предложил преподаватель. Используем следующую команду:
git clone [email protected]:ydvorzhetskiy/sb-server.git
Далее просто запускаем, например, так:
mvnw spring-boot:run
Самым большим достижением Spring Boot является возможность запустить сервер простым запуском Main-класса в IntelliJ IDEA.
В файле BlockedSite.java находится наш исходный код:
package ru.otus.demoserver.domain;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class BlockedSite {
@Id
@GeneratedValue
private int id;
private String url;
А вот содержимое контроллера BlockedSitesController.java:
package ru.otus.demoserver.rest;
@RestController
public class BlockedSitesController {
private final Logger logger = LoggerFactory.getLogger(BlockedSitesController.class);
private final BlockedSitesRepository repository;
public BlockedSitesController(BlockedSitesRepository repository) {
this.repository = repository;
}
@GetMapping("/blocked-sites")
public List<BlockedSite> blockedSites() {
logger.info("Request has been performed");
return repository.findAll();
}
}
Также обратите внимание на вложенную БД в pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>ru.otus</groupId>
<artifactId>demo-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<url>demo-server</url>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Теперь простым и незатейливым образом сохраняем в нашу БД через репозиторий два заблокированных сайта (DemoServerApplication.java):
package ru.otus.demoserver;
@SpringBootApplication
public class DemoServerApplication {
public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(DemoServerApplication.class, args);
BlockedSitesRepository repository = ctx.getBean(BlockedSitesRepository.class);
repository.save(new BlockedSite("https://telegram.org/"));
repository.save(new BlockedSite("https://azure.microsoft.com/"));
}
}
Осталось запустить сервер с помощью Spring Boot и открыть соответствующий урл на локальном хосте (localhost:8080/blocked-sites)
. При этом наш сервер будет нам возвращать список заблокированных нами сайтов, то есть те сайты, которые мы добавили через БД.
Что же, пришла пора писать клиента к этому серверу. Но прежде чем к этому перейти, нужно кое-что вспомнить.
Теоретическое отступление
Давайте перечислим некоторые HTTP-методы (глаголы):
- GET — получение entity или списка;
- POST — создание entity;
- PUT — изменение entity;
- PATCH — изменение entity (RFC-…);
- DELETE — удаление entity;
- HEAD, OPTIONS — «хитрые» методы для поддержки HTTP-протокола и вообще REST-сервисов;
- TRACE — устаревший метод, который не используется.
Нельзя не вспомнить и про такое важное свойство, как идемпотентность. Говоря простым языком, сколько бы раз вы не применяли операцию, её результат будет один и тот же, как если бы вы применили её всего один раз. Например, вы поздоровались с утра с человеком, сказав ему «Привет!» В результате ваш знакомый переходит в состояние «поздорованный» :-). И если вы ещё несколько раз в течение дня ему скажете «Привет!», ничего не изменится, он останется в том же состоянии.
А теперь, давайте подумаем, какие из вышеперечисленных HTTP-методов идемпотентны? Конечно, подразумевается, что вы соблюдаете семантику. Если не знаете, то подробнее об этом рассказывает преподаватель, начиная с 26-й минуты видео.
REST
Для того чтобы писать REST-контроллер, нужно вспомнить, что такое REST:
- REST — REpresentational State Transfer;
- это архитектурный стиль, а не стандарт;
- это, по сути, набор принципов-ограничений;
- REST был давно, но термин появился сравнительно недавно;
- Web-приложение в стиле REST называется RESTful, его API в таком случае — RESTful API (антоним — Stateful);
- REST-ом сейчас называют всё что хотят…
Во-первых, если говорить о взаимодействии в виде клиент-сервер, то его нужно строить в виде запрос-ответ. Да, не всегда взаимодействие так строится, но сейчас такое взаимодействие крайне распространено, а для веб-приложений что-то другое выглядит совсем странно. А вот, например, веб-сокеты — это как раз не REST.
Во-вторых, самое важное ограничение в REST — отсутствие состояния клиента на сервере. Предполагается, что серверу клиент всегда передаёт всё необходимое состояние с каждым запросом, то есть состояние сохраняется на стороне клиента, и нет никаких сессий на сервере.
Как писать клиента на Spring
Для продолжения работы рассмотрим и запустим клиента (используем ссылку на репозиторий):
git clone [email protected]:ydvorzhetskiy/sb-client.git
mvnw spring-boot:run
Это уже написанный клиент и консольное приложение, а не веб-сервер.
Смотрим зависимости:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>ru.otus</groupId>
<artifactId>demo-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
<url>demo-client</url>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Это для RestTemplate, это ещё не веб-приложение -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.1.4.RELEASE</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.9.8</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.9.8</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.8</version>
</dependency>
<!-- Cache -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Retry -->
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
<!-- Hystrix -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-javanica</artifactId>
<version>1.5.12</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
У клиента есть конфигурация:
1. RestTemplateConfig.java
package ru.otus.democlient.config;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder
.setConnectTimeout(Duration.ofSeconds(2))
.setReadTimeout(Duration.ofSeconds(3))
.build();
}
2. CacheConfig.java
package ru.otus.democlient.config;
@Configuration
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("sites");
}
}
А вот содержимое файла SiteServiceRest.java:
package ru.otus.democlient.service;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.Collections;
import java.util.List;
@Service
public class SiteServiceRest implements SiteService {
private final RestTemplate restTemplate;
private final String serverUrl;
public SiteServiceRest(
RestTemplate restTemplate, @Value("${application.server.url}") String serverUrl
) {
this.restTemplate = restTemplate;
this.serverUrl = serverUrl;
}
@Override
public List<SiteInfo> findAllBlockedSites() {
return restTemplate.exchange(
serverUrl + "/blocked-sites",
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<SiteInfo>>() {
}
).getBody();
}
public List<SiteInfo> getDefaultSites() {
return Collections.singletonList(new SiteInfo() {{
setUrl("http://vk.com/");
}});
}
}
Слегка подрезюмируем:
- Запросы делаются через RestTemplate.
- RestTemplate можно настраивать, и это обычный бин.
- Jackson используется для маппинга JSON в объекты.
- Дальше – только ваш полёт фантазии (подробности о запуске клиента есть в видео).
Коллеги, вебинар получился очень содержательным, поэтому, чтобы ничего не пропустить, лучше смотрите его полностью. Вы попробуете «в боевых условиях» реальное API, добавите @Cacheable
на сервис, поработаете со Spring Retry, узнаете о Hystrix и много чего ещё. Также мы приглашаем вас на очередной вебинар по Spring.