软件开发中的一行代码错误

写这篇文章主要是回顾了一下最近两周带来的2个错误,这2个错误都只是一行代码造成的,当然不是相同的一行。

我分别贴出下面的2行代码:

            CorpContract corpContract = corpContractService.getById(contractId);
            boolean useAutoDateInSignature = Optional.ofNullable(corpContract)
                    .map(CorpContract::getContractTemplateId)
                    .map(it -> sysContractTemplateService.getById(it))
                    .map(it -> Objects.equals(it.getUseAutoDateInSignature(), 1))
                    .orElse(true);

第一处出错的地方是在orElse(true),第一次修改时,改为了false,应该是true.

第二处代码:

    private FlexTaskPayService flexTaskPayService;

    public List<String> queryUrlList(String taskPayId, String bankCardNo) {
        FlexTaskPay flexTaskPay = flexTaskPayService.getById(taskPayId);
        return getReceiptUrl(flexTaskPay, bankCardNo);
    }

第二处出错的地方在flexTaskPayService字段上没有加上@Autowired,或者我常用的@Resource.

先回到第一个错误,这个错误引出了一个bug,就是证明盖章的时候,原来是有默认的盖章日期的,结果一改,就没有了。

第一个错误,主要还是逻辑思维问题,在想问题的时候,有点没有想清楚,在使用有点陌生的Optional.map()链式调用时,上面代码的逻辑是:如果corpContract不空,则判断它的contractTemplateId是否为空,如果不为空,则去查询SysContractTemplate,如果查询出来也不为空,则判断它的签名中是否带日期是开启的(值为1),如果是就是true,否则其它逻辑都会返回false. 这时候其它逻辑就是:corpContract为空,或者它的contractTemplateId为空,或者根据这个id查询出来的SysContractTemplate为空,或者SysContractTemplate中签名是否带日期是关闭的(值为0)。

为了兼容历史数据,SysContractTemplate中签名是否带日期默认值是1,也就是开启的,同时由于证明也是用了这段逻辑,所以orElse(true)才能兼容历史的情况,否则就会造成bug.把证明中的盖章日期弄丢了。

第二个错误就不是思维问题了,简单一点讲就是不小心引入的一个漏写的问题。这个错误,一般情况下很容易发现,因为少了注入就会抛NPE,结果在我们的日志中就是没有任何错误,同时这个项目的特殊性:只有内部可以调用,和支付相关。我们测试也只有在生产环境中才能测试,所以还一度怀疑自己,怀疑项目,不清楚任何问题,因为日志中只打印了一个code:1001,status:200,看上去正常,实则很慌,那会刚好这个接口用curl命令一调(很少用),服务也不打印其它的日志,还以为这个接口把服务整坏了,就一度担心和怀疑。但是写的代码就很简单,从controller传值,调用已经写的方法,只是中间写一个查询数据的方法,结果就是这个service没有注入导致的。如果在一般情况下,肯定会打印NPE的,能很快发现问题,结果在我们目前的框架下,就是发现不了,因为我们不会怀疑会有NPE,也不会想到忘了写@Autowired.然后那会我们的关注点还在传参数上,因为我平时写的GET请求都是用@RequestParam(“payTaskId”)String payTaskId,这样,结果和同事一起看下,他说不用带,还有就是我就省懒只写了一个@RequestParam String payTaskId,当然这些都是暴露了对于spring mvc对于参数注入知识理解和掌握的不到位。

从事后的角度来看,对于知识掌握的不足会让问题偏离方向。

在依赖看日志的时候,如果日志本身出了问题,那么就很有可能需要先修复日志本身的问题,但是这会带来额外的工作量,会延长本身问题的修复时间。

最终还是需要回归到代码层面,从代码找问题。

从这个小的问题来看有3个代办:(1)需要验证@RequestParam传参数问题。

(2)需要改进Springboot对于日志拦截曾的处理,不然一个很小错误,都会排查很久。

(3)可以用更好的方式避免,不写@Autowired带来的问题,那就是使用final,同时使用构造器注入的方式,由于这种方式在项目中很多处都不是这么写的,会带来代码的不一致问题,不过也是一个好的改进,如果是新的项目,值得推广。

最后从解决这个问题来看,写这些代码本质是为了解决另一个问题,今天所写只是临时解决了一个问题,从问题的根本性上来说,需要一个更加完美的自动化去解决问题,而不是再手动处理,尤其是需要一直这么手动处理的话,就更需要找到根本问题,同时让系统尽量自动化。

软件开发中的错误

我反思最近一段时间的工作来,快有4周的时间,我在一个新的环境中着手开发了一些功能,从功能的完整性来说,不是什么全新的功能,基本上是系统现有功能的增强类型,或者小小的功能扩展。

如果说一个全面的系统,或者说是一个全面的模块,甚至一个模块中的一部份功能,都是很多人共同努力的结果,也是由一小块一小块代码组成的。

我们程序员每天工作产出的代码也是一小块,一小块的。错误的引进在软件的使用者来说,就是bug,显然bug会降低用户的信心,同时也会打击相关人员的热情,降低公司的影响力和对外的形象。所以不管站在什么角度来说,我们都会注重代码的质量,注重工作的流程,代码只是工作的成果,从因果法则来说,我们应该注重因,我们种的因是代码,那么怎么产出代码的?怎么产出bug的。

有一些是代码的问题,写的时候不够细心,一下子就敲错了,比如字段取的不匹配,我最近就犯了这个错误,这个错误通过测试可以发现,但是测试也可能不能发现,只是刚好数据匹配上了错误的情况。测试算是一种检验。这种不够细心,敲错的情况时有发生,人总是会犯错,就像铅笔的那头就是橡皮。

今天查询的时候就忘了带条件,但是由于数据的原因这个错误巧合地隐藏起来了,测试反应的结果并不能证明程序没有错误,测试如果发现了错误可以证明程序有问题,但没有发现错误可能需要更多的努力。

在处理字符串转数字的时候,如果把toPlainString()换成toString()就可能出现科学计数法表示的数字。

BigDecimal.valueOf(Double.parseDouble(value)).stripTrailingZeros().toPlainString()

像这种错误属于基本功不够扎实,记忆不够牢靠导致的。

我一直相信有些错误是可以避免的,好的编程习惯就可以实现。上面的错误不是习惯的问题,但是测试是可以立马反馈的,所以说对于不确定的地方 编写测试是一个好的习惯。但是没有写测试,是过于自信?

我想对于写测试这件事,如果是测试驱动开发,天然的环境是有利的,所以辨别是否不确定性是一个编程功底,而不是盲目地自信和放任天性中的偷懒行为。

吾之生涯而有限知而无限

在我每次去“每日一本编程书”网站的时候,我就发现几乎过一段时间就会有新的技术书籍出版了,我也会尽快保存下来,尽管我没有及时去看,因为我知道看书是需要时间的,同时就算有时间,你也不一定会去看书,因为看书还需要心境,那种求知的心境,那种甘于寂寞的心境,还有一点就是技术书籍还需要实际去练习的,不仅仅是看完了就是掌握了,最多是了解了,因为很多的知识地掌握需要靠练习,仅仅通过阅读一次就真正掌握是不可能的。

在技术的路上,回归本质,就是到了数据结构和算法,这是最最本质的东西。在本质之外都是一些编程的方法和技巧,还有一些工程的实践。

我在这编程的6年多时间里,绝大多数的编程任务都是对数据进行处理,也就是常说的ACID,几乎所有的编程任务都是围绕这个主题,但是在它的外围体现的是我们对于模拟真正世界的思考,可能会用上一些设计模式,用上一些编程实践,比如测试驱动开发,还有敏捷开发。当然,这满满的都是在后端的基础之上的,前段的编程任务还是负责数据的输入和展示问题。这几年,我写前端的代码时间还是少了很多。

技术一直都在发展,因为总是有新的问题不断地产生和被发现,所以用于解决问题的框架也就多了起来,但是我们把时间花在不断地学习框架上,显然这些投入是将是会不断的过时,比如现在很少会去写JSP页面,因为现在都是前后端分离的框架。同时现在用到Struts2的时间也是少了,因为都是用SpringMVC来完成的,这些Web技术虽然都是过时的,但是对于掌握了其底层的原理来说是极好的,因为新的框架的一项基本功能就是封装以便提供便捷性,也就是编程友好性,当然它也有它的口号——约定优于配置,所以学习的一些成本就是掌握它的一些事先的默认配置。Springboot框架就是其中的典范。

新的技术总是在不断地替代老的技术,总是在不断地更新迭代,SpringCloud项目的发展就可以看到其中的一些端倪。在微服务兴起的那段时间,最早我还去用过Dubbo,最后还是被SpringCloud全家桶所捕获,在Java的生态圈下,或者说是企业级web应用,基本上都没有离开spring框架的领地。

从网关开始,最早的时候还是zuul,后面就是spring cloud gateway。然后就是服务注册和发现—-先是Eureka,后面就是Consul。还有服务的调用,现实Feign,后面又是Open Feign。为了解决服务调用过程中的熔断和降级,之前是Spring Retry,后面又有Resilience4J。甚至过了一段时间Spring Cloud Alibaba三大微服务开发利器问世——Nacos, Sentinel,Seata. 其中Nacos不仅仅解决了服务的注册发现问题,还顺便把服务的配置问题也一块解决了,不然就是之前的Spring Cloud config server那一套。在有些场景之下,分布式事务的引入也是一个不小的挑战,Seata就是其中的利器。在解决无需重启的服务的情况下,配置的更新问题又有了Spring Stream . 当然在它的背后是消息中间件在发挥作用,那时我开始学习Kafka,后来的工作中我又用到了RabbitMQ.在一整套微服务的框架后面又是需要追踪服务的调用链路,很早的时候是hystrix,zipkin ,后面又有sleuth甚至更广的还有Skywalking。还有服务的日志聚合问题,在分布式的环境下之前用的是ELK那一套。同时服务的部署问题也开始引入了Docker, 甚至后面的服务编排——k8s。

这些技术的展开需要花上很久的时间,因为技术一旦深入都是一个个的细节,我们很有可能会错过其中一些。在微服务的开发风行之下,的确服务开发和维护的成本较高,但是面对需要不断市场和业务挑战,这是一个很好的方式,因为事情一旦小,就容易控制风险,也是符合控制变量法的逻辑,这种能快速找到问题的关键,从何发现问题,解决问题或者得出结论。所以这些都是在说明技术产生的原因。

技术的不断更新,也是会带给我们新的认知。在云原生的时代,CNCF是一个新的起点。一切都是在云上,还有infrastructure as code, 基础设施即代码,还有从devops到了gitops时代,新的技术Terraform又是新兴之星。

从整个软件的生命周期而言,我的绝大多数的时间都是在软件的需求分析和设计,编码和测试上后面的部署问题,就基本只是了解一番。我多走了一步,就是学了一番。因为那时我的想法就是在一个小的公司,全才是很重要的竞争力。

在一个大的公司环境,我越来越感受到了全是一件不可取的事情。时间和精力的有限性,在分工明确,合作良好的公司,做好自己本职的工作就是最清醒的认知。

在编程的世界,我之前一直认为我们在从事创作性工作,从无到有,每天的工作内容都是新的,接受的是新的挑战,但是再回归一层就是模拟世界的角度,它不是被创造而不过是模仿,希望模仿还不是拙劣的。

最后的结论,在生涯有限的情况下,学海无涯,我可以做好一件事,认真的做一件事就是极好的。少即是多。业精于勤。