微应用之间的服务调用
服务调用示例
以商品下单为例,比如将业务拆分为商品服务和订单服务,订单服务会调用商品服务的库存扣减。
单个微服务工程,统一按以下目录编排:
-product
–product-common 商品服务公用对象
–product-client 商品服务客户端,以 jar 包方式被订单服务依赖
–product-server 商品服务,要注册到 Eureka Server,外部通过 product-client 来调用
我们的微服务工程之间,依赖关系如下:
我们从商品微服务工程开始
外层 product 初始 pom.xml:
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
<relativePath/>
</parent>
<groupId>com.hicoview</groupId>
<artifactId>product</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>product</name>
<description>Demo project for Spring Boot</description>
<modules>
<module>product-common</module>
<module>product-client</module>
<module>product-server</module>
</modules>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
<product-common.version>0.0.1-SNAPSHOT</product-common.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.hicoview</groupId>
<artifactId>product-common</artifactId>
<version>${product-common.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
product-common 初始 pom.xml:
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.hicoview</groupId>
<artifactId>product</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>product-common</artifactId>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
product-client 初始 pom.xml:
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.hicoview</groupId>
<artifactId>product</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>product-client</artifactId>
<dependencies>
<dependency>
<groupId>com.hicoview</groupId>
<artifactId>product-common</artifactId>
</dependency>
</dependencies>
product-server 的初始 pom.xml:
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.hicoview</groupId>
<artifactId>product</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>product-server</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.hicoview</groupId>
<artifactId>product-common</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
接下来我们调整 product-server 工程
商品服务需注册到 Eureka Server,添加 Eureka Client 相关依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
启动类加注解 @EnableDiscoveryClient:
package com.hicoview.product;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class ProductApplication {
public static void main(String[] args) {
SpringApplication.run(ProductApplication.class, args);
}
}
配置 application.yml:
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
spring:
application:
name: product
server:
port: 8080
启动 product-server,访问注册中心 http://localhost:8761,PRODUCT 注册上去了,再继续往下看。
我们写个简单的服务调用示例一下,订单服务下单逻辑调用商品服务进行扣减库存。
继续修改 product-server 工程
package com.hicoview.product.service;
import java.util.List;
public interface ProductService {
// 扣减库存
void decreaseStock();
}
package com.hicoview.product.service.impl;
import com.hicoview.product.service.ProductService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class ProductServiceImpl implements ProductService {
@Override
public void decreaseStock() {
log.info(“—— 扣减库存 —–“);
}
}
spring cloud 的 RPC 服务是使用 HTTP 方式调用的,所以还要创建 ProductController:
package com.hicoview.product.controller;
import com.hicoview.product.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(“/product”)
public class ProductController {
@Autowired
private ProductService productService;
@PostMapping(“/decreaseStock”)
public void decreaseStock() {
productService.decreaseStock();
}
}
接下来修改 product-client 工程
pom.xml 增加 Feign 依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
创建 ProductClient:
package com.hicoview.product.client;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@FeignClient(name=”product”)
public interface ProductClient {
// 通过 Feign 来代理对 PRODUCT 服务的 HTTP 请求
@PostMapping(“/product/decreaseStock”)
void decreaseStock();
}
再次启动 product-server,没问题的话,把 product 上传到本地 maven 仓库:
mvn -Dmaven.test.skip=true -U clean install
然后初始化订单微服务工程后继续。
修改 order-server 工程
order-server 的服务可能会被 User 等其他服务调用,也是个 Eureka client,添加依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
启动类加注解 @EnableDiscoveryClient
package com.hicoview.product;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class ProductApplication {
public static void main(String[] args) {
SpringApplication.run(ProductApplication.class, args);
}
}
配置 application.yml:
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
spring:
application:
name: order
server:
port: 8081
启动 order-server,访问注册中心 http://localhost:8761/:
ORDER 服务注册成功后,继续调整 order-server 工程
由于订单服务要调用商品服务,需添加对 product-client 依。修改 pom.xml:
<dependency>
<groupId>com.hicoview</groupId>
<artifactId>product-client</artifactId>
</dependency>
对版本的管理统一交给上层,修改上层 order 的 pom.xml,增加以下配置:
<properties>
…
<product-client.version>0.0.1-SNAPSHOT</product-client.version>
</properties>
<dependencyManagement>
<dependencies>
…
<dependency>
<groupId>com.hicoview</groupId>
<artifactId>product-client</artifactId>
<version>${product-client.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
回到 order-server,修改启动类,添加 Feign 扫描路径:
package com.hicoview.order;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = “com.hicoview.product.client”)
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
然后按顺序创建 Controller、Service:
package com.hicoview.order.controller;
import com.hicoview.order.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(“/order”)
public class OrderController {
@Autowired
private OrderService orderService;
// 创建订单
@PostMapping(“/create”)
public void create() {
orderService.createOrder();
}
}
public interface OrderService {
void createOrder();
}
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Autowired
private ProductClient productClient;
@Override
public void createOrder() {
log.info(“—— 创建订单 —–“);
// 调用商品扣减服务
productClient.decreaseStock();
}
}
启动 order-server,发起下单请求:
curl -X POST http://localhost:8081/order/create
服务调用成功,order-server 和 product-server 输出:
2019-02-26 11:09:58.408 INFO 3021 — [nio-8081-exec-3] c.h.order.service.impl.OrderServiceImpl : —— 创建订单 —–
2019-02-26 11:09:58.430 INFO 3015 — [nio-8080-exec-3] c.h.p.service.impl.ProductServiceImpl : —— 扣减库存 —–
为了示例的极简,上面服务调用没有涉及到入参和返回值。如果需要定义参数或返回值,考虑到内外部都会用到,需将参数 bean 定义在 product-common 中作为公共 bean。
Feign 和 RestTemplate
Rest 服务的调用,可以使用 Feign 或 RestTemplate 来完成,上面示例我们使用了 Feign。
Feign(推荐)
Feign 是一个声明式的 Web Service 客户端,它的目的就是让 Web Service 调用更加简单。Feign 提供了 HTTP 请求的模板,通过编写简单的接口和插入注解,就可以定义好 HTTP 请求的参数、格式、地址等信息。
Feign 会完全代理 HTTP 请求,我们只需要像调用方法一样调用它就可以完成服务请求及相关处理。Feign 整合了 Ribbon 和 Hystrix(关于 Hystrix 我们后面再讲),可以让我们不再需要显式地使用这两个组件。
RestTemplate
RestTemplate 提供了多种便捷访问远程 Http 服务的方法,可以了解下。
第一种方式,直接使用 RestTemplate,URL 写死:
// 1.
RestTemplate restTemplate = new RestTemplate();
restTemplate.getForObject(“http://localhost:8080/product/decreaseStock”, String.class);
这种方式直接使用目标的 IP,而线上部署的 IP 地址可能会切换,同一个服务还会启多个进程,所以有弊端。
第二种方式,利用 LoadBalancerClient,通过应用名获取 URL,然后再使用 RestTemplate:
@Autowired
private LoadBalancerClient loadBalancerClient;
// 1. 第二种方式,通过应用名字拿到其中任意一个 host 和 port
ServiceInstance serviceInstance = loadBalancerClient.choose(“PRODUCT”);
String url = String.format(“http://%s:%s”, serviceInstance.getHost(), serviceInstance.getPort() + “/product/decreaseStock”);
RestTemplate restTemplate = new RestTemplate();
restTemplate.getForObject(url, String.class);
第三种方式,写一个 config 把 RestTemplate 作为一个 bean 配置上去:
@Component
public class RestTemplateConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
@Autowired
private RestTemplate restTemplate;
// 使用时 url 里直接用应用名 PRODUCT 即可
restTemplate.getForObject(“http://PRODUCT/product/decreaseStock”, String.class);
Ribbon 客户端负载均衡器
Spring Cloud Ribbon 是一个基于 HTTP 和 TCP 的客户端负载均衡工具,它基于 Netflix Ribbon 实现。通过 Spring Cloud 的封装,可以让我们轻松地将面向服务的 REST 模版请求自动转换成客户端负载均衡的服务调用。Spring Cloud Ribbon 虽然只是一个工具类框架,它不像服务注册中心、配置中心、API 网关那样需要独立部署,但是它几乎存在于每一个 Spring Cloud 构建的微服务和基础设施中。
在 Spring Cloud 中,Ribbon 可自动从 Eureka Server 获取服务提供者地址列表,并基于负载均衡算法,请求其中一个服务提供者实例。还有微服务之间的调用,通过 Feign 或 RestTemplate 找到一个目标服务。以及 API 网关 Zuul 的请求转发等内容,实际上都是通过 Ribbon 来实现的。