目录
环境说明
微服务案例的搭建
新建父工程
微服务模块
product-service(商品服务)
创建子工程
添加依赖
商品模块业务开发
创建业务数据库
测试
order-service(订单服务)
创建子工程
添加依赖
订单模块业务开发
测试
注册中心的使用
搭建注册中心
创建子工程
添加依赖
注册中心代码开发
测试
把服务注册到注册中心
将商品服务注册到注册中心
添加依赖
服务注册
添加服务发现支持
测试
将订单服务注册到注册中心
用服务列表名称进行调用
原理
修改代码
测试
注册中心的高可用
原理
两台Eureka互相注册
把各个微服务注册到两台Eureka中
测试
jdk1.8
maven3.6.3
mysql8
idea2022
spring cloud2022.0.8
打开IDEA,File->New ->Project,填写Name(工程名称)和选择Location(工程存储位置),选择Java语言和Maven,点击Create创建maven工程,该工程为所有工程的父工程
官方查看Spring Cloud与Spring Boot的版本匹配问题
Spring Cloud
Spring Boot2.7.x匹配的Spring Cloud的版本为2021.0.x
修改pom.xml
4.0.0
org.example
spring-cloud-bk-2023
1.0-SNAPSHOT
8
8
UTF-8
org.springframework.boot
spring-boot-starter-parent
2.7.12
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-logging
org.springframework.boot
spring-boot-starter-test
test
org.projectlombok
lombok
1.18.4
provided
org.springframework.cloud
spring-cloud-dependencies
2021.0.8
pom
import
spring-snapshots
Spring Snapshots
http://repo.spring.io/libs-snapshot-local
true
spring-milestones
Spring Milestones
http://repo.spring.io/libs-milestone-local
false
spring-releases
Spring Releases
http://repo.spring.io/libs-release-local
false
spring-snapshots
Spring Snapshots
http://repo.spring.io/libs-snapshot-local
true
spring-milestones
Spring Milestones
http://repo.spring.io/libs-milestone-local
false
org.springframework.boot
spring-boot-maven-plugin
注意:添加依赖后,需要刷新依赖。
父工程创建好之后,接下来就搭建各个微服务模块,这里以product-service(商品服务)和order-service(订单服务)为例。实现用户下订单的功能。
用户下订单业务流程如下:用户通过浏览器下订单,浏览器发起请求到订单服务,订单服务通过调用商品服务得到商品信息。
创建product-service子模块,右键父工程->New->Module
填写模块名称:product-service,选择Java,Maven,点击创建,如下图:
修改product-service的pom.xml,在的上方添加如下依赖
mysql
mysql-connector-java
8.0.33
org.springframework.boot
spring-boot-starter-data-jpa
刷新依赖
代码结构如下
实体类
package org.example.product.entity;
import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.math.BigDecimal;
/**
* 商品实体类
*/
@Data
@Entity
@Table(name="tb_product")
public class Product {
@Id
private Long id;
private String productName;
private Integer status;
private BigDecimal price;
private String productDesc;
private String caption;
private Integer inventory;
}
Dao接口
package org.example.product.dao;
import org.example.product.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface ProductDao extends JpaRepository, JpaSpecificationExecutor {
}
Service接口
package org.example.product.service;
import org.example.product.entity.Product;
public interface ProductService {
/**
* 根据id查询
*/
Product findById(Long id);
/**
* 保存
*/
void save(Product product);
/**
* 更新
*/
void update(Product product);
/**
* 删除
*/
void delete(Long id);
}
Service接口实现类
package org.example.product.service.impl;
import org.example.product.dao.ProductDao;
import org.example.product.entity.Product;
import org.example.product.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class ProductServiceImpl implements ProductService {
@Autowired
private ProductDao productDao;
@Override
public Product findById(Long id) {
return productDao.findById(id).get();
}
@Override
public void save(Product product) {
productDao.save(product);
}
@Override
public void update(Product product) {
productDao.save(product);
}
@Override
public void delete(Long id) {
productDao.deleteById(id);
}
}
Controller类
package org.example.product.controller;
import org.example.product.entity.Product;
import org.example.product.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/product")
public class ProductController {
@Autowired
private ProductService productService;
@Value("${server.port}")
private String port;
@Value("${client.ip-address}")
private String ip;
@RequestMapping(value = "/{id}",method = RequestMethod.GET)
public Product findById(@PathVariable Long id) {
Product product = productService.findById(id);
product.setProductName("访问的服务地址:"+ip + ":" + port);
return product;
}
@RequestMapping(value = "",method = RequestMethod.POST)
public String save(@RequestBody Product product) {
productService.save(product);
return "保存成功";
}
}
启动类
package org.example.product;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
@SpringBootApplication
@EntityScan("org.example.product.entity")
public class ProductApplication {
public static void main(String[] args) {
SpringApplication.run(ProductApplication.class, args);
}
}
application.yml配置
server:
port: 9001
spring:
application:
name: service-product
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/shop1?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
username: root
password: 123
jpa:
database: MySQL
show-sql: true
open-in-view: true
generate-ddl: true #自动创建表
client:
ip-address: 10.111.50.229
注意修改数据库信息,例如url、username、password
使用mysql创建数据库:shop1
mysql> create database shop1;
运行启动类:ProductApplication.java
因为application.yml的spring.jpa.generate-ddl 配置为true会自动创建表,启动成功后,刷新数据库能看到tb_product表,表还没有具体数据
手动为tb_product表添加两行测试数据,例如:
浏览器访问
http://localhost:9001/product/1
访问到了数据库的数据
子模块:order-service
修改order-service的pom.xml,在的上方添加如下依赖
mysql
mysql-connector-java
8.0.33
org.springframework.boot
spring-boot-starter-data-jpa
刷新依赖
代码结构如下:
实体类
package org.example.order.entity;
import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.math.BigDecimal;
/**
* 商品实体类
*/
@Data
@Entity
@Table(name="tb_product")
public class Product {
@Id
private Long id;
private String productName;
private Integer status;
private BigDecimal price;
private String productDesc;
private String caption;
private Integer inventory;
}
控制类
package org.example.order.controller;
import org.example.order.entity.Product;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private RestTemplate restTemplate;
@RequestMapping(value = "/buy/{id}", method = RequestMethod.GET)
public Product findById(@PathVariable Long id){
Product product = null;
//调用其他微服务,get请求用getXxx post请求用postXxx
product = restTemplate.getForObject("http://localhost:9001/product/1", Product.class);
return product;
}
}
启动类
package org.example.order;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
@EntityScan("org.example.order.entity")
public class OrderApplication {
/**
* 使用spring提供的RestTemplate发送http请求到商品服务
* 1.创建RestTemplate对象交给容器管理
* 2.在使用的时候,调用其方法完成操作 (getXX,postxxx)
*/
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class,args);
}
}
application.yml配置
server:
port: 9002
spring:
application:
name: service-order
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/shop1?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
username: root
password: 123
jpa:
database: MySQL
show-sql: true
open-in-view: true
generate-ddl: true #自动创建表
client:
ip-address: 10.111.50.229
注意修改数据库信息。
运行启动类:OrderApplication.java
浏览器访问
http://localhost:9002/order/buy/1
效果如下
和之前直接访问product服务返回一致,说明order服务调用了product服务
http://localhost:9001/product/1
代码总结:
这里使用Eureka作为注册中心。
在父工程下,创建子工程模块eureka_server
eureka_server代码结构如下
修改eureka_service的pom.xml,在的上方添加如下依赖
org.springframework.cloud
spring-cloud-starter-netflix-eureka-server
刷新依赖
启动类
package org.example.eureka;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
// 激活eurekaserver
@EnableEurekaServer
public class EurekaServerAppliation {
public static void main(String[] args) {
SpringApplication.run(EurekaServerAppliation.class, args);
}
}
application.yml配置文件
spring:
application:
name: eureka-server
server:
port: 9000
eureka:
instance:
hostname: localhost
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
server:
enable-self-preservation: false
eviction-interval-timer-in-ms: 4000
注意:eureka要顶格写,没有缩进。
Eureka配置含义:
register-with-eureka:是否将自己注册到注册中心
fetch-registry:是否从eureka中获取注册信息
service-url:配置暴露给Eureka Client的请求地址
enable-self-preservation:关闭自我保护
eviction-interval-timer-in-ms:剔除服务间隔的时间
运行启动类
浏览器访问
http://localhost:9000/
能看到如下界面,说明eureka注册中心服务搭建成功
把各个微服务注册到注册中心步骤如下:
1.添加EurekaClient依赖
2.服务注册:修改application.yml添加EurekaServer的信息
3.修改启动类,添加服务发现的支持(可选)
把product-serviceI商品服务注册到Eureka注册中心。
修改product-service的pom.xml,添加如下依赖
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
刷新依赖
修改product-service的application.yml添加EurekaServer的信息
eureka:
client:
service-url:
defaultZone: http://localhost:9000/eureka/
添加服务发现支持有3中方式,任意选3种方式其中之一进行操作。
方式1:
在启动类上方添加@EnableEurekaClient
// 激活EurekaClient
@EnableEurekaClient
public class ProductApplication {
方式2:在启动类上方添加@EnableDiscoveryClient
方式3:启动类不用加注解
启动eureka服务和product服务
浏览器访问
http://localhost:9000/
Instances currently registered with Eureka看到了一行SERVICE-PRODUCT相关数据,说明商品服务成功注册到了Eureka注册中心
与注册到商品服务同样的方式,把order-service(订单服务)
注册到eureka中。
之前的调用方式如下,直接把调用的服务地址写在代码(硬编码)里,如果调用的服务地址变化了,相应调用的地方都需要修改,代码耦合度高。
product = restTemplate.getForObject("http://localhost:9001/product/1", Product.class);
解决代码耦合度高的方法是把所有服务都注册到注册中心,调用时使用的是服务名进行调用,服务名字到注册中心找到(发现)对应的服务地址,然后发起服务调用。
图中服务发现是通过服务名称从Eureka中拿到服务的元数据: 服务的主机名,ip等,只要服务名称不变,服务地址发生变化后只要把最新变化的信息注册到Eureka,就能从Eureka拿到最新的元数据,把元数据中的主机名和ip等信息进行拼接发起服务调用,从而避免服务调用的硬编码问题。
修改OrderController.java
使用服务名称到Eureka发现服务实例
// 通过服务名称获取实例,同一个服务名称可能有多个实例
List instances = discoveryClient.getInstances("SERVICE-PRODUCT");
完整代码如下
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private RestTemplate restTemplate;
/**
* 注入DiscoveryClient
* springcloud提供的获取原数组的工具类
* 调用方法获取服务的元数据信息
*/
@Autowired
private DiscoveryClient discoveryClient;
@RequestMapping(value = "/buy/{id}", method = RequestMethod.GET)
public Product findById(@PathVariable Long id){
Product product = null;
// 通过服务名称获取实例,同一个服务名称可能有多个实例
List instances = discoveryClient.getInstances("SERVICE-PRODUCT");
for (ServiceInstance instance : instances) {
System.out.println(instance);
}
//调用其他微服务,get请求用getXxx post请求用postXxx
product = restTemplate.getForObject("http://localhost:9001/product/1", Product.class);
return product;
}
}
在如下方是添加断点,进行调试。看到instance里面的信息有ipAddr主机信息和port端口信息。
拼接服务主机和端口,进行调用
//获取对应的服务
ServiceInstance instance = instances.get(0);
//解析得到主机和端口
String host = instance.getHost();
int port = instance.getPort();
//调用其他微服务,拼接服务调用url
product = restTemplate.getForObject("http://"+host+":"+port+"/product/1", Product.class);
完整代码如下
package org.example.order.controller;
import org.example.order.entity.Product;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import java.util.List;
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private RestTemplate restTemplate;
/**
* 注入DiscoveryClient
* springcloud提供的获取原数组的工具类
* 调用方法获取服务的元数据信息
*/
@Autowired
private DiscoveryClient discoveryClient;
@RequestMapping(value = "/buy/{id}", method = RequestMethod.GET)
public Product findById(@PathVariable Long id){
Product product = null;
// 通过服务名称(大小写不敏感)获取实例(元数据),同一个服务名称可能有多个实例
List instances = discoveryClient.getInstances("service-product");
// for (ServiceInstance instance : instances) {
// System.out.println(instance);
// }
ServiceInstance instance = instances.get(0);
String host = instance.getHost();
int port = instance.getPort();
//调用其他微服务,get请求用getXxx post请求用postXxx
product = restTemplate.getForObject("http://"+host+":"+port+"/product/1", Product.class);
return product;
}
}
启动eureka服务、product服务、order服务
浏览器访问
http://localhost:9002/order/buy/1
能访问到数据,效果如下
解决了硬编码服务调用问题。
注册中心只有单节点Eureka服务,如果Eureka发生故障,这时候服务调用也被影响,存在单点故障问题。
注册中心的高可用方案是从1台Eureka变为2台Eureka(或更多),即使其中的一台Eureka出现故障,还有其他的Eureka提供服务,确保注册中心的高可用。
具体实现如下:
通过启动两个Eureka实例得到两个Eureka服务
修改eureka_server的application.yml,修改应用名称,把eureka-server1
(9000)向eureka-server2
(8000)注册
spring:
application:
name: eureka-server1
server:
port: 9000
# Eureka配置
eureka:
instance:
hostname: localhost
client:
#配置暴露给Eureka Client的请求地址
service-url:
defaultZone: http://127.0.0.1:8000/eureka/
server:
#关闭自我保护
enable-self-preservation: false
#剔除服务间隔的时间
eviction-interval-timer-in-ms: 4000
启动eureka-server1服务(9000端口)
这时候发现idea控制台输出如下异常,是正常情况,因为8000的实例还没有启动,等8000启动了就好了。
r-0] c.n.d.s.t.d.RedirectingEurekaHttpClient : Request execution error. endpoint=DefaultEndpoint{ serviceUrl='http://127.0.0.1:8000/eureka/}, exception=java.net.ConnectException: Connection refused: connect stacktrace=com.sun.jersey.api.client.ClientHandlerException: java.net.ConnectException: Connection refused: connect
修改eureka_server的application.yml,修改端口号,修改应用名称,把eureka-server2
(8000)向eureka-server1
(9000)注册
spring:
application:
name: eureka-server2
server:
port: 8000
# Eureka配置
eureka:
instance:
hostname: localhost
client:
#配置暴露给Eureka Client的请求地址
service-url:
defaultZone: http://127.0.0.1:9000/eureka/
server:
#关闭自我保护
enable-self-preservation: false
#剔除服务间隔的时间
eviction-interval-timer-in-ms: 4000
再启动一个eureka实例(模拟启动,真实环境应该是在不同机器启动):右键EurakaServerApplication-->Copy Configuration
修改名字
点开Not Started,启动EurekaApplication2
浏览器访问
http://localhost:9000/
http://localhost:8000/
可以看到访问9000,能看到两个实例,访问8000也能看到两个实例。
我们只实例把9000注册到8000,把8000注册到9000,但不管我们访问到哪一个端口,都能看到两个实例,说明两个Eureka之间能进行信息同步。
可以进一步验证这个结论,查看product-service的application.yml,只向9000注册
我们启动product-service,再次查看9000端口和8000端口
只向9000注册SERVICE-PRODUCT
服务,发现9000和8000都有SERVICE-PRODUCT
既然存在两个Eureka,每个服务可以同时向这两个Eureka去获取,两个地址用逗号隔开
修改order和product的配置文件
eureka:
client:
service-url:
defaultZone: http://localhost:9000/eureka/,http://localhost:8000/eureka/
启动oder服务和product服务
分别查看9000和8000端口,能看到order服务和product服务都注册成功了
在其中一台Eureka模拟故障,例如停止EurekaServerApplication2,看注册中心是否依然正常可用
查看9000端口,order和product服务均正常看到,说明Eureka高可用实现了。
查看9000的日志报异常,因为9000向8000注册,8000端口服务停止了,所以属于正常情况。
Caused by: java.net.ConnectException: Connection refused: connect
浏览器访问
http://localhost:9002/order/buy/1
依然能访问到数据,效果如下
完成!enjoy it!