
阿里妹导读
问题代码
public String insert(PayRequest payRequest) {// 省略部分无关代码PayRequestDO payRequestDO = convertor.toDO(payRequest);payMapper.insert(payRequestDO);return payRequest.getPayNo();}
这个方法很简单,就是把传过来的 PayRequest 对象转成 PayRequestDO 对象,然后插入数据库。
public class PayRequest {private String payId;private String payNo;private Status status;// 省略其它属性和 getter setter}
public class PayRequestDO {private String payId;private String payNo;private String status;// 省略其它属性和 getter setter}
至于 PayConvertor#toDO 方法,也很简单,就是属性拷贝:
public PayRequestDO toDO(PayRequest payRequest) {PayRequestDO payRequestDO = new PayRequestDO();payRequestDO.setPayId(payRequest.getPayId());payRequestDO.setPayNo(payRequest.getPayNo());payRequestDO.setStatus(payRequest.getStatus().getCode());// 省略其它代码return payRequestDO;}
排查问题
先简单花些时间,排除掉一些写了代码没发布、或是部署错了版本等等类似的低级问题,确保服务器上面跑的代码就是上面贴出来的代码,这一点非常非常重要,永远是查问题时第一件要做的事情(其实大部分的问题在这一步就可以得到解决)。
# 配置开放的 Actuator 端点,开放 endpoint 需要注意数据安全,可以配置不同的 management port 或脱敏敏感内容management.endpoints.web.exposure.include=info
mvn packge 构建并以 java -jar 启动后,接下来就可以访问 localhost:8080/actuator/info 来获得当前的 git 提交信息:
{"git":{"commit":{"time":{"epochSecond":17011234567,"nano":0},"id":"1234567"},"branch":"master"}}
通过这个 commit id 就可以找到代码具体是哪个版本。
2023-11-14 21:06:04.221|PayCenter|00|8||N|trace8423002774916857900o38o50|payCenter|pay_request_record|pay_request_record|INSERT|insert into pay_request (id, pay_id, pay_no, status) values (null, 'pay2023001', '20231114000000001', '')|0|ServiceHandler-11.2.60.188:20880-thread-8|0.1
通过上面的 digest 日志的 sql 可以看出来,insert sql 里面的 status 字段,传的就已经是 '' 空白字符了,这说明问题不是发生在 orm 框架里面,这里排除掉了 xml 中 sql 的语句写的不对的问题。所以问题一定发生在插入数据库之前的业务方法中。
watch com.xxxxx.paycenter.service.repository.impl.PayRequestRepositoryImpl insert '{params,returnObj,throwExp}' -n 1 -x 3
接着我们等待方法执行到 insert,就可以观察 Arthas watch 输出的内容:
method=com.xxxxx.paycenter.service.repository.impl.PayRequestRepositoryImpl$$EnhancerBySpringCGLIB$$4f979dec.insert location=AtExitts=2023-11-15 14:54:49; [cost=6.001557ms] result=@ArrayList[@Object[][@PayRequest[serialVersionUID=@Long[1],payId=@String[pay2023001],payNo=@String[20231114000000001],status=@Status[INIT],// ...],],@String[20231114000000001],null,]
可以清楚地看到,这里入参的时候 status 属性还是有值的:Status.INIT
watch com.xxxxx.paycenter.infrastructure.dal.mapper.PayMapper insert '{params,returnObj,throwExp}' -n 1 -x 3
接着等待方法执行到 mapper 的 insert,观察 Arthas watch 到的内容:
Affect(class count: 2 , method count: 1) cost in 289 ms, listenerId: 10method=com.sun.proxy.$Proxy153.insert location=AtExitts=2023-11-15 14:51:36; [cost=3.933553ms] result=[[][[id=null,payId=[pay2023002],payNo=[20231114000000002],status=[],// ...],],[1],null,]
可以看到,很明显,到插入数据库时候,status 已经变成空了!!!
watch com.xxxxx.paycenter.core.convertor.PayConvertor toDO '{params,returnObj,throwExp}' -n 1 -x 3
输出:
method=com.xxxxx.paycenter.core.convertor.PayConvertor.toDO location=AtExitts=2023-11-15 15:01:35; [cost=0.432887ms] result=[[][[serialVersionUID=[1],payId=[pay2023003],payNo=[20231114000000003],status=[INIT],// ...],],[id=null,payId=[pay2023003],payNo=[20231114000000003],status=[],// ...],null,]
看输出结果,问题确实发生在 toDO 的内部,数据转换后 status 的属性没了
payRequestDO.setStatus(payRequest.getStatus().getCode());
找到原因
public enum Status {INIT("INIT", "初始态"),SUCCESS("SUCCESS", "成功"),FAILED("FAILED", "失败"),;private String code;private String desc;Status(String code, String desc) {this.code = code;this.desc = desc;}public String getCode() {return code;}public void setCode(String code) {this.code = code;}public String getDesc() {return desc;}public void setDesc(String desc) {this.desc = desc;}}
看着枚举类属性的 setter 方法,我不由得陷入了沉思:为什么一个枚举类的属性,要提供 setter 方法?
Status.FAILED.setCode("SUCCESS");Status.SUCCESS.setCode("FAILED");
很显然这里提供的 setter 调用直接破坏了枚举类,所以,最好的办法就是为枚举类属性加上 final。
watch com.xxxxx.paycenter.core.enums.Status getCode '{target}' -n 1 -x 3method=com.xxxxx.paycenter.core.enums.Status.getCode location=AtExitts=2023-11-15 15:05:36; [cost=0.005638ms] result=[[INIT=[INIT=[INIT],SUCCESS=[SUCCESS],FAILED=[FAILED],code=[],desc=[],name=[INIT],ordinal=[0],],
根本原因
public void setCode(String code) {try {throw new RuntimeException();} catch (Exception e) {log.error("code before: {}, after: {}", this.code, code, e);}this.code = code;}
有的小伙伴反馈抛异常来看堆栈太丑了,这也提供一个不用抛异常的方案:
public void setCode(String code) {StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();log.error("code before: {}, after: {}", this.code, code, formatStackTrace(stackTrace));this.code = code;}public static String formatStackTrace(StackTraceElement[] stackTrace) {StringBuilder stringBuilder = new StringBuilder();for (StackTraceElement element : stackTrace) {stringBuilder.append(element.getClassName()).append(".").append(element.getMethodName()).append("(").append(element.getFileName()).append(":").append(element.getLineNumber()).append(")").append(System.lineSeparator());}return stringBuilder.toString();}
增加了代码后发布上去,很快打印出来了堆栈:
改进措施
欢迎加入【阿里云开发者公众号】读者群
转载于:https://mp.weixin.qq.com/s/38teLuhPe17h0yuTa9dbcg
支付宝打赏
微信打赏