搭建一个完整的微服务系统(四):微服务的公共依赖

        任何一个系统中,都有一个或多个基础项目,可生成jar包给所有服务依赖。在本示例(工程basejar)中,我给大家找了一些常用的进行说明,这些内容和业务无关,大家可以直接使用。

幂等相关

        这部分包括:AutoIdempotent.java、AutoIdempotentInterceptor.java、TokenService.java三个文件。其中前两个文件是声明注解及其拦截器,只要在需要幂等处理的Controller方法上加注解:@AutoIdempotent,就表示客户端在请求该方法时,必须要在header里加入:idenpotent=XXXX才能请求成功;最后一个文件是用来生成和验证幂等token的。
        使用场景:例如我们通过app来购买商品,在支付前会进入一个订单确认页,实际在进入这个画面前,已经向后台发起一个请求,获取一个幂等token,在用户确认支付时,再将这个token连同其它参数一起提交后台。这样,就可以避免一个订单多次支付的情况出现(不要说前台可以防止多次提交,因为后台业务的严谨不能依赖客户端)。
        原理:实际就是用户申请幂等token时,将生成的token放入缓存,用户发起支付时,检查缓存中有无token,如果有,则删除此token放行,否则抛出异常。

public boolean checkToken(HttpServletRequest request) throws Exception {
    String token = ThreadLocalHolder.getSingle();
    if(StringUtils.isBlank(token)){
        throw new CustomerException("幂等token没有提交");
    }
    if(!redisBaseService.exists(token)) {
        throw new CustomerException("17002");
    }
    redisBaseService.remove(token);
    return true;
}

表的创建和修改时间

        很多人的喜欢在表里定义两个字段create_time和update_time,而且经常把这两个字段当成有业务意义的字段来使用,虽然我个人觉得不太好,但也没什么不对。如果仅仅这两个字段当作无业务意义的字段使用,在我多年的经验中,真正发现它有用,只遇上过一次:曾经做过一个数据量超大的项目的重构,部署时需要将老的数据库向新库里导入,白天是不能查询业务系统数据库的(因为I/O消耗太大),但一个晚上,老系统的数据不能全部导出,那时,这两个字段对我们的批处理起到了决定性的作用。
        这部分包括三个文件:CreatedTimeFuncation.java、UpdatedTimeFuncation.java、CreateUpdateTimeInterceptor.java,两个注解声明及其拦截器。只需要在数据库实体文件中,在相应的对象前加上这两个注解就可以了。当你对这个表进行插入时,create_time和update_time会自动插入当前时间戳,修改时update_time会更新成新的时间戳。

Header传递

        微服务之间将Http Header中的内容传递下去,是个很常见的需求,在我们的示例中,将sessionId(app侧登录凭据)、language(语言)、version(版本号,用来向下兼容)、timestamp(时间戳,重要,留到将网关时讲)、idenpotent(幂等token)放到了header中。
        这部分包括两个文件:ThreadLocalHolder.java和AuthenticationInterceptor.java,前者可以理解为声明header中内容,后者是自动将header中内容放到ThreadLocalHolder中,这样开发人员无论在开发哪个微服务,直接可以从ThreadLocalHolder中获取header中的内容,而不用关心它是怎么传递的。

public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
    // 从 http 请求头中取出 token
    String token = httpServletRequest.getHeader(ThreadLocalHolder.SESSION_FIELD);
    String lan = httpServletRequest.getHeader(ThreadLocalHolder.LAN_FIELD);
    String ver = httpServletRequest.getHeader(ThreadLocalHolder.VER_FIELD);
    String curTime = httpServletRequest.getHeader(ThreadLocalHolder.TIME_FIELD);
    String idenpotent = httpServletRequest.getHeader(ThreadLocalHolder.SINGLE_FIELD);

    ThreadLocalHolder.set(ThreadLocalHolder.SESSION_FIELD, token);
    ThreadLocalHolder.set(ThreadLocalHolder.LAN_FIELD, lan);
    ThreadLocalHolder.set(ThreadLocalHolder.VER_FIELD, ver);
    ThreadLocalHolder.set(ThreadLocalHolder.TIME_FIELD, curTime);
    ThreadLocalHolder.set(ThreadLocalHolder.SINGLE_FIELD, idenpotent);

    return true;
}

方法统一返回对象

        各微服务所有Controller方法都返回统一结构对象是很有必要的,在本示例中的BaseResponse.java就声明了这么一个对象。如果Controller方法成功返回,在方法尾写上:return new BaseResponse(Object)就可以了,如果是失败返回,可以在任何代码位置抛出自定义异常,如throw new CustomerException(“10001”);

错误信息国际化

        系统是需要支持多语言的,那返回的错误信息也要是多语言的。本示例中,每个微服务要么没有操作数据库,要么操作一个数据库,每个数据库里有一张表:multi_language,用来保存每个错误码对应的message。

异常处理

        这部分包括:CustomerException.java、GlobalException.java两个文件。前者是自定义异常,包括错误码和参数两种入参;后者是异常处理,它可以处理如下情况(下面描述的都是业务异常,系统异常开发人员不用管,GlobalException一并处理):
            1. throw new CustomerException(“10001”) — GlobalException会从数据库中读取这个errCode对应的message返回给调用者。
            2. throw new CustomerException(“10001”,“张三”) — GlobalException会从数据库中读取这个errCode对应的message,且将读出的message里的“%s”换成"张三"。
            3. throw new CustomerException(“10001”,“张三的美金账户里只有100元,不够支付”) — 这有2种应用场景:
                a. 有时,需要一个很复杂的业务逻辑才能拼出错误信息,无法用No2来事先约定,这时,就可以使用这种方式。
                b. 前一个微服务返回的失败的BaseResponse对象,本微服务对应的数据库里没必要也声明同样的code和message,只需要将收到的BaseResponse对象的code和message重新throw就可以了。

@ExceptionHandler(CustomerException.class)
public JSONObject globalException(CustomerException e) {
    String code = e.getCode();
    String message = getMessage(code);

    if (message == null && e.getArgs().length>0){  //说明是上一个服务出的问题,本服务的数据库里没有这个errcode,直接返回就好
        Object[] args = e.getArgs();
        message = args[0]+"";
    }else {
        if (!StringUtils.isEmpty(message)) {  //比如想返回e.getMessage(),则在表里保存的errCode对应的message为null
            Object[] args = e.getArgs();
            if (args.length > 0) {
                if (message.indexOf("%") > 0) {
                    if (args != null && args.length > 0) {
                        message = String.format(message, args);
                    }
                }
            }
        } else {
            Object[] args = e.getArgs();
            if (args.length > 0) {
                message = args[0] + "";
            }
        }
    }
    log.error("自定义异常:{} {}", message, code);

    JSONObject result = new JSONObject();
    result.put("msg", message);
    result.put("code", code);
    return result;
}

SQL文打印

        log4j打印出来的sql文和参数是分开的,PrintSqlInterceptor.java可以合在一起打印,并显示出sql文的执行时间。

public Object intercept(Invocation invocation) throws Throwable {
    String sql = "";
    long beginTime = System.currentTimeMillis();
    try {
        // 获取xml中的一个select/update/insert/delete节点,是一条SQL语句
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        Object parameter = null;
        // 获取参数,if语句成立,表示sql语句有参数,参数格式是map形式
        if (invocation.getArgs().length > 1) {
            parameter = invocation.getArgs()[1];
        }
        String sqlId = mappedStatement.getId(); // 获取到节点的id,即sql语句的id

        BoundSql boundSql = mappedStatement.getBoundSql(parameter); // BoundSql就是封装myBatis最终产生的sql类
        Configuration configuration = mappedStatement.getConfiguration(); // 获取节点的配置
        sql = getSql(configuration, boundSql, sqlId); // 获取到最终的sql语句
        // 执行完上面的任务后,不改变原有的sql执行过程
        return invocation.proceed();
    } catch (Exception e) {
        e.printStackTrace();
        // 执行完上面的任务后,不改变原有的sql执行过程
        return invocation.proceed();
    } finally {
        long endTime = System.currentTimeMillis();
        long costTime = endTime - beginTime;
        sql = String.format("[耗时:%sms] ", costTime) + sql;
        if (costTime > 100) {
            logSlow.info(sql);
        } else {
            log.info(sql);
        }
    }
}

总结:任何一个系统,公共依赖里都有大量内容,示例中其它一些内容没有解释,有兴趣就自己看,另外一些留到使用时再讲解。

上一章:搭建一个完整的微服务系统(三):代码总体说明
 
示例代码

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值