首页 文章 精选 留言 我的

精选列表

搜索[深度集成],共10000篇文章
优秀的个人博客,低调大师

数仓安全:数据脱敏技术深度解析

本文分享自华为云社区《GaussDB(DWS)安全管理之数据脱敏原理与使用方法介绍》,作者: VV一笑。 1. 前言 适用版本:8.2.0及以上版本 GaussDB (DWS)产品数据脱敏功能,是数据库产品内化和夯实数据安全能力的重要技术突破。提供指定用户范围内列级敏感数据的脱敏功能,具有灵活、高效、透明、友好等优点,极大地增强产品的数据安全能力,实现敏感数据的可靠保护。 大数据时代的到来,颠覆了传统业态的运作模式,激发出新的生产潜能。数据成为重要的生产要素,是信息的载体,数据间的流动也潜藏着更高阶维度的价值信息。对于数据控制者和数据处理者而言,如何最大化数据流动的价值,是数据挖掘的初衷和意义。然而,一系列信息泄露事件的曝光,使得数据安全越来越受到广泛的关注。各国各地区逐步建立健全和完善数据安全与隐私保护相关法律法规,提供用户隐私保护的法律保障。如何加强技术层面的数据安全和隐私保护,对数据仓库产品本身提出更多的功能要求,也是数据安全建设最行之有效的办法。 GaussDB (DWS)产品8.1.1版本发布数据脱敏特性,提供指定用户范围内列级敏感数据的脱敏功能,具有灵活、高效、透明、友好等优点,极大地增强产品的数据安全能力。 2. 数据脱敏概念 数据脱敏(Data Masking),顾名思义,是对于敏感数据进行屏蔽。任何泄露后可能会给社会或个人带来严重危害的数据都属于常见的敏感数据。个人身份信息,如姓名、身份证号、住址、手机号、邮箱,企业不适合公开信息,如营业执照号码、税务登记证、员工薪水,设备信息如IP地址、MAC地址,银行卡号、受保护的健康信息、知识产权等都属于敏感信息。对这些敏感信息通过脱敏规则进行数据的变形,实现隐私数据的可靠保护。业界常见的脱敏规则有,替换、重排、加密、截断、掩码,用户也可以根据期望的脱敏算法自定义脱敏规则。 通常,良好的数据脱敏实施,需要遵循如下两个原则,第一,尽可能地为脱敏后的应用,保留脱敏前的有意义信息;第二,最大程度地防止黑客进行破解。 数据脱敏分为静态数据脱敏和动态数据脱敏。静态数据脱敏,是数据的“搬移并仿真替换”,是将数据抽取进行脱敏处理后,下发给下游环节,随意取用和读写的,脱敏后数据与生产环境相隔离,满足业务需求的同时保障生产数据库的安全。而动态数据脱敏则是在访问敏感数据的同时实时进行脱敏处理,可以为不同角色、不同权限、不同数据类型执行不同的脱敏方案,从而确保返回的数据可用而安全。 2.1 DWS动态数据脱敏 GaussDB (DWS)的数据脱敏功能,摒弃业务应用层脱敏依赖性高、代价大等痛点,将数据脱敏内化为数据库产品自身的安全能力,提供了一套完整、安全、灵活、透明、友好的数据脱敏解决方案,属于动态数据脱敏。用户识别敏感字段后,基于目标字段,绑定内置脱敏函数,即可创建脱敏策略。脱敏策略(Redaction Policy)与表对象是多对一的关系。一个脱敏策略包含表对象、生效条件、脱敏列-脱敏函数对三个关键要素,是该表对象上所有脱敏列的集合,不同字段可以根据数据特征采用不同的脱敏函数,根据生效条件、脱敏列-脱敏函数对的不同也可以在同一张表上设置不同的脱敏策略。当且仅当生效条件为真时,查询语句才会触发敏感数据的脱敏,而脱敏过程是内置在SQL引擎内部实现的,对生成环境用户是透明不可见的。 三方代理脱敏工具 vs 数仓DWS脱敏引擎 三方代理工具是用户与数仓集群的中转站,是底座之外的外挂脱敏工具,无法直接参与生成环境,复杂场景较难处理 DWS则是基于数仓底座与存储引擎、SQL引擎直接交互的脱敏引擎,在查询执行过程中实时脱敏,脱敏结果直接返回给用户 代理脱敏工具是静态脱敏,DWS脱敏引擎是动态脱敏 DWS动态脱敏引擎的优势 良好的底座协同。脱敏引擎贯穿于数仓底座的诸多环节,基于预置脱敏策略,参与SQL引擎的解析、重写、优化与执行。脱敏过程用户无感知。 策略可配置。客户可结合自身业务场景识别敏感数据并对业务表的指定列灵活预置脱敏策略。 策略可扩展。产品内置脱敏函数,可以涵盖大部分常见脱敏效果,支持用户自定义脱敏函数。 数据可用性。数据库内原始敏感数据参与运算,仅在出库时刻(返回结果时)才会做脱敏处理。 数据访问受控。脱敏策略生效条件的用户均对原始敏感数据不可见。 全场景数据不泄露。底座交互,可减少敏感数据传输链路潜在的泄露风险, 更加安全可靠,且充分识别各种恶意套取潜在场景并有效防护。 3. 数据脱敏使用方法 动态数据脱敏,是在查询语句执行过程中,根据生效条件是否满足,实现实时的脱敏处理。生效条件,通常是针对当前用户角色的判断。敏感数据的可见范围,即是针对不同用户预设的。系统管理员,具有最高权限,任何时刻对任何表的任何字段都可见。确定受限制用户角色,是创建脱敏策略的第一步。 敏感信息依赖于实际业务场景和安全维度,以自然人为例,用户个体的敏感字段包括:姓名、身份证号、手机号、邮箱地址等等;在银行系统,作为客户,可能还涉及银行卡号、过期时间、支付密码等等;在公司系统,作为员工,可能还涉及薪资、教育背景等;在医疗系统,作为患者,可能还涉及就诊信息等等。所以,识别和梳理具体业务场景的敏感字段,是创建脱敏策略的第二步。 产品内置一系列常见的脱敏函数接口,可以针对不同数据类型和数据特征,指定参数,从而达到不一样的脱敏效果。脱敏函数可采用如下三种内置接口,同时支持自定义脱敏函数。三种内置脱敏函数能够涵盖大部分场景的脱敏效果,不推荐使用自定义脱敏函数。 MASK_NONE:不作脱敏处理,仅内部测试用。 MASK_FULL:全脱敏成固定值。 MASK_PARTIAL:使用指定的脱敏字符对脱敏范围内的内容做部分脱敏 不同脱敏列可以采用不同的脱敏函数。比如,手机号通常显示后四位尾号,前面用"*"替换;金额统一显示为固定值0,等等。确定脱敏列需要绑定的脱敏函数,是创建脱敏策略的第三步。 以某公司员工表emp,表的属主用户alice以及用户matu、july为例,简单介绍数据脱敏的使用过程。其中,表emp包含员工的姓名、手机号、邮箱、发薪卡号、薪资等隐私数据,用户alice是人力资源经理,用户matu和july是普通职员。 假设表、用户及用户对表emp的查看权限均已就绪。 1.创建脱敏策略mask_emp,仅允许alice查看员工所有信息,matu和july对发薪卡号、薪资均不可见。字段card_no是数值类型,采用MASK_FULL全脱敏成固定值0;字段card_string是字符类型,采用MASK_PARTIAL按指定的输入输出格式对原始数据作部分脱敏;字段salary是数值类型,采用数字9部分脱敏倒数第二位前的所有数位值。 postgres=# CREATE REDACTION POLICY mask_emp ON emp WHEN (current_user != 'alice') ADD COLUMN card_no WITH mask_full(card_no), ADD COLUMN card_string WITH mask_partial(card_string, 'VVVVFVVVVFVVVVFVVVV','VVVV-VVVV-VVVV-VVVV','#',1,12), ADD COLUMN salary WITH mask_partial(salary, '9', 1, length(salary) - 2); 切换到matu和july,查看员工表emp。 postgres=> SET ROLE matu PASSWORD 'Gauss@123'; postgres=> SELECT * FROM emp; id | name | phone_no | card_no | card_string | email | salary | birthday ----+------+-------------+---------+---------------------+----------------------+------------+--------------------- 1 | anny | 13420002340 | 0 | ####-####-####-1234 | smithWu@163.com | 99999.9990 | 1999-10-02 00:00:00 2 | bob | 18299023211 | 0 | ####-####-####-3456 | 66allen_mm@qq.com | 9999.9990 | 1989-12-12 00:00:00 3 | cici | 15512231233 | | | jonesishere@sina.com | | 1992-11-06 00:00:00 (3 rows) postgres=> SET ROLE july PASSWORD 'Gauss@123'; postgres=> SELECT * FROM emp; id | name | phone_no | card_no | card_string | email | salary | birthday ----+------+-------------+---------+---------------------+----------------------+------------+--------------------- 1 | anny | 13420002340 | 0 | ####-####-####-1234 | smithWu@163.com | 99999.9990 | 1999-10-02 00:00:00 2 | bob | 18299023211 | 0 | ####-####-####-3456 | 66allen_mm@qq.com | 9999.9990 | 1989-12-12 00:00:00 3 | cici | 15512231233 | | | jonesishere@sina.com | | 1992-11-06 00:00:00 (3 rows) 2.由于工作调整,matu进入人力资源部参与公司招聘事宜,也对员工所有信息可见,修改策略生效条件。 postgres=> ALTER REDACTION POLICY mask_emp ON emp WHEN(current_user NOT IN ('alice', 'matu')); 切换到用户matu和july,重新查看员工表emp。 postgres=> SET ROLE matu PASSWORD 'Gauss@123'; postgres=> SELECT * FROM emp; id | name | phone_no | card_no | card_string | email | salary | birthday ----+------+-------------+------------------+---------------------+----------------------+------------+--------------------- 1 | anny | 13420002340 | 1234123412341234 | 1234-1234-1234-1234 | smithWu@163.com | 10000.0000 | 1999-10-02 00:00:00 2 | bob | 18299023211 | 3456345634563456 | 3456-3456-3456-3456 | 66allen_mm@qq.com | 9999.9900 | 1989-12-12 00:00:00 3 | cici | 15512231233 | | | jonesishere@sina.com | | 1992-11-06 00:00:00 (3 rows) postgres=> SET ROLE july PASSWORD 'Gauss@123'; postgres=> SELECT * FROM emp; id | name | phone_no | card_no | card_string | email | salary | birthday ----+------+-------------+---------+---------------------+----------------------+------------+--------------------- 1 | anny | 13420002340 | 0 | ####-####-####-1234 | smithWu@163.com | 99999.9990 | 1999-10-02 00:00:00 2 | bob | 18299023211 | 0 | ####-####-####-3456 | 66allen_mm@qq.com | 9999.9990 | 1989-12-12 00:00:00 3 | cici | 15512231233 | | | jonesishere@sina.com | | 1992-11-06 00:00:00 (3 rows) 3.员工信息phone_no、email和birthday也是隐私数据,更新脱敏策略mask_emp,新增三个脱敏列。 postgres=> ALTER REDACTION POLICY mask_emp ON emp ADD COLUMN phone_no WITH mask_partial(phone_no, '*', 4); postgres=> ALTER REDACTION POLICY mask_emp ON emp ADD COLUMN email WITH mask_partial(email, '*', 1, position('@' in email)); postgres=> ALTER REDACTION POLICY mask_emp ON emp ADD COLUMN birthday WITH mask_full(birthday); 切换到用户july,查看员工表emp。 postgres=> SET ROLE july PASSWORD 'Gauss@123'; postgres=> SELECT * FROM emp; id | name | phone_no | card_no | card_string | email | salary | birthday ----+------+-------------+---------+---------------------+----------------------+------------+--------------------- 1 | anny | 134******** | 0 | ####-####-####-1234 | ********163.com | 99999.9990 | 1970-01-01 00:00:00 2 | bob | 182******** | 0 | ####-####-####-3456 | ***********qq.com | 9999.9990 | 1970-01-01 00:00:00 3 | cici | 155******** | | | ************sina.com | | 1970-01-01 00:00:00 (3 rows) 4.考虑用户交互的友好性,GaussDB (DWS) 提供系统视图redaction_policies和redaction_columns,方便用户直接查看更多脱敏信息。 postgres=> SELECT * FROM redaction_policies; object_schema | object_owner | object_name | policy_name | expression | enable | policy_description ---------------+--------------+-------------+-------------+-----------------------------------+--------+-------------------- public | alice | emp | mask_emp | ("current_user"() = 'july'::name) | t | (1 row) postgres=> SELECT object_name, column_name, function_info FROM redaction_columns; object_name | column_name | function_info -------------+-------------+------------------------------------------------------------------------------------------------------- emp | card_no | mask_full(card_no) emp | card_string | mask_partial(card_string, 'VVVVFVVVVFVVVVFVVVV'::text, 'VVVV-VVVV-VVVV-VVVV'::text, '#'::text, 1, 12) emp | email | mask_partial(email, '*'::text, 1, "position"(email, '@'::text)) emp | salary | mask_partial(salary, '9'::text, 1, (length((salary)::text) - 2)) emp | birthday | mask_full(birthday) emp | phone_no | mask_partial(phone_no, '*'::text, 4) (6 rows) 5.突然某一天,公司内部可共享员工信息时,直接删除表emp的脱敏策略mask_emp即可。 postgres=> DROP REDACTION POLICY mask_emp ON emp; 更多用法详情,请参考GaussDB (DWS)产品文档。 4. 可算不可见的数据脱敏 在使用数据脱敏功能时,存在先对敏感数据进行加工计算在进行输出的情况。对于这种情况,如果采用经过脱敏后的数据进行库内计算,那么在例如聚集函数、过滤条件等条件下经过脱敏的数据本身会对查询结果产生影响,因此针对这一现象对数据脱敏引入了可算不可见功能。所谓可算不可见,就是在数据库内使用原始的敏感数据参与加工计算,只在出库时对敏感数据进行脱敏处理。想要使用可算不可见功能,需要设置开关enable_redactcol_computable=on。 目前支持将敏感数据直接参与加工计算的场景如下: SELECT nullif(salary, 1) FROM emp;投影列表达式nullif SELECT email LIKE ‘%.com’ FROM emp;投影列LIKE表达式 SELECT to_days(birth) FROM david.emp;投影列函数to_days SELECT count(*) FROM emp;聚集函数 SELECT * FROM emp WHERE cardid IS NOT NULL; 过滤条件 SELECT name, avg(salary) * 12 FROM emp GROUP BY name; 分组条件(name是脱敏字段) SELECT (SELECT salary+10 FROM emp ORDER BY id LIMIT 1);子查询位置投影列表达式 两表使用敏感字段作关联条件 CTE表达式投影列 出库时刻会触发数据脱敏的场景如下: 表查询 视图查询 DML RETURNING子句 COPY导出 GDS外表导出 游标CURSOR… FETCH 存储过程定义使用脱敏表,查询存储过程 4.1 脱敏策略的继承 对于INSERT/UPDATE/MERGE INTO/CREATE TABLE AS语句,当子查询是对某个敏感字段的投影操作时,就会触发脱敏继承,从而使得包含了敏感信息的新表上含有与源表相同的脱敏策略,避免了通过在新表中插入源表敏感数据,再查询新表导致敏感数据泄露的问题。此外,脱敏策略的继承属于表维度的活动,继承活动不关注子查询部分当前会话或角色条件下脱敏策略是否生效。 继承脱敏策略的第一步是进行敏感血缘分析,对于任何用户执行DML语句,都遍历子查询部分源表及其目标投影列,一旦源表存在脱敏策略且目标投影列是脱敏字段,则认为使用源表插入/更新目标表数据时,存在暴露源表敏感数据的风险。 对脱敏策略进行继承时,首先从从遍历标记的源表敏感信息中,生成作用于目标表的脱敏策略信息及脱敏字段信息。随后系统内置生成策略创建语句并执行写入系统表pg_redaction_policy/pg_redaction_column,对于内置创建的脱敏策略,统一命名为“inherited_rp”。最后再将系统表元数据inherit标记字段为true。 注意,如果INSERT执行会话/用户满足触发条件,当带有RETURNING子句打印插入结果时,返回结果会脱敏,日志信息“Parent redaction policies/columns”会记录策略继承的来源。 随着脱敏策略继承行为的引入,产生了一些脱敏策略冲突的场景。例如SELECT语句查询目标列非原始敏感字段,而是敏感字段的复杂表达式,表达式先算后脱敏,此时如何界定脱敏行为?SETOP集合运算两个子分支的对应同一目标列采用不同的脱敏效果,此时如何界定外层语句目标列的脱敏结果?多次INSERT/UPDATE操作的敏感血缘分析中,同一目标列的源表投影列采用的不一样的脱敏效果,且源表策略的生效条件也可能不同,此时脱敏策略该如何继承? 针对这些冲突场景,基于保护用户任何敏感数据不致泄露优先于敏感数据脱敏效果不具有原始特征的原则,当遇到脱敏效果冲突时,都提升为通用脱敏效果mask_full。mask_full是可覆盖任何数据类型的全脱敏函数,只关注表达式返回值类型,可以保证脱敏数据不会泄露,但是会导致脱敏结果无法表征原始数据特征,使得脱敏结果的可读性较差。此外,针对length、count等函数表达式,其计算结果不会暴露任何原始数据特征及信息,所以提供了ALTER FUNCTION … [NOT] MASKED语法,支持用户手动配置不脱敏函数白名单。 4.2 防护恶意套取 已知某些敏感信息,通过多次试探性匹配,反向佐证可见的用户信息,从而窃取用户的隐私数据。借助助等值判断形式表达式的过滤条件或投影操作试探性匹配敏感信息。这些通过已知常量值和等值/类等值判断表达式来进行套取用户隐私数据的行为成为恶意套取。 postgres=> SELECT name FROM emp WHERE name in('张三'); INFO: The result of target column {"name"} is masked. name ------ 张* (1 row) 如上述例子所示,尽管已经对用户的name信息进行了脱敏,但由于查询条件是针对’张三’用户,因此即使被脱敏为’张*’,我们还是可以很容易确定这里的脱敏前信息是‘张三’,从而导致张三用户的信息存在泄漏的风险。 针对这一问题,我们采用了“悲观主义”模式,任何常量等值判断都可能存在恶意套取的风险,都应当禁止,实例如下: postgres=> SELECT name FROM emp WHERE name in('张三'); ERROR: Redaction column "name" cannot be referenced in equivalence conditions with const value. HINT: Please use EXPLAIN command to see more details. 禁止使用常量恶意套取的场景总结如下: 1.脱敏字段的常量等值判断表达式、复合表达式、等价表达式 2.假设name字段是脱敏字段且当前会话满足策略触发条件,则语句有如下(且不限于)特征,存在恶意套取风险,禁止执行: • name = '张三‘ • name = ‘张三’ OR name = ‘李四’ • name in (‘张三’, ‘李四’) • CASE name WHEN ‘张三’ THEN true … • CASE WHEN name in (‘张三’, ‘李四’) THEN … • 高级包dbms_output.put_line 3.语句执行会报错:ERROR: Redaction column “name” cannot be referenced in equivalence conditions with const value. 5. 数据脱敏实现原理 GaussDB (DWS)数据脱敏功能,基于SQL引擎既有的实现框架,在受限用户执行查询语句过程中,实现外部不感知的实时脱敏处理。关于其内部实现,如上图所示。我们将脱敏策略(Redaction Policy)视为表对象上绑定的规则,在优化器查询重写阶段,遍历Query Tree中TargetList的每个TargetEntry,如若涉及基表的某个脱敏列,且当前脱敏规则生效(即满足脱敏策略的生效条件且enable开启状态),则断定此TargetEntry中涉及要脱敏的Var对象,此时,遍历脱敏列系统表pg_redaction_column,查找到对应脱敏列绑定的脱敏函数,将其替换成对应的FuncExpr即可。经过上述对Query Tree的重写处理,优化器会自动生成新的执行计划,执行器遵照新的计划执行,查询结果将对敏感数据做脱敏处理。 带有数据脱敏的语句执行,相较于原始语句,增加了数据脱敏的逻辑处理,势必会给查询带来额外的开销。这部分开销,主要受表的数据规模、查询目标列涉及的脱敏列数、脱敏列采用的脱敏函数三方面因素影响。 针对简单查询语句,以tpch表customer为例,针对上述因素展开测试,如下图所示。 图(a)、(b)中基表customer根据字段类型和特征,既有采用MASK_FULL脱敏函数的,也有采用MASK_PARTIAL脱敏函数的。MASK_FULL对于任何长度和类型的原始数据,均只脱敏成固定值,所以,输出结果相较于原始数据,差异很大。图(a)显示不同数据规模下,脱敏和非脱敏场景简单查询语句的执行耗时。实心图标为非脱敏场景,空心图标为被限制用户,即脱敏场景。可见,数据规模越大,带有脱敏的查询耗时与原始语句差异越大。图(b)显示10x数据规模下查询涉及脱敏列数不同对于语句执行性能的影响。涉及1列脱敏列时,带有脱敏的查询比原始语句慢,追溯发现,此列采用的是MASK_PARTIAL部分脱敏函数,查询结果只是改变了结果的格式,结果内容的长度并未变化,符合“带有脱敏的语句执行会有相应的性能劣化”的理论猜想。随着查询涉及脱敏列数的增加,我们发现一个奇怪的现象,脱敏场景反倒比原始语句执行更快。进一步追溯多列场景下脱敏列关联的脱敏函数,发现,正是因为存在使用MASK_FULL全脱敏函数的脱敏列,导致输出结果集部分相比原始数据节省很多时间开销,从而多列查询下带有数据脱敏的简单查询反倒提速不少。 为了佐证上述猜测,我们调整脱敏函数,所有脱敏列均采用MASK_PARTIAL对原始数据做部分脱敏,从而能够在脱敏结果上保留原始数据的外部可读性。于是,如图©所示,当脱敏列均关联部分脱敏函数时,带有数据脱敏的语句比原始语句劣化10%左右,理论上讲,这种劣化是在可接受范围的。上述测试仅针对简单的查询语句,当语句复杂到带有聚集函数或复杂表达式运算时,可能这种性能劣化会更明显。 6. 总结 GaussDB (DWS)产品数据脱敏功能,是数据库产品内化和夯实数据安全能力的重要技术突破,主要涵盖以下三个方面: 一套简单、易用的数据脱敏策略语法; 一系列可覆盖常见隐私数据脱敏效果的、灵活配置的内置脱敏函数; 一个完备、便捷的脱敏策略应用方案,使得原始语句在执行过程中可以实时、透明、高效地实现脱敏。 总而言之,此数据脱敏功能可以充分满足客户业务场景的数据脱敏诉求,支持常见隐私数据的脱敏效果,实现敏感数据的可靠保护。 【温馨提示】使用过程中,如有疑问,欢迎随时交流反馈。 点击关注,第一时间了解华为云新鲜技术~

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

深度剖析线上应用节点流量隔离技术

作者:谢文欣(风敬) 为什么要做流量隔离 源于一个 EDAS 客户遇到的棘手情况:他们线上的一个 Pod CPU 指标异常,为了进一步诊断问题,客户希望在不重建此 Pod 的情况下保留现场,但诊断期间流量还会经过这个异常 Pod,导致影响服务质量,于是询问我们有没有办法可以把流入异常节点的流量摘除掉,形成一个隔离的诊断环境。经诊断后,如果异常可以修复,待修复完成后,再解除流量隔离,节点恢复正常工作。 除了在诊断场景需要对所有输入流量进行隔离外,在一些线上演练中还需对特定流量进行隔离以实现模拟演练效果。面对这类流量隔离问题时,我们首先考虑的是全链路流量控制。目前,EDAS 上的全链路流控能够在不重启应用节点的情况下控制流量走向。然而,全链路流控仅能控制微服务框架流量,无法满足隔离所有或特定流量的需求。 为此,我们进行了深入研究,实现了一套开箱即用的流量隔离工具,能够动态隔离特定流量,并在隔离后可随时恢复,以满足各种场景下的流量隔离需求。 隔离哪些流量 流量隔离的目的是阻断应用节点的流入流量,首先明确下微服务应用节点流入的流量有哪些。 流入微服务应用节点的流量大致可以分为两大类:服务流量、事件流量。以常见的微服务应用为例,其流量组成如下图所示。 服务流量指一个微服务应用的所有节点作为一个网络实体,对外提供一组服务,被其他系统、服务或用户发起请求产生的调用。对于服务流量,节点本身不直接决定流量的流入与否,而是由一套服务注册与发现机制维护流量路径的逻辑关系。节点经过注册,成为服务的一个端点。调用方对服务发起请求时,被调用方是服务的逻辑地址,经过转发和地址转换,请求被路由到服务端点的实体节点。隔离服务流量的一个可选方案是破坏服务调用的通信连接,但这种方法势必会影响服务质量。在保持服务整体功能正常运行的同时,一个更优雅的方案是破坏服务与实体节点之间的映射关系。这样,在路由过程中,流量将按照预期避开特定节点,而被引导至其他节点。服务流量主要涵盖了 K8s Service 以及使用 Nacos 等注册中心发布的由 Spring Cloud、Dubbo 等微服务框架构建的服务。 事件流量指应用内部的事件驱动架构产生的流量,包括由中间件传递至应用节点的事件或消息,这类通信通常是异步的,例如来自消息队列 RocketMQ 的消息流量,来自调度框架 SchedulerX 触发调度的事件流量。中间件和应用节点之间通常遵循 client-server 通信,因此可以考虑通过破坏通信连接来隔离中间件发来的消息或事件流量。 服务流量隔离 K8s Service 对于使用 K8s Service 暴露服务的应用,Service 声明的服务与应用 Pod 之间的映射关系由 Endpoints 对象维护。Endpoints 对象的 subsets 字段表示 Serivce 的一组端点,每个端点代表一个应用 Pod 的网络地址,即一个实际提供服务的 Pod 实例。subsets 字段包含了这些端点的详细信息,如 IP 地址和端口。Endpoints 控制器通过 API Server 监听 Pod 的变更情况,并随后同步更新 Endpoints 的端点列表。因此,要隔离 K8s Service 的流量,需要破坏 Endpoints 对 Pod 的指向,将待隔离的 Pod 网络地址从 Endpoints 的端点列表中移除。同时,需要通过 Informer 机制监听 Endpoints 对象的变化,以保证 Endpoints 在后续变更或控制器 Reconcile 过程中也能维持预期状态。 Dubbo 对于使用注册中心暴露服务的应用,注册中心负责管理服务节点。只要注册关系存在且应用节点存活,注册中心会将流量调度到该应用节点。而破坏服务注册关系的操作被称为服务注销,应用节点进行服务注销之后,注册中心便不会将流量导入到注销节点,也就形成了流量隔离。 要实现 Dubbo 微服务的动态注销,首先需要从源码级别了解 Dubbo 服务注册原理。以 Dubbo 2.7.0 为例,其服务注册模块的大致结构如下: Dubbo 应用中存在一个 AbstractRegistryFactory 单例,负责注册中心 Registry 的容器初始化。类属性 REGISTRIES 维护了微服务列表与注册中心实例的映射关系。 AbstractRegistry 实现了 Registry 接口,作为一个模板,实现了特定的公共方法,如服务注册(register)、服务注销(unregister)等。它还维护了已注册服务 URL 列表。 FailbackRegistry 基于 AbstractRegistry,提供了失败重试机制。同时,它提供了注册中心的 doRegister 和 doUnregister 抽象方法。当执行 register/unregister 时,会调用 doRegister/doUnregister 方法。 注册中心(如 NacosRegistry、RedisRegistry)实现了具体的服务注册(doRegister)和服务注销(doUnregister)逻辑。 由源码可见,Dubbo 的服务注册模块已经内置了可动态注销/重注册服务的方法。因此, Dubbo 微服务隔离可通过主动触发其注册中心对象的服务注销方法来实现。同理,如果需要恢复服务节点,主动触发服务注册方法,更新注册中心的服务映射关系。 在确定「触发注册中心对象的服务注销方法」这一技术方向之后,需要解决如何获取对象和触发方法这两个问题。在 Java 环境中,我们很容易想到使用 Agent 技术对进程行为进行干预。然而,常规的基于字节码埋点的 Agent 无法满足随时启用的需求,因为它依赖于应用代码的具体执行路径。只有当执行路径触及埋点时,Agent 代码才会被触发,从而从上下文中获取对象并通过反射调用相关方法。然而,与注册中心相关的埋点通常设置在程序启动初期,此时会执行注册中心初始化、服务注册等操作,比较容易找到合适的埋点。在程序对外提供服务期间,程序主动发起的注册中心操作较少,因此很难找到合适的埋点来获取预期的上下文。在需要隔离应用流量时,此时动态挂入 Agent,由于执行路径中没有能获取注册中心上下文的埋点,Agent 代码将无法生效。 因此,我们需要一个能够主动获取对象并触发对象方法的即开即用的 Agent 工具。在这里,我们引入了 JVMTI 技术。JVMTI(JVM Tool Interface)是一种虚拟机提供的原生编程接口,允许开发人员创建 Agent 以探查 JVM 内部的运行状态,甚至控制 JVM 应用程序的执行。JVMTI 能够从 Java 堆中获取特定类和对象信息,然后通过反射触发方法,完美地满足了我们的需求。 由于 JVMTI 是一套 JVM 原生编程接口,需要使用 C/C++ 进行编写。编译后的产物是动态链接库(.so 或 .dll 文件)。Java 运行环境通过 JNI(Java Native Interface)与 JVMTI 进行交互。整体作为一个 Java Agent,通过 Attach API 动态地挂载到目标 JVM 中。 得益于 JVMTI Agent 的强大功能,我们能够在 Java 应用内相对简便地实施某些控制逻辑。为实现 Dubbo 服务流量隔离,首先需要获取 AbstractRegistryFactory 类的静态属性 REGISTRIES,它包含应用当前已注册服务的服务列表以及相应的注册中心 Registry 实例。对于特定的微服务,仅需调用其注册中心 Registry 的 register/unregister 方法,便可实现服务的动态摘除和恢复。这一方案直接在较高抽象层级上操作,而无需依赖具体的注册中心 Registry 实现类,使其兼容所有注册中心。 Spring CLoud Spring Cloud 服务流量隔离方法类似于 Dubbo,在了解 Spring Cloud 服务注册原理后,获得服务注册/注销方法路径,然后通过 JVMTI 干预应用的服务注册/注销行为。 Spring Cloud 的服务注册原理较为简单。在 Spring 容器启动时,AbstractAutoServiceRegistration 监听启动事件,并调用 ServiceRegistry 的 register 方法将 Registration(服务实例数据)注册到注册中心。例如,Nacos服务注册类 NacosServiceRegistry 实现了 ServiceRegistry 接口,通过重载 register/deregister 方法完成服务在注册中心的注册和注销。 // 服务注册类 public abstract class AbstractAutoServiceRegistration<R extends Registration>...{ // 注册中心实例 private final ServiceRegistry<R> serviceRegistry; // 服务注册 protected void register() { this.serviceRegistry.register(getRegistration()); } // 服务注销 protected void deregister() { this.serviceRegistry.deregister(getRegistration()); } } 在处理 Spring Cloud 服务流量隔离时,首先获取 AbstractAutoServiceRegistration 的服务注册实例,然后调用 register/deregister 方法以在注册中心上完成服务的注销和重注册。这种方法同样不依赖于某个特定注册中心的具体实现类,兼容所有注册中心。 事件流量隔离 应用节点和中间件通常采用 client-server 模式通信,像 RocketMQ 和 SchedulerX 使用了 Netty 作为底层网络框架完成客户端和服务端通信 。在此,我们以 RocketMQ 为例,来说明如何实现类似的事件驱动中间件的流量隔离。 RocketMQ client 端的主要实现类是 NettyRemotingClient。如下图所示,NettyRemotingClient 类中的属性 channelTables 存储了用于传输数据的 Channel,而 lockChannelTables 是用于控制 channelTables 更新的锁。与此同时,有几个 invoke 方法负责处理通信过程。 通信处理流程如下图。首先,尝试从 channelTables 中获取用于通信的 Channel。如果没有可用的 Channel,则重新连接 server 端以创建 Channel。为了保证线程间同步,新 Channel 更新到 channelTables 时需要获得 lockChannelTables 锁。如果在指定时间窗口内 lockChannelTables 一直被占用,将会抛出连接异常。 根据以上的原理分析,我们可以通过占用 lockChannelTables 锁来阻止 Channel 的建立,再把现存的 Channel 关闭,则 client 端在 lockChannelTables 被释放之前都无法与 server 端建立通信连接。若要恢复流量,仅需释放 lockChannelTables 锁,client 端将自动重建 Channel 并恢复通信。由于这种管控是在网络客户端层进行的,因此它不受应用消息模型的影响,既适用于同步消息也适用于异步消息;同时也与 client 角色无关,既适用于消费者也适用于生产者。 结语 目前流量隔离工具在 EDAS-云原生工具箱 可试用体验。如果对流量隔离以及更多云原生工具感兴趣,欢迎留言或加入钉群:21958624 与我们进行沟通与交流。

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

每日一博 | React Hooks 源码深度解析

作者:京东零售 郑炳懿 前言 React Hooks是React16.8 引入的一个新特性,它允许函数组件中使用state和其他 React 特性,而不必使用类组件。Hooks是一个非常重要的概念,因为它们提供了更简单、更易于理解的React开发体验。 React Hooks的核心源码主要包括两个部分:React内部的Hook管理器和一系列预置的Hook函数。 首先,让我们看一下React内部的Hook管理器。这个管理器是React内部的一个重要机制,它负责管理组件中的所有Hook,并确保它们在组件渲染期间以正确的顺序调用。 内部Hook管理器 示例: const Hook = { queue: [], current: null, }; function useState(initialState) { const state = Hook.current[Hook.queue.length]; if (!state) { Hook.queue.push({ state: typeof initialState === 'function' ? initialState() : initialState, setState(value) { this.state = value; render(); }, }); } return [state.state, state.setState.bind(state)]; } function useHook(callback) { Hook.current = { __proto__: Hook.current, }; try { callback(); } finally { Hook.current = Hook.current.__proto__; } } function render() { useHook(() => { const [count, setCount] = useState(0); console.log('count:', count); setTimeout(() => { setCount(count + 1); }, 1000); }); } render(); 在这个示例中,Hook对象有两个重要属性:queue和current。queue存储组件中所有Hook的状态和更新函数,current存储当前正在渲染的组件的Hook链表。useState和useHook函数则分别负责创建新的Hook状态和在组件中使用Hook。 预置 Hook 函数 useState Hook 以下是useState Hook的实现示例: function useState(initialState) { const hook = updateWorkInProgressHook(); if (!hook.memoizedState) { hook.memoizedState = [ typeof initialState === 'function' ? initialState() : initialState, action => { hook.queue.pending = true; hook.queue.dispatch = action; scheduleWork(); }, ]; } return hook.memoizedState; } 上述代码实现了useState Hook,其主要作用是返回一个state和更新函数的数组,state 初始值为initialState。 在这个实现中,updateWorkInProgressHook()函数用来获取当前正在执行的函数组件的 fiber 对象并判断是否存在对应的hook。它的实现如下: function updateWorkInProgressHook() { const fiber = getWorkInProgressFiber(); let hook = fiber.memoizedState; if (hook) { fiber.memoizedState = hook.next; hook.next = null; } else { hook = { memoizedState: null, queue: { pending: null, dispatch: null, last: null, }, next: null, }; } workInProgressHook = hook; return hook; } getWorkInProgressFiber()函数用来获取当前正在执行的函数组件的fiber对象,workInProgressHook则用来存储当前正在执行的hook对象。在函数组件中,每一个useState调用都会创建一个新的 hook 对象,并将其添加到fiber对象的hooks链表中。这个hooks链表是通过fiber对象的memoizedState属性来维护的。 我们还需要注意到在useState Hook的实现中,每一个hook对象都包含了一个queue对象,用来存储待更新的状态以及更新函数。scheduleWork()函数则用来通知React调度器有任务需要执行。 在React的源码中,useState函数实际上是一个叫做useStateImpl的内部函数。 下面是useStateImpl的源码: function useStateImpl<S>(initialState: (() => S) | S): [S, Dispatch<SetStateAction<S>>] { const dispatcher = resolveDispatcher(); return dispatcher.useState(initialState); } 可以看到,useStateImpl函数的作用就是获取当前的dispatcher并调用它的useState方法,返回一个数组,第一个元素是状态的值,第二个元素是一个dispatch函数,用来更新状态。这里的resolveDispatcher函数用来获取当前的dispatcher,其实现如下: function resolveDispatcher(): Dispatcher { const dispatcher = currentlyRenderingFiber?.dispatcher; if (dispatcher === undefined) { throw new Error('Hooks can only be called inside the body of a function component. (https://fb.me/react-invalid-hook-call)'); } return dispatcher; } resolveDispatcher函数首先尝试获取当前正在渲染的fiber对象的dispatcher属性,如果获取不到则说 明当前不在组件的渲染过程中,就会抛出一个错误。 最后,我们来看一下useState方法在具体的dispatcher实现中是如何实现的。我们以useReducer的 dispatcher为例,它的实现如下: export function useReducer<S, A>( reducer: (prevState: S, action: A) => S, initialState: S, initialAction?: A, ): [S, Dispatch<A>] { const [dispatch, currentState] = updateReducer<S, A>( reducer, // $FlowFixMe: Flow doesn't like mixed types [initialState, initialAction], // $FlowFixMe: Flow doesn't like mixed types reducer === basicStateReducer ? basicStateReducer : updateStateReducer, ); return [currentState, dispatch]; } 可以看到,useReducer方法实际上是调用了一个叫做updateReducer的函数,返回了一个包含当前状态和dispatch函数的数组。updateReducer的实现比较复杂,涉及到了很多细节,这里不再展开介绍。 useEffect Hook useEffect是React中常用的一个Hook函数,用于在组件中执行副作用操作,例如访问远程数据、添加/移除事件监听器、手动操作DOM等等。useEffect的核心功能是在组件的渲染过程结束之后异步执行回调函数,它的实现方式涉及到 React 中的异步渲染机制。 以下是useEffect Hook的实现示例: function useEffect(callback, dependencies) { // 通过调用 useLayoutEffect 或者 useEffect 方法来获取当前的渲染批次 const batch = useContext(BatchContext); // 根据当前的渲染批次判断是否需要执行回调函数 if (shouldFireEffect(batch, dependencies)) { callback(); } // 在组件被卸载时清除当前 effect 的状态信息 return () => clearEffect(batch); } 在这个示例中,useEffect接收两个参数:回调函数和依赖项数组。当依赖项数组中的任何一个值发生变化时, React会在下一次渲染时重新执行useEffect中传入的回调函数。 useEffect函数的实现方式主要依赖于React中的异步渲染机制。当一个组件需要重新渲染时,React会将所有的state更新操作加入到一个队列中,在当前渲染批次结束之后再异步执行这些更新操作,从而避免在同一个渲染批次中连续执行多次更新操作。 在useEffect函数中,我们通过调用useContext(BatchContext)方法来获取当前的渲染批次,并根据shouldFireEffect方法判断是否需要执行回调函数。在回调函数执行完毕后,我们需要通过clearEffect方法来清除当前effect的状态信息,避免对后续的渲染批次产生影响。 总结 总的来说,React Hooks的实现原理并不复杂,它主要依赖于React内部的fiber数据结构和调度系统,通过这些机制来实现对组件状态的管理和更新。Hooks能够让我们在函数组件中使用状态和其他React特性,使得函数组件的功能可以和类组件媲美。 除了useState、useEffect等hook,React还有useContext等常用的Hook。它们的实现原理也基本相似,都是利用fiber架构来实现状态管理和生命周期钩子等功能。 以上是hook简单实现示例,它们并不是React中实际使用的代码,但是可以帮助我们更好地理解hook的核心实现方式。

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

从源码层面深度剖析Spring循环依赖

作者:郭艳红 以下举例皆针对单例模式讨论 图解参考 https://www.processon.com/view/link/60e3b0ae0e3e74200e2478ce 1、Spring 如何创建Bean? 对于单例Bean来说,在Spring容器整个生命周期内,有且只有一个对象。 Spring 在创建 Bean 过程中,使用到了三级缓存,即 DefaultSingletonBeanRegistry.java 中定义的: /** Cache of singleton objects: bean name to bean instance. */ private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); ​ /** Cache of singleton factories: bean name to ObjectFactory. */ private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16); ​ /** Cache of early singleton objects: bean name to bean instance. */ private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16); 以 com.gyh.general 包下的 OneBean 为例,debug springboot 启动过程,分析spring是如何创建bean的。 参考图中 spring创建bean 的过程。其中最关键的几步有: 1. getSingleton(beanName, true) 依次从一二三级缓存中查找bean对象,如果缓存中存在对象,则直接返回(early); 2. createBeanInstance(beanName, mbd, args) 选一个合适的构造函数,new实例对象(instance),此时的instance中依赖的属性还都是null,属于半成品; 3. singletonFactories.put(beanName, oneSingletonFactory) 利用上一步的instance,构建一个 singletonFactory,并将其放到三级缓存中; 4. populateBean(beanName, mbd, instanceWrapper) 填充bean:为该bean定义的属性创建对象或赋值; 5. initializeBean("one",oneInstance, mbd) 初始化bean:对bean进行初始化或其他加工,如生成代理对象(proxy); 6. getSingleton(beanName, false) 依次在一二级缓存中查找,检查是否有因循环依赖导致提前生成的对象,有的话与初始化后的对象是否一致; 2、Spring 如何解决循环依赖? 以 com.gyh.circular.threeCache 包下的 OneBean 和 TwoBean 为例 ,两个 Bean 相互依赖(即形成闭环)。 参考图中 spring解决循环依赖 的过程可知,spring利用三级缓中的 objectFactory 生成并返回一个 early 对象,提前暴露这个 early 地址,供其他对象依赖注入使用,以此解决循环依赖问题。 3、Spring 不能解决哪些循环依赖? 3.1 循环中使用了 @Async 注解 3.1.1 为什么循环中使用了 @Async 会报错? 以 com.gyh.circular.async 包下的 OneBean 和 TwoBean 为例,两个bean相互依赖,且oneBean中的方法使用了 @Async 注解,此时启动spring失败,报错信息为:org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a.one': Bean with name 'a.one' has been injected into other beans [a.two] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example. 并通过debug代码,发现报错位置在 AbstractAutowireCapableBeanFactory#doCreateBean 方法内,由于 earlySingletonReference != null 且 exposedObject != bean,导致报错。    结合流程图中 spring解决循环依赖 及上述图片中可知: 1. 行1中 bean 为 createBeanInstance 创建的实例(address1) 2. 行2中 exposedObject 为 initializeBean 后生成的代理对象(address2) 3. 行3中 earlySingletonReference 为 getEarlyBeanReference 时创建的对象【此处地址同bean(address1)】 深层原因为:先前 TwoBean 在 populateBean 时已经依赖了地址为 address1 的 earlySingletonReference 对象,而此时 OneBean 经过 initializeBean 之后,返回了地址为 address2 的新对象,导致spring不知道哪个才是最终版的bean,所以报错。 earlySingletonReference 是如何生成的,参考getSingleton("one", true)过程。 3.1.2 循环中使用了 @Async 一定会报错吗? 依然以 com.gyh.circular.async 包下的 OneBean 和 TwoBean 为例,两个bean相互依赖,使 TwoBean(非OneBean)中的方法使用了 @Async 注解,此时启动spring成功,并未报错。 debug代码可知:虽然TwoBean 使用了 @Async 注解,但其 earlySingletonReference = null; 故不会引起报错。    深层原因为:OneBean 先被创建,TwoBean 后创建,再整条链路中,并未在三级缓存中查找过 TwoBean 的 objectFactory 。(OneBean在创建过程中,被找过两次,即 one-> two ->one;TwoBean 的创建过程中,只找过它一次,即 two ->one。) 由此可得:@Async 造成循环依赖报错的先约条件为: 1. 循环依赖中的 Bean 使用了 @Async 注解 2. 且这个 Bean,比循环内其他 Bean 先创建。 3. 注:一个Bean可能会同时存在于多个循环内;只要存在它是某个循环内第一个被创建的Bean,那么就会报错。 3.1.3 为什么循环中使用了 @Transactional 不会报错? 已知使用了 @Transactional 注解的 Bean,Spring 也会为其生成代理对象,但为什么这种 Bean 在循环里时不会产生报错呢? 以 com.gyh.circular.transactional 包下的 OneBean 和 TwoBean 为例,两个 Bean 相互依赖,且 OneBean 中的方法使用了 @Transactional 注解,启动Spring成功,并不会报错。 debug 代码可知,生成 OneBean 过程中,虽然 earlySingletonReference != null,但 initializeBean 之后的 exposedObject 和 原始实例的地址相同(即 initializeBean 步骤中,并未对实例生成代理),所以不会产生报错。     3.1.4 为什么同样是代理会产生两种不同的现象? 同样是生成代理对象,同样是参与到循环依赖中,会产生不同现象的原因是:当他们处在循环依赖中时,生成代理的节点不同: 1. @Transactional 在 getEarlyBeanReference 时生成代理,提前暴露出代理之后的地址(即最终地址); 2. @Async 在 initializeBean 时生成代理,导致提前暴露出去的地址不是最终地址,造成报错。 为什么 @Async 不能在 getEarlyBeanReference 时生成代理呢?对比下两者执行的代码过程发现: 两者都是在 AbstractAutoProxyCreator#getEarlyBeanReference 的方法对原始实例对象进行包装,如下图    使用 @Transactional 的Bean 在 create proxy 时,获取到一个advice ,随即生成了代理对象 proxy.    而使用 @Async 的Bean 在 create proxy 时,没有获取到 advice,不能被代理.    3.1.5 为什么@Async 在 getEarlyBeanReference 时不能返回一个 advice? 在 AbstractAutoProxyCreator#getAdvicesAndAdvisorsForBean 方法内,其主要做的事情有: 1. 找到当前 spring 容器中所有的 Advisor 2. 返回适配当前 bean 的所有 Advisor 第一步返回的 Advisor 有 BeanFactoryCacheOperationSourceAdvisor 和 BeanFactoryTransactionAttributeSourceAdvisor,并无处理 Async 相关的 Advisor. 刨根问底,追查为什么第一步不会返回处理 Async 相关的 Advisor? 已知使用 @Async @Transactional @Cacheable 需要提前进行开启,即提前标注 @EnableAsync、@EnableTransactionManagement、@EnableCaching 。 以 @EnableTransactionManagement、@EnableCaching 为例,在其注解定义中,引入了Selector类,Selector中又引入了Configuration 类,在 Configuration 类中,创建了对应 Advisor 并放到了 spring容器中,所以第一步才能得到这两个 Advisor. 而 @EnableAsync的定义中引入的 Configuration 类,创建的是 AsyncAnnotationBeanPostProcessor 并非一个 Advisor,所以第一步不会得到它,所以 @Async 的 bean 不会在这一步被代理。 3.2 构造函数引起的循环依赖 以 com.gyh.circular.constructor 包下的 OneBean 和 TwoBean 为例,两个类的构造函数中各自依赖对方,启动spring,报错:org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'c.one': Requested bean is currently in creation: Is there an unresolvable circular reference? debug 代码可知,两个bean在根据构造函数 new instance 时,就已经陷入的死循环,无法提前暴露可用的地址,所以只能报错。  4、如何解决以上循环依赖报错? 1. 不用 @Async,将需要异步操作的方法,放到线程池中执行。(推荐) 2. 提出 @Async 标注的方法。(推荐) 3. 将使用 @Async 的方法提出到单独的类中,该类只做异步处理,不做其他业务依赖,即避免形成循环依赖,从而解决报错问题。参考 com.gyh.circular.async.extract 包。 4. 尽量不使用构造函数依赖对象。(推荐) 5. 破坏循环(不推荐)即不形成闭环,在开发之前,规划好对象依赖,方法调用链,尽量做到不使用循环依赖。(较难,随着迭代开发不断变化,很可能产生循环) 6. 破坏创建顺序(不推荐) 7. 由于使用 @Async 注解的所在类,比循环依赖内其他类先创建时才会报错,那么想办法使该类不先于其他类先创建,也可解决该问题,如:@DependsOn、 @Lazy

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

深度 deepin 开源自研编程语言:Unilang

deepin 宣布了自研的全新通用目的编程语言:Unilang,其仓库提供了文档和作为参考实现的解释器。 deepin 表示,目前桌面应用开发虽然有许多选项,例如 Qt、Electron、PySide 和 Flutter 等。但它们存在各自的优势和不足。 据介绍,Unilang 目标是成为一门现代的通用目的编程语言,能够有效、灵活地开发桌面环境应用,去统筹解决现有不足的新的方案中的语言部分。为了保持通用性,Unilang 不内建提供 GUI 功能,而通过库提供相关 API 。当前计划中,Unilang 将会支持基于 Qt 绑定的库,以便衔接过渡现有的一些桌面应用项目。Unilang 的语言设计保持足够的抽象能力和可扩展性,未来支持直接实现 GUI 框架。 此外,Unilang 在语言特性的层次上被设计为能支持不同的应用开发场景,但原则上对这些场景保持中立。也就是说,Unilang 可以同时支持服务端和客户端应用的开发,不需要用户切换思维范式或者大幅更改对语言的使用习惯。 以下内容引用自官方介绍: 表现力:Unilang 是图灵完备的通用计算语言。Unilang 的创新式的语言特性,有助于构建强大而易于使用的抽象。 可复用:Unilang 对一等对象(first-class) 的强调使几乎任何源程序组件都更比往常意义上更容易复用——只要语言的用户愿意。 可扩展:Unilang 的基础语言和语言扩展的底层设计使开发者能有机会以前所未有的方式平滑地改进现有语言的设计和实现并保持兼容——而非等待和语言设计及实现者的沟通与反馈。 可伸缩:Unilang 的资源管理模型和抽象能力使程序在具有不同计算资源的平台上的表现默认自然地一致,且易于调整。 多泛型(multi-paradigm) :Unilang 的语言特性不要求用户拘泥于具体的语用范型——例如,不会限制副作用来要求习惯纯函数式的风格,或者要求隐含对象而要求程序按面向对象的形式进行表达。灵活的抽象使用户能够针对不同领域的设计方案按需使用不同风格的实现。 易用性:Unilang 的核心语言特性的极简设计使它的入门相当容易。而可扩展的特性和强大的抽象机制使语言保留了丰富的进阶内容,供开发者按需选择。合理的基础设计使不同内容的掌握不易出错而更易使用。 专注性:Unilang 不是一门需要面面俱到精通各种特性才能用好的语言——如果问题不是需要修改语言,语言的用户就应能更集中注意力于解决语言之外的问题上。 Unilang 主要特性 核心语言特性 实体特性 存储和对象模型(类似 C++) 对象和值类别(类似 C++) 一等函数:合并子(combiner) 一等环境:环境引用 一等引用:引用值 求值算法 变量解析 函数调用 运行时支持 动态类型检查 动态加载和执行 不依赖 GC 不安全操作 PTC(proper tail call) 互操作 对 C++ 友好的对象模型 在 C++ 中调用 Unilang 代码 FFI :调用 C 和 C++ 等本机语言实现的模块 …… 库特性 核心库 变量绑定 块作用域 列表算法 模块机制 …… 字符串 数值算术操作 输入/输出 类型系统增强 类型标注 静态类型检查 FFI API Qt 绑定 …… Hello World 示例 第一个程序: display "Hello, world!" 输出: Hello, world! 详情查看 Unilang的 README、语言介绍文档、特性介绍文档、解释器实现文档和语言规范文档。

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

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部分的功能。

用户登录
用户注册