Spring Cloud 环境和工程基本搭建
1. 开发环境安装
1.1 JDK
1.1.1 JDK 版本介绍
Oracle 从 JDK9 开始每半年发布一个新版本,新版本发布后,老版本就不再进行维护。但是会有几个长期维护的版本。 目前长期维护的版本有:JDK8, JDK11, JDK17, JDK21。在 JDK 版本的选择上,尽量选择长期维护的版本。 为什么选择 JDK17? Spring Cloud 是基于 SpringBoot 进行开发的,SpringBoot 3.X 以下的版本,Spring 官方已不再进行维护(还可以继续使用),SpringBoot 3.X 的版本,使用的 JDK 版本基线为 JDK17。所以本文选择使用 JDK17。
2. 案例介绍
2.1 需求
实现一个电商平台(不真实实现,仅为演示)。一个电商平台包含的内容非常多,以京东为例,仅从首页上就可以看到巨多的功能。 我们该如何实现呢?如果把这些功能全部写在一个服务里,这个服务将是巨大的。巨多的会员,巨大的流量,微服务架构是最好的选择。 微服务应用开发的第一步,就是服务拆分。拆分后才能进行各自开发。
2.2 服务拆分
服务拆分原则: 微服务到底多小才算'微',这个在业界并没有明确的标准。微服务并不是越小越好,服务越小,微服务架构的优点和缺点都会越来越明显。 服务越小,微服务的独立性就会越来越高,但同时,微服务的数量也会越多,管理这些微服务的难度也会提高。所以服务拆分也要考虑场景。
还是以企业管理为例 企业中一个员工的工作内容与企业规模、项目规模等都有关系。 在小公司,一个员工可能需要负责很多部门的事情,大公司的话,一个部门的工作可能需要多个员工来处理。
拆分微服务一般遵循如下原则:
- 单一职责原则 单一职责原则原本是面向对象设计中的一个基本原则,它指的是一个类应该专注于单一功能。不要存在多于一个导致类变更的原因。在微服务架构中,一个微服务也应该只负责一个功能或业务领域,每个服务应该有清晰的定义和边界,只关注自己的特定业务领域。
- 服务自治 服务自治是指每个微服务都应该具备高度自治的能力,即每个服务要能做到独立开发,独立测试,独立构建,独立部署,独立运行。 以上面的电商系统为例,每一个微服务应该有自己的存储,配置,在进行开发、构建、部署、运行和测试时,并不需要过多关注其他微服务的状态和数据。
- 单向依赖 微服务之间需要做到单向依赖,严禁循环依赖,双向依赖。 循环依赖:A -> B -> C -> A 双向依赖:A -> B, B -> A
微服务架构并无标准架构,合适的就是最好的,不然架构师大会也不会各个系统架构百花齐放了。 在架构设计的过程中,坚持'合适优于业界领先',避免'过度设计'(为了设计而设计)。很多业界领先方案并不是一群天才在某个时期一下子做出来的,而是经过数年的发展逐步完善。业界领先的方案大多是'逼'出来的,随着业务的发展,量变导致质变,新的问题出现了,当前的方案无法满足需求,需要用新的方案来解决。通过不断的创新和尝试,业界领先的方案才得以形成。
服务拆分示例: 一个完整的电商系统是庞大的,咱们课程中重点关注如何使用 SpringCloud 解决微服务架构中遇到的问题。 以订单列表为例:订单列表需要包含的主要信息有:
- 订单列表
- 商品信息
根据服务的单一职责原则,我们把服务进行拆分为:订单服务,商品服务。 订单服务:提供订单 ID,获取订单详细信息。 商品服务:根据商品 ID,返回商品详细信息。
3. 数据准备
根据服务自治原则,每个服务都应有自己独立的数据库。
订单服务:
-- 建库
create database if not exists cloud_order charset utf8mb4;
-- 订单表
DROP TABLE IF EXISTS order_detail;
CREATE TABLE order_detail (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '订单 id',
`user_id` BIGINT(20) NOT NULL COMMENT '用户 ID',
`product_id` BIGINT(20) NULL COMMENT '产品 id',
`num` INT(10) NULL DEFAULT 0 COMMENT '下单数量',
`price` BIGINT(20) NOT NULL COMMENT '实付款',
`delete_flag` TINYINT(4) NULL DEFAULT 0,
`create_time` DATETIME DEFAULT now(),
`update_time` DATETIME DEFAULT now(),
PRIMARY KEY (id)
) ENGINE = INNODB DEFAULT CHARACTER SET = utf8mb4 COMMENT ='订单表';
-- 数据初始化
insert into order_detail (user_id, product_id, num, price)
values (2001, 1001, 1, 99), (2002, 1002, 1, 30), (2001, 1003, 1, 40), (2003, 1004, 3, 58), (2004, 1005, 7, 85), (2005, 1006, 7, 94);
商品服务:
create database if not exists cloud_product charset utf8mb4;
-- 产品表
DROP TABLE IF EXISTS product_detail;
CREATE TABLE product_detail (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '产品 id',
`product_name` varchar(128) NULL COMMENT '产品名称',
`product_price` BIGINT(20) NOT NULL COMMENT '产品价格',
`state` TINYINT(4) NULL DEFAULT 0 COMMENT '产品状态 0-有效 1-下架',
`create_time` DATETIME DEFAULT now(),
`update_time` DATETIME DEFAULT now(),
PRIMARY KEY (id)
) ENGINE = INNODB DEFAULT CHARACTER SET = utf8mb4 COMMENT ='产品表';
-- 数据初始化
insert into product_detail (id, product_name, product_price, state)
values (1001, "T 恤", 101, 0), (1002, "短袖", 30, 0), (1003, "短裤", 44, 0), (1004, "卫衣", 58, 0), (1005, "马甲", 98, 0), (1006, "羽绒服", 101, 0), (1007, "冲锋衣", 30, 0), (1008, "袜子", 44, 0), (1009, "鞋子", 58, 0), (10010, "毛衣", 98, 0);
4. 工程搭建
4.1 构建父子工程
4.1.1 创建父工程
- 创建一个空的 Maven 项目,删除所有代码,只保留 pom.xml。
- 目录结构:保持标准的 Maven 目录结构。
- 完善 pom 文件:使用 properties 来进行版本号的统一管理,使用 dependencyManagement 来管理依赖,声明父工程的打包方式为 pom。
<?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-demo</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.6</version>
<relativePath/>
</parent>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<java.version>17</java.version>
<mybatis.version>3.0.3</mybatis.version>
<mysql.version>8.0.33</mysql.version>
<spring-cloud.version>2022.0.3</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<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>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter-test</artifactId>
<version>${mybatis.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
DependencyManagement 和 Dependencies:
- dependencies:将所依赖的 jar 直接加到项目中。子项目也会继承该依赖。
- dependencyManagement:只是声明依赖,并不实现 Jar 包引入。如果子项目需要用到相关依赖,需要显式声明。如果子项目没有指定具体版本,会从父项目中读取 version。如果子项目中指定了版本号,就会使用子项目中指定的 jar 版本。此外父工程的打包方式应该是 pom,不是 jar,这里需要手动使用 packaging 来声明。
4.1.2 创建子项目 - 订单服务
做如下操作:创建模块并声明项目依赖和项目构建插件。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
4.1.3 创建子项目 - 商品服务
创建模块部分同上,下面声明项目依赖和项目构建插件。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
4.2 完善订单服务
4.2.1 完善启动类,配置文件
启动类:
@SpringBootApplication
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
配置文件(application.yml):
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/cloud_order?characterEncoding=utf8&useSSL=false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
configuration:
# 配置打印 MyBatis 日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true # 配置驼峰自动转换
4.2.2 业务代码
- 实体类:
@Data
public class OrderInfo {
private Integer id;
private Integer userId;
private Integer productId;
private Integer num;
private Integer price;
private Integer deleteFlag;
private Date createTime;
private Date updateTime;
}
- Controller:
@RequestMapping("/order")
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@RequestMapping("/{orderId}")
public OrderInfo getOrderById(@PathVariable("orderId") Integer orderId) {
return orderService.selectOrderById(orderId);
}
}
- Service:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
public OrderInfo selectOrderById(Integer orderId) {
OrderInfo orderInfo = orderMapper.selectOrderById(orderId);
return orderInfo;
}
}
- Mapper:
@Mapper
public interface OrderMapper {
@Select("select * from order_detail where id=#{orderId}")
OrderInfo selectOrderById(Integer orderId);
}
完善商品服务类似,故代码不展示。
5. 远程调用
5.1 需求
根据订单查询订单信息时,根据订单里产品 ID,获取产品的详细信息。
5.2 实现
实现思路: order-service 服务向 product-service 服务发送一个 HTTP 请求,把得到的返回结果,和订单结果融合在一起,返回给调用方。 实现方式:采用 Spring 提供的 RestTemplate。
- 定义 RestTemplate:
@Configuration
public class BeanConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
- 修改 order-service 中的 OrderService:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RestTemplate restTemplate;
public OrderInfo selectOrderById(Integer orderId) {
OrderInfo orderInfo = orderMapper.selectOrderById(orderId);
String url = "http://127.0.0.1:9090/product/" + orderInfo.getProductId();
ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class);
orderInfo.setProductInfo(productInfo);
return orderInfo;
}
}
6. RestTemplate 详解
RestTemplate 是从 Spring3.0 开始支持的一个 HTTP 请求工具,它是一个同步的 REST API 客户端,提供了常见的 REST 请求方案的模版。
什么是 REST? REST (Representational State Transfer),表现层资源状态转移。 REST 是由 HTTP 的主要设计者提出来的软件架构风格。这里面主要有三个概念:
- 资源:网络上的所有事物都可以抽象为资源,每个资源都有一个唯一的资源标识符 (URI)。
- 表现层:资源的表现形式,比如文本作为资源,可以用 txt 格式表现,也可以通过 HTML, XML, JSON 等格式来表现,甚至以二进制的格式表现。
- 状态转移:访问 URI,也就是客户端和服务器的交互过程。客户端用到的手段,只能是 HTTP 协议。这个过程中,可能会涉及到数据状态的变化。比如对数据的增删改查,都是状态的转移。
REST 是一种设计风格,指资源在网络中以某种表现形式进行状态转移。 简单来说:REST 描述的是在网络中 Client 和 Server 的一种交互形式,REST 本身不实用,实用的是如何设计 RESTful API(REST 风格的网络接口)。
什么是 RESTful? REST 是一种设计风格,并没有一个明确的标准。满足这种设计风格的程序或接口我们称之为 RESTful(从单词字面来看就是一个形容词)。所以 RESTful API 就是满足 REST 架构风格的接口。
RESTful 风格大致有以下几个主要特征:
- 资源:资源可以是一个图片,音频,视频或者 JSON 格式等网络上的一个实体,除了二进制的资源外普通的文本资源更多以 JSON 为载体、面向用户的一组数据 (通常从数据库中查询而得到)。
- 统一接口:对资源的操作。比如获取,创建,修改和删除。这些操作正好对应 HTTP 协议提供的 GET、POST、PUT 和 DELETE 方法。换而言之,如果使用 RESTful 风格的接口,从接口上你可能只能定位其资源,但是无法知晓它具体进行了什么操作,需要具体了解其发生了什么操作动作要从其 HTTP 请求方法类型上进行判断。
比如同一个的 URL:
- GET /blog/{blogId}:查询博客
- DELETE /blog/{blogId}:删除博客 这些内容都是通过 HTTP 协议来呈现的。所以 RESTful 是基于 HTTP 协议的。 RestTemplate 是 Spring 提供,封装 HTTP 调用,并强制使用 RESTful 风格。它会处理 HTTP 连接和关闭,只需要使用者提供资源的地址和参数即可。
RESTful 实践 RESTful 风格的 API 固然很好很规范,但大多数互联网公司并没有按照其规则来设计,因为 REST 是一种风格,而不是一种约束或规则,过于理想的 RESTful API 会付出太多的成本。 RESTful API 缺点:
- 操作方式繁琐,RESTful API 通常根据 GET, POST, PUT, DELETE 来区分对资源的操作动作。但是 HTTP Method 并不可直接见到,需要通过抓包等工具才能观察。如果把动作放在 URL 上反而更加直观,更利于团队的理解和交流。
- 一些浏览器对 GET, POST 之外的请求支持不太友好,需要额外处理。
- 过分强调资源。而实际业务需求可能比较复杂,并不能单纯使用增删改查就能满足需求,强行使用 RESTful API 会增加开发难度和成本。 所以,在实际开发中,如果业务需求和 RESTful API 不太匹配或者很麻烦时,也可以不用 RESTful API。如果使用场景和 REST 风格比较匹配,就可以采用 RESTful API。 总之:无论哪种风格的 API,都是为了方便团队开发,协商以及管理,不能墨守成规。尽信规范不如无规范。
7. 项目存在问题
- 远程调用时,URL 的 IP 和端口号是写死的 (
http://127.0.0.1:9090/product/),如果更换 IP,需要修改代码。- 调用方如何可以不依赖服务提供方的 IP?
- 多机部署,如何分摊压力?
- 远程调用时,URL 非常容易写错,而且复用性不高,如何优雅的实现远程调用。
- 所有的服务都可以调用该接口,是否有风险?
除此之外,微服务架构还面临很多问题,后续文章将为大家介绍学习如何使用 Spring Cloud 来解决这些问题。
8. 总结
本文搭建了 Spring Cloud 后续组件使用的基本工程环境,介绍了服务拆分原则及基于 RestTemplate 的远程调用实现,并指出了当前方案存在的局限性,为后续深入学习服务发现与负载均衡打下基础。


