Qida's Blog

纸上得来终觉浅,绝知此事要躬行。

在随手科技这几年,兼任了金融前端团队的负责人,将团队从只有 1 名前端开始,扩展到了 10 多人的前端团队,推动了整个金融前端技术栈的建设及发展,是一段挑战自己未知领域的有趣之旅。

我总结了一下,这几年团队共经历了这样几个阶段:

  • 单块项目(服务端渲染) > 项目分层(前后端分离) > 项目拆分(按业务拆分)
  • 前端模块化 > 组件化 > 工程化
  • 事件驱动 > 数据驱动
  • 后台选型:JSP 服务端渲染 + EasyUI > 前后端分离 + Element
  • 浏览器 > 服务端

下面分几个阶段总结下。

阶段1 服务端渲染

2016 年之前,由于团队和项目规模所限,人员构成以后端开发为主、前端开发为辅(就一个前端开发),只能通过最基础的技术栈,以后端人员最熟悉的技术着手进行业务开发并快速上线,因此技术选型都是偏向服务端的:前端只需按照设计师要求切图并输出静态页面(HTML + CSS),加上一些基础的 ES5 实现所需的动画效果和基础交互效果,后端套成 JSP (或 freemarker velocity thymeleaf)进行服务端渲染。后端开发一般会这样解决问题:

  • 通过 SiteMesh 等框架在 JSP 中将网页内容和页面结构分离,以达到页面结构共享的目的;
  • 通过 tld 文件自定义标签,给 JSP 页面提供一些便捷工具(如货币、时间、字符串格式化);
  • 对于一些复杂的页面交互逻辑,在 JSP 页面上通过 <script> 标签直接引用所需的 JavaScript 文件。

作为后端开发人员会觉得:这么写代码也没什么问题啊,毕竟身边的同事都是这么写的,项目也跑得好好的。但问题在于,后端开发写 JS 都是很业余的,而且随着功能越做越多,业务越做越深,前端脚本开始变得难以扩展与维护:

  • 脚本间依赖关系脆弱,加载顺序需要手工维护,一不小心顺序乱了就 JS 报错;
  • 脚本中潜藏着各种全局变量(函数),导致命名冲突、作用域污染,没有合理的进行前端模块化;
  • 各页面没有主入口脚本,代码不知从何看起……

阶段2 前端模块化

2016 年初,我着手重构前端的第一件事就是将前端模块化。

JavaScript 这门语言(或者说老版本 ES5),最为糟糕的地方就是基于全局变量的编程模型(如何避免使用全局变量?),并且由于不支持“类”与“模块”,使得稍具规模的应用都难以扩展。

一番对比和调研 AMD 和 CMD 规范的相关框架之后,第二阶段决定引入 Require.js(英文中文)这个前端框架。Require.js 以模块化的方式组织前端脚本,通过 AMD 规范定义的两个关键函数 define()require() ,我们可以很轻松的在老版本 ES5 上实现模块化功能,解决模块依赖、全局变量与命名冲突的问题,并提供了统一的脚本主入口。

Require.js 入门教程参考此前博文

阶段3 项目分层(前后端分离)

前端模块化虽然提升了项目的可维护性,但由于此阶段前后端项目仍然强耦合,项目和团队仍存在以下问题:

  • 前端完成的 HTML 页面需交付给后端转换为 JSP 页面,多一道无谓的工序。更重要的是,后续前端任何页面修改,都需要通知后端进行同步修改,操作繁琐且易出错;
  • 由于 JSP 页面由后端编写,后端开发如果觉悟不够或者贪图方便,在 JSP 页面中各种 JavaScript 代码信手拈来、Java 变量和 JS 变量混用,导致前后端难以解耦、代码后续难以维护;
  • 后端开发无法专注于业务开发,大量精力浪费于编写前端样式及脚本,分工不明确、不专业。

更为重要的是,当下前端领域日新月异,ES 新版本、层出不穷的新框架,SPA 单页技术、CSS 预处理语言、前端性能优化、自动化构建…… 受限于项目结构、迫于后端人员能力,新技术无法推广落地,前端人员能力也无法完全施展。

2016 年中,我开始渐进式的推动前后端分离,为了不让步子太大扯着蛋,前端主体技术栈仍采用 HTML + Require.js + Zepto,后台采用 EasyUI,并重点解决下面两类问题:

引入自动化构建工具

传统的前端是无需构建的:前端开发编写的 HTML、JS、CSS 可以直接运行在浏览器中,代码所见即所得。但这种传统方式也带来了以下问题:

  • 无法根据不同环境构建代码,解决各环境间的差异。例如不同环境下资源引用路径是不同的,生产往往会使用 CDN 域名;
  • HTML 页面之间各种代码重复,例如一些全局 rem 设置、全局变量、事件,公共样式、脚本、页面布局,提升了维护成本;
  • JS 脚本没被检查(如静态语法分析),团队协作时代码规范程度无法保证;没有单元测试,潜藏缺陷容易直接流到生产环境;
  • CSS 样式无法扩展、浏览器兼容性问题处理复杂(如需手工添加厂商前缀);
  • 静态资源没被合并、压缩,体积大、数量多,导致用户请求慢;
  • 静态资源没被 hash,带来版本管理和缓存问题,更新困难;
  • 静态资源需手工打包上传,操作繁琐;

为了解决这些问题,这个阶段我引入了自动化构建工具 Gulp.js,一些使用实践请参考此前博文。对于一个前端新手来说,这是一个很大的思维转变,自动化构建极大提升前端项目的工程能力,“构建”阶段能够实现很多之前无法实现的效果。

引入 CSS 预处理语言

前后端分离后,由于引入构建工具,前端开发能够自由发挥的空间更多了,这个阶段我们还引入了 less,一门 CSS 预处理语言,提升了编写前端样式的效率。

API 接口设计

前后端分离的另一个重点,在于数据与页面交互方式的改变——服务端渲染 > 前端渲染。因此定义一套统一的 API 接口规范尤其重要。这个阶段我解决掉的问题:

  • 跨域方案选型:代理、JSONP、CORS,平衡了浏览器兼容性和开发便利性最终采用 CORS 方案;
  • 接口规范:编写后端 API 网关层框架,大一统全公司项目的接口入参、出参规范及处理流程;
  • 文档管理:前期手工编写 Markdown 文档 > 后期使用 SwaggerUI 自动生成文档;
  • 搭建 API Mock Server,前期 gulp-mock-server > 后期 RAP Mock Server ,大大提升前后端并行开发效率。

阶段4 项目拆分

2017 年开始,随着业务做大(新业务越做越多,每周还搞各种运营活动)、人员增多,原来的一两个前端项目已经不能满足快速增长的需求了。这个阶段浮现出来的新问题:

  • 人员多、特性多,由于只有几个前端项目,并行开发时 git 分支难以管理,代码合版时容易发生冲突;
  • 测试环境当时只有两套,测试时容易发生代码被覆盖的问题,特性间不好并行测试;
  • 生产发版风险较大,出问题时只能整体回滚,粒度太大,影响前端项目内的其它正常特性。

为了解决上述问题,2017 年我们按业务、活动两个维度进行了项目分拆:

  • 业务
    • 帐户项目
    • 非标项目
    • 基金项目
    • XX 项目 …
  • 活动
    • 首投活动
    • 邀请活动
    • XX 活动 …

各业务、各项目分而治之,由专门的前端组长统筹、排期、开发、发版,满足各业务的个性化需求及节奏差异。

为了进一步提升开发效率,解决模块及组件的复用问题,这个阶段还:

  • 引入了新版 ES6 + Babel 编译器提升 JavaScript 开发效率;
  • 引入了 MV* 库 Vue.js + 自动化构建工具 Webpack,解决之前的 DOM 节点操作 + 事件驱动机制的开发效率低的问题。
  • 引入了 NPM 包管理器,搭建团队专属的仓库(控件库 + 组件库),提升代码复用性。

阶段5 重回服务端渲染

前端技术近年来日新月异,目前 Node.js 的应用已经铺天盖地,Node.js 中间层的出现改变了前后端的合作模式,各大公司前端都把 Node.js 作为前后端分离的新手段,并且在测试、监控等方面沉淀了大量内容。

2018 年起,前端团队也开始在预研 Node.js 技术、搭建各类基础库并尝试在生产中投入使用。以史为鉴,展望未来,只要我们有不断突破自我的勇气,一定能克服困难,让新技术在公司中落地开花,进一步提升团队的开发效率,为公司创造更大的价值。

待续。

参考

Web前后端分离开发思路

前后端分离后的契约

什么是基于数据驱动的前端框架?

Apache Commons 是一个 Apache 项目,专注于可重用 Java 组件的方方面面。

Apache Commons 项目由三个部分组成:

Apache Commons

其中,Apache Commons Lang 是 Java 开发过程中很常用的一个类库,可以理解为它是对 Java Lang and Util Base Libraries 的增强。

Commons Lang 安装方法:

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>x.x.x</version>
</dependency>

Commons Lang 总览:

Commons Lang

Commons Lang 提供了以下 package:

  • org.apache.commons.lang3
  • org.apache.commons.lang3.builder
  • org.apache.commons.lang3.concurrent
  • org.apache.commons.lang3.event
  • org.apache.commons.lang3.exception
  • org.apache.commons.lang3.math
  • org.apache.commons.lang3.mutable
  • org.apache.commons.lang3.reflect
  • org.apache.commons.lang3.text
  • org.apache.commons.lang3.text.translate
  • org.apache.commons.lang3.time
  • org.apache.commons.lang3.tuple
    • PairMutablePairImmutablePair
    • TripleMutableTripleImmutableTriple

下面重点来看下最常用的几个工具:

org.apache.commons.lang3

  • StringUtils
  • ArrayUtils
  • BooleanUtils

StringUtils

StringUtils

判空函数

API:

1
2
3
4
5
6
7
8
9
StringUtils.isEmpty(String str)
StringUtils.isNotEmpty(String str)
StringUtils.isBlank(String str)
StringUtils.isNotBlank(String str)
StringUtils.isAnyBlank(CharSequence… css)
StringUtils.isAnyEmpty(CharSequence… css)
StringUtils.isNoneBlank(CharSequence… css)
StringUtils.isNoneEmpty(CharSequence… css)
StringUtils.isWhitespace(CharSequence cs)

看下 isBlankisEmpty 的区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
StringUtils.isBlank(null) // true
StringUtils.isEmpty(null) // true

StringUtils.isBlank("") // true
StringUtils.isEmpty("") // true

StringUtils.isBlank(" ") // true
StringUtils.isEmpty(" ") // false

StringUtils.isBlank("\n\t") // true
StringUtils.isEmpty("\n\t") // false

isNotEmpty = !isEmpty, isBlank同理

使用 isAnyBlankisAnyEmpty 进行多维判空:

1
2
3
4
StringUtils.isAnyBlank("", "bar", "foo"); // true
StringUtils.isAnyEmpty(" ", "bar", "foo"); // false

isNoneBlank = !isAnyBlank;isNoneEmpty同理

使用 isWhitespace 判断空白:

1
2
3
StringUtils.isWhitespace(null); // false
StringUtils.isWhitespace(""); // true
StringUtils.isWhitespace(" "); // true

判断是否相等函数

API:

1
2
equals(CharSequence cs1,CharSequence cs2)
equalsIgnoreCase(CharSequence str1, CharSequence str2)

例子:

1
2
3
StringUtils.equals("abc", null)  = false
StringUtils.equals("abc", "abc") = true
StringUtils.equals("abc", "ABC") = false

忽略大小写判断:

1
2
3
StringUtils.equalsIgnoreCase("abc", null)  = false
StringUtils.equalsIgnoreCase("abc", "abc") = true
StringUtils.equalsIgnoreCase("abc", "ABC") = true

是否包含函数

API:

1
2
3
4
5
6
containsOnly(CharSequence cs,char… valid)
containsNone(CharSequence cs,char… searchChars)

startsWith(CharSequence str,CharSequence prefix)
startsWithIgnoreCase(CharSequence str,CharSequence prefix)
startsWithAny(CharSequence string,CharSequence… searchStrings)

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//判断字符串中所有字符,是否都是出自参数2中
StringUtils.containsOnly("ab", "") = false
StringUtils.containsOnly("abab", "abc") = true
StringUtils.containsOnly("ab1", "abc") = false
StringUtils.containsOnly("abz", "abc") = false

//判断字符串中所有字符,都不在参数2中。
StringUtils.containsNone("abab", 'xyz') = true
StringUtils.containsNone("ab1", 'xyz') = true
StringUtils.containsNone("abz", 'xyz') = false

//判断字符串是否以第二个参数开始
StringUtils.startsWith("abcdef", "abc") = true
StringUtils.startsWith("ABCDEF", "abc") = false

索引下标函数

API:

1
2
3
4
indexOf(CharSequence seq,CharSequence searchSeq)
indexOf(CharSequence seq,CharSequence searchSeq,int startPos)
indexOfIgnoreCase/lastIndexOfIgnoreCase(CharSequence str,CharSequence searchStr)
lastIndexOf(CharSequence seq,int searchChar)

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//返回第二个参数开始出现的索引值
StringUtils.indexOf("aabaabaa", "a") = 0
StringUtils.indexOf("aabaabaa", "b") = 2
StringUtils.indexOf("aabaabaa", "ab") = 1

//从第三个参数索引开始找起,返回第二个参数开始出现的索引值
StringUtils.indexOf("aabaabaa", "a", 0) = 0
StringUtils.indexOf("aabaabaa", "b", 0) = 2
StringUtils.indexOf("aabaabaa", "ab", 0) = 1
StringUtils.indexOf("aabaabaa", "b", 3) = 5
StringUtils.indexOf("aabaabaa", "b", 9) = -1

//返回第二个参数出现的最后一个索引值
StringUtils.lastIndexOf("aabaabaa", 'a') = 7
StringUtils.lastIndexOf("aabaabaa", 'b') = 5

StringUtils.lastIndexOfIgnoreCase("aabaabaa", "A", 8) = 7
StringUtils.lastIndexOfIgnoreCase("aabaabaa", "B", 8) = 5
StringUtils.lastIndexOfIgnoreCase("aabaabaa", "AB", 8) = 4
StringUtils.lastIndexOfIgnoreCase("aabaabaa", "B", 9) = 5

截取函数

API:

1
2
3
4
5
substring(String str,int start)
substringAfter(String str,String separator)
substringBeforeLast(String str,String separator)
substringAfterLast(String str,String separator)
substringBetween(String str,String tag)

例子:

1
2
3
4
5
6
//start>0表示从左向右, start<0表示从右向左, start=0则从左第一位开始
StringUtils.substring("abcdefg", 0) = "abcdefg"
StringUtils.substring("abcdefg", 2) = "cdefg"
StringUtils.substring("abcdefg", 4) = "efg"
StringUtils.substring("abcdefg", -2) = "fg"
StringUtils.substring("abcdefg", -4) = "defg"

// start>0&&end>0从左开始(包括左)到右结束(不包括右),

//start<0&&end<0从右开始(包括右),再向左数到end结束(包括end)

substring

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//从第二个参数字符串开始截取,排除第二个字符串
StringUtils.substringAfter("abc", "a") = "bc"
StringUtils.substringAfter("abcba", "b") = "cba"
StringUtils.substringAfter("abc", "c") = ""

//从最后一个字母出现开始截取
StringUtils.substringBeforeLast("abcba", "b") = "abc"
StringUtils.substringBeforeLast("abc", "c") = "ab"
StringUtils.substringBeforeLast("a", "a") = ""
StringUtils.substringBeforeLast("a", "z") = "a"

StringUtils.substringAfterLast("abc", "a") = "bc"
StringUtils.substringAfterLast("abcba", "b") = "a"
StringUtils.substringAfterLast("abc", "c") = ""

StringUtils.substringBetween("tagabctag", null) = null
StringUtils.substringBetween("tagabctag", "") = ""
StringUtils.substringBetween("tagabctag", "tag") = "abc"

删除函数

API:

1
2
3
4
5
6
7
8
StringUtils.remove(String str, char remove)
StringUtils.remove(String str, String remove)
StringUtils.removeEnd(String str, String remove)
StringUtils.removeEndIgnoreCase(String str, String remove)
StringUtils.removePattern(String source, String regex)
StringUtils.removeStart(String str, String remove)
StringUtils.removeStartIgnoreCase(String str, String remove)
StringUtils.deleteWhitespace(String str)

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//删除字符
StringUtils.remove("queued", 'u') = "qeed"

//删除字符串
StringUtils.remove("queued", "ue") = "qd"

//删除结尾匹配的字符串
StringUtils.removeEnd("www.domain.com", ".com") = "www.domain"

//删除结尾匹配的字符串,找都不到返回原字符串
StringUtils.removeEnd("www.domain.com", "domain") = "www.domain.com"

//忽略大小写的
StringUtils.removeEndIgnoreCase("www.domain.com", ".COM") = "www.domain")

//删除所有空白(好用)
StringUtils.deleteWhitespace("abc") = "abc"
StringUtils.deleteWhitespace(" ab c ") = "abc"

删除空白函数

API:

1
2
3
4
trim(String str)
trimToEmpty(String str)
trimToNull(String str)
deleteWhitespace(String str)

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
StringUtils.trim("     ")       = ""
StringUtils.trim("abc") = "abc"
StringUtils.trim(" abc ") = "abc"

StringUtils.trimToNull(" ") = null
StringUtils.trimToNull("abc") = "abc"
StringUtils.trimToNull(" abc ") = "abc"
StringUtils.trimToEmpty(" ") = ""
StringUtils.trimToEmpty("abc") = "abc"
StringUtils.trimToEmpty(" abc ") = "abc"

StringUtils.deleteWhitespace("") = ""
StringUtils.deleteWhitespace("abc") = "abc"
StringUtils.deleteWhitespace(" ab c ") = "abc"

替换函数

API:

1
2
3
4
5
6
7
8
9
replace(String text, String searchString, String replacement)
replace(String text, String searchString, String replacement, int max)
replaceChars(String str, char searchChar, char replaceChar)
replaceChars(String str, String searchChars, String replaceChars)
replaceEach(String text, String[] searchList, String[] replacementList)
replaceEachRepeatedly(String text, String[] searchList, String[] replacementList)
replaceOnce(String text, String searchString, String replacement)
replacePattern(String source, String regex, String replacement)
overlay(String str,String overlay,int start,int end)

replace 例子:

1
2
3
4
5
6
7
8
9
StringUtils.replace("aba", "a", "")    = "b"
StringUtils.replace("aba", "a", "z") = "zbz"

//数字就是替换个数,0代表不替换,1代表从开始数起第一个,-1代表全部替换
StringUtils.replace("abaa", "a", "", -1) = "b"
StringUtils.replace("abaa", "a", "z", 0) = "abaa"
StringUtils.replace("abaa", "a", "z", 1) = "zbaa"
StringUtils.replace("abaa", "a", "z", 2) = "zbza"
StringUtils.replace("abaa", "a", "z", -1) = "zbzz"

replaceEach 是对 replace 的增强版,用于一次性替换多个字符。搜索列表和替换长度必须一致,否则报 IllegalArgumentException 异常:

1
2
StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"w", "t"})  = "wcte"
StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"}) = "dcte"

replaceChars 用于对单个字符逐一替换,其操作如下:

replaceChars

1
2
StringUtils.replaceChars("dabcba", "bcd", "yzx") = "xayzya"
StringUtils.replaceChars("abcba", "bc", "y") = "ayya"

replaceOnce 例子:

1
2
StringUtils.replaceOnce("aba", "a", "")    = "ba"
StringUtils.replaceOnce("aba", "a", "z") = "zba"

overlay 例子:

1
2
3
4
5
6
StringUtils.overlay("abcdef", "zzzz", 2, 4)   = "abzzzzef"
StringUtils.overlay("abcdef", "zzzz", 4, 2) = "abzzzzef"
StringUtils.overlay("abcdef", "zzzz", -1, 4) = "zzzzef"
StringUtils.overlay("abcdef", "zzzz", 2, 8) = "abzzzz"
StringUtils.overlay("abcdef", "zzzz", -2, -3) = "zzzzabcdef"
StringUtils.overlay("abcdef", "zzzz", 8, 10) = "abcdefzzzz"

反转函数

API:

1
2
reverse(String str)
reverseDelimited(String str, char separatorChar)

例子:

1
2
3
StringUtils.reverse("bat") = "tab"
StringUtils.reverseDelimited("a.b.c", 'x') = "a.b.c"
StringUtils.reverseDelimited("a.b.c", ".") = "c.b.a"

分隔函数

API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
split(String str)
split(String str, char separatorChar)
split(String str, String separatorChars)
split(String str, String separatorChars, int max)
splitByCharacterType(String str)
splitByCharacterTypeCamelCase(String str)
splitByWholeSeparator(String str, String separator)
splitByWholeSeparator(String str, String separator, int max)
splitByWholeSeparatorPreserveAllTokens(String str, String separator)
splitByWholeSeparatorPreserveAllTokens(String str, String separator, int max)
splitPreserveAllTokens(String str)
splitPreserveAllTokens(String str, char separatorChar)
splitPreserveAllTokens(String str, String separatorChars)
splitPreserveAllTokens(String str, String separatorChars, int max)

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//用空白符做空格
StringUtils.split("abc def") = ["abc", "def"]
StringUtils.split("abc def") = ["abc", "def"]
StringUtils.split("a..b.c", '.') = ["a", "b", "c"]

//用字符分割
StringUtils.split("a:b:c", '.') = ["a:b:c"]

//0 或者负数代表没有限制
StringUtils.split("ab:cd:ef", ":", 0) = ["ab", "cd", "ef"]

//分割字符串 ,可以设定得到数组的长度,限定为2
StringUtils.split("ab:cd:ef", ":", 2) = ["ab", "cd:ef"]

//null也可以作为分隔
StringUtils.splitByWholeSeparator("ab de fg", null) = ["ab", "de", "fg"]
StringUtils.splitByWholeSeparator("ab de fg", null) = ["ab", "de", "fg"]
StringUtils.splitByWholeSeparator("ab:cd:ef", ":") = ["ab", "cd", "ef"]
StringUtils.splitByWholeSeparator("ab-!-cd-!-ef", "-!-") = ["ab", "cd", "ef"]

//带有限定长度的分隔
StringUtils.splitByWholeSeparator("ab:cd:ef", ":", 2) = ["ab", "cd:ef"]

合并函数

API:

1
2
3
join(byte[] array,char separator)
join(Object[] array,char separator)
join(Object[] array,char separator,int startIndex,int endIndex)

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//只有一个参数的join,简单合并在一起
StringUtils.join(["a", "b", "c"]) = "abc"
StringUtils.join([null, "", "a"]) = "a"

//null的话,就是把字符合并在一起
StringUtils.join(["a", "b", "c"], null) = "abc"

//从index为0到3合并,注意是排除3的
StringUtils.join([null, "", "a"], ',', 0, 3) = ",,a"
StringUtils.join(["a", "b", "c"], "--", 0, 3) = "a--b--c"

//从index为1到3合并,注意是排除3的
StringUtils.join(["a", "b", "c"], "--", 1, 3) = "b--c"
StringUtils.join(["a", "b", "c"], "--", 2, 3) = "c"

大小写转换和判断

API:

1
2
3
4
5
6
7
8
9
10
StringUtils.capitalize(String str)
StringUtils.uncapitalize(String str)
StringUtils.upperCase(String str)
StringUtils.upperCase(String str,Locale locale)
StringUtils.lowerCase(String str)
StringUtils.lowerCase(String str,Locale locale)
StringUtils.swapCase(String str)

StringUtils.isAllUpperCase(CharSequence cs)
StringUtils.isAllLowerCase(CharSequence cs)

大小写转换:

  • capitalize 首字母大写
  • upperCase 全部转化为大写
  • swapCase 大小写互转

大小写判断:

  • isAllUpperCase 是否全部大写
  • isAllLowerCase 是否全部小写

缩短省略函数

API:

1
2
3
abbreviate(String str, int maxWidth)
abbreviate(String str, int offset, int maxWidth)
abbreviateMiddle(String str, String middle, int length)

例子:

1
2
3
4
5
6
7
8
9
StringUtils.abbreviate("abcdefg", 6) = "abc..."
StringUtils.abbreviate("abcdefg", 7) = "abcdefg"
StringUtils.abbreviate("abcdefg", 8) = "abcdefg"
StringUtils.abbreviate("abcdefg", 4) = "a..."
StringUtils.abbreviate("abcdefg", 3) = IllegalArgumentException

StringUtils.abbreviate("abcdefghijklmno", 6, 10) = "...ghij..."

StringUtils.abbreviateMiddle("abcdef", ".", 4) = "ab.f"

字符串的长度小于或等于最大长度,返回该字符串。

运算规律:(substring(str, 0, max-3) + “…”)

如果最大长度小于 4,则抛出异常 IllegalArgumentException

相似度函数

API:

1
difference(String str1,String str2)

例子:

1
2
3
4
5
6
7
8
//在str1中寻找str2中没有的的字符串,并返回     
StringUtils.difference("", "abc") = "abc"
StringUtils.difference("abc", "") = ""
StringUtils.difference("abc", "abc") = ""
StringUtils.difference("abc", "ab") = ""
StringUtils.difference("ab", "abxyz") = "xyz"
StringUtils.difference("abcde", "abxyz") = "xyz"
StringUtils.difference("abcde", "xyz") = "xyz"

difference

BooleanUtils

BooleanUtils

ArrayUtils

添加方法

add(boolean[] array,boolean element)
add(T[] array,int index,T element)
addAll(boolean[] array1,boolean… array2)

1
2
3
4
5
6
7
8
//添加元素到数组中        
ArrayUtils.add([true, false], true) = [true, false, true]

//将元素插入到指定位置的数组中
ArrayUtils.add(["a"], 1, null) = ["a", null]
ArrayUtils.add(["a"], 1, "b") = ["a", "b"]
ArrayUtils.add(["a", "b"], 3, "c") = ["a", "b", "c"]
ArrayUtils.add(["a", "b"], ["c", "d"]) = ["a", "b", "c","d"]

克隆方法

1
ArrayUtils.clone(new int[] { 3, 2, 4 }); = {3,2,4}

包含方法

contains(boolean[] array,boolean valueToFind)

1
2
// 查询某个Object是否在数组中
ArrayUtils.contains(new int[] { 3, 1, 2 }, 1); = true

获取长度方法

getLength(Object array)

1
ArrayUtils.getLength(["a", "b", "c"]) = 3

获取索引方法

indexOf(boolean[] array,boolean valueToFind)
indexOf(boolean[] array,boolean valueToFind,int startIndex)

1
2
3
4
5
6
7
8
9
10
//查询某个Object在数组中的位置,可以指定起始搜索位置,找不到返回-1
//从正序开始搜索,搜到就返回当前的index否则返回-1
ArrayUtils.indexOf(new int[] { 1, 3, 6 }, 6); = 2
ArrayUtils.indexOf(new int[] { 1, 3, 6 }, 2); = -1

//从逆序开始搜索,搜到就返回当前的index,否则返回-1
ArrayUtils.lastIndexOf(new int[] { 1, 3, 6 }, 6); = 2

//从逆序索引为2开始搜索,,搜到就返回当前的index,否则返回-1
ArrayUtils.lastIndexOf(new Object[]{"33","yy","uu"}, "33",2 ) = 0

判空方法

isEmpty(boolean[] array)等等
isNotEmpty(T[] array)

1
2
3
//判断数组是否为空(null和length=0的时候都为空)
ArrayUtils.isEmpty(new int[0]); = true
ArrayUtils.isEmpty(new Object[] { null }); = false

长度相等判断方法

isSameLength(boolean[] array1,boolean[] array2)

1
2
//判断两个数组的长度是否相等
ArrayUtils.isSameLength(new Integer[] { 1, 3, 5 }, new Long[] { "1", "3", "5"}); = true

空数组转换

nullToEmpty(Object[] array)等等

1
2
3
//讲null转化为相应数组
int [] arr1 = null;
int [] arr2 = ArrayUtils.nullToEmpty(arr1);

删除元素方法

remove(boolean[] array,int index)等等
removeElement(boolean[] array,boolean element)
removeAll(T[] array,int… indices)
removeElements(T[] array,T… values)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//删除指定下标的元素        
ArrayUtils.remove([true, false], 1) = [true]
ArrayUtils.remove([true, true, false], 1) = [true, false]

//删除第一次出现的元素
ArrayUtils.removeElement([true, false], false) = [true]
ArrayUtils.removeElement([true, false, true], true) = [false, true]

//删除所有出现的下标的元素
ArrayUtils.removeAll(["a", "b", "c"], 0, 2) = ["b"]
ArrayUtils.removeAll(["a", "b", "c"], 1, 2) = ["a"]

//删除数组出现的所有元素
ArrayUtils.removeElements(["a", "b"], "a", "c") = ["b"]
ArrayUtils.removeElements(["a", "b", "a"], "a") = ["b", "a"]
ArrayUtils.removeElements(["a", "b", "a"], "a", "a") = ["b"]

反转方法

reverse(boolean[] array)等等
reverse(boolean[] array,int startIndexInclusive,int endIndexExclusive)

1
2
3
4
5
6
7
8
//反转数组
int[] array =new int[] { 1, 2, 5 };
ArrayUtils.reverse(array);// {5,2,1}

//指定范围的反转数组,排除endIndexExclusive的
int[] array =new int[] {1, 2, 5 ,3,4,5,6,7,8};
ArrayUtils.reverse(array,2,5);
System.out.println(ArrayUtils.toString(array)); = {1,2,4,3,5,5,6,7,8}

截取数组

subarray(boolean[] array,int startIndexInclusive,int endIndexExclusive)

1
2
3
4
//起始index为2(即第三个数据)结束index为4的数组
ArrayUtils.subarray(newint[] { 3, 4, 1, 5, 6 }, 2, 4); = {1,5}
//如果endIndex大于数组的长度,则取beginIndex之后的所有数据
ArrayUtils.subarray(newint[] { 3, 4, 1, 5, 6 }, 2, 10); = {1,5,6}

打印数组方法

toString(Object array)
toString(Object array,String stringIfNull)

1
2
3
4
5
//打印数组
ArrayUtils.toString(newint[] { 1, 4, 2, 3 }); = {1,4,2,3}
ArrayUtils.toString(new Integer[] { 1, 4, 2, 3 }); = {1,4,2,3}
//如果为空,返回默认信息
ArrayUtils.toString(null, "I'm nothing!"); = I'm nothing!

参考

https://commons.apache.org/

https://www.tutorialspoint.com/commons_collections/index.htm

https://www.tutorialspoint.com/commons_io/index.htm

RPC 框架对比

市面上的 RPC 框架功能比较:

RPC框架功能比较

协议对比

连接个数 连接方式 传输协议 传输方式 序列化 适用范围 适用场景 参考
dubbo:// 单连接 长连接 TCP NIO 异步传输 Hessian 二进制序列化 传入传出参数数据包较小(建议小于100K),消费者比提供者个数多,单一消费者无法压满提供者,尽量不要用 dubbo 协议传输大文件或超大字符串。 常规远程服务方法调用 dubbo
rmi:// 多连接 短连接 TCP 同步传输 Java 标准二进制序列化 传入传出参数数据包大小混合,消费者与提供者个数差不多,可传文件。 常规远程服务方法调用,与原生RMI服务互操作
hessian:// 多连接 短连接 HTTP 同步传输 Hessian二进制序列化 传入传出参数数据包较大,提供者比消费者个数多,提供者压力较大,可传文件。 页面传输,文件传输,或与原生hessian服务互操作 hession
http:// 多连接 短连接 HTTP 同步传输 表单序列化 传入传出参数数据包大小混合,提供者比消费者个数多,可用浏览器查看,可用表单或URL传入参数,暂不支持传文件。 需同时给应用程序和浏览器 JS 使用的服务。
webservice:// 多连接 短连接 HTTP 同步传输 SOAP 文本序列化 系统集成,跨语言调用 Apache CXF
thrift:// Thrift
memcached://
redis://
rest:// 多连接 可长可短 HTTP 同步传输 JSON/XML JAX-RS

REST 协议小结

根据 dubbox、dubbo REST 官方文档,摘录了使用上的一些注意点:

配置总览

服务提供端

1
<dubbo:protocol name="rest" server="" port="" contextpath="" threads="" iothreads="" keepalive="" accepts="" />
配置项 描述 生效范围
name 启用 REST 协议 all
server REST Server 的实现 all
port 端口号 all
contextpath 应用上下文路径 all
threads 线程池大小 jetty、netty、tomcat
iothreads IO worker线程数 netty
keepalive 是否长连接,默认为 true 长连接 netty、tomcat
accepts 最大的HTTP连接数 tomcat

目前在dubbo中支持5种嵌入式rest server的实现:

1
2
3
4
5
6
7
8
<!-- rest协议默认选用jetty。jetty是非常成熟的java servlet容器,并和dubbo已经有较好的集成(目前5种嵌入式server中只有jetty、tomcat、tjws,与dubbo监控系统等完成了无缝的集成)。 -->
<dubbo:protocol name="rest" server="jetty"/>
<!-- 在嵌入式tomcat上,REST的性能比jetty上要好得多(参见官网的基准测试),建议在需要高性能的场景下采用tomcat。 -->
<dubbo:protocol name="rest" server="tomcat"/>
<dubbo:protocol name="rest" server="netty"/>
<!-- 轻量级嵌入式server,非常方便在集成测试中快速启动使用,当然也可以在负荷不高的生产环境中使用。注意,tjws is now deprecated -->
<dubbo:protocol name="rest" server="tjws"/>
<dubbo:protocol name="rest" server="sunhttp"/>

同时也支持采用外部应用服务器来做rest server的实现:

1
2
<!-- web.xml 参考官网 -->
<dubbo:protocol name="rest" server="servlet"/>

服务消费端

如果REST服务的消费端也是dubbo系统,可以配置每个消费端的超时时间和HTTP连接数,详情参考官方文档。

REST 服务提供端

标准 Java REST API:JAX-RS

  • Dubbox 基于标准的 Java REST API——JAX-RS 2.0(Java API for RESTful Web Services 的简写),提供了接近透明的REST调用支持。由于完全兼容Java标准API,所以为dubbo开发的所有REST服务,未来脱离dubbo或者任何特定的REST底层实现一般也可以正常运行。
  • Dubbo的REST调用和dubbo中其它某些RPC不同的是,需要在服务代码中添加JAX-RS的annotation(以及JAXB、Jackson的annotation),如果你觉得这些annotation一定程度“污染”了你的服务代码,你可以考虑编写额外的Facade和DTO类,在Facade和DTO上添加annotation,而Facade将调用转发给真正的服务实现类。当然事实上,直接在服务代码中添加annotation基本没有任何负面作用,而且这本身是Java EE的标准用法,另外JAX-RS和JAXB的annotation是属于java标准,比我们经常使用的spring、dubbo等等annotation更没有vendor lock-in的问题,所以一般没有必要因此而引入额外对象。
  • JAX-RS与Spring MVC的对比:
    • JAX-RS 相对更适合纯粹的服务化应用,也就是传统Java EE中所说的中间层服务。
    • 在dubbo应用中,我想很多人都比较喜欢直接将一个本地的spring service bean(或者叫manager之类的)完全透明的发布成远程服务,则这里用JAX-RS是更自然更直接的,不必额外的引入MVC概念。
  • 就学习 JAX-RS 来说,一般主要掌握其各种 annotation 的用法即可。参考:

HTTP POST/GET 的实现

  • REST服务中虽然建议使用HTTP协议中四种标准方法POST、DELETE、PUT、GET来分别实现常见的“增删改查”,但实际中,我们一般情况直接用POST来实现“增改”,GET来实现“删查”即可(DELETE和PUT甚至会被一些防火墙阻挡)。

JSON、XML 等多数据格式的支持

  • 在一个REST服务同时对多种数据格式支持的情况下,根据JAX-RS标准,一般是通过HTTP中的MIME header(content-type和accept)来指定当前想用的是哪种格式的数据。
  • 目前业界普遍使用的方式,是使用一个URL后缀(.json和.xml)来指定想用的数据格式。比用HTTP Header更简单直观。Twitter、微博等的REST API都是采用这种方式。

定制序列化

  • Dubbo中的REST实现是用JAXB做XML序列化,用Jackson做JSON序列化,所以在对象上添加JAXB或Jackson的annotation即可以定制映射。更多资料请参考JAXB和Jackson的官方文档。
  • 由于JAX-RS的实现一般都用标准的JAXB(Java API for XML Binding)来序列化和反序列化XML格式数据,所以我们需要为每一个要用XML传输的对象添加一个类级别的JAXB annotation @XmlRootElement,否则序列化将报错。

添加自定义的 Filter、Interceptor 等

  • JAX-RS标准的 FilterInterceptor 可以对请求与响应过程做定制化的拦截处理。

添加自定义的 Exception 处理

  • JAX-RS标准的 ExceptionMapper,可以用来定制特定exception发生后应该返回的HTTP响应。

REST 服务消费端

场景1:非 dubbo 的消费端调用 dubbo 的 REST 服务(non-dubbo > dubbo)

  • 使用标准的JAX-RS Client API或者特定REST实现的Client API来调用REST服务。当然,在java中也可以直接用自己熟悉的比如HttpClient,FastJson,XStream等等各种不同技术来实现REST客户端。

场景2:dubbo 消费端调用 dubbo 的 REST 服务(dubbo > dubbo)

  • dubbo消费端调用dubbo的REST服务,这种场景下必须把JAX-RS的annotation添加到服务接口上,这样在dubbo在消费端才能共享相应的REST配置信息,并据之做远程调用。
  • dubbo的REST支持采用Java标准的bean validation annotation(JSR 303)来做输入校验。为了和其他dubbo远程调用协议保持一致,在rest中作校验的annotation必须放在服务的接口上,这样至少有一个好处是,dubbo的消费端可以共享这个接口的信息,dubbo消费端甚至不需要做远程调用,在本地就可以完成输入校验。

参考

http://dubbo.apache.org/zh-cn/docs/user/references/protocol/rest.html

https://github.com/dangdangdotcom/dubbox

https://dangdangdotcom.github.io/dubbox/rest.html

https://mvnrepository.com/artifact/com.gaosi/dubbox

Dubbox fork from Dubbo,目前只发布了一个版本:2.8.4

有了 HTTP,为什么还要 RPC?

本文主要总结 Dubbo 日常使用时的一些常用配置。

配置之间的关系

配置之间的关系

XML 配置 Java Config 配置 配置 解释
<dubbo:application/> com.alibaba.dubbo.config.ApplicationConfig 应用配置 用于配置当前应用信息,不管该应用是提供者还是消费者
<dubbo:registry/> com.alibaba.dubbo.config.RegistryConfig 注册中心配置 用于配置连接注册中心相关信息
<dubbo:monitor/> com.alibaba.dubbo.config.MonitorConfig 监控中心配置 用于配置连接监控中心相关信息,可选
<dubbo:protocol/> com.alibaba.dubbo.config.ProtocolConfig 协议配置 用于配置提供服务的协议信息,协议由提供方指定,消费方被动接受
<dubbo:provider/> com.alibaba.dubbo.config.ProviderConfig 提供方配置 ProtocolConfigServiceConfig 某属性没有配置时,采用此缺省值,可选
<dubbo:service/> com.alibaba.dubbo.config.ServiceConfig 服务配置 用于暴露一个服务,定义服务的元信息,一个服务可以用多个协议暴露,一个服务也可以注册到多个注册中心。对应注解:@Service
<dubbo:consumer/> com.alibaba.dubbo.config.ConsumerConfig 消费方配置 ReferenceConfig 某属性没有配置时,采用此缺省值,可选
<dubbo:reference/> com.alibaba.dubbo.config.ReferenceConfig 引用配置 用于创建一个远程服务代理,一个引用可以指向多个注册中心。对应注解:@Reference
<dubbo:method/> com.alibaba.dubbo.config.MethodConfig 方法配置 用于 ServiceConfigReferenceConfig 指定方法级的配置信息
<dubbo:argument/> com.alibaba.dubbo.config.ArgumentConfig 参数配置 用于指定方法参数配置
<dubbo:module/> com.alibaba.dubbo.config.ModuleConfig 模块配置 用于配置当前模块信息,可选

下面是一些 dubbo 配置的总结:

协议

服务提供者

服务消费者

配置覆盖关系

配置覆盖关系:

  • 方法级优先,接口级次之,全局配置再次之。
  • 如果级别一样,则消费方优先,提供方次之。

规则二是指,所有配置最终都将转换为 URL 表示,并由服务提供方生成,经注册中心传递给消费方。其 URL 格式如下:protocol://username:password@host:port/path?key=value&key=value

dubbo 配置覆盖关系

参考:XML 配置

属性配置关系

dubbo 属性覆盖关系

参考:属性配置

注解配置实践

如果想用现代的 Java Config 替代传统的 XML 配置方式,配置如下:

声明组件

  • 服务提供方使用 @Service 注解暴露服务

    1
    2
    3
    4
    5
    6
    import com.alibaba.dubbo.config.annotation.Service;

    @Service(timeout = 5000)
    public class AnnotateServiceImpl implements AnnotateService {
    // ...
    }
  • 服务消费方使用 @Reference 注解引用服务

    1
    2
    3
    4
    5
    6
    7
    8
    import com.alibaba.dubbo.config.annotation.Reference;

    public class AnnotationConsumeService {
    @Reference
    public AnnotateService annotateService;

    // ...
    }

开启组件扫描

  • 2.5.7 (Nov, 2017) 以上版本,使用 @DubboComponentScan 指定 dubbo 组件扫描路径
  • 老版本或 Dubbox,使用:<dubbo:annotation package="com.alibaba.dubbo.test.service" />

Java Config 配置

  • 使用 @Configuration 注解开启 Java Config 并使用 @Bean 进行公共模块的 bean 配置,参考:API配置

自动化配置

  • 最后开启 @EnableDubboConfig

参考

在 Dubbo 中使用注解

https://www.oschina.net/news/92687/dubbo-spring-boot-starter-1-0-0

https://mvnrepository.com/artifact/com.alibaba.boot/dubbo-spring-boot-starter

https://mvnrepository.com/artifact/com.alibaba/dubbo

https://github.com/apache/incubator-dubbo

本文总结的一些学习笔记,用于建立安全观。

信任域与信任边界

  • 首先,安全问题的本质,是信任。一旦我们作为决策依据的条件被打破、被绕过,那么就会导致安全假设的前提条件不再可靠,变成一个伪命题。因此,把握住信任条件的度,使其恰到好处,正是设计安全方案的难点所在,也是安全这门学问的艺术魅力所在。
  • 通过一个安全检查(过滤、净化)的过程,可以梳理未知的人或物,使其变得可信任。被划分出来的具有不同信任级别的区域,我们称为信任域,划分两个不同信任域之间的边界,我们称为信任边界
  • 因为信任关系被破坏,从而产生了安全问题。我们可以通过信任域的划分、信任边界的确定,来发现问题是在何处产生的。
  • 数据从高等级的信任域流向低等级的信任域,是不需要经过安全检查的;数据从低等级的信任域流向高等级的信任域,则需要经过信任边界的安全检查。

安全基本三要素(CIA)

  • 机密性(Confidentiality):要求保护数据内容不能泄露,常见手段是加密。
  • 完整性(Integrity):要求保护数据内容是完整、没有被篡改的。常见手段是数字签名。
  • 可用性(Availability):要求保护资源是“随需而得”。如拒绝服务攻击 (简称DoS,Denial of Service) 破坏的是安全的可用性。
  • 真实性(Authenticity):通信双方的身份确认,确保数据来源于合法的用户。
  • 不可抵赖性(Non-repudiation),防抵赖。常见手段是数字签名。

认证(Authentication):我是谁?——身份

授权(Authorization):我能做什么?——权利

凭证(Credentials):依据是什么?——依据(凭证实现认证和授权的一种媒介,标记访问者的身份或权利)

3A 黄金法则

针对各个安全环节,可以使用 3A 黄金法则:

事前防御——认证(Authentication)

事中防御——授权(Authorization)

事后防御——审计(Audit)

认证、授权技术 HTTP 请求头
基本认证 Authorization: Basic <Base64("username:password")>
摘要认证 Authorization: Digest <MD5(username, password, nonce, ...)>
JWT Authorization: Bearer <JWT Token>
OAuth Authorization: Bearer <Access Token>

认证(Authentication)

认证其实包括两个部分:身份识别和认证。

  • 身份识别其实就是在问 “你是谁?”,你会回答 “你是你”。
  • 身份认证则会问 “你是你吗?”,那你要证明 “你是你” 这个回答是合法的。

身份识别和认证通常是同时出现的一个过程。身份识别强调的是主体如何声明自己的身份,而身份认证强调的是,主体如何证明自己所声明的身份是合法的。

比如说:

  • 当你在使用用户名和密码登录的过程中,用户名起到身份识别的作用,而密码起到身份认证的作用;
  • 当你用指纹、人脸或者门卡等进行登入的过程中,这些过程同时包含了身份识别和认证。

认证形式可以大致分为三种。按照认证强度由弱到强排序,分别是:

  • 你知道什么(密码、密保问题等);
  • 你拥有什么(门禁卡、手机验证码、安全令牌、U 盾等);
  • 你是什么(生物特征,如指纹、人脸、虹膜等)。

authentication

参考:认证技术总结

授权(Authorization)

在确认完 “你是你” 之后,下一个需要明确的问题就是 “你能做什么”。

除了对 “你能做什么” 进行限制,授权机制还会对 “你能做多少” 进行限制。比如:

  • 手机流量授权了你能够使用多少的移动网络数据。
  • 我们申请签证的过程,其实就是一次申请授权的过程。

参考:OAuth 2

审计(Audit)

当你在授权之下完成操作后,安全需要检查一下 “你做了什么”,这个检查的过程就是审计。

当发现你做了某些异常操作时,安全还会提供你做了这些操作的 “证据”,让你无法抵赖,这个过程就是问责。

安全评估的四阶段

  1. 资产等级划分:对资产进行等级划分,就是对数据做等级划分。当完成划分后,对要保护的目标数据已经有了一个大概的了解,接下来就是要划分信任域和信任边界了。

  2. 威胁分析:威胁(Threat)是指可能造成危害的来源。威胁分析即把所有的威胁都找出来。可以采用头脑风暴法。或采用 STRIDE 等模型。

STRIDE

  1. 风险分析:风险(Risk)是指可能会出现的损失。风险公式:Risk = Probability * Damage Potential,即影响风险高低的因素,除了造成损失的大小外,还需要考虑到发生的可能性。可以采用 DREAD 等模型。

DREAD

  1. 确认解决方案

安全方案设计的四原则

  • 默认安全性原则(Secure by Default),最基本也是最重要的原则。即:
    • 黑、白名单。随着防火墙、ACL 技术的兴起,使得直接暴露在互联网上的系统得到了保护。比如一个网站的数据库,在没有保护的情况下,数据库服务端口是允许任何人随意连接的;在有了防火墙的保护后,通过ACL可以控制只允许信任来源的访问。这些措施在很大程度上保证了系统软件处于信任边界之内,从而杜绝了大部分的攻击来源。因此如果更多地使用白名单(如防火墙、ACL),系统就会变得更安全。
    • 最小权限原则。安全设计的基本原则之一。最小权限原则要求系统只授予主体必要的权限,而不要过度授权,这样能有效地减少系统、网络、应用、数据库出错的机会。
  • 纵深防御原则 (Defense in Depth),其包含两层含义:
    • 首先,在各个不同层面、不同方面实施安全方案,避免出现疏漏,不同安全方案之间需要相互配合,构成一个整体;
    • 其次,在正确的地方做正确的事情,即:在解决根本问题的地方实施针对性的安全方案。
  • 数据与代码分离原则
    • 适用于各种由于“注入”而引发的安全问题,如 XSS、SQL 注入、CRLF 注入、X-Path 注入。
  • 不可预测性原则(Unpredictable)
    • 能有效地对抗基于篡改、伪造(如 CSRF)的攻击,其实现往往需用到加密算法、随机数算法、哈希算法等。

总结这几条原则:

  • Secure By Default:是时刻要牢记的原则;
  • 纵深防御:是要更全面、更正确地看待问题;
  • 数据与代码分离:是从漏洞成因上看问题;
  • 不可预测性:则是从克服攻击方法的角度看问题。

参考

互联网安全的核心问题,是数据安全的问题。

《白帽子讲 Web 安全》

https://time.geekbang.org/column/intro/262

同源策略

要了解什么是同源策略,先来看下如果没有同源策略,我们将遇到什么安全问题:

假设用户正在访问银行网站并且没有退出登录。然后,用户打开新 Tab 转到另一个恶意网站,其中有一些恶意 JavaScript 代码在后台运行并请求来自银行网站的数据。由于用户仍然在银行网站登录,恶意代码可以模拟用户在银行网站上做任何事情。这是因为浏览器可以向同域的银行网站发送和接收会话cookie。

同源策略(Same Origin Policy)是一种约定,是浏览器最核心也最基本的安全功能。如果缺少了同源策略,则浏览器的正常功能可能都会受到影响,可以说 Web 是构建在同源策略的基础之上的,浏览器只是针对同源策略的一种实现。

同源策略限制了来自不同源站的“document”或脚本,对当前“document”读取或设置某些属性。受同源策略限制的元素:

  • Cookie
  • DOM
  • XMLHttpRequest
  • 第三方插件
    • Adobe Flash
    • Java Applet
    • Microsoft Silverlight
    • Google Gears
    • ……

一个域名的组成:

http://

Chrome 增强的网站隔离功能 [桌面版 / Android]

因为同源政策(Same Origin Policy)的存在,A 网站一般无法访问 B 网站存储在设备中的网站数据。不过因为安全漏洞的存在,部分恶意网站偶尔也可以绕过同源政策、获取这些文件并对其他网站进行攻击。

所以除了第一时间对各种浏览器安全漏洞进行修补,Chrome 也在早些时候引入了网站隔离功能:通过在独立进程中加载网站页面,确保网站与网站之间的数据安全性。在 Chrome 92 稳定版的桌面端,这个功能从浏览器标签页延伸到了浏览器扩展,不同扩展插件也将在独立进程中进行加载,同时几乎不会影响大部分扩展的现有功能。

出于性能考虑,网站隔离功能在 Android 版 Chrome 中一直都没有像桌面端那样全盘开启,仅在一些需要用户手动输入登录信息的网站中启用,并且仅支持拥有 2GB 及以上运行内存的设备。

本次 Chrome 92 则通过对 OAuth 2.0 协议和 Cross-Origin-Opener-Policy (COOP) 策略的额外支持扩展了网站隔离功能在移动端上的可用性和兼容性,换句话说,移动版 Chrome 现在能够识别并保护更多类型的网站了(比如采用第三方登录的那种)。

不过增强网站隔离功能在 Android 端的硬件限制依然为 2GB RAM,Google 同时也表示,如果你的设备可用内存足够,也可以通过 chrome://flags#enable-site-per-process 这一功能标签来手动开启全盘网站隔离。

2021-07-27 Updated

跨域资源共享

由于 XMLHttpRequest 受同源策略的约束,不能跨域访问资源,因此 W3C 委员会制定了 XMLHttpRequest 跨域访问标准 Cross-Origin Resource Sharing,通过目标域返回的 HTTP 头(Access-Control-Allow-Origin)来授权是否允许跨域访问。由于 HTTP 头对于 JavaScript 来说,一般是无法控制的,所以认为这个方案可以安全施行。

参考

https://en.wikipedia.org/wiki/Same-origin_policy

https://en.wikipedia.org/wiki/Cross-origin_resource_sharing

http://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html

http://www.ruanyifeng.com/blog/2016/04/cors.html

https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy

http://blog.csdn.net/wang379275614/article/details/53333054

开会是一门学问,也是管理的必经之路。这里总结一点开会的小心得,目的是提升工作效率。

如何开好会?

Spring 4.0 引入的条件化注解

假设你希望一个或多个 bean 只有在应用的类路径下包含特定的库时才创建。或者我们希望某个 bean 只有当另外某个特定的 bean 也声明了之后才会创建。我们还可能要求只有某个特定的环境变量设置之后,才会创建某个 bean。
在 Spring 4 之前,很难实现这种级别的条件化配置,但是 Spring 4 引入了一个新的 @Conditional 注解,它可以用到带有 @Bean 注解的方法上。如果给定的条件计算结果为 true,就会创建这个 bean,否则的话,这个 bean 会被忽略。

@Conditional 注解的源码如下:

1
2
3
4
5
6
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
Class<? extends Condition>[] value();
}

Condition 接口的源码如下:

1
2
3
public interface Condition {
boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2);
}

其中,通过 Condition 接口的入参 ConditionContext,我们可以做到如下几点:

  • 借助 getRegistry() 返回的 BeanDefinitionRegistry 检查 bean 定义;
  • 借助 getBeanFactory() 返回的 ConfigurableListableBeanFactory 检查 bean 是否存在,甚至探查 bean 的属性;
  • 借助 getEnvironment() 返回的 Environment 检查环境变量是否存在以及它的值是什么;
  • 读取并探查 getResourceLoader() 返回的 ResourceLoader 所加载的资源;
  • 借助 getClassLoader() 返回的 ClassLoader 加载并检查类是否存在。

环境与 profile

在开发软件的时候,有一个很大的挑战就是将应用程序从一个环境迁移到另外一个环境。开发阶段中,某些环境相关做法可能并不适合迁移到生产环境中,甚至即便迁移过去也无法正常工作。跨环境部署时会发生变化的几个典型例子:

  • 数据库配置
  • 加密算法
  • 与外部系统的集成

解决办法:

  • 在单独的 Java Config(或 XML)中配置每个 bean,然后在构建时根据不同的环境分别打包,典型方法是采用 Maven profile。这种方式的问题在于要为每种环境重新构建应用。
  • 运行时指定不同的环境变量,典型方法是采用 Spring profile bean。这种方式的好处在于无需为每种环境重新构建应用。

Spring profile bean 的使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
@Profile("dev") // 类级别(Spring 3.1 引入)
public class DevProfileConfig() {

}

@Configuration
public class GlobalConfig() {

@Bean
@Profile("dev") // 方法级别(Spring 3.2 引入)
public DataSource embeddedDataSource() {
...
}

@Bean
@Profile("prod") // 方法级别(Spring 3.2 引入)
public DataSource jndiDataSource() {
...
}

}

激活方式,使用属性:spring.profiles.activespring.profiles.default。有多种方式来设置这两个属性:

  • 作为 DispatcherServlet 的初始化参数;
  • 作为 Web 应用的上下文参数;
  • 作为 JNDI 条目;
  • 作为环境变量;
  • 作为 JVM 的系统属性;
  • 在集成测试类上,使用 @ActiveProfiles 注解设置。

注意,Spring profile bean 注解 @Profile 底层其实也是基于 @ConditionalCondition 实现:

1
2
3
4
5
6
7
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional({ProfileCondition.class})
public @interface Profile {
String[] value();
}

Spring Boot 的条件化注解

Spring Boot 没有引入任何形式的代码生成,而是利用了 Spring 4 的条件化 bean 配置特性,以及 Maven 和 Gradle 提供的传递依赖解析,以此实现 Spring 应用上下文里的自动配置。Spring Boot 实现的条件化注解如下:

Spring Boot 实现的条件化注解

Spring Boot 的 @Conditional 注解实现及相关支持类,详见文档:Package org.springframework.boot.autoconfigure.condition

依赖配置:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>

通过运行 DEBUG=true mvn spring-boot:run,可以看到 DEBUG 级别的日志输出,从而观察自动配置的详情(AUTO-CONFIGURATION REPORT):

  • Positive matches
  • Negative matches
  • Exclusions
  • Unconditional classes
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
============================
CONDITIONS EVALUATION REPORT
============================

Positive matches:
-----------------

GenericCacheConfiguration matched:
- Cache org.springframework.boot.autoconfigure.cache.GenericCacheConfiguration automatic cache type (CacheCondition)

JmxAutoConfiguration matched:
- @ConditionalOnClass found required class 'org.springframework.jmx.export.MBeanExporter' (OnClassCondition)
- @ConditionalOnProperty (spring.jmx.enabled=true) matched (OnPropertyCondition)

PropertyPlaceholderAutoConfiguration#propertySourcesPlaceholderConfigurer matched:
- @ConditionalOnMissingBean (types: org.springframework.context.support.PropertySourcesPlaceholderConfigurer; SearchStrategy: current) did not find any beans (OnBeanCondition)


Negative matches:
-----------------

ActiveMQAutoConfiguration:
Did not match:
- @ConditionalOnClass did not find required class 'javax.jms.ConnectionFactory' (OnClassCondition)

AopAutoConfiguration:
Did not match:
- @ConditionalOnClass did not find required class 'org.aspectj.lang.annotation.Aspect' (OnClassCondition)

Exclusions:
-----------

None

Unconditional classes:
----------------------

org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration

org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration

org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration

参考

Spring in Action, 4th

SpringBoot源码分析之条件注解的底层实现

工作中由于前端项目打包、各类工具使用(如本博客就是基于 Node.js)经常要用到 npm,下面总结一下。

npm 是随 Node.js 自动安装的,由三个独立的部分组成:

  • 网址:开发者查找包(package)、设置参数以及管理 npm 使用体验的主要途径。
  • 注册表(Registry):一个巨大的数据库,保存了每个包(package)的信息。
  • 命令行工具(CLI):命令行或终端运行。开发者通过 CLI 与 npm 打交道。

安装

OS X 推荐使用 HomeBrew 安装:

1
brew install node --with-npm

其它:

https://nodejs.org/zh-cn/download/

npm 命令

NPM CLI Commands

npm install -g <package> 全局安装:

  • Windows:

    • 安装目录 C:\Users\Admin\AppData\Roaming\npm\node_modules\<package>
    • 生成可执行脚本:C:\Users\Admin\AppData\Roaming\npm\<package>
    • 由于目录 C:\Users\Admin\AppData\Roaming\npm 已加入 PATH 环境变量,因为可以直接执行对应命令。
  • OS X:

    • 安装目录 /usr/local/lib/node_modules/<package>
    • 生成软链,例如:/usr/local/bin/npm -> /usr/local/lib/node_modules/npm/bin/npm-cli.js

npm list -g -depth=0 验证是否安装到位。

npm 配置

npm 配置主要有两份:

  • npmrc:npm 从命令行、环境变量、npmrc 文件中读取配置。
  • package.json:包(package)配置文件。

重点了解下 package.json,重点属性有 nameversiondependenciesmain

属性 描述
name 包名
version 包的语义版本号:X.Y.Z:如果有大变动,向下不兼容,需要更新主版本号X;如果是新增了功能,但是向下兼容,需要更新次版本号 Y;如果只是修复bug,需要更新补丁版本号 Z
description 包的描述
homepage 包的官网 url
author 包的作者姓名
contributors 包的其他贡献者姓名
dependencies 依赖包列表。如果依赖包没有安装,npm 会自动将依赖包安装在 node_module 目录下
repository 包代码存放的地方的类型,可以是 git 或 svn,git 可在 Github 上
main main 字段是一个模块ID,它是一个指向你程序的主要项目。就是说,如果你包的名字叫 express,然后用户安装它,然后require("express")
keywords 关键字

淘宝 npm 镜像

为了解决慢的问题,推荐使用淘宝 npm 镜像。三种使用方式:

  • 使用淘宝定制的 cnpm 命令行工具代替默认的 npm,安装方法:

    1
    $ npm install -g cnpm --registry=https://registry.npm.taobao.org
  • 永久配置 npm 命令行工具:

    1
    2
    3
    $ npm config set registry https://registry.npm.taobao.org
    --配置后验证是否成功
    $ npm config get registry
  • npm 安装包时,临时使用淘宝 npm 镜像:

    1
    $ npm install vue-cli --registry=https://registry.npm.taobao.org

参考

https://www.npmjs.com/

https://www.npmjs.com.cn/

https://npm.taobao.org/

http://www.runoob.com/nodejs/nodejs-tutorial.html

Spring Boot 应用中的”自动配置”是通过 @EnableAutoConfiguration 注解进行开启的。@EnableAutoConfiguration 可以帮助 Spring Boot 应用将所有符合条件的 @Configuration 配置类的 bean 都加载到 Spring IoC 容器中。本文解析了实现这个效果的原理。

自动配置的原理

Spring Boot 应用的自动配置流程如下:

Spring SPI

Spring Factories 机制

首先,注解的实现类使用了 spring-core 中的加载类 SpringFactoriesLoader 加载指定配置,详见文档

SpringFactoriesLoader loads and instantiates factories of a given type from “META-INF/spring.factories” files which may be present in multiple JAR files in the classpath. The spring.factories file must be in Properties format, where the key is the fully qualified name of the interface or abstract class, and the value is a comma-separated list of implementation class names.

该类会通过类加载器从 classpath 中搜索所有 META-INF/spring.factories 配置文件,然后获取 key 为 org.springframework.boot.autoconfigure.EnableAutoConfiguration 部分。

例如 spring-boot-autoconfigure.jar/META-INF/spring.factories

1
2
3
4
5
6
7
8
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
......

加载结果如图:

SpringFactoriesLoader 解析结果

继续加载 key 为 org.springframework.boot.autoconfigure.AutoConfigurationImportFilter 部分:

1
2
3
4
5
# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition

执行条件注解

针对这 118 个候选的 Auto Configure,执行 Import Filters 对应的条件注解:

  • OnClassCondition

    1
    2
    3
    4
    5
    6
    7
    8
    // 底层实现,加载成功则返回 true;加载失败则抛出 java.lang.ClassNotFoundException,捕获异常后返回 false
    private static Class<?> forName(String className, ClassLoader classLoader)
    throws ClassNotFoundException {
    if (classLoader != null) {
    return classLoader.loadClass(className);
    }
    return Class.forName(className);
    }
  • OnBeanCondition

    判断指定的 bean 类或名称是否已存在于 BeanFactory

过滤结果如下:

TRACE 日志如下:

1
Filtered 30 auto configuration class in 1000 ms

总结

在日常工作中,我们可能需要实现一些 Spring Boot Starter 给被人使用,这个时候我们就可以使用这个 Factories 机制,将自己的 Starter 注册到 org.springframework.boot.autoconfigure.EnableAutoConfiguration 命名空间下。这样用户只需要在服务中引入我们的 jar 包即可完成自动加载及配置。Factories 机制可以让 Starter 的使用只需要很少甚至不需要进行配置。

本质上,Spring Factories 机制与 Java SPI 机制原理都差不多:都是通过指定的规则,在规则定义的文件中配置接口的各种实现,通过 key-value 的方式读取,并以反射的方式实例化指定的类型。

spring.factories 已废弃

https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.7-Release-Notes

Loading auto-configurations from spring.factories is deprecated.

If you have created your own auto-configurations, you should move the registration from spring.factories to a new file named META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. Each line contains the fully qualified name of the auto-configuration. See the included auto-configurations for an example.

For backwards compatibility, entries in spring.factories will still be honored.

《提升维护效率,利用开源项目 mica-auto 自动生成这两个文件》

https://mp.weixin.qq.com/s/ASBRANcdMI2VXflyvD6wiA

参考

Auto-configuration Classes

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/io/support/SpringFactoriesLoader.html

https://github.com/spring-projects/spring-framework/blob/master/spring-core/src/main/java/org/springframework/core/io/support/SpringFactoriesLoader.java

Spring Boot spring.factories vs @Enable annotations

https://www.cnblogs.com/zheting/p/6707035.html

http://www.cnblogs.com/whx7762/p/7832985.html