И снова доброго времени суток! Совсем скоро у нас стартует обучение очередной группы «Разработчик на 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/");
	    	}});
        	}
}

Слегка подрезюмируем:

  1. Запросы делаются через RestTemplate.
  2. RestTemplate можно настраивать, и это обычный бин.
  3. Jackson используется для маппинга JSON в объекты.
  4. Дальше – только ваш полёт фантазии (подробности о запуске клиента есть в видео).

Коллеги, вебинар получился очень содержательным, поэтому, чтобы ничего не пропустить, лучше смотрите его полностью. Вы попробуете «в боевых условиях» реальное API, добавите @Cacheable на сервис, поработаете со Spring Retry, узнаете о Hystrix и много чего ещё. Также мы приглашаем вас на очередной вебинар по Spring.