目录
环境说明
jdk1.8
maven3.6.3
mysql8
idea2022
微服务案例的搭建
新建父工程
打开IDEA,File->New ->Project,填写Name(工程名称)和选择Location(工程存储位置),选择Java语言和Maven,点击Create创建maven工程,该工程为所有工程的父工程
官方查看Spring Cloud与Spring Boot的版本匹配问题
Spring Boot2.7.x匹配的Spring Cloud的版本为2021.0.x
修改pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>spring-cloud-bk-2023</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.12</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.4</version>
<scope>provided</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2021.0.8</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>http://repo.spring.io/libs-snapshot-local</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>http://repo.spring.io/libs-milestone-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>http://repo.spring.io/libs-release-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>http://repo.spring.io/libs-snapshot-local</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>http://repo.spring.io/libs-milestone-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- 父工程引入公共的依赖,例如:所有微服务模块均需要用到spring boot,spring boot依赖版本为2.7.12
- 同时声明spring cloud的依赖。spring cloud依赖版本为2021.0.8,声明依赖后,以后子工程就不需要再指定spring cloud的相关版本了。
- dependencies标签里配置远程仓库地址
注意:添加依赖后,需要刷新依赖。
微服务模块
父工程创建好之后,接下来就搭建各个微服务模块,这里以product-service(商品服务)和order-service(订单服务)为例。实现用户下订单的功能。
用户下订单业务流程如下:用户通过浏览器下订单,浏览器发起请求到订单服务,订单服务通过调用商品服务得到商品信息。
product-service(商品服务)
创建子工程
创建product-service子模块,右键父工程->New->Module
填写模块名称:product-service,选择Java,Maven,点击创建,如下图:
添加依赖
修改product-service的pom.xml,在</project>的上方添加如下依赖
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>
刷新依赖
商品模块业务开发
代码结构如下
实体类
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<Product, Long>, JpaSpecificationExecutor<Product> {
}
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表添加两行测试数据,例如:
浏览器访问
访问到了数据库的数据
order-service(订单服务)
创建子工程
子模块:order-service
添加依赖
修改order-service的pom.xml,在</project>的上方添加如下依赖
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>
刷新依赖
订单模块业务开发
代码结构如下:
实体类
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
浏览器访问
效果如下
和之前直接访问product服务返回一致,说明order服务调用了product服务
代码总结:
- 在order启动类,创建RestTemplate对象交给Spring容器管理
- 在order控制类,注入restTemplate对象,在具体方法里调用商品服务:restTemplate.getForObject("http://localhost:9001/product/1", Product.class);
注册中心的使用
这里使用Eureka作为注册中心。
搭建注册中心
创建子工程
在父工程下,创建子工程模块eureka_server
eureka_server代码结构如下
添加依赖
修改eureka_service的pom.xml,在</project>的上方添加如下依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
刷新依赖
注册中心代码开发
启动类
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:剔除服务间隔的时间
测试
运行启动类
浏览器访问
能看到如下界面,说明eureka注册中心服务搭建成功
把服务注册到注册中心
把各个微服务注册到注册中心步骤如下:
1.添加EurekaClient依赖
2.服务注册:修改application.yml添加EurekaServer的信息
3.修改启动类,添加服务发现的支持(可选)
将商品服务注册到注册中心
把product-serviceI商品服务注册到Eureka注册中心。
添加依赖
修改product-service的pom.xml,添加如下依赖
<!-- 引入EurekaClient -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
刷新依赖
服务注册
修改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服务
浏览器访问
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<ServiceInstance> 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<ServiceInstance> 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<ServiceInstance> 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服务
浏览器访问
能访问到数据,效果如下
解决了硬编码服务调用问题。
注册中心的高可用
原理
注册中心只有单节点Eureka服务,如果Eureka发生故障,这时候服务调用也被影响,存在单点故障问题。
注册中心的高可用方案是从1台Eureka变为2台Eureka(或更多),即使其中的一台Eureka出现故障,还有其他的Eureka提供服务,确保注册中心的高可用。
具体实现如下:
- 两个Eureka互相注册(通过启动两个Eureka实例得到两个Eureka服务)
- 各个微服务注册到两台Eureka中
两台Eureka互相注册
通过启动两个Eureka实例得到两个Eureka服务
- Eureka1服务的端口为9000
- Eureka2服务的端口为8000
修改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
浏览器访问
可以看到访问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,每个服务可以同时向这两个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
浏览器访问
依然能访问到数据,效果如下
完成!enjoy it!