总概
A、技术栈
- 开发语言:Java 1.8
- 数据库:MySQL、Redis、MongoDB、Elasticsearch
- 微服务框架:Spring Cloud Alibaba
- 微服务网关:Spring Cloud Gateway
- 服务注册和配置中心:Nacos
- 分布式事务:Seata
- 链路追踪框架:Sleuth
- 服务降级与熔断:Sentinel
- ORM框架:MyBatis-Plus
- 分布式任务调度平台:XXL-JOB
- 消息中间件:RocketMQ
- 分布式锁:Redisson
- 权限:OAuth2
- DevOps:Jenkins、Docker、K8S
B、本节实现目标
- 搭建ac-mall2-cloud微服务基础骨架。
- 搭建微服务子项目:mall-pom、mall-common、mall-member、mall-product。
- MyBatis-Plus配置:雪花ID、创建时间/修改时间 自动填充。
- 单个微服务子项目Swagger配置及访问。
- 返回JSON数据日期格式化。
- Swagger优化:mall-common支持多个微服务Swagger配置、Swagger传参(语言参数、token、测试账号)
一、新建项目目录
新建项目目录ac-mall2-cloud,该目录并列存放所有微服务。
编辑
二、创建mall-pom父级依赖工程
2.1 创建项目
编辑
2.2 选择maven项目
编辑
2.3 项目名称和路径
编辑
2.4 配置.gitignore文件
.idea target *.iml
2.5 删除src目录,配置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> <!-- 创建mall-member微服务时会自动配置 --> <modules> <module>../mall-member</module> </modules> <groupId>com.ac</groupId> <artifactId>mall-pom</artifactId> <version>${mall.version}</version> <name>mall-pom</name> <packaging>pom</packaging> <description>基础pom依赖包</description> <!-- lombok要与mapstruct版本匹配,用同一时间的版本,不然会出现各种问题 --> <properties> <mall.version>1.0-SNAPSHOT</mall.version> <java.version>1.8</java.version> <spring-cloud.version>Hoxton.SR9</spring-cloud.version> <mysql.version>8.0.17</mysql.version> <mybatis.plus.version>3.2.0</mybatis.plus.version> <druid.version>1.1.10</druid.version> <boot.version>2.3.6.RELEASE</boot.version> <alibaba.cloud.version>2.2.3.RELEASE</alibaba.cloud.version> <lombok.version>1.18.6</lombok.version> <org.mapstruct.version>1.3.0.Final</org.mapstruct.version> <org.projectlombok.version>1.18.6</org.projectlombok.version> <swagger2.version>2.9.2</swagger2.version> <hibernate-validator.version>6.0.17.Final</hibernate-validator.version> <jwt.version>0.9.1</jwt.version> <fastjson.version>1.2.62</fastjson.version> <commons.version>3.9</commons.version> <mybatis.version>3.5.3</mybatis.version> <hutool.version>5.1.4</hutool.version> </properties> <!-- 管理子类所有的jar包的版本,这样的目的是方便去统一升级和维护 --> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>${boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> <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>${alibaba.cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${boot.version}</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-jdk8</artifactId> <version>${org.mapstruct.version}</version> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${org.mapstruct.version}</version> </dependency> <!--swagger2 start--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>${swagger2.version}</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>${swagger2.version}</version> </dependency> <!--swagger2 end--> <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>${hibernate-validator.version}</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>${jwt.version}</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>${fastjson.version}</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>${commons.version}</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>${hutool.version}</version> </dependency> </dependencies> </dependencyManagement> <!-- 所有的子工程都会自动加入下面的依赖 --> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>${boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> <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>${alibaba.cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-jdk8</artifactId> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> </dependency> <!--swagger2 start--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> </dependency> <!--swagger2 end--> <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> </dependency> </dependencies> <!-- SpringBoot 工程编译打包的插件,放在父pom中就直接给所有子工程继承 --> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${boot.version}</version> </plugin> </plugins> </build> </project>
2.6 modules问题
在目前模式下,所有微服务都会在mall-pom.xml下自动生成<modules>
依赖,而且采用的是相对路径。后面我们可以搭建私有maven仓库来处理,替换掉<modules>
依赖,即将:
<modules> <module>../mall-member</module> </modules>
替换成:
<distributionManagement> <repository> <id>maven-public</id> <name>maven-public</name> <url>http://192.168.100.74:8081/repository/maven-public/</url> </repository> </distributionManagement>
三、创建mall-member微服务工程
3.1 创建module
编辑
3.2 maven项目
编辑
3.3 项目名称和路径
编辑
3.4 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> <parent> <artifactId>mall-pom</artifactId> <groupId>com.ac</groupId> <version>1.0-SNAPSHOT</version> </parent> <groupId>com.ac</groupId> <artifactId>mall-member</artifactId> <version>${mall.version}</version> <name>mall-member</name> <description>用户服务</description> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>8</source> <target>8</target> </configuration> </plugin> </plugins> </build> </project>
3.5 entity包
entity包放实体bean
package com.ac.member.entity; import com.ac.member.enums.MemberSexEnum; import com.baomidou.mybatisplus.annotation.FieldFill; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableLogic; import com.baomidou.mybatisplus.annotation.TableName; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.time.LocalDate; import java.time.LocalDateTime; @Data @TableName("t_member") public class Member { @ApiModelProperty("ID") private Long id; @ApiModelProperty("用户姓名") private String memberName; @ApiModelProperty("手机号") private String mobile; @ApiModelProperty("性别") private MemberSexEnum sex; @ApiModelProperty("生日") private LocalDate birthday; @ApiModelProperty("逻辑删除标志") @TableLogic private Boolean deleted; @ApiModelProperty("数据插入时间") @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @ApiModelProperty("数据修改时间") @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; }
3.6 mapper包
mapper包放mybatisplus.BaseMapper子类,与个mapper.xml自定义sql接口对应
package com.ac.member.mapper; import com.ac.member.dto.MemberDTO; import com.ac.member.entity.Member; import com.ac.member.qry.MemberQry; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; import java.util.List; /** * @description 用户-数据访问层(依赖mybatis-plus),对应mapper.xml里的自定义sql */ public interface MemberMapper extends BaseMapper<Member> { /** * 查询用户 * * @param qry * @return */ List<MemberDTO> searchMember(@Param("qry") MemberQry qry); }
3.7 resources/mapper包
放mapper.xml对应的sql语句,Member.Mapper.xml:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.ac.member.mapper.MemberMapper"> <sql id="MemberDTO_Column"> t.id, t.member_name, t.mobile, t.sex, t.birthday </sql> <select id="searchMember" resultType="com.ac.member.dto.MemberDTO"> select <include refid="MemberDTO_Column"></include> from t_member t <where> t.deleted = 0 <if test="qry.memberName!=null and qry.memberName!=''"> and t.member_name like concat('%', #{qry.memberName}, '%') </if> <if test="qry.mobile!=null and qry.mobile!=''"> and t.mobile = #{qry.mobile} </if> </where> order by t.create_time desc </select> </mapper>
3.8 dao包
dao为数据访问层(依赖mybatis-plus),重用mybatis-service提供的CURD方法
package com.ac.member.dao; import com.ac.member.dto.MemberDTO; import com.ac.member.entity.Member; import com.ac.member.qry.MemberQry; import com.baomidou.mybatisplus.extension.service.IService; import java.util.List; /** * @author 阳光倾洒 * @description 用户-数据访问层(依赖mybatis-plus),重用mybatis-service提供的CURD方法 */ public interface MemberDao extends IService<Member> { /** * 查询用户 * * @param qry * @return */ List<MemberDTO> searchMember(MemberQry qry); }
@Slf4j @Repository public class MemberDaoImpl extends ServiceImpl<MemberMapper, Member> implements MemberDao { @Resource private MemberMapper memberMapper; @Override public List<MemberDTO> searchMember(MemberQry qry) { return memberMapper.searchMember(qry); } }
3.9 service包
public interface MemberService { /** * 通过ID查询 * * @param id * @return */ Member findById(Long id); /** * 新增用户 * * @param editVO * @return */ Boolean addMember(MemberEditVO editVO); /** * 查询用户 * * @param qry * @return */ List<MemberDTO> searchMember(MemberQry qry); }
@Slf4j @Service public class MemberServiceImpl implements MemberService { @Resource private MemberDao memberDaoImpl; @Override public Member findById(Long id) { return Optional.ofNullable(memberDaoImpl.getById(id)).orElseThrow(() -> new RuntimeException("数据不存在")); } @Override public Boolean addMember(MemberEditVO editVO) { Member entity = MemberConvert.instance.editVoToEntity(editVO); return memberDaoImpl.save(entity); } @Override public List<MemberDTO> searchMember(MemberQry qry) { return memberDaoImpl.searchMember(qry); } }
3.10 controller包
@Api(tags = "用户") @RestController @RequestMapping("member") public class MemberController { @Resource private MemberService memberServiceImpl; @ApiOperation(value = "通过ID查询") @GetMapping("{id}") public Member findById(@PathVariable Long id) { return memberServiceImpl.findById(id); } @ApiOperation(value = "新增用户") @PostMapping public Boolean addMember(@RequestBody @Valid MemberEditVO editVO) { return memberServiceImpl.addMember(editVO); } @ApiOperation(value = "查询用户") @GetMapping("qry") public List<MemberDTO> searchMember(MemberQry qry) { return memberServiceImpl.searchMember(qry); } }
3.11 Application类
package com.ac.product; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan("com.ac.member.mapper") public class ProductApplication { public static void main(String[] args) { SpringApplication.run(ProductApplication.class, args); } }
3.12 yml项目配置文件
application.yml
spring: profiles: active: dev
application-dev.yml
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.100.51:3306/ac_db?serverTimezone=Asia/Shanghai&useUnicode=true&tinyInt1isBit=false&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true username: ac_u password: ac_PWD_123 #hikari数据库连接池 hikari: pool-name: YH_HikariCP minimum-idle: 10 #最小空闲连接数量 idle-timeout: 600000 #空闲连接存活最大时间,默认600000(10分钟) maximum-pool-size: 100 #连接池最大连接数,默认是10 auto-commit: true #此属性控制从池返回的连接的默认自动提交行为,默认值:true max-lifetime: 1800000 #此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟 connection-timeout: 30000 #数据库连接超时时间,默认30秒,即30000 connection-test-query: SELECT 1 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl swagger: enabled: true
3.13 项目结构截图
mall-pom、mall-member项目截图
编辑
3.14 说明
3.14.1 mapper层与dao层
在大部分项目中,代码分为3层,大概是这样的:
- controller层:RESTful API 或 HTTP请求转发,没有业务逻辑。
- service层:放业务逻辑代码,有些会继承MyBatisPlus的IService接口,获取通用的对象CURD方法。
- mapper层:继承MyBatisPlus的BaseMapper,获取通用的对象CURD方法,同时对应mapper.xml里的自定义sql。
而在本项目中,代码分为4层,分别是:controller、service、dao、mapper。
service层不依赖任何第三方框架,即service不去继承MyBatisPlus的IService接口,这样就保证了service层的高内聚低耦合,将来哪一天项目要将ORM框架由MyBatisPlus换成其他的框架时,service层完全不需要改动,保障了业务逻辑代码的稳定性。
但MyBatisPlus的IService接口提供了丰富的通用CURD方法,如果不用又很浪费,因此,本项目就多加了一个dao层,dao层继承MyBatisPlus的IService接口,这样service再依赖dao,调用dao提供的CURD,而mapper层专门对应mapper.xml里的自定义sql。从广义上来讲,数据存储层(通常说的dao层)在本项目中包含两部分:dao和mapper,将来哪一天项目要将ORM框架由MyBatisPlus换成其他的框架时,只需要改造dao和mapper,且dao和mapper一般相对简单,没有业务逻辑代码,改造完成后容易测试。
3.14.2 @Autowired
与@Resource
在Spring项目中,IOC注入有的人用@Autowired
的,而有的人用@Resource
,而且写法各式各样。
3.14.2.1 我的用法
本项目中,我的写法是:
- 1、
@Autowired
与@Resource
,我用的是@Resource
,理由是@Autowired
依赖Spring框架,而@Resource
是Java自带注解。 - 2、引用名字用的是接口实现类的名字,而不是接口名字。
例如,我在MemberController里写的是实现类的类名memberServiceImpl
,而不是接口名memberService
。
理由是@Resource
默认是byName查找的,如果写的是接口名memberService
,IOC注入时默认byName查找,发现没有找到name为memberService
,然后通过byType进行第二次查找,才找到接口类型为MemberService的类,而且只发现一个实现类MemberServiceImpl
,注入成功。
即,用实现类的类名,IOC通过byName一次就能注入成功,而用接口名,需要两次查找,性能会差一些。
public class MemberController { @Resource private MemberService memberServiceImpl; }
3.14.2.2 @Autowired
与@Resource
的区别
A、相同点
这个两个注解都是用来完成组件的装配的,即利用依赖注入(DI),完成对IOC容器当中各个组件之间依赖的装配赋值。
B、不同点
B1、来源不同
@Resource
@Resource
是javaEE的注解,它遵循的是JSR-250规范,需要导入包javax.annotation.Resource
。
@Autowired
@Autowired
为Spring提供的注解,需要导入包org.springframework.beans.factory.annotation.Autowired
。
B2、装配顺序不同
@Resource
- 默认按照byName方式进行装配,属于J2EE自带注解,没有指定name时,name指的是变量名。
- 如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常。
- 如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常。
- 如果指定了type,则从上下文中找到类型匹配的唯一bean进行装配,找不到或者找到多个,都会抛出异常。
- 如果既没有指定name,又没有指定type,则自动按照byName方式进行装配。如果没有匹配,则回退为一个原始类型进行匹配,如果匹配则自动装配。
@Autowired
- 默认按byType自动注入,是Spring的注解。
- 默认情况下必须要求依赖对象必须存在,如果要允许null值,可以设置它的required属性为false,
@Autowired(required = false)
。 - 按类型装配的过程中,如果发现找到多个bean,则又按照byName方式进行比对,如果还有多个,则报出异常。
四、MyBatis-Plus配置
4.1 雪花ID生成策略
4.1.1 雪花ID生成代码
import com.baomidou.mybatisplus.core.toolkit.SystemClock; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.RandomUtils; import org.apache.commons.lang3.StringUtils; import java.net.Inet4Address; import java.net.UnknownHostException; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; @Slf4j public class SnowFlake { /** 初始偏移时间戳 */ private static final long OFFSET = 1546300800L; /** 机器id (0~15 保留 16~31作为备份机器) */ private static final long WORKER_ID; /** 机器id所占位数 (5bit, 支持最大机器数 2^5 = 32)*/ private static final long WORKER_ID_BITS = 5L; /** 自增序列所占位数 (16bit, 支持最大每秒生成 2^16 = 65536) */ private static final long SEQUENCE_ID_BITS = 16L; /** 机器id偏移位数 */ private static final long WORKER_SHIFT_BITS = SEQUENCE_ID_BITS; /** 自增序列偏移位数 */ private static final long OFFSET_SHIFT_BITS = SEQUENCE_ID_BITS + WORKER_ID_BITS; /** 机器标识最大值 (2^5 / 2 - 1 = 15) */ private static final long WORKER_ID_MAX = ((1 << WORKER_ID_BITS) - 1) >> 1; /** 备份机器ID开始位置 (2^5 / 2 = 16) */ private static final long BACK_WORKER_ID_BEGIN = (1 << WORKER_ID_BITS) >> 1; /** 自增序列最大值 (2^16 - 1 = 65535) */ private static final long SEQUENCE_MAX = (1 << SEQUENCE_ID_BITS) - 1; /** 发生时间回拨时容忍的最大回拨时间 (秒) */ private static final long BACK_TIME_MAX = 1000L; /** 上次生成ID的时间戳 (秒) */ private static long lastTimestamp = 0L; /** 当前秒内序列 (2^16)*/ private static long sequence = 0L; /** 备份机器上次生成ID的时间戳 (秒) */ private static long lastTimestampBak = 0L; /** 备份机器当前秒内序列 (2^16)*/ private static long sequenceBak = 0L; static { // 初始化机器ID long workerId = getWorkId(); if (workerId < 0 || workerId > WORKER_ID_MAX) { throw new IllegalArgumentException(String.format("cmallshop.workerId范围: 0 ~ %d 目前: %d", WORKER_ID_MAX, workerId)); } WORKER_ID = workerId; } private static Long getWorkId(){ try { String hostAddress = Inet4Address.getLocalHost().getHostAddress(); int[] ints = StringUtils.toCodePoints(hostAddress); int sums = 0; for(int b : ints){ sums += b; } return (long)(sums % WORKER_ID_MAX); } catch (UnknownHostException e) { // 如果获取失败,则使用随机数备用 return RandomUtils.nextLong(0,WORKER_ID_MAX-1); } } /** 私有构造函数禁止外部访问 */ private SnowFlake() {} /** * 获取自增序列 * @return long */ public static long nextId() { return nextId(SystemClock.now() / 1000); } /** * 主机器自增序列 * @param timestamp 当前Unix时间戳 * @return long */ private static synchronized long nextId(long timestamp) { // 时钟回拨检查 if (timestamp < lastTimestamp) { // 发生时钟回拨 log.warn("时钟回拨, 启用备份机器ID: now: [{}] last: [{}]", timestamp, lastTimestamp); return nextIdBackup(timestamp); } // 开始下一秒 if (timestamp != lastTimestamp) { lastTimestamp = timestamp; sequence = 0L; } if (0L == (++sequence & SEQUENCE_MAX)) { // 秒内序列用尽 // log.warn("秒内[{}]序列用尽, 启用备份机器ID序列", timestamp); sequence--; return nextIdBackup(timestamp); } return ((timestamp - OFFSET) << OFFSET_SHIFT_BITS) | (WORKER_ID << WORKER_SHIFT_BITS) | sequence; } /** * 备份机器自增序列 * @param timestamp timestamp 当前Unix时间戳 * @return long */ private static long nextIdBackup(long timestamp) { if (timestamp < lastTimestampBak) { if (lastTimestampBak - SystemClock.now() / 1000 <= BACK_TIME_MAX) { timestamp = lastTimestampBak; } else { throw new RuntimeException(String.format("时钟回拨: now: [%d] last: [%d]", timestamp, lastTimestampBak)); } } if (timestamp != lastTimestampBak) { lastTimestampBak = timestamp; sequenceBak = 0L; } if (0L == (++sequenceBak & SEQUENCE_MAX)) { // 秒内序列用尽 // logger.warn("秒内[{}]序列用尽, 备份机器ID借取下一秒序列", timestamp); return nextIdBackup(timestamp + 1); } return ((timestamp - OFFSET) << OFFSET_SHIFT_BITS) | ((WORKER_ID ^ BACK_WORKER_ID_BEGIN) << WORKER_SHIFT_BITS) | sequenceBak; } /** * 并发数 */ private static final int THREAD_NUM = 30000; private static volatile CountDownLatch countDownLatch = new CountDownLatch(THREAD_NUM); public static void main(String[] args) { ConcurrentHashMap<Long,Long> map = new ConcurrentHashMap<>(THREAD_NUM); List<Long> list = Collections.synchronizedList(new LinkedList<>()); for (int i = 0; i < THREAD_NUM; i++) { Thread thread = new Thread(() -> { // 所有的线程在这里等待 try { countDownLatch.await(); Long id = SnowFlake.nextId(); list.add(id); map.put(id,1L); } catch (InterruptedException e) { e.printStackTrace(); } }); thread.start(); // 启动后,倒计时计数器减一,代表有一个线程准备就绪了 countDownLatch.countDown(); } try{ Thread.sleep(50000); }catch (Exception e){ e.printStackTrace(); } System.out.println("listSize:"+list.size()); System.out.println("mapSize:"+map.size()); System.out.println(map.size() == THREAD_NUM); } }
4.1.2 与mybatis-plus结合
@Slf4j @Component public class CustomIdGenerator implements IdentifierGenerator { @Override public Long nextId(Object entity) { return SnowFlake.nextId(); } }
4.2 自动填充功能
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.reflection.MetaObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.time.LocalDateTime; import java.util.Date; /** * @description 处理时间自动填充 */ @Slf4j @Component public class GlobalMetaObjectHandler implements MetaObjectHandler { private Logger logger = LoggerFactory.getLogger(GlobalMetaObjectHandler.class); String createUserIdFieldName = "createUserId"; String updateUserIdFieldName = "updateUserId"; String createTimeFieldName = "createTime"; String updateTimeFieldName = "updateTime"; String deletedFieldName = "deleted"; @Override public void insertFill(MetaObject metaObject) { try { Object createUserId = getFieldValByName(createUserIdFieldName, metaObject); Object createTime = getFieldValByName(createTimeFieldName, metaObject); Object updateTime = getFieldValByName(updateTimeFieldName, metaObject); Object delTag = getFieldValByName(deletedFieldName, metaObject); if(null ==createUserId){ setFieldValByName(createUserIdFieldName, getUserId(), metaObject); } LocalDateTime date = LocalDateTime.now(); if (null == createTime) { setFieldValByName(createTimeFieldName, date, metaObject); } if (null == updateTime) { setFieldValByName(updateTimeFieldName, date, metaObject); } if (null == delTag) { setFieldValByName(deletedFieldName, false, metaObject); } } catch (Exception e) { logger.warn(e.getMessage(), e); } } @Override public void updateFill(MetaObject metaObject) { try { setFieldValByName(updateTimeFieldName, LocalDateTime.now(), metaObject); setFieldValByName(updateUserIdFieldName, getUserId(), metaObject); } catch (Exception e) { logger.warn(e.getMessage(), e); } } private String getUserId(){ String userId = null; RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); if(requestAttributes !=null){ HttpServletRequest request = ((ServletRequestAttributes)requestAttributes).getRequest(); if(request!=null){ userId = request.getHeader("userId"); } } return userId; } }
4.3 MyBatis配置类截图
编辑
五、Swagger配置
5.1 Swagger依赖包
Swagger依赖的包已经在mall-pom项目的pom.xml配置,如下
<!--swagger2 start--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>${swagger2.version}</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>${swagger2.version}</version> </dependency> <!--swagger2 end-->
5.2 Swagger配置
package com.ac.member.config.swagger; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.*; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; /** * @description Swagger文档配置 */ @EnableWebMvc @Configuration @EnableSwagger2 public class SwaggerConfig implements WebMvcConfigurer { //是否开启swagger,生产环境一般关闭 @Value("${swagger.enabled}") private boolean enabled; /** * @return */ @Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .enable(enabled) .apiInfo(apiInfo()) .select() //.apis(RequestHandlerSelectors.any()) .apis(RequestHandlerSelectors.basePackage("com.ac.member.controller")) .paths(PathSelectors.any()) .build(); } /** * 一些接口文档信息的简介 * * @return */ private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("用户服务") .description("用户服务相关接口") .termsOfServiceUrl("") .version("1.0") .build(); } /** * swagger ui资源映射 * * @param registry */ @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/"); registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/"); } /** * swagger-ui.html路径映射,浏览器中使用/api-docs访问 * * @param registry */ @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addRedirectViewController("/api-docs", "/swagger-ui.html"); } }
5.3 Swagger访问效果
在浏览器中输入地址http://127.0.0.1:8080/swagger-ui.html
或地址http://127.0.0.1:8080/api-docs
,效果如下:
编辑
5.4 Swagger访问404问题
5.4.1 404效果图
编辑
5.4.2 原因
由于要处理日期返回到前端格式化的问题,项目里加了一个处理日期格式化的配置类,继承了WebMvcConfigurationSupport类,导致swagger访问404
@Configuration public class DateJsonConfig extends WebMvcConfigurationSupport { //代码省略 }
5.4.3 解决方案
将继承WebMvcConfigurationSupport类,改成实现WebMvcConfigurer接口,代码如下:
@Configuration public class DateJsonConfig implements WebMvcConfigurer { //代码省略 }
六、返回JSON数据日期格式化
6.1 未处理前日期返回的格式
编辑
6.2 处理日期格式配置类
package com.ac.member.config; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.io.IOException; import java.text.ParseException; import java.text.SimpleDateFormat; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.Date; import java.util.List; @Configuration public class DateJsonConfig implements WebMvcConfigurer { /** * 日期时间格式 yyyy-MM-dd HH:mm:ss */ private static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; /** * 日期格式 yyyy-MM-dd */ private static final String DATE_FORMAT = "yyyy-MM-dd"; /** * 时间格式 HH:mm:ss */ private static final String TIME_FORMAT = "HH:mm:ss"; @Override public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); ObjectMapper objectMapper = converter.getObjectMapper(); objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); objectMapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE); JavaTimeModule javaTimeModule = new JavaTimeModule(); //LocalDateTime的序列化和反序列化 javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT))); javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT))); //LocalDate的序列化和反序列化 javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DATE_FORMAT))); javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DATE_FORMAT))); //LocalTime的序列化和反序列化 javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(TIME_FORMAT))); javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(TIME_FORMAT))); //Date类型的序列化 javaTimeModule.addSerializer(Date.class, new JsonSerializer<Date>() { @Override public void serialize(Date date, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { SimpleDateFormat formatter = new SimpleDateFormat(DATE_TIME_FORMAT); String formattedDate = formatter.format(date); jsonGenerator.writeString(formattedDate); } }); //Date类型的反序列化 javaTimeModule.addDeserializer(Date.class, new JsonDeserializer<Date>() { @Override public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { SimpleDateFormat format = new SimpleDateFormat(DATE_TIME_FORMAT); String date = jsonParser.getText(); try { return format.parse(date); } catch (ParseException e) { throw new RuntimeException(e); } } }); objectMapper.registerModule(javaTimeModule); converter.setObjectMapper(objectMapper); converters.add(0, converter); } }
日期格式返回效果
编辑
七、创建mall-common服务工程
各微服务虽然是独立的,但一般都会有一些公用重复的功能,可以把这些重复的代码提取到mall-common服务,然后其他微服务依赖mall-common。
7.1 创建module
编辑
7.2 pom.xml配置
参考上面的mall-member服务的pom.xml来写mall-common服务的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> <parent> <artifactId>mall-pom</artifactId> <groupId>com.ac</groupId> <version>1.0-SNAPSHOT</version> </parent> <groupId>com.ac</groupId> <artifactId>mall-common</artifactId> <version>${mall.version}</version> <name>mall-common</name> <description>公共服务</description> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>8</source> <target>8</target> </configuration> </plugin> </plugins> </build> </project>
7.3 提取功能config包
将mall-member服务里的config包提取到mall-common服务中,并将mall-member服务里的config包删除掉。
删除config包编辑
mall-common包结构编辑
7.4 mall-member服务依赖mall-common服务
让mall-member服务里的Member实体类继承mall-common服务里的BaseEntity类,并删除deleted、createTime、updateTime字段。
根据IDEA提示:Add dependency on module mall-common
,IDEA会帮我们自动创建依赖
Add dependency on module编辑
IDEA自动创建依赖编辑
我们可以手动把<scope>compile</scope>
删除。
说明:后面我将mall-common里的内容放到了一个新服务mall-core,我对mall-core的定义是:稳定的不经常改动的基础服务。而mall-common里则放一些各个服务都需要依赖的一些公共类,如全局常量(topic name)、枚举等,该服务经常会被改动。
7.5 配置@ComponentScan
@SpringApplication
注解里已经包含了@ComponentScan
注解,但@ComponentScan
注解默认扫描的是Application所在目录下的所有Bean,但mall-common里的配置类显然不在mall-member服务的Application所在目录下,导致mall-common里的配置类没有生效,因此我们需要修改mall-member服务的Application配置,扫描mall-common服务里的Bean。需要添加的配置为:@ComponentScan("com.ac.*")
,MemberApplication完整代码如下:
package com.ac.member; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.ComponentScan; @SpringBootApplication @MapperScan("com.ac.member.mapper") @ComponentScan("com.ac.*") public class MemberApplication { public static void main(String[] args) { SpringApplication.run(MemberApplication.class, args); } }
八、创建mall-product服务工程
mall-product 服务主要维护产品数据,可参考mall-member来建该服务。
创建mall-product编辑
mall-product项目截图编辑
九、Swagger优化
9.1 mall-common支持多个微服务Swagger配置
Swagger配置类SwaggerConfig由原来的mall-member移到了mall-common服务,因此之前在SwaggerConfig写死的配置信息需要放到yml文件进行动态配置。
9.1.1 修改代码
修改后的SwaggerConfig如下:
package com.ac.common.config.swagger; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; /** * @description Swagger文档配置 */ @EnableWebMvc @Configuration @EnableSwagger2 @ConfigurationProperties("swagger") @Data public class SwaggerConfig implements WebMvcConfigurer { //取yml配置文件参数 private boolean enabled; private String title; private String description; private String version; private String basePackage; /** * @return */ @Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .enable(enabled) .apiInfo(apiInfo()) .select() //.apis(RequestHandlerSelectors.any()) //.apis(RequestHandlerSelectors.basePackage(basePackage)) .apis(RequestHandlerSelectors.withClassAnnotation(RestController.class)) .paths(PathSelectors.any()) .build(); } /** * 一些接口文档信息的简介 * * @return */ private ApiInfo apiInfo() { return new ApiInfoBuilder() .title(title) .description(description) .termsOfServiceUrl("") .version(version) .build(); } /** * swagger ui资源映射 * * @param registry */ @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/"); registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/"); } /** * swagger-ui.html路径映射,浏览器中使用/api-docs访问 * * @param registry */ @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addRedirectViewController("/api-docs", "/swagger-ui.html"); } }
修改mall-member服务的yml配置文件
swagger: enabled: true title: 用户服务 basePackage: com.ac.member.controller version: 1.0 description: 用户服务相关接口
修改mall-product服务的yml配置文件
swagger: enabled: true title: 产品服务 basePackage: com.ac.product.controller version: 1.0 description: 产品服务相关接口
9.1.2 swagger访问效果
编辑
用户服务
编辑
产品服务
9.2 Swagger传参
9.2.1 修改代码
修改SwaggerConfig配置类,让Swagger支持token、语言、测试账号参数
SwaggerConfig类
package com.ac.common.config.swagger; import com.ac.common.enums.LanguageEnum; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.ParameterBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.schema.ModelRef; import springfox.documentation.service.AllowableListValues; import springfox.documentation.service.ApiInfo; import springfox.documentation.service.Parameter; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.util.ArrayList; import java.util.List; @EnableWebMvc @Configuration @EnableSwagger2 @ConfigurationProperties("swagger") @Data public class SwaggerConfig implements WebMvcConfigurer { //取yml配置文件参数 private boolean enabled; private String title; private String description; private String version; private String basePackage; /** * @return */ @Bean public Docket api() { List<Parameter> pars = new ArrayList<>(); pars.add(buildToken().build()); pars.add(buildLang().build()); pars.add(buildDemos().build()); return new Docket(DocumentationType.SWAGGER_2) .enable(enabled) .apiInfo(apiInfo()) .directModelSubstitute(LocalDateTime.class, String.class) .directModelSubstitute(LocalDate.class, String.class) .directModelSubstitute(LocalTime.class, String.class) .directModelSubstitute(Byte.class, Integer.class) .select() //.apis(RequestHandlerSelectors.any()) //.apis(RequestHandlerSelectors.basePackage(basePackage)) .apis(RequestHandlerSelectors.withClassAnnotation(RestController.class)) .paths(PathSelectors.any()) .build() .globalOperationParameters(pars); } /** * 一些接口文档信息的简介 * * @return */ private ApiInfo apiInfo() { return new ApiInfoBuilder() .title(title) .description(description) .termsOfServiceUrl("") .version(version) .build(); } private ParameterBuilder buildToken() { ParameterBuilder pb = new ParameterBuilder(); pb.name("Authorization") .description("授权的token, 格式: Bearer xxx(需通过网关)") .modelRef(new ModelRef("string")) .parameterType("header") .required(false) .build(); //header中的ticket参数非必填,传空也可以 return pb; } private ParameterBuilder buildLang() { List<String> languages = new ArrayList<>(); languages.add(LanguageEnum.zh_CN.getCode()); languages.add(LanguageEnum.zh_HK.getCode()); languages.add(LanguageEnum.en_US.getCode()); ParameterBuilder pb = new ParameterBuilder(); pb.name("lang") .description("当前国际化语言环境") .modelRef(new ModelRef("string")) .allowableValues(new AllowableListValues(languages, "string")) .parameterType("header") .required(false) .build(); //header中的ticket参数非必填,传空也可以 return pb; } private ParameterBuilder buildDemos() { List<String> demouis = new ArrayList<>(); demouis.add("test01"); demouis.add("test02"); ParameterBuilder pb = new ParameterBuilder(); pb.name("Swagger测试用户") .description("Swagger模拟用户, 选中时Authorization字段将失效(无需通过网关)") .modelRef(new ModelRef("string")) .allowableValues(new AllowableListValues(demouis, "string")) .parameterType("header") .required(false) .order(0) .build(); //header中的ticket参数非必填,传空也可以 return pb; } /** * swagger ui资源映射 * * @param registry */ @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/"); registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/"); } /** * swagger-ui.html路径映射,浏览器中使用/api-docs访问 * * @param registry */ @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addRedirectViewController("/api-docs", "/swagger-ui.html"); } }
LanguageEnum类
@Getter @NoArgsConstructor @AllArgsConstructor @ApiModel(description = "多语言类型枚举") public enum LanguageEnum { zh_CN("zh_CN", "简体中文", new Locale("zh", "CN")), zh_HK("zh_HK", "繁體中文", new Locale("zh", "HK")), en_US("en_US", "English", new Locale("en", "US")); /** * 语言代码 */ private String code; /** * 语言名称 */ private String name; /** * Locale */ private Locale locale; /** * 解析 * * @param type * @return */ public static LanguageEnum parse(String type) { return parse(type, null); } /** * 解析 * * @param type * @param dau * @return */ public static LanguageEnum parse(String type, LanguageEnum dau) { if (null != type && !type.isEmpty()) { try { return LanguageEnum.valueOf(type); } catch (IllegalArgumentException e) { } } return dau; } }
9.2.2 Swagger参数效果图
编辑