Pull to refresh
1048.83
OTUS
Цифровые навыки от ведущих экспертов

Начало работы с микросервисами в Spring Boot

Reading time13 min
Views25K
Всем привет!

В этой статье мы продемонстрируем основные компоненты для создания RESTful микросервисов, используя реестр служб Consul, Spring Boot для всего скаффолдинга, инжекции зависимостей, Maven для сборки, а также Spring REST и Jersey/JaxRS API Java RESTful.

Основные преимущества микросервисов:

  • Микросервисы позволяют ослабить зацепленность вашего кода

  • Микросервисы позволяют различным командам работать над небольшими составляющими, используя независимые технологии, обеспечивая более безопасное и частое развертывание Spring Boot поддерживает различные реализации для создания REST API

  • Обнаружение и вызов сервисов не зависят от сервисной платформы

  • Swagger создает надежную документацию API и интерфейс вызова

Если вы еще не используете микросервисы, вы благополучно не успели в фазу ранних последователей на кривой восприятия технологии, и, вероятно, как раз самое время начинать.



За последние два десятилетия предприятие стало очень гибким в нашем SDLC-процессе, но наши приложения, как правило, по-прежнему остаются монолитными, с огромными jar-ами, поддерживающими все разнообразные API и версии на рынке. Но в настоящее время существует стремление к более Lean, DevOps-ным процессам, а функциональность становится «безсерверной». Рефакторинг в микросервисы может уменьшить зацепленность кода и ресурсов, сделать сборки меньше, релизы безопаснее, а API более стабильными.

В этой статье мы создадим простое приложение для управления портфелем на фондовом рынке, которое клиенты могут вызывать для оценки своего портфеля акций (биржевые тикеры и величины). Микросервис портфеля будет извлекать портфель клиента, отправлять его в микросервис ценообразования для применения последних цен, а затем возвращать полностью оцененный и субтотализированный портфель, демонстрируя все это посредством rest-вызова.



Прежде чем мы начнем работу над созданием наших микросервисов, давайте подготовим нашу среду, настроив Consul.

Загрузите Consul


Мы будем использовать Hashicorp Consul для обнаружения сервисов, поэтому перейдите на www.consul.io/downloads.html и загрузите Consul для Windows, Linux, Mac и т.д. Это предоставит вам исполняемый файл, который нужно добавить к своему пути.

Запустите Consul


В командной строке запусктие Consul в режиме dev:

consul agent -dev

Чтобы убедиться, что он запущен, перейдите в браузер и получите доступ к интерфейсу консула http://localhost:8500. Если все будет хорошо, консул должен сообщить, что он жив и здоров. Нажав на службу консула (слева), вы получите дополнительную информацию (справа).



Если на данный момент есть какие-либо проблемы, убедитесь, что вы добавили Consul к пути выполнения, и доступны порты 8500 и 8600.

Создайте приложение SpringBoot


Мы будем использовать Spring Initializr, который интегрирован в большинство IDE, для скаффолдинга наших SpringBoot приложений. Скриншоты ниже используют IntelliJ IDEA.

Выберите «File/New Project», чтобы открыть новый шаблон проекта, и затем «Spring Initializr».



Вообще, вы можете настроить скаффолдинг без IDE, заполнив онлайн-форму через веб-страницу SpringBoot Initializr start.spring.io, которая создаст zip-файл вашего пустого проекта, готовый для загрузки.

Нажмите «Next» и заполните метаданные проекта. Используйте следующую конфигурацию:



Нажмите «Next», чтобы выбрать зависимости, и введите «Jersey» и «Consul Discovery» в поиске зависимостей. Добавьте эти зависимости:



Нажмите «Next», чтобы указать название проекта и его расположение. Сохраните имя по умолчанию «portfolio» и укажите предпочтительное расположение проекта, затем нажмите «finish», чтобы создать и открыть проект:



Мы можем использовать сгенерированные application.properties, но SpringBoot также распознает формат YAML, что немного легче визуализировать, поэтому давайте переименуем его в application.yml.

Назовем микросервис «portfolio-service». Мы можем указать порт или использовать порт 0, чтобы приложение использовало доступный порт. В нашем случае мы будем использовать 57116. Если вы разместите эту службу в качестве контейнера Docker, вы сможете сопоставить ее с любым выбранным вами портом. Назовите приложение и укажите наш порт, добавив следующее к нашему application.yml:

spring:
 application:
   name: portfolio-service
server:
 port: 57116

Чтобы сделать наш сервис доступным, добавим аннотацию к нашему классу приложений SpringBoot. Откройте приложение PortfolioApplication и добавьте @EnableDiscoveryClient над объявлением класса.

Подтвердите импорты. Класс должен выглядеть следующим образом:

package com.restms.demo.portfolio;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
. . .
@SpringBootApplication
@EnableDiscoveryClient
public class PortfolioApplication {

  public static void main(String[] args) {
     SpringApplication.run(PortfolioApplication.class, args);
  }
}

(Чтобы продемонстрировать, как микросервисы могут состоять из независимых платформ, мы будем использовать Jersey для этого сервиса и Spring REST для следующего).
Чтобы настроить веб-службу RESTful на Jersey, нам нужно указать класс конфигурации ResourceConfig. Добавьте класс JerseyConfig (для демонстрации мы сохраним его в том же пакете, что и наш класс приложения). Это должно выглядеть так, плюс правильный пакет и импорт:

@Configuration
@ApplicationPath("portfolios")
public class JerseyConfig extends ResourceConfig {
   public JerseyConfig()
   {
       register(PortfolioImpl.class);
   }
}

Обратите внимание, что он наследуется от ResourceConfig, чтобы обозначить его как класс конфигурации Jersey. Атрибут @ApplicationPath («portfolios») определяет контекст вызова, а это означает, что вызовы должны начинаться с элемента пути «portfolios». (Если вы его опустите, контекст по умолчанию «/»).

Класс PortfolioImpl будет обслуживать два запроса: portfolios/customer/{customer-id} возвращает все портфели и portfolios/customer/{customer-id}/portfolio/{portfolio-id} возвращает один портфель. Портфель состоит из набора тикеров и количества акций, принадлежащих этому тикеру. (Для демонстрации есть три клиента с идентификаторами 0, 1 и 2, каждый из которых имеет три портфеля с идентификаторами 0, 1 и 2).

Ваша IDE попросит вас создать PortfolioImpl; сделайте это сейчас. Для демонстрации добавим его в тот же пакет. Введите код ниже и подтвердите все импорты:

@Component
@Path("/")
public class PortfolioImpl implements InitializingBean {
   private Object[][][][] clientPortfolios;
   @GET
   @Path("customer/{customer-id}")
   @Produces(MediaType.APPLICATION_JSON)
   // a portfolio consists of an array of arrays, each containing an array of 
   // stock ticker and associated shares
   public Object[][][] getPortfolios(@PathParam("customer-id") int customerId)
   {
       return clientPortfolios[customerId];
   }

   @GET
   @Path("customer/{customer-id}/portfolio/{portfolio-id}")
   @Produces(MediaType.APPLICATION_JSON)
   public Object[][] getPortfolio(@PathParam("customer-id") int customerId, 
                           @PathParam("portfolio-id") int portfolioId) {
       return getPortfolios(customerId)[portfolioId];
   }

   @Override
   public void afterPropertiesSet() throws Exception {
       Object[][][][] clientPortfolios =
       {
         {
		// 3 customers, 3 portfolios each
           {new Object[]{"JPM", 10201}, new Object[]{"GE", 20400}, new Object[]{"UTX", 38892}},
           {new Object[]{"KO", 12449}, new Object[]{"JPM", 23454}, new Object[]{"MRK", 45344}},
           {new Object[]{"WMT", 39583}, new Object[]{"DIS", 95867}, new Object[]{"TRV", 384756}},
         }, {
           {new Object[]{"GE", 38475}, new Object[]{"MCD", 12395}, new Object[]{"IBM", 91234}},
           {new Object[]{"VZ", 22342}, new Object[]{"AXP", 385432}, new Object[]{"UTX", 23432}},
           {new Object[]{"IBM", 18343}, new Object[]{"DIS", 45673}, new Object[]{"AAPL", 23456}},
         }, {
           {new Object[]{"AXP", 34543}, new Object[]{"TRV", 55322}, new Object[]{"NKE", 45642}},
           {new Object[]{"CVX", 44332}, new Object[]{"JPM", 12453}, new Object[]{"JNJ", 45433}},
           {new Object[]{"MRK", 32346}, new Object[]{"UTX", 46532}, new Object[]{"TRV", 45663}},
         }
       };

       this.clientPortfolios = clientPortfolios;
   }
}

Аннотация Component обозначает это как класс компонента Spring и предоставляет его как эндпоинт. Аннотации Path о объявлении класса объявляют, что к классу обращаются через элемент пути “/”, а два поддерживаемых вызова api доступны через portfolios/customer/{customer-id} и portfolios/customer/{customer-id}/portfolio/{portfolio-id}, как мы видим из аннотаций метода. Обратите внимание, что путь ("/") является значением по умолчанию, но мы оставляем его для справки. Методы обозначаются как HTTP GET через @GETannotation. Наш метод предназначен для возврата массива и аннотируется для возврата Json, поэтому он возвращает массив Json. Обратите внимание, как аннотации PathParam используются в сигнатуре метода для извлечения отображенных параметров из отображаемых запросов.

(Для нашего демо мы возвращаем захардкоженные значения. Конечно, на практике реализация будет запрашивать базу данных или другую службу или источник данных вместо хардкода.)
Теперь создайте проект и запустите его. Если вы используете IntelliJ, он создаст исполняемый файл по умолчанию, поэтому просто нажмите зеленую стрелку «run». Вы также можете использовать

mvn spring-boot:run

Или вы можете выполнить установку maven и запустить приложение с помощью java -jar, указав на сгенерированный jar в целевом каталоге:

java -jar target\portfolio-0.0.1-SNAPSHOT.jar

Теперь мы должны увидеть этот сервис в Consul, поэтому давайте вернемся к нашему браузеру, загрузите http://localhost:8500/ui/#/dc1/services (или обновите, если вы уже там).



Хм, мы видим там наш сервис, но он отображен как неудавшийся. Это потому, что Consul ожидает «здорового» heartbeat-сигнала от нашей службы.
Чтобы генерировать heartbeat-сигналы, мы можем добавить зависимость от службы Spring «Actuator» к pom нашего приложения.

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

В то время как мы находимся в pom-е, обратите внимание, что существует конфликт версий с Jersey между Consul-стартером и Jersey-стартером. Чтобы сгладить это, назначьте Jersey-стартер первой зависимостью.

Теперь ваш pom должен содержать следующие зависимости:

<dependencies>
  <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-jersey</artifactId>
  </dependency>
  <dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-consul-discovery</artifactId>
  </dependency>
  <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-actuator</artifactId>
  </dependency>
  <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-test</artifactId>
     <scope>test</scope>
  </dependency>
</dependencies>

Перезапустив Consul, служба Portfolio отображает счастливое:



Теперь в portfolio-service есть два передающих узла: один из них — наша реализация портфельного сервиса, а другой — heartbeat.

Давайте проверим порт, который был назначен. Вы можете видеть, что в выводе приложения:

INFO 19792 --- [       	main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 57116 (http)

Вы также можете увидеть порт непосредственно в пользовательском интерфейсе Consul. Нажмите «customer-service», затем выберите ссылку «Service ‘customer-service’ check link», в которой отображается служебный порт, в данном случае 57116.



Запросите http://localhost:57116/portfolios/customer/1/portfolio/2, и вы увидите массив json [[«IBM», 18343], [«DIS», 45673], [«AAPL», 23456]]

Наш первый микросервис открыт для бизнеса!

Сервис ценообразования


Далее мы создадим наш сервис ценообразования, на этот раз используя Spring RestController вместо Jersey.

Служба ценообразования будет принимать в качестве параметров идентификатор клиента и идентификатор портфеля и будет использовать RestTemplate для запроса услуги портфеля, получения тикеров и акций и возврата текущих цен. (Мне не нужно говорить вам, что эти значения — это поддельные новости, поэтому не используйте их для принятия торговых решений!)

Создайте новый проект, используя следующую информацию:



На этот раз выберите зависимости Web, Consul Discovery и Actuator:



Оставьте название проекта по умолчанию «pricing» и создайте проект в выбранном вами каталоге.

На этот раз мы будем использовать application.properties вместо application.yml.
Задайте имя и порт в application.properties как:

spring.application.name=pricing
server.port=57216

Аннотируйте PricingApplication с @EnableDiscoveryClient. Класс должен выглядеть так, плюс пакет и импорт.

@SpringBootApplication
@EnableDiscoveryClient
public class PricingApplication {
  public static void main(String[] args) {
     SpringApplication.run(PricingApplication.class, args);
  }
}

Затем мы создадим класс PricingEndpoint. Здесь я приведу более подробный пример, поскольку он демонстрирует несколько важных функций, включая обнаружение сервисов (поиск портфельной службы) и использование RestTemplate для запроса:

@RestController
@RequestMapping("/pricing")
public class PricingEndpoint implements InitializingBean {
   @Autowired
   DiscoveryClient client;
   Map<String, Double> pricingMap = new HashMap<>();

   RestTemplate restTemplate = new RestTemplate();

   @GetMapping("/customer/{customer-id}/portfolio/{portfolio-id}")
   public List<String> getPricedPortfolio(
                           @PathVariable("customer-id") Integer customerId, 
                           @PathVariable("portfolio-id") Integer portfolioId)
   {
      List<ServiceInstance> instances 
                                  = client.getInstances("portfolio-service");
      ServiceInstance instance 
             = instances.stream()
                        .findFirst()
                        .orElseThrow(() -> new RuntimeException("not found"));
      String url = String.format("%s/portfolios/customer/%d/portfolio/%d", 
                                 instance.getUri(), customerId, portfolioId);
      // query for the portfolios, returned as an array of List 
      // of size 2, containing a ticker and a position (# of shares)
      Object[] portfolio = restTemplate.getForObject(url, Object[].class);
      // Look up the share prices, and return a list of Strings, formatted as
      // ticker, shares, price, total
      List<String> collect = Arrays.stream(portfolio).map(position -> {
          String ticker = ((List<String>) position).get(0);
          int shares = ((List<Integer>) position).get(1);
          double price = getPrice(ticker);
          double total = shares * price;
          return String.format("%s %d %f %f", ticker, shares, price, total);
      }).collect(Collectors.toList());
      return collect;
   }

   private double getPrice(String ticker)
   {
      return pricingMap.get(ticker);
   }

   @Override
   public void afterPropertiesSet() throws Exception {
       pricingMap.put("MMM",201.81);
       pricingMap.put("AXP",85.11);
       pricingMap.put("AAPL",161.04);
       pricingMap.put("BA",236.32);
       pricingMap.put("CAT",118.02);
       pricingMap.put("CVX",111.31);
       pricingMap.put("CSCO",31.7);
       pricingMap.put("KO",46.00);
       pricingMap.put("DIS",101.92);
       pricingMap.put("XOM",78.7);
       pricingMap.put("GE",24.9);
       pricingMap.put("GS",217.62);
       pricingMap.put("HD",155.82);
       pricingMap.put("IBM",144.29);
       pricingMap.put("INTC",35.66);
       pricingMap.put("JNJ",130.8);
       pricingMap.put("JPM",89.75);
       pricingMap.put("MCD",159.81);
       pricingMap.put("MRK",63.89);
       pricingMap.put("MSFT",73.65);
       pricingMap.put("NKE",52.78);
       pricingMap.put("PFE",33.92);
       pricingMap.put("PG",92.79);
       pricingMap.put("TRV",117.00);
       pricingMap.put("UTX",110.12);
       pricingMap.put("UNH",198.00);
       pricingMap.put("VZ",47.05);
       pricingMap.put("V",103.34);
       pricingMap.put("WMT", 80.05);

   }
}

Чтобы найти портфельный сервис, нам необходимо иметь доступ к DiscoveryClient. Его легко получить с помощью аннотации Spring's Autowired.

@Autowired
   DiscoveryClient client;

Этот экземпляр DiscoveryClient затем используется для поиска службы в вызове:

List<ServiceInstance> instances = client.getInstances("portfolio-service");
ServiceInstance instance = instances.stream().findFirst().orElseThrow(() -> new RuntimeException("not found"));

После того, как служба найдена, мы можем использовать ее для выполнения нашего запроса, который мы составляем в соответствии с вызовом api, созданным в нашем портфельном сервисе.

String url = String.format("%s/portfolios/customer/%d/portfolio/%d", instance.getUri(), customerId, portfolioId);

Наконец, мы используем RestTemplate для выполнения нашего GET-запроса.

Object[] portfolio = restTemplate.getForObject(url, Object[].class);

Обратите внимание, что для Rest-контроллеров (как и для Spring MVC Request Controller) переменные пути извлекаются с помощью аннотации PathVariable, в отличие от Jersey, который, как мы видели, использует PathParam.

На этом мы завершаем наше ценообразование с помощью Spring RestController.

Документация


Мы решили все эти проблемы, чтобы создать наши микросервисы, но они не будут приносить достаточно пользы, если мы не дадим миру знание о том, как их использовать.

Для этого мы используем удобный и простой в использовании инструмент Swagger, который не только документирует наши вызовы API, но также предоставляет удобный веб-клиент для их вызова.

Во-первых, давайте укажем Swagger в нашем pom:

<dependency>
  <groupId>io.springfox</groupId>
  <artifactId>springfox-swagger2</artifactId>
  <version>2.7.0</version>
</dependency>
<dependency>
  <groupId>io.springfox</groupId>
  <artifactId>springfox-swagger-ui</artifactId>
  <version>2.7.0</version>
</dependency>

Затем нам нужно указать Swagger, какой из наших классов мы хотим документировать. Давайте представим новый класс SwaggerConfig, содержащий спецификацию Swagger.

@Configuration
@EnableSwagger2
public class SwaggerConfig {
   @Bean
   public Docket api() {
       return new Docket(DocumentationType.SWAGGER_2)
               .select()
               .apis(RequestHandlerSelectors.any())
               .paths(PathSelectors.regex("/pricing.*"))
               .build();
   }
}

Посмотрим, что делает этот класс. Сначала мы обозначили это как конфигурацию Swagger с аннотацией @ EnableSwagger2.

Затем мы создали компонент Docket, который сообщает Swagger, какие API-интерфейсы должны отображаться. В приведенном выше примере мы сказали Swagger продемонстрировать любой путь, начинающийся с «/pricing». Альтернативой было бы указать классы для документирования, а не для путей:

.apis(RequestHandlerSelectors.basePackage("com.restms.demo"))
.paths(PathSelectors.any())

Перезапустите ценовой микросервис и вызовите из браузера http://localhost:57216/swagger-ui.html



Нажмите «List Operations», чтобы подробно просмотреть операции сервиса.
Нажмите «Expand Operations», чтобы создать запрос на основе формы. Задайте некоторые параметры, нажмите «Try it out!» и дождитесь ответа:



Вы можете добавить намного больше цветов, добавив аннотации Swagger к вашим методам.
Например, украсьте существующий метод PricingImpl.getPricedPortfolio, используя аннотацию @ApiOperation, как показано ниже:

@ApiOperation(value = "Retrieves a fully priced portfolio",
       notes = "Retrieves fully priced portfolio given customer id and portfolio id")
@GetMapping("/customer/{customer-id}/portfolio/{portfolio-id}")
public List<String> getPricedPortfolio(@PathVariable("customer-id") Integer customerId, @PathVariable("portfolio-id") Integer portfolioId)

Перезагрузите и обновите swagger-ui, чтобы увидеть новую уточненную документацию:



И это далеко не все, что вы можете сделать с Swagger, поэтому ознакомьтесь с документацией.

Еще больше о работе Spring Boot вам расскажет Юрий Дворжецкий, преподаватель нашего курса «Разработчик на Spring Framework»:


Оригинал статьи
Tags:
Hubs:
Total votes 10: ↑7 and ↓3+4
Comments6

Articles

Information

Website
otus.ru
Registered
Founded
Employees
101–200 employees
Location
Россия
Representative
OTUS