首页 文章 精选 留言 我的

精选列表

搜索[MSA技术],共10000篇文章
优秀的个人博客,低调大师

关于自动限流的思考 | 京东云技术团队

目标 保证系统不因流量过载而挂。 现状:人工限流 正常的微服务限流工具都需要人工配置:支持应用负责人事先配置限流规则(接口+调用方+限流阈值),流量在阈值以下可以正常响应,超过阈值的流量会快速失败。这种方案存在如下问题: 问题1. 接口多,无法全面覆盖 要想保证系统不因流量过载而挂,那就需要对所有中高频接口进行流量管控,不然任意接口的流量上升都可能成为“压倒骆驼的最后一根稻草”。假设存在a个应用,按每个应用平均b个中高频接口, 每个接口对应c个调用方,限流规则配置那数量为(axbxc),稍微有点规模的部门这个数量就能上万, 要想全面覆盖靠人工基本不可行。 问题2.限流阈值无法准确评估 当前限流阈值评估主要有2类: 历史流量峰值:比如近7天系统正常提供服务的流量峰值。但是这个值偏低,容易产生误杀。 压测:通过压测演练得出接口的容量上限。但是压测的方式很难模拟真实的线上环境,无论是数据质量,流量的参数质量,依赖方的性能,亦或同应用内不同接口的流量分布都很难与真实环境保持一致。 问题3. 限流阈值无法长期有效 限流阈值会随着环境的变化而变化。例如流程中新增了一个依赖、或者数据库的数据量增多、热点数据增多、其他接口流量上升占用了更多系统资源、底层的基础设施发生变化等都会导致真实容量降低,限流阈值失效。在这种情况下,持续评估阈值来匹配系统的最新状态根本无法通过人工进行保证。 解决方案:自动限流 针对如上问题解法如下: 问题1. 接口多,无法全面覆盖 解:系统自动配置 问题2.限流阈值无法准确评估 解:系统自动评估 问题3. 限流阈值无法长期有效 解:系统动态调整 具体方案如下: 系统资源到达使用率到达预警线的时候, 系统自动触发系统进行全面限流, 各接口的限流值根据应用当前的流量状态以及历史的流量状态而定。 什么时候限流:应用的容量取决于系统的资源瓶颈,当资源的使用率到达某一水平的时候才需要限流。资源包括数据库、缓存、应用服务器等。 谁来限:系统自动 限哪些接口:由于同一个应用不同接口都共享了数据库、缓存等、应用服务器等资源,接口之间的容量会相互影响,所以需要全部接口都限制才能保证资源的使用率不再上升。 各接口限多少?在资源使用率到达瓶颈的时候,所有的接口性能都会下降,对应的限流阈值也应该下调。具体的限流计算有两种方式: 可以把系统在当前状态下各接口能够正常完成的请求量作为限流的参考值,来保证资源利用率不在上升。比如接口A接受到的请求速率为100,其中50排队,20报错,30正常完成,那么该接口限流值可以参考30(为排除正常抖动,具体的值可以通过滑动窗口进行平滑)。 可以把上一同比周期的同时间(比如昨天的同一时间)的各接口的请求量作为限流的参考值。 可以看着一种回滚:我不知道问题出在哪个接口,但是按照上个周期同时刻的流量来是没问题的。 落地实践: 为防止自动限流的不可控性,可同时使用自动限流和人工限流两种方式,具体方法如下: 系统分为正常状态和戒严状态:正常状态下使用人工限流,戒严状态下使用自动限流。 正常情况下系统使用人工限流,开发人员可以针对重点接口进行限流配置。 当限流值失效或者未配置限流的时候导致系统资源到达预警值时,系统进入戒严状态,此时系统由自动限流接管,并通知开发人员。 开发人员收到通知后进行排查,确定导致资源利用率上升的原因,并针对相关接口进行人工限流值的调整(可以参考到达瓶颈前的qps),并使系统重新切换到正常状态。 备注 该方案还需更多场景验证,如有疏漏还请指出,欢迎有兴趣的小伙伴共同探讨。 作者:京东零售马坚 来源:京东云开发者社区

优秀的个人博客,低调大师

MYSQL中JSON类型介绍 | 京东物流技术团队

1 json对象的介绍 在mysql未支持json数据类型时,我们通常使用varchar、blob或text的数据类型存储json字符串,对mysql来说,用户插入的数据只是序列化后的一个普通的字符串,不会对JSON文档本身的语法合法性做检查,文档的合法性需要用户自己保证。在使用时需要先将整个json对象从数据库读取出来,在内存中完成解析及相应的计算处理,这种方式增加了数据库的网络开销并降低处理效率。 从 MySQL 5.7.8 开始,MySQL 支持RFC 7159定义的全部json 数据类型,具体的包含四种基本类型(strings, numbers, booleans,and null)和两种结构化类型(objects and arrays)。可以有效地访问 JSON文档中的数据。与将 JSON 格式的字符串存储在字符串列中相比,该数据类型具有以下优势: 自动验证存储在 JSON列中的 JSON 文档。无效的文档会产生错误。 优化的存储格式。存储在列中的 JSON 文档被转换为允许快速读取文档元素的内部格式。当读取 JSON 值时,不需要从文本表示中解析该值,使服务器能够直接通过键或数组索引查找子对象或嵌套值,而无需读取文档中它们之前或之后的所有值。 2 json类型的存储结构 mysql为了提供对json对象的支持,提供了一套将json字符串转为结构化二进制对象的存储方式。json会被转为二进制的doc对象存储于磁盘中(在处理JSON时MySQL使用的utf8mb4字符集,utf8mb4是utf8和ascii的超集)。 doc对象包含两个部分,type和value部分。其中type占1字节,可以表示16种类型:大的和小的json object类型、大的和小的 json array类型、literal类型(true、false、null三个值)、number类型(int6、uint16、int32、uint32、int64、uint64、double类型、utf8mb4 string类型和custom data(mysql自定义类型),具体可以参考源码json_binary.cc和json_binary.h进行学习。 下面进行简单介绍: type ::= 0x00 | // small JSON object 0x01 | // large JSON object 0x02 | // small JSON array 0x03 | // large JSON array 0x04 | // literal (true/false/null) 0x05 | // int16 0x06 | // uint16 0x07 | // int32 0x08 | // uint32 0x09 | // int64 0x0a | // uint64 0x0b | // double 0x0c | // utf8mb4 string 0x0f // custom data (any MySQL data type) value包含 object、array、literal、number、string和custom-data六种类型,与type的16种类型对应。 object表示json对象类型,由6部分组成: object ::= element-count size key-entry value-entry key value 其中: element-count表示对象中包含的成员(key)个数,在array类型中表示数组元素个数。 size表示整个json对象的二进制占用空间大小。小对象用2Bytes空间表示(最大64K),大对象用4Bytes表示(最大4G) key-entry可以理解为一个用于指向真实key值的数组。本身用于二分查找,加速json字段的定位。 key-entry由两个部分组成: key-entry ::= key-offset key-length 其中: key-offset:表示key值存储的偏移量,便于快速定位key的真实值。 key-length:表示key值的长度,用于分割不同key值的边界。长度为2Bytes,这说明,key值的长度最长不能超过64kb. value-entry与key-enter功能类似,不同之处在于,value-entry可能存储真实的value值。 value-entry由两部分组成: value-entry ::= type offset-or-inlined-value 其中: type表示value类型,如上文所示,支持16种基本类型,从而可以表示各种类型的嵌套。 offset-or-inlined-value:有两层含义,如果value值足够小,可以存储于此,那么就存储数据本身,如果数据本身较大,则存储真实值的偏移用于快速定位。 key 表示key值的真实值,类型为:key ::= utf8mb4-data,这里无需指定key值长度,因为key-entry中已经声明了key的存储长度。同时,在同一个json对象中,key值的长度总是一样的。 array表示json数组,array类型主要包含4部分: array ::= element-count size value-entry value 我们来使用示意图更清晰的展示它的结构: 举例说明: 需要注意的是: JSON对象的Key索引(图中橙色部分)都是排序好的,先按长度排序,长度相同的按照code point排序;Value索引(图中黄色部分)根据对应的Key的位置依次排列,最后面真实的数据存储(图中白色部分)也是如此 Key和Value的索引对存储了对象内的偏移和大小,单个索引的大小固定,可以通过简单的算术跳转到距离为N的索引 通过MySQL5.7.16源代码可以看到,在序列化JSON文档时,MySQL会动态检测单个对象的大小,如果小于64KB使用两个字节的偏移量,否则使用四个字节的偏移量,以节省空间。同时,动态检查单个对象是否是大对象,会造成对大对象进行两次解析,源代码中也指出这是以后需要优化的点 现在受索引中偏移量和存储大小四个字节大小的限制,单个JSON文档的大小不能超过4G;单个KEY的大小不能超过两个字节,即64K 索引存储对象内的偏移是为了方便移动,如果某个键值被改动,只用修改受影响对象整体的偏移量 索引的大小现在是冗余信息,因为通过相邻偏移可以简单的得到存储大小,主要是为了应对变长JSON对象值更新,如果长度变小,JSON文档整体都不用移动,只需要当前对象修改大小 现在MySQL对于变长大小的值没有预留额外的空间,也就是说如果该值的长度变大,后面的存储都要受到影响 结合JSON的路径表达式可以知道,JSON的搜索操作只用反序列化路径上涉及到的元素,速度非常快,实现了读操作的高性能 MySQL对于大型文档的变长键值的更新操作可能会变慢,可能并不适合写密集的需求 3 json类型基本操作 3.1 json数据插入 json类型数据插入时有两种方式,一种是基于字符串格式插入,另一种是基于json_object()函数,在使用json_object()函数只需按k-v顺序,以,符号隔开顺序插入即可,MYSQL会自动验证 JSON 文档,无效的文档会产生错误。 mysql> CREATE TABLE t1 (jdoc JSON); Query OK, 0 rows affected (0.20 sec) mysql> INSERT INTO t1 VALUES('{"key1": "value1", "key2": "value2"}'); Query OK, 1 row affected (0.01 sec) mysql> INSERT INTO t1 VALUES('[1, 2,'); ERROR 3140 (22032) at line 2: Invalid JSON text: "Invalid value." at position 6 in value (or column) '[1, 2,'. 当一个字符串被解析并发现是一个有效的 JSON 文档时,它也会被规范化:具有与文档中先前找到的键重复的键的成员被丢弃(即使值不同)。以下第一个sql中通过 JSON_OBJECT()调用生成的对象值不包括第二个key1元素,因为该键名出现在值的前面;第二个sql中只保留了x第一次出现的值: mysql> SELECT JSON_OBJECT('key1', 1, 'key2', 'abc', 'key1', 'def'); +------------------------------------------------------+ | JSON_OBJECT('key1', 1, 'key2', 'abc', 'key1', 'def') | +------------------------------------------------------+ | {"key1": 1, "key2": "abc"} | +------------------------------------------------------+ mysql> INSERT INTO t1 VALUES > ('{"x": 17, "x": "red"}'), > ('{"x": 17, "x": "red", "x": [3, 5, 7]}'); mysql> SELECT c1 FROM t1; +-----------+ | c1 | +-----------+ | {"x": 17} | | {"x": 17} | +-----------+ 3.2 json合并 MySQL 5.7支持JSON_MERGE()的合并算法,多个对象合并时产生一个对象。 可将多个数组合并为一个数组: mysql> SELECT JSON_MERGE('[1, 2]', '["a", "b"]', '[true, false]'); +-----------------------------------------------------+ | JSON_MERGE('[1, 2]', '["a", "b"]', '[true, false]') | +-----------------------------------------------------+ | [1, 2, "a", "b", true, false] | +-----------------------------------------------------+ 当合并数组与对象时,会将对象转换为新数组进行合并: mysql> SELECT JSON_MERGE('[10, 20]', '{"a": "x", "b": "y"}'); +------------------------------------------------+ | JSON_MERGE('[10, 20]', '{"a": "x", "b": "y"}') | +------------------------------------------------+ | [10, 20, {"a": "x", "b": "y"}] | +------------------------------------------------+ 如果多个对象具有相同的键,则生成的合并对象中该键的值是包含键值的数组 mysql> SELECT JSON_MERGE('{"a": 1, "b": 2}', '{"c": 3, "a": 4}'); +----------------------------------------------------+ | JSON_MERGE('{"a": 1, "b": 2}', '{"c": 3, "a": 4}') | +----------------------------------------------------+ | {"a": [1, 4], "b": 2, "c": 3} | +----------------------------------------------------+ MySQL 8.0.3(及更高版本)支持两种合并算法,由函数 JSON_MERGE_PRESERVE()和 JSON_MERGE_PATCH(). 它们在处理重复键的方式上有所不同:JSON_MERGE_PRESERVE()保留重复键的值(与5.7版本的JSON_MERGE()相同),而 JSON_MERGE_PATCH()丢弃除最后一个值之外的所有值。具体的 JSON_MERGE_PRESERVE() 函数接受两个或多个 JSON 文档并返回组合结果。如果参数为两个object,相同的key将会把value合并为array(即使value也相同,也会合并为array),不同的key则直接合并。如果其中一个参数为json array,则另一个json object整体作为一个元素,加入array结果。 JSON_MERGE_PATCH()函数接受两个或多个 JSON 文档并返回组合结果。如果参数为两个object,相同的key的value将会被后面参数的value覆盖,不同的key则直接合并。如果合并的是数组,将按照“最后一个重复键获胜”逻辑仅保留最后一个参数。 mysql> SELECT JSON_MERGE_PRESERVE('{"a":1,"b":2}', '{"a":3,"c":3}'); +-------------------------------------------------------+ | JSON_MERGE_PRESERVE('{"a":1,"b":2}', '{"a":3,"c":3}') | +-------------------------------------------------------+ | {"a": [1, 3], "b": 2, "c": 3} | +-------------------------------------------------------+ 1 row in set (0.01 sec) mysql> SELECT JSON_MERGE_PATCH('{"a":1,"b":2}', '{"a":3,"c":3}'); +----------------------------------------------------+ | JSON_MERGE_PATCH('{"a":1,"b":2}', '{"a":3,"c":3}') | +----------------------------------------------------+ | {"a": 3, "b": 2, "c": 3} | +----------------------------------------------------+ 1 row in set (0.02 sec) mysql> SELECT JSON_MERGE_PRESERVE('["a", 1]', '"a"','{"key": "value"}'); +-----------------------------------------------------------+ | JSON_MERGE_PRESERVE('["a", 1]', '"a"','{"key": "value"}') | +-----------------------------------------------------------+ | ["a", 1, "a", {"key": "value"}] | +-----------------------------------------------------------+ 1 row in set (0.00 sec) mysql> SELECT JSON_MERGE_PATCH('["a", 1]', '"a"','{"key": "value"}') ; +--------------------------------------------------------+ | JSON_MERGE_PATCH('["a", 1]', '"a"','{"key": "value"}') | +--------------------------------------------------------+ | {"key": "value"} | +--------------------------------------------------------+ 1 row in set (0.01 sec) 3.3 json数据查询 MySQL 5.7.7+本身提供了很多原生的函数以及路径表达式来方便用户访问JSON数据。 JSON_EXTRACT()函数用于解析json对象,->符号是就一种JSON_EXTRACT()函数的等价模式。例如查询上面t1表中 jdoc字段中key值为x的值 SELECT jdoc->'$.x' FROM t1; SELECT JSON_EXTRACT(jdoc,'$.x') FROM t1; JSON_EXTRACT返回值会带有” “,如果想获取原本的值可以使用JSON_UNQUOTE mysql> SELECT JSON_EXTRACT('{"id": 14, "name": "Aztalan"}', '$.name'); +---------------------------------------------------------+ | JSON_EXTRACT('{"id": 14, "name": "Aztalan"}', '$.name') | +---------------------------------------------------------+ | "Aztalan" | +---------------------------------------------------------+ mysql> SELECT JSON_UNQUOTE(json_extract('{"id": 14, "name": "Aztalan"}', '$.name'));; +-----------------------------------------------------------------------+ | JSON_UNQUOTE(json_extract('{"id": 14, "name": "Aztalan"}', '$.name')) | +-----------------------------------------------------------------------+ | Aztalan | +-----------------------------------------------------------------------+ json路径的语法: pathExpression: scope[(pathLeg)*] pathLeg: member | arrayLocation | doubleAsterisk member: period ( keyName | asterisk ) arrayLocation: leftBracket ( nonNegativeInteger | asterisk ) rightBracket keyName: ESIdentifier | doubleQuotedString doubleAsterisk: '**' period: '.' asterisk: '*' leftBracket: '[' rightBracket: ']' 以json { “a”: [ [ 3, 2 ], [ { “c” : “d” }, 1 ] ], “b”: { “c” : 6 }, “one potato”: 7, “b.c” : 8 } 为例: $.a[1] 获取的值为 [ { “c” : “d” }, 1 ] $.b.c 获取的值为 6 $.”b.c” 获取的值为 8(因为键名包含不合法的表达式所以需要使用引号) mysql> select json_extract('{ "a": [ [ 3, 2 ], [ { "c" : "d" }, 1 ] ], "b": { "c" : 6 }, "one potato": 7, "b.c" : 8 }','$**.c'); +-------------------------------------------------------------------------------------------------------------------+ | JSON_EXTRACT('{ "a": [ [ 3, 2 ], [ { "c" : "d" }, 1 ] ], "b": { "c" : 6 }, "one potato": 7, "b.c" : 8 }','$**.c') | +-------------------------------------------------------------------------------------------------------------------+ | ["d", 6] | +-------------------------------------------------------------------------------------------------------------------+ $**.c 匹配到了两个路径 : $.a[1].c 获取的值是”d” $.b.c 获取的值为 6 3.4 json数据更新 一些函数采用现有的 JSON 文档,以某种方式对其进行修改,然后返回结果修改后的文档。路径表达式指示在文档中进行更改的位置。例如,JSON_SET()、 JSON_INSERT()和 JSON_REPLACE()函数各自采用现有的 JSON 文档,加上一个或多个路径和值对,来描述修改文档和要更新的值。这些函数在处理文档中现有值和不存在值的方式上有所不同。 具体如下 mysql> SET @j = '["a", {"b": [true, false]}, [10, 20]]'; JSON_SET()替换存在的路径的值并添加不存在的路径的值: mysql> SELECT JSON_SET(@j, '$[1].b[0]', 1, '$[2][2]', 2); +--------------------------------------------+ | JSON_SET(@j, '$[1].b[0]', 1, '$[2][2]', 2) | +--------------------------------------------+ | ["a", {"b": [1, false]}, [10, 20, 2]] | +--------------------------------------------+ 在这种情况下,路径$[1].b[0]选择一个现有值 ( true),该值将替换为路径参数 ( 1) 后面的值。该路径$[2][2]不存在,因此将相应的值 ( 2) 添加到 选择的值中$[2]。 JSON_INSERT()添加新值但不替换现有值: mysql> SELECT JSON_INSERT(@j, '$[1].b[0]', 1, '$[2][2]', 2); +-----------------------------------------------+ | JSON_INSERT(@j, '$[1].b[0]', 1, '$[2][2]', 2) | +-----------------------------------------------+ | ["a", {"b": [true, false]}, [10, 20, 2]] | +-----------------------------------------------+ JSON_REPLACE()替换现有值并忽略新值: mysql> SELECT JSON_REPLACE(@j, '$[1].b[0]', 1, '$[2][2]', 2); +------------------------------------------------+ | JSON_REPLACE(@j, '$[1].b[0]', 1, '$[2][2]', 2) | +------------------------------------------------+ | ["a", {"b": [1, false]}, [10, 20]] | +------------------------------------------------+ JSON_REMOVE()接受一个 JSON 文档和一个或多个路径,这些路径指定要从文档中删除的值。返回值是原始文档减去文档中存在的路径选择的值: mysql> SELECT JSON_REMOVE(@j, '$[2]', '$[1].b[1]', '$[1].b[1]'); +---------------------------------------------------+ | JSON_REMOVE(@j, '$[2]', '$[1].b[1]', '$[1].b[1]') | +---------------------------------------------------+ | ["a", {"b": [true]}] | +---------------------------------------------------+ $[2]匹配[10, 20] 并删除它。 $[1].b[1]匹配 元素false中 的第一个实例b并将其删除。 不匹配的第二个实例$[1].b[1]:该元素已被删除,路径不再存在,并且没有效果。 3.5 json比较与排序 JSON值可以使用=, <, <=, >, >=, <>, !=, <=>等操作符,BETWEEN, IN,GREATEST, LEAST等操作符现在还不支持。JSON值使用的两级排序规则,第一级基于JSON的类型,类型不同的使用每个类型特有的排序规则。 JSON类型按照优先级从高到低为 BLOB BIT OPAQUE DATETIME TIME DATE BOOLEAN ARRAY OBJECT STRING INTEGER, DOUBLE NULL 优先级高的类型大,不用再进行其他的比较操作;如果类型相同,每个类型按自己的规则排序。具体的规则如下: BLOB/BIT/OPAQUE: 比较两个值前N个字节,如果前N个字节相同,短的值小 DATETIME/TIME/DATE: 按照所表示的时间点排序 BOOLEAN: false小于true ARRAY: 两个数组如果长度和在每个位置的值相同时相等,如果不想等,取第一个不相同元素的排序结果,空元素最小。例:[] < [“a”] < [“ab”] < [“ab”, “cd”, “ef”] < [“ab”, “ef”] OBJECT: 如果两个对象有相同的KEY,并且KEY对应的VALUE也都相同,两者相等。否则,两者大小不等,但相对大小未规定。例:{“a”: 1, “b”: 2} = {“b”: 2, “a”: 1} STRING: 取两个STRING较短的那个长度为N,比较两个值utf8mb4编码的前N个字节,较短的小,空值最小。例:”a” < “ab” < “b” < “bc”;此排序等同于使用 collat​​ion 对 SQL 字符串进行排序utf8mb4_bin。因为 utf8mb4_bin是二进制排序规则,所以 JSON 值的比较区分大小写:”A” < “a” INTEGER/DOUBLE: 包括精确值和近似值的比较 4 JSON的索引 现在MySQL不支持对JSON列进行索引,官网文档的说明是: JSON columns cannot be indexed. You can work around this restriction by creating an index on a generated column that extracts a scalar value from the JSON column. 虽然不支持直接在JSON列上建索引,但MySQL规定,可以首先使用路径表达式对JSON文档中的标量值建立虚拟列,然后在虚拟列上建立索引。这样用户可以使用表达式对自己感兴趣的键值建立索引。举个具体的例子来说明: ALTER TABLE features ADD feature_street VARCHAR(30) AS (JSON_UNQUOTE(feature->"$.properties.STREET")); ALTER TABLE features ADD INDEX (feature_street); 两个步骤,可以对feature列中properties键值下的STREET键(feature->”$.properties.STREET”)创建索引。 其中,feature_street列就是新添加的虚拟列。之所以取名虚拟列,是因为与它对应的还有一个存储列(stored column)。它们最大的区别为虚拟列只修改数据库的metadata,并不会存储真实的数据在硬盘上,读取过程也是实时计算的方式;而存储列会把表达式的列存储在硬盘上。两者使用的场景不一样,默认情况下通过表达式生成的列为虚拟列。 这样虚拟列的添加和删除都会非常快,而在虚拟列上建立索引跟传统的建立索引的方式并没有区别,会提高虚拟列读取的性能,减慢整体插入的性能。虚拟列的特性结合JSON的路径表达式,可以方便的为用户提供高效的键值索引功能。 5 总结 JSON类型无须预定义字段,适合拓展信息的存储 单个JSON文档的大小不能超过4G;单个KEY的大小不能超过两个字节,即64K JSON类型适合应用于不常更新的静态数据 对搜索较频繁的数据建议增加虚拟列并建立索引 作者:京东物流 王凤辉 来源:京东云开发者社区 自猿其说Tech

优秀的个人博客,低调大师

缕析条分Scroll属性 | 京东云技术团队

最近有项目需要使用js原生开发滑动组件,频繁要用到dom元素的各种属性,其中以各种类型的height和top属性居多,名字相近,含义也很容易搞混。因此特地总结归纳了一下常用的知识点,在文末我们来挑战实现一个简易的移动端Scroll组件。 要理解height和top,要从盒模型开始说起,首先我们来认识一下css3中定义的盒模型。dom元素在页面上实际占据的面积可以由下面这张图来说明:  我们可以把它想象成一枚鸡蛋,从外到内依次是 橙色区域——外边距margin:鸡蛋壳 黑色区域——边框border:蛋壳膜 绿色区域——内边距padding:蛋白 白色区域——内容content:蛋黄 我们真正关心的部分是内容content,也就是鸡蛋营养最丰富的部分。content外面包裹了这么多层,我们在页面中才会感觉dom结构整体疏密有致,不会被密密麻麻的文字和图片所困扰。为了更好地描述盒模型,web规范中还定义了一些其他接口来描述他们,也就是我们今天要聊到的主角们: 在上面这张图中,我们描述了具有嵌套关系的两层dom元素,注意这里的内容区和前述一张图中有所不同,这里的内容区也是一个独立的dom元素(即它也有自己的margin、border、padding和content!,为了简化起见,子元素不再具体绘制出来),子元素由于整体高度很高,甚至超出了父元素的高度,在父元素中不能完全展示出来(超出的不可见部分用灰色表示),也就是两者构成了滚动关系。下面我们来尝试描述以下属性: 一、clientHeight 只读属性。clientHeight实际上就是垂直滚动条的高度,一般情况下,垂直滚动条都是要紧贴上下border的,因此clientHeight = 上下padding + 内容height。别忘了有个特殊情况,当存在水平方向滚动条时,还需要考虑水平滚动条挤占了垂直滚动条的一部分空间,即:clientHeight = 上下padding + 内容height - 水平滚动条高度。用鸡蛋来比喻的话,clientHeight就是剥了壳的鸡蛋。 二、clientTop 只读属性。描述了顶部border的宽度。顶部border容易让我们联想到头发的厚度,下次在镜子前可以对自己说:你的clientTop变少了,不过你也变强了。 三、offsetHeight 只读属性。offsetHeight和clientHeight比较类似,观察可以发现,比clientHeight多了border这一层:offsetHeight = clientHeight + 上下border。现在我们把时光回退到给鸡蛋剥壳的前一刻,鸡蛋从水里捞出来洗干净呈现的样子——offsetHeight。 四、offsetTop 只读属性。它返回当前元素相对于其offsetParent元素的顶部内边距的距离。这段距离是不包含自身和父元素的border宽度的,实际上:offsetTop=自己的上margin + 父元素的上padding,相当于鸡蛋和鸡蛋盒之间挡板的厚度。 五、scrollHeight 只读属性。描述了元素内容高度的度量,包括由于溢出导致的视图中不可见内容。我们可以hack一下,把父元素中不可见的内容也展示出来,子元素在垂直方向可以分为两部分:外层的margin和内部的offsetHeight: 对于具有滚动关系的父子元素,其scrollHeight具有不同的含义: (1)对于子元素而言,由于子元素本身不包含溢出部分,其scrollHeight和clientHeight具有相同的值 (2)对于父元素而言,由于父元素的内容实际上被子元素“撑起来”了,因此其scrollHeight为把内容区域“展开”后的实际高度:父元素scrollHeight = 子元素的offsetHeight + 子元素上下margin + 自己的上下padding 六、scrollTop 读写属性,可以获取或设置一个元素的内容垂直滚动的像素数。这个是目前为止咱们遇到的第一个可以支持设置的属性。 (1)初始状态时,内容垂直方向未滚动,其值为0 (2)当内容垂直方向滚动到底时,由于内容区实际高度为scrollHeight,可显示高度为clientHeight,多出来的部分就是此时的scrollTop值为scrollHeight - clientHeight 因此可以得出结论:0 <= scrollTop <= scrollHeight - clientHeight 七、实战 学习了以上属性和信息,我们模仿京东小程序的scroll-view组件功能,来实现一个H5版滑动组件的常见功能:列表的下拉刷新和上拉加载。要实现这个功能,大概可以分为3个核心要点: (1)可滚动:需要有一个不可动的外壳和可滑动的内容区。 (2)手势识别:通过移动端的touch属性,我们可以对比touchend和touchstart的手指位置,来简单进行手势识别。 (3)事件触发:根据滑动位置的临界条件,来判断是否应该触发刷新和加载事件。 部分实现代码如下: // scroller.js export default class Scroller { constructor(el, option) { this._el = el this._parent = el.parentNode this._option = option this._pos = 0 this._handleScrollStart = this.handleScrollStart.bind(this) this._handleScroll = this.handleScroll.bind(this) this.init() } init() { this._el.addEventListener('touchstart', this._handleScrollStart) this._el.addEventListener('touchend', this._handleScroll) if(this._option.auto) { this.handleRefresh() } } destroy() { this._el.removeEventListener('touchstart', this._handleScrollStart) this._el.removeEventListener('touchend', this._handleScroll) } handleScrollStart(e) { const touch = e.targetTouches[0] || e.changedTouches[0] this._pos = touch.clientY } handleScroll(e) { const touch = e.targetTouches[0] || e.changedTouches[0] const delta = touch.clientY - this._pos if(delta >= this._option.threhold) { // 手势向下,且下行滚动距离超过判定阈值 if(this._parent.scrollTop == 0) { this.handleRefresh(e) } } else if(this._parent.scrollHeight > this._parent.clientHeight && delta <= -this._option.threhold){ // 内容可滚动,且上行滚动距离超过判定阈值 if(this._parent.scrollTop > this._parent.scrollHeight - this._parent.clientHeight - this._option.threhold){ this.handleLoad(e) } } } handleRefresh(...params) { this._option.onRefresh && this._option.onRefresh.apply(this, params) } handleLoad(...params) { this._option.onRefresh && this._option.onLoad.apply(this, params) } } 调用demo: var inner = document.getElementsById('inner') var list = [] var pageIndex = 1 function getMockData() { const newData = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(i => i+(pageIndex-1)*10) setTimeout(() => { if(pageIndex==1) { list = newData } else if(pageIndex < 5) { list.push(...newData) } else { alert('没有更多内容了') } pageIndex++ inner.innerHTML = list.map(i => `<div class="inner-item">${i}</div>`).join('') }, 300) } function onRefresh() { pageIndex = 1 getMockData() } new Scroller(inner, { threhold: 20, onRefresh: onRefresh, onLoad: getMockData, auto: true }) 大家可以自己试用一下,感觉效果还不错~~~欢迎留言区讨论交流。 作者:京东零售 陈震 来源:京东云开发者社区

优秀的个人博客,低调大师

从TL、ITL到TTL | 京东云技术团队

1、概述 ThreadLocal(TL)是Java中一种线程局部变量实现机制,他为每个线程提供一个单独的变量副本,保证多线程场景下,变量的线程安全。经常用于代替参数的显式传递。 InheritableThreadLocal(ITL)是JDK提供的TL增强版,而TransmittableThreadLocal(TTL)是阿里开源的ITL增强版 这些ThreadLocal在不同场景下有不同用途,我们来分析一下: 2、ThreadLocal ThreadLocal主要的方法有四个:initialValue、set、get、remove 2.1、初始化——initialValule 当线程首次访问该ThreadLocal时(ThreadLocal.get()),会进行初始化赋值。我们常用两种方法初始化ThreadLocal 2.1.1、重写initialValue ThreadLocal<String> threadLocal = new ThreadLocal<String>() { @Override protected String initialValue() { return ""; } }; 2.1.2、调用ThreadLocal.withInitial ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> ""); 他会创建一个SuppliedThreadLocal内部类 public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) { return new SuppliedThreadLocal<>(supplier); } 该类重写了initialValue方法 static final class SuppliedThreadLocal<T> extends ThreadLocal<T> { private final Supplier<? extends T> supplier; SuppliedThreadLocal(Supplier<? extends T> supplier) { this.supplier = Objects.requireNonNull(supplier); } @Override protected T initialValue() { //当该线程首次访问ThreadLocal时,会间接调用lambda表达式初始化 return supplier.get(); } } ⚠️ITL并没有重新实现withInitial,如果使用withInitial则会创建STL,失去自己增强的特性 2.2、赋值——set public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } 这里出现了一个关键属性ThreadLocalMap,类定义在ThreadLocal中,是Thread的成员变量 ThreadLocalMap getMap(Thread t) { return t.threadLocals; } ThreadLocalMap内部还有一个内部类Entry,是存值的地方 static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { //ThreadLocal的引用是“key” super(k); //线程局部变量是value value = v; } } //Entry数组 //value具体放在哪个index下,是由ThreadLocal的hashCode算出来的 private Entry[] table; } 2.3、取值——get public T get() { Thread t = Thread.currentThread(); //1、获取线程的ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) { //2、根据ThreadLocal的hashCode,获取对应Entry下的value ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } //3、如果没有赋过值,则初始化 return setInitialValue(); } 2.4、清空——remove public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) //会将对应Entry、包括他的key、value手动置null m.remove(this); } 3、InheritableThreadLocal 3.1、TL在父子线程场景下存在的问题 我们先来看一个例子 public static void main(String[] args) throws InterruptedException { ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "A"); threadLocal.set("B"); Thread thread = new Thread(() -> { System.out.println("子线程ThreadLocal:" + threadLocal.get()); }, "子线程"); thread.start(); thread.join(); } 打印结果如下,可见子线程的ThreadLocal是初始值,并没有使用父线程修改后的值: 子线程ThreadLocal:A 线程的ThreadLocalMap是首次访问时创建的,所以子线程使用ThreadLocal的时候,会初始化一个新的ThreadLocal,线程局部变量为默认值 ⚠️所以,TL不具有遗传性 3.2、ITL的解决方案 为了解决TL子线程遗传性的问题,JDK引入了ITL 他继承ThreadLocal,重写了childValue、getMap、createMap三个方法 public class InheritableThreadLocal<T> extends ThreadLocal<T> { protected T childValue(T parentValue) { return parentValue; } ThreadLocalMap getMap(Thread t) { return t.inheritableThreadLocals; } void createMap(Thread t, T firstValue) { t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); } } 这里出现了inheritableThreadLocals,他存储的就是从父线程拷贝过来的ThreadLocal,这个值是在父线程首次修改ThreadLocal的时候赋值的,然后在子线程创建时拷贝过来的 //父线程部分: public void set(T value) { Thread t = Thread.currentThread(); //该方法被ITL重写,访问inheritableThreadLocals为null ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else //该方法同样被ITL重写,创建一个ThreadLocalMap赋值给inheritableThreadLocals createMap(t, value); } //子线程部分: public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); } private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { //省略一些代码... //获取当前线程(父线程、也就是创建子线程的线程) Thread parent = currentThread(); //1、允许ThreadLocal遗传(这个默认为true) //2、inheritableThreadLocals不为空,因为父线程调用set了 //父线程不调用set,那ThreadLocal就是初始值,那直接初始化就好了,也不用进该分支 if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); } //createInheritedMap使用该构造函数,根据父线程的inheritableThreadLocals进行深拷贝 private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable = parentMap.table; int len = parentTable.length; setThreshold(len); table = new Entry[len]; //深拷贝父线程ThreadLocalMap for (int j = 0; j < len; j++) { Entry e = parentTable[j]; if (e != null) { @SuppressWarnings("unchecked") ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); if (key != null) { //childValue被ITL重写,返回父线程ThreadLocal的值 Object value = key.childValue(e.value); Entry c = new Entry(key, value); int h = key.threadLocalHashCode & (len - 1); while (table[h] != null) h = nextIndex(h, len); table[h] = c; size++; } } } } 使用ITL的效果 public static void main(String[] args) throws InterruptedException { ThreadLocal<String> threadLocal = new InheritableThreadLocal<String>() { @Override protected String initialValue() { return "A"; } }; threadLocal.set("B"); Thread thread = new Thread(() -> { System.out.println("子线程ThreadLocal:" + threadLocal.get()); }, "子线程"); thread.start(); thread.join(); } 打印结果如下,子线程拷贝了父线程ThreadLocal: 子线程ThreadLocal:B 总结一下,ITL解决父子线程遗传性的核心思路是,将可遗传的ThreadLocal放在父线程新的ThreadLocalMap中,在子线程首次使用时进行拷贝 4.、TransmittableThreadLocal 4.1、ITL在线程复用场景下存在的问题 我们再从一个简单的例子说起 public static void main(String[] args) throws InterruptedException, ExecutionException { ThreadLocal<String> threadLocal = new InheritableThreadLocal<String>() { @Override protected String initialValue() { return "A"; } }; threadLocal.set("B"); ExecutorService executorService = Executors.newFixedThreadPool(1); //1、子线程第一次获取ThreadLocal executorService.submit(() -> System.out.println("子线程ThreadLocal:"+threadLocal.get())).get(); Thread.sleep(1000); //2、父线程修改ThreadLocal threadLocal.set("C"); System.out.println("父线程修改ThreadLocal为"+threadLocal.get()); //3、子线程第二次获取ThreadLocal executorService.submit(() -> System.out.println("子线程ThreadLocal:"+threadLocal.get())).get(); } 打印结果如下,子线程在第二次打印时,并没有拷贝父线程的ThreadLocal,使用的还是首次拷贝的值: 子线程ThreadLocal:B 父线程修改ThreadLocal为C 子线程ThreadLocal:B ⚠️可复用的子线程不会感知父线程ThreadLocal的变化 4.2、TTL的解决方案 4.2.1、TTL的使用 TTL在ITL上做了稍微复杂的封装,我们从使用开始了解 引入依赖 <dependency> <groupId>com.alibaba</groupId> <artifactId>transmittable-thread-local</artifactId> <version>latest</version> </dependency> 在使用TTL时,线程需要经过TTL封装,线程池同理 public static void main(String[] args) throws InterruptedException, ExecutionException { ThreadLocal<String> threadLocal = new TransmittableThreadLocal<String>() { @Override protected String initialValue() { return "A"; } }; threadLocal.set("B"); ExecutorService executorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1)); executorService.submit(() -> System.out.println("子线程ThreadLocal:" + threadLocal.get())).get(); Thread.sleep(1000); threadLocal.set("C"); System.out.println("父线程修改ThreadLocal为" + threadLocal.get()); executorService.submit(() -> System.out.println("子线程ThreadLocal:" + threadLocal.get())).get(); Thread.sleep(1000); executorService.submit(() -> { threadLocal.set("D"); System.out.println("子线程修改ThreadLocal为" + threadLocal.get()); }); Thread.sleep(1000); executorService.submit(() -> System.out.println("子线程ThreadLocal:" + threadLocal.get())); Thread.sleep(1000); } 打印结果如下,子线程每次都会获取父线程的ThreadLocal 子线程ThreadLocal:B 父线程修改ThreadLocal为C 子线程ThreadLocal:C 子线程修改ThreadLocal为D 子线程ThreadLocal:C 从使用上看,TTL要求将任务封装,那我们就从ThreadLocal和ExecutorService两部分入手 4.2.2、TTL对ThreadLocal的封装 下面是TTL的取值和赋值逻辑,都涉及一个关键方法addThisToHolder,对应的属性holder会在线程池执行任务时用到 //TransmittableThreadLocal.addThisToHolder() private void addThisToHolder() { //InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder if (!holder.get().containsKey(this)) { //holder是静态变量,他会把TTL存到当前线程的map中 //value是null,他其实是把Map当Set用 //主线程赋值时,会获取主线程的holderMap,然后把TTL存进去 holder.get().put((TransmittableThreadLocal<Object>) this, null); } } @Override public final void set(T value) { if (!disableIgnoreNullValueSemantics && null == value) { remove(); } else { super.set(value); //当主线程赋值时,会将自己的TTL放到自己的map中 addThisToHolder(); } } @Override public final T get() { T value = super.get(); if (disableIgnoreNullValueSemantics || null != value) addThisToHolder(); return value; } 4.2.3、TTL对任务的封装 //我们通过TtlExecutors.getTtlExecutorService()对线程池进行封装 public static ExecutorService getTtlExecutorService(@Nullable ExecutorService executorService) { if (TtlAgent.isTtlAgentLoaded() || executorService == null || executorService instanceof TtlEnhanced) { return executorService; } //入参是线程池,通过包装类代理线程池的操作 return new ExecutorServiceTtlWrapper(executorService); } //ExecutorServiceTtlWrapper.submit() public Future<?> submit(@NonNull Runnable task) { //将提交的任务进行封装 return executorService.submit(TtlRunnable.get(task)); } 4.2.3.1、任务构建 TtlRunnable构造方法 这里都是主线程在操作,因为任务是主线程提交的 private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) { this.capturedRef = new AtomicReference<Object>(capture()); this.runnable = runnable; this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun; } 这里有一个关键属性capturedRef,他是一个原子引用,存了TTL //TrasmitableThreadLocal.Transmitter public static Object capture() { //获取ttl的值构建快照 return new Snapshot(captureTtlValues(), captureThreadLocalValues()); } private static HashMap<TransmittableThreadLocal<Object>, Object> captureTtlValues() { HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value = new HashMap<TransmittableThreadLocal<Object>, Object>(); for (TransmittableThreadLocal<Object> threadLocal : holder.get().keySet()) { //将主线程TTL的值存到当前任务中 ttl2Value.put(threadLocal, threadLocal.copyValue()); } return ttl2Value; } 4.2.3.2、任务执行 任务执行的代码如下,在任务执行前回放ThreadLocal,在任务执行后恢复ThreadLocal: 这里都是子线程在操作,因为任务都是子线程执行的 @Override public void run() { Object captured = capturedRef.get(); if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) { throw new IllegalStateException("TTL value reference is released after run!"); } //1、备份子线程ThreadLocal //2、使用主线程提交任务时构建的ThreadLocal副本,将子线程ThreadLocal覆盖 Object backup = replay(captured); try { //3、任务执行 runnable.run(); } finally { //3、使用之前备份的子线程ThreadLocal进行恢复 restore(backup); } } 总结一下,TTL让子线程感知父线程变化的核心思路是,主线程在任务提交时构建ThreadLocal副本,在子线程执行任务时供其使用 ⚠️提交和执行任务会对TTL进行若干操作,理论上对性能有一点点影响,官方性能测试结论说损耗可忽略 TTL官方性能测试 作者:京东物流 刘朝永 来源:京东云开发者 自猿其说

优秀的个人博客,低调大师

浅谈常态化压测 | 京东物流技术团队

一、常态化压测介绍 1.什么是常态化压测 常态是指:“正常的状态”;“化”在这里是表示转变为某种性质或状态。 “常态化”的含义就是:趋向正常的状态。 那么常态化压测顾名思义就可以解释为,让压测趋于正常的状态,趋于合理;因此通过调研给了如下定义:常态化压测是指在某个产品或系统上进行自定义周期(常态化)的、系统自动执行的、可验证结果的压测过程。目的是检测产品或系统的稳定性、可靠性和性能,确保它们能够在不同的场景下正常运行。 2.为什么要进行常态化压测 随着业务的不断增长,支撑业务系统的压力也逐渐增加,会面临如系统越来越厚重、逻辑越来复杂、迭代节奏越来越快等繁杂的情况。我们当前并没有做到在每次变化时快速识别出性能风险,检测产品或系统的稳定性、可靠性,而且我们还在不断的投入人力成本在压测这件事情上也是不合理的,所以我们要将性能验证融入到我们日常的工作中,把压测做到常态化,做成平常的一件事。 3.常态化压测的价值 快进快出,最小单位安排压测任务,减少人员投入 尽早识别性能波动,避免风险后置 可复用性高,压测模型、业务模型可复用 业务可用性保障 二、常态化压测实践 1.常态化压测流程介绍 借助泰山平台中的Forcebot工具,进行常态化压测执行。设置常态化压测任务,配置压测执行计划,按照预设的数据模型、基线值等进行执行。在累积一段时间的压测结果后,可以对基线值、压测目标进行调整和调优,然后继续进行自动化周期性的压测。同时跟进结果,及时关注最近业务和系统的变化。 明确压测目标 明确常态化压测的目标,可以区分日常和极限两个场景。根据不同的场景设置不同的压测目标,第一次实践以单接口的日常流量为主,观测日常流量下的性能波动,确定正常范围值。 常态化计划 根据接口的优先级,接口服务的流量,日常迭代代码改动的频率,设置对应的计划。核心接口、流量较大、代码改动频繁的接口,常态化压测的周期建议短一些,可以一周1-2次,且放在每次上线后进行;如果是非核心、流量小的接口,可以每周或每双周进行一次。 压测前期准备 确定测试场景根据实际场景,设计压测场景,包括用户数量、请求类型、请求频率、请求参数等,以尽可能模拟真实的业务场景。编写压测脚本,设置按照真实请求数据比例设置参数化,以便于进行自动化测试。 压测中期关注 在压测过程中,主要是无人值守的状态,所以需要提前识别压测可能带来的风险,以及面对不同的风险需要采取的措施。 同时还要关注压测过程中如果出现性能波动,系统发出的预警方式是否及时、预警内容是否准确。 压测后期跟进 每次常态化压测计划执行后,都需要关注本次的结果:① 结果符合预期,则要关注下指标的波动; ② 若结果并不符合预期,则要刨根问底,找到问题的所在; ③ 跟进问题直到解决,解决后重新验证。 辅助功能-流量染色 流量染色指根据流量协议,设置对应的流量染色规则,对指定的流量进行染色标记,并在整个调用链中携带该标记。 通过流量染色,可以实现压测流量隔离,同时可以保证日常压测对生产流量无影响。但是对于常态化压测来讲,流量染色并非必须,而是锦上添花的功能,可以进一步保证我们的流量对生产无影响。 2.首次进行常态化压测实践 2.1 准备阶段 ① 获取基线值数据 ② 压测脚本、场景、数据模型准备 ③ 压测环境准备 ④ 压测计划制定 2.2 执行阶段 2.3 调优阶段 以7-10天为一个周期,记录数据。不考虑特殊节日、特殊活动的场景,验证基线数据的可信赖性。如发现高于30%的概率,每天的数据都不符合基线值,可调整基线值的浮动范围,以保结证结果在日常值的范围内。 调优的过程也要关注,在数据统计过程中代码的改动,如果确定是代码的改动影响了整体的性能,这就要根据实际的场景和影响范围进行评估。 2.4 复盘阶段 经过一段时间的常态化压测,需要对整个流程和结果进行复盘,好的复盘结果能够帮助我们避免后续的一些“坑”。参与常态化压测的所有人员一起,主要关注一下几个方面: ① 数据是否正确,是否达标,是否可信赖 ② 常态化压测流程、计划相关问题 ③ 压测过程中的性能问题总结 三、常态化压测总结 ① 建议覆盖场景 可以根据自己所负责的业务进行考量,如果不会出现很大流量的情况下,建议覆盖日常场景即可满足需求;如果需要考虑大流量并发的场景,建议覆盖日常的基础上,在覆盖极限的场景。 ② 关注风险控制 常态化压测要做到的是模拟生产真实场景,实现日常自动化压测,尽量做到无人值守,所以提前识别出可能的风险是非常必要的,同时要列出不同的风险项对应的举措。 ③ 常态化压测只是辅助验证业务性能的一种手段 不是说我们进行常态化压测之后就不需要进行性能测试了,两者之间是没有冲突的。 寄语:每个人负责的工作是不同的,不同人会有不同诉求,对同一件事也会有不同看法,不过苏轼有句话:“犯其至难而图其至远”,意思是说“向最难之处攻坚,追求最远大的目标”。只希望我们能在工作中克服各种困难,去实现最长远的、可持续的目标。 作者:京东物流 冯海艳 来源:京东云开发者社区自猿其说Tech

优秀的个人博客,低调大师

文盘Rust -- FFI 浅尝 | 京东云技术团队

rust FFI 是rust与其他语言互调的桥梁,通过FFI rust 可以有效继承 C 语言的历史资产。本期通过几个例子来聊聊rust与C 语言交互的具体步骤。 场景一 调用C代码 创建工程 cargo new --bin ffi_sample Cargo.toml 配置 [package] name = "ffi_sample" version = "0.1.0" edition = "2021" build = "build.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [build-dependencies] cc = "1.0.79" [dependencies] libc = "0.2.146" libloading = "0.8.0" 编写一个简单的c程序sample.c int add(int a,int b){ return a+b; } main.rs use std::os::raw::c_int; #[link(name = "sample")] extern "C" { fn add(a: c_int, b: c_int) -> c_int; } fn main() { let r = unsafe { add(2, 18) }; println!("{:?}", r); } build.rs fn main() { cc::Build::new().file("sample.c").compile("sample"); } 代码目录树 . ├── Cargo.lock ├── Cargo.toml ├── build.rs ├── sample.c └── src └── main.rs cargo run 场景二 使用bindgen 通过头文件绑定c语言动态链接库 修改Cargo.toml,新增bindgen依赖 [package] name = "ffi_sample" version = "0.1.0" edition = "2021" build = "build.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [build-dependencies] cc = "1.0.79" bindgen = "0.65.1" [dependencies] libc = "0.2.146" libloading = "0.8.0" 新增 sample.h 头文件 #ifndef ADD_H #define ADD_H int add(int a, int b); #endif 新增 wrapper.h 头文件 wrapper.h 文件将包括所有各种头文件,这些头文件包含我们想要绑定的结构和函数的声明 #include "sample.h"; 改写build.rs 编译 sample.c 生成动态链接库sample.so;通过bindgen生成rust binding c 的代码并输出到 bindings 目录 use std::path::PathBuf; fn main() { // 参考cc 文档 println!("cargo:rerun-if-changed=sample.c"); cc::Build::new() .file("sample.c") .shared_flag(true) .compile("sample.so"); // 参考 https://doc.rust-lang.org/cargo/reference/build-scripts.html println!("cargo:rustc-link-lib=sample.so"); println!("cargo:rerun-if-changed=sample.h"); let bindings = bindgen::Builder::default() .header("wrapper.h") .parse_callbacks(Box::new(bindgen::CargoCallbacks)) .generate() .expect("Unable to generate bindings"); let out_path = PathBuf::from("bindings"); bindings .write_to_file(out_path.join("sample_bindings.rs")) .expect("Couldn't write bindings!"); } 修改main.rs include 宏引入sample 动态链接库的binding。以前我们自己手写的C函数绑定就不需要了,看看bindings/sample_bindings.rs 的内容与我们手写的函数绑定是等效的 include!("../bindings/sample_bindings.rs"); // #[link(name = "sample")] // extern "C" { // fn add(a: c_int, b: c_int) -> c_int; // } fn main() { let r = unsafe { add(2, 18) }; println!("{:?}", r); } 代码目录树 . ├── Cargo.lock ├── Cargo.toml ├── bindings │ └── sample_bindings.rs ├── build.rs ├── sample.c ├── sample.h ├── src │ └── main.rs └── wrapper.h ffi_sample 工程的完整代码位置,读者可以clone https://github.com/jiashiwen/wenpanrust,直接运行即可 cargo run -p ffi_sample 场景三 封装一个c编写的库 secp256k1是一个椭圆曲线计算的 clib,这玩意儿在密码学和隐私计算方面的常用算法,下面我们从工程方面看看封装secp256k1如何操作 cargo new --lib wrapper_secp256k1 cargo.toml [package] name = "wrapper_secp256k1" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [build-dependencies] cc = "1.0.79" bindgen = "0.65.1" [dependencies] git 引入 submodule cd wrapper_secp256k1 git submodule add https://github.com/bitcoin-core/secp256k1 wrapper_secp256k1/secp256k1_sys 工程下新建bindings目录用来存放绑定文件,该目录与src平级 wrapper.h #include "secp256k1_sys/secp256k1/include/secp256k1.h" build.rs use std::path::PathBuf; fn main() { println!("cargo:rustc-link-lib=secp256k1"); println!("cargo:rerun-if-changed=wrapper.h"); let bindings = bindgen::Builder::default() .header("wrapper.h") .parse_callbacks(Box::new(bindgen::CargoCallbacks)) .generate() .expect("Unable to generate bindings"); let out_path = PathBuf::from("bindings"); bindings .write_to_file(out_path.join("bindings.rs")) .expect("Couldn't write bindings!"); } cargo build 通过 编写测试 lib.rs include!("../bindings/secp256k1.rs"); #[cfg(test)] mod tests { use super::*; #[test] fn test_create_pubkey() { // secp256k1返回公钥 let mut pubkey: secp256k1_pubkey = secp256k1_pubkey { data: [0; 64] }; let prikey: u8 = 1; unsafe { let context = secp256k1_context_create(SECP256K1_CONTEXT_SIGN); assert!(!context.is_null()); let ret = secp256k1_ec_pubkey_create(&*context, &mut pubkey, &prikey); assert_eq!(ret, 1); } } } 运行测试 cargo test 报错 warning: `wrapper_secp256k1` (lib) generated 5 warnings error: linking with `cc` failed: exit status: 1 | = note: LC_ALL="C" PATH="/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/bin:/Users/jiashiwen/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/libobject-6d1da0e5d7930106.rlib" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/libmemchr-d6d74858e37ed726.rlib" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/libaddr2line-d75e66c6c1b76fdd.rlib" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/libgimli-546ea342344e3761.rlib" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/librustc_demangle-8ad10e36ca13f067.rlib" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/libstd_detect-0543b8486ac00cf6.rlib" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/libhashbrown-7f0d42017ce08763.rlib" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/libminiz_oxide-65e6b9c4725e3b7f.rlib" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/libadler-131157f72607aea7.rlib" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/librustc_std_workspace_alloc-f7d15060b16c135d.rlib" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/libunwind-a52bfac5ae872be2.rlib" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/libcfg_if-1762d9ac100ea3e7.rlib" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/liblibc-f8e0e4708f61f3f4.rlib" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/liballoc-af9a608dd9cb26b2.rlib" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/librustc_std_workspace_core-9777023438fd3d6a.rlib" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/libcore-83ca6d61eb70e9b8.rlib" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib/libcompiler_builtins-ea2ca6e1df0449b8.rlib" "-lSystem" "-lc" "-lm" "-L" "/usr/local/Cellar/rust/1.70.0/lib/rustlib/x86_64-apple-darwin/lib" "-o" "/Users/jiashiwen/rustproject/wrapper_secp256k1/target/debug/deps/wrapper_secp256k1-4bf30c62ecfdf2a7" "-Wl,-dead_strip" "-nodefaultlibs" = note: ld: library not found for -lsecp256k1 clang: error: linker command failed with exit code 1 (use -v to see invocation) warning: `wrapper_secp256k1` (lib test) generated 5 warnings (5 duplicates) error: could not compile `wrapper_secp256k1` (lib test) due to previous error; 5 warnings emitted 报错显示找不到编译 secp256k1 相对应的库。 手动编译secp256k1 cd secp256k1_sys ./autogen.sh ./configure make make install 编译完成后,测试通过 其实 secp256k1 有对应的 rust wrapper,我们这里只是展示一下封装的过程。 wrapper_secp256k1 工程的完整代码位置,有兴趣的朋友可以clone https://github.com/jiashiwen/wenpanrust。通过以下操作查看运行结果: clone 项目 git clone https://github.com/jiashiwen/wenpanrust cd wenpanrust update submodule git submodule init git submodule update 编译 secp256k1 cd wrapper_secp256k1/secp256k1_sys ./autogen.sh ./configure make make install run test cargo test -p wrapper_secp256k1 参考资料 Rust FFI (C vs Rust)学习杂记.pdfbindgen官方文档Rust FFI 编程 - bindgen 使用示例 作者:京东科技 贾世闻 来源:京东云开发者社区

优秀的个人博客,低调大师

Maven进阶学习指南 | 京东云技术团队

前言 当我们在开发项目时,有时需要用到外部依赖组件,例如当我们需要Json序列化的时候需要用到FastJson组件,我们可以通过下载对应jar包加载到项目中。但当一个大的项目同时需要依赖各种各样的外部服务,就存在着配置繁琐、依赖冲突等问题,因此可以通过maven来完成对应的依赖管理功能。 一、Settings配置 settings.xml用来配置maven项目中的各种参数文件,包括本地仓库、远程仓库、私服、认证等信息。 1.1 配置概述 1.1.1 全局settings、用户setting、pom的区别 全局 settings.xml 是 maven 的全局配置文件,一般位于${maven.home}/conf/settings.xml,即maven文件夹下的conf中。 用户 setting是maven的用户配置文件,一般位于${user.home}/.m2/settings.xml,即每位用户都有一份配置文件。 pom.xml 文件是项目配置文件,一般位于项目根目录下或子目录下。 配置优先级从高到低:pom.xml > 本地 settings > 全局 settings 如果这些文件同时存在,在应用配置时,会合并它们的内容,如果有重复的配置,优先级高的配置会覆盖优先级低的。 1.1.2 仓库【重要】 如前言所述,我们依赖的外部服务是需要有地方进行存储的,而存储的地方就称之为仓库。其中仓库又分为本地仓库、中央仓库、镜像仓库、私服。 其对应关系为: (1)本地仓库 当项目在本地编译或运行时,直接加载本地的依赖服务无疑是最快的。默认情况下,不管在Window还是Linux下,每个用户在自己用户目录下都有一个路径名为.m2/repository/的仓库目录。 而原始的本地仓库是为空的,因此maven需要知道一个网络上的仓库,在本地仓库不存在时前往下载网络上的仓库,也就是远程仓库。 (2)中央仓库 当maven未配置时,会默认请求maven的中央仓库,中央仓库包含了这个世界上绝大多数流行的开源java构件,以及源码、作者信息、SCM,信息、许可证信息等,每个月这里都会接受全世界java程序员大概1亿次的访问,它对全世界java开发者的贡献由此可见一斑。 但是由于最常见的例如网络原因等,国外的中央仓库使用起来并不顺利,因此就有了下载地址为国内的中央仓库,也就是镜像仓库。 (3)镜像仓库 总结来说,镜像仓库就是将国外的中心仓库复制一份到国内,这样一来下载速度以及访问速度都将很快。 (4)私服 一般来说中央仓库或者镜像仓库都能满足我们的需求,但是当我们在公司内合作开发代码时,可能因为系统保密性原因,有一些其他同事开发的外部依赖只希望能够被本公司的人使用,而如果上传到镜像仓库则保密性就不复存在了。因此私服最主要的功能时存储一些公司内部不希望被公开的依赖服务。 1.2 settings配置详解 settings.xml配置了本地全局maven的相关配置。 以下是一份seetings.xml的文件配置顶级元素。 <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd"> <localRepository> <interactiveMode> <usePluginRegistry> <offline> <pluginGroups> <servers> <mirrors> <proxies> <profiles> <activeProfiles> </settings> 1.2.1 localRepository 用来标识本地仓库的位置 D:/Maven/Repository 1.2.2 interactiveMode maven 是否需要和用户交互以获得输入。默认为true。 true 1.2.3 usePluginRegistry maven 是否需要使用 plugin-registry.xml 文件来管理插件版本。 false 1.2.4 offline 用来标识是否以离线模式运营maven。 当系统不能联网时,可以通过次配置来离线运行。 false 1.2.5 servers 当使用maven私服时,某些私服需要配置认证信息,需要在此处填写相应的配置。之所以不写在pom.xml中是因为一般项目在上传至代码仓库时同样会将pom.xml上传,而setting.xml一般位于用户本地,因此相对比较安全。 <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd"> ... <!--配置服务端的一些设置。一些设置如安全证书不应该和pom.xml一起分发。这种类型的信息应该存在于构建服务器上的settings.xml文件中。 --> <servers> <!--服务器元素包含配置服务器时需要的信息 --> <server> <!--这是server的id(注意不是用户登陆的id),该id与distributionManagement中repository元素的id相匹配。 --> <id>server001</id> <!--鉴权用户名。鉴权用户名和鉴权密码表示服务器认证所需要的登录名和密码。 --> <username>my_login</username> <!--鉴权密码 。鉴权用户名和鉴权密码表示服务器认证所需要的登录名和密码。密码加密功能已被添加到2.1.0 +。详情请访问密码加密页面 --> <password>my_password</password> <!--鉴权时使用的私钥位置。和前两个元素类似,私钥位置和私钥密码指定了一个私钥的路径(默认是${user.home}/.ssh/id_dsa)以及如果需要的话,一个密语。将来passphrase和password元素可能会被提取到外部,但目前它们必须在settings.xml文件以纯文本的形式声明。 --> <privateKey>${usr.home}/.ssh/id_dsa</privateKey> <!--鉴权时使用的私钥密码。 --> <passphrase>some_passphrase</passphrase> <!--文件被创建时的权限。如果在部署的时候会创建一个仓库文件或者目录,这时候就可以使用权限(permission)。这两个元素合法的值是一个三位数字,其对应了unix文件系统的权限,如664,或者775。 --> <filePermissions>664</filePermissions> <!--目录被创建时的权限。 --> <directoryPermissions>775</directoryPermissions> </server> </servers> ... </settings> 1.2.6 mirrors【重要】 用来配置相应的镜像仓库。 如果仓库X可以提供仓库Y存储的所有内容,那么就可以认为X是Y的一个镜像。用过Maven的都知道,国外的中央仓库因为网络原因用起来太慢了,所以选择一个国内的镜像就很有必要,配置简单,修改conf文件夹下的settings.xml文件,添加如下镜像配置: <mirrors> <mirror> <id>alimaven</id> <name>aliyun maven</name> <url>http://maven.aliyun.com/nexus/content/groups/public/</url> <mirrorOf>central</mirrorOf> </mirror> <mirror> <id>alimaven1</id> <name>aliyun maven1</name> <url>http://maven.aliyun.com/nexus/content/groups/public/</url> <mirrorOf>*</mirrorOf> </mirror> </mirrors> 其中id与name用来标识唯一的仓库,url为镜像仓库地址,mirrorOf用来匹配当请求什么仓库依赖时使用该镜像。 这里介绍下<mirrorOf>配置的各种选项 <mirrorOf>*<mirrorOf>:匹配所有远程仓库。 <mirrorOf>external:*<mirrorOf>:匹配所有远程仓库,使用localhost的除外,使用file://协议的除外。也就是说,匹配所有不在本机上的远程仓库。 <mirrorOf>repo1,repo2<mirrorOf>:匹配仓库repo1h和repo2,使用逗号分隔多个远程仓库。 <mirrorOf>*,!repo1<mirrorOf>:匹配所有远程仓库,repo1除外,使用感叹号将仓库从匹配中排除。 需要注意的是,由于镜像仓库完全屏蔽了被镜像仓库,当镜像仓库不稳定或者停止服务的时候,Maven仍将无法访问被镜像仓库,因而将无法下载构件。 此外, maven 读取mirror 配置是 从上往下读取的,因此谨慎配置<mirrorOf>*<mirrorOf>,因为如果第一个镜像仓库配置了如此标志,那么如果该仓库即使不存在对应依赖也不会向下游查询。 1.2.7 proxies 用来配置代理。 <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd"> ... <proxies> <!--代理元素包含配置代理时需要的信息 --> <proxy> <!--代理的唯一定义符,用来区分不同的代理元素。 --> <id>myproxy</id> <!--该代理是否是激活的那个。true则激活代理。当我们声明了一组代理,而某个时候只需要激活一个代理的时候,该元素就可以派上用处。 --> <active>true</active> <!--代理的协议。 协议://主机名:端口,分隔成离散的元素以方便配置。 --> <protocol>http</protocol> <!--代理的主机名。协议://主机名:端口,分隔成离散的元素以方便配置。 --> <host>proxy.somewhere.com</host> <!--代理的端口。协议://主机名:端口,分隔成离散的元素以方便配置。 --> <port>8080</port> <!--代理的用户名,用户名和密码表示代理服务器认证的登录名和密码。 --> <username>proxyuser</username> <!--代理的密码,用户名和密码表示代理服务器认证的登录名和密码。 --> <password>somepassword</password> <!--不该被代理的主机名列表。该列表的分隔符由代理服务器指定;例子中使用了竖线分隔符,使用逗号分隔也很常见。 --> <nonProxyHosts>*.google.com|ibiblio.org</nonProxyHosts> </proxy> </proxies> ... </settings> 1.2.8 profiles【重要】 根据环境参数来调整构建配置的列表。用于定义一组profile seetings中的profile是pom.xml中profile元素的裁剪版本。 它包含了id、activation、repositories、pluginRepositories和properties元素。这里的 profile 元素只包含这五个子元素是因为这里只关心构建系统这个整体(这正是 settings.xml 文件的角色定位),而非单独的项目对象模型设置。如果一个settings.xml中的profile被激活,它的值会覆盖任何其它定义在pom.xml中带有相同 id 的profile。 (1)repositories 定义了一组远程仓库的列表,当该属性对应的profile被激活时,会使用该远程仓库。 <repositories> <!--包含需要连接到远程仓库的信息 --> <repository> <!--远程仓库唯一标识 --> <id>codehausSnapshots</id> <!--远程仓库名称 --> <name>Codehaus Snapshots</name> <!--如何处理远程仓库里发布版本的下载 --> <releases> <!--true或者false表示该仓库是否为下载某种类型构件(发布版,快照版)开启。 --> <enabled>false</enabled> <!--该元素指定更新发生的频率。Maven会比较本地POM和远程POM的时间戳。这里的选项是:always(一直),daily(默认,每日),interval:X(这里X是以分钟为单位的时间间隔),或者never(从不)。 --> <updatePolicy>always</updatePolicy> <!--当Maven验证构件校验文件失败时该怎么做-ignore(忽略),fail(失败),或者warn(警告)。 --> <checksumPolicy>warn</checksumPolicy> </releases> <!--如何处理远程仓库里快照版本的下载。有了releases和snapshots这两组配置,POM就可以在每个单独的仓库中,为每种类型的构件采取不同的策略。例如,可能有人会决定只为开发目的开启对快照版本下载的支持。参见repositories/repository/releases元素 --> <snapshots> <enabled /> <updatePolicy /> <checksumPolicy /> </snapshots> <!--远程仓库URL,按protocol://hostname/path形式 --> <url>http://snapshots.maven.codehaus.org/maven2</url> <!--用于定位和排序构件的仓库布局类型-可以是default(默认)或者legacy(遗留)。Maven 2为其仓库提供了一个默认的布局;然而,Maven 1.x有一种不同的布局。我们可以使用该元素指定布局是default(默认)还是legacy(遗留)。 --> <layout>default</layout> </repository> </repositories> (2)properties 定义了一组拓展属性,当对应的profile被激活时该属性有效。 <!-- 1. env.X: 在一个变量前加上"env."的前缀,会返回一个shell环境变量。例如,"env.PATH"指代了$path环境变量(在Windows上是%PATH%)。 2. project.x:指代了POM中对应的元素值。例如: <project><version>1.0</version></project>通过${project.version}获得version的值。 3. settings.x: 指代了settings.xml中对应元素的值。例如:<settings><offline>false</offline></settings>通过 ${settings.offline}获得offline的值。 4. java System Properties: 所有可通过java.lang.System.getProperties()访问的属性都能在POM中使用该形式访问,例如 ${java.home}。 5. x: 在<properties/>元素中,或者外部文件中设置,以${someVar}的形式使用。 --> <properties> <user.install>${user.home}/our-project</user.install> </properties> (3)id 全局唯一标识,如果一个settings.xml中的profile被激活,它的值会覆盖任何其它定义在pom.xml中带有相同 id 的profile。 (4)pluginRepositories 同repositories差不多,不过该标签定义的是插件的远程仓库。 (5)activation 触发激活该profile的条件。 <activation> <!--profile默认是否激活的标识 --> <activeByDefault>false</activeByDefault> <!--当匹配的jdk被检测到,profile被激活。例如,1.4激活JDK1.4,1.4.0_2,而!1.4激活所有版本不是以1.4开头的JDK。 --> <jdk>1.5</jdk> <!--当匹配的操作系统属性被检测到,profile被激活。os元素可以定义一些操作系统相关的属性。 --> <os> <!--激活profile的操作系统的名字 --> <name>Windows XP</name> <!--激活profile的操作系统所属家族(如 'windows') --> <family>Windows</family> <!--激活profile的操作系统体系结构 --> <arch>x86</arch> <!--激活profile的操作系统版本 --> <version>5.1.2600</version> </os> <!--如果Maven检测到某一个属性(其值可以在POM中通过${name}引用),其拥有对应的name = 值,Profile就会被激活。如果值字段是空的,那么存在属性名称字段就会激活profile,否则按区分大小写方式匹配属性值字段 --> <property> <!--激活profile的属性的名称 --> <name>mavenVersion</name> <!--激活profile的属性的值 --> <value>2.0.3</value> </property> <!--提供一个文件名,通过检测该文件的存在或不存在来激活profile。missing检查文件是否存在,如果不存在则激活profile。另一方面,exists则会检查文件是否存在,如果存在则激活profile。 --> <file> <!--如果指定的文件存在,则激活profile。 --> <exists>${basedir}/file2.properties</exists> <!--如果指定的文件不存在,则激活profile。 --> <missing>${basedir}/file1.properties</missing> </file> </activation> 1.2.9 ActiveProfiles 在运行时手工激活的profile。 该元素包含了一组activeProfile元素,每个activeProfile都含有一个 profile id。任何在activeProfile中定义的 profile id,不论环境设置如何,其对应的profile都会被激活。如果没有匹配的profile,则什么都不会发生。 <activeProfiles> <!-- 要激活的profile id --> <activeProfile>env-test</activeProfile> </activeProfiles> 1.2.10 激活profile的三种方式【重要】 上面有提到了两种激活的profile的方式,还有一种可以通过命令行激活profile。 (1)通过ActiveProfiles激活 如题1.2.9可以同时激活多个profile。 (2)通过activation结果 如题1.2.8 (5) (3)通过命令行的方式激活 也是我们经常使用的方式,例如: mvn -P 我们可以通过在pom.xml或setting.xml中指定不同环境的profile,在编译构建不同的项目时,通过上述的命令行方式激活对应的profIle。例如在开发环境下: mvn package -P dev 可以打包开环环境下的项目。 1.3 Q&A 1.3.1 mirrors与repositories的关系【重要】 从上文可以看到,repository标签与mirror标签都定义了一个远程仓库的位置,那么当一个依赖同时存在于两个仓库时,会先加载那个依赖呢? 这里需要阐述一下maven加载真正起作用的repository的步骤, 首先获取pom.xml中repository的集合,然后获取setting.xml中mirror中元素。 如果repository的id和mirror的mirrorOf的值相同,则该mirror替代该repository。 如果该repository找不到对应的mirror,则使用其本身。 依此可以得到最终起作用的repository集合。可以理解mirror是复写了对应id的repository。 mirror相当于一个拦截器,会拦截被mirrorOf匹配到的repository,匹配原则参照1.2.6,在匹配到后,会用mirror里定义的url替换到repository。 没有配置mirror 配置了mirror 二、Pom.xml详解 上章中setting.xml定义了某个环境下全局项目的相关依赖配置,而pom.xml则具体定义了某一个项目中的依赖配置。 2.1 pom元素 2.1.1 基本信息 <project xmlns = "http://maven.apache.org/POM/4.0.0" xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation = "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <!-- 模型版本 一般不用更改 --> <modelVersion>4.0.0</modelVersion> <!-- 公司或者组织的唯一标志,也是打包成jar包路径的依据 --> <!-- 例如com.companyname.project-group,maven打包jar包的路径:/com/companyname/project-group --> <groupId>com.companyname.project-group</groupId> <!-- 项目的唯一ID,一个groupId下面可能多个项目,就是靠artifactId来区分的 --> <artifactId>project</artifactId> <!-- 项目当前版本,格式为:主版本.次版本.增量版本-限定版本号 --> <version>1.0</version> <!--项目产生的构件类型, jar、war主要用来标识项目打包出的服务是jar包还是war包 pom一般用作多moudle的项目中 顶层的pom用来指定子moudle中需要依赖的版本 详见2.1.3 --> <packaging>jar</packaging> <!--定义了本项目的名称与example的网址 --> <name>itoken dependencies</name> <url>www.funtl.com</url> </project> 基本信息都比较易懂,主要定义了本项目的相关配置说明,例如唯一坐标、版本、项目介绍等。 2.1.2 dependencies【重要】 (1)dependencies 本元素定义了项目中所需要的相关依赖信息,也是最重要的元素之一。 <!--该元素描述了项目相关的所有依赖。 这些依赖自动从项目定义的仓库中下载 --> <dependencies> <dependency> <!------------------- 依赖坐标 -------------------> <!--依赖项目的坐标三元素:groupId + artifactId + version --> <!--其中三要素的来源就是2.1.1中别人定义好的相关信息 --> <groupId>org.apache.maven</groupId> <artifactId>maven-artifact</artifactId> <version>3.8.1</version> <!------------------- 依赖传递 -------------------> <!--依赖排除,即告诉maven只依赖指定的项目,不依赖该项目的这些依赖。此元素主要用于解决版本冲突问题 --> <exclusions> <exclusion> <artifactId>spring-core</artifactId> <groupId>org.springframework</groupId> </exclusion> </exclusions> <!-- 可选依赖,用于阻断依赖的传递性。如果在项目B中把C依赖声明为可选,那么依赖B的项目中无法使用C依赖 --> <optional>true</optional> <!------------------- 依赖范围 -------------------> <!--依赖范围。在项目发布过程中,帮助决定哪些构件被包括进来 - compile:默认范围,适用于所有阶段,会随着项目一起发布; - runtime: 在执行时需要使用,如JDBC驱动,适用运行和测试阶段,不同于例如fastjson,需要在编译时使用; - test: 只在测试时使用,用于编译和运行测试代码,例如junit,不同于junit,在发布时并不需要; - optional: 当项目自身被依赖时,标注依赖是否传递。用于连续依赖时使用 --> <scope>test</scope> </dependency> </dependencies> (2)如何解决依赖传递问题或jar包版本冲突问题 解决上述问题一般有两种方式: 当我们作为依赖服务提供者时,可以通过<optional>标签排除掉不想被传递的服务。 <!-- Project A --> <project> ... <dependencies> <!-- declare the dependency to be set as optional --> <dependency> <groupId>sample.ProjectB</groupId> <artifactId>Project-B</artifactId> <version>1.0</version> <optional>true</optional> </dependency> </dependencies> </project> 我们的A服务中引用了外部的依赖B服务,此时有A -> B,在别人引用我们时有C -> A ->B,若此时我们A在提供对外服务时不想让别人依赖B服务,可以在引用B时添加<optional>标签为true,这样带C依赖于A时并不会引入B。如果C在此时想要使用B的相关服务,需要在C的pom中显示的调用B的相关服务。 当我们作为依赖服务使用者时,可以通过<exclusions>来去除掉我们依赖包中所不想依赖的其他服务。 如果此时我们的A服务依赖于B服务,B服务依赖于C服务,则有A ->B ->C,因为某种原因例如jar包冲突,我们在A中并不想依赖于C服务的版本,可以在调用B服务时去除掉C的相关依赖,然后自己再在A中使用C的相关版本。 <project> ... <dependencies> <dependency> <groupId>sample.ProjectB</groupId> <artifactId>Project-B</artifactId> <version>1.0</version> <exclusions> <exclusion> <!-- 排除掉B中C的相关依赖 --> <groupId>sample.ProjectC</groupId> <artifactId>Project-C</artifactId> </exclusion> </exclusions> </dependency> <!-- 自己引用C的相关版本 --> <dependency> <groupId>sample.ProjectC</groupId> <artifactId>Project-C</artifactId> <version>2.0</version> </dependency> </dependencies> </project> 2.1.3<dependencyManagement> 当一个服务中存在有多个moudle时,每个子module都可能引用了相同的jar包,但是如果将版本维护到子module的pom中,当需要升级时需要修改所有的pom文件版本。maven提供了继承的方式来解决此问题。 <!--在父pom中定义子pom需要的相关依赖 --> <dependencyManagement> <dependencies> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.0.0</version> </dependency> </dependencies> </dependencyManagement> 在父pom的<dependencyManagement>中定义子pom需要的依赖及版本。 <!--在子pom中 如下定义了父pom中相关依赖信息 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.10.RELEASE</version> <relativePath/> </parent> <dependencies> <dependency> <!--因为引用了父pom 所以可以不指定版本 maven会自动去父pom中查找指定版本 此处为1.0.0 --> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency> </dependencies> 当我们的pom中有定义父pom的元素后,可以在指定需要的依赖时不指定其版本,maven会帮我们去父pom中查找相关的版本信息。 2.1.4 properties properties主要用来定义常量,通过${value}来使用。 <!--配置依赖版本--> <properties> <!-- Environment Settings --> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <!-- Spring cloud Settings --> <spring-cloud.version>Finchley.RELEASE</spring-cloud.version> <spring-boot-admin.version>2.0.1</spring-boot-admin.version> <zipkin.version>2.10.1</zipkin.version> </properties> <dependencies> <!--spring cloud--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!--zipkin--> <dependency> <groupId>io.zipkin.java</groupId> <artifactId>zipkin</artifactId> <version>${zipkin.version}</version> </dependency> <dependencies> 此外,maven还通过约定大于配置的方式定义了一些常用的属性。 属性 定义 ${basedir} 存放pom.xml和所有的子目录 ${basedir}/src/main/java 项目的java源代码 ${basedir}/src/main/resources 项目的资源,比如说property文件,springmvc.xml ${basedir}/src/main/webapp/WEB-INF web应用文件目录,web项目的信息,比如存放web.xml、本地图片、jsp视图页面 ${basedir}/target 打包输出目录 ${project.version} 项目版本 ${project.groupId} 项目groupid 2.1.5 resources resources标签用来标识项目在编译运行时需要额外编译的文件。例如手工引入jar包、不同运行环境对应不同的profile。 <build> <resources> <!--首先将默认resources目录下的所有文件包括 --> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> <!--只编译所有以.fxml结尾的文件 --> <includes> <include>**/*.fxml</include> </includes> <!--排除掉所有的yaml文件 --> <excludes> <exclude>**/*.yaml</exclude> </excludes> </resource> <!--将不同环境下对应的不同yaml或properties文件编译运行 --> <resource> <!-- <directory>src/main/profiles/dev</directory> <directory>src/main/profiles/beta<directory> <directory>src/main/profiles/pre</directory> --> <directory>src/main/profiles/product</directory> <filtering>true</filtering> <includes> <include>**/*.fxml</include> </includes> </resource> <!--将手工引入的jar包编译运行 --> <resource> <directory>lib</directory> <targetPath>BOOT-INF/lib/</targetPath> <includes> <include>**/*.jar</include> </includes> </resource> </resources> </build> 2.1.6 profile 与setting.xml中profile所不同的是(参照1.2.8),pom中的profile有着更多的标签来描述一组环境。从作用上来区分的话,一般setting.xml用来标识不同的远程仓库,而pom中的profile一般用来标识当前项目属于什么环境,如下是一组常见的pom中的profiles。 <profiles> <profile> <id>dev</id> <!--激活条件 其中默认为该profile 更多激活条件可以参考1.2.8 --> <activation> <activeByDefault>true</activeByDefault> </activation> <!--当此profile被激活时,会将 project.active 的属性赋值为dev --> <properties> <project.active>dev</project.active> </properties> </profile> <profile> <id>test</id> <!--当此profile被激活时,会将 project.active 的属性赋值为test --> <properties> <project.active>test</project.active> </properties> </profile> </profiles> <resources> <resource> <!--根据不同的环境 project.active 取得不同的值 从而会将不同的环境下的yaml或properties文件编译进项目中 达到只需要在编译时指定环境变量即可 不用每次都要修改配置文件 --> <directory>src/main/${project.active}</directory> <filtering>true</filtering> <includes> <include>**/*.fxml</include> </includes> </resource> </resources> 此外,一般通过mvn -P dev/beta/pre/product命令来激活不同的环境,也可以通过其他的方式来激活profile,详见1.2.10。 当然,pom中的profile不止有指定编译环境的功能,同样也可以指定远程仓库等其他功能。 2.1.6 modules 当我们项目中有多个模块时,如果我们要单独打包的话需要在每一个模块都执行对应的maven命令,但是通过<modules>标签可以将子服务或平级服务进行聚合,只需要打包该服务,也就会将其对应的子模块同时打包。 <modules> <!-- 引入子模块所在的相对目录 --> <module>springmybatis</module> <!-- 引入同级模块所在的相对目录 --> <module>../utils</module> </modules> 三、null:jrdp-common:null:jar问题解决 3.1包找不到问题 我们某次在开发功能的时候,在我们的项目中引用了伏羲另外一个系统的jar包,但是预发环境下编译的时候却发现构建失败,原因是 因为当时项目有用到京东自己的仓库,所以我们当时第一反应是去仓库中查找,结果发现仓库中是有这个jar包的。 在发现并不是最常见的jar包不存在的问题后,我们开始分析报错原因,发现报错的jrdp-common这个包并不是我们直接引用的,而是在我们引用的jar包中引用的,并且null:jrdp-common:null:jar可以推测前面应该是groupID与version。 假设我们的项目是A项目,引用的项目是B项目,也就是A->B->jrdp-common 于是我们打开B项目,查看B的pom结构。 发现B项目的pom中确实引用了jrdp-common这个包,但是并没有指定版本,是因为继承了xx-parent这个包,我们在这个包中确实也找到了指定的版本号,因此就排除了项目中没有指定groupid与版本号的问题。 这个时候好像就问题就陷入了思路,但是我们注意到,我们之前另一个私服上也是有这个包的,那么之前的在另一个私服上引用好像是没有问题的,我们查看了私服了上的pom文件,发现也是跟项目一样的。 然后我们就突然想到,会不会是仓库中的包有问题,果不其然 没有指定parent也没有指定版本号,于是我们修改了这个版本的pom,至此问题解决。 总结:因为我们的系统已经被好多个团队中转接手过,因此可能在某些地方踩了不少坑,像这种问题应该就是之前的团队上传了一些有问题的jar包,导致依赖不可用,但是因为我们之前一直用的私服是没有什么问题的,只到这次用到了仓库问题才浮现。 另外,此问题并不具有普适性,但是当遇到了groupid不能未空的时候可以按照此方法进行排查。 作者:京东科技 韩国凯 内容来源:京东云开发者社区

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册