Открытый урок «Создание REST-клиентов на Spring»

    И снова доброго времени суток! Совсем скоро у нас стартует обучение очередной группы «Разработчик на 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 git@github.com: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 git@github.com: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, который состоится совсем скоро.

    И, как обычно, ждём ваших комментариев к прошедшему открытому уроку!
    Отус
    534,00
    Профессиональные онлайн-курсы для разработчиков
    Поделиться публикацией

    Комментарии 7

      +1
      Вау!
      И потом мы удивляемся откуда на собеседования приходят кучи программистов-недоучек.
      Оказывается их так учат.
      Вызов репозиториев из контроллеров, пустые тесты, которые повесят сборку приложения, бизнес-логика прямо в main.
      И не надо про то, что это демка и все такое.
      Студентам надо показывать как делать правильно.
        0
        Вызов репозиториев из контроллеров

        — а чем плох вызов репозитория из контроллера? Слишком мало кода получится? Не «энтерпрайзно» выйдет?
          0
          Да, не энтерпрайзно. Слоеный пирог из уровней приложения не зря придумали.
          Да, можно накатать и оно даже будет работать. Первое время.
          А потом, во время саппорта, баги будут сыпаться как из рога изобилия и ни протестировать это нельзя нормально, ни пофиксать без рефакторинга.
          А потом, в один прекрасный момент, придут интеграторы и скажут, что теперь у нас не REST, а другой протокол общения. Ну, там, SQS, например.
          И вот тут-то самое интересное и начнется.
            +2
            А потом

            — живите настоящим, за такое «а потом», заложенное в код, обыватели джаву считают сложной и не подходящей для простых проектов, типа web-сайт. Зачем плодить сложность («бритва Оккама и т п»), когда мы ничего не знаем о проекте? Если бы речь шла о курсе по архитектуре, тогда да, имело бы смысл рассмотреть «правильную» архитектуру (с оговорками зачем тот или иной слой нужен), в других курсах это только забивает слушателям голову мусором и создает ощущение непостижимости происходящего.
        0
        Кстати, исправьте javascript на java. И в блогах и в тагах.
        Это, таки, не одно и тоже
          0
          Воу. Спасибо за внимательность, поправил.
          0
          Спасибо большое за ваш пост!

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое