前言
基于对Eureka的认识,可以很容易地建立一个独立的服务了,但是服务间还不能相互调用,这一节将着重解决这个问题。
创建项目
扩展商品服务
创建一个项目ms-c1-product-service
,其中Eureka的配置部分和之前章节一样,接下来重点添加其他部分代码。
添加实体 Product
package com.chao.entity;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Product {
private Integer id;
private String name;
private double price;
}
这里给出了全部代码,为了简化其中的setter/getter
,我使用了一个工具’lombok’。
关于lombok
我是在一个开源项目中接触到的lombok
,这个工具在编写Java实体对象的时候能节省不少代码。通过注解的形式可以方便地自动生成setter/getter/toString
等等方法,这些方法显得很冗长也没有太多技术含量。
lombok
使用起来很方法,花几分钟就能上手,对于练习项目再好不过。
不过,在软件开发领域,一般遵循"约定大于配置",在项目合作中,建议不要轻易使用这个工具,否则不了解lombok
的同事拿到你的代码将是厄运的开始。
在项目中使用lombok
,需要做两件事情:
- 安装插件:如果是首次使用,请在IDE中安装
lombok
插件,官网(https://projectlombok.org/)包含各种常用编辑器的插件。当然,这是个一劳永逸的步骤。 - 添加依赖:在
pom.xml
中添加lombok
的依赖,请注意版本号,Spring-boot已经可以管理lombok
的版本。
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
添加业务模块ProductService:
@Service
public class ProductService {
private List<Product> products = Arrays.asList(new Product(0,"phone", 4999),new Product(1,"glass", 23.5), new Product(2, "clothes", 234.99));
public Product getById(Integer id){
return products.get(id);
}
public List<Product> getAll(){
return products;
}
}
这里模拟数据采用的是内存存储的方式(List),两个方法分别通过id拿单条数据和取出所有数据。
添加外部访问接口ProductController:
@RestController
@RequestMapping("/product")
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping("/{id}")
public Product findOne(@PathVariable Integer id){
return productService.getById(id);
}
@GetMapping("/list")
public List<Product> all(){
return productService.getAll();
}
@GetMapping("/ping")
public String hello(HttpServletRequest request){
return "Hello, message from "+ request.getServerName()+ ":"+ request.getServerPort();
}
}
代码逻辑很简单,暴露出三个Rest接口
http://ip:port:product/{id}
,占位符{id}取值0、1、2,还有一个接口http://ip:port:product/list
,获取所有商品信息,http://ip:port:product/ping
可以测试连通性。
至此,商品服务的代码已经完成,项目的结果如下:
启动项目后,在浏览器中访问http://localhost:10001/product/1
,返回如下数据:
{"id":1,"name":"glass","price":23.5}
访问http://localhost:10001/product/ping
,返回数据:
Hello, message from localhost:10001
创建订单服务
新建项目模块ms-c1-order-ribbon
,其中Eureka最基本的配置和之前的项目一致。
创建订单实体类Order:
@Data
@AllArgsConstructor
public class Order {
/**
* 订单ID
*/
private String id;
/**
* 订单中的商品,假设一个订单中只能包含一种商品
*/
private Product item;
/**
* 订单中商品的数量
*/
private Integer amount;
}
商品实体类Product的代码和上一个项目完全一样。
由于需要调用商品服务,利用RestTemplate
来做服务间调用。
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
为什么之前的项目没有用到过?在单体应用时代,服务可以直接通过代码调用。比如
@Autowired
某个service,然后直接调用其中的方法。微服务时代需要服务拆分,服务间属于不同的项目,通过Restfule接口调用。
在订单服务中,需要调用商品服务OrderServiceA :
@Service
public class OrderServiceA {
@Autowired
private RestTemplate restTemplate;
/**
* 生成一个订单
* @param id
* @param amount
*/
public Order create(Integer id, Integer amount){
String url = "http://localhost:10001/product/"+id;
Product p = restTemplate.getForObject(url, Product.class);
return new Order(UUID.randomUUID().toString(), p, amount);
}
/**
* 查看所有可以购买的商品
*/
public List<Product> findAll(){
String url = "http://localhost:10001/product/list";
Product[] p = restTemplate.getForObject(url, Product[].class);
return Arrays.asList(p);
}
}
/**
* 为了测试是否可以和product-service链接
*/
public String hello() {
String url = "http://localhost:10001/product/ping";
return restTemplate.getForObject(url,String.class);
}
订单服务对外暴露Rest接口OrderAController:
@RestController
@RequestMapping("/order-a")
public class OrderAController {
@Autowired
private OrderServiceA orderServiceA;
@RequestMapping("/create")
public Order add(Integer productId, Integer amount){
return orderServiceA.create(productId, amount);
}
@GetMapping("/product/list")
public List<Product> list(){
return orderServiceA.findAll();
}
@GetMapping("/product/ping")
public String pingProduct(){
return orderServiceA.hello();
}
}
在浏览器中访问http://localhost:10010/order-a/product/list
可以看到:
[{"id":0,"name":"phone","price":4999.0},{"id":1,"name":"glass","price":23.5},{"id":2,"name":"clothes","price":234.99}]
访问http://localhost:10010/order-a/product/ping
可以看到:
Hello, message from localhost:10001
上面的这一系列代码不使用SpringCloud完全可以做到,那么需要优化的点在哪里呢?当然就在OrderAService
,在调用其他服务的时候,地址是写死的:主机名和端口号不能灵活改变。当然你也可以将其放到某个可以配置的地方,不过这不是万全之策。
接下来,引入Ribbon,优化之前的代码。
在pom.xml
中添加依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
需要包装一个RestTemplate
,为了不影响之前的代码,新加一个Bean 于 CustomConfig
中:
@Bean
@LoadBalanced
public RestTemplate ribbonRestTemplate(){
return new RestTemplate();
}
注意和之前的RestTemplate唯一的不同在于加了注解
@LoadBalanced
,说明这个RestTemplate需要被Ribbon
托管。
新建OrderBService:
@Service
public class OrderBService {
@Autowired
private RestTemplate ribbonRestTemplate;
/**
* 生成一个订单
* @param id
* @param amount
*/
public Order create(Integer id, Integer amount){
String url = "http://product-service/product/"+id;
Product p = ribbonRestTemplate.getForObject(url, Product.class);
return new Order(UUID.randomUUID().toString(), p, amount);
}
/**
* 查看所有可以购买的商品
*/
public List<Product> findAll(){
String url = "http://product-service/product/list";
Product[] p = ribbonRestTemplate.getForObject(url, Product[].class);
return Arrays.asList(p);
}
/**
* 为了测试是否可以和product-service链接
* @return
*/
public String hello() {
String url = "http://product-service/product/ping";
return ribbonRestTemplate.getForObject(url,String.class);
}
}
注意,其中只有url
部分做的修改,我没有采用之前的hostname:port
的方式,而是直接添加需要调用的服务名"product-service",这这值是哪里来的呢?
在ms-c1-product-service
的配置文件中,有这样的配置:
spring.application.name=product-service
,它就限定了我们的服务名称。
新建OrderBController:
@RestController
@RequestMapping("/order-b")
public class OrderBController {
@Autowired
private OrderBService orderBService;
@RequestMapping("/create")
public Order add(Integer productId, Integer amount){
return orderBService.create(productId, amount);
}
@GetMapping("/product/list")
public List<Product> list(){
return orderBService.findAll();
}
@GetMapping("/product/ping")
public String pingProduct(){
return orderBService.hello();
}
}
其中的代码逻辑和OrderAController
一样。
测试是否可以正常使用,在浏览器中访问http://localhost:10010/order-b/product/ping
:
Hello, message from 192.168.2.200:10001
得到的结果和之前的方式有一点不同,返回的是ip地址’192.168.2.200’,而不是主机名’localhost’。
在浏览器中访问http://localhost:10010/order-b/create?productId=1&amount=2
得到如下结果:
[{"id":0,"name":"phone","price":4999.0},{"id":1,"name":"glass","price":23.5},{"id":2,"name":"clothes","price":234.99}]
这个接口是我们真正需要的,返回的数据和之前的一致。
Ribbon 负载均衡
上面测试可以看到,引入Ribbon以后通过服务名来做服务间调用可以避免在项目中配置大量的’主机名:端口号’信息。而这并不是Ribbon的主要作用,Ribbon提供的是一种负载均衡机制。
说起负载均衡,需要要提到Nginx,它属于最常见的负载均衡器,其他还有像F5、LVS都属于服务端负载均衡,而Ribbon属于客户端负载均衡,它赋予了应用支配HTTP与TCP的能力。
之前在做Eureka集群的时候,我创建了两个项目。在测试的时候,完全不必这样,我们可以充分利用IDE,启动多个实例。
1、在IDEA中,右上角找到启动信息-> Edit COnfigurations...
2、复制一个商品服务,命个名,添加端口信息-Dserver.port=xxx
启动以后可以在IDEA下方看到:
访问Eureka首页http://localhost:8761/
:
此时访问http://localhost:10010/order-a/product/ping
,看到返回的信息始终为:
Hello, message from localhost:10001
访问http://localhost:10010/order-b/product/ping
,会发现信息交替显示:
Hello, message from 192.168.2.200:10001
Hello, message from 192.168.2.200:10002
这里的’order-b’的接口是通过Ribbon实现了轮询的调用,实际上,并没有增加任何代码,就实现了负载均衡。
未完待续
通过对代码小小的改进,服务实现了负载均衡的能力,负载均衡的策略与原理也是比较重要的内容,下一节我将继续探究基于Ribbon的软负载均衡。
项目代码托管于Github。