图源:laiketui.com
Spring Cloud 的基本宗旨是将项目进行拆分,并分别开发、部署和统一管理。
本文将搭建一个基本的 Spring Cloud 框架,并创建两个子模块,两个子模块之间会进行最简单的接口调用进行交互,这可以体现最简单的分布式架构。
这个架构会在之后进行一步步完善。
创建一个 Maven 项目,默认生成的 POM 文件如下:
<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.0modelVersion>
<groupId>org.examplegroupId>
<artifactId>shoppingartifactId>
<version>1.0version>
<properties>
<maven.compiler.source>18maven.compiler.source>
<maven.compiler.target>18maven.compiler.target>
properties>
project>
修改 POM 文件:
<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.0modelVersion>
<groupId>org.examplegroupId>
<artifactId>shoppingartifactId>
<version>1.0version>
<modules>
<module>shopping-usermodule>
<module>shopping-ordermodule>
modules>
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<java.version>1.8java.version>
<spring-cloud.version>Hoxton.SR10spring-cloud.version>
<mysql.version>5.1.47mysql.version>
<mybatis.version>2.1.1mybatis.version>
properties>
<packaging>pompackaging>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.9.RELEASEversion>
<relativePath/>
parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>${mysql.version}version>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>${mybatis.version}version>
dependency>
dependencies>
dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
dependencies>
project>
需要注意的是,Spring Cloud 的版本与项目使用的 Spring Boot 版本有对应关系,所以这里使用的 Spring Cloud 版本是 Hoxton.SR10,其对应的 Spring Boot 版本是 2.3.x,具体这里使用了 2.3.9 版本。
- 完整的 Spring Cloud 与 Spring Boot 版本对应列表见 Spring 官网。
- 这里删除了 maven.compiler.source 和 maven.compiler.target 配置,否则会报错。
- lombok 依赖要放在 dependencyManagement 节点外部,否则子模块无法使用。
删除项目中的src
目录,因为根项目不需要添加任何代码。
在根项目上右键 New->Module 添加模块。
模块同样以 Maven 项目的方式创建,过程类似根项目:
这个子模块我命名为 Shopping-order,作为订单相关的微服务。
用同样的方式创建子模块 shopping-user。
自动生成的子模块 pom.xml 文件内容如下:
<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">
<parent>
<artifactId>shoppingartifactId>
<groupId>org.examplegroupId>
<version>1.0version>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>shopping-orderartifactId>
<properties>
<maven.compiler.source>18maven.compiler.source>
<maven.compiler.target>18maven.compiler.target>
properties>
project>
为其添加必要的依赖和插件:
<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">
<parent>
<artifactId>shoppingartifactId>
<groupId>org.examplegroupId>
<version>1.0version>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>shopping-orderartifactId>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
子模块只需要添加3个必须依赖:
- 子模块的依赖不需要指定版本,因为这些都在父项目中定义好了。
- 如果 Maven 下载依赖出错,可能是国内最常用的阿里云镜像的问题(可能该镜像站不保存老旧版本的依赖),本人通过注释阿里云镜像,改从其它镜像站(比如 repo1.maven.org)正常下载。更多的 Maven 镜像站地址见这里。
另一个子模块以同样的方式添加依赖。
在子模块 shopping-order 的 src/main/java 目录下添加包 org.example.shopping.order 作为子模块的根包。
在该包下添加子模块的入口类 OrderApplication:
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
对另一个子模块以同样的方式添加包 org.example.shopping.user 和入口类 UserApplication。
为子模块 shopping-order 添加配置文件 application.yml:
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
username: root
password: mysql
driver-class-name: com.mysql.jdbc.Driver
mybatis:
type-aliases-package: org.example.shopping.order.entity
configuration:
map-underscore-to-camel-case: true
logging:
level:
org.example.shopping.order: debug
pattern:
dateformat: MM-dd HH:mm:ss:SSS
添加两个数据库 cloud_user 和 cloud_order,分别对应两个子模块。
为另一个子模块添加类似的配置文件。
注意,要将两个子模块启动端口区分开,因为它们需要同时运行。
现在启动一下两个子模块,应该都可以正常启动了。
下面给两个子模块分别添加两个作为示例的接口,一个用于查询订单数据,另一个用于查询用户信息。
在这之前先创建两张表:
CREATE TABLE `tb_order` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单id',
`user_id` bigint NOT NULL COMMENT '用户id',
`name` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8_general_ci DEFAULT NULL COMMENT '商品名称',
`price` bigint NOT NULL COMMENT '商品价格',
`num` int DEFAULT '0' COMMENT '商品数量',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `username` (`name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=109 DEFAULT CHARSET=utf8mb3 ROW_FORMAT=COMPACT;
CREATE TABLE `tb_user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8_general_ci DEFAULT NULL COMMENT '收件人',
`address` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8_general_ci DEFAULT NULL COMMENT '地址',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `username` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=109 DEFAULT CHARSET=utf8mb3 ROW_FORMAT=COMPACT;
两张表分别保存在 cloud_order 和 cloud_user 数据库中。
测试数据:
mysql> select * from tb_user;
+----+----------+--------------------+
| id | username | address |
+----+----------+--------------------+
| 1 | 柳岩 | 湖南省衡阳市 |
| 2 | 文二狗 | 陕西省西安市 |
| 3 | 华沉鱼 | 湖北省十堰市 |
| 4 | 张必沉 | 天津市 |
| 5 | 郑爽爽 | 辽宁省沈阳市大东区 |
| 6 | 范兵兵 | 山东省青岛市 |
+----+----------+--------------------+
mysql> select * from tb_order;
+-----+---------+----------------------------------+--------+------+
| id | user_id | name | price | num |
+-----+---------+----------------------------------+--------+------+
| 101 | 1 | Apple 苹果 iPhone 12 | 699900 | 1 |
| 102 | 2 | 雅迪 yadea 新国标电动车 | 209900 | 1 |
| 103 | 3 | 骆驼(CAMEL)休闲运动鞋女 | 43900 | 1 |
| 104 | 4 | 小米10 双模5G 骁龙865 | 359900 | 1 |
| 105 | 5 | OPPO Reno3 Pro 双模5G 视频双防抖 | 299900 | 1 |
| 106 | 6 | 美的(Midea) 新能效 冷静星II | 544900 | 1 |
| 107 | 2 | 西昊/SIHOO 人体工学电脑椅子 | 79900 | 1 |
| 108 | 3 | 梵班(FAMDBANN)休闲男鞋 | 31900 | 1 |
+-----+---------+----------------------------------+--------+------+
下面为子模块 shopping-order 添加使用 MyBatis 的持久层代码:
package org.example.shopping.order.entity;
// ...
@Data
public class Order {
private Long id;
private Long userId;
private String name;
private Long price;
private Integer num;
}
package org.example.shopping.order.mapper;
// ...
public interface OrderMapper {
@Select("select * from tb_order where id = #{id}")
Order findById(Long id);
}
为了能让 MyBatis 检索到 Mapper 目录,需要添加:
@MapperScan("org.example.shopping.order.mapper")
@SpringBootApplication
public class OrderApplication {
// ...
}
编写 Service :
package org.example.shopping.order.service;
// ...
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
public Order findOrderById(Long orderId) {
Order order = orderMapper.findById(orderId);
return order;
}
}
编写 Controller :
package org.example.shopping.order.controller;
// ...
@RestController
@RequestMapping("/order")
@Validated
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("/{id}")
public Result<Order> getOrderInfo(@Min(1) @NotNull @PathVariable Long id) {
return Result.success(orderService.findOrderById(id));
}
}
Controller 中使用了 Hibernate Validation 对入参进行校验,所以需要在根项目的 POM 文件中添加相关依赖:
<project>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-validationartifactId>
dependency>
dependencies>
project>
为了让接口返回统一格式,还使用了一个辅助类 Result :
@Data
@NoArgsConstructor
public class Result<T> {
T data;
@NonNull
String errorMsg;
@NonNull
String errorCode;
boolean success;
private static final String DEFAULT_ERROR_CODE = "error.default";
private Result(T data, @NonNull String errorMsg, @NonNull String errorCode, boolean success) {
this.data = data;
this.errorMsg = errorMsg;
this.errorCode = errorCode;
this.success = success;
}
public static <D> Result<D> success(D data) {
return new Result<>(data, "", "", true);
}
public static Result<Object> fail(String errorMsg, String errorCode) {
return new Result<>(null, errorMsg, errorCode, false);
}
public static Result<Object> fail(String errorMsg) {
return fail(errorMsg, DEFAULT_ERROR_CODE);
}
}
现在重新启动子模块 shopping-order,在浏览器访问 http://localhost:8080/order/101,没有意外的话会看到类似下面的输出:
{
"data": {
"id": 101,
"userId": 1,
"name": "Apple 苹果 iPhone 12 ",
"price": 699900,
"num": 1
},
"errorMsg": "",
"errorCode": "",
"success": true
}
用类似的方式为 shopping-user 编写一个接口,用于查询用户信息。
现在编写两个子模块之间的交互,我们希望 shopping-order 的订单信息接口返回的数据中包含用户信息。
为了能让 shopping-order 调用 shopping-user 的接口,添加一个RestTemplate
:
package org.example.shopping.order;
// ...
@Configuration
public class WebConfig {
@Bean
RestTemplate restTemplate(){
return new RestTemplate();
}
}
修改 Service :
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RestTemplate restTemplate;
public Order findOrderById(Long orderId) {
Order order = orderMapper.findById(orderId);
String url = String.format("http://localhost:8081/user/%d", order.getUserId());
Result<?> result = restTemplate.getForObject(url, Result.class);
User user = Result.parseData(result, User.class);
order.setUser(user);
return order;
}
}
在获取到订单信息后,利用订单中的用户 ID 查询 shopping-user 的用户信息接口以获取用户信息,并组装到返回数据中。
虽然RestTemplate.getForObject
可以将接口返回的 JSON 串反序列化为指定对象,但因为Result
是一个泛型,所以并不能正确解析出其中的data
属性类型,所以这里使用一个辅助方法Result.parseData
完成二次转换:
package org.example.shopping.order;
// ...
@Data
@NoArgsConstructor
public class Result<T> {
// ...
@SneakyThrows
public static <D> D parseData(Result<?> result, Class<D> dataCls) {
if (result == null || result.getData() == null) {
return null;
}
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(result.getData());
return objectMapper.readValue(json, dataCls);
}
}
重启子模块 shopping-order,并访问 http://localhost:8080/order/101,应该可以看到以下输出:
{
"data": {
"id": 101,
"userId": 1,
"name": "Apple 苹果 iPhone 12 ",
"price": 699900,
"num": 1,
"user": {
"id": 1,
"userName": "柳岩",
"address": "湖南省衡阳市"
}
},
"errorMsg": "",
"errorCode": "",
"success": true
}
本文的完整示例代码可以从这里获取。