写点什么

使用 SpringBoot 开启微服务之旅

  • 2018-01-09
  • 本文字数:8302 字

    阅读完需:约 27 分钟

本文要点

  • 微服务可以使你的代码解耦
  • 微服务可以使不同的团队专注于更小范围的工作职责、使用独立的技术、更安全更频繁地部署
  • SpringBoot 支持各种 REST API 的实现方式
  • 服务发现和服务调用是独立于服务平台的
  • Swagger 生成稳健的 API 文档和调用接口

如果还没有准备好使用微服务,那你肯定落后于学习曲线中的早期接受者阶段了,而且是时候开启微服务之旅了。本文中,我们将演示创建 REST 风格微服务所必需的各种组件,使用 Consul 服务注册中心和 Spring Boot 搭建各种脚手架、进行依赖注入和依赖管理,使用 Maven 进行构建,使用 Spring REST 和 Jersey/JaxRS 创建 Java REST 风格 API。

在过去的二十年里,企业使用 SDLC 流程变得非常敏捷,但是应用程序仍然相当庞大而且耦合在一起,包含大量支持各种版本的各种各样 API 的 jar 包。但是,如今有一种趋势朝着更精简的 DevOps 范的流程推进,功能也变得“无服务器化”。进行微服务重构可以解耦代码和资源,让构建流程更小,让发布更安全,让 API 更稳定。

本文中,我们将构建一个简易的股票市场投资组合管理应用程序。在这个应用中,客户可以通过服务调用,为他们的股票投资组合(股票代码和数量)进行定价。投资组合微服务将检索用户的投资组合,将它发送给定价微服务来应用最新的定价,然后返回完全定价和分类汇总过的投资组合,通过一个 REST 调用将所有这些信息展示给客户。

在我们开始创建微服务之前,需要安装Consul 来准备我们的环境。

下载Consul 服务注册中心

我们将使用Hashicorp Consul 来实现服务发现,所以请前往 https://www.consul.io/downloads.html 下载 Consul,有 Windows 版、Linux 版和 Mac 版等。这个链接将会提供一个可执行程序,你需要将这个程序添加到你的 path 环境变量中。

启动 Consul

从一个脚本弹出框以 dev 模式启动 Consul:

consul agent -dev为了验证它确实已经在运行,可以打开浏览器,访问 consul UI http://localhost:8500 。如果一切正常,consul 应该会报告它的运行状态良好。点击(在左边的)consul 服务,会(在右边)提供更多信息。

如果这个地方有什么问题,请确保你已经将consul 添加到执行路径中而且8500 和8600 端口是可用的。

创建SpringBoot 应用程序

我们将使用集成在主流IDE 中的 Spring Initializr ,来创建我们的 SpringBoot 应用程序的脚手架。下面的截屏使用的是 IntelliJ IDEA。

选择 File/New Project,来打开新建项目模板弹出框,然后选择 Spring Initializr。

事实上,你可以无需IDE 就安装脚手架。通过SpringBoot Initializr 网站 https://start.spring.io 完成一个在线 web 表格,会产出一个可以下载的包含你的空项目的 zip 文件。

点击“Next”按钮,填写所有的项目元数据。使用下面的配置:

点击“Next”按钮来选择依赖,然后在依赖搜索栏输入 Jersey 和 Consul Discovery。添加那些依赖:

点击“Next“按钮来指定你的项目名字和存放位置。使用在 web 表单中配置的默认名字“portfolio”,指定你希望存放项目的地址,然后点击“Finish”来生成并打开项目:


(点击图片放大)

你可以使用生成的 application.properties 文件,但是 SpringBoot 也接受 YAML 文件格式,YAML 格式看起来更直观,因此可以将这个文件重命名为 application.yml。

我们将这个微服务命名为“portfolio-service”。我们可以指定一个端口或者使用端口 0 来让应用程序使用一个可用的端口。在我们的例子中,我们使用端口 57116。如果你将这个服务作为一个 Docker container 部署,你可以将它映射到任何你选中的端口。让我们通过添加如下配置到 applicatin.yml 文件,来为应用程序命名并指定端口:

复制代码
spring:
application:
name: portfolio-service
server:
port: 57116

为了让我们的服务可以被发现,需要为 SpringBoot 的 application 类添加注解。打开 PortfolioApplication,在这个类声明的上方添加 @EnableDiscoveryClient。

接受 imports。这个 class 看起来会是这样:

复制代码
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)。

为了安装 Jersey REST 风格 Web Service,我们需要指定一个 ResourceConfig Configuration 类。增加 JerseyConfig 类(本例中,我们会把它放在相同的 package 下作为我们的 application 类。)它应该看起来像这样,加上适当的 package 和 imports:

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

需要注意的是,它继承了 ResourceConfig 来表明它是一个 Jersey 的配置类。@ApplicationPath(“portfolios”) 属性指定了调用的上下文,意味着调用路径应该以“portfolios”开头。(如果你没有指定,上下文默认为“/”。)

PortfolioImpl 类将服务两种请求,其中 portfolios/customer/{customer-id}返回所有的 portfolios,而 portfolios/customer/{customer-id}/portfolio/{portfolio-id}返回一个 portfolio。一个 portfolio 包括一组股票代码和相应的持有份额。

(本例中,有 3 个客户,id 分别为 0、1、2,而且每一个客户都有 3 个 portfolio,id 分别为 0、1、2)。

你的 IDE 会让你创建 PortfolioImpl,照着做就行了。本例中,将它添加在相同的 package。输入如下代码并接受所有 imports:

复制代码
@Component
@Path("/portfolios")
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 注解声明这个类可以通过“portfolios”路径访问到,两个支持的 api 调用可以通过 portfolios/customer/{customer-id}和 portfolios/customer/{customer-id}/portfolio/{portfolio-id}。这些方法通过 @GET 注解表明它服务 HTTP GET 请求,这个方法声明返回一个数组并注解为返回 Json,因此它会返回一个 Json 数组。注意如何在方法声明中使用 @PathParam 注解来从 request 中提取映射的参数。

(本例中,我们返回硬编码的值。当然,在实际应用中,实现的服务在这里会查询数据库或其它一些服务或者数据源。)

现在构建这个项目,然后运行。如果你是在使用 IntelliJ,它会创建一个默认的可运行程序,你只需点击绿色的“运行”箭头。你还可以使用

mvn spring-boot:run

或者,你可以运行一次 maven install,然后使用 java -jar 并指定 target 目录下生成的 jar 文件来运行这个应用程序:

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

我们现在应该可以在 Consul 中查看这个服务,所以返回浏览器,打开 http://localhost:8500/ui/#/dc1/services (如果你已经打开了这个地址,刷新就可以了)。

我们看到我们的portfolio-service 在那里了,但是显示为failing(失败)。那是因为Consol 在等待从我们的服务发送一个“健康”的心跳请求。

为了生成心跳请求,我们在应用程序的pom 文件中增加SpringBoot “Actuator”服务的依赖。

复制代码
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

在 pom 文件中,请注意,Jersey 版本在 consul-starter 和 jersey-starter 中有一个版本冲突。为了解决这个冲突,将 jersey starter 移为第一个依赖。

你的 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-service 会显示正常:

现在在portfolio-service 下有两个通过的节点,其中一个是我们实现的portfolio 服务,另外一个是心跳服务。

检查分配的端口。你可以在应用程序输出台看到:

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

你也可以直接在 consul UI 中查看这个端口。点击 portfolio-service,然后选择“Service ‘portfolio-service’”链接,会显示该服务的端口,本例中为 57116。

调用 http://localhost:57116/portfolios/customer/1/portfolio/2 ,然后你会看到 json 数组 [[“IBM”,18343],[“DIS”,45673],[“AAPL”,23456]]。

我们第一个微服务就正式开放了!

定价服务

接下来,我们会创建定价服务,这一次使用 Spring RestController 而不是 Jersey。

定价服务会接受客户端 id 和 portfolio id 作为参数,然后会使用一个 RestTemplate 查询 portfolio 服务来获取股票代码和份额,随后返回当前的价格。(这些都是假数据,所以不要用这些数据来做交易决策!)

使用如下信息创建一个新项目:

这次选择Web、Consul Discovery 和Actuator 依赖:

(点击图片放大)

将项目命名为“pricing”,在你选中的目录中生成项目。

这次我们会使用application.properties 而不是application.yml。

在application.properties 中设置名字和端口如下:

复制代码
spring.application.name=pricing
server.port=57216

用 @EnableDiscoveryClient 给 PricingApplication 注解。这个类应该看起来像这样,加上 package 和 imports。

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

接下来,我们会创建 PricingEndpoint 类。这个类有一点冗长,因为它演示了一些重要的功能,包括服务发现(查找 portfolio service)和使用 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;
}
{1}
private double getPrice(String ticker)
{
return pricingMap.get(ticker);
}
{1}
@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);
{1}
}
}
{1}
{1}

为了发现 portfolio 服务,我们需要访问一个 DiscoveryClient。这可以通过 Spring 的 @Autowired 注解轻松实现

复制代码
@Autowired
DiscoveryClient client;

然后在服务调用中,用这个 DiscoveryClient 实例来寻址我们的服务:

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

一旦寻址到这个服务,我们可以用它来执行我们的请求。这个请求是我们根据在 portflo-service 中创建的 api 调用组合而成的。

String url = String.format("%s/portfolios/customer/%d/portfolio/%d", instance.getUri(), customerId, portfolioId);最终,我们使用一个 RestTemplate 来执行我们的 GET 请求。

Object[] portfolio = restTemplate.getForObject(url, Object[].class);需要注意的是,对于 RestControllers(和 SpringMVC RequestController 一样),路径变量可以从 @PathVariable 注解中提取,而不像 Jersey 那样从 @PathParam 中提取。

这里使用一个 Spring RestController 来将定价服务发布出去。

文档

我们已经克服所有困难创建了我们的微服务,但是如果不让世界知道如何使用它们,它们就不会产生任何价值。

为此,我们使用了一个称作 Swagger 的工具。Swagger 是一个简单易用的工具,不仅为我们的 API 调用生成文档,还提供了一个可以援引这些文档的易用的 web 客户端。

首先,让我们在 pom 文件中指定 Swagger:

复制代码
<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();
}
}

我们可以看下这个类做了什么。首先,我们用 @EnableSwagger2 注解表明它是一个 Swagger 配置。

接下来,我们创建了一个 Docket bean,告诉 Swagger 要暴露哪些 API。在上面的例子中,我们告诉 Swagger 暴露所有以“/pricing”开头的路径。还可以选择指定 class 文件而不是路径来生成文档:

复制代码
.apis(RequestHandlerSelectors.basePackage("com.restms.demo"))
.paths(PathSelectors.any())

重启定价微服务,然后在浏览器上调用 http://localhost:57216/swagger-ui.html

点击“List Operations”按钮来查看详细的服务操作。

点击“Expand Opeartions”来创建一个基于form 的查询调用。提供一些参数,点击“Try it out!”,然后等待响应结果:

(点击图片放大)

你可以通过给方法增加Swagger 注解来增加更多的颜色。

例如,使用@ApiOperation 注解来装饰已有的方法PricingImpl.getPricedPortfolio:

复制代码
@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 做许多事情,更多详情请查看它的文档。

关于作者

Victor Grazi 在 Nomura Securities 从事核心平台工具开发工作,还是一位技术顾问和 Java 传道士。他是技术大会的常客,主导“ Java Concurrent Animated ”和“ Bytecode Explorer ”开源项目。他作为 InfoQ 中 Java 队列的一名编辑,在 Java Champions 中胜出成为一名 Oracle Java Champion。

查看英文原文: Getting Started with Microservices in SpringBoot

感谢罗远航对本文的审校。

2018-01-09 17:3374200

评论 1 条评论

发布
用户头像
启动报错了
2021-11-18 14:32
回复
没有更多了
发现更多内容

干货 | 解决 App 自动化测试的常见痛点(弹框及首页启动加载完成判断处理)

霍格沃兹测试开发学社

CeresDB 技术架构简介

TRaaS

#开源项目

有哪些方法可以提高企业的文档管理水平?

Baklib

文档 文档管理

WAIC 2022 | 洞见科技王湾湾:隐私计算在金融产业的应用与挑战

洞见科技

干货 | 背熟这些 Docker 命令,面试再也不怕啦~

霍格沃兹测试开发学社

字节前端必会面试题

夏天的味道123

JavaScript 前端

软件测试 | 测试开发 | 专项测试技术初识Hook

测吧(北京)科技有限公司

测试

相约 ArchSummit 杭州站,参与官方评论赢取精美周边!

InfoQ写作社区官方

热门活动 ArchSummit

干货 | 在Docker 上搭建持续集成平台 Jenkins

霍格沃兹测试开发学社

力扣17 - 电话号码的字母组合【回溯、哈希映射、队列】

Fire_Shield

队列 深度优先搜索 9月月更

干货 | 测试人职场晋升“潜规则”:15 年经验资深测试经理的职场忠告

霍格沃兹测试开发学社

手把手教你如何进行拆解竞品

产品海豚湾

产品经理 需求分析 竞品分析 9月月更 市场调研

2022年哪些工具适合设计企业产品手册?

Baklib

产品 产品手册

解读《Benchmarking Hybrid OLTP&OLAP Database Systems》| StoneDB学术分享会

StoneDB

数据库 国产数据库 StoneDB 企业号九月金秋榜 9月月更

干货 | 环境问题还是测试的老大难?两个步骤轻松搞定

霍格沃兹测试开发学社

干货 | 谁懂这篇文,玩游戏还会卡顿?

霍格沃兹测试开发学社

Kyligence 联合创始人兼 CEO 韩卿荣获金融科技风云人物奖

Kyligence

金融科技大会

干货| app自动化测试之Andriod微信小程序的自动化测试

霍格沃兹测试开发学社

知识管理,知识经济时代必不可缺的工具

Baklib

知识管理 知识 知识经济

干货 | 利用 pytest 玩转数据驱动测试框架

霍格沃兹测试开发学社

百度前端高频面试题(附答案)

beifeng1996

JavaScript 前端

干货 | 录制你的第一个web 自动化测试用例

霍格沃兹测试开发学社

TDengine 如何进行 SQL 写入?官方最全教程来了

TDengine

数据库 时序数据库 企业号九月金秋榜

软件测试 | 测试开发 | 测试人生 | 疫情之下,1个月内涨薪50%拿下亿级流量金融上市公司新 offer,我柠檬了~

测吧(北京)科技有限公司

测试

WAIC 2022 | 洞见科技CTO何浩:隐私计算统一底座赋能金融数字化转型

洞见科技

干货 | 应用打包还是测试团队老大难问题?

霍格沃兹测试开发学社

干货 | 读懂 Appium 日志,让测试效率翻倍!

霍格沃兹测试开发学社

干货|APP自动化Android特殊控件Toast识别

霍格沃兹测试开发学社

干货 | 原来升职加薪的测试工程师都擅长做接口测试

霍格沃兹测试开发学社

2022 世界人工智能大会|人工智能与开源技术先锋论坛成功举办

Kyligence

人工智能大会 先锋科技论坛

干货 | 移动端App自动化之App控件定位

霍格沃兹测试开发学社

使用SpringBoot开启微服务之旅_Java_Victor Grazi_InfoQ精选文章