任何一个系统中,都有一个或多个基础项目,可生成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会更新成新的时间戳。
微服务之间将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;
}
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);
}
}
}
总结:任何一个系统,公共依赖里都有大量内容,示例中其它一些内容没有解释,有兴趣就自己看,另外一些留到使用时再讲解。
上一章:搭建一个完整的微服务系统(三):代码总体说明
示例代码