程序员小岑成长记

程序员小岑成长记 查看完整档案

上海编辑上海海事大学  |  计算机技术 编辑  |  填写所在公司/组织 kailuncen.github.io 编辑
编辑

公众号:程序员小岑成长记,90后互联网工程师,5年互联网服务端开发经验。

个人动态

程序员小岑成长记 发布了文章 · 2020-05-21

小岑的架构学习笔记-架构设计的历史背景

最近在学习极客时间《从0到1学架构》,记一些笔记。

在上一篇《架构是什么?》记录了架构和一些常用名字的一些基础概念。

这一章主要是学习架构设计的历史背景。

软件开发进化的历史

软件开发语言的进化历史

机器语言

最早的软化使用的是“机器语言”,使用单纯的0和1来写代码,使用0和1的组合表示不同的指令,让计算机去执行程序,类似于这种。

101100000000000000000011
000001010000000000110000
001011010000000000000101

而且写这种程序的时候,不小心哪个地方写错了,估计眼睛都得看花了。

最早的时候都是将0,1的表示打在纸带上,让计算机去执行,一个不小心打错了,整个估计得重来,程序员的心理阴影面积要多大得多大。

汇编语言

为了解决机器语言编写、阅读、修改复杂的问题,汇编语言就出现了,有一些助记符代替机器指令的操作码,用地址符号或者标号代替指令或操作数的地址。

例如,为了完成“将寄存器 BX 的内容送到 AX 中”的简单操作,汇编语言和机器语言分别如下。

机器语言:1000100111011000
汇编语言:mov ax,bx

相对而言,汇编语言比机器语言会更好懂一些,虽然仍需要了解计算机底层的知识,比如CPU指令、寄存器等,但相比机器语言,已经抽象了一个层次。

但由于汇编语言因为不同CPU下的指令集不同,代码不能够复用,而且仍然需要关注计算机底层知识,因此,还需要进一步抽象。

高级语言

高级语言想必大家都很熟悉了,比如LISP,Python,Java,C,C++等,让程序员可以不需要关注底层的细节,专注于自身的问题和实现即可,又相比汇编语言抽象了一个层次。

并且通过编译程序,可以实现同样的一份代码,可以在不同的机器上编译运行,不需要根据不同的机器指令,重写程序。

小结

总体来看,软件开发语言进化的历史,是让开发者更加少的关注和自身要做的事情不相关的细节。

软件开发方式的进化历史

作者在原文中提到的GOTO等,离我太过于遥远,我最早接触的就是结构化程序设计。

在结构化程序设计中,引入了模块化的指导思想,将变化点进行隔离,将软件的复杂度控制在一定的范围内。

但随着业务需求越来越复杂,软件的扩展变得更加困难,因此面向对象的编程思想又开始流行起来,现在面向领域驱动变成,慢慢从纸上谈兵,开始在国内的软件开发中大行其道。

软件架构的历史背景

软件架构的概念,根据《软件架构介绍》(An Introduction to Software Architecture)的描述,指的是:

随着软件系统规模的增加,计算相关的算法和数据结构不再构成主要的设计问题;当系统由许多部分组成时,整个系统的组织,也就是所说的“软件架构”,导致了一系列新的设计问题。

这段话解释了为什么软件架构往往现在大公司逐步流行起来,因为只有大公司才有较大的软件规模,规模较大的软件系统才有可能遇到各种问题:

  1. 因为规模大了,内部耦合随着时间长,可能会比较严重,开发效率受到制衡。
  2. 因为耦合重,改某个功能可能影响很多的模块,扩展性不足
  3. 逻辑复杂,排查和修复难度增加。

结构化编程 -> 面向对象编程 -> 软件架构,我理解都是对一定规模软件的拆分,结构化重视在模块层面,面向对象编程更注重在对象划分,领域划分层面,软件架构更多在系统层面如何去规划,拆分的粒度越来越粗,层次也越来越高。

总结

结构化编程,面向对象编程,架构设计 都是软件领域开发的一种方法,不同规模的需求采用不同的方法。

查看原文

赞 0 收藏 0 评论 0

程序员小岑成长记 赞了文章 · 2020-05-21

这才是面试官想听的:详解「递归题」正确的打开方式

前言

递归,是一个非常重要的概念,也是面试中非常喜欢考的。因为它不但能考察一个程序员的算法功底,还能很好的考察对时间空间复杂度的理解和分析。

本文只讲一题,也是几乎所有算法书讲递归的第一题,但力争讲出花来,在这里分享四点不一样的角度,让你有不同的收获。

  • 时空复杂度的详细分析
  • 识别并简化递归过程中的重复运算
  • 披上羊皮的狼
  • 适当炫技助我拿到第一份工作

算法思路

大家都知道,一个方法自己调用自己就是递归,没错,但这只是理解递归的最表层的理解。

那么递归的实质是什么?

答:<span style="color:blue">递归的实质是能够把一个大问题分解成比它小点的问题,然后我们拿到了小问题的解,就可以用小问题的解去构造大问题的解。</span>

那小问题的解是如何得到的?

答:用再小一号的问题的解构造出来的,小到不能再小的时候就是到了零号问题的时候,也就是 base case 了。

那么总结一下递归的三个步骤:

Base case:就是递归的零号问题,也是递归的终点,走到最小的那个问题,能够直接给出结果,不必再往下走了,否则,就会成死循环;

拆解:每一层的问题都要比上一层的小,不断缩小问题的 size,才能从大到小到 base case;

组合:得到了小问题的解,还要知道如何才能构造出大问题的解。

所以每道递归题,我们按照这三个步骤来分析,把这三个问题搞清楚,代码就很容易写了。

斐波那契数列

这题虽是老生常谈了,但相信我这里分享的一定会让你有其他收获。

题目描述

斐波那契数列是一位意大利的数学家,他闲着没事去研究兔子繁殖的过程,研究着就发现,可以写成这么一个序列:1,1,2,3,5,8,13,21… 也就是每个数等于它前两个数之和。那么给你第 n 个数,问 F(n) 是多少。

解析

用数学公式表示很简单:

$$f(n) = f(n-1) + f(n-2)$$

代码也很简单,用我们刚总结的三步:

  • base case: f(0) = 0, f(1) = 1.
  • 分解:f(n-1), f(n-2)
  • 组合:f(n) = f(n-1) + f(n-2)

那么写出来就是:

class Solution {
    public int fib(int N) {
        if (N == 0) {
            return 0;
        } else if (N == 1) {
            return 1;
        }
        return fib(N-1) + fib(N-2);
    }
}

但是这种解法 Leetcode 给出的速度经验只比 15% 的答案快,因为,它的时间复杂度实在是太高了!

过程分析

那这就是我想分享的第一点,如何去分析递归的过程。

首先我们把这颗 Recursion Tree 画出来,比如我们把 F(5) 的递归树画出来:

那实际的执行路线是怎样的?

首先是沿着最左边这条线一路到底:F(5) → F(4) → F(3) → F(2) → F(1),好了终于有个 base case 可以返回 F(1) = 1 了,然后返回到 F(2) 这一层,再往下走,就是 F(0),又触底反弹,回到 F(2),得到 F(2) = 1+0 =1 的结果,把这个结果返回给 F(3),然后再到 F(1),拿到结果后再返回 F(3) 得到 F(3) = 左 + 右 = 2,再把这个结果返上去...

这种方式本质上是由我们计算机的冯诺伊曼体系造就的,目前一个 CPU 一个核在某一时间只能执行一条指令,所以不能 F(3) 和 F(4) 一起进行了,一定是先执行了 F(4) (本代码把 fib(N-1) 放在前面),再去执行 F(3).

我们在 IDE 里 debug 就可以看到栈里面的情况:这里确实是先走的最左边这条线路,一共有 5 层,然后再一层层往上返回。

没看懂的小伙伴可以看视频讲解哦~

时间复杂度分析

如何评价一个算法的好坏?

很多问题都有多种解法,毕竟条条大路通罗马。但如何评价每种方法的优劣,我们一般是用大 O 表达式来衡量时间和空间复杂度。

时间复杂度:随着自变量的增长,所需时间的增长情况。

这里大 O 表示的是一个算法在 worst case 的表现情况,这就是我们最关心的,不然春运抢车票的时候系统 hold 不住了,你跟我说这个算法很优秀?

当然还有其他衡量时间和空间的方式,比如

Theta: 描述的是 tight bound
Omega(n): 这个描述的是 best case,最好的情况,没啥意义

<span style="color:blue">这也给我们了些许启发,不要说你平时表现有多好,没有意义;面试衡量的是你在 worst case 的水平;不要说面试没有发挥出你的真实水平,扎心的是那就是我们的真实水平。

那对于这个题来说,时间复杂度是多少呢?

答:因为我们每个节点都走了一遍,所以是把所有节点的时间加起来就是总的时间。

在这里,我们在每个节点上做的事情就是相加求和,是 O(1) 的操作,且每个节点的时间都是一样的,所以:

总时间 = 节点个数 * 每个节点的时间

那就变成了求节点个数的数学题:

在 N = 5 时,

最上面一层有1个节点,
第二层 2 个,
第三层 4 个,
第四层 8 个,
第五层 16 个,如果填满的话,想象成一颗很大的树:)

这里就不要在意这个没填满的地方了,肯定是会有差这么几个 node,但是大 O 表达的时间复杂度我们刚说过了,求的是 worst case.

那么总的节点数就是:
1 + 2 + 4 + 8 + 16

这就是一个等比数列求和了,当然你可以用数学公式来算,但还有个小技巧可以帮助你快速计算:

<span style="color:blue"> 其实前面每一层的节点相加起来的个数都不会超过最后一层的节点的个数,总的节点数最多也就是最后一层节点数 * 2,然后在大 O 的时间复杂度里面常数项也是无所谓的,所以这个总的时间复杂度就是:

<span style="color:blue">

最后一层节点的个数:2^n

空间复杂度分析

一般书上写的空间复杂度是指:

算法运行期间所需占用的所有内存空间

但是在公司里大家常用的,也是面试时问的指的是
Auxiliary space complexity

运行算法时所需占用的额外空间。

<span style="color:blue"> 举例说明区别:比如结果让你输出一个长度为 n 的数组,那么这 O(n) 的空间是不算在算法的空间复杂度里的,因为这个空间是跑不掉的,不是取决于你的算法的。

那空间复杂度怎么分析呢?

我们刚刚说到了冯诺伊曼体系,从图中也很容易看出来,是最左边这条路线占用 stack 的空间最多,一直不断的压栈,也就是从 5 到 4 到 3 到 2 一直压到 1,才到 base case 返回,每个节点占用的空间复杂度是 O(1),所以加起来总的空间复杂度就是 O(n).

我在上面👆的视频里也提到了,不懂的同学往上翻看视频哦~

优化算法

那我们就想了,为什么这么一个简简单单的运算竟然要指数级的时间复杂度?到底是为什么让时间如此之大。

那也不难看出来,在这棵 Recursion Tree 里,有太多的重复计算了。

比如一个 F(2) 在这里都被计算了 3 次,F(3) 被计算了 2 次,每次还都要再重新算,这不就是狗熊掰棒子吗,真的是一把辛酸泪。

那找到了原因之后,为了解决这种重复计算,计算机采用的方法其实和我们人类是一样的:记笔记

对很多职业来说,比如医生、律师、以及我们工程师,为什么越老经验值钱?因为我们见得多积累的多,下次再遇到类似的问题时,能够很快的给出解决方案,哪怕一时解决不了,也避免了一些盲目的试错,我们会站在过去的高度不断进步,而不是每次都从零开始。

回到优化算法上来,那计算机如何记笔记呢?

我们要想求 F(n),无非也就是要
记录 F(0) ~ F(n-1) 的值
那选取一个合适的数据结构来存储就好了。

那这里很明显了,用一个数组来存:

Index012345
F(n)011235

那有了这个 cheat sheet,我们就可以从前到后得到结果了,这样每一个点就只算了一遍,用一个 for loop 就可以写出来,代码也非常简单。

class Solution {
    public int fib(int N) {
        if (N == 0) {
            return 0;
        }
        if (N== 1) {
            return 1;
        }
        int[] notes = new int[N+1];
        notes[0] = 0;
        notes[1] = 1;
        for(int i = 2; i <= N; i++) {
            notes[i] = notes[i-1] + notes[i-2];
        }
        return notes[N];
    }
}

这个速度就是 100% 了~

但是我们可以看到,空间应该还有优化的余地。

那仔细想想,其实我们记笔记的时候需要记录这么多吗?需要从幼儿园到小学到初中到高中的笔记都留着吗?

那其实每项的计算只取决于它前面的两项,所以只用保留这两个就好了。

那我们可以用一个长度为 2 的数组来计算,或者就用 2 个变量。

更新代码:

class Solution {
    public int fib(int N) {
        int a = 0;
        int b = 1;
        if(N == 0) {
            return a;
        }
        if(N == 1) {
            return b;
        }
        for(int i = 2; i <= N; i++) {
            int tmp = a + b;
            a = b;
            b = tmp;
        }
        return b;
    }
}

这样我们就把空间复杂度优化到了 O(1),时间复杂度和用数组记录一样都是 O(n).

这种方法其实就是动态规划Dynamic Programming,写出来的代码非常简单。

<span style="color:blue">那我们比较一下 Recursion 和 DP:

Recursion 是从大到小,层层分解,直到 base case 分解不了了再组合返回上去;
DP 是从小到大,记好笔记,不断进步。

也就是 Recursion + Cache = DP

如何记录这个笔记,如何高效的记笔记,这是 DP 的难点。

有人说 DP 是拿空间换时间,但我不这么认为,这道题就是一个很好的例证。

在用递归解题时,我们可以看到,空间是 O(n) 在栈上的,但是<span style="display:block;color:blue">用 DP 我们可以把空间优化到 O(1),DP 可以做到时间空间的双重优化。</span>

其实呢,斐波那契数列在现实生活中也有很多应用。

比如在我司以及很多大公司里,每个任务要给分值,1分表示大概需要花1天时间完成,然后分值只有1,2,3,5,8这5种,(如果有大于8分的任务,就需要把它 break down 成8分以内的,以便大家在两周内能完成。)
因为任务是永远做不完的而每个人的时间是有限的,所以每次小组会开会,挑出最重要的任务让大家来做,然后每个人根据自己的 available 的天数去 pick up 相应的任务。

披着羊皮的狼

那有同学可能会想,这题这么简单,这都 2020 年了,面试还会考么?

答:真的会。

只是不能以这么直白的方式给你了。

比如很有名的爬楼梯问题:

一个 N 阶的楼梯,每次能走一层或者两层,问一共有多少种走法。

这个题这么想:

站在当前位置,只能是从前一层,或者前两层上来的,所以 f(n) = f(n-1) + f(n-2).

这题是我当年面试时真实被问的,那时我还在写 python,为了炫技,还用了lambda function:

f = lambda n: 1 if n in (1, 2) else f(n-1) + f(n-2)

递归的写法时间复杂度太高,所以又写了一个 for loop 的版本

def fib(n)
  a, b = 1, 1
  for i in range(n-1):
    a, b = b, a+b
  return a 

然后还写了个 caching 的方法:

def cache(f):
    memo = {}
    def helper(x):
        if x not in memo:
            memo[x] = f(x)
        return memo[x]
    return helper
@cache
def fibR(n):
    if n==1 or n==2: return 1
    return fibR(n-1) + fibR(n-2)

<span style="color:blue">还顺便和面试官聊了下 tail recursion:

tail recursion 尾递归:就是递归的这句话是整个方法的最后一句话。

那这个有什么特别之处呢?

尾递归的特点就是我们可以很容易的把它转成 iterative 的写法,当然有些智能的编译器会自动帮我们做了(不是说显性的转化,而是在运行时按照 iterative 的方式去运行,实际消耗的空间是 O(1))

那为什么呢?

因为回来的时候不需要 backtrack,递归这里就是最后一步了,不需要再往上一层返值。
def fib(n, a=0, b=1):
    if n==0: return a
      if n==1: return b
    return fib(n-1, b, a+b)

<span style="color:blue">最终,拿出了我的杀手锏:lambda and reduce

fibRe = lambda n: reduce(lambda x, n: [x[1], x[0]+x[1]], range(n), [0, 1])

看到面试官满意的表情后,就开始继续深入的聊了...

所以说,不要以为它简单,同一道题可以用七八种方法来解,分析好每个方法的优缺点,引申到你可以引申的地方,展示自己扎实的基本功,这场面试其实就是你 show off 的机会,这样才能骗过面试官啊~lol

这就是本文的所有内容了,不知道大家看完感受如何?留言告诉我你的感受吧~

今天这个视频做了半天,大家如果喜欢这种形式,请素质三连:

点击在看,鼓励下我啊!

瞎写评论,显得我很红啊!

转发转发转发,爱她,就送给她!

还想跟我看更多数据结构和算法题的小伙伴们,记得关注我,我是田小齐,算法就这么回事。

查看原文

赞 22 收藏 15 评论 3

程序员小岑成长记 发布了文章 · 2020-05-20

小岑架构学习笔记-架构到底是什么?

最近在学习极客时间《从0到1学架构》,记一些笔记。

每一个从事技术工作的同学,都有一个成为架构师的梦想,去从事设计架构方面的工作。那么架构这个词具体是什么?

在我平时的工作中,会进行团队内部分大功能的架构设计,平时也会通过各种渠道了解其他公司的架构设计。

但当我们聊架构的时候,我们具体是在聊什么?

基础名词

在具体定义什么是架构前,先将架构设计中经常提到的一些名词,先进行梳理。

系统和子系统

系统

系统泛指由一群有关联的个体组成,根据某种规则运作,能完成个别元件不能单独完成的工作的群体。它的意思是“总体”“整体”或“联盟”。

引用的是来自维基百科的定义,有一些特点,如下所示:

关联: 系统是有一群有关联的个体所组成的,没有关联的个体不能够成为一个系统,也就是说在一个系统内部的东西,肯定是互相有所关联的。比如说我们在打造一间卧室,不可能把厕所放进卧室,只有当床,书桌,灯这些东西都在一个房间内时,才组成了卧室。如果从技术系统的角度上来说,我理解就是,一个系统内的功能或者模块的存在都是为了这个这个系统的定位服务的,比如说一个负责商户管理的系统,往往不会把用户相关的管理放进来。

规则: 一个系统内的个体肯定是按系统统一的规则运作,规定了系统内个体之间分工和协作的方式。比如一个交易系统,会定义校验模块,风控模块,限流模块,算价模块,落单模块,发券模块等,会定义好每个模块的职能以及协作的方式,完成整个交易流程。

能力: 个体组合在一起产生了新的系统能力,是单个个体所不具备的,比如汽车能够载重前进,但汽车中的单个个体抽出来都不具备这样的能力,比如发动机、变速器、传动轴、车轮。

子系统

我理解子系统的定义其实和系统是一样的,一个系统可能就是另一个系统的子系统。拿交易系统举个例子。

  1. 交易系统本身是一个系统,又包含了支付,退款,结算,下单等子系统
  2. 支付系统里可能又包含了对接支付系统和处理支付回调的系统。

模块和组件

维基百科定义如下

软件模块(Module)是一套一致而互相有紧密关连的软件组织。它分别包含了程序和数据结构两部分。现代软件开发往往利用模块作为合成的单位。模块的接口表达了由该模块提供的功能和调用它时所需的元素。模块是可能分开被编写的单位。这使它们可再用和允许人员同时协作、编写及研究不同的模块。软件组件定义为自包含的、可编程的、可重用的、与语言无关的软件单元,软件组件可以很容易被用于组装应用程序中。

我觉得模块,从个人感受上来说,模块主要定义的是功能的集合,是为了职责上进行分离,比如交易系统的下单流程 可能会分为 校验模块,优惠计算模块,风控模块,下单模块等,每个模块各司其职,只关注在自己的事情,当有一些业务需求变更时,只需要改动相应的模块,不会影响其他模块。

我觉得组件的话,从其英文单词component上,也可以翻译成零件一次,感觉像是独立可替换的东西,我个人对组件的理解是一个可复用的东西,可以根据需求放在不同模块里去使用,比如定义一个数据转换组件,统一的校验组件,统一的异常处理器,都可以理解是组件。

框架和架构

学习时,作者提到,很多人往往分不清楚框架和架构。但我个人理解框架和架构还是很好区分的。

框架,从维基百科的释义上是:

软件框架(Software framework)通常指的是为了实现某个业界标准或完成特定基本任务的软件组件规范,也指为了实现某个软件组件规范时,提供规范所要求之基础功能的软件产品。

也就是说框架关注的是规范,按照某一个规范去用这个框架,去实现我们要求的功能,并且额外提供一些基础功能,比如Srping MVC 是MVC的开发框架,除了满足MVC的规范,还提供了很多基础功能帮助开发者实现功能,比如注解,Security,JPA等。

框架,我的个人层面理解就是一种帮助开发者实现软件功能的工具,而架构是开发者决定如何去设计软件。

架构,从维基百科上的释义是:

软件架构指软件系统的“基础结构”,创造这些基础结构的准则,以及对这些结构的描述。

架构更关注的是结构,如何组成你的整个软件系统这件事。但由于在软件架构的定义中,并未将什么是基础结构明确说明,所以从不同的角度解读架构,其实都可以。

比如从业务逻辑角度来说,学生管理系统的架构可以是:

从物理部署的角度,学生管理系统的架构可以是:

从开发规范的角度,因为采用标准的MVC框架开发,学生管理系统的架构又可以是:

这些“架构”,都是“学生管理系统”正确的架构,只是从不同的角度来分解而已,这也是 IBM 的 RUP 将软件架构视图分为著名的“4+1 视图”的原因。

架构比较合理的理解

架构是一个软件系统的顶层结构,就像建筑设计师的草图,定义了整体的骨架,每一块布局之间如何串联,形成协同效应。

架构定义了软件中的各个系统,子系统,模块和组件,如何组成,并且如何运作和协作。

比如同样的一个交易系统,因为选择的架构不同,各个系统之间的交互方式,组成方式也会不同。

所以我的理解是架构决定的是软件系统的顶层设计,而且细化到每个单独的系统,也有其自己的架构设计。

站的视角越高,看到的架构越宽泛一些。

总结一下

架构是顶层设计;框架是面向编程或配置的半成品;组件是从技术维度上的复用;模块是从业务维度上职责的划分;系统是相互协同可运行的实体。

查看原文

赞 0 收藏 0 评论 0

程序员小岑成长记 发布了文章 · 2020-05-16

不要做软件开发团队里打破窗户的那个人

以下内容人物均为杜撰,如有巧合,纯属雷同。

从前有个程序员,叫阿星,在小公司工作了2,3年,经过了好多轮技术面试的奋战,终于成功加入了Banana公司,是一个很有名的技术大厂。阿星加入的部门是一个负责公司支付业务的中台团队。

打杂期

阿星在Banana公司的前几个月,主要做一些零散的小需求,一直没机会上手一些核心系统的开发,不过偶尔也会翻阅一些老系统,有不懂或者觉得和自己想法不一样的地方会咨询老同事。

有些时候,老同事都会有句口头禅,这是历史代码的问题,这一块改了影响不知道有哪些,会增加回归成本,这期先这样往上加吧,后面再看看。

每次听到这样的结论,阿星因为是新人,也会赞同老同事的看法,认为这样的考虑是合理的。

但阿星内心总隐隐感觉不对...

Break or Repair?

因为在入职这段时间表现不错,领导走到阿星背后,拍了拍阿星的肩膀说:“阿星,最近我们要对接一个新的支付公司,叫好就付,你找老同事了解下之间是怎么对接其他支付公司的,你来做一下这件事情。”

阿星内心OS:“终于可以参加核心流程的开发了,太棒了”

阿星找老同事了解了下,对接一个新的支付厂商,需要增加一个对应的开通支付的功能,对接支付的功能以及处理开通信息回调的功能。

阿星拉取了主要用来对接支付的的系统,主要是通过定义了一个标准接口给上游系统,内部通过上游分发的展示Id,将展示Id转换成该系统可识别的厂商Id,进入各自的主流程。

阿星要来了新的厂商的接口文档,将上面的三个功能都增加了一个新的分支加以实现,并添加了上游展示Id和好就付厂商之间的转换代码。

进入测试阶段,阿星发现,直接调支付功能是通的,但是开通功能一直不通,仔细查阅了代码发现,原来开通和支付这里各自维护了一套展示Id和厂商之间的转换关系。

阿星为了避免部署上代码后,还是测不通,仔细查阅了整个系统,发现这样的转换关系,在系统内维护了7个地方。

阿星看了看老同事的实现,都是用到一个新功能时,将原有的转换代码直接拿了过来,增加了一个新的分支。

阿星觉得这样的转换逻辑应该维护在一个统一的地方,否则之后新增或者修改一个厂商,每次改动一个厂商需要修改这7个地方,而且随着功能的新增,可能还会增加。

阿星询问了之前的老同事,老同事说 他实现的时候也是看之前的代码就是这么写的,确实也觉得不太合理,但历史都是这么写的,自己就继续往后加了。

阿星陷入了犹豫,自己应该是优化这块功能,还是继续延续老的用法,等到实在改不动了再修复。

你会怎么做呢?想一想,可以留言回答~

做正确的事,不要做容易的事

阿星想了想,如果我也继续这么做的话,那么下一个人可能也会继续这么做,那么这段不合理的处理方式就会一直延续下去,永远没有结束的那天,也就无法成为一份好的代码,这可是我梦寐以求加入的公司啊~

可是改这么多地方,会不会影响很多,阿星主动找了资深的同事,说明了自己的想法,同事也认可了自己的做法,之前开了一道口子,现在越来越大了,是应该修复了。

来吧,看我的天马流星拳!(抱歉,阿星有一些中二)

阿星将所有负责转换的逻辑,都抽取了出来,统一到了一个地方,增加了注释,并将原来各处的调用,都收拢到了一处,虽然为此阿星加班测了好多不是他这个需求的功能,但阿星内心是满足的,自己做了一件正确的事。

在阿星修复了这个口子之后,又紧接着对接了好几个新的厂商,大家再也不需要修改7个地方了,阿星有种深藏功与名的感觉。

破窗效应

之前阿星在修复的问题的时候,在想,为什么之前大家都看到了这个问题,但是没人在第一时间发现这个问题后修复呢?

直到有一天,阿星了解到了这个理论 - 破窗效应。

破窗效应(英语:Broken windows theory)是犯罪学的一个理论,该理论由詹姆士·威尔逊(James Q. Wilson)及乔治·凯林(George L. Kelling)提出,并刊于《The Atlantic Monthly》1982年3月版的一篇题为《Broken Windows》的文章。

此理论认为环境中的不良现象如果被放任存在,会诱使人们仿效,甚至变本加厉。一幢有少许破窗的建筑为例,如果那些窗不被修理好,可能将会有破坏者破坏更多的窗户。最终他们甚至会闯入建筑内,如果发现无人居住,也许就在那里定居或者纵火。一面墙,如果出现一些涂鸦没有被清洗掉,很快的,墙上就布满了乱七八糟、不堪入目的东西;一条人行道有些许纸屑,不久后就会有更多垃圾,最终人们会视若理所当然地将垃圾顺手丢弃在地上。这个现象,就是犯罪心理学中的破窗效应。

阿星看完之后明白了,在软件开发中其实也存在着破窗效应,当一处不合理的开发出现后,没有在第一时间修完这个破碎的窗户,接下来的人就可能会在修和不修之间动摇,有概率让这个窗户变的更大,让这个窗户变的更难修复。

让我们一起修窗户

阿星最后意识到,无论是小厂还是大厂,代码是靠大家一起维护的,只有大家都有修窗户的意识,才会让系统变的越来越好,否则只会将问题都甩在历史问题上,可是历史问题又是谁造成的呢?

好了,这就是程序员小岑笔下人物阿星的第一篇历险记,如果觉得还不错的话,支持一下,我的公众号是程序员小岑成长记。

查看原文

赞 0 收藏 0 评论 0

程序员小岑成长记 发布了文章 · 2020-05-16

Mybatis版本升级踩坑及背后原理分析

1、背景

某一天的晚上,系统服务正在进行常规需求的上线,因为发布时,提示统一的pom版本需要升级,于是从 1.3.9.6 升级至 1.4.2.1。
当服务开始上线后,开始陆续出现了一些更新系统交互日志方面的报警,属于系统辅助流程,报警下图所示, 具体系统数据已脱敏,内容是Mybatis相关的报警,在进行类型转换的时候,产生了强转错误。

更新开票请求返回日志, id:{#######}, response:{{"code":XXX,"data":{"callType":3,"code":XXX,"msg":"XXXX","shopId":XXXXX,"taxPlateDockType":"XXXXXXX"},"msg":"XXXXX","success":XXXX}}
nested execption is org.apache.ibatis.type.TypeException: Could not set parameters for mapping: ParameterMapping{property='updateTime', mode=IN, javaType=class java.lang.String,
jdbcTyp=null,resultMapId='null',jdbcTypeName='null',expression='null'}.Cause org.apache.ibatis.type.TypeException,Error setting non null parameter #2 with JdbcType null. Try setting a
different Jdbc Type for this parameter or a different configuration property.Cause java.lang.ClassCastException:java.time.LocalDateTime cannot be cast to java.lang.String

报警的一块代码,属于历史功能,失败并不会影响主流程,但在定位期间,会频繁报警,造成一定的干扰,因此当时首先采取回滚操作,将统一的pom版本回滚至历史版本,报警消失,再进行问题的定位和分析。
以下章节是对报警原因的定位及原因详细分析的介绍。

2、报警原因定位

首先是具体的报警原因:

由于mybatis版本由inf-bom引入而来,在inf-bom升级后,由3.2.3 升级至了 3.4.6版本,而Mybatis自3.2.4开始就不支持目前系统内的SQL Mapper的用法,因此上线后,线上出现频繁报警。接下来是定位的过程。

回滚完毕后,开始具体分析报警产生的主要原因,进行了以下几步的排查。

1.查看了报警的Mapper方法,如下代码所示, 这个是接收返回参数,根据主键id,更新具体响应内容和时间的代码,入参有3个,类型分别为long, String 和 LocalDateTime

int updateResponse(@Param("id")long id, @Param("response")String response, @Param("updateTime")LocalDateTime updateTime);

2.查看了Mapper方法对应的XML文件,如下代码,对应的parameterType类型是String,而实际参数的类型有Long,有String,也有LocalDateTime。

<update id="updateResponse" parameterType="java.lang.String">
UPDATE invoice_log
  SET response = #{response}, update_time = #{updateTime}
WHERE id = #{id}
</update>

3.查看了Mybatis上线前后的版本,因为报警的内容是Mybatis处理sql语句时,发现不能将LocalDateTime转型为String,这一段逻辑在上线前是ok的,上线的业务逻辑对这段历史代码无改动,因此猜测是统一pom的升级,导致Mybatis的版本发生了变化,某些历史功能不支持了。 mybatis版本上线前后的变化,1.3.9.6对应的版本是3.2.3,1.4.2.1对应的版本是3.4.6。

4.通过第3步可以得到,在这次inf-bom的版本升级中,mybatis3的版本直接升了两个大版本,因此可以基本将原因猜测为 Mybatis升级跨度大,导致部分历史功能没有兼容支持,引起的线上sql更新报错。

5.为了具体验证第4步的想法,通过UT的方式,通过将Mybatis的版本不断从3.4.6往下降,直至没有报错位置,最终定位是Mybatis版本为3.2.3时,线上代码是正常可用的,只要升一个版本也就是自3.2.4开始,就开始不兼容目前的用法。(这个当时思路不是很好,应该从小版本逐个往上升,可以去加速定位版本的效率)

最后定位报警原因,由于mybatis版本由统一pom引入而来,在统一pom升级后,由3.2.3 升级至了 3.4.6版本,而Mybatis自3.2.4开始就不支持目前系统内的SQL Mapper的用法,因此上线后,线上出现频繁报警。

报警原因已定位,但为什么版本升级后就不兼容历史的用法,并且具体不兼容的是哪一块内容,背后的原理又是什么,请看接下来章节的详细分析。

3、详细分析

3.1 Mybatis 升级3.2.4版本的官方Release公告

首先从报错的原因上来看,Caused by: java.lang.ClassCastException: java.lang.LocalDateTime cannot be cast to java.lang.String ,是Mybatis在构建sql语句时,发现时间字段 类型为LocalDateTime 不能强制转为String类型。 这个SQL XML的配置在3.2.3的版本是正常可以用,那么首先是从Mybatis 的 release log上查看3.2.4版本 发生了什么变化。

An special remark about this feature. Previous versions ignored the "parameterType" attribute and used the actual parameter to calculate bindings. This version builds the binding information during startup and the "parameterType" attribute is used if present (though it is still optional), so in case you had a wrong value for it you will have to change it.

从官网的Release Log可以看出,Mybatis在3.2.4以前的版本,是忽略XML中的parameterType这个属性,并且使用真实的变量类型进行值的处理,在3.2.4及以后的版本中,这个属性会被启用,因此如果出现类型不匹配的话,就会出现转型失败的报错,也提示我们开发者在升级到这个版本及以上时,需要检查系统内的XML配置,使类型相匹配,或者不设置该属性,让Mybatis自行进行计算。

从以上内容,可以了解到,在版本升级后,mybatis在构建sql语句,获取字段值的时候逻辑发生了变化,那么接下来通过一个普通的示例,了解mybatis在获取字段值这一块的具体代码流程是怎样的,以3.2.3版本为例。

3.2 以版本3.2.3为例,mybatis构建SQL语句过程的原理分析

首先,先看以下配置,定义了一个通过主键id获取学生信息的方法,仿造系统内的历史代码,也将parameterType定义为 java.lang.String 和 方法对应的参数 int 并不相同。

public StudentEntity getStudentById(@Param("id") int id);

<select id="getStudentById" parameterType="java.lang.String" resultType="entity.StudentEntity">
SELECT id,name,age FROM student WHERE id = #{id}
</select>

mybatis框架要做的事情就是在运行getStudentById(2)的时候,将 #{id}进行替换,使SQL语句变成 SELECT id,name,age FROM student WHERE id = 2 。Mybatis要将SQL语句完整替换成带参数值的版本,需要经历框架初始化以及实际运行时动态替换两个部分。因为Mybatis的代码非常多,接下来主要阐释和本次案例相关的内容。

在框架初始化阶段,主要有以下流程,如下图所示

在框架初始化阶段,有一些组件会被构建,接下来进行逐一做个简单的介绍:

SqlSession 作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能。

SqlSource 负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回。

Configuration MyBatis所有的配置信息都维持在Configuration对象之中。

接下来主要关注SqlSource,这个类会负责在负责生成SQL语句,也是本次案例中,3.2.3和3.2.4差异比较大的地方。接下来会一些源码部分的介绍。

在构建Configuration的过程中,会涉及到构建对应每一条sql语句对应的MappedStatemnt,在parmeterTypeClass就是根据我们在xml配置中写的parmeterType转换而来,值为java.lang.String,在接下来构建SqlSource中,传入了这个参数,如下图所示:

在SqlSource的构建阶段中,parameterType参数其实是被忽略不使用的,这也和官方的描述是一致的,3.2.4之前这个parameterType属性是被忽略的,然后创建了DynamicSqlSource,这个类主要是用于处理Mybatis动态Sql的类。

在框架初始化阶段,需要介绍的内容,在3.2.3版本已经介绍完毕,接下来是当执行getStudentById方法时,Mybatis的流程,如下图所示,受限于图片长度,进行了布局的调整:

在具体执行阶段,也有一些组件,我们需要做了解

SqlSession 作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能

Executor MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护

BoundSql 表示动态生成的SQL语句以及相应的参数信息

StatementHandler 封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合。

ParameterHandler 负责对用户传递的参数转换成JDBC Statement 所需要的参数,

TypeHandler 负责java数据类型和jdbc数据类型之间的映射和转换

接下来主要关注在获取BoundSql以及参数化语句的流程,也是本次案例中,3.2.3和3.2.4差异比较大的地方。接下来会一些源码部分的介绍。

在进入Executor的query方法后,会首先通过对应的MappedStatement获取BoundSql,用来帮助我们动态生成SQL语句,里面绑定了对应的SQL以及参数映射关系,在构建框架阶段,我们使用的SqlSource是DynamicSqlSource,通过这个类来生成获取BoundSql。

通过上图的代码可以得知,parameterType在初始化阶段未被使用,而是在SQL执行时,获取到的,但获取到的类型是parameterObject对应的类型,这个类是用来记录mapper方法上对应的参数的。如下图所示,并非在Sql配置文件中标注的java.lang.String。

接下来,通过SqlSourceBuilder sqlSourceParser 对sql以及计算得到的类型进行再次处理,当中流程代码比较长,主要是在这个过程中去制作 sql方法的入参 和 java类型的绑定关系,mybatis依赖这个绑定关系使用对应的TypeHandler去进行值的转换,调用链路是SqlSourceParser.parse -> 内部类 ParameterMappingTokenHandler.handleToken -> 私有方法 buildParameterMapping, 如下图代码所示。因为当前的parmeterType为 MapperMethod$ParamMap,进过了多个if判断,判定当前property id 的 propertyType 为Object.class类型,接下来就是制作 sql方法的入参 和 java类型的绑定关系 parameterMapping,并进行了返回。

制作完成的ParameterMapping的结构如下图代码所示,参数id对应的javaType类型为 java.lang.Object,对应的TypeHander处理器为UnknownTypeHandler,也就是未找到合适的TypeHandler的兜底选项。

接下来流程就会流转到Executor, org.apache.ibatis.executor.SimpleExecutor#doQuery进行查询时,会根据当前的SQL类型,生成对应的statmentHandler,因为我们目前都是用的预编译SQL,因此生成的statementHandler就是PrepareStatmentHandler,熟悉JDBC的小伙伴应该马上可以猜到这对应的语句是什么类型了。接下来就会对这句SQL语句进行填充,如下图代码所示,会通过PrepareStatmentHandler的parameterize方法对Statment进行参数化,也就是进行填充过程。

在PreparseStatmentHandler进行参数化时,会将参数化的职责交给DefaultParameterHandler进行,如下图代码所示,主要关注红线部分,首先会获取parameterMapping对应的TypeHander,如上章节所示,获取到的是UnknownTypeHandler,然后会通过setParameter方法,将参数id替换成对应的值。

在typehandler的流程里,首先会进入BaseTypeHandler,然后在具体设置时,进入子类的方法,在UnknownTypeHandler,首先会再次对parameter进行解析,判断最正确的TypeHandler类型,如下图代码所示:

在resolveTypeHandler方法中,因为已知参数值的类型,通过Integer这个class在typeHandlerRegistry中寻找对应的TypeHandler,TypeHandlerRegistry是Mybatis启动时内置好的,java对象类型和TypeHandler的映射关系,有兴趣的可以进这个类详细看下,在本案例中,会直接获取到IntegerHandler,如下图代码所示:

在获取到IntegerHandler后,就可以使用IntegerTypeHandler的setInt方法,对SQL语句中的参数进行替换,如下图代码所示,sql语句被成功替换。

后续就是执行SQL并处理返回结果,不在本文的讨论范围内,从上文的分析中,我们可以了解到,在3.2.3及以下版本,Mybatis会忽略parmeterType,在真正进行sql转换时,重新根据sql方法入参类型计算合适的TypeHandler处理器,所以本案例中的代码在3.2.3时运行时正常的。

3.3 以版本3.2.4为例,相比版本3.2.3,mybatis构建SQL语句过程的变化分析

在3.2章节中,得知mybatis是在运行sql阶段重新计算参数对应的TypeHandler进行sql参数替换,那么在版本3.2.4中,mybatis做了什么改动,导致了原有的使用方式不可用了呢。从官方的release log来看,版本3.2.4做了这样一个改动。

This version builds the binding information during startup and the "parameterType" attribute is used

意思是说 parameterType会在框架运行阶段就被使用到,从这个中,我们将分析的重点放在构建阶段,同时负责处理绑定关系的BoundSql由配置阶段的SqlSource生成,因此主要查看SqlSource的构建,3.2.4发生了什么变化,如下图所示。与3.2.3不同,3.2.4首先判断了是否为动态SQL,在非动态SQL情况下,将parameterType java.lang.String作为参数,传入了SqlSource的构造方法。

后续流程与3.2.3一致,因为parameter类型为java.lang.String,在构建parameterMapping时,使用的类型就是java.lang.String。

因为在框架初始化阶段,SqlSource中 parameterMapping, id对应的类型就是java.lang.String,导致在进行Sql语句替换时,获取到的TypeHandler是StringTypeHandler,如下图所示:

后面的报错原因就比较好理解了,在调用StringTypeHandler的setString方法时,报出了java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String的错误。

4、总结

总结一下这个案例的主要原因是:

mybatis 3.2.3版本 兼容parameterType和实际参数类型不匹配,运行时动态计算值处理器类型,在大版本升级2个版本号后,parameterType开始生效,以parameterType作为参数的实际类型进行TypeHandler的获取计算,导致类型不匹配时,强转报错。

带给我自己的在后续编写编写代码及系统上线方面的启示是:

1.在统一pom升级时,需要线下进行全面回归,避免框架存在不兼容的用法,导致线上错误。

2.开发同学可以检查自己系统内的mybatis版本,如果是3.2.4以下,需要全面检查下现在的mapper文件里 对于parameterType的使用 和实际的参数类型是否一致,避免升级到3.2.4及以上版本时发生兼容报错,如果有不匹配的情况存在,需要进行修正 或者 不使用parameterType,让Mybatis在运行SQL时自动计算对应的类型,

3.可以考虑使用mybatis-generator来自动生成xml和mapper文件,有专业团队维护,相对来说稳定性更好,也避免自己手动修改xml文件容易带来误操作。

4.可以主动关注强依赖的一些开源框架的Release log,有很多重要的信息。

5、作者简介

岑凯伦、90后软件工程师、5年服务端开发经验 微信公众号: 程序员小岑成长记。

查看原文

赞 0 收藏 0 评论 0

程序员小岑成长记 发布了文章 · 2020-05-16

面技术岗位实习生,这些你得了解啊!

最近是实习生招聘季节,作为团队的面试官之一,也面试了有接近20个后端的实习生。举一些例子,谈一些个人体会,提一些小建议,望君笑纳。

1.遇到过好多学弟学妹,实习招聘一开始就投递了简历,但迟迟未看到他们进入面试环节,一问才知道,这是打算参加最后一轮笔试,希望可以准备充分一些,说不定网上还有些资料可以放出来呢?

小岑看法: offer名额是有限的,越早参加笔试的,那就肯定是越早能获得被面试的机会,也就可以更早拿到offer名额,越往后的话,不但是offer数量会少,而且随着数量减少,待定选手增多,只能优中再选优,无形当中把自己的游戏难度调到了Hard模式。

小岑建议: 在实习招聘季到来前,就做好面试的准备,全力备战前几轮的笔试,越早肯定是越好,然后尽量走内推通道,增加自己的简历被看到的机会。

2.遇到过几个实习生,意向工作地没有填上海,也不愿接受调配,抱着试一试的态度,给同学打了电话,结果回答是,我这个其实是随便选的,我对上海也是OK的。

小岑看法: 如果面试官没有很留心你的话,可能因为这个城市,就不会关注你的简历了,因为地域不符,也没法把你招过来,可能你无形中就错过了一个机会。

小岑建议: 填写公司的招聘系统时,尽量把必填的,选填的都填写完整,自己的意向工作地,把自己愿意去实习工作的城市都选上,另外通常系统里都会有是否愿意接受调配,一定要考虑清楚选择。

3.看到一些不错的简历,很兴奋,打电话过去,结果是只考虑C++,Java不考虑 或者是 主要做C++,可以转Java,结果Java这块都没怎么复习,自身的计算机基础也不是很扎实,面试结果也不是很理想。

小岑看法: 捞简历阶段,其实是面试官根据自身团队的属性,挑选合适的简历,捞到简历后,通常是根据你简历投递的岗位展开面试。

小岑建议: 标注清楚自己找的是哪种语言的实习生,因为捞简历的部门不一定和你的技术栈是契合的,会增加你和面试官前期的沟通成本。另外提前考虑下有没有跨语言的可能,通常来说一些大厂的主要技术栈,网上都是可以查到相关资料的,如果可以接受跨语言,建议需要提前准备一下,会进行基本的考察。

4.看到一些学生的简历,动辄是精通Java,精通Redis,精通算法,可能是因为确实不知道自己不知道,也可能是因为想展示下自己的竞争力。那么我看到这些精通的,我会认为学生这块掌握的一定很好了,然后可能会针对精通技能问一些深入的问题,但往往大多数同学在涉及到这类问题时,才会表达下,自己其实目前主要在应用阶段。

小岑看法: 针对自己比较熟悉的技能,在简历上增加标注是可行的,但切勿夸大,如果面试官对这块技能考察后,你的回答不是很理想,会非常减分。

小岑建议: 针对自己熟悉的技能,根据真实的情况写熟悉XXX技能及其原理,也可以写目前是学会应用XXX技能,这都是比较合理的写法,不要过度夸大,反而容易给自己的面试阶段埋坑。

5.有个印象比较深刻的沟通例子,就是我和学生沟通可以面试的时间,电话打通的那一刻,语气非常不好,嗓门也很大,背景音也很嘈杂,我都怀疑是平时是不是被骚扰电话打多了,接到陌生电话就很烦躁,知道我是面试的之后,稍微缓和了一些,但感觉还是比较急躁,沟通完时间后,匆忙挂断了电话。

小岑看法: 面试其实从你接听面试官电话的那一刻就开始了。保持一个职业化的沟通,可以给面试官留下比较好的第一印象,相信大家也知道,第一印象在任何场景中都是很重要的。

小岑建议: 不论平时接电话的语气是怎么样的,这段期间接电话,礼貌平静一些。通常面试官会和你沟通一下可以面试的时间,建议报一些可以选择的时段给面试官,然后最好也反问下面试官,这个时间段他是否也方便,多一些换位思考。

6.面试当天准时到场,仪表整洁一些,提前调试好网络和视频清晰度,手机保持免打扰状态。

7.面试过程中 一定会遇到不会的,不会不要紧,但你要根据目前学习到的知识提出你的一些设想,切记不要冷场以及胡乱回答。

8.现在没有听明白的问题,多和面试官确认下问题想问的是什么,回答时不要流水账,思考后,概括一下,有一些层次结构回答,可以看一下金字塔原理这本书。

9.代码环节的话,不要一听完问题就一股脑去写,尽可能多和面试官交流一下题意,明确边界范围,边写的时候也可以边交流。代码的话,不要在主方法里就蒙头写,有一些结构拆分,封装,现场写代码往往不是要你一定写出,更看重的是你写代码过程的习惯和基本功。在远程online coding时,在没有得到面试官的许可时,不要切出屏幕,面试官都会看到的,如果你想ide优化下你的代码,提申请,但最好不要。

10.面试结束后,往往会给同学一些提问的机会,遇到过一些同学准备很充分,问了和个人,公司都相关的一些问题,也有部分同学什么问题也没有就匆匆结束了 面试。

小岑看法: 面试完的提问环节,也很重要,尽可能表达下你对这个目标岗位的疑问和实习的热诚,是你可以再次展示自我,吸引面试官的机会,也可以增加一些印象分数。

小岑建议: 事先准备好一些问题,思考下自己想从实习中获得什么,想在这家公司获得些什么,会更有准备一些。

如果你有一些关于实习的问题,想要咨询,可以加公众号 程序员小岑成长记,我会尽快回复你的。

查看原文

赞 0 收藏 0 评论 0

程序员小岑成长记 发布了文章 · 2020-05-16

程序员小白的个人思考

这个周末,听了一个关于程序员从小白到骨干的成长经验分享,谈谈个人体会。

了解自己当前正处于什么样的阶段

首先提到的是达克效应,全称为邓宁-克鲁格效应(Dunning-Kruger effect)。它是一种认知偏差现象,指的是能力欠缺的人在自己欠考虑的决定的基础上得出错误结论,但是无法正确认识到自身的不足,辨别错误行为。这些能力欠缺者们沉浸在自我营造的虚幻的优势之中,常常高估自己的能力水平,却无法客观评价他人的能力。

file

这个图想描述的是,自信程度和知识水平之间是有一定的波动关系的。

刚开始,可能我们是不知道自己不知道,觉得自己的设计天下无敌,固执己见,随着自信心的爆棚,慢慢爬上愚昧之山的山顶。
在碰了一些壁,踩过一些坑之后才发现,自己是有很多不知道的地方,这时候是慢慢觉醒,知道自己还有很多不知道的地方,自信心也逐渐下降,进入传说中的绝望之谷。谷底待久了,开始慢慢开悟,知道自己真的懂了一些东西,知识+经验的结合,开始发挥作用,图的最后一部分就是不知道自己知道,代表随着自己的开悟,可能我已经在实际过程当中运用了一些比较高深的东西,然而我不一定事先学习过这些东西,可能是在开悟之路上自己总结出来的。

我觉得我应该是正在准备走上开悟之坡,我之前做过很多设计,总觉得自己的设计十分好,但其实也听到了很多的不同声音,反复🤔之后,开始明白自己确实有很多不足之处。现在阶段的我,知道自己确实对很多东西的了解还有待进一步深入,争取早日走向开悟之坡吧。

宏观无法改变,微观可以改变

就像标题所说的,宏观我们个人力量无法改变,那么微观上,我们能做到的可能是改变我们自己,改变身边可以影响的小圈子。

当我自己在吐槽别人的时候,我有没有做到最好,我写过的代码,我做的设计,我撰写过的文档,我负责的系统,是不是就是完美水准,换一个视角来看自己,我有没有槽点,先改变自己,再改变别人。

如果技术上的提升需要时间的话,至少让我自己做到,靠谱二字。
靠谱: 凡事有交待,件件有着落,事事有回音

你不会一直一个人solo做事

随着时间的推移,在未来,我必然重心会从一个人处理掉所有事情,转变为带着小伙伴,拿到更好的业务成果。

当我不再是一个人时,我无法把每个细节都cover到,也可能没办法上阵解决每一个技术问题,我需要学会沟通和协调,根据事情的大小,每个人的特点,分配好每个人应该做的事情,唯一不变的是始终要保持高质量的交付内容。

我要尝试授人以渔,而不是授人以鱼。尝试多赞美别人做得好的地方,毕竟你无法要求每个人和你想的完全一样,每次都做的是你想的样子,你只能尽力让他感受到你期望他们这样去做。我要尝试当我担任项目负责人时,我必然是最快出现在我的项目成员身前的那个人。

谈谈焦虑

分享中提到,好多程序员都在焦虑,担心35岁事业,担心不知道学什么好,继续做技术还是管理。

我其实也有这样的,那样的焦虑,但我是真的热爱这份行业,所以想太多其实也没有用处,我也不会转行。不如以那么比我年长很多的,身居高位的人为榜样,继续努力吧。

总结

写了好多,回顾一下,是正能量,是鸡汤更多哈哈,不过所谓成长和努力,更多是先在精神层面安抚好自己,确定了意识,肉体才会跟着力气往一处使。

有了更多的思考再来谈谈体会吧。

查看原文

赞 0 收藏 0 评论 0

程序员小岑成长记 发布了文章 · 2020-05-06

解读《阿里巴巴Java开发手册》(泰山版) - 更新中

前言

《阿里巴巴Java开发手册》一直深受Java开发爱好者和业界人士的认可,里面提出了许多宝贵的开发经验和建议,相信会对大家有很多的帮助。

但是所谓尽信书不如无书,需要对提到的规范有自己的思考和认同后,这本手册才能够对自己起到更大的帮助。

小弟不才,只是从个人观点谈谈对于这本手册阅读的理解,期望可以持续更新。

编程规约篇

命名风格

1.【强制】代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。

赞同,可以强制遵守。首先实际代码开发时确实可以这样去做,但这条规范应该是业内同行的共识了,而且以下划线和美元符号开始和结束并不会带来什么其他的好处。

2.【强制】所有编程相关的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式。

赞同,可以强制遵守。用纯英文表达释义更容易理解,拼音或者拼音与英文结合,还要考虑多音字的可能性,也不太好理解。

3.【强制】类名使用UpperCamelCase风格,但以下情形例外:DO/BO/DTO/VO/AO/ PO / UID 等。

赞同,可以强制遵守。驼峰式命名这个基本已经是共识了,但关于DO,BO等,其实项目中有人喜欢写Bo,有人先写BO,我个人感觉更倾向于两个都大写,这些其实是一些专用术语的缩写,所以都大写是比较合适的,比如DTO,其实是 Data Transfer Object的缩写。

4.【强制】方法名、参数名、成员变量、局部变量都统一使用lowerCamelCase风格。

赞同,可以强制遵守。基本都是业内共识了。

  1. 【强制】常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。

赞同,可以强制遵守。全部大写,单词间下划线隔开,基本都是共识了,看到过部分代码因为考虑到长度问题,使用了一些缩写来表达,我个人不是很喜欢,属于宁可长一些,但更喜欢可读性强一些的开发者。

  1. 【强制】抽象类命名使用Abstract或Base开头;异常类命名使用Exception结尾;测试类 命名以它要测试的类的名称开始,以 Test 结尾。

赞同,可以强制遵守。这样理解会更加清晰一些。

  1. 【强制】类型与中括号紧挨相连来表示数组。

赞同,可以强制遵守。这样看起来会更加的直观清晰,手册中提到的反例是 在main函数的入参中,使用String args[]来定义。我看了下目前IDEA在自动生成main函数时已经是String[] args这种风格了。

  1. 【强制】POJO类中的任何布尔类型的变量,都不要加is前缀,否则部分框架解析会引起序列 化错误。

有待考究,待补充,手册中给的反例是定义为基本数据类型 Boolean isDeleted的属性,方法也是isDeleted(),框架在反向解析时,会以为对应的属性名称是deleted,导致属性获取不到,进而抛出异常。

那么是哪些框架会出现这些问题呢,现在是否仍然有这些问题,这个有一定历史感的规范是否仍要遵守,毕竟我觉得加上is后,如果是布尔类型,我会更好理解一些,也可以和数据库中的命名一一对应。

9.【强制】包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用 单数形式,但是类名如果有复数含义,类名可以使用复数形式。

到类名之前的部分赞同,可以强制遵守。 关于类名有复数含义,可以用复数形式,这个困扰的主要是为什么类名会有复数含义呢,类代表的应该就是一个功能和属性的集合,按我的理解来说,应该是不具备复数含义的。

10.【强制】避免在子父类的成员变量之间、或者不同代码块的局部变量之间采用完全相同的命名,使可读性降低。

基本赞同,可以遵守,如果是代表同一个意思,用一个变量名表达,可读性会更好,管理维护也在一处地方。

查看原文

赞 1 收藏 0 评论 0

程序员小岑成长记 发布了文章 · 2020-03-05

Mybatis版本升级踩坑及背后原理分析

1、背景

某一天的晚上,系统服务正在进行常规需求的上线,因为发布时,提示统一的pom版本需要升级,于是从 1.3.9.6 升级至 1.4.2.1。
当服务开始上线后,开始陆续出现了一些更新系统交互日志方面的报警,属于系统辅助流程,报警下图所示, 具体系统数据已脱敏,内容是Mybatis相关的报警,在进行类型转换的时候,产生了强转错误。

更新开票请求返回日志, id:{#######}, response:{{"code":XXX,"data":{"callType":3,"code":XXX,"msg":"XXXX","shopId":XXXXX,"taxPlateDockType":"XXXXXXX"},"msg":"XXXXX","success":XXXX}}
nested execption is org.apache.ibatis.type.TypeException: Could not set parameters for mapping: ParameterMapping{property='updateTime', mode=IN, javaType=class java.lang.String,
jdbcTyp=null,resultMapId='null',jdbcTypeName='null',expression='null'}.Cause org.apache.ibatis.type.TypeException,Error setting non null parameter #2 with JdbcType null. Try setting a
different Jdbc Type for this parameter or a different configuration property.Cause java.lang.ClassCastException:java.time.LocalDateTime cannot be cast to java.lang.String

报警的一块代码,属于历史功能,失败并不会影响主流程,但在定位期间,会频繁报警,造成一定的干扰,因此当时首先采取回滚操作,将统一的pom版本回滚至历史版本,报警消失,再进行问题的定位和分析。
以下章节是对报警原因的定位及原因详细分析的介绍。

2、报警原因定位

首先是具体的报警原因:

由于mybatis版本由inf-bom引入而来,在inf-bom升级后,由3.2.3 升级至了 3.4.6版本,而Mybatis自3.2.4开始就不支持目前系统内的SQL Mapper的用法,因此上线后,线上出现频繁报警。接下来是定位的过程。

回滚完毕后,开始具体分析报警产生的主要原因,进行了以下几步的排查。

1.查看了报警的Mapper方法,如下代码所示, 这个是接收返回参数,根据主键id,更新具体响应内容和时间的代码,入参有3个,类型分别为long, String 和 LocalDateTime

int updateResponse(@Param("id")long id, @Param("response")String response, @Param("updateTime")LocalDateTime updateTime);

2.查看了Mapper方法对应的XML文件,如下代码,对应的parameterType类型是String,而实际参数的类型有Long,有String,也有LocalDateTime。

<update id="updateResponse" parameterType="java.lang.String">
UPDATE invoice_log
  SET response = #{response}, update_time = #{updateTime}
WHERE id = #{id}
</update>

3.查看了Mybatis上线前后的版本,因为报警的内容是Mybatis处理sql语句时,发现不能将LocalDateTime转型为String,这一段逻辑在上线前是ok的,上线的业务逻辑对这段历史代码无改动,因此猜测是统一pom的升级,导致Mybatis的版本发生了变化,某些历史功能不支持了。 mybatis版本上线前后的变化,1.3.9.6对应的版本是3.2.3,1.4.2.1对应的版本是3.4.6。

4.通过第3步可以得到,在这次inf-bom的版本升级中,mybatis3的版本直接升了两个大版本,因此可以基本将原因猜测为 Mybatis升级跨度大,导致部分历史功能没有兼容支持,引起的线上sql更新报错。

5.为了具体验证第4步的想法,通过UT的方式,通过将Mybatis的版本不断从3.4.6往下降,直至没有报错位置,最终定位是Mybatis版本为3.2.3时,线上代码是正常可用的,只要升一个版本也就是自3.2.4开始,就开始不兼容目前的用法。(这个当时思路不是很好,应该从小版本逐个往上升,可以去加速定位版本的效率)

最后定位报警原因,由于mybatis版本由统一pom引入而来,在统一pom升级后,由3.2.3 升级至了 3.4.6版本,而Mybatis自3.2.4开始就不支持目前系统内的SQL Mapper的用法,因此上线后,线上出现频繁报警。

报警原因已定位,但为什么版本升级后就不兼容历史的用法,并且具体不兼容的是哪一块内容,背后的原理又是什么,请看接下来章节的详细分析。

3、详细分析

3.1 Mybatis 升级3.2.4版本的官方Release公告

首先从报错的原因上来看,Caused by: java.lang.ClassCastException: java.lang.LocalDateTime cannot be cast to java.lang.String ,是Mybatis在构建sql语句时,发现时间字段 类型为LocalDateTime 不能强制转为String类型。 这个SQL XML的配置在3.2.3的版本是正常可以用,那么首先是从Mybatis 的 release log上查看3.2.4版本 发生了什么变化。

An special remark about this feature. Previous versions ignored the "parameterType" attribute and used the actual parameter to calculate bindings. This version builds the binding information during startup and the "parameterType" attribute is used if present (though it is still optional), so in case you had a wrong value for it you will have to change it.

从官网的Release Log可以看出,Mybatis在3.2.4以前的版本,是忽略XML中的parameterType这个属性,并且使用真实的变量类型进行值的处理,在3.2.4及以后的版本中,这个属性会被启用,因此如果出现类型不匹配的话,就会出现转型失败的报错,也提示我们开发者在升级到这个版本及以上时,需要检查系统内的XML配置,使类型相匹配,或者不设置该属性,让Mybatis自行进行计算。

从以上内容,可以了解到,在版本升级后,mybatis在构建sql语句,获取字段值的时候逻辑发生了变化,那么接下来通过一个普通的示例,了解mybatis在获取字段值这一块的具体代码流程是怎样的,以3.2.3版本为例。

3.2 以版本3.2.3为例,mybatis构建SQL语句过程的原理分析

首先,先看以下配置,定义了一个通过主键id获取学生信息的方法,仿造系统内的历史代码,也将parameterType定义为 java.lang.String 和 方法对应的参数 int 并不相同。

public StudentEntity getStudentById(@Param("id") int id);

<select id="getStudentById" parameterType="java.lang.String" resultType="entity.StudentEntity">
SELECT id,name,age FROM student WHERE id = #{id}
</select>

mybatis框架要做的事情就是在运行getStudentById(2)的时候,将 #{id}进行替换,使SQL语句变成 SELECT id,name,age FROM student WHERE id = 2 。Mybatis要将SQL语句完整替换成带参数值的版本,需要经历框架初始化以及实际运行时动态替换两个部分。因为Mybatis的代码非常多,接下来主要阐释和本次案例相关的内容。

在框架初始化阶段,主要有以下流程,如下图所示

在框架初始化阶段,有一些组件会被构建,接下来进行逐一做个简单的介绍:

SqlSession 作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能。

SqlSource 负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回。

Configuration MyBatis所有的配置信息都维持在Configuration对象之中。

接下来主要关注SqlSource,这个类会负责在负责生成SQL语句,也是本次案例中,3.2.3和3.2.4差异比较大的地方。接下来会一些源码部分的介绍。

在构建Configuration的过程中,会涉及到构建对应每一条sql语句对应的MappedStatemnt,在parmeterTypeClass就是根据我们在xml配置中写的parmeterType转换而来,值为java.lang.String,在接下来构建SqlSource中,传入了这个参数,如下图所示:

在SqlSource的构建阶段中,parameterType参数其实是被忽略不使用的,这也和官方的描述是一致的,3.2.4之前这个parameterType属性是被忽略的,然后创建了DynamicSqlSource,这个类主要是用于处理Mybatis动态Sql的类。

在框架初始化阶段,需要介绍的内容,在3.2.3版本已经介绍完毕,接下来是当执行getStudentById方法时,Mybatis的流程,如下图所示,受限于图片长度,进行了布局的调整:

在具体执行阶段,也有一些组件,我们需要做了解

SqlSession 作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能

Executor MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护

BoundSql 表示动态生成的SQL语句以及相应的参数信息

StatementHandler 封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合。

ParameterHandler 负责对用户传递的参数转换成JDBC Statement 所需要的参数,

TypeHandler 负责java数据类型和jdbc数据类型之间的映射和转换

接下来主要关注在获取BoundSql以及参数化语句的流程,也是本次案例中,3.2.3和3.2.4差异比较大的地方。接下来会一些源码部分的介绍。

在进入Executor的query方法后,会首先通过对应的MappedStatement获取BoundSql,用来帮助我们动态生成SQL语句,里面绑定了对应的SQL以及参数映射关系,在构建框架阶段,我们使用的SqlSource是DynamicSqlSource,通过这个类来生成获取BoundSql。

通过上图的代码可以得知,parameterType在初始化阶段未被使用,而是在SQL执行时,获取到的,但获取到的类型是parameterObject对应的类型,这个类是用来记录mapper方法上对应的参数的。如下图所示,并非在Sql配置文件中标注的java.lang.String。

接下来,通过SqlSourceBuilder sqlSourceParser 对sql以及计算得到的类型进行再次处理,当中流程代码比较长,主要是在这个过程中去制作 sql方法的入参 和 java类型的绑定关系,mybatis依赖这个绑定关系使用对应的TypeHandler去进行值的转换,调用链路是SqlSourceParser.parse -> 内部类 ParameterMappingTokenHandler.handleToken -> 私有方法 buildParameterMapping, 如下图代码所示。因为当前的parmeterType为 MapperMethod$ParamMap,进过了多个if判断,判定当前property id 的 propertyType 为Object.class类型,接下来就是制作 sql方法的入参 和 java类型的绑定关系 parameterMapping,并进行了返回。

制作完成的ParameterMapping的结构如下图代码所示,参数id对应的javaType类型为 java.lang.Object,对应的TypeHander处理器为UnknownTypeHandler,也就是未找到合适的TypeHandler的兜底选项。

接下来流程就会流转到Executor, org.apache.ibatis.executor.SimpleExecutor#doQuery进行查询时,会根据当前的SQL类型,生成对应的statmentHandler,因为我们目前都是用的预编译SQL,因此生成的statementHandler就是PrepareStatmentHandler,熟悉JDBC的小伙伴应该马上可以猜到这对应的语句是什么类型了。接下来就会对这句SQL语句进行填充,如下图代码所示,会通过PrepareStatmentHandler的parameterize方法对Statment进行参数化,也就是进行填充过程。

在PreparseStatmentHandler进行参数化时,会将参数化的职责交给DefaultParameterHandler进行,如下图代码所示,主要关注红线部分,首先会获取parameterMapping对应的TypeHander,如上章节所示,获取到的是UnknownTypeHandler,然后会通过setParameter方法,将参数id替换成对应的值。

在typehandler的流程里,首先会进入BaseTypeHandler,然后在具体设置时,进入子类的方法,在UnknownTypeHandler,首先会再次对parameter进行解析,判断最正确的TypeHandler类型,如下图代码所示:

在resolveTypeHandler方法中,因为已知参数值的类型,通过Integer这个class在typeHandlerRegistry中寻找对应的TypeHandler,TypeHandlerRegistry是Mybatis启动时内置好的,java对象类型和TypeHandler的映射关系,有兴趣的可以进这个类详细看下,在本案例中,会直接获取到IntegerHandler,如下图代码所示:

在获取到IntegerHandler后,就可以使用IntegerTypeHandler的setInt方法,对SQL语句中的参数进行替换,如下图代码所示,sql语句被成功替换。

后续就是执行SQL并处理返回结果,不在本文的讨论范围内,从上文的分析中,我们可以了解到,在3.2.3及以下版本,Mybatis会忽略parmeterType,在真正进行sql转换时,重新根据sql方法入参类型计算合适的TypeHandler处理器,所以本案例中的代码在3.2.3时运行时正常的。

3.3 以版本3.2.4为例,相比版本3.2.3,mybatis构建SQL语句过程的变化分析

在3.2章节中,得知mybatis是在运行sql阶段重新计算参数对应的TypeHandler进行sql参数替换,那么在版本3.2.4中,mybatis做了什么改动,导致了原有的使用方式不可用了呢。从官方的release log来看,版本3.2.4做了这样一个改动。

This version builds the binding information during startup and the "parameterType" attribute is used

意思是说 parameterType会在框架运行阶段就被使用到,从这个中,我们将分析的重点放在构建阶段,同时负责处理绑定关系的BoundSql由配置阶段的SqlSource生成,因此主要查看SqlSource的构建,3.2.4发生了什么变化,如下图所示。与3.2.3不同,3.2.4首先判断了是否为动态SQL,在非动态SQL情况下,将parameterType java.lang.String作为参数,传入了SqlSource的构造方法。

后续流程与3.2.3一致,因为parameter类型为java.lang.String,在构建parameterMapping时,使用的类型就是java.lang.String。

因为在框架初始化阶段,SqlSource中 parameterMapping, id对应的类型就是java.lang.String,导致在进行Sql语句替换时,获取到的TypeHandler是StringTypeHandler,如下图所示:

后面的报错原因就比较好理解了,在调用StringTypeHandler的setString方法时,报出了java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String的错误。

4、总结

总结一下这个案例的主要原因是:

mybatis 3.2.3版本 兼容parameterType和实际参数类型不匹配,运行时动态计算值处理器类型,在大版本升级2个版本号后,parameterType开始生效,以parameterType作为参数的实际类型进行TypeHandler的获取计算,导致类型不匹配时,强转报错。

带给我自己的在后续编写编写代码及系统上线方面的启示是:

1.在统一pom升级时,需要线下进行全面回归,避免框架存在不兼容的用法,导致线上错误。

2.开发同学可以检查自己系统内的mybatis版本,如果是3.2.4以下,需要全面检查下现在的mapper文件里 对于parameterType的使用 和实际的参数类型是否一致,避免升级到3.2.4及以上版本时发生兼容报错,如果有不匹配的情况存在,需要进行修正 或者 不使用parameterType,让Mybatis在运行SQL时自动计算对应的类型,

3.可以考虑使用mybatis-generator来自动生成xml和mapper文件,有专业团队维护,相对来说稳定性更好,也避免自己手动修改xml文件容易带来误操作。

4.可以主动关注强依赖的一些开源框架的Release log,有很多重要的信息。

5、作者简介

岑凯伦、90后软件工程师、5年服务端开发经验 微信公众号: KailunTalk,知识星球:IT编程成长。

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • SegmentFault 讲师
  • 获得 207 次点赞
  • 获得 14 枚徽章 获得 1 枚金徽章, 获得 1 枚银徽章, 获得 12 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • Leetcode_Learning

    目前是将目前Leetcode的所有算法题的 Java和Python版本 独立制作完成

注册于 2017-01-01
个人主页被 22.9k 人浏览