透过现象看本质–编程中的错误解决

我之前在思考怎么快速地开发高质量软件,其中就有“ 快速 ”二字,我们开发中如果顺畅地写代码,时间不会占用太多,但如果写代码的过程中“不顺”,我们可能会陷入进去,这是一个很自然的结果。然后一陷入进去,意味着你可能去“探索”去了,你如果快速地解决了问题,这倒是一个很好的结果。

但我们现在回到问题解决的本身上。

解决问题,尤其是开发中的问题,有一些是,技术问题,比如我在写代码过程中遇到的:

这个错误是在badUser.html页面中使用到了
<h1 class=”alert alert-danger” th:utext=”${param.message[0]}”>error</h1>

这主要是Thymeleaf中的问题,由于我使用的较高版本的Thymeleaf 3.0.11,上面的代码在低版本中是没有问题的,如果在不会退版本的前提下——假定的项目框架环境,我们来解决这个问题。

具体的错误信息还是比较清晰的:

A problem occurred whilst attempting to access the property ‘param’: ‘Access to variable “param” is forbidden in this context. Note some restrictions apply to variable access. For example, direct access to request parameters is forbidden in preprocessing and unescaped expressions, in TEXT template mode, in fragment insertion specifications and in some specific attribute processors.’

根据错误我们看一下源码:

错误信息指出,在受限的访问上下文中,如果访问的是受限变量–>param,就会有TemplateProcessingException。

那么我们可能思考的是怎么去解决这个问题?

错误说它不允许访问param变量中的信息,它不允许是出于安全的角度来限制的,那么它不允许我们访问,我们又需要访问,那框架总要提供一种方式,让我们访问,可能我需要需要做一些调整,或者额外的努力,但是我们的功能性需求就是访问到后台传到页面中的信息。

……

上面很容易陷入到“思考怎么解决问题”这个细节出来,这是我们软件开发中可能会经常出现的技术问题。

但是在这些技术问题的背后,其本质是什么呢?

在遇到技术问题时,如果你还记得—— 当问题是一个问题的时候,那一定是

问题 > 人 

我们太关注问题本身了,所以让它难住了我们。

我们深刻地意识到问题之所以是问题的时候,那么就是开始没有问题的时候。

上面说到的技术问题,其实本质是我们对Thymeleaf框架的内部细节没有掌握,如果我们看到上面出错的源码,只是问题的局部,也就是问题森林中的一棵树。

但如果我们看到的是整个森林,我们就不会陷入到具体的一棵树上。我们如果熟悉Thymeleaf框架,我们会就理解为什么在受限的上下文中访问受限的变量需要抛出一个异常。我们会进一步理解,什么是受限的上下文。什么是受限的变量。如果需要访问受限的变量,怎么做。

所以我们需要去“探索”它产生的原因,于是我们来到了GitHub

而我们代码中真正报错的也是 th:utext=”${param.message[0]}”>error,所以我们可以换一个标签th:text=”${param.message}”,这是直觉,也是一些知识的积累。然后就是这个小小的改动就可以实现错误的解决,也同时完成了功能性需求。

我以前也是经常想,为什么我不能快速把问题解决掉呢?还总是带着一丝丝自责,觉得自己不够好,不够优秀,至少是没有快速地解决问题。其实在开发中我们基本上不可能是一番风顺的,软件的不透明性就决定了软件开发的复杂性的存在,所以积累知识是一方面,另一方面是快速定位问题的发生根本原因。

还有一种很大的可能你没有遇到上面那个问题—— 使用th:text 而不是th:utext, 因为你可能一开始就是接触到了th:text, 问题的解决就发生在了问题发生之前。有时候这是习惯的力量,有时候这是知识的力量。

但上面的问题发生在“遗留的代码”中,也就是说并不是你自己从头开始写的代码,也是维护别人的代码,这也是开发中经常存在的。我们经常会忽略一些细节,因为我们也可能不知道,比如上面的th:utext标签随着版本迭代升级而新出的问题。

如果说知识的积累是一个方面,我们可能在这一块没有什么优势–>由于我们年轻,所以积累是件好事,年轻的我们可以积累。同时在“ 积累”有所欠缺的情况下, 我们要动脑筋,想办法弄清楚问题发生的根本原因,然后可以利用一些工具,比如搜索引擎来收集信息,定位问题,然后再解决问题,当然这是个人的独立解决问题的方式,如果你的环境中有集体帮助,比如头脑风暴,也是会有帮助的。

集体中,不要害怕问题,也不要害怕问问题,只要是你经过思考的问题,提炼问题,你可以在合适的时候尝试与别人交流一下。因为解决问题本身是件快乐的过程,帮助他人也是,问高质量问题也是的。一个好的环境一定是彼此共同学习,共同成长,共同提高的。

如果要总结一下全文,编程中的确会有遇到一些不顺的时候,本文就只提到技术问题,可能还会有其它的问题,在解决技术问题的过程中,我们要透过问题本身看问题本质,比如出现这个问题的根本原因是你不知道某些技术细节导致问题发生,所以你要去发现、学习那些技术细节,然后才能解决问题。同时你也要认识到积累的力量,但更重要的是你要去想想怎么去收集那些未知的细节,这种能力对快速解决问题还是很重要的。

How to develop quality software with a good velocity

快速地开发一个高质量软件,可以说这是软件工程师追求的目标之一,甚至有点像学篮球的人想扣篮一样,说是追求,甚至也可以说梦想。

首先一个高质量的软件是什么呢?首先是正确地完成预期的功能,如果软件有错误,这个就算不上是高质量的软件了。也就是正确性是第一步。再就是定义高质量的非功能性要求,用户体验良好。这个有点需要区分一下前后端,站在后端开发而言就是接口要及时响应,前端需要有流畅的交互体验或者浏览体验。纯技术而言,也就是软件层面,目前做到这些已经可以了。但是要想做得更好,我们需要配合硬件资源,比如一个相对较高的服务器配置,包括网络带宽,磁盘读写,内存大小,CPU核数。也就是说一个高质量的软件从来不都是哪一个人的事,也不是哪一个部门的事,它需要大家共同的努力,首先做好每个人各自的模块,最后组合在一起,发挥其组合效应。

一个高质量的软件需要有人统筹大局,把握整个全局架构,同时协调各个组成部分,让每个成员有全局观,同时既要做好自己的部分,也要配合其它组成部分。本文讨论的范畴因该会更小一点,我们只讨论后端开发怎么做好自己的那一部分。

在前后端开发分离的架构下,前后端的分工更加明确,在一定的条件下,可以说是提高了开发效率。我作为java后端开发人员,我也在一直想怎么又好又快地开发软件,软件说开发功能。

软件开发中有些功能,可能之前的开发经历中已经开发过类似地,或者相同的功能。但是并不意味着,你就可把以前的代码全部复制粘贴进来,如果有代码,只是说是借鉴,当前的项目背景和开发框架可能会有一些变化。但是总体而言,开发这样的功能会比没有开发过的功能更有信心。因为通过以前的功能开发,你积累了比较多的经验,更具体地说,就是开发这一块功能的相关知识和细节。探索阶段所花的时间就会相应地减少,能更快地开发出一个“可以工作”的软件,或者说功能模块,甚至说是API接口,这在一定层度上缩短了交付时间。

说到这里,不得不提的是,一些企业招聘中,经常会提到3年开发经验,5~10年开发经验,这些工作经验成了一个必要条件,但是现实中的软件开发真得就是比开发年限吗?

我觉得不是。不是说开发年限,开发经验不重要。它们重要。但是如果说一个拥有开发年限比较久就是王道,那么就等同说,软件都是建立在经验之上的。

显然不是的,我们的生活的这个世界,变化之快,同样软件也是要学会适应这个快速变化的世界。因为很大程度上,软件都只是在模拟真实的世界,它在用一些类似人的处理过程去处理现实的问题。

现实就是这样,可能一直会发生变化。所以你的软件也需要发生一些变化,有些人可能更喜欢用“需求”变化来更具体化“软件变化”,这个没有关系,我们就变化这一块达成共识就行。

回到上面的问题。开发的经验只能算是软件开发中一个加分项,类似于试卷中的附加题。那么软件开发中还有什么比较重要呢?或者说怎么去快速地开发一个高质量的软件呢?

我们上面聊过高质量软件——功能性要求正确,非功能性要求用户体验良好。

就功能性正确而言,我们需要测试其功能,让它和期待的一样。有时候的功能性不正确,一般都是测试不到位。举个例子,有个功能,它表现形式有多样,它变化的部分较多,如果测试只关注那些变换的部分,那么测试的力度怎么都不会足够深,通过测试建立的信心也不能那么足,甚至在出现生产事故的时候也没有那么意外。因为它本身就是多变的,测试集也随机抽样,所以它可能出错的概率就是存在的,根据“墨菲定律”——只要有可能会出错的事,就一定会出错。所以测试那些期望比较模糊的功能时,一定要把握其不变性,抓住它不变的部分,怎么用不变来应对万变。这是一个思维模式的转换,表面上是关注点的不同,实质上是抓住了变化的重点。

这个例子其实来自于我以前开发的一个答题卡功能,以前在绘制答题卡的时候,边界经常出现问题,因为之前的关注点就是在怎么处理边界的题目。后来到这个问题的彻底解决是在改变了绘制题目的方式,由一次绘制一个大题,到一次绘制一行题目,不是采用预先计算方式,而是采用实时计算方式来处理剩余空间,这就是抓住了变化的本质,用不变以应对万变。

至于测试方式,或者说测试行为,你可以用Unit Test 某个方法,或者采用Postman来测试RESTFulLl接口.甚至其它的测试框架。关注于测试,和不关注测试。说的更具体一点就是,如果你写代码是由测试的,你会有一个新的视角。有点像你的API接口是消费品,是商品,它会被消费,而你就是商家。通常你没有测试的概念,或者思想。你就只会站在商家的角度去考虑问题,去写代码。但是如果你测试你的代码,你就多了一种身份——消费者。你会消费你的接口,也就是调用的接口,这时如果调用方式很烦,比如路径,比如请求参数,比如处理请求结果。这是你可能就会“抱怨”这个商家提供的API接口的确不够好。所以如果你测试了你的代码就有了让你优先改善你的接口,你的代码能力,这一切都会给你树立一个良好的职业素养形象。

所以测试,这项技能你值得拥有。它不仅让你对你的代码建立信心,还让你树立良好的职业形象。

说完了功能性,再提到非功能性,我想这点还是容易让人理解的。这要求你不再是一位软件开发者,而是软件使用者。如果让你来用这款软件,在功能型满足的条件下,你希望它有哪些让你喜欢的plus功能。如果用户喜欢这会从侧面反应你的软件质量高。

聊完了高质量,我们再回到主题,怎么快速开发高质量软件。

高质量的软件的开发离不开整个团队的努力,其中开发团队离不开测试体系的保障,但同时我们可以关注软件本身的一些度量。比如测试中的代码覆盖率,还有模块耦合程度,代码抽象程度,甚至代码中命名规范等等。一些比较好的开发工具,插件和专门的框架对这一块可以给我们提供一些很好帮助。

但是怎么快速呢?这个字眼格外地突出。软件开发中,时间都去哪了?敲代码的时间会很多吗?敲的代码会很多吗?

软件开发——开发一款高质量的软件,我们需要理解需求,首先要知道我们要做的是什么,也就是实例化需求的过程,这个过程会花去一些时间。然后就是理解过程,开始尝试定义好API接口,这样前后端就可以同步进行了。

编码过程会有一些编程上的问题,然后使用编程的方法把它解决掉。这差不多就是写代码的工作。你的技能熟练程度一般会缩小你写代码的时间,但是你的视野,或者说知识及背后的学习能力决定了你的代码量的多少,我指的是完成软件功能的代码量。语言的特性,新的API,或者相关的工具类都会对你的代码质量有一个比较高的提升,这个不仅仅取决于你追求的高质量软件的程度,还会与当时的项目背景,需求交付时间等等一系列外部因素有关。

我在追求这个《快速地开发高质量软件》过程中,个人努力的方向一般都是关注新的技术,学习新的知识,扩大自己的知识视野。

你的想法又是什么呢?欢迎留言讨论。

Docker deploy Seata with Nacos

使用Docker部署Seata,同时使用Nacos作为配置中心,如果在你了解了Nacos和Seata之后开始捣鼓整一个Demo来学习一下,那么你一定是看了Seata的官网的,而且还很有可能是看了Github上的seata和seata-samples项目的,然后开始在本机上把 springcloud-nacos-seata 这个项目尝试跑起来,很遗憾你没能轻易如愿。我之前也是一样,现在把项目真正跑起来的过程整理一遍。

我们就以Seata官网的 一个微服务示例 架构图

然后参照 seata-samples项目的 springcloud-nacos-seata实例代码,做一个梳理。

目标:实现SpringCloud + Seata + Nacos + Mybatis + Druid + Mysql 整合。

说明:
(1)其中 Seata 和 Nacos 均采用Docker 部署,且是单机模式,准确来说是docker-compose部署。
(2)Seata 和 Nacos 版本均是最新版,Seata-1.2.0, Nacos-1.3.0.
Mysql 是5.7,我的用户名/密码:root/123456.
(3)Seata和Nacos是部署在两个不同的容器中,所以不要想着你的localhost,而要用你的宿主机ip (我的IP是192.168.1.2)

具体步骤:
(1)由于使用Nacos作为配置中心,所以第一步是启动nacos这个比较容易,直接使用下面standalone-derby.yaml文件,这个文件你最好不要直接copy,因为你会发现它挂载了其它文件。它来源于
https://github.com/nacos-group/nacos-docker example文件夹,所以你需要先下载这个仓库。至于下面的prometheus和grafana你可以暂且不管,如果你之前学习Nacos就知道了就更好。

version: "2"
services:
  nacos:
    image: nacos/nacos-server:latest
    container_name: nacos-standalone
    environment:
    - PREFER_HOST_MODE=hostname
    - MODE=standalone
    volumes:
    - ./standalone-logs/:/home/nacos/logs
    - ./init.d/custom.properties:/home/nacos/init.d/custom.properties
    ports:
    - "8848:8848"

  prometheus:
    container_name: prometheus
    image: prom/prometheus:latest
    volumes:
      - ./prometheus/prometheus-standalone.yaml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"
    depends_on:
      - nacos
    restart: on-failure

  grafana:
    container_name: grafana
    image: grafana/grafana:latest
    ports:
      - 3000:3000
    restart: on-failure

Nacos启动之后你就可以验证了:
http://localhost:8848/nacos/ 登录用户名和密码均是nacos

(2)由于Seata采用Nacos作为注册中心,所以在你启动Seata之前,你需要把配置写入到Nacos,当然Seata给你提供了一个shell脚本———nacos-config.sh,它来源于 https://github.com/seata/seata script文件夹,你只需要先配置好config.txt文件,然后一行命令就可以搞定。注意:config.txt和nacos-config.sh在同一目录下。原始的脚本在windows平台下可能需要修改一下,我会上传修改后的。

配置完成之后你可以在Nacos中验证了:找到->配置管理 -> 配置列表

当然上面是最后配置的效果,在此之前,我希望关注一下config.txt文件本身

transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=false
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
transport.serialization=seata
transport.compressor=none
service.vgroupMapping.my_test_tx_group=default
service.default.grouplist=192.168.1.2:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=false
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.logTable=undo_log
client.log.exceptionRate=100
store.mode=db
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://192.168.1.2:3306/seata?useUnicode=true
store.db.user=root
store.db.password=123456
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9090

上面的配置是基于我的环境的,你可能需要做一些调整,我想强调几行分别是

(一)service.vgroupMapping.my_test_tx_group=default
说明my_test_tx_group 需要和代码中的tx-service-group一致

(二)store.mode=db
store.db.datasource=druid
store.db.url=jdbc:mysql://192.168.1.2:3306/seata?useUnicode=true
store.db.user=root
store.db.password=123456
说明上面采用db模式,Seata默认采用file模式,所以我们写db来覆盖,同时我们注意到url中有一个Schema是seata,没错我们一会需要在数据库中建好这个Schema同时我们注意到下面还有三张Table,我们一会一同建好。
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.lockTable=lock_table

(三) 既然说到了数据库,那么我们顺便把实例微服务中的数据库也建好,主要是三个Schema,每一个Schema中都有一张业务表和undo_log表。可以先看一下

上面数据库的sql文件其实分为两个部分,一部分是Seata Server作为db模式所需要的seata库(branch_table,global_table,lock_table)及Seata Client (分别是account,order,storage)各自业务表及一张undo_log表。第一部分sql语句可以在Seata的下载文件中找到

第二部分可以在
https://github.com/seata/seata-samples/tree/master/springcloud-nacos-seata

(3)在数据库准备工作完毕后,我们使用docker-compose.yml启动Seata服务器

version: "3.1"

services:

  seata-server:
    image: seataio/seata-server:latest
    hostname: seata-server
    ports:
      - 8091:8091
    volumes:
      - ./config:/root/seata-config
    environment:
      - SEATA_PORT=8091
      - SEATA_IP=192.168.1.2
      - SEATA_CONFIG_NAME=file:/root/seata-config/registry

上面的配置中我们重点关注最后两行,一个是指定了IP,一个是指定了配置文件的位置。然后我们看一下配置文件——registry.conf。

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"

  nacos {
    serverAddr = "192.168.1.2"
    namespace = ""
    cluster = "default"
  }
  eureka {
    serviceUrl = "http://localhost:8761/eureka"
    application = "default"
    weight = "1"
  }
  redis {
    serverAddr = "localhost:6379"
    db = 0
    password = ""
    cluster = "default"
    timeout = 0
  }
  zk {
    cluster = "default"
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  consul {
    cluster = "default"
    serverAddr = "127.0.0.1:8500"
  }
  etcd3 {
    cluster = "default"
    serverAddr = "http://localhost:2379"
  }
  sofa {
    serverAddr = "127.0.0.1:9603"
    application = "default"
    region = "DEFAULT_ZONE"
    datacenter = "DefaultDataCenter"
    cluster = "default"
    group = "SEATA_GROUP"
    addressWaitTime = "3000"
  }
  file {
    name = "file.conf"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "nacos"

  nacos {
    serverAddr = "192.168.1.2"
    namespace = ""
    group = "SEATA_GROUP"
  }
  consul {
    serverAddr = "127.0.0.1:8500"
  }
  apollo {
    appId = "seata-server"
    apolloMeta = "http://192.168.1.204:8801"
    namespace = "application"
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  file {
    name = "file.conf"
  }
}

上面配置中
registry.type=nacos,
config.type=nacos,
nacos.serverAddr=”192.168.1.2″
nacos.group= “SEATA_GROUP” (这个和Nacos中相匹配)

(4)我们首先来看一下项目结构。

重点说明: 
(一)讲道理我们也是需要把account服务加上去的,但是用来验证分布式事务Seata的使用情况下,我们用两个就可以了。
(二)上面最主要的registry.conf文件和bootstrap.yml中spring.cloud.alibaba.seata.tx-service-group: ‘my_test_tx_group’(要和前面config.txt中 service.vgroupMapping.my_test_tx_group=default 相呼应)。
(三)再看registry.conf中最重要的部分:

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"
  nacos {
    application = "seata-server"
    serverAddr = "192.168.1.2"
    namespace = ""
    cluster = "default"
    username = "nacos"
    password = "nacos"
  }
  # 为了节省空间,其余type 省略
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "nacos"

  nacos {
    application = "seata-server"
    serverAddr = "192.168.1.2"
    namespace = ""
    group = "SEATA_GROUP"
    username = "nacos"
    password = "nacos"
  }
  # 为了节省空间,其余type 省略
}

上面的application = “seata-server”和Nacos配置列表中保持一致

5.启动项目开始测试

(1)分布式事务成功,模拟正常下单、扣库存
localhost:9091/order/placeOrder/commit

(2)分布式事务失败,模拟下单成功、扣库存失败,最终同时回滚
localhost:9091/order/placeOrder/rollback

后记:这次整合Seata和Nacos的确花了一些时间,主要的原因是因为问题>人,自己能量级不够,所以这个问题才是问题,根本原因是因为没有理清思路,在众多配置文件和纷繁的零碎资料中走进了“问题森林”,会重点去查看一棵树,比如调试本地启动脚本jvm参数导致问题,配置文件中某项具体选项同时变化多个等等细节,而忘了初心是找到一个方向迅速走出这片森林。理思路,找方向,这件事一定要在头脑清晰的时候去做,头脑不清晰的时候只能算是“糊涂探索”。

最后希望这篇文章对你有帮助,我会把相关的脚本上传到我的github,欢迎clone.