智牛股_第8章_Sentinel
智牛股_第8章_Sentinel
文章目录
- 智牛股_第8章_Sentinel
- 学习目标
- 第1章 Sentinel集成使用
- 1. 目标
- 2. 步骤
- 3. 实现
- 3.1 生产环境最优配置方案
- 3.2 用户服务集成
- 3.3 熔断规则配置
- 3.4 启动Sentinel监控台
- 3.5 功能使用验证
- 4. 总结
- 第2章 用户注册功能
- 1. 目标
- 2. 步骤
- 3. 实现
- 3.1 用户注册流程
- 3.2 数据库结构
- 3.3 创建实体
- 3.4 创建公用系统层组件
- 3.5 数据层
- 3.6 全局序列号生成方案
- 3.7 服务层
- 3.8 接入层实现
- 3.9 添加自动化校验
- 3.10 启动服务
- 3.11 功能验证
- 4. 总结
学习目标
目标1:Sentinel最优生产实践方案
目标2:用户服务集成Sentinel使用
目标3:用户注册业务功能设计与代码实现
目标4: 全局序列号生成使用
目标5: 自动化校验使用
第1章 Sentinel集成使用
1. 目标
-
掌握Sentinel生产环境最佳配置实现方案
-
Sentinel与用户服务集成配置, 完成整体功能验证
2. 步骤
- 生产环境最优配置方案
- 用户服务集成
- 熔断规则配置
- 启动Sentinel监控台
- 功能使用验证
3. 实现
第二天的课程已经讲过Sentinel的使用与原理, 接下来我们在Spring Cloud 微服务的生产环境中配置和运用Sentinel。
3.1 生产环境最优配置方案
配置模式选择:
推送模式 | 说明 | 优点 | 缺点 |
---|---|---|---|
原始模式 | API 将规则推送至客户端并直接更新到内存中,扩展写数据源(WritableDataSource ) | 简单,无任何依赖 | 不保证一致性;规则保存在内存中,重启即消失。严重不建议用于生产环境 |
Pull 模式 | 扩展写数据源(WritableDataSource ), 客户端主动向某个规则管理中心定期轮询拉取规则,这个规则中心可以是 RDBMS、文件 等 | 简单,无任何依赖;规则持久化 | 不保证一致性;实时性不保证,拉取过于频繁也可能会有性能问题。 |
Push 模式 | 扩展读数据源(ReadableDataSource ),规则中心统一推送,客户端通过注册监听器的方式时刻监听变化,比如使用 Nacos、Zookeeper 等配置中心。这种方式有更好的实时性和一致性保证。生产环境下一般采用 push 模式的数据源。 | 规则持久化;一致性;快速 | 引入第三方依赖 |
- 原始模式
- Pull拉取模式
- Push推送模式
优点: 基于较强的实时性,支持动态修改发布,重启服务不受影响, 具有较高的可靠性与一致性。
官方建议采用Push模式。
3.2 用户服务集成
-
功能设计
我们把上一章完成的用户接口进行改造, 加入降级与限流处理。
整个部署结构采用PUSH模式, 配置规则放置Nacos配置中心,统一发布与更新。
在生产环境中, 如果在峰值时间段, 出现大量用户请求或者内部系统出现问题, 这个时候就有必要进行限流和降级处理, 防止穿透、雪崩导致整个服务瘫痪。
-
修改MAVEN配置
pom.xml增加以下依赖:
<!-- Sentinel 限流组件 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-sentinel-datasource</artifactId> </dependency> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency>
由于采用Nacos作为Sentinel的数据源, sentinel-datasource-nacos该依赖须加上。
-
修改用户登陆接口
修改StockUserServiceImpl类:
/** * 用户登陆限流处理逻辑 * @param userNo * @param userPwd * @param ex * @return * @throws BlockException */ public TradeUser userLoginHanlder(String userNo, String userPwd, BlockException ex ) throws ComponentException { log.error("userLogin flow limit, call userLoginHandler: " + ex.getMessage()); // 1. 获取熔断限流规则 AbstractRule abstractRule = ex.getRule(); // 2. 根据不同规则, 进行不同逻辑处理 if(abstractRule instanceof DegradeRule ) { throw new ComponentException(ApplicationErrorCodeEnum.SYS_BUSY); }else if(abstractRule instanceof FlowRule) { throw new ComponentException(ApplicationErrorCodeEnum.SYS_FLOW); } // 3. 默认, 未捕获, 不符合配置的规则, 进入系统异常 throw new ComponentException(ApplicationErrorCodeEnum.FAILURE); } /** * 用户登陆 * @param userNo * @param userPwd * @return */ @SentinelResource(value ="userLogin", blockHandler = "userLoginHanlder") public TradeUser userLogin(String userNo, String userPwd) throws Exception { // 模拟降级异常 if("error".equals(userNo)) { throw new ComponentException(ApplicationErrorCodeEnum.FAILURE); } // 获取用户对象 TradeUser tradeUser= stockUserDao.getByUserNo(userNo); if(null == tradeUser) { throw new ComponentException(ApplicationErrorCodeEnum.USER_NOT_FOUND); } // 用户密码加密判断 String encryptPassword = EncryptUtil.encryptSigned(userPwd); boolean pwdMatch= tradeUser.getUserPwd().equals(encryptPassword); if(!pwdMatch) { log.error(ApplicationErrorCodeEnum.USER_PWD_ERROR); throw new ComponentException(ApplicationErrorCodeEnum.USER_PWD_ERROR); } return tradeUser; }
采用SentinelResource注解, 定义资源名称与降级方法。注意1.6.0以下版, 降级只支持DegradeException异常, 在使用上有些差别, 但功能是一致。
用户登陆方法如果符合降级规则, 会进入userLoginHanlder方法, 里面返回一个null对象。为了方便验证, 在代码里面我们模拟了一个异常, 抛出ApplicationErrorCodeEnum.FAILURE错误码。
使用ComponentException, 不要抛出DegradeException, 否则只要出现该异常就会进入降级, 配置的降级规则条件(比如根据异常比例触发)就不会生效。
注意: 如果在同一个方法上面, 同时要配置降级和限流的处理规则, 它优先会进入blockHandler定义的方法,如果没有定义blockHandler或者fallback 等任何熔断限流处理属性, 那么它会抛出BlockException异常。
有关注解的详细配置, 可参考: 官方SentinelResource注解说明
修改StockUserController类:
/** * 用户登陆接口 * @param userNo * @param userPwd * @return */ @RequestMapping("/userLogin") public ApiRespResult userLogin(@RequestParam("userNo")String userNo, @RequestParam("userPwd") String userPwd) { ApiRespResult result = null; try { // 用户登陆逻辑处理 TradeUser tradeUser = stockUserService.userLogin(userNo, userPwd); result = ApiRespResult.success(tradeUser); }catch(ComponentException e) { log.error(e.getMessage(), e); result = ApiRespResult.error(e.geterrorCodeEnum()); }catch(Exception e) { log.error(e.getMessage(), e); result = ApiRespResult.sysError(e.getMessage()); } return result; }
外围增加熔断降级和限流的异常捕获, 1.6以下版本的限流异常为FlowException, 返回错误提示给客户端。
-
修改工程配置
修改bootstrap.xml配置文件:
spring: application: name: stock-user cloud: nacos: discovery: server-addr: 127.0.0.1:8848 config: server-addr: 127.0.0.1:8848 sentinel: transport: # Sentinel监控台地址 dashboard: 127.0.0.1:8090 datasource: # 用户降级规则配置 user-degrade: nacos: server-addr: 127.0.0.1:8848 dataId: sentinel-user-degrade groupId: DEFAULT_GROUP data-type: json rule-type: degrade # 用户限流规则配置 user-flow: nacos: server-addr: 127.0.0.1:8848 dataId: sentinel-user-flow groupId: DEFAULT_GROUP data-type: json rule-type: flow
以上主要增加Sentinel监控台配置, 用户降级规则配置和用户限流规则配置。
这是1.6以下版本的配置, 和最新版会有所不同, 官方文档一般是最新版, 参看时要注意。
3.3 熔断规则配置
启动Nacos服务, 在配置管理里面新建两项配置:
sentinel-user-degrade为降级配置策略, 内容:
[
{
"resource": "userLogin",
"count": 0.2,
"grade": 1,
"timeWindow": 4
}
]
resource: 为资源名称。
count: 为百分比[0-1], 这里代表20%
grade: 为降级策略, 0: 代表响应时间, 1: 代表异常比例, 2: 代表异常数量, 这里采用的是异常比。
timeWindow:为时间窗, 单位为秒。
sentinel-user-flow为限流配置策略: 内容:
[
{
"resource": "userLogin",
"controlBehavior": 0,
"count": 12,
"grade": 1,
"limitApp": "default",
"strategy": 0
}
]
resource: 为资源名称。
controlBehavior:流量整形的控制效果,目前支持快速失败和匀速排队两种模式,默认是0, 快速失败。
count: 线程数量。
grade:限流配置策略, 0:代表线程数量, 1:代表QPS并发数。
limitApp: 限流针对的来源, 填写default即可。
strategy: 基于调用关系的流量控制策略, 有三种
0-STRATEGY_DIRECT,根据调用方进行限流, 结合limitApp使用。
1-STRATEGY_RELATE, 根据关联流量限流, 当多个资源间具有资源争抢和关联关系的时候,比如同一个数据表的读与写请求, 如果写操作比较频繁, 那么读数据的请求就会被限流处理。
2- STRATEGY_CHAIN, 根据调用链的入口限流, 比如两个请求Req1和Req2, 同时再配合设置FlowRule.refResource 指定Req1为入口请求, 那么Req1就会受到限流控制, Req2则可放行。
注意配置内容的JSON格式要符合要求, 如果填写错误, 配置不会生效。
3.4 启动Sentinel监控台
执行命令:
java -Dserver.port=8090 -Dcsp.sentinel.dashboard.server=localhost:8090 -jar sentinel-dashboard-1.6.2.jar
端口设置为8090, 监控台采用最新版本1.6.2。
启动成功后, 监控台不设置任何策略, 基于push模式, 由Nacos配置中心统一维护更新。
3.5 功能使用验证
-
启动用户服务
如果策略配置通过Nacos获取成功, 会有以下提示信息,如果没有提示(新版本会变化)也没关系, 能够在监控台显示即可。
我们在Nacos配置的一条降级策略与一条限流策略加载成功。
-
启动认证服务
上一章我们把用户服务集成了OAUTH2, 需要把认证服务启动, 并申请TOKEN。
-
Sentinel监控台
由于Sentinel采用懒加载方式设计, 如果启动服务成功, 监控台不能发现,先调用一次服务接口即可。
进入监控台查看规则, 可以看到都已经同步拉取下来:
与Nacos中配置的内容一致。
-
限流策略验证
我们配置了限流和降级两个策略, 便于演示, 把两个策略规则修改下, 在Nacos中把限流策略的QPS并发数设为2。在重新查看Sentinel控制台, 可以看到能够动态更新。
构造两个请求,
一个是正常访问:
http://127.0.0.1:10681/user/userLogin?userNo=admin&userPwd=admin&access_token=8c86d26e-1152-40d1-8b0c-bc40d2047b37
一个是会触发异常的访问:
http://127.0.0.1:10681/user/userLogin?userNo=error&userPwd=admin&access_token=8c86d26e-1152-40d1-8b0c-bc40d2047b37
限流策略与是否触发异常无关, 拿正常访问链接, 加快刷新频率, 可以看到出现了限流的错误提示:
-
降级策略验证:
先把QPS放大一些, 重新设为12, 便于我们进行降级策略的演示。
上面设置的降级策略条件是异常比为20%, 时间窗口为4s, 开始验证。
请求会触发异常的访问链接, 多刷新几次, 加快请求频率, 可以看到出现降级错误提示:
再快速访问正常链接, 可以看到也出现降级错误提示(时间窗口4s较短, 不能间隔太长时间访问):
过完时间窗之后, 重新访问, 恢复正常:
4. 总结
- 在生产环境中采用Push模式,优点支持规则持久化, 较强的一致性与可靠性, 能够快速处理, 实现最佳生产配置, 适用大型生产项目中使用, 掌握与理解熔断规则配置, 结合Sentinel监控台, 更好的发挥Sentinel组件作用。
第2章 用户注册功能
1. 目标
-
了解用户注册流程与数据库结构
-
掌握全局序列号生成方案, 自动化校验实现
-
完成用户注册功能与验证
2. 步骤
- 用户注册流程介绍
- 数据库结构设计
- 创建实体
- 创建公用系统层组件
- 数据层实现
- 服务层实现
- 全局序列号生成方案
- 接入层实现
- 自动化校验实现
- 服务启动与功能验证
3. 实现
3.1 用户注册流程
这是一个业界常用的注册流程,一般会有代理系统,提案信息,审批功能, 这个一期我们简化,直接实现用户注册功能, 业务设计上是先注册后开户, 这里通过开关来控制, 如果注册即开户, 会生成开户账号; 本章节, 我们保留此开关, 实现注册即开户的功能。
3.2 数据库结构
增加用户文件表:
drop table if exists t_trade_user_file;
/*==============================================================*/
/* Table: t_trade_user_file */
/*==============================================================*/
create table t_trade_user_file
(
id bigint not null auto_increment comment '主键标识',
userId bigint(16) not null comment '用户ID',
bizType tinyint(3) not null comment '业务类型(0:身份证, 1:银行卡, 2:信用卡)',
fileId varchar(32) not null comment '文件ID',
filename varchar(64) comment '文件名称',
fileType varchar(32) comment '文件类型',
filePath varchar(255) comment '文件路径',
status tinyint(3) not null comment '状态(0:有效, 1:无效)',
createTime datetime comment '创建时间',
updateTime datetime not null default CURRENT_TIMESTAMP comment '更新时间',
primary key (id)
);
alter table t_trade_user_file comment '用户文件表';
/*==============================================================*/
/* Index: idx_userId */
/*==============================================================*/
create index idx_userId on t_trade_user_file
(
userId
);
增加公司表:
drop table if exists t_company;
CREATE TABLE `t_company` (
`id` bigint(19) NOT NULL AUTO_INCREMENT COMMENT '主键标识',
`companyName` varchar(32) DEFAULT NULL COMMENT '公司名称\r\n ',
`institutionTypeId` varchar(32) CHARACTER SET utf8 DEFAULT NULL COMMENT '机构类型',
`contactUser` varchar(32) NOT NULL COMMENT '联系人',
`contactPhone` varchar(32) DEFAULT NULL COMMENT '联系电话',
`adminUser` varchar(32) DEFAULT NULL COMMENT '管理员账号',
`status` tinyint(3) NOT NULL COMMENT '状态(0:有效, 2:禁用)',
`createUser` varchar(32) CHARACTER SET utf8 DEFAULT NULL COMMENT '创建人名称',
`createTime` datetime DEFAULT NULL COMMENT '创建时间',
`lastUpdateUser` varchar(32) CHARACTER SET utf8 DEFAULT NULL COMMENT '最后更新人名称',
`lastUpdateTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
PRIMARY KEY (`id`),
KEY `idx_accountNo` (`contactUser`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='公司(交易商)表';
增加机构表:
drop table if exists t_institution;
CREATE TABLE `t_institution` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '机构id',
`institutionTypeId` varchar(48) DEFAULT NULL COMMENT '机构类型id',
`detailInstitutionId` bigint(20) DEFAULT NULL COMMENT '机构关联id',
`detailInstitutionName` varchar(255) DEFAULT NULL COMMENT '机构关联名称',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='机构表';
增加系统全局配置表:
drop table if exists t_trade_global_config;
/*==============================================================*/
/* Table: t_trade_global_config */
/*==============================================================*/
create table t_trade_global_config
(
id bigint not null auto_increment comment '主键标识',
code varchar(32) comment '配置项编号',
category varchar(32) comment '分类编号(BASIC:基础配置, BUSINESS: 业务配置, SYSTEM:系统项配置)',
value varchar(128) comment '配置项的值',
status tinyint(2) not null comment '状态(0:启用, 1:禁用)',
primary key (id)
);
alter table t_trade_global_config comment '系统全局配置表';
/*==============================================================*/
/* Index: idx_key */
/*==============================================================*/
create index idx_key on t_trade_global_config
(
code
);
INSERT INTO `trade_stock`.`t_trade_global_config`(`id`, `code`, `category`, `value`, `status`) VALUES (1, 'REG_OPEN_ACCOUNT', 'SYSTEM', 'N', 0);
增加序列表:
drop table if exists t_seq;
/*==============================================================*/
/* Table: t_seq */
/*==============================================================*/
create table t_seq
(
id bigint not null auto_increment comment '主键标识',
code varchar(32) comment '配置项编号',
value bigint(21) comment '序列值',
primary key (id)
);
alter table t_seq comment '序列表';
/*==============================================================*/
/* Index: idx_code */
/*==============================================================*/
create index idx_code on t_seq
(
code
);
用户注册与开户是两个功能, 这里我们实现用户注册即开户的功能,会生成一个交易帐号, 通过开关控制。 因涉及到开关配置参数以及将来的系统参数配置处理, 需要用到系统全局配置表。
3.3 创建实体
-
修改Mybatis Generator 配置文件, 生成t_trade_account、t_trade_global_config、 t_trade_user_file、t_company、t_institution实体与数据层代码。
-
创建TradeAccount与TradeUserFile:
3.4 创建公用系统层组件
这里放置系统全局配置的处理, 便于各微服务模块的复用。
3.5 数据层
创建DAO与MAPPER文件:
将生成的代码做相应调整与修改, 数据层提供查询与创建接口。
创建用户注册逻辑相关接口:
-
校验用户是否已经注册接口:
IStockUserDao:
/** * 校验用户是否已经注册(包括手机号, 邮箱, 用户编号) * @param accountNo * @return */ Integer checkRegister(@Param("userNo") String userNo, @Param("email") String email, @Param("phone")String phone);
mapper文件:
<select id="checkRegister" resultType="java.lang.Integer" > select 1 from t_trade_user where userNo = #{userNo} or email = #{email} or phone = #{phone} limit 1 </select>
-
获取公司信息接口
作公司判断, 并冗余公司相关信息,IStockUserDao:
/** * 根据公司ID获取公司对象信息 * @param id * @return */ CompanyVo getCompanyVoById(@Param("id")Long id);
CompanyVo定义:
@Data public class CompanyVo { /** * 公司ID */ private Long id; /** * 公司名称 */ private String companyName; /** * 机构类型 */ private String institutionTypeId; /** * 联系人 */ private String contactUser; /** * 联系电话 */ private String contactPhone; /** * 管理员账号 */ private String adminUser; /** * 机构ID */ private String institutionId; }
mapper文件:
<select id="getCompanyVoById" resultType="com.itcast.trade.stock.user.vo.CompanyVo" > select u.id, u.companyName, u.institutionTypeId, u.contactUser, u.contactPhone, u.adminUser, u.status, u.createUser, u.createTime, t.id as institutionId from t_company u left join t_institution t on u.id = t.detailInstitutionId where u.id = #{id} limit 1 </select>
3.6 全局序列号生成方案
-
全局序列可能在多出需使用, 我们把它的实现放在stock-common-system公用组件下面。
-
这种方式适合并发量不大的业务场景, 对于高频度的交易场景,要结合缓存适用, 如果不需要保持全局序号,可以采用雪花算法。
-
数据层, 定义接口:
ITradeGlobalConfigDao:
/** * 根据编号获取序列ID * @param code * @return */ int getNextId(TradeSeq record);
-
Mapper文件, 通过update 来获取自增ID, 注意属性名称和类型不要写错:
<update id="getNextId" keyColumn="nextId" keyProperty="nextId" parameterType="com.itcast.trade.stock.entity.system.TradeSeq"> update t_seq set nextId = last_insert_id(nextId + 1) where code =#{code}; <selectKey resultType="long" keyProperty="nextId" keyColumn="nextId" order="AFTER"> SELECT LAST_INSERT_ID() </selectKey> </update>
-
Service层接口实现:
TradeGlobalConfigServiceImpl增加:
/** * 获取指定序列增长ID * @param code * @return */ public Long getNextSeqId(String code) { TradeSeq seq = new TradeSeq(); seq.setCode(code); tradeGlobalConfigDao.getNextId(seq); return seq.getNextId(); }
需要使用的地方, 通过ITradeGlobalConfigService服务类调用即可。
-
Service层接口实现:
StockUserServiceImpl增加:
/** * 生成用户编号 * @return */ private String generateUserNo() { // 获取用户账号 Long nextUserNo = tradeGlobalConfigService.getNextSeqId(GlobalSeq.USER_NO); log.info(" get the next userNo : " + nextUserNo); // 其中0表示补零, 后面标识长度 return String.format("%08d", nextUserNo); }
3.7 服务层
- 增加用户注册接口
StockUserServiceImpl代码实现:
/**
* 用户注册
* @param tradeUser
* @return
* @throws ComponentException
*/
@Transactional(rollbackFor = Exception.class)
public TradeUser userRegister(TradeUser tradeUser) throws ComponentException {
TradeUser newTradeUser = new TradeUser();
// 1. 判断用户信息是否已经注册
Integer checkResult = stockUserDao.checkRegister(tradeUserVo.getUserNo(), tradeUserVo.getEmail(), tradeUserVo.getPhone());
if(null != checkResult && checkResult > 0 ) {
throw new BusinessException(ApplicationErrorCodeEnum.USER_EXISTS);
}
// 2. 对公司信息作校验
CompanyVo companyVo = stockUserDao.getCompanyVoById(tradeUserVo.getCompanyId());
if(null == companyVo) {
throw new BusinessException(ApplicationErrorCodeEnum.USER_COMPANY_NOT_FOUND);
}
// 3. 构造生成用户信息
BeanUtils.copyProperties(tradeUserVo, newTradeUser);
newTradeUser.setUserNo(generateUserNo());
newTradeUser.setUserPwd(EncryptUtil.encryptSigned(tradeUserVo.getUserPwd()));
// 4. 完善冗余信息
newTradeUser.setInstitutionId(companyVo.getInstitutionId());
newTradeUser.setInstitutionTypeId(companyVo.getInstitutionTypeId());
newTradeUser.setCompanyName(companyVo.getCompanyName());
stockUserDao.insert(newTradeUser);
// 5. 注册即开户, 生成交易账户信息
TradeGlobalConfig tradeGlobalConfig = tradeGlobalConfigService.getTradeGlobalConfigByCode(GlobalConfig.REG_OPEN_ACCOUNT);
if(null != tradeGlobalConfig && GlobalConfig.VALUE_TRUE.equals(tradeGlobalConfig.getValue())) {
// 如果存在该项配置, 并且值为Y, 则代表注册即开户
// 交易账户信息的生成
TradeAccount tradeAccount = new TradeAccount();
tradeAccount.setAccountNo(generateUserNo());
// 先设置为系统的默认账户组, 后面可通过后台转组去划分到不同的组别
tradeAccount.setTradeGroupId(1L);
tradeAccount.setStatus(GlobalConfig.STATUS_VALID);
// 保存交易账户
tradeAccountDao.insert(tradeAccount);
}
//TODO 发送邮件/短信通知 (后面章节阿里云邮件发送功能时再做实现)
return newTradeUser;
}
-
先做用户检查, 支持手机号和邮箱方式注册, 要做重复判断。
-
这里用到了全局唯一序列生成用户编号, 通过Mysql表方式实现,结合last_insert_id函数, 只影响Connection, 防止并发问题。
-
校验通过之后, 保存用户信息。
-
邮件通知待后面邮件功能实现完成之后再加上。
3.8 接入层实现
新建StockUserOpenController类:
@RestController()
@RequestMapping("/open")
@Log4j2
public class StockUserOpenController {
@Autowired
private IStockUserService stockUserService;
/**
* 用户注册接口
* @return
*/
@RequestMapping("/register")
public ApiRespResult register(@Valid TradeUserVo tradeUser) {
ApiRespResult result = null;
try {
stockUserService.userRegister(tradeUser);
result = ApiRespResult.success(tradeUser);
}catch(ComponentException e) {
log.error(e.getMessage(), e);
result = ApiRespResult.error(e.geterrorCodeEnum());
}catch(BusinessException e) {
log.error(e.getMessage(), e);
result = ApiRespResult.error(e.geterrorCodeEnum());
}catch(Exception e) {
log.error(e.getMessage(), e);
result = ApiRespResult.sysError(e.getMessage());
}
return result;
}
}
通过调用stockUserService的userRegister处理登陆逻辑。
3.9 添加自动化校验
作为一个健壮的后台服务, 需要完善的校验机制, 但接口繁多, 通过自动化校验组件能让我们快速实现。
-
添加依赖, 采用较新的稳定版本6.0.16.Final:
<!-- 自动化校验 --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.16.Final</version> </dependency>
-
在Controller中在需要校验的参数中, 增加校验注解
-
在参数体中增加校验规则:
/** * 用户名称 */ @Size(min = 1, max = 32,message = "姓名长度必须为1到32") private String name; /** * 用户密码 */ @Size(min = 1, max = 32,message = "密码长度必须为1到32") private String userPwd; /** * 手机号 */ @Pattern(regexp="^((13[0-9])|(15[^4,\\D])|(18[0,5-9]))\\d{8}$", message="手机号格式错误") private String phone;
-
自定义校验错误信息
默认返回的校验提示信息繁杂, 为统一接口数据格式, 需要对自动化校验错误做层封装。
增加校验异常拦截器ParamValidExceptionHandler:
@ControllerAdvice
public class ParamValidExceptionHandler {
/**
* 捕获所有校验异常信息, 进行封装, 返回给客户端, 捕获的是BindException
* @param ex
* @return
*/
@ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public ApiRespResult handlerException(BindException ex) {
// 获取所有校验错误提示
StringBuffer stringBuffer = new StringBuffer();
List<FieldError> errors = ex.getBindingResult().getFieldErrors();
// 遍历属性校验结果集
for(FieldError fieldError : errors) {
stringBuffer.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage());
}
// 封装校验异常返回信息
ApiRespResult errorWebResult = ApiRespResult.validError(stringBuffer.toString());
return errorWebResult;
}
/**
* 拦截约束性异常的处理, 比如@NotBlank, 非空的必要性约束等, 捕获的是ConstraintViolationException
* @param ex
* @return
*/
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiRespResult handlerConstraintException(ConstraintViolationException ex) {
// 获取所有校验错误提示
StringBuffer stringBuffer = new StringBuffer();
Set<ConstraintViolation<?>> errors = ex.getConstraintViolations();
// 遍历属性校验结果集
for(ConstraintViolation<?> constraintViolation : errors) {
stringBuffer.append(constraintViolation.getMessage());
}
// 封装校验异常返回信息
ApiRespResult errorWebResult = ApiRespResult.validError(stringBuffer.toString());
return errorWebResult;
}
}
这样就完成了自动化校验的处理。
附上一些常用的校验规则的配置:
限制 | 说明 |
---|---|
@Null | 限制只能为null |
@NotNull | 限制必须不为null |
@AssertFalse | 限制必须为false |
@AssertTrue | 限制必须为true |
@DecimalMax(value) | 限制必须为一个不大于指定值的数字 |
@DecimalMin(value) | 限制必须为一个不小于指定值的数字 |
@Digits(integer,fraction) | 限制必须为一个小数,且整数部分的位数不能超过 |
@Future | 限制必须是一个将来的日期 |
@Max(value) | 限制必须为一个不大于指定值的数字 |
@Min(value) | 限制必须为一个不小于指定值的数字 |
@Past | 限制必须是一个过去的日期 |
@Pattern(value) | 限制必须符合指定的正则表达式 |
@Size(max,min) | 限制字符长度必须在min到max之间 |
@Past | 验证注解的元素值(日期类型)比当前时间早 |
@NotEmpty | 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0) |
@NotBlank | 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空 |
验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格 |
FAQ补充:
如果使用约束性异常@NotBlank不生效的情况, 要注意在使用的类上加上@Validated
public ApiRespResult userLogin(@NotBlank(message = "用户编号不能为空!") String userNo, String userPwd)
对应的类为StockUserController, 加上:
@Validated
public class StockUserController extends BaseController
3.10 启动服务
- 启动必要的依赖服务
启动用户服务, 认证服务可以不用启动, 因为注册功能是开放的, 用户注册成功, 拥有账号之后才能进行认证。
这也是新增的用户注册接口, 前缀路径为/open的原因, 便于我们区分管理。
检查认证配置ResourceSecurityConfigurer, 不要把包含/open前缀的请求加入验证:
3.11 功能验证
- 请求注册接口, 地址: 127.0.0.1:10681/open/register
注册成功, 返回用户信息。
- 校验功能验证(逻辑校验与自动化校验)
- 重复注册请求
提示用户已存在。
- 密码为空, 发出请求
出现我们预期封装的错误提示。
- 用户名和密码都为空, 发出请求, 能够将所有错误都提示出来
4. 总结
-
用户注册功能虽然简单, 但想要做得完善, 更为健全, 需要把每一步都做好, 通过全局序列号生成, 能够保障分布式服务的正常运行, 加入自动化校验, 能够避免非法请求, 不必要的异常或错误数据。
-
任何一个业务功能不要着急去实现, 先了解其业务流程, 做好底层数据库结构设计, 考虑需要提供哪些主要接口, 再从数据层到服务层编码实现, 形成良好的思路, 编写的代码也会层次清晰, 避免反复调整修改, 功能实现起来自然水到渠成。