Effective java第三版笔记

阅读全文 »

Spring Boot整体概述

Spring Boot是对Spring Framework的二次封装, 重要是为了解决Spring Framework中配置, 环境的部署等琐碎问题, 而只需专注与业务开发

Spring Framework是一个开源的java开发框架, 它的核心是IOCAOP, 可以更容易地构建出企业级java应用

Spring Boot如何做到帮助开发者省略/简化配置

  1. 约定大于配置: Spring Boot对日常开发中比较常见的场景提供了默认配置
  2. 场景启动器starter: Spring Boot对常用场景进行了整合, 将这些场景中所需的依赖都收集整理到一个依赖的
  3. 嵌入式web容器: Spring Boot在运行时可以不依赖外部的Web容器, 而是使用内嵌嵌入式Web容器来支撑应用运行

Spring Boot的自动装配

Web容器包括Tomcat, JettyServlet容器以及Netty等非堵塞Web容器在内的所有能部署Web项目的应用服务器

Spring FrameWork配置第三方组件有两种方式, 1. 通过配置文件; 2. 编写注解配置类. 这个过程被称为组件装配

自动装配: 框架自动根据项目中整合的场景依赖, 做出判断并装配合适的Bean到IOC容器中

Spring Boot的IOC容器

问题

  1. SPI

Go的问题

  1. itoa是个什么
    就是一个常量计数器, 只能用于const中. 我觉得没啥用, 一点通用性都没有, 作用也非常有限. 忘记它都可以, java中有这样的东西吗? 好像是有的, 叫什么生成器., 但这个有些用, 也具有通用性.
  2. intuint有什么用, 已经有int8, int16, int32, int64uint, uint8, uint16, uint32, uint64
    intuint应该是在程序内部自己使用的, 如果要调用外部的接口, 也就是别人包里面的东西, 肯定只能用确定位数的数字, 否则大小有可能不匹配, 会吗. go编译的时候应该也会把别人包里面的东西一起编译吧, 会吗? 那go没哟编译好的库吗? 所有的东西都要自己编译吗? java是怎么引入jar包的呢? go语言的包管理是真的乱, 下载的包的位置, 找包的路径, go get这个东西又一直在变, go install又是什么东西, 下载了包为什么还要自己编译, 真是吐了. java如何使用jar包呢. javago对于编译成包, 和使用包都有些复杂, 复杂吗? 学习这些语言时, 好像都不关心这些东西? 真的不重要吗?
  3. “如果是在函数外部定义, 那么将在当前包的所有文件中都可以访问”. 包是类或者类的集合, 包中还可以包含包, 包用于组织类或者结构.
  4. go语言中有声明和定义和区别吗? java有吗? go区分函数的声明和定义吗? 现在go函数的定义不需要出现使用之前
  5. go语言不存在未初始化的值, 无论是包级别的变量, 还是函数内部的, 都会被赋零值. 那怎么区分不存在和零值呢? 例如输入空字符串和不输入字符串
  6. 短变量声明只能用于函数内部吗? 为什么不能用于包级别呢? 有没有什么区别?
  7. 当短变量声明对某个变量进行赋值时, 这个变量在函数内还是函数外, 会决定这个是赋值, 还是声明, 这可能会导致不熟悉go语言的人写出bug. java没有这种赋值和定义同时进行的操作, 只能同时定义, int a = 10, b = 20;
  8. 为什么go的可运行程序的第一个非注释行必须是package main? go语言中package与文件路径无关, 那它有什么用呢?
  9. go语言允许返回局部变量的地址, 且这个地址是有效的, 不会被覆盖, 这个跟c语言不同, c语言地址里面的值是会被修改, 导致不可用, 这个更像java中的返回函数中new 的对象. 看来goc的实现存在明显区别
  10. java没有自带的命令行参数解析库, 需要自己去下载jar包, 这里可以借助maven
  11. 如果类型的大小为0, 例如struct{}[0]int, 可能会有相同的地址, 同时go语言对于大小为0的结构的处理还存在问题, 那为什么不修呢
  12. new只是一个预定义的函数, 不是一个关键字, 我们因此可以将new名字重新定义为别的类型. 这个该说是go语言的有点呢, 还是确定呢? 我认为还是缺点更大一些, 反正都有别的关键字了, 多这一个也不多啊. 也可以认为new并不重要, 毕竟可以通过返回内部变量的地址来达到同样的效果
  13. go语言的每一行不是以分号分隔的, 有一种复杂的规则来认定一行, 值得吗, 为了省一个分号
  14. go自己判断变量是在栈上分配空间还是在堆上分配空间, 不是靠new来决定的, 真是增加了理解程序的难度, 和c语言完全不同
  15. go语言中++--是语句, 不可以在++--的同时再进行赋值, 而在java中是允许的, go中这种强制行为, 感觉不灵活, 应该还是缺点更多
  16. go的元组赋值(多变量同时赋值), 比起java多了一种选择
  17. go中返回多个值的函数, 还能用说与返回值个数的变量接收, 导致和全部接收不同的结果. 简直离谱. 例如map的取值操作
  18. go语言只用常量的赋值才会发生隐式类型转换吗? java只有数值类型的赋值和计算才会发生隐式转换, 什么是隐式转换呢?
  19. go语言中type的用法是type 类型名字 底层类型, 与c语言的顺序相反, type就是为了给类型一个别名, 就是为了增加代码可读性吗, 还可以防止类型混用
  20. 打印时的%v, %s, %g有什么区别, 为什么会有这么多种打印一个类型值的方式
  21. float64(1.0)1.0虽然底层类型时相同的, 但是一个是命名了, 一个是未命名. 未命名的可以用于赋值给 相同底层数据结构的变量(运算符都是支持的)
  22. 直接命名基本数据类型, 还是得到运算操作, 只能说聊生于无吧
  23. go语言中一个目录下只能有一个包, 也就是说package xxx, xxx必须相同, xxx还会作为别的包使用这个包时带的前缀, 同一个包中可以不带前缀. 感觉跟java的包还是有些不同的
  24. go moudle语言对于包导入, 不再依赖$GOPATH, 那它从那里找包呢? go mod文件定义了一个模块名, 导入包时, 以这个模块名开头的路径, 会把不带模块头的路径作为相对路径, 从go mod文件所在文件夹, 开始找包. 感觉比java指定类路径的方式复杂了
  25. go包的初始化, 外部变量循环依赖怎么办, 内部包级变量循环依赖怎么办? 感觉比java的复杂
  26. 一个程序可能包含多个同名的声明, 只要它们在不同的词法域就没有关系. 这个跟c语言是一样的
  27. go语言中if语句的判断条件中可以定义变量, 而且这个变量还可以在整个ifelse语句体中使用, 是一个全新的写法, 之前没有任何一种编程语言使用过这种方式, 有增加了复杂度
  28. go语言中包级别的变量按照依赖顺序进行解析和初始化, 局部变量只会按照执行顺序初始化
  29. go语言的main函数的参数是空, 更javac语言不一样, 如果要使用命令行参数, 就得使用go自带的一个库
  30. go语句只有局部变量会检查是否被使用吗, 包级变量不会吗?
  31. go语言把数据类型分成了四类, 基础数据类型, 复合类型, 引用类型和接口类型. 字符串为什么被归为基础数据类型呢, 它不是一个数组吗. 数组为什么是复合类型, 不是引用类型?, 指针不是一个值吗, 为什么不是基础类型呢, 接口类型不是引用类型吗? go的数据分类的根据是什么呢, 真是看不懂
  32. go语言中intuint的大小, 不仅和编译器, 还和平台有关系, 我们根本不知道它的大小, 我们该怎么使用它呢. 这是不是说明go编译的程序, 无法在32位和64位上同时运行, 需要同时编译, uintptr的大小也跟平台有关, 是不是用到了, 就得在特定的机器上编译呢? 那有点麻烦, 但是还好, 不用修改源码, 重新编译就行
  33. 为什么runeint32作为底层, byteint8作为底层, 一个用有符号数, 一个用符号数, 明明只是一个编码, 不关注数据的正负
  34. go语言有<<>>, 用的是逻辑位移还是算术位移呢, c的呢, 反正java<<<>>>来做区分. go语言中, 在x<<n中, n必须是无符号数, 为什么呢, 正的有符号数不行吗? go语言的>>是算术左移, 但可以通过类型转换来实现首位的填充值, java只有有符号数所以用了两种位移来实现
  35. go语言的&^是啥运算符, 叫做AND NOT, 位清空, 有什么用呢, 别的语言都没有? 通过与, 或, 非, 应该也可以实现吧
  36. go语言中%只能用于整数运算, c语言和java语言也是一样吗? 其实也不用管这些细枝末节的运算细节, 编程中几乎用不到, 通常只会将%用于正整数, 这时这个行为在任何编程语言中都是一致的, 不会有什么差别, 其他的细节, 记还容易记错
  37. go语言多次import一个包, 这个包会被多次初始化吗? 我认为只初始化一次比较合理, 不然没办法共享变量了
  38. go语言中len函数返回的是一个有符号类型, 有符号类型使逆序遍历时, 不会转换为最大的正整数而发生panic. 其实也对无符号数, 太容易变成最大的正整数了, 非常容易出错, 这有可能也是java只用有符号数的原因吧, 所以只在数值的大小没用的时候才使用无符号数, 例如位运算
  39. go语言中, rune变量存的是符号的码表值, 还是经过UTF-8编码后的值呢? 应该是码表值. java中也是一样
  40. go语言除以float变量的0值, 居然不报错, 真是离谱, 但是直接除以0.0又会报错, 就离谱
  41. go语言短变量声明的默认类型为intfloat64
  42. go语言true不会隐式转换为1, false也不会隐式转换为0, 反之亦然, java也是这样
  43. go语言中字符串是不可变的, 跟java是一样的, 为什么它们都是不可变的呢? 我只能想到一个原因共享提升性能
  44. go中字符串的len返回的是字节的个数, java中返回的字符的个数
  45. 字符串中汉字的自然编码顺序等于拼音的顺序吗? 应该是不等于, 有些非常少用的汉字, 拼音靠前, 不会在65536个常用字符中, 常用字符的编码应该是很小的
  46. go中字符串不能简单的看成字符的数组, 字符串是不可变的, 而数组是会相互影响的
  47. go的字符串支持[]来取值, java不支持[], 但有charAt函数
  48. go支持`扩起来的原生字符串, java支持吗
  49. UTF-8大小为14字节, 那对于4位的unicode码点, 是不是有可能放不下呢?
  50. go语言字符串不是rune的数组, 直接就是UTF-8编码好的字节流,, 跟java完全不一样. 那怎么取第几个字节呢? 使用utf8.DecodeRuneInString来解码, 或使用range, range自动就是按字符读取的, 那我就想取第i个字符呢? 可以将string转为rune数组, 这丫就可以取了, 感觉go语言使用string还是好没必要, 几乎每什么用. 那要向java那样使用字符串, 还要利用string来赋值, 再转乘[]rune来使用, rune可以用字符字面量, unicode码点, 对于小于256的字符, 还可以使用\x, 好像每啥用, string可以字符串字面量, UTF-8字节序列, 和unicode码点序列
  51. go语言和c语言都有scanf, 但java没有, 但java应该有从字符串提取出值的办法
  52. go中每种常量的潜在类型都是基础类型, 那是说只要潜在类型为基础类型的, 就能是常量吗?
  53. goconsttype都可以使用括号来同时定义多个
  54. go中常量间的所有算术运算, 逻辑运算和比较运输的结果也是常量, 对于常量的类型转换操作或部分函数也是常数, 常数就是提升了一点可读性和性能, 好像也没有什么大作用
  55. go中通过%T来打印类型, java可以通过打印类名
  56. go语言常量也一些简化的赋值方式, 还有一个作用相当有限的itoa关键字来赋值, 感觉没有什么必要
  57. go中字面量称为无类型常量, 无类型常量可以自己转为声明的类型, 如果没有声明类型, 就是go定义的规则指定的类型, 算术运算也是同理, 这跟cjava中字面量有默认的类型不同
  58. 零大小的数组有什么用? 零大小的接口和数据结构对于go语言好像有bug
  59. govar a [3]intvar b [4]int, ab是不同类型, 不能相互赋值, 在c语言中也是一样. 但在java中长度不会导致数组类型不一样, 而且定义数组变量时, 也没有指定长度. java中数组怎么初始化的呢? 会有默认值吗
  60. go语言中数组的相等判断非常奇怪, 只有数据类型, 长度, 且数据类型能比较的情况下, 数组才能比较, 否则会报错. 当数据长度, 且数据值相等时, 才相等. 能比较的数据结构, 不是只有基础数据结构, 结构体支持吗
  61. go语言传递数组时, 是会挨个复制数组值的, 这个跟cjava都是不一样的. 可以说是多了一种防止修改数组的方式
  62. go对指向数组的指针是可以直接便利的, ptr[i](*ptr)[i]的效果是一样的, 在rangeptr(*ptr)也是一样的, 这很离谱
  63. go语言数组和slice的语法很难区分, 只有定义的时候, slice[]不写值, 而数组是需要指定大小或者.... lencap既可以运用于数组, 也可以用于slice. s[i:j]切片操作中j可以超过len, 但不能超过cap, 真的无语, 切片操作可以用在数组和slice上, 切出来是slice
  64. go语言的slice不支持比较, java的是支持的, go语言面向对象的特性太差了. 是怕slice自己引用自己吗, java不也有这种问题吗, 那为什么java可以比较.
  65. 不要用可变类型作为mapkey, 无论是go还是java, key如果变了, hash没变会导致位置不变, 如果引用了同一个key, 会导致判断不出key已经存在, 或者相等. 放入map后, keyhash值应该是不会在变了, 不然还要watch, key的值变没有, 太消耗性能了, 这只是一个推测还没有自我验证过.
  66. 这本书说了两个理由, 都无法说明goslice为啥不能比较, 吐了
  67. slice只能和nil比较, 可以用[]int(nil)nil来赋值给以slice变量, nil可以赋值给指针和引用类型, 接口类型算引用类型吗, 结构体可以用nil赋值吗
  68. golencap为0的slice, 就可以为nil, 也可以不为nil. 为nilsclie底层没有引用数组, gonil有什么用呢???
  69. goprintf怎么用的, 数组怎么打印, 类型怎么打印
  70. go语言中copy函数就只能用于数组的复制吗? 能使用的范围太小了吧,append也只能用于slice
  71. go语言中var x, y []int可以声明多个slice, 那声明多个指针呢? 指针能var x, y *int这么写吗
  72. go语言支持可变参数, java支持吗? 可变参数可以用slice来实现, 那直接用slice来传参数不行吗, 就为了调用函数时写起来好看吗
  73. go程序设计语言没有教delete的用法, 无语, 增删改查, 不应该是最重要的吗, go中数组删除是自己用分片来实现吗? delete是用来删除map中的元素的
  74. go语言中cap有必要让go语言使用者感知到吗? 真不知道有什么用, java是没有让使用者感知到的
  75. gomapkey必须是支持==的, 可是go中结构体支持==吗, 我认为应该必须得支持, 不然map也太鸡肋了, 确认了, 结构体是支持==
  76. go中在map中取值时, 如果key不存在, 返回的是零值, 其实也只能这样, 总不能报错吧
  77. go中不能对map中的value取地址, value是会移动, 地址可能会随时失效. 地址的抽象程度太低了, 很容易受别的因素变化
  78. go语言中map的零值是nil, 这时map没有引用任何哈希表, nilslice和空的slice几乎没有差别, nilmap无法添加值, 毕竟append函数是有能力自己创建新slice
  79. go语言中map是无法比较的, 除了和nil, 这个是和slice是一致的. javaListMap是能比较的吗
  80. go语言中len函数可以运用于map
  81. go中0个值的聚合体有什么用呢?
  82. go语言中点操作符可以用于指向结构体的指针, 这是什么骚操作, 直接用->不好吗, 这样很容易把结构体和结构体指针弄混, go语言中指针是不能用->, 好像都没有这个操作符.
  83. go中调用函数返回的是值, 并不是一个可取地址的变量. 头一次在编程语言中听说这种说法. c语言会有这种问题吗? 应该会有吧, 没有验证过
  84. go中定义结构体时, 成员的顺序不同, 会导致定义的结构体不同, 那这种不同体现在哪里呢? 不是有类型名称来做区分了吗? 简直无法理解
  85. go语言中结构体的导出, 也是根据首字母的大小写来判断的, 导出能在包外访问, 没有导出的, 只能在包内访问
  86. go语言中一个命名为S的结构体类型不能再包含S类型的成员. 但是S类型的结构体可以包含*S指针类型的成员, cjava是怎样的呢
  87. go中为什么结构体字面量可以取地址, 而整数不行? 看不懂看不懂
  88. go语言中结构体的比较, 限制好大, 而且非常不灵活. go要求结构体的所有成员都必须可以比较, 而且必须全部相等, 这比java差太多了
  89. go的匿名成员机制, 也没有啥用呀, 就是为了使用成员时不用写出中间的成员. 如果有多个相同类型的成员呢? 而且结构体的命名还会影响导出, 又是一个鸡肋的东西. 唯一的用处可以获得匿名成员的方法, 其实可以把匿名成员当成继承来用
  90. 编程语言中第一类值或第一公民, 指的是能被赋值, 拥有类型的类型, go中函数是有类型的, c也是, java中函数只能放在class中, 不能被赋值啥的
  91. go中函数值的零值是nil, 当调用nil时会发生panic, 同时函数值是不能比较的, 只能和nil比较. 那是不是go中每种类型都要记一下能不能比较呢, 感觉容易出现混乱. 现在不能比较的是slice, map, 函数
  92. 匿名函数可以访问外部函数的变量, 有点像java的匿名类, 当访问外部变量这种用法, 还是有些高级, 不知到该用在什么地方, 不用这种高级用法, 用传参数可以吗
  93. 闭包是什么意思, go中函数值也被称为闭包, 闭包是一种很底层的概念了吧
  94. go程序设计语言举的例子都不简单, 真的垃圾
  95. go中函数值使用外部的变量的时, 非常容易出现bug, 使用所有值都是地址, 又是一个坑呀
  96. goPrintf是可变参数吗, 为什么它可以任意类型呢? 自己可变参数只能是同一类型的, Printf用的是interface{}类型, 这个类型有点像java中的object
  97. 可变参数可以用slice来实现, 那多一种语法其实也没有什么必要, 可以看作是一种语法糖
  98. go语言中defer的作用跟finally差不多, 但是使用的位置是不同的, 同时defer调用的顺序是相反的. defer有一个很独特的操作func ()(), 它只会在函数结束后调用最后一个函数, 前面的函数会在使用defer的地方被调用. 这就实现了, 使用前调用和使用后调用的结果, 很像aop, defer能实现的东西finally也能实现, 但是defer的写法更好看一些
  99. 什么是异常呢? 就是调用者本应该遵守规则去使用某个函数, 但使用时却违反规则去使用. 什么时候使用err, 调用者遵守了规则, 但是实现者发现因为某种原因, 无法给出正确的结果. 说到底, 就是谁负责.
  100. go中结构体的成员和方法共用一个命名空间, 就是成员和方法的名称不能相同. java中成员方法应该是可以同名的吧
  101. go中方法可以被声明到任意类型, 只要不是一个指针或者一个interface, interface不能有方法我能理解, 毕竟自己定义时已经写了, 为什么指针也不能有方法呢? 不明白, 也许设计如此吧
  102. go中调用方法是不是有两种方式, 一种是对象.语法, 另一种是函数类似的调用, 用第一参数作为对象, 应该是有两种的. 为什么有两种呢? 第一种是第二种的语法糖, 这是我认为的, 其实也是非常合理的
  103. go语言中指针可以调用类型的方法, 类型也可以调用指针的方法前提是它是变量, 不是字面量, 那为什么地址字面量可以调用类型的方法呢? 到底什么是字面量啊), 这当中go会存在隐式转换, 语法糖太多了, 让初学者很困惑, 垃圾. 如果指针和类型, 都声明了同名称方法, 这样会有什么后果, 或者这样是在同一个命名中的. 应该是在同一个命名空间中的吧. 验证出来, 是在同一个命名空间中的, 这样挺合理, 不然就太混乱了
  104. go语言允许类型的nil来调用方法, 跟java不一样, java会报空指针异常的
  105. go中结构体除了能内嵌类型, 还能内嵌指针, 内嵌指针有什么用呢? 其实没有什么区别, 除了利用指针可以共享同一个对象.
  106. go中除了匿名函数, 还有匿名结构, 但是匿名结构没有地方也方法, 不像java匿名类可以在块中写方法. 但是go匿名结构中匿名成员能带来一些方法, 算是一种有效的弥补手段吧. 匿名函数和匿名结构, 一般什么时候会用呢? 一个函数或结构只会被用一次时, 可以懒得取方法, 还是非常有用的, 取名字可太麻烦了
  107. go中接口是隐式实现的, java中需要自己写明, 这两个有什么区别呢? 隐式可以让旧的类实现新实现的方, 但是java可以用适配器模式来实现, 说到底, 其实也没有啥区别是吗? 就是稍微方便点
  108. 面向接口编程是指导思想, LSP是面向接口编程的实现的要求和规则
  109. gofor循环对于string有特殊的操作, slicestring也有特殊操作, []byte("霄剑")可以直接变成UTF8编码的字节slice
  110. go中结构体和接口都有内嵌语法, 还有别的类型有这种语法吗?
  111. go值接收器和指针接收器有很多不同的, 太坑了. 既然值接收器会生成一个指针接收器的方法, 那为啥用指针类型调用方法时, 还要编译器来解引用呢? 不是已经生成了指针接收器的方法了吗? 搞不懂
  112. go语言中匿名结构体可以有相同名称的方法, 例如String方法, 如果有两个匿名结构体都有String方法, 编译会报错, 这个有点像多继承带来的问题
  113. go语言结构体字面量可以取地址, c语言不行, 我认为这应该又是go语言的语法糖
  114. go中接口是可以比较的, 但是接口的动态类型不能比较的话, 会报panic, 还有一个问题, 接口比较有什么用呢? 这又是什么骚操作
  115. go中实现排序需要实现三个方法, java中只需要实现一个方法, c语言中是怎么实现的呢?
  116. go中类型断言和转型的语法很像. x.(T)T(x), 真的像. 类型断言中x只能是接口, T可以是接口或者类型. 当xnil时, 会报panic. 类型断言是反多态的, 在java中是非常不值得推荐的. go却非常推荐, 我觉得应该时不值得推荐的, 毕竟违反了多态,当然特例情况也是存在的, 具体类型有更好解决方案时, 可以使用断言
  117. 子类型多态和非参数多态(可识别联合), 子类型多态是抽象, 隐藏细节, 而非参数多态是将接口强制转成特定类型, 那在java中是不是把object接收值再转成各种类型, 只能说很像, 这种做法面向对象不是不提倡吗? 也不是面向对象编程的使用接口的目的, 真是各种用法都有, 只要有用就行. 这种用法在go语言中有一种语法来支持, 叫类型开关, x.(type), java中有类似的语法吗?
  118. 类型开关在单一类型的case内部, 才会进行类型转换, 在其他情况下都是没有转换之前的值. 行为挺奇怪的

  • 为什么要学习设计模式
  • 如何评价代码的好坏
  • 什么是设计原则, 常用的设计原则有哪些
  • 面向对象, 设计原则, 设计模式, 编程规范, 重构有何关系
  • 什么是面向对象编程和面向对象编程语言
  • 如何判定一门语言是否是面向对象编程语言
  • 面向对象编程和面向对象编程之间的关系
  • 什么是面向对象分析和面向对象设计
  • 面向对象的四大特性是什么, 它们的作用分别是什么
  • 什么是面向过程编程和面向过程编程语言
  • 面向对象编程相比面向过程有哪些优势
  • 有在开发过程中, 有哪些操作是在进行面向过程编程
  • 为什么在面向对象编程时, 容易写出面向过程的代码
  • 面向过程编程就真的没有用了吗
  • 抽象类和接口有什么区别? 分别是为了解决什么问题
  • 如何用抽象类和普通类模拟接口
  • 如何决定该用抽象类还是接口
  • 如何理解”基于接口而非实现编程”中”接口”二字
  • 如何定义一个好的接口
  • 是否需要为每个类定义接口
  • 为什么不推荐继承
  • 什么是基于贫血模型的开发模式
  • 什么是基于充血模型的开发模式
  • 什么是重放攻击
  • 如何面向对象分析和设计
  • 如何判断类的职责是否足够单一
  • 判断类的职责是否单一的几个技巧
  • 类的职责是否设计得越单一越好
  • 如何理解”对扩展开放, 修改关闭”
  • 如何理解”里氏替换”
  • “里氏替换”的目的
  • 如何理解”接口隔离原则”
  • “接口隔离原则”的三种应用
  • 什么是”控制反转”
  • 什么是”依赖注入”
  • 什么是”依赖注入框架”
  • 怎么理解”依赖反转原则”
  • 什么是KISS原则和YANGI原则
  • 如何提高代码复用性
  • 什么是”高内聚”和”松耦合”
  • “高内聚”和”松耦合”之间的关系
  • 什么是”迪米特法则”
  • 如何实践”高内聚”和”松耦合”
  • 为什么要重构, 重构的对象是什么, 什么时候进行重构
  • 什么是单元测试, 为什么要编写单元测试
  • 什么是代码的可测试性, 常见的测试性不友好的代码
  • “解耦”为什么非常重要
阅读全文 »

保障集群内节点和网络安全

pod中使用宿主节点的Linux命名空间

部分pod(特别是系统pod)需要在宿主节点的默认命名空间中运行, 以允许它们看到和操作节点级别的资源和设备.

绑定宿主节点上的端口而不使用宿主节点的网络命名空间

这可以通过配置podspec.containers.ports字段中某个容器某一个端口的hostPort属性来实现.

如果一个pod绑定了宿主节点上的一个特定端口, 每个宿主节点只能调度一个这样的pod实例, 因为两个进程不能绑定宿主机上的同一个端口. 调度器在调度pod时会考虑这一点, 所以它不会把这样的两个pod调度到同一个节点上.

1
2
3
4
5
6
7
8
spec:
containers:
- image: some/image
name: <pod的名称>
ports:
- containerPort: 8080
hostPort: 9000
protocol: TCP

hostPort功能最初是用于暴露通过DeamonSet部署在每个节点上的系统服务的.

配置节点的安全上下文

安全上下文中可配置的内容:

  • 指定容器中运行进程的用户(用户ID)
  • 阻止容器使用root用于运行(容器的默认运行用户通常在其镜像中指定, 所以可能需要阻止容器以root用户运行)
  • 使用特权模式运行容器, 使其对宿主节点的内核具有完全的访问权限
  • 与以上相反, 通过添加或禁用内容功能, 配置细粒度的内核访问权限
  • 设置SELinux选项, 加强对容器的限制
  • 阻止进程写入容器的根文件系统

使用指定用户运行容器

1
2
3
4
5
6
7
spec:
containers:
- name: main
image: alpine
command: ["/bin/sleep", "999999"]
securityContext:
runAsUser: 405

阻止容器以root用户运行

1
2
3
4
5
6
7
spec:
containers:
- name: main
image: alpine
command: ["/bin/sleep", "999999"]
securityContext:
runAsNonRoot: true

使用特权模式运行pod

1
2
3
4
5
6
7
spec:
containers:
- name: main
image: alpine
command: ["/bin/sleep", "999999"]
securityContext:
privileged: true

为容器单独添加内核功能

相比于让容器运行在特权模式下以给予其无限的权限, 一个更加安全地做法是只给予它使用真正需要的内核功能的权限. Kubernetes允许为特定的容器添加内核功能, 或禁止部分内核功能, 以允许对容器进行更加细致的权限控制, 限制攻击者潜在侵入的影响.

1
2
3
4
5
6
7
8
spec:
containers:
- name: main
image: alpine
command: ["/bin/sleep", "999999"]
securityContext:
add:
- SYS_TIME

在容器中禁用内核功能

1
2
3
4
5
6
7
8
spec:
containers:
- name: main
image: alpine
command: ["/bin/sleep", "999999"]
securityContext:
drop:
- CHOWN

阻止对容器跟文件系统的写入

1
2
3
4
5
6
7
spec:
containers:
- name: main
image: alpine
command: ["/bin/sleep", "999999"]
securityContext:
readOnlyRootFilesystem: true

容器使用不同用户时共享存储卷

1
2
3
4
spec:
securityContext:
fsGroup: 555
supplementalGroups: [666, 777]

限制pod使用安全相关的特性

PodSecurityPolicy资源介绍

PodSecuriyPolicy是一种集群级别(无命名空间)的资源, 它定义了用户能否在pod中使用各种安全相关的特性. 维护PodSecurityPolicy资源中配置策略的工作由集成在API服务器中的PodSercurityPolicy准入控制插件完成.

PodSecurityPolicy可以做的事

  • 是否允许pod使用宿主节点的PID, IPC, 网络命名空间
  • pod允许绑定的宿主节点的端口
  • 容器运行时允许使用的用户ID
  • 是否允许拥有特权模式容器的pod
  • 允许添加哪些内核功能, 默认添加哪些内核功能, 总是禁用哪些内核功能
  • 允许容器使用哪些SELinux选项
  • 容器是否允许使用可写的根文件系统
  • 允许容器在哪些文件系统组下运行
  • 允许pod使用哪些类型的存储卷
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
apiVersion: extensions/v1beta1
kind: PodSecurityPolicy
metadata:
name: default
spec:
hostIPC: false
hostPID: false
hostNetwork: false
hostPorts:
- min: 10000
max: 11000
- min: 13000
max: 14000
privileged: false
readOnlyRootFilesystem: true
runAsUser:
rule: RunAsAny
fsGroup:
rule: RunAsAny
supplementalGroups:
rule: RunAsAny
seLinux:
rule: RunAsAny
volumes:
- '*'

了解runAsUser, fsGroupsupplementGroup策略

如果需要限制容器可以使用的用户和用户组ID, 可以将规则改为MustRunAs, 并指定允许使用的ID范围.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
runAsUser:
rule: MustRunAs
ranges:
- min: 2
max: 2
fsGroup:
rule: MustRunAs
ranges:
- min: 2
max: 10
- min: 20
max: 30
supplementalGroups:
rule: MustRunAs
ranges:
- min: 2
max: 10
- min: 20
max: 30

如果pod spec试图将其中的任一字段设置为该范围之外的值, 这个pod将不会被API服务器接收.

部署镜像中用户ID在指定范围之外的pod

PodSecurityPolicy会将硬编码覆盖到镜像中的用户ID.

runAsUser字段中使用mustRunAsNonRoot规则

runAsUser字段中还可以使用另一种规则: mustRunAsNonRoot. 它将阻止用户部署以root用户运行的容器. 在这种情况下, spec容器中必须指定runAsUser字段, 并且不能为0, 或者容器的镜像本身指定了一个用非0的用户ID运行.

配置允许, 默认添加, 禁止使用的内核功能

1
2
3
4
5
6
7
8
9
10
apiVersion: extensions/v1beta1
kind: PodSecurityPolicy
spec:
allowedCapabilities
- SYS_TIME
defaultAddCapabilities
- CHOWN
requiredDropCapabilities
- SYS_ADMIN
- SYS_MODULES

对不同的用户与组分配不同的PodSecurityPolicy

PodSecurityPolicy是集群级别的资源, 这意味着它不能存储和应用在某一特定的命名空间上. 对不同用户分配不同PodSecurityPolicy是通过RBAC机制实现的. 这个方式是, 创建你需要的PodSecurityPolicy资源, 然后创建ClusterRole资源并通过名称将它们指向不同的策略, 以此使PodSecurityPolicy资源中的策略对不同的用户或组生效. 通过ClusterRoleBinding资源将特定的用户组绑定到ClusterRole上, 当PodSecurityPolicy访问控制插件需要决定是否接纳一个pod时, 它只会考虑创建pod的用户可以访问到的PodSecurityPolicy中的策略.

隔离pod的网络

一个NetworkPolicy会应用在匹配它的标签选择器的pod上, 指明这些允许访问这些pod的源地址, 或这些pod可以访问的目标地址. 这些分别由入向ingress和出向egress规则指定. 这两种规则都可以匹配由标签选择器选出的pod, 或者一个namespace中的所有pod, 或者通过无类别域间路由(CIDR)指定的IP地址段.

在一个命名空间中阻止所有访问

1
2
3
4
5
6
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
spec:
podSelector:

空的标签选择器匹配命名空间中的所有pod.

允许同一命名空间中的部分pod访问一个服务端pod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: postgres-netpolicy
spec:
podSelector:
matchLabels:
app: database
ingress:
- from:
- podSelector:
matchLabels:
app: webserver
ports:
- port: 5432

在不同Kubernetes命名空间之间进行网络隔离

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: shoppingcart-netpolicy
spec:
podSelector:
matchLabels:
app: shopping-cart
ingress:
- from:
- namespaceSelector:
matchLabels:
tenant: manning
ports:
- port: 80

使用CIDR隔离网络

1
2
3
4
ingress:
- from:
- ipBlock:
cidr: 192.168.1.0/24

限制pod的对外访问流量

1
2
3
4
5
6
7
8
9
spec:
podSelector:
matchLabels:
app: webserver
egress:
- to:
- podSelector:
matchLabels:
app: database

ConfigMapSecret

向容器传递命令行参数

Docker中定义命令与参数

容器中运行的完整指令由两部分组成, 命令与参数.

了解ENTRYPOINTCMD

ENTRYPOINT定义容器启动时被调用的可执行程序.

CMD指定传递给ENTRYPOINT的参数.

了解shellexec形式的区别

shell形式–如ENTRYPOINT node app.js.

exec形式– 如ENTRYPONT ["node", "app.js"].

两者的区别在于指定的命令是否在shell中被调用, 确定PID为1的进程是谁.

Kubernetes中覆盖命令和参数

Kubernetes中定义容器时, 镜像的ENTRYPOINTCMD均可以被覆盖, 仅需要在容器定义中设置属性commandargs的值.

1
2
3
4
5
6
kind: Pod
spec:
containers:
- image: some/image
command: ["/bin/command"]
args: ["args1", "args2", "args3"]

绝大多数情况下, 只需要设置自定义参数. 命令一般很少被覆盖, 除非针对一些未定义ENTRYPINT的通用镜像, 例如busybox.

commandargs字段在pod创建后无法被修改.

为容器设置环境变量

Kubernetes允许为pod中每一个容器都指定自定义的环境变量集合.

与容器的命令和参数设置相同, 环境变量列表无法在pod创建后被修改.

在容器定义中指定环境变量

1
2
3
4
5
6
7
8
kind: Pod
spec:
containers:
- image: luksa/fortune:env
env:
- name: INTERVAL
value: "30"
name: html-generator

在每个容器中, Kubernetes会自动暴露相同命名空间下每个service对应的黄金变量. 这些环境变量基本上可以被看作自动注入的配置.

了解硬编码环境变量的不足之处

pod定义硬编码意味着需要有效区分生产环境与开发过程中的pod定义. 为了能在多个环境下复用pod的定义, 需要将配置从pod定义描述中解耦出来.

利用ConfigMap解耦配置

应用配置的关键在于能够在多个环境中区分配置选项, 将配置从应用程序源码中分离, 可频繁变更配置值.

ConfigMap介绍

Kubernetes允许将配置选项分离到单独的资源对象ConfigMap中, 本质上就是一个键/值对映射, 值可以是短字面量, 也可以是完整的配置文件.

应用无须直接读取ConfigMap, 甚至根本不需要知道其是否存在. 映射的内容通过环境变量或者卷文件的形式传递给容器, 而非直接传递给容器. 命令行参数的定义中可以通过$(ENV_VAR)语法引用环境变量, 因而可以达到将ConfigMap条目当作命令行参数传递给进程的效果.

当然, 应用程序同样可以通过Kubernetes Rest API按需直接读取ConfigMap的内容. 不过除非是需求如此, 应尽可能使你的应用保持对Kubernetes的无感知.

创建ConfigMap

使用指令kubectl创建ConfigMap

1
kubectl create configmap <configMap的名字> --from-literal=<键>=<值>

键只能包含数字, 字母, 破折号, 下画线以及圆点. --from-literal参数用于指定多个条目.

1
2
3
4
5
6
apiVersion: v1
kind: ConfigMap
metadata:
name: <configMap的名字>
data:
<键>: <值>

从文件内容创建ConfigMap条目

ConfigMap同样可以存储粗粒度的配置数据, 比如完整的配置文件.

1
kubectl create configmap <configmap的名字> --from-file=<配置文件名>

运行上述命令时, kubectl会在当前目录下查找配置文件, 并将文件内容存储在ConfigMap中以配置文件名为键名的条目下. 也可以手动指定键名

1
kubectl create configmap <configmap的名字> --from-file=<键>=<配置文件名>

从文件夹创建ConfigMap

1
kubectl create configmap <configmap的名字> --from-file=/path/to/dir

这种情况下, kubectl会为文件夹中的每个文件单独创建条目, 仅限于那些文件名可作为合法ConfigMap键名的文件.

给容器传递ConfigMap条目作为环境变量

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Pod
metadata:
name: <Pod名称>
spec:
containers:
- image: some/image
env:
- name: <环境变量名>
valueFrom:
configMapKeyRef:
name: <configMap的名称>
key: <configMap的值>

引用不存在ConfigMap的容器会启动失败. 当之后创建了这个缺失的ConfigMap, 失败的容器会自动启动, 无须重新创建pod.

可以标记对ConfigMap的引用时可选的(设置configMapKeyRef.optional: true). 这样, 即便ConfigMap不存在, 容器也能正常启动.

一次性传递ConfigMap的所有条目作为环境变量

1
2
3
4
5
6
7
spec:
containers:
- image: some-image
envFrom:
- prefix: CONFIG_
configMapRef:
name: <configMap的名称>

前缀设置是可选的, 若不设置前缀值, 环境变量的名称与ConfigMap中的键名相同.

传递Configmap条目作为命令行参数

在字段pod.spec.containers.args中无法直接引用ConfigMap的条目, 但是可以利用ConfigMap条目初始化某个环境变量, 然后再在参数字段中引用该环境变量.

使用ConfigMap卷将条目暴露为文件

环境变量或者命令行参数值作为配置值通常适用于变量值较短的场景.

configMap卷会将ConfigMap中每个条目均暴露成一个文件. 运行在容器中的进程可通过读取文件内容获得对应的条目值.

1
2
3
4
volumes:
- name: config
configMap:
name: <configMap的名称>

卷内暴露指定的ConfigMap条目

1
2
3
4
5
6
7
volumes:
- name: config
configMap:
name: <configMap的名称>
items:
- key: <configMap中的键>
path: <cofigMap中键的新名称>

ConfigMap独立条目作为文件被挂载且不隐藏文件夹中的其他文件

假设拥有一个包含文件myconfig.confconfigMap卷, 希望能将奇添加为/etc文件夹下的文件someconfig.conf. 通过属性subPath可以将该文件挂载的同时又不影响文件夹中的其他文件.

1
2
3
4
5
6
7
spec:
containers:
- image: some/image
volumeMounts:
- name: myvolume
mountPath: /etc/someconfig.conf
subPath: myconfig.conf

挂载任意一种卷时均可以使用subPath属性. 可以选择挂载部分卷而不是挂载完整的卷. 捕获这种独立文件的挂载方式会带来文件更新上的缺陷.

configMap卷中的文件设置权限

configMap卷中所有文件的权限默认被设置为644. 可以通过卷规格定义中的defaultMode属性改变默认权限.

1
2
3
4
5
volumes:
- name: config
configMap:
name: <configMap的名称>
defaultMode: "6600"

更新应用配置且不重启应用程序

使用环境变量或命令行参数作为配置源的弊端在于无法在进程运行时更新配置. 将ConfigMap暴露为卷可以达到配置热更新的效果, 无须重新创建pod或者重启容器.

ConfigMap被更新之后, 卷中引用它的所有文件也会相应更新, 进程发现文件被改变之后进行重载(这要进程自己实现).

挂载至已存在文件夹的文件不会更新

如果挂载的是容器中的单个文件而不是完整的卷, ConfigMap更新之后对应的文件不会被更新.

如果现在你需要挂载单个文件并且在修改源ConfigMap的同时会自动修改这个文件, 一种方案是挂载完整卷至不同的文件夹并创建指向文件的符号链接. 符号链接可以原生创建在容器镜像中, 也可以在容器启动时创建.

使用Secret给容器传递敏感数据

SecretConfigMap相似, 均是键/值对的映射, 但用于保存一些敏感数据.

默认令牌Secret介绍

每个pod都会默认自动挂载上一个Secret卷, 引用一个默认的Secret.

为二进制数据创建Secret

采用Base64编码的原因很简单. Secret的条目可以涵盖二进制数据, 而不仅仅是纯文本. Base64编码可以将二进制数据转换为纯文本, 以YAMLJSON格式展示.

Secret甚至可以被用来存储非敏感二进制数据, Secret的大小限于1MB.

stringData字段介绍

由于并非所有敏感数据都是二进制形式, Kubernetes允许通过SecretstringData字段设置条目的纯文本值.

1
2
3
4
5
6
7
apiVersion: v1
kind: Secret
stringData:
fool: plain text
data:
https.cert: LSOtLSAAA
https.key: LSOtLSAAA

通过kubectl get -o yaml查看Secret时, stringData会出现在data字段.

pod中读取Secret条目

通过secret卷将Secret暴露给容器之后, Secret条目的值会被解码并以真实形式(纯文本或二进制)写入对应的文件. 通过环境变量暴露Secret条目也是如此. 应用程序无序主动解码, 可直接读取文件内容或查找环境变量.

Secret卷存储于内存

Secret卷采用内存文件系统列出容器的挂载点, 由于使用的是tmpfs, 存储在Secret中的数据不会写入磁盘, 这就无法被窃取.

通过环境变量暴露Secret条目

1
2
3
4
5
6
env:
- name: FOO_SECRET
valueFrom:
secretKeyRef:
name: <secret的名字>
key: <secret的键>

kubernetes允许通过环境变量暴露Secret, 然而此特性的使用往往不是一个好注意. 应用程序通常会在错误报告时转储环境变量, 或者是在启动时打印在应用日志中, 无意中暴露了Secret信息.

0%