Создание асинхронных методов
Этот урок освещает пошагово процесс создания вами асинхронных запросов к Facebook. Основное внимание уделено асинхронной части, широко используемой возможности при масштибировании сервисов.
Что вы создадите
Вы создадите сервис поиска, который запрашивает Facebook страницы и
получает данные через Facebook Graph API. одним из подходов к масштабированию
сервисов является запуск дорогостоящих заданий в фоне и ожидание результатов
их работы, используя Java интерфейс
Future
.
По сути, это контейнер, используемый для хранения потенциальных результатов.
Он дает вам методы для проверки, если результаты уже есть, то можно их использовать.
Что вам потребуется
- Примерно 15 минут свободного времени
- Любимый текстовый редактор или IDE
- JDK 6 и выше
- Gradle 1.11+ или Maven 3.0+
- Вы также можете импортировать код этого урока, а также просматривать web-страницы прямо из Spring Tool Suite (STS), собственно как и работать дальше из него.
Как проходить этот урок
Как и большинство уроков по Spring, вы можете начать с нуля и выполнять каждый шаг, либо пропустить базовые шаги, которые вам уже знакомы. В любом случае, вы в конечном итоге получите рабочий код.
Чтобы начать с нуля, перейдите в Настройка проекта.
Чтобы пропустить базовые шаги, выполните следующее:
- Загрузите и
распакуйте архив с кодом этого урока, либо кнонируйте из репозитория с помощью
Git:
git clone https://github.com/spring-guides/gs-async-method.git
- Перейдите в каталог
gs-async-method/initial
- Забегая вперед, создайте представления страницы
Когда вы закончите, можете сравнить получившийся результат с образцом в gs-async-method/complete
.
Настройка проекта
Для начала вам необходимо настроить базовый скрипт сборки. Вы можете использовать любую систему сборки, которая вам нравится для сборки проетов Spring, но в этом уроке рассмотрим код для работы с Gradle и Maven. Если вы не знакомы ни с одним из них, ознакомьтесь с соответсвующими уроками Сборка Java-проекта с использованием Gradle или Сборка Java-проекта с использованием Maven.
Создание структуры каталогов
В выбранном вами каталоге проекта создайте следующую структуру каталогов; к примеру,
командой mkdir -p src/main/java/hello
для *nix систем:
└── src └── main └── java └── hello
Создание файла сборки Gradle
Ниже представлен начальный файл сборки Gradle. Файл pom.xml находится здесь. Если вы используете Spring Tool Suite (STS), то можете импортировать урок прямо из него.
pom.xml
, вы найдете, что указана версия для maven-compiler-plugin.
В общем, это не рекомендуется делать. В данном случае он предназначен для решения проблем с нашей CI системы,
которая по умолчанию имеет старую(до Java 5) версию этого плагина.
build.gradle
buildscript {
repositories {
maven { url "http://repo.spring.io/libs-release" }
mavenLocal()
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:1.1.8.RELEASE")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'spring-boot'
jar {
baseName = 'gs-async-method'
version = '0.1.0'
}
repositories {
mavenLocal()
mavenCentral()
maven { url "http://repo.spring.io/libs-release" }
}
dependencies {
compile("org.springframework.boot:spring-boot-starter")
compile("org.springframework:spring-web")
compile("com.fasterxml.jackson.core:jackson-databind")
testCompile("junit:junit")
}
task wrapper(type: Wrapper) {
gradleVersion = '1.11'
}
Spring Boot gradle plugin предоставляет множество удобных возможностей:
- Он собирает все jar'ы в classpath и собирает единое, исполняемое "über-jar", что делает более удобным выполнение и доставку вашего сервиса
- Он ищет
public static void main()
метод, как признак исполняемого класса - Он предоставляет встроенное разрешение зависимостей, с определенными номерами версий для соответсвующих Spring Boot зависимостей. Вы можете переопределить на любые версии, какие захотите, но он будет по умолчанию для Boot выбранным набором версий
Создание представления страницы
Перед тем, как вы создадите сервис поиска к Facebook, вам необходимо описать представление для данных, которые вы получите через Facebook Graph API.
Чтобы смоделировать представление страницы, вы создаете класс представления.
Он представляет собой POJO с полями, конструкторами и методами доступа к
name
и website
:
src/main/java/hello/Page.java
package hello;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown=true)
public class Page {
private String name;
private String website;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getWebsite() {
return website;
}
public void setWebsite(String website) {
this.website = website;
}
@Override
public String toString() {
return "Page [name=" + name + ", website=" + website + "]";
}
}
Spring использует библиотеку Jackson JSON
для конвертации JSON ответа в Page
объект. аннотация @JsonIgnoreProperties
говорит Spring игнорировать любые атрибуты, которые не определены в классе. Это делает его простым
для создания REST вызовов и создания доменных объектов.
В этом уроке в качестве демонстрации мы будем использовать только name
и website
.
Создание сервиса поиска в Facebook
Следующим шагом вам необходимо создать сервис запросов к Facebook для поиска страниц.
src/main/java/hello/FacebookLookupService.java
package hello;
import java.util.concurrent.Future;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class FacebookLookupService {
RestTemplate restTemplate = new RestTemplate();
@Async
public Future<Page> findPage(String page) throws InterruptedException {
System.out.println("Looking up " + page);
Page results = restTemplate.getForObject("http://graph.facebook.com/" + page, Page.class);
Thread.sleep(1000L);
return new AsyncResult<Page>(results);
}
}
Класс FacebookLookupService
использует Spring RestTemplate
для вызова удаленного REST сервиса(graph.facebook.com), а затем конвертации ответа в
Page
объект.
Класс помечен аннотацией Service
, делая его кандидатом для сканирования
компонентов в Spring для обнаружения и помещения его в
контекст приложения.
Метод findPage
отмечен Spring аннотацией @Async
, означая, что он
будет запущен в отдельном потоке. Метод возвращает тип Future<Page>
вместо Page
, как требование любого асинхронного сервиса.
FacebookLookupService
НЕ позволит методу findPage
запускаться асинхронно. Он должен
быть создан внутри @Configuration
класса и подхвачен @ComponentScan
.
Время ответа Facebook Graph API может сильно различаться. Для демонстрации позднего ответа в этом уроке добавлена задержка в одну секунду.
Создание приложения исполняемым
Чтобы запустить пример, вам необходимо создать исполняемый jar. Spring аннотация
@Async
работает в web-приложениях, но вам нет необходимости в
дополнительных шагах по установке web-контейнера, чтобы увидеть её преимущества.
src/main/java/hello/Application.java
package hello;
import java.util.concurrent.Future;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
@Configuration
@EnableAsync
@EnableAutoConfiguration
@ComponentScan
public class Application implements CommandLineRunner {
@Autowired
FacebookLookupService facebookLookupService;
@Override
public void run(String... args) throws Exception {
// Start the clock
long start = System.currentTimeMillis();
// Kick of multiple, asynchronous lookups
Future<Page> page1 = facebookLookupService.findPage("PivotalSoftware");
Future<Page> page2 = facebookLookupService.findPage("CloudFoundry");
Future<Page> page3 = facebookLookupService.findPage("SpringFramework");
// Wait until they are all done
while (!(page1.isDone() && page2.isDone() && page3.isDone())) {
Thread.sleep(10); //millisecond pause between each check
}
// Print results, including elapsed time
System.out.println("Elapsed time: " + (System.currentTimeMillis() - start));
System.out.println(page1.get());
System.out.println(page2.get());
System.out.println(page3.get());
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Метод main()
передает управление вспомогательному классу
SpringApplication
, предоставляя Application.class
как аргумент его run()
методу. Это говорит Spring о том, чтобы
прочитать аннотацию метаданных из Application
и управлять им
как компонентом в Spring Application Context.
Аннотация @ComponentScan
сообщает Spring о запуске рекурсивного
поиска в пакете hello
и потомках классов, отмеченных прямо или
косвенно Spring аннотацией @Component
. При этом гарантируется,
что Spring найдет и зарегистрирует FacebookLookupService
,
потому что он отмечен @Service
, что в свою очередь является
своего рода @Component
аннотацией.
@EnableAsync
аннотация подключает возможность Spring запускать @Async
методы в
фоновом пуле потоков.
@EnableAutoConfiguration
аннотация переключает на доступные по умолчанию настройки, основанные на
содержимом вашего classpath. К примеру, он ищет любые классы, которые реализуют
интерфейс CommandLineRunner
и вызывает его метод run()
.
В данном случае, он запускает демонстрационный код этого урока.
Сборка исполняемого JAR
Вы можете собрать единый исполняемый JAR-файл, который содержит все необходимые зависимости, классы и ресурсы. Это делает его легким в загрузке, версионировании и развертывании сервиса как приложения на протяжении всего периода разработки, на различных средах и так далее.
./gradlew build
Затем вы можете запустить JAR-файл:
java -jar build/libs/gs-async-method-0.1.0.jar
Если вы используете Maven, вы можете запустить приложение, используя mvn spring-boot:run
,
либо вы можете собрать приложение с mvn clean package
и запустить JAR примерно так:
java -jar target/gs-async-method-0.1.0.jar
Если вы используете Gradle, вы можете запустить ваш сервис из командной строки:
./gradlew clean build && java -jar build/libs/gs-async-method-0.1.0.jar
mvn clean package && java -jar target/gs-async-method-0.1.0.jar
.
Как вариант, вы можете запустить ваш сервис напрямую из Gradle примерно так:
./gradlew bootRun
mvn spring-boot:run
.
Выходные данные отображают каждый запрос к Facebook. Каждый Future
результат проверяется до тех пор, пока они все не выполнятся, а потом напечатаются
результаты с общим количеством потраченного времени.
Looking up PivotalSoftware Looking up CloudFoundry Looking up SpringFramework Elapsed time: 1298 Page [name=Pivotal, website=http://www.gopivotal.com] Page [name=Cloud Foundry, website=http://www.cloudfoundry.com] Page [name=Spring Framework, website=null]
Сравните, как долго он будет работать не асинхронно, просто закомментировав
аннотацию @Async
и запустив сервис снова. Сумма потраченного
времени должна заметно увеличиться, потому что каждый запрос требует, как
минимум секунду.
По факту, чем больше задачи работают и чем больше задач запускается одновременно,
тем больше преимуществ вы увидите, делая их асинхронными. Компромиссом является
обращение к Future
интерфейсу. Он добавляет слой абстракции, поэтому
вы не работаете непосредственно с результатами, а опрашиваете на их наличие. Если
существуют несколько вызываемых методов, где каждый из них связан с предыдущим в
синхронной манере, то конвертация в асинхронный подход потребует синхронизации
результатов. Но эта дополнительная работа может быть необходима, только если
асинхронные методы вызываются для решения критической проблемы масштабирования.
Итог
Поздравляем! Вы только что разработали асинхронный сервис, который позволяет масштабировать несколько вызовов одновременно.
С оригинальным текстом урока вы можете ознакомиться на spring.io.
comments powered by Disqus