项目总体结构
项目总体上包含一个Maven父工程,实体类模块、工具类模块、用户微服务、商品微服务和订单微服务都以Maven子模块的形式存在,项目总体结果如下所示。
其中每个部分的含义如下所示。
其中各模块的说明如下所示:
- shop-springcloud-alibaba:Maven父工程。
- shop-bean:各服务都会使用的JavaBean模块,包含实体类、Dto、Vo等JavaBean。
- shop-utils:各服务都会使用的工具类模块。
- shop-order:订单微服务,监听的端口为8080。
- shop-product:商品微服务,监听的端口为8070。
- shop-user:用户微服务,监听的端口为8060。
创建Maven父工程
这里,我使用的开发环境是大家都比较熟悉的IDEA,关于IDEA的使用,这里我就不再赘述了,如果有对IDEA的使用不太熟悉的小伙伴,那就自行百度或者谷歌吧,今天我们先重点撸源码。
在IDEA中创建Maven工程,名称为shop-springcloud-alibaba,创建后在项目的pom.xml文件中添加StringBoot与SpringCloud alibaba相关的配置,如下所示。
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.6.RELEASE</version> </parent> <properties> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <spring-cloud.version>Greenwich.RELEASE</spring-cloud.version> <spring-cloud-alibaba.version>2.1.0.RELEASE</spring-cloud-alibaba.version> <logback.version>1.1.7</logback.version> <slf4j.version>1.7.21</slf4j.version> <common.logging>1.2</common.logging> <fastjson.version>1.2.51</fastjson.version> <mybatis.version>3.4.6</mybatis.version> <mybatis.plus.version>3.4.1</mybatis.plus.version> <mysql.jdbc.version>8.0.19</mysql.jdbc.version> <druid.version>1.1.10</druid.version> </properties> <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>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring-cloud-alibaba.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
创建工具类模块
在父工程下创建工具类模块shop-utils,作为整个项目的通用工具类模块。工具类模块的总体结构如下所示。
添加项目依赖
在shop-utils模块的pom.xml文件中添加项目依赖的一些类库,如下所示。
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.jdbc.version}</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>${druid.version}</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>${druid.version}</version> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>${common.logging}</version> </dependency> <!-- log --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>${slf4j.version}</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>${logback.version}</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>${fastjson.version}</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.8</version> </dependency> </dependencies>
核心类开发
1.创建HTTP状态码封装类
在项目的io.binghe.shop.utils.constants包下创建HttpCode类,作为HTTP状态码的常量类。这里,暂时定义了两个状态码,200表示处理成功,500表示服务器异常,源码如下所示。
/** * @author binghe * @version 1.0.0 * @description http状态码 */ public class HttpCode { /** * 成功的状态码 */ public static final int SUCCESS = 200; /** * 错误状态码 */ public static final int FAILURE = 500; }
2.创建全局异常捕获类
在项目的io.binghe.shop.utils.exception包下新建全局异常捕获类RestCtrlExceptionHandler,统一捕获整个项目抛出的Exception异常,源码如下所示。
/** * @author binghe * @version 1.0.0 * @description 全局异常处理器 */ @RestControllerAdvice public class RestCtrlExceptionHandler { private final Logger logger = LoggerFactory.getLogger(RestCtrlExceptionHandler.class); /** * 全局异常处理,统一返回状态码 */ @ExceptionHandler(Exception.class) public Result<String> handleException(Exception e) { logger.error("服务器抛出了异常:{}", e); return new Result<String>(HttpCode.FAILURE, "执行失败", e.getMessage()); } }
3.创建通用MD5与密码加密类
在io.binghe.shop.utils.md5包下新建MD5Hash类,提供通用的MD5加密算法,在io.binghe.shop.utils.psswd包下新建PasswordUtils类,提供密码的加密功能。这两个类的实现比较简单,这里就不再赘述了。感兴趣的小伙伴加入 【冰河技术】 知识星球获取源码。
4.创建通用数据响应类
在项目的io.binghe.shop.utils.resp包下新建Result类,用于封装统一的数据返回格式,源码如下所示。
/** * @author binghe * @version 1.0.0 * @description 返回的结果数据 */ @Data @NoArgsConstructor @AllArgsConstructor public class Result<T> implements Serializable { private static final long serialVersionUID = 1497405107265595284L; /** * 状态码 */ private Integer code; /** * 状态描述 */ private String codeMsg; /** * 返回的数据 */ private T data; }
这里,需要注意的是:在Result类中使用了泛型,返回的具体业务数据类型会根据泛型的具体类型确定。Result类中的每个字段的含义如下所示。
- code:返回的状态码。
- codeMsg:返回的状态描述信息。
- data:具体的业务数据,数据类型根据泛型确定。
5.创建分布式id核心类
(1)在项目的io.binghe.shop.utils.id包下创建实现整个分布式id最核心的类SnowFlake,SnowFlake类主要是使用Java实现了雪花算法,具体的逻辑见如下源码。
/** * @author binghe * @version 1.0.0 * @description 雪花算法生成分布式序列号 */ public class SnowFlake { /** * 起始的时间戳:2022-04-12 11:56:45,使用时此值不可修改 */ private final static long START_STAMP = 1649735805910L; /** * 每一部分占用的位数 */ private final static long SEQUENCE_BIT = 12; //序列号占用的位数 private final static long MACHINE_BIT = 5; //机器标识占用的位数 private final static long DATACENTER_BIT = 5;//数据中心占用的位数 /** * 每一部分的最大值 */ private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT); private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT); private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT); /** * 每一部分向左的位移 */ private final static long MACHINE_LEFT = SEQUENCE_BIT; private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT; private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT; private long datacenterId; //数据中心 private long machineId; //机器标识 private long sequence = 0L; //序列号 private long lastStmp = -1L;//上一次时间戳 public SnowFlake(long datacenterId, long machineId) { if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) { throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0"); } if (machineId > MAX_MACHINE_NUM || machineId < 0) { throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0"); } this.datacenterId = datacenterId; this.machineId = machineId; } /** * 产生下一个ID */ public synchronized long nextId() { long currStmp = getNewstmp(); if (currStmp < lastStmp) { throw new RuntimeException("Clock moved backwards. Refusing to generate id"); } if (currStmp == lastStmp) { //相同毫秒内,序列号自增 sequence = (sequence + 1) & MAX_SEQUENCE; //同一毫秒的序列数已经达到最大 if (sequence == 0L) { currStmp = getNextMill(); } } else { //不同毫秒内,序列号置为0 sequence = 0L; } lastStmp = currStmp; return (currStmp - START_STAMP) << TIMESTMP_LEFT //时间戳部分 | datacenterId << DATACENTER_LEFT //数据中心部分 | machineId << MACHINE_LEFT //机器标识部分 | sequence; //序列号部分 } private long getNextMill() { long mill = getNewstmp(); while (mill <= lastStmp) { mill = getNewstmp(); } return mill; } private long getNewstmp() { return System.currentTimeMillis(); } public static Long getMaxDataCeneterNum() { return MAX_DATACENTER_NUM; } public static Long getMaxMachineNum() { return MAX_MACHINE_NUM; } }
根据雪花算法的实现可以发现,SnowFlake类提供了一个有参构造函数,如下所示。
public SnowFlake(long datacenterId, long machineId) { if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) { throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0"); } if (machineId > MAX_MACHINE_NUM || machineId < 0) { throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0"); } this.datacenterId = datacenterId; this.machineId = machineId; }
其中,第一个参数datacenterId表示数据中心id,也可以认为是机房的id,machineId表示机器id,也可以认为是服务所在的服务器id。在SnowFlake的构造方法中,对这两个参数进行了限制,如下所示。
- datacenterId:大于或者等于0,并且要小于MAX_DATACENTER_NUM,也就是小于31。
- machineId:大于或者等于0,并且要小于MAX_DATACENTER_NUM,也就是小于31。
所以,类实现的雪花算法支持32个不同的数据中心或机房,并且在每个数据中心或机房中支持32个机器上部署分布式id服务。这对一般的场景来说,已经足够了。
注意:雪花算法的实现强依赖时间戳,所以在SnowFlake源码中存在如下常量,并标注了使用时此值不可更改的注释。
/** * 起始的时间戳:2022-04-12 11:56:45,使用时此值不可修改 */ private final static long START_STAMP = 1649735805910L;
有关雪花算法的核心原理,以及如何实现在分布式场景下做到id唯一并且整体呈现递增趋势,会在后续的拓展篇中详细介绍,这里就不再赘述了,我们先把工具类和实体类的源码撸完。
(2)为了防止每次使用SnowFlake类时都会新建一个对象,这里,在io.binghe.shop.utils.id包下新建SnowFlakeFactory类,作为SnowFlake的简单工厂类,在SnowFlakeFactory类中,主要是定义了一个ConcurrentMap类型的成员变量snowFlakeCache用来缓存SnowFlake类的对象,这样就不用在使用SnowFlake类时,每次都要新建一个类对象了。
也许有小伙伴会问:不就是新建一个对象嘛,为啥还要缓存起来呢。
其实,在普通场景下,新建不新建对象,缓存不缓存对象几乎没啥影响,但是在高并发、大流量的场景下,尤其是冰河经历过了高并发、大流量的秒杀系统,如果每次都创建对象的话,系统的性能与资源损耗还是比较大的。
SnowFlakeFactory类的源码如下所示。
/** * @author binghe * @version 1.0.0 * @description 雪花算法工厂 */ public class SnowFlakeFactory { /** * 默认数据中心id */ private static final long DEFAULT_DATACENTER_ID = 1; /** * 默认的机器id */ private static final long DEFAULT_MACHINE_ID = 1; /** * 默认的雪花算法句柄 */ private static final String DEFAULT_SNOW_FLAKE = "snow_flake"; /** * 缓存SnowFlake对象 */ private static ConcurrentMap<String, SnowFlake> snowFlakeCache = new ConcurrentHashMap<>(2); public static SnowFlake getSnowFlake(long datacenterId, long machineId) { return new SnowFlake(datacenterId, machineId); } public static SnowFlake getSnowFlake() { return new SnowFlake(DEFAULT_DATACENTER_ID, DEFAULT_MACHINE_ID); } public static SnowFlake getSnowFlakeFromCache() { SnowFlake snowFlake = snowFlakeCache.get(DEFAULT_SNOW_FLAKE); if(snowFlake == null) { snowFlake = new SnowFlake(DEFAULT_DATACENTER_ID, DEFAULT_MACHINE_ID); snowFlakeCache.put(DEFAULT_SNOW_FLAKE, snowFlake); } return snowFlake; } /** * 根据数据中心id和机器id从缓存中获取全局id * @param dataCenterId: 取值为1~31 * @param machineId: 取值为1~31 */ public static SnowFlake getSnowFlakeByDataCenterIdAndMachineIdFromCache(Long dataCenterId, Long machineId) { if (dataCenterId > SnowFlake.getMaxDataCeneterNum() || dataCenterId < 0) { throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0"); } if (machineId > SnowFlake.getMaxMachineNum() || machineId < 0) { throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0"); } String key = DEFAULT_SNOW_FLAKE.concat("_").concat(String.valueOf(dataCenterId)).concat("_").concat(String.valueOf(machineId)); SnowFlake snowFlake = snowFlakeCache.get(key); if(snowFlake == null) { snowFlake = new SnowFlake(dataCenterId, machineId); snowFlakeCache.put(key, snowFlake); } return snowFlake; } }
在SnowFlakeFactory类中,主要对外提供了两个获取SnowFlake的方法,一个是getSnowFlakeFromCache()方法,另一个是getSnowFlakeByDataCenterIdAndMachineIdFromCache()方法。
- getSnowFlakeFromCache()方法
在snowFlakeCache缓存中获取默认的SnowFlake对象实例,如果对象不存在,则调用SnowFlake类的构造方法,并且传入默认的数据中心id和机器id,将实例化后的SnowFlake对象加入缓存,并且返回SnowFlake对象。源码如下所示。
public static SnowFlake getSnowFlakeFromCache() { SnowFlake snowFlake = snowFlakeCache.get(DEFAULT_SNOW_FLAKE); if(snowFlake == null) { snowFlake = new SnowFlake(DEFAULT_DATACENTER_ID, DEFAULT_MACHINE_ID); snowFlakeCache.put(DEFAULT_SNOW_FLAKE, snowFlake); } return snowFlake; }
- getSnowFlakeByDataCenterIdAndMachineIdFromCache()方法
getSnowFlakeByDataCenterIdAndMachineIdFromCache()方法提供了两个参数,一个是Long类型的dataCenterId,表示数据中心或者机房的id,一个是Long类型的machineId,表示机器id或者服务所在的服务器id。
在getSnowFlakeByDataCenterIdAndMachineIdFromCache()方法中,会对传入的两个参数进行限制。然后生成缓存SnowFlake对象实例的缓存Key,根据生成的Key到snowFlakeCache缓存中获取SnowFlake对象实例,如果对象实例不存在,则根据传入的dataCenterId和machineId生成SnowFlake对象实例,并放入snowFlakeCache缓存中,最后返回SnowFlake对象实例。源码如下所示。
/** * 根据数据中心id和机器id从缓存中获取全局id * @param dataCenterId: 取值为1~31 * @param machineId: 取值为1~31 */ public static SnowFlake getSnowFlakeByDataCenterIdAndMachineIdFromCache(Long dataCenterId, Long machineId) { if (dataCenterId > SnowFlake.getMaxDataCeneterNum() || dataCenterId < 0) { throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0"); } if (machineId > SnowFlake.getMaxMachineNum() || machineId < 0) { throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0"); } String key = DEFAULT_SNOW_FLAKE.concat("_").concat(String.valueOf(dataCenterId)).concat("_").concat(String.valueOf(machineId)); SnowFlake snowFlake = snowFlakeCache.get(key); if(snowFlake == null) { snowFlake = new SnowFlake(dataCenterId, machineId); snowFlakeCache.put(key, snowFlake); } return snowFlake; }
(3)为了便于管理每个服务的dataCenterId和machineId,这里将每个服务的dataCenterId和machineId作为配置参数,后续也可以存储到Zookeeper或者Etcd等分布式配置中心。
所以,在io.binghe.shop.utils.id包下新建SnowFlakeLoader类,io.binghe.shop.utils.id.SnowFlakeLoader类的作用主要是加载classpath类路径下的snowflake/snowflake.properties文件,读取dataCenterId和machineId,SnowFlakeLoader类的源码如下所示。
/** * @author binghe * @version 1.0.0 * @description 定义加载params.properties文件的工具类 */ public class SnowFlakeLoader { public static final String DATA_CENTER_ID = "data.center.id"; public static final String MACHINE_ID = "machine.id"; private volatile static Properties instance; static { InputStream in = SnowFlakeLoader.class.getClassLoader().getResourceAsStream("snowflake/snowflake.properties"); instance = new Properties(); try { instance.load(in); } catch (IOException e) { e.printStackTrace(); } } private static String getStringValue(String key){ if(instance == null) return ""; return instance.getProperty(key, ""); } private static Long getLongValue(String key){ String v = getStringValue(key); return (v == null || v.trim().isEmpty()) ? 0 : Long.parseLong(v); } public static Long getDataCenterId() { return getLongValue(DATA_CENTER_ID); } public static Long getMachineId() { return getLongValue(MACHINE_ID); } }
(4)为了配合SnowFlakeLoader类读取配置文件中的内容,在项目的resources目录下新建snowflake目录,并在snowflake目录下新建snowflake.properties文件,snowflake.properties文件的内容如下所示。
data.center.id=1 machine.id=1
至此,我们项目的通用工具类模块就实现完毕了,后续在开发具体业务时,如果需要扩展,我们在一起扩展通用工具类模块。
另外,源码中提供了针对分布式id的测试用例,可以加入【冰河技术】知识星球获取源码。
创建实体类模块
在父工程下创建实体类模块shop-bean,作为整个项目的通用实体类模块,实体类模块的总体结构如下所示。
添加项目依赖
shop-bean模块的依赖相对来说就比较简单了,只需要依赖shop-utils模块即可。在shop-bean模块的pom.xml文件中添加如下配置。
<dependencies> <dependency> <groupId>io.binghe.shop</groupId> <artifactId>shop-utils</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency> </dependencies>
核心类开发
对于shop-bean模块来说,主要的功能就是提供JavaBean,目前主要提供四个实体类,分别如下所示。
(1)io.binghe.shop.bean#User类,表示用户类,源码如下所示。
/** * @author binghe(冰河技术) * @version 1.0.0 * @description 用户实体类 */ @Data @TableName("t_user") public class User implements Serializable { private static final long serialVersionUID = -7032479567987350240L; /** * 数据id */ @TableId(value = "id", type = IdType.INPUT) @TableField(value = "id", fill = FieldFill.INSERT) private Long id; /** * 用户名 */ @TableField("t_username") private String username; /** * 密码 */ @TableField("t_password") private String password; /** * 手机号 */ @TableField("t_phone") private String phone; /** * 地址 */ @TableField("t_address") private String address; public User(){ this.id = SnowFlakeFactory.getSnowFlakeFromCache().nextId(); //默认密码 this.password = PasswordUtils.getPassowrd("123456"); } }
(2)io.binghe.shop.bean#Product类,表示商品类,源码如下所示。
/** * @author binghe * @version 1.0.0 * @description 商品 */ @Data @TableName("t_product") public class Product implements Serializable { private static final long serialVersionUID = -2907409980909070073L; /** * 数据id */ @TableId(value = "id", type = IdType.INPUT) @TableField(value = "id", fill = FieldFill.INSERT) private Long id; /** * 商品名称 */ @TableField("t_pro_name") private String proName; /** * 商品价格 */ @TableField("t_pro_price") private BigDecimal proPrice; /** * 商品库存 */ @TableField("t_pro_stock") private Integer proStock; public Product(){ this.id = SnowFlakeFactory.getSnowFlakeFromCache().nextId(); } }
(3)io.binghe.shop.bean#Order类,表示订单类,源码如下所示。
/** * @author binghe * @version 1.0.0 * @description 订单 */ @Data @TableName("t_order") public class Order implements Serializable { private static final long serialVersionUID = -2907409980909070073L; /** * 数据id */ @TableId(value = "id", type = IdType.INPUT) @TableField(value = "id", fill = FieldFill.INSERT) private Long id; /** * 用户id */ @TableField("t_user_id") private Long userId; /** * 用户名 */ @TableField("t_user_name") private String username; /** * 手机号 */ @TableField("t_phone") private String phone; /** * 地址 */ @TableField("t_address") private String address; /** * 商品价格(总价) */ @TableField("t_total_price") private BigDecimal totalPrice; public Order(){ this.id = SnowFlakeFactory.getSnowFlakeFromCache().nextId(); } }
(4)io.binghe.shop.bean#OrderItem类,表示订单条目类,源码如下所示。
/** * @author binghe * @version 1.0.0 * @description 订单明细 */ @Data @TableName("t_order_item") public class OrderItem implements Serializable { private static final long serialVersionUID = -1329173923755780293L; /** * 数据id */ @TableId(value = "id", type = IdType.INPUT) @TableField(value = "id", fill = FieldFill.INSERT) private Long id; @TableField("t_order_id") private Long orderId; /** * 商品id */ @TableField("t_pro_id") private Long proId; /** * 商品名称 */ @TableField("t_pro_name") private String proName; /** * 商品价格(单价) */ @TableField("t_pro_price") private BigDecimal proPrice; /** * 购买数量 */ @TableField("t_number") private Integer number; public OrderItem(){ this.id = SnowFlakeFactory.getSnowFlakeFromCache().nextId(); } }
注意:四个实体类都比较简单,小伙伴们可以直接看源码的注释,这里就不再赘述了。同时,这里是为简化商品的下单逻辑而创建的实体类,实际场景下的实体类会远比这些复杂。
创建数据表
这里,主要创建四个数据表,分别为用户表、商品表、订单表和订单条目表,分别对应着四个实体类,如下所示。
- t_user用户表,与User实体类对应
- t_product商品表,与Product实体类对应
- t_order订单表,与Order实体类对应
- t_order_item订单条目表,与OrderItem实体类对应
创建数据表的脚本已经放在项目源码里啦。至此,项目中的通用工具类模块、通用实体类模块就开发完成了,同时,数据表也创建完毕了。下一篇,我们开撸用户微服务、商品微服务和订单微服务。
小伙们如需获取《SpringCloud Alibaba实战》专栏整体项目的源代码,可以加入【冰河技术】知识星球,至于知识星球嘛,继续往下看。