谈 DevOps 平台落地:你们流水线从编译到部署需要多少分钟啊?

一位同学的疑问 有一位同学问我: 你们一个流水线从编译到部署成功需要多少分钟啊。我们快的2分钟,Java 普遍10分钟,开发同学总是觉得慢,我不知道业界是什么水平。 我的回答是: 没有行业标准的,有些项目10万行代码,有些100万行代码,没法比。 后来,我又问他:你们的流水线都包括了哪些阶段? “编译,带数据库的测序,sonar,docker build,ansible deploy,mvn release”,他回答。 后来的沟通中,我得知,他们流水线时间长发生在两个阶段:带数据库的测试和下载依赖。 下载依赖慢是因为他们每次构建都重新下载依赖,这样做又是因为总是遇到缓存问题,所以,干脆就每次重新下载了。 带数据库的测试通常会慢,因为要启动应用,然后操作数据库,这个数据不确定他是类似 MySQL 这样的真实数据库,还是使用 H2 这样的内存数据库。 出乎我意料的是,他们的 SonarQube 扫描倒是很快。 所以,他们的优化点就是那两个慢的阶段。具体解决办法还要看具体情况,比如带数据库的测试是不是可以通过并行解决、构建时依赖缓存遇到的问题是不是可以通修改构建工具配置来解决。 写到这里,我想表达的是,优化流水线的速度的思路,差不多就是这样:先找到最慢的阶段,然后根据具体情况来优化。 从 DevOps 平台设计角度解决 那么,作为 DevOps 平台,我们能通过什么办法帮助用户提高流水线的速度呢? 笔者认为,只要将流水线中的每个阶段中的每个步骤的耗时都记录下来,然后显示给用户,用户自然会注意到每次流水线的执行速度差异。当然,管理层也可以对这部分内容进行考核。 同时,要将耗时进行分类,一类是用户步骤的耗时,比如执行mvn package,执行单元测试等,一类是 DevOps 平台本身的耗时,比如初始化构建环境耗时,上传制品耗时等。 为什么要进行这样的分类?是因为 DevOps 平台使用过程中,用户遇到问题,往往是区分不了,是平台的问题,还是自己的问题。这时,我们将平台的信息显示给用户,用户就可以自行判断,自行处理了。这会大大节约平台维护者的时间。而且,平台维护者也可以根据平台运行耗时统计来对平台进行有依有据的优化。这是一箭双雕。 我把这个功能叫做:流水线耗时统计。 那么这个功能,到底应该如何实现?不同的平台有不同的实现,比如基于 Jenkins 的话,在每个步骤后加上一个回调请求就可以了;基于 GitLab 的话,就不了解了。 图来自:https://wiki.jenkins.io/display/JENKINS/Pipeline+Stage+View+Plugin 后记 流水线的速度是一个很重要的指标,它直接显示了一个软件开发团队在工程方面的效率(正确性问题是另一个问题)。而流水线耗时统计功能可以有效地帮助用户提高自己的流水线速度。

2019-11-21 · 1 min · 46 words · 翟志军 Jack Zhai

谈 DevOps 平台落地:前端项目构建又失败了

同事发了一个前端构建失败的链接过来,接着就是那句:任务执行失败了,麻烦帮忙看看。 DevOps 平台的“老手”了,所以,在找我们解决问题时,都知道附上平台任务的链接。 我们打开链接,第一件事情就是看日志。是的,DevOps 平台的使用者很多都认为:在本地执行构建成功,那么在平台上构建失败就是平台的问题。所以部分人连构建日志都不看,直接把链接发给我们这些平台维护者看。 不出意外,这次又是依赖管理问题。只不过,这次是发生在前端项目上。错误截图下如下: 日志里(画红线部分)已经说得很清楚了。虽然我不清楚“Tristan”是什么,但是可以猜到是他的业务代码报这样的错。但是他本地执行没报错,那通常就是依赖的版本的问题了。 他的前端的依赖定义(package.json)类似以下这样: { ..... "dependencies": { "cookie-parser": "^1.4.3", "debug": "~2.6.9", "express": "^4.16.0", "http-errors": "^1.6.2", "morgan": "~1.9.0", "pug": "2.0.0-beta11" }, ... } 我们看到依赖的版本号的前缀有 ~,有也 ^。这是什么意思呢? ~: 前缀表示,安装大于指定的这个版本,并且匹配到 x.y.z 中 z 最新的版本。 ^: 前缀在 ^0.y.z 时的表现和 ~0.y.z 是一样的,然而 ^1.y.z 的时候,就会 匹配到 y 和 z 都是最新的版本。 也就是说,每次执行 npm install ,该项目所依赖的内容,都是有可能变的。 这对我来说是不可思议的。为什么? 因为依赖的版本代表着一个软件的基础。依赖的版本在你不知道的情况下发生变更,就好比建房子,建第一层时,地基是100个平方,建第二层时,地基突然就变成了90个平方。而前端项目中大量这种情况。 你可能会说开源前端node项目都会遵循语义化的版本号,小版本升级不会出问题的。我想说,那只是约定,还是要看那个人遵守不遵守。如果你真相信每个人都遵守,本质上是把软件开发风险的控制权交给了开源软件作者的个人习惯。开发出来的软件注定是不稳定的。 但是,为什么前端项目的依赖的版本号前普遍会加上 ~ 和 ^ 。在我亲自执行 npm install express 命令时,我知道了原因。因为在执行命令后, package.json 文件中就出现了:"express": "^4.16.0"。也是 npm 在安装依赖时,默认就给版本号加上 ^ 前缀。而很多人可能改都不会去改。这就导致了文章开头所说的那位同事的问题。 后记 真心希望大家固定下 package.json 中的依赖的版本号。这样的前端项目构建起来才有稳定的基础。 ...

2019-11-20 · 1 min · 80 words · 翟志军 Jack Zhai

谈DevOps平台落地:前端构建怎么这么变态

题记:DevOps 平台通常搭建于内网环境,不能直接外网,所以,如果你也要在内网环境构建前端,就一定会遇到本文所说的问题。 我们发现在 DevOps 平台构建前端项目时,会报这以下这样的错误: node scripts/install.js Downloading binary from https://github.com/sass/node-sass/releases/download/v4.9.0/linux-x64-57_binding.node Cannot download “https://github.com/sass/node-sass/releases/download/v4.9.0/linux-x64-57_binding.node”: tunneling socket could not be established, statusCode=500 Hint: If github.com is not accessible in your location try setting a proxy via HTTP_PROXY, e.g. export HTTP_PROXY=http://example.com:1234 or configure npm proxy via npm config set proxy http://example.com:8080 [email protected] postinstall 以上的错误日志的意思是node在安装 node-sass 时,要去 github.com/sass/node-sass 下载一个名为 linux-x64-57_binding.node 的二进制包。然后它无法下载(其实是因为DevOps平台搭建在企业的内网,是无法直接连接外网的),就建议你设置一下系统的HTTP代理,让它能连接到 github.com。 除此之外,错误日志中,还发现了,node-sass 依赖本身的构建,还需要 Python2 环境: gyp verb check python checking for Python executable "python2" in the PATH gyp verb `which` failed Error: not found: python2 对于一个 Java 后端开发人员,看到这样的错误就懵了。心里在想: ...

2019-11-12 · 1 min · 165 words · 翟志军 Jack Zhai

谈 DevOps 平台实施:我在本地跑明明成功的,为什么在你平台跑就报错?

我在本地跑明明成功的,为什么在你平台跑就报错? 用户在 Jenkins 上跑构建时,失败了,把日志截图给我看,如下图: 在过去几个月,每个星期都会有一两个 Jenkins 用户就会给我发送类似的错误日志。 这样的日志,我通常回:请检查你们的依赖,是不是有依赖没有上传到咱们的 Nexus 仓库。验证方法是先在本地删除你的 .m2 目录,然后再执行一次构建。 当用户业务开发比较急的时候,他们还会说本文标题中的那句话。有些抱怨的意思。我都已经习惯了。 出现这样的情况,我总结大概会有以下原因: 用户对于 Maven 这类构建工具不熟悉。 用户对于依赖管理不重视,或者没有依赖管理的意识。 用户根本不看日志。 面对这三个原因,我就在思考:我们 DevOps 平台能做些什么呢? 我觉得 DevOps 平台是不是可以直截了当地告诉用户: xxx 依赖在 Nexus 仓库(maven.abc.com)中没有找到,请您先 deploy 该依赖到 Nexus 仓库后,再执行此任务。 如果能检测到缺少的依赖放在哪个代码仓库就更好了。因为这样,就可以提示用户直接到该代码仓库的 deploy 了。 这样的技术,我称为依赖AI管理技术(笑)。当然,这样的技术,应该可以应用于所有的语言。 同时,我们将这些数据(依赖管理失误)统计起来,就可以看出一个团队在依赖管理方面的能力表现了,进而可以有效的对团队进行培训,以提高相应的能力。 回到本文主题,当用户自行检查依赖后,大多数时候,用户就不会来找我了,因为问题已经解决了。可是有一次,用户还是说不行,他已经把 .m2 删除,并把依赖包上传到 Nexus 仓库了。 我检查了他的 pom.xml 文件,发现版本号的定义也是正确的。可是,放在 Jenkins 上执行时,使用的还是旧版本的类的定义。 这就奇怪了。这种情况还是头一回遇到。来来回回检查了好几次,查了好久才知道,是因为用户 deploy 依赖到 Nexus 时,deploy 的是相同的版本号,就是覆盖了原来的版本的包,但是版本没有升级。而 Maven 检测到本地就该版本的依赖,就不会重新下载了。最后,就是大家看到的,本地可以,但是 Jenkins 上就是不行。 最后的解决方式是: 用户 deploy 一个新的版本到 Nexus 仓库,并在 pom.xml 中使用新的版本。 我们将 Nexus 设置为不允许重复 deploy。 小结 经过这次事件,我们可以看出,依赖管理对于工程质量的重要性。因为,依赖管理不当,很有可能在连开发人员都不知情的情况下引入Bug。 ...

2019-11-11 · 1 min · 75 words · 翟志军 Jack Zhai

Ansible 开发环境的搭建

通常我不喜欢写开发环境搭建类文章的,但是见到不少同学在 Ansible 的开发环境花了很多时间。所以,就想写这么一篇文章。希望能帮助到有需要的同学。 在介绍开发环境搭建之前,需要介绍 Ansible 脚本的开发流程。 不像普通的业务系统的开发,只需要打开 IDE 就可以写代码,然后调试了。当然 Ansible 脚本也可以进行单元测试,但是 Ansible 脚本还是需要真实运行并部署,才能验证脚本的正确性。 所以,Ansible 脚本的开发过程通常是这样的: 启动一台虚拟机。 在开发机上编辑 Ansible 脚本。 在开发机上执行 ansible-playbook -i hosts playbook.yml 命令。 通过 Ansible 脚本的开发过程了解到,开发环境的搭建可以分成3部分: 测试机器的准备。 文本编辑器的准备。 Ansible 的安装。 1. 测试机器的准备 见到不少同学使用 Vmware 或 VirtualBox 手工创建虚拟机。这种方式是可以达到搭建测试机器的目的。但是笔者认为这样不够好。因为验证 Ansible 脚本,我们需要频繁创建新的虚拟机。手工创建虚拟机的效率太低。而且不利于版本控制。 所以,测试机器的准备,笔者使用的是 Vagrant。通过它,可以自动化创建和配置虚拟机。当然,整个过程还是版本控制的。 同时需要注意,Vagrant 本身并不是一个虚拟机的实现,它是基于 VirtualBox 和 Vmware 的。换句说就是我们可以通过 Vagrant 去控制 Vmware 和 VirtualBox。所以,在安装 Vagrant 的同时,也需要安装 VirtualBox 或 Vmware。本文使用 VirtualBox。 Vagrant 和 VirtualBox 的具体安装在本文末有官方教程。 2. Vagrant 介绍 Vagrant 本身只是一个软件,提供了 vagrant 命令。我们通过一个名为 Vagrantfile 的文件声明启动什么配置的虚拟机。 ...

2019-11-07 · 3 min · 452 words · 翟志军 Jack Zhai

谈 DevOps 平台设计:版本号相关功能的设计

在设计 DevOps 平台时,笔者认为版本号的管理是一个绕不开的课题。可是,行业里似乎很少人提这个事,笔者觉得要谈一谈,所以就有了这篇文章。 一万个人的眼里有一万个“版本号” 笔者这三年在同一家公司里,换岗换了四个团队。团队的成员组成各异,有的团队都是在大型跨国企业跳槽过来的,有的团队大部人都是刚毕业的。 每到一个团队,团队运行一段时间,都会做一件事情:讨论该怎么定义这个版本号。版本号的制定,有些只有开发人员参与,有时会有产品经理参与,有时还有 PMO 参与。 经过这些讨论,我发现:一万人的眼里有一万个“版本号”。讨论的最后,基本上就是谁的嗓子大,听谁的。 所以,在讨论“版本号”之前,一定要搞清楚讨论各方对于“版本号”的理解,再深入讨论,否则,大家谈的都会是牛头不对马嘴的东西。浪费时间。 为什么对于“版本号”,各方的理解,差异会如此大。笔者认为,主要是因为他们关心的面不同。 APP产品经理关心的是该APP在用户界面上显示的版本号,比如当前爱彼迎的APP的版本号是:1.9.44.china。 对于后端开发工程师,关心的是网关服务的版本是1.2.1、客服服务的版本是4.11.1。 对于前端开发工程师,关心的是通用组件的版本是2.1.1、首页组件的版本是3.1.1。 而对于 PMO,他们可能只关心在 Staging 环境的最后一个版本是否为一个稳定的版本(这写在他们的管理规范里),保证不影响测试人员的工作,根本不关心具体的“版本号”是多少。 重新认识版本号 各方的关注点不同,不是问题,但是我们作为一个平台的设计必须对“版本号”有更深入的理解。 笔者分析各方的关注点,他们所说的“版本号”分布在以下两个层面: 技术层面:程序员关心线上跑的是哪份代码(对应的是Git\SVN中的Commit ID)、运维关心线上跑是哪个版本(对应的就是具体哪个包)。 业务层面:方便终端用户识别的版本号,产品经理也属于这一层面。 认识到这点,我们设计DevOps平台,就会对两种版本号进行区别对待,进而设计出对团队非常有用的功能,最终帮助团队更好的实现交付。 为方便沟通,技术层面的版本号,如 Commit ID 我们称为技术版本号,业务层面的版本号,称为业务版本号。 版本号相关功能设计 但是版本号有什么用?仔细想想,除了产品经理发布时要定个版,后端服务的版本用于保证服务之间的相互引用或调用不出问题,就没有什么别的用处了。 也许是因为大家都不了解版本号的用处,也或者是认为它根本就不值得讨论,所以,笔者在国内的几个大的平台都没有看到版本号的相关功能的设计。唯一使用到版本号的地方就是在制品库,部署时需要指定制品的版本号。而业务版本号与技术号之间的关系被隐藏得很深,用户很难查到。 笔者不想一开始就谈它的好处。我直接上功能,下图是笔者臆想出来的。 笔者认为,DevOps 平台应该有的功能之一:能输出这么一幅图,暂定名为版本关系图。图中的方块下,同时标有业务版本号和技术版本号。而图中的系统之间的连接线是应用系统的调用链,读者可忽略。 版本关系图应该能提供以下信息: 系统应用之间的版本依赖。 系统内部所依赖的组件的版本。 能根据某系统的版本查到目前直接依赖于或间接依赖于它的其他系统。 各系统的版本变迁信息。 这些信息能给用户带来的价值如下: 团队内信息更透明,沟通效率更高,可以有效避免某个员工成为单点。你不必等其他成员,自己也可以得到整个系统的版本信息。 可以提高团队成员的排错能力,因为当A发布新版本后,APP 首页打开变慢,有了版本关系,我们可以首根据整个平台的“版本事件”来排查问题。同时,团队也很快可以找到相应的代码变更,然后进行 review 及修复。 上图中,当 A 服务是一个集群时,我们还可以将部署的目标机器与版本号关联起来了。这样,团队就可以轻松的知道,哪台机器部署了哪个版本。 上图只是整个业务系统的某个时间点的“快照”。事实上,我们还可以在版本号上做更大的文章。比如让技术版本号与代码质量、构建速度等过程指标关联起来,这样我们可以在不同的版本之间进行对比。再比如计算两个业务版本号之间,代码质量的差异,长期积累下来这些数据后,我们就有能力计算出代码质量与业务指标之间的关系。 总的来说,版本号就是整个研发流程中的各项指标数据的枢纽。 后记 版本号和其它数据的关系的价值,笔者认为被大大低估了。希望本文能给 DevOps 平台设计者带来不一样的想法。

2019-11-06 · 1 min · 55 words · 翟志军 Jack Zhai

谈DevOps平台实施:实现从内网拉取外网依赖的一种方案

背景 在大型企业内部,网络通常会被划分成多个不能直接访问的区域。比如本例中,网络被分成了内网和DMZ两个区域。出于安全的考虑,内网的机器不能直接访问外网。内网访问 DMZ 的机器、DMZ的机器要访问外网都需要单独提流程。 但是,我们的应用能部署到 DMZ 区域中吗?答案是技术上不是问题,但是管理上不允许这样做。 所以,在这样的大型企业内部,应用都会部署到内网中(本例中的A、B、C、D)。 可是,总会有一些应用需要发 HTTP 请求到外网。比如实施DevOps平时,我们的应用需要从外网拉取依赖。 这时,怎么办呢?本文就是为解决此问题而写。 解决方案 最后的解决方案如下: Privoxy 是一个HTTP 协议过滤代理。 Squid 是HTTP代理服务器软件。Squid用途广泛,可以作为缓存服务器,可以过滤流量帮助网络安全,也可以作为代理服务器链中的一环,向上级代理转发数据或直接连接互联网。 说实话,光看介绍,笔者一开始也一头雾水。不过,看完本文就应该知道它们的作用了。 以下是方案具体实施步骤: 在应用机器上设置全局环境变量: export http_proxy=http://192.168.1.100:3126 export https_proxy=http://192.168.1.100:3126 这一步的作用是将本机的 http 流量都代理到 192.168.1.100 的 3126 端口 在 192.168.1.100 上安装 Privoxy。它的作用是根据配置,决定流量走哪个网络。本例中,它的作用是我们指定的http请求,走到 dmz。而其它的则和原来一样。它的配置如下: cat /etc/privoxy/config listen-address 192.168.1.100:3126 forward .abc.com/ 192.168.42.12:3127 listen-address 指 Privoxy 监听的IP和端口 forward 指接收到符合域名规则(.abc.com)的请求,将转发给 192.168.42.12 的 3127 端口。 到此,得到的效果就是当在应用机器访问 abc.com,admin.abc.com 等时,这些流量都会被 Privoxy 转发到 192.168.42.12 的 3127 端口。其它 HTTP 请求则不会。 而 192.168.42.12 则是安装了 Squid 实现 HTTP 代理的机器的 IP。 ...

2019-11-04 · 1 min · 107 words · 翟志军 Jack Zhai

这样理解Ansible更容易

滚滚长江东逝水,浪花淘尽英雄。是非成败转头空。青山依旧在,几度夕阳红。—— 《临江仙》 电脑店 从前,有一家电脑店。原来你即是老板,又是店员时,拿到清单,你就必须亲自动手采购,然后一个个零件组装。每天都做着即重复又辛苦的活。 虽说你的组装技术已经很娴熟了,但是偶尔还发生装错的情况(大概是那天和老板娘吵架了),把一个客人要求的 CPU i5 装成了 CPU i7。结果是你亏本或者赚得少了。 后来,你采购了一个自动组装电脑的机器人。你只要告诉它电脑的配置,并把零件放到指定的箱子中。接着启动这台机器,它就自动帮你组装好电脑了。它每天都干重复的活也不会叫辛苦。 最重要的是准确,它不会因为心情不好,而装错。因为它根本不会闹情绪。 这样,老板就可以从重复的工作解放出来。然后将多出来的时间花在与人的沟通上,为有不同需求的人设计更合适的电脑配置清单。毕竟游戏发烧友和办公小白领的需求是不一样的。 在运维领域,不少运维人都干着即是老板又是店员的工作。如果在运维领域也能有这样的“机器人”该多好。事实上,Ansible、Puppet、Check 就是这样的机器人。 为什么要从零设计一个运维机器人 本文并不想生硬地罗列 Ansible 的各个知识点。因为那样,大家不如直接看 Ansible 官方文档就好。 笔者采用从零设计一个运维机器人的方式来告诉你,为什么 Ansible 会是现在这个样子。当然,现实中的 Ansible 不会像本文所写的那样一步步设计。 为什么要这样呢?因为笔者觉得只有知道一个工具背后的设计原理,真正用这个工具才会得心应手。 运维机器人的最终模样 首先,需要确定一下实现这个运维机器人的目的是什么。我们并不是希望所有的运维工作都交给运维机器人,而是希望运维工作中重复的那部分尽可能的交给机器人,把创造性的工作全部交给人。如下图所示。 以终为始是一种非常有效的实现目标的思考模型。根据此思考模型,我们首先必须探讨运维机器人的最终模样。然后,再讨论可能的解决方案。 那么,什么样的运维机器人能帮助我们实现上述的自动化运维目标呢?想像一下。是不是只要我们对着运维机器人说一句:“我要部署一个 Nginx 到 192.168.12.11”。它就可以帮我们完成了? 但是它怎么知道如何连接到 192.168.12.11 呢?是使用用户名密码的方式,还是使用私钥?它又怎么知道 Nginx 需要什么样的配置呢?一问下来,其实,语音运维只适用于启动一些预定义的动作。就像汽车的一键启动。你不可能使用语音来对 Nginx 进行大量的配置。 而纯文本才是进行大量配置的最好媒介。 所以,运维机器人的最终模样是:我们将部署的主机 IP、登录方式、Nginx 的配置放在一个文本文件中,然后运维机器人读取这个文本文件,然后根据配置进行部署。如果部署的是业务系统,我们还需要准备该业务系统的二进制包。如下图所示。 那么,我们在文本文件中使用何种语言描述我们的配置需求呢?可以分成两种。一种是利于人类学习的自然语言(如英语)。另一种是利于机器读取的结构化数据(如YAML、JSON)。 按当前的技术实现的可能性,不论是运维机器人,还是交给其它程序,都需要将自然语言转到结构化的数据。就像程序员,需要将业务知识翻译成编程语言;像编译器将编程语言翻译成机器真正能识别的二进制代码。 运维机器人的真正核心不是将自然语言转成结构化的数据。所以,文本文件中,我们直接写结构化数据。同时,我们决定使用 YAML 格式作为结构化数据的载体。因为它是非常流行的配置文件格式。降低人们写结构化数据的难度。 当然,好的设计不应该与具体配置文件格式耦合。 实现运维机器人要解决哪些问题 以上只是确定了运维机器人的最终模样及使用方式,解决的是用户的问题。但是因为我们是运维机器人的设计者,我们必须考虑如何实现它。 回想一下,平时我们的运维人员是如何实现自动化的?是不是写好了 bash 脚本后,然后将脚本上传到目标机器,最后在目标机器上执行该脚本。 这个 bash 脚本其实也可以算是一种结构化的数据格式,而且是一种不需要再做编译,目标机器能直接运行的格式。 在平时的自动化方式基础上进行抽象。我们觉得要实现运维机器人要解决的关键问题有: 需要将 YAML 转成目标机器可执行的程序(或脚本)。 需要将可执行的程序上传到目标机器,并执行。 为什么一定要将 YAML 转成目标机器可执行的程序呢?直接写 bash 不就可以了? ...

2019-09-19 · 7 min · 1362 words · 翟志军 Jack Zhai

你们的 save 方法是写在实体上,还是写 Dao 上?

注:Dao 在不同语言中的叫法可能不一样。Dao 可以理解为对数据进行持久化的具体实现。 关于实体的保存,笔者知道行业内有两种方式: dogDao.save(dog); dog.save(); 相信不少同学,现实中,通常使用第一种,很少见到第二种写法。 为了让大家站在同一个讨论上下文,笔者决定贴出更详细的代码。 注:以下代码会省略很多本文不相关的代码,比如数据校验。读者朋友不必太纠结。 第一种写法:save 方法写在 Dao 上 Dog dog = new Dog() dog.setName('didi'); dog.setColor('white'); dogDao.save(dog); 第二种写法:save 方法写在实体上 // DogService.java Dog dog = new Dog() dog.setName('didi'); dog.setColor('white'); dog.save(); 这两种写法,有什么区别吗?事实上,从字面上看,没有什么区别。因为 Dog 类的 save() 方法是这样实现: // Dog.java public void save(){ dogDao.save(this); } 但是从抽象的角度来看,就不一样了。 假如你的项目中存在这样的抽象: 如果采用第一种写法,意味着,每多出多一种 Animal,我们就必须多写一套 Service。Service 中会很多这样的方法: // DogService.java void save(Dog dog){ dogDao.save(dog); } // FishService.java void save(Fish fish){ fishDao.save(fish); } // BirdService.java void save(Bird bird){ birdDao.save(bird); } 相信大家对于以上代码并不陌生。 ...

2019-07-31 · 2 min · 228 words · 翟志军 Jack Zhai

那只住在我们楼上的大黄狗

当电梯到的时候,一只大土狗也跟着我进了电梯。我按了7楼。大狗依然张开嘴吐着舌头,站在原地。电梯直达7楼,我不知道大土狗要去几楼,所以没帮它按。走出电梯后,电梯理所当然的关门,然后向下了。它还在电梯里。 一天,我们一家人挤进电梯下楼。电梯有一股恶臭。儿子叫了一声:大黄。这时,我才注意到拥挤的电梯里,大土狗在角落里。看样子,它也是要下楼。 后来,从我老婆那里了解到。她经常看到这只大土狗,所以和儿子给它起了一个名字:大黄。因为它的毛是黄色的。 而狗的主人就住在11楼。只不过,现在主人不要它了。它现在每天就睡在前主人的11楼的门外。饭点的时候,就会看到它端端正正的坐在餐桌不远处,目不转睛地看着吃饭的人。偶尔会有好心丢给吃的给它。 当听到这些时,就酸了鼻子,两眼湿润。 再后来,当我和大黄坐电梯时,都会帮它按下11楼。 某一天,在楼下的操场边上,看到大黄兴奋地围在一年轻人的身边疯跑。看得出来,年轻人是它的前主人。只是当年的主人已经不是它的主人。年轻人直视前方冷漠地自走自的,仿佛大黄不存在一样。 而我身为旁观者,本来想说点什么,最后,也只能路过。

2019-07-26 · 1 min · 8 words · 翟志军 Jack Zhai