免费下载案例集|20+数字化领先企业人才培养实践经验 了解详情
写点什么

使用 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:3374040

评论 1 条评论

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

上榜!天翼分布式云操作系统入选“科创中国”先导技术榜单!

天翼云开发者社区

云计算 分布式 云服务

CAD绘图软件Autodesk AutoCAD 2021 for Mac v2021.1.2中文激活版

Mac相关知识分享

利用淘宝商品详情API接口,打造智能化电商数据分析平台

技术冰糖葫芦

API 安全 API 文档 API 开发 API 协议

软件测试学习笔记丨接口请求体-文件

测试人

软件测试

Redis Desktop Manager for Mac(Redis可视化工具)v2020中文激活版

Mac相关知识分享

可视化工具 数据库设计软件

SecureCRT Mac版:强大的SSH与远程连接工具

Mac相关知识分享

“文心•跨模态大模型”又有新动态,支持内容分析时输出自定义标签库

爱AI的猫猫头

音视频 低代码 大模型

软件测试学习笔记丨接口请求头

测试人

软件测试 接口测试 测试开发

软件测试学习笔记丨接口自动化测试框架介绍

测试人

软件测试

PDF专业制作与编辑软件Acrobat Pro DC 2021 for Mac v21.中文激活版

Mac相关知识分享

PDF 办公

数据库管理软件Navicat Premium 15 for Mac

Mac相关知识分享

WAIC 2024盛大召开,天翼云以全栈智算能力赋能AI时代!

天翼云开发者社区

人工智能大会 人工智能生态论坛

Go语言设计模式:使用Option模式简化类的初始化

伤感汤姆布利柏

分页查询及其拓展应用案例

快乐非自愿限量之名

分页查询

性能测试:性能测试计划

霍格沃兹测试开发学社

开发体育直播平台:高并发问题解决手段及架构设计思路

软件开发-梦幻运营部

Parallels Desktop 19一键激活版:强大的Mac虚拟机解决方案

Mac相关知识分享

Web3 游戏周报(7.07-7-13)

Footprint Analytics

链游

小智常见报表示例--层次坐标--同比报表

小智数据

Sentieon | 应用教程:Sentieon分布模式

INSVAST

基因数据分析 生信服务 分布式数据存储模块 分布模式 Sentieon

OpenAI 曝新项目「草莓」,提升 AI 推理能力;智谱 AI 开源视频理解模型丨 RTE 开发者日报

声网

安全可信| 通过双项测试!TeleDB实力亮剑!

天翼云开发者社区

数据库 云计算 天翼云

WAIC 2024,好city啊!

天翼云开发者社区

人工智能 世界人工智能大会 WAIC

Microsoft Remote Desktop for Mac(远程桌面连接工具) 10.9.5正式版

Mac相关知识分享

Mac远程控制软件

如何保护你的网络安全?

网络安全服务

TCP 服务器 DDoS waf DDoS 攻击

腾讯云首发大数据高性能计算引擎Meson,支持三大产品线性能升级

腾讯云大数据

大数据

Java程序员眼中的Rust系列 — 1.初见

Geek_zbvt62

Java rust

开发者指南:挑选高效的缺陷管理工具

爱吃小舅的鱼

软件缺陷管理

MoneyPrinterPlus全面支持本地Ollama大模型

程序那些事

工具 程序那些事 AIGC

企业如何利用小程序生态实现差异化?

Geek_2305a8

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