写点什么

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

评论 1 条评论

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

软件工程,其实没有任何工程而言

实力程序员

Java8 的时间库(1):介绍 Java8 中的时间类及常用 API

看山

Java 6月日更

用EasyRecovery“监控硬盘”功能检测硬盘问题的方法

淋雨

数据恢复 EasyRecovery 文件恢复

译文 | AI产品经理:如何打造一款SaaS+AI的优质产品

LigaAI

产品经理 研发管理

Python——字典的使用

在即

6月日更

PO 就是Scrum中的产品经理?别再搞不清啦

万事ONES

项目管理 Scrum 敏捷开发 PO ONES

6月26日,HarmonyOS开发者日将于杭州举办

科技汇

阿里云视频云 Retina 多媒体 AI 体验馆开张啦!

阿里云视频云

阿里云 短视频 视频处理 媒体处理 视频制作

故事|订单系统中的补偿事务

悟空聊架构

故事 事务 6月日更 订单系统 补偿事务

Java包装类(Integer 详解 )

若尘

java编程 6月日更

【Flutter 专题】103 初识 Flutter Mixin

阿策小和尚

Flutter 小菜 0 基础学习 Flutter Android 小菜鸟 6月日更

一分钟开发一个表单

蛋先生DX

vue.js 表单 动态表单 6月日更

【LeetCode】石子游戏Java题解

Albert

算法 LeetCode 6月日更

拍乐云受邀2021亚太CDN峰会,技术创新赋能行业新价值

拍乐云Pano

RTC

校友会小程序开发笔记三:数据库设计

CC同学

小程序云开发 校友录小程序 校友会小程序

缓存的世界 Redis(二)-持久化

卢卡多多

redis redis持久化 配置文件持久化 6月日更

密码学系列之:feistel cipher

程序那些事

加密解密 密码学 程序那些事

并发王者课-铂金1:探本溯源-为何说Lock接口是Java中锁的基础

MetaThoughts

Java 多线程 并发 并发王者

JAVA笔记(三)--变量及运算符

加百利

Java 程序员 后端 6月日更

618 技术特辑(二)几百万人同时下单的秒杀,为什么越来越容易抢到了

华为云开发者联盟

数据库 服务器 流量 618 弹性负载均衡

校友会小程序开发笔记一:背景与技术方案的选型

CC同学

小程序云开发 校友录小程序 校友会小程序

连续七年,我们持续领跑

react源码解析13.hooks源码

全栈潇晨

React

JavaScript 学习(三)

空城机

JavaScript 大前端 6月日更

快来,这里有23种设计模式的Go语言实现

华为云开发者联盟

线程 设计模式 单例模式 Go 语言

618 技术特辑(一)不知不觉超预算3倍,你为何买买买停不下来?

华为云开发者联盟

电商 图数据库 知识图谱 618 图引擎服务

教你两招,解决数据膨胀

华为云开发者联盟

数据 GaussDB(DWS) VACUUM 数据膨胀 FSM

项目经理如何有效管理需求变更?

万事ONES

需求管理 ONES 项目经理

MySQL中的pid与socket是什么?

Simon

MySQL

针对 MySQL IO 特点进行的存储优化揭秘

焱融科技

MySQL 技术 分布式 高性能 文件存储

校友会小程序开发笔记二:功能需求设计

CC同学

小程序云开发 校友录小程序 校友会小程序

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