智牛股_第8章_Sentinel

智牛股_第8章_Sentinel

学习目标

目标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 模式的数据源。规则持久化;一致性;快速引入第三方依赖
  1. 原始模式

在这里插入图片描述

  1. Pull拉取模式

在这里插入图片描述

  1. Push推送模式

在这里插入图片描述

优点: 基于较强的实时性,支持动态修改发布,重启服务不受影响, 具有较高的可靠性与一致性。

官方建议采用Push模式。

3.2 用户服务集成
  1. 功能设计
    在这里插入图片描述

    我们把上一章完成的用户接口进行改造, 加入降级与限流处理。

    整个部署结构采用PUSH模式, 配置规则放置Nacos配置中心,统一发布与更新。

    在生产环境中, 如果在峰值时间段, 出现大量用户请求或者内部系统出现问题, 这个时候就有必要进行限流和降级处理, 防止穿透、雪崩导致整个服务瘫痪。

  2. 修改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该依赖须加上。

  3. 修改用户登陆接口

    修改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, 返回错误提示给客户端。

  4. 修改工程配置

    修改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 功能使用验证
  1. 启动用户服务

    如果策略配置通过Nacos获取成功, 会有以下提示信息,如果没有提示(新版本会变化)也没关系, 能够在监控台显示即可。

在这里插入图片描述

我们在Nacos配置的一条降级策略与一条限流策略加载成功。

  1. 启动认证服务

    上一章我们把用户服务集成了OAUTH2, 需要把认证服务启动, 并申请TOKEN。

  2. Sentinel监控台

    由于Sentinel采用懒加载方式设计, 如果启动服务成功, 监控台不能发现,先调用一次服务接口即可。

    进入监控台查看规则, 可以看到都已经同步拉取下来:

在这里插入图片描述

与Nacos中配置的内容一致。

  1. 限流策略验证

    我们配置了限流和降级两个策略, 便于演示, 把两个策略规则修改下, 在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

在这里插入图片描述

限流策略与是否触发异常无关, 拿正常访问链接, 加快刷新频率, 可以看到出现了限流的错误提示:
在这里插入图片描述

  1. 降级策略验证:

    先把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 创建实体
  1. 修改Mybatis Generator 配置文件, 生成t_trade_account、t_trade_global_config、 t_trade_user_file、t_company、t_institution实体与数据层代码。

  2. 创建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 全局序列号生成方案
  1. 全局序列可能在多出需使用, 我们把它的实现放在stock-common-system公用组件下面。

  2. 这种方式适合并发量不大的业务场景, 对于高频度的交易场景,要结合缓存适用, 如果不需要保持全局序号,可以采用雪花算法。

  3. 数据层, 定义接口:

    ITradeGlobalConfigDao:

        /**
         * 根据编号获取序列ID
         * @param code
         * @return
         */
        int getNextId(TradeSeq record);
    
  4. 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>
    
  5. 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服务类调用即可。

  6. 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 服务层
  1. 增加用户注册接口

在这里插入图片描述

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 添加自动化校验

作为一个健壮的后台服务, 需要完善的校验机制, 但接口繁多, 通过自动化校验组件能让我们快速实现。

  1. 添加依赖, 采用较新的稳定版本6.0.16.Final:

        <!-- 自动化校验 -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>6.0.16.Final</version>
        </dependency>
    
  2. 在Controller中在需要校验的参数中, 增加校验注解
    在这里插入图片描述

  3. 在参数体中增加校验规则:

        /**
         * 用户名称
         */
        @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;
    
    
  4. 自定义校验错误信息

默认返回的校验提示信息繁杂, 为统一接口数据格式, 需要对自动化校验错误做层封装。

增加校验异常拦截器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验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格

FAQ补充:

如果使用约束性异常@NotBlank不生效的情况, 要注意在使用的类上加上@Validated

public ApiRespResult userLogin(@NotBlank(message = "用户编号不能为空!") String userNo, String userPwd)

对应的类为StockUserController, 加上:

@Validated
public class StockUserController extends BaseController
3.10 启动服务
  1. 启动必要的依赖服务
    启动用户服务, 认证服务可以不用启动, 因为注册功能是开放的, 用户注册成功, 拥有账号之后才能进行认证。
    这也是新增的用户注册接口, 前缀路径为/open的原因, 便于我们区分管理。
    检查认证配置ResourceSecurityConfigurer, 不要把包含/open前缀的请求加入验证:
    在这里插入图片描述
3.11 功能验证
  1. 请求注册接口, 地址: 127.0.0.1:10681/open/register

在这里插入图片描述

注册成功, 返回用户信息。

  1. 校验功能验证(逻辑校验与自动化校验)
  • 重复注册请求

在这里插入图片描述

 提示用户已存在。
  • 密码为空, 发出请求

在这里插入图片描述

 出现我们预期封装的错误提示。
  • 用户名和密码都为空, 发出请求, 能够将所有错误都提示出来

在这里插入图片描述

4. 总结

  • 用户注册功能虽然简单, 但想要做得完善, 更为健全, 需要把每一步都做好, 通过全局序列号生成, 能够保障分布式服务的正常运行, 加入自动化校验, 能够避免非法请求, 不必要的异常或错误数据。

  • 任何一个业务功能不要着急去实现, 先了解其业务流程, 做好底层数据库结构设计, 考虑需要提供哪些主要接口, 再从数据层到服务层编码实现, 形成良好的思路, 编写的代码也会层次清晰, 避免反复调整修改, 功能实现起来自然水到渠成。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

管程序猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值