2023年10月

二分搜索(Binary Search)

文承上篇,搜索算法中除了
深度优先搜索(DFS)和广度优先搜索(BFS)
,二分搜索(Binary Search)也是最基础搜索算法之一。

二分搜索也被称为折半搜索(Half-interval Search)也有说法为对数搜索算法(Logarithmic Search),用于在已排序的数据集中查找特定元素。

搜索过程从排序数据集的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束返回元素;如果某一特定元素大于或者小于中间元素,则在排序数据集中大于或小于中间元素的那一半中查找,继续重复开始的流程。反之亦然,如果在某一步骤排序数据集为空,则代表找不到。正如其名“二分”:每一次比较都使搜索范围缩小一半。

如果是对算法发展史有兴趣,二分搜索算法是算得上拥有一段悠长历史。最早可追溯到公元前200年的巴比伦尼亚中就有出现利用已排序的物件序列去加快搜索的构想,虽然该算法在计算机上的清楚描述出现在1946年约翰莫齐利(John Mauchly)的一篇文章里。

基本应用

二分搜索,最基本的应用就是查找特定元素。

LeetCode 35. 搜索插入位置【简单】

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。

请在此添加图片描述

使用递归进行编码逻辑也二分搜索常见的编程技巧之一,当然也并非一定要用递归的方式;不妨再练习一道题。

LeetCode 275. H指数 II 【中等】

给你一个整数数组 citations ,其中 citationsi 表示研究者的第 i 篇论文被引用的次数,citations 已经按照 升序排列 。计算并返回该研究者的 h 指数。

h 指数的定义:h 代表“高引用次数”(high citations),一名科研人员的 h 指数是指他(她)的 (n 篇论文中)总共有 h 篇论文分别被引用了至少 h 次。

请你设计并实现对数时间复杂度的算法解决此问题。

请在此添加图片描述

综合应用

对于问题解决往往可以有不同的算法思路来实现,对比来看或许更能感受到"二分"与"折半"的意义。不妨来一起感受下下面这题:

LeetCode 209. 长度最小的子数组【中等】

给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其总和大于等于 target 的长度最小的 连续子数组 numsl, numsl+1, ..., numsr-1, numsr ,并返回其长度。如果不存在符合条件的子数组,返回 0 。

请在此添加图片描述

请在此添加图片描述

请在此添加图片描述

除开以上的解法,抛开使用前缀和的思路,也可以应用本系列第一篇数组中双指针的编程技巧来解。利用两个指针标识连续子数组的首位,再根据 总和 与 target之间的情况进行灵活调整指针也可以计算出最小长度;

在此不过多描述,附上代码:

public int minSubArrayLen(int target, int[] nums) {
        
        int left = 0,right = 0,sum = 0,min = Integer.MAX_VALUE;
        
        while(right < nums.length){
            sum += nums[right++];
            while (sum  >= target && left < right){
                min = Math.min(min,right - left);
                sum -= nums[left++];
            } 
        }
        
        return  min == Integer.MAX_VALUE ? 0:min;
    }

总结下

  • 二分搜索是一种具有悠久历史的高效搜索算法,介绍基本算法流程;
  • 透过算法问题进行了递归编码、递推编码以及使用JDK库函数实现二分搜索;
  • 算法问题一般都有多种解法,通过对比更好理解二分的特性;

导读

本文核心内容聚焦为什么要埋点治理、埋点治理的方法论和实践、奇点一站式埋点管理平台的建设和创新功能。读者可以从全局角度深入了解埋点、埋点治理的整体思路和实践方法,落地的埋点工具和创新功能都有较高的实用参考价值。遵循埋点治理的方法论,本文作者团队已在实践中取得优异成效,在同行业内有突出的创新功能,未来也将继续建设数智化经营能力,持续打造更好的服务。

一、埋点治理背景

1.1 埋点数据的价值

随着线上流量红利高峰逐渐达到瓶颈,在精细化运营、数智化运营的大背景下,越来越多的公司开始认识到数据的重要性,并将其打造成为公司的核心资产,以数据为中心驱动业务发展。而
埋点数据
作为企业内部最重要的两大来源(埋点数据、业务数据)之一,其重要性不言而喻。

埋点是一种常用的数据采集方法。基于业务需求或产品需求,在应用页面中植入数据采集代码,监听用户各种行为事件(页面浏览、关闭,元素曝光、点击等),然后将采集的数据上报至服务端,服务端分别下发到大数据平台和搜索、推荐等各业务系统。通过分析数据,追踪用户行为和应用使用情况,推动产品优化或指导运营;通过实时的获取用户点击、浏览、停留等行为作为关键特征提供给搜索、推荐、广告等系统,来提升智能分发的转化和用户体验。

埋点数据上能影响业务运营数据分析、智能推荐、AB实验的准确性,下能影响数据仓库结构设计和数据采集团队的维护成本。

1.2 业内主流埋点方式的对比

从技术层面上,埋点分为代码埋点、可视化埋点、无埋点/全埋点。目前国内主要的第三方数据分析服务商和大型公司内部普遍支持。代码埋点又衍生出了声明式埋点、无痕埋点、服务端埋点等丰富的埋点方式。

通过多种埋点方式组合,可以在不同场景业务中灵活使用。比如在页面中元素或页面事件使用前端代码埋点;在Debug链路长的搜推代码中使用服务端埋点;产品运营等非研发使用可视化埋点。

1.3 为什么要治理埋点数据

然而随着业务的迭代变更,部分埋点数据失去效用。为了确保数据的质量、效率、安全、标准及易用性,需要对埋点数据进行治理。不仅是存量数据的治理,新增数据更是要保证从源头开始就是正确的。在埋点数据的生命周期内,每个环节制定原则性的管理方法和具体的落地措施。一个稳定的治理链路是埋点治理的基石。

从平台视角来看,埋点治理要解决的问题如下:

质量问题:
最重要,大部分公司的数据部门启动数据治理的起因就是数据质量存在问题。例如数仓的及时性、准确性、规范性,以及数据应用指标的逻辑一致性等。

成本问题:
互联网行业数据膨胀速度非常快,大型互联网公司在大数据基础设施上的成本投入占比非常高,而且随着数据量的增加,成本也将继续攀升。

效率问题:
在数据开发和数据管理过程中都会遇到一些影响效率的问题,多是靠“盲目”地推人力在做。

安全问题:
业务部门特别关注用户数据,一旦泄露,对业务的影响非常之大,甚至能左右整个业务的生死。

标准问题:
当公司业务部门比较多的时候,各业务部门、开发团队的数据标准不一致,数据打通和整合过程中存在很多问题。

从业务视角来看,埋点治理要解决的问题如下:

埋点数据“全”:
因整体协助链条非常长,许多时候在需要做数据分析时,才发现页面有部分功能漏报埋点,产品需求未涉及等。

埋点数据“准”:
需求开发测试阶段,往往重点关注业务逻辑,对于埋点上报这些辅助异步流程,设计评估不准确。会存在因验证不充分而导致数据不准确的情况。

埋点数据“快”:
推荐算法主要依赖数据驱动,埋点数据需要及时上报并反馈,推荐等智能应用系统才能根据用户当前行为给出精准的策略决策。

埋点数据“统一”:
智能场景往往要通过多个业务线交叉数据作为输入特征或算法画像,每个业务线如没有统一标准规范,数据处理计算逻辑复杂且迭代维护成本很高。

埋点数据“链路长”:
埋点数据从生产到使用,涉及运营、产品、研发、测试、数据分析师或算法工程师多个环节(如下图),问题沟通排查链路长。

埋点数据“历史长”:
页面埋点随需求迭代更新较快,历史埋点设计文档缺少统一管理,不利于长期维护。

二、埋点治理实践

为解决上述问题,几经探索总结经验后,本文作者团队为埋点治理制定了全面的标准制度。遵循相应的制度,使得埋点治理工作有序有效开展。

2.1 制定全链路标准

作者团队制定了一套覆盖数据生产到使用,全链路的数据标准方法,从埋点数据定义、采集、验证、指标定义到数据生命周期管理都建立了相应环节的标准化的研发规范,发布了《埋点流程规范标准》。

2.2 制定埋点流程规范

作者团队制定了完整的埋点上报规范规程,并邮件通知各部门产研按流程,照规范上报数据。上报流程为埋点方案设计、埋点方案配置、埋点开发/测试、数据存储/服务、数据应用五个环节,每个环节都要通过必要的步骤才可继续向下执行。

2.3 建设一站式埋点管理平台

奇点埋点管理平台是科技内部统一的埋点平台,覆盖埋点数据定义、采集、生产、验证、基础指标应用、数据质量监控治理等埋点全生命周期。做到了埋点元数据统一管理,埋点信息查询简易化、埋点上报验证一键化、埋点数据质量追踪可视化。

2.4 成立组织保执行

通过和数据技术产品部门合作,在两个部门领导的支持下,作者团队成立了埋点治理盘古项目及埋点数据管理委员会。平台研发部团队是采集埋点数据工具的产研方,数据仓库体系是由数据技术部负责建设,所以以这两个团队作为核心,并由这两个团队负责联合各个业务线团队,一起完成数据治理各个环节工作和流程的保障。

奇点团队作为埋点数据采集和管理的主力,负责数据采集SDK,数据上报、清洗、存储、查询,埋点管理平台等。

2.5 宣导埋点和数据文化

过去由于数据文化的缺失,很多业务方意识不到规范埋点的重要性。未正确录入页面埋点信息、使用低版本采集SDK,造成了大量不符合标准的数据。组织培训会和埋点规范宣讲,推动数据合理规范上报,也是埋点治理的重点工作之一。

三、埋点治理阶段性成果

作者团队提供的数据采集服务范围除了京东科技下金融科技、京东云、数字城市等全部业务线外,还扩展到了京东物流等兄弟部门。

奇点针对金融业务深耕多年,对数据的安全性、稳定性、实时性有多种保障方案,已是业务运营过程中不可或缺的重要环节。奇点管理平台现已实现埋点管理、数据分析一体化。在埋点数据上报查询、数据监控、数据计算可视化展示等各个环节都有相应的管理工具。

3.1 埋点验证工具

过去验证上报数据是否准确,需要测试人员申请数据库表权限,然后手写SQL查询数据。为此作者团队做了埋点验证工具,既可以扫码查看本机实时数据、查看所有上报实时数据,也可以一键检测上报数据是否符合规范。该工具为测试人员节省了大量时间,也为埋点治理,推动用户规范录入起了辅助作用。奇点服务端使用Lua脚本并发处理,而不是传统的Web服务,处理请求速度更快,减少了服务器资源使用。实时数据存放在ES中,相比MYSQL数据库能容纳更多的数据量,查询速度更快。

3.2 埋点验证工具

作者团队在客户端数据上报、服务端数据转换、数据发送、落仓等每步都加入了监控,保证整条链路数据质量。监控定时检查计算数据上报的成功率、缓存率、丢失率,数据加工清洗后的留存率、落仓率等,一旦数据浮动超过设定的阈值,会自动发告警邮件给奇点研发人员。有了数据监控,能及时发现、高效处理数据量问题,降低数据损失,节省人力,极大提升了数据质量。

3.3 实时数据一站式看板

过去作者团队只关注埋点范围的研发业务,平台升级后,用户录入埋点信息后可通过看板即时查看PV、UV、点击率等指标实时数据。对于用户来说,省去了从各种库表取数分析的步骤;对于埋点治理来说,不但降本增效,推动用户规范录入页面信息,而且指标计算结果比各个业务方自己分析更加准确。

四、奇点埋点对比行业创新功能

4.1 埋点可视化展示

查看某个页面的埋点信息,通常采用分页列表的方式,详细数据要跳转到看板浏览。这种方式虽然罗列出了页面所有埋点,但是每个埋点的录入人不同,埋点多了之后具体每个埋点表示什么含义其他人并不清楚。

为此作者团队研发了埋点可视化工具,完美解决了上述问题。只要输入页面URL,选择合适的设备大小,页面哪些元素有埋点就呈现出来。每个坑位的埋点ID,点击曝光的数据只要点击一下浮框即可见。埋点可视化工具还支持查看实时上报的日志和汇总的实时数据。

埋点可视化展示通过数据采集脚本-奇点 JS SDK 自动加载可视化插件实现,使用postMessage 和addEventListener('message'),实现埋点可视化工具和所查看页面的数据双向发送与接收,从而实时展示埋点数据和埋点日志。为减少加载SDK的页面开销,作者团队做了优化处理,只有在可视化工具中打开页面才会加载该插件。

4.2 H5与原生App全链路数据打通

类似京东金融这样使用Native和WEB技术开发的混合应用,之前H5页面和原生页面的数据,使用了不同的SDK采集,用户在两端页面间跳转,数据是断裂的,只能分开统计,不能从整体上统计分析用户行为。采用归因统计的方法能关联部分两端的数据,但会导致数据统计不准确,不但增加数据分析人力、物力成本,不可靠的数据还会使运营无法精准投放广告,从而影响最终收益;

如今奇点团队实现了H5页面和原生页面数据打通,包括以下打通点:

访次打通:
访次是指用户在当前设备中累计访问次数,在京东金融 App 中,用户每次重新打开或者切后台超过 2 分钟后,访问的次数就会加1。可以根据访次来统计用户活跃度。

访序打通:
访序是指用户在当前访次内,页面的访问顺序,H5和原生页面打通后,页面的访序是连续的,可以更精准的查看用户访问页面路径。

来源埋点:
来源埋点是指上一个页面用户点击点最后一个埋点ID。根据来源埋点,可以精准定位上一个页面触发点。数据打通后,可以确定当前页面的热点来源。

首访埋点:
首访埋点是指用户打开App时首次点击的坑位埋点,根据首访埋点可以定位到进入某一 H5 或原生页面起始点。

上一个页面 URL 或原生页面 CTP:
为了精准分析用户行为轨迹,奇点会采集上一个页面 URL 或原生页面CTP,数据打通后,会形成闭环,即使是后退操作也会记录后退的前一个页面,从而可以更好的进行路径分析、页面可达分析、用户丢失率分析。

其他采集字段打通:
为了统一口径,统一指标,打通的字段还包含以下字段:设备 ID、手机品牌、手机型号、App 名称、App 版本。

两端打通前:

两端打通后:

数据打通的收益是巨大的,下面是一个实际使用案例-小金库页面流量来源归因分析:

4.3 页面ID自动匹配上报

过去统计PV时,根据访问页面的URL作为唯一标识,这个URL需要在奇点管理平台录入后方可进行计算。然而这种方式存在很大的缺陷。当遇到以下场景,根据哪个URL来计算,边界并不清晰。

  • URL中带参数,例如/path1/path2?param=value。不同参数可能代表同一个页面,也可能是不同的页面;
  • 动态路由,例如/path1/path2/:path3/,某个path是动态的,如果这个path是数字ID,是无法在奇点管理平台全部录入的;
  • Hash路由,例如/path1/path2/#/route1 / route2。如今前端单页面盛行,不同业务方做出的网站大相径庭,hash值不同,有的希望统计成一个页面,也有想统计成不同的页面;
  • 以上场景混合的情况。

针对此问题,作者团队提出了使用pageId代替URL的方案。即业务方在奇点管理平台录入时指定URL的哪部分是动态的还是固定的,并生成唯一页面的ID。在访问页面时,当前页面的链接与录入的动态规则做计算,找到最匹配的pageId后上报数据,最终使用pageId做数据统计,极大的提高了指标计算正确率。

为保证此方案的稳健性,作者团队也做了很多细节把控。比如为防止拉取CDN pageId JSON文件失败,增加了重试机制,在未获取到文件时先将上报数据缓存在本地。比如没有匹配成功的URL另做打标处理。还有监控站点更新页面,同步生成最新的配置关系等等。

五、未来规划

在埋点数据治理方向,奇点团队联合数据团队通过一系列方案实现自动化治理埋点数据。例如对不规范数据打标,使数据不进入数据分析模型层;各端统一使用页面唯一ID的上报方式;不规范录入信息的页面自动认领到页面站点下;向未录入页面的用户定向推送邮件等方式持续提升数据质量。

在平台能力建设方向,首先从精细化运营角度还要持续建设可视化埋点及与页面活动搭建平台打通提供组件化埋点能力,提升埋点开发效率。其次从埋点生命周期管理角度,奇点平台提供的埋点设计管理、代码扫描、埋点验证、埋点指标看板一系列工具要更好流程化整合,提升产、运、研等各方的协同效率。最后从智能化建设角度,对于流量数据看板增加智能分析、智能预测能力,提升数据应用效率。通过埋点数据作为基石,赋能业务场景,更好地服务支撑公司整体的数智化经营能力建设。

作者:京东科技 奇点研发组 转载请注明来源

一、SpringCloud 简介

Spring Cloud 是一系列框架的有序集合如服务发现注册、配置中心、消息总线、负载均衡、熔断器、数据监控等。

SpringCloud 将多个服务框架组合起来,通过Spring Boot进行再封装,屏蔽掉了复杂的配置和实现原理,最终给开发者提供了一套简单易懂、易部署和易维护的分布式系统开发工具包。

Spring Cloud是一个基于SpringBoot实现的微服务开发方案,Spring boot 是 Spring 的一套快速配置框架。可以基于spring boot 快速开发单个微服务。

二、NACOS简介

一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。

Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。

Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。 Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。

1、Nacos中的概念

地域

物理的数据中心,资源创建成功后不能更换。

可用区

同一地域内,电力和网络互相独立的物理区域。同一可用区内,实例的网络延迟较低。

接入点

地域的某个服务的入口域名。

命名空间

用于进行租户粒度的配置隔离。不同的命名空间下,可以存在相同的 Group 或 Data ID 的配置。Namespace 的常用场景之一是不同环境的配置的区分隔离,例如开发测试环境和生产环境的资源(如配置、服务)隔离等。

配置

在系统开发过程中,开发者通常会将一些需要变更的参数、变量等从代码中分离出来独立管理,以独立的配置文件的形式存在。目的是让静态的系统工件或者交付物(如 WAR,JAR 包等)更好地和实际的物理运行环境进行适配。配置管理一般包含在系统部署的过程中,由系统管理员或者运维人员完成。配置变更是调整系统运行时的行为的有效手段。

配置管理

系统配置的编辑、存储、分发、变更管理、历史版本管理、变更审计等所有与配置相关的活动。

配置项

一个具体的可配置的参数与其值域,通常以 param-key=param-value 的形式存在。例如我们常配置系统的日志输出级别(logLevel=INFO|WARN|ERROR) 就是一个配置项。

配置集

一组相关或者不相关的配置项的集合称为配置集。在系统中,一个配置文件通常就是一个配置集,包含了系统各个方面的配置。例如,一个配置集可能包含了数据源、线程池、日志级别等配置项。

配置集 ID

Nacos 中的某个配置集的 ID。配置集 ID 是组织划分配置的维度之一。Data ID 通常用于组织划分系统的配置集。一个系统或者应用可以包含多个配置集,每个配置集都可以被一个有意义的名称标识。Data ID 通常采用类 Java 包(如 com.taobao.tc.refund.log.level)的命名规则保证全局唯一性。此命名规则非强制。

配置分组

Nacos 中的一组配置集,是组织配置的维度之一。通过一个有意义的字符串(如 Buy 或 Trade )对配置集进行分组,从而区分 Data ID 相同的配置集。当您在 Nacos 上创建一个配置时,如果未填写配置分组的名称,则配置分组的名称默认采用 DEFAULT_GROUP 。配置分组的常见场景:不同的应用或组件使用了相同的配置类型,如 database_url 配置和 MQ_topic 配置。

配置快照

Nacos 的客户端 SDK 会在本地生成配置的快照。当客户端无法连接到 Nacos Server 时,可以使用配置快照显示系统的整体容灾能力。配置快照类似于 Git 中的本地 commit,也类似于缓存,会在适当的时机更新,但是并没有缓存过期(expiration)的概念。

服务

通过预定义接口网络访问的提供给客户端的软件功能。

服务名

服务提供的标识,通过该标识可以唯一确定其指代的服务。

服务注册中心

存储服务实例和服务负载均衡策略的数据库。

服务发现

在计算机网络上,(通常使用服务名)对服务下的实例的地址和元数据进行探测,并以预先定义的接口提供给客户端进行查询。

元信息

Nacos数据(如配置和服务)描述信息,如服务版本、权重、容灾策略、负载均衡策略、鉴权配置、各种自定义标签 (label),从作用范围来看,分为服务级别的元信息、集群的元信息及实例的元信息。

应用

用于标识服务提供方的服务的属性。

服务分组

不同的服务可以归类到同一分组。

虚拟集群

同一个服务下的所有服务实例组成一个默认集群, 集群可以被进一步按需求划分,划分的单位可以是虚拟集群。

实例

提供一个或多个服务的具有可访问网络地址(IP:Port)的进程。

权重

实例级别的配置。权重为浮点数。权重越大,分配给该实例的流量越大。

健康检查

以指定方式检查服务下挂载的实例 (Instance) 的健康度,从而确认该实例 (Instance) 是否能提供服务。根据检查结果,实例 (Instance) 会被判断为健康或不健康。对服务发起解析请求时,不健康的实例 (Instance) 不会返回给客户端。

健康保护阈值

为了防止因过多实例 (Instance) 不健康导致流量全部流向健康实例 (Instance) ,继而造成流量压力把健康实例 (Instance) 压垮并形成雪崩效应,应将健康保护阈值定义为一个 0 到 1 之间的浮点数。当域名健康实例数 (Instance) 占总服务实例数 (Instance) 的比例小于该值时,无论实例 (Instance) 是否健康,都会将这个实例 (Instance) 返回给客户端。这样做虽然损失了一部分流量,但是保证了集群中剩余健康实例 (Instance) 能正常工作。

2、Nacos 架构

基础架构如下:

逻辑架构及组件如下:

  • 服务管理:实现服务CRUD,域名CRUD,服务健康状态检查,服务权重管理等功能
  • 配置管理:实现配置管CRUD,版本管理,灰度管理,监听管理,推送轨迹,聚合数据等功能
  • 元数据管理:提供元数据CURD 和打标能力
  • 插件机制:实现三个模块可分可合能力,实现扩展点SPI机制
  • 事件机制:实现异步化事件通知,sdk数据变化异步通知等逻辑
  • 日志模块:管理日志分类,日志级别,日志可移植性(尤其避免冲突),日志格式,异常码+帮助文档
  • 回调机制:sdk通知数据,通过统一的模式回调用户处理。接口和数据结构需要具备可扩展性
  • 寻址模式:解决ip,域名,nameserver、广播等多种寻址模式,需要可扩展
  • 推送通道:解决server与存储、server间、server与sdk间推送性能问题
  • 容量管理:管理每个租户,分组下的容量,防止存储被写爆,影响服务可用性
  • 流量管理:按照租户,分组等多个维度对请求频率,长链接个数,报文大小,请求流控进行控制
  • 缓存机制:容灾目录,本地缓存,server缓存机制。容灾目录使用需要工具
  • 启动模式:按照单机模式,配置模式,服务模式,dns模式,或者all模式,启动不同的程序+UI
  • 一致性协议:解决不同数据,不同一致性要求情况下,不同一致性机制
  • 存储模块:解决数据持久化、非持久化存储,解决数据分片问题
  • Nameserver:解决namespace到clusterid的路由问题,解决用户环境与nacos物理环境映射问题
  • CMDB:解决元数据存储,与三方cmdb系统对接问题,解决应用,人,资源关系
  • Metrics:暴露标准metrics数据,方便与三方监控系统打通
  • Trace:暴露标准trace,方便与SLA系统打通,日志白平化,推送轨迹等能力,并且可以和计量计费系统打通
  • 接入管理:相当于阿里云开通服务,分配身份、容量、权限过程
  • 用户管理:解决用户管理,登录,sso等问题
  • 权限管理:解决身份识别,访问控制,角色管理等问题
  • 审计系统:扩展接口方便与不同公司审计系统打通
  • 通知系统:核心数据变更,或者操作,方便通过SMS系统打通,通知到对应人数据变更
  • OpenAPI:暴露标准Rest风格HTTP接口,简单易用,方便多语言集成
  • Console:易用控制台,做服务管理、配置管理等操作
  • SDK:多语言sdk
  • Agent:dns-f类似模式,或者与mesh等方案集成
  • CLI:命令行对产品进行轻量化管理,像git一样好用

部署架构如下:

nacos 官网以及帮助文档和部署手册:https://nacos.io/zh-cn/index.html

nacos github:   https://github.com/alibaba/nacos

三、NACOS源码分析

1、Nacos注册源码分析-Clinet端

cosumer启动的时候,从nacos server上读取指定服务名称的实例列表,缓存到本地内存中。

开启一个定时任务,每隔10s去nacos server上拉取服务列表

nacos的push机制:

通过心跳检测发现服务提供者出现心态超时的时候,推送一个push消息到consumer,更新本地的缓存数据。

客户端Client

我们自己的项目在配置了nacos作为注册中心后,至少要配置这么一个属性

spring.cloud.nacos.discovery.server-addr=ip地址:8848# 从逻辑上看,这个是通过grpc去注册还是通过http去注册。false-http1.x注册  true-gRPC注册,默认是true,也就是通过gRPC去注册,毕竟gRPC的性能上要比http1.x高很多
spring.cloud.nacos.discovery.ephemeral
=false

这个属性会让应用找到nacos的server地址去注册。如果不配置的话,会一直报错

springboot的@EnableAutoConfiguration这里就不再讲解了。都到nacos的源码了,springboot默认是熟悉的。

我们再去打开NacosServiceRegistryAutoConfiguration这个类。

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties
@ConditionalOnNacosDiscoveryEnabled
@ConditionalOnProperty(value
= "spring.cloud.service-registry.auto-registration.enabled",
matchIfMissing
= true)
@AutoConfigureAfter({ AutoServiceRegistrationConfiguration.
class,
AutoServiceRegistrationAutoConfiguration.
class,
NacosDiscoveryAutoConfiguration.
class})public classNacosServiceRegistryAutoConfiguration {

@Bean
publicNacosServiceRegistry nacosServiceRegistry(
NacosServiceManager nacosServiceManager,
NacosDiscoveryProperties nacosDiscoveryProperties) {
return newNacosServiceRegistry(nacosServiceManager, nacosDiscoveryProperties);
}

@Bean
@ConditionalOnBean(AutoServiceRegistrationProperties.
class)publicNacosRegistration nacosRegistration(
ObjectProvider
<List<NacosRegistrationCustomizer>>registrationCustomizers,
NacosDiscoveryProperties nacosDiscoveryProperties,
ApplicationContext context) {
return newNacosRegistration(registrationCustomizers.getIfAvailable(),
nacosDiscoveryProperties, context);
}

@Bean
@ConditionalOnBean(AutoServiceRegistrationProperties.
class)publicNacosAutoServiceRegistration nacosAutoServiceRegistration(
NacosServiceRegistry registry,
AutoServiceRegistrationProperties autoServiceRegistrationProperties,
NacosRegistration registration) {
return newNacosAutoServiceRegistration(registry,
autoServiceRegistrationProperties, registration);
}

}

其中第三个类
NacosAutoServiceRegistration
实现了一个抽象类
AbstractAutoServiceRegistration
.

public abstract class AbstractAutoServiceRegistration<R extends Registration>
        implementsAutoServiceRegistration, ApplicationContextAware,
ApplicationListener
<WebServerInitializedEvent>{

@Override
@SuppressWarnings(
"deprecation")public voidonApplicationEvent(WebServerInitializedEvent event) {
bind(event);
}

@Deprecated
public voidbind(WebServerInitializedEvent event) {
ApplicationContext context
=event.getApplicationContext();if (context instanceofConfigurableWebServerApplicationContext) {if ("management".equals(((ConfigurableWebServerApplicationContext) context)
.getServerNamespace())) {
return;
}
}
this.port.compareAndSet(0, event.getWebServer().getPort());this.start();
}
public voidstart() {if (!isEnabled()) {if(logger.isDebugEnabled()) {
logger.debug(
"Discovery Lifecycle disabled. Not starting");
}
return;
}
//only initialize if nonSecurePort is greater than 0 and it isn't already running//because of containerPortInitializer below if (!this.running.get()) {this.context.publishEvent(new InstancePreRegisteredEvent(this, getRegistration()));
register();
if(shouldRegisterManagement()) {
registerManagement();
}
this.context.publishEvent(new InstanceRegisteredEvent<>(this, getConfiguration()));this.running.compareAndSet(false, true);
}

}
}

这里有实现一个ApplicationListener<WebServerInitializedEvent>的类,这个类是spring的一个监听事件(观察者模式),而这个事件就是webserver初始化的时候去触发的。onApplicationEvent方法调用了bind()方法。而bind()中又调用了start().

start()中有一个register()。而这个register就是NacosServiceRegistry中的register()。

public class NacosServiceRegistry implements ServiceRegistry<Registration>{

@Override
public voidregister(Registration registration) {if(StringUtils.isEmpty(registration.getServiceId())) {
log.warn(
"No service to register for nacos client...");return;
}

NamingService namingService
=namingService();
String serviceId
=registration.getServiceId();
String group
=nacosDiscoveryProperties.getGroup();

Instance instance
=getNacosInstanceFromRegistration(registration);try{
namingService.registerInstance(serviceId, group, instance);
log.info(
"nacos registry, {} {} {}:{} register finished", group, serviceId,
instance.getIp(), instance.getPort());
}
catch(Exception e) {if(nacosDiscoveryProperties.isFailFast()) {
log.error(
"nacos registry, {} register failed...{},", serviceId,
registration.toString(), e);
rethrowRuntimeException(e);
}
else{
log.warn(
"Failfast is false. {} register failed...{},", serviceId,
registration.toString(), e);
}
}
}
}
  • getNacosInstanceFromRegistration 获取注册的实例信息。
privateInstance getNacosInstanceFromRegistration(Registration registration) {
Instance instance
= newInstance();
instance.setIp(registration.getHost());
instance.setPort(registration.getPort());
instance.setWeight(nacosDiscoveryProperties.getWeight());
instance.setClusterName(nacosDiscoveryProperties.getClusterName());
instance.setEnabled(nacosDiscoveryProperties.isInstanceEnabled());
instance.setMetadata(registration.getMetadata());
instance.setEphemeral(nacosDiscoveryProperties.isEphemeral());
returninstance;
}
  • namingService.registerInstance(serviceId, group, instance);

clientProxy有3个实现类,NamingClientProxyDelegate、NamingGrpcClientProxy、NamingHttpClientProxy。

这个类的构造方法中有个init(properties)方法,这个方法中给clientProxy赋值了。走的是NamingClientProxyDelegate方法。一般情况下,带有delegate的方法都是委派模式。

public NacosNamingService(String serverList) throwsNacosException {
Properties properties
= newProperties();
properties.setProperty(PropertyKeyConst.SERVER_ADDR, serverList);
init(properties);
}
public NacosNamingService(Properties properties) throwsNacosException {
init(properties);
}
private void init(Properties properties) throwsNacosException {
ValidatorUtils.checkInitParam(properties);
this.namespace =InitUtils.initNamespaceForNaming(properties);
InitUtils.initSerialization();
InitUtils.initWebRootContext(properties);
initLogName(properties);
this.changeNotifier = newInstancesChangeNotifier();
NotifyCenter.registerToPublisher(InstancesChangeEvent.
class, 16384);
NotifyCenter.registerSubscriber(changeNotifier);
this.serviceInfoHolder = newServiceInfoHolder(namespace, properties);this.clientProxy = new NamingClientProxyDelegate(this.namespace, serviceInfoHolder, properties, changeNotifier);
}
@Override
public void registerInstance(String serviceName, String groupName, Instance instance) throwsNacosException {
NamingUtils.checkInstanceIsLegal(instance);
clientProxy.registerService(serviceName, groupName, instance);
}
基于http1.x协议注册
  • NamingClientProxyDelegate.registerService

    委派这里做了一个可执行的判断

@Overridepublic void registerService(String serviceName, String groupName, Instance instance) throwsNacosException {
getExecuteClientProxy(instance).registerService(serviceName, groupName, instance);
}

NamingClientProxyDelegate.getExecuteClientProxy

做了一个判断,配置ephemeral=false就走http,否则grpc。这里请注意,如果nacos-server还是用的1.x.x版本的话,会报错的。因为2.x.x增加一个grpc的支持,会额外的多增加一个端口,默认对外提供端口为8848和9848

privateNamingClientProxy getExecuteClientProxy(Instance instance) {return instance.isEphemeral() ?grpcClientProxy : httpClientProxy;
}
  • NamingHttpClientProxy.registerService

    这里的clientProxy=NamingHttpClientProxy

@Overridepublic void registerService(String serviceName, String groupName, Instance instance) throwsNacosException {

NAMING_LOGGER.info(
"[REGISTER-SERVICE] {} registering service {} with instance: {}", namespaceId, serviceName,
instance);
String groupedServiceName
=NamingUtils.getGroupedName(serviceName, groupName);if(instance.isEphemeral()) {
BeatInfo beatInfo
=beatReactor.buildBeatInfo(groupedServiceName, instance);
beatReactor.addBeatInfo(groupedServiceName, beatInfo);
}
final Map<String, String> params = new HashMap<String, String>(32);
params.put(CommonParams.NAMESPACE_ID, namespaceId);
params.put(CommonParams.SERVICE_NAME, groupedServiceName);
params.put(CommonParams.GROUP_NAME, groupName);
params.put(CommonParams.CLUSTER_NAME, instance.getClusterName());
params.put(IP_PARAM, instance.getIp());
params.put(PORT_PARAM, String.valueOf(instance.getPort()));
params.put(WEIGHT_PARAM, String.valueOf(instance.getWeight()));
params.put(REGISTER_ENABLE_PARAM, String.valueOf(instance.isEnabled()));
params.put(HEALTHY_PARAM, String.valueOf(instance.isHealthy()));
params.put(EPHEMERAL_PARAM, String.valueOf(instance.isEphemeral()));
params.put(META_PARAM, JacksonUtils.toJson(instance.getMetadata()));

reqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.POST);

}

NamingHttpClientProxy.reqApi

public String reqApi(String api, Map<String, String> params, String method) throwsNacosException {returnreqApi(api, params, Collections.EMPTY_MAP, method);
}
public String reqApi(String api, Map<String, String> params, Map<String, String>body, String method)throwsNacosException {returnreqApi(api, params, body, serverListManager.getServerList(), method);
}
public String reqApi(String api, Map<String, String> params, Map<String, String> body, List<String>servers,
String method)
throwsNacosException {

params.put(CommonParams.NAMESPACE_ID, getNamespaceId());
if (CollectionUtils.isEmpty(servers) && !serverListManager.isDomain()) {throw new NacosException(NacosException.INVALID_PARAM, "no server available");
}

NacosException exception
= newNacosException();if(serverListManager.isDomain()) {
String nacosDomain
=serverListManager.getNacosDomain();for (int i = 0; i < maxRetry; i++) {try{returncallServer(api, params, body, nacosDomain, method);
}
catch(NacosException e) {
exception
=e;if(NAMING_LOGGER.isDebugEnabled()) {
NAMING_LOGGER.debug(
"request {} failed.", nacosDomain, e);
}
}
}
}
else{
Random random
= newRandom(System.currentTimeMillis());int index =random.nextInt(servers.size());for (int i = 0; i < servers.size(); i++) {
String server
=servers.get(index);try{returncallServer(api, params, body, server, method);
}
catch(NacosException e) {
exception
=e;if(NAMING_LOGGER.isDebugEnabled()) {
NAMING_LOGGER.debug(
"request {} failed.", server, e);
}
}
index
= (index + 1) %servers.size();
}
}

NAMING_LOGGER.error(
"request: {} failed, servers: {}, code: {}, msg: {}", api, servers, exception.getErrCode(),
exception.getErrMsg());
throw newNacosException(exception.getErrCode(),"failed to req API:" + api + " after all servers(" + servers + ") tried: " +exception.getMessage());

}

serverListManager.isDomain()这个判断是配置了几个nacos server的值,如果是一个的话,走if逻辑,如果多余1个的话,走else逻辑。

else中的servers就是nacos server服务列表,通过Ramdom拿到一个随机数,然后去callServer(),如果发现其中的一个失败,那么index+1 获取下一个服务节点再去callServer。如果所有的都失败的话,则抛出错误。

NamingHttpClientProxy.callServer

前边的判断支线省略,拼接url,拼好了后,进入try逻辑块中,这里封装了一个nacosRestTemplate类。请求完成后,返回一个restResult,拿到了请求结果后,把请求结果code放入了一个交MetricsMonitor的类中了,从代码上看很明显是监控相关的类,点击进去果然发现是prometheus相关的。这里我们不扩展了,继续回到主线。

如果返回结果是200的话,把result.content返回去。

public String callServer(String api, Map<String, String> params, Map<String, String>body, String curServer,
String method)
throwsNacosException {long start =System.currentTimeMillis();long end = 0;
String namespace
=params.get(CommonParams.NAMESPACE_ID);
String group
=params.get(CommonParams.GROUP_NAME);
String serviceName
=params.get(CommonParams.SERVICE_NAME);
params.putAll(getSecurityHeaders(namespace, group, serviceName));
Header header
=NamingHttpUtil.builderHeader();

String url;
if (curServer.startsWith(HTTPS_PREFIX) ||curServer.startsWith(HTTP_PREFIX)) {
url
= curServer +api;
}
else{if (!InternetAddressUtil.containsPort(curServer)) {
curServer
= curServer + InternetAddressUtil.IP_PORT_SPLITER +serverPort;
}
url
= NamingHttpClientManager.getInstance().getPrefix() + curServer +api;
}
try{
HttpRestResult
<String> restResult =nacosRestTemplate
.exchangeForm(url, header, Query.newInstance().initParams(params), body, method, String.
class);
end
=System.currentTimeMillis();

MetricsMonitor.getNamingRequestMonitor(method, url, String.valueOf(restResult.getCode()))
.observe(end
-start);if(restResult.ok()) {returnrestResult.getData();
}
if (HttpStatus.SC_NOT_MODIFIED ==restResult.getCode()) {returnStringUtils.EMPTY;
}
throw newNacosException(restResult.getCode(), restResult.getMessage());
}
catch(Exception e) {
NAMING_LOGGER.error(
"[NA] failed to request", e);throw newNacosException(NacosException.SERVER_ERROR, e);
}
}
  • NacosRestTemplate.exchangeForm

    关键方法:this.requestClient().execute()

  • public <T> HttpRestResult<T> exchangeForm(String url, Header header, Query query, Map<String, String>bodyValues,
    String httpMethod, Type responseType)
    throwsException {
    RequestHttpEntity requestHttpEntity
    = newRequestHttpEntity(
    header.setContentType(MediaType.APPLICATION_FORM_URLENCODED), query, bodyValues);
    returnexecute(url, httpMethod, requestHttpEntity, responseType);
    }
    private <T> HttpRestResult<T>execute(String url, String httpMethod, RequestHttpEntity requestEntity,
    Type responseType)
    throwsException {
    URI uri
    =HttpUtils.buildUri(url, requestEntity.getQuery());if(logger.isDebugEnabled()) {
    logger.debug(
    "HTTP method: {}, url: {}, body: {}", httpMethod, uri, requestEntity.getBody());
    }

    ResponseHandler
    <T> responseHandler = super.selectResponseHandler(responseType);
    HttpClientResponse response
    = null;try{
    response
    = this.requestClient().execute(uri, httpMethod, requestEntity);returnresponseHandler.handle(response);
    }
    finally{if (response != null) {
    response.close();
    }
    }
    }

    private finalHttpClientRequest requestClient;private final List<HttpClientRequestInterceptor> interceptors = new ArrayList<HttpClientRequestInterceptor>();publicNacosRestTemplate(Logger logger, HttpClientRequest requestClient) {super(logger);this.requestClient =requestClient;
    }
    privateHttpClientRequest requestClient() {if(CollectionUtils.isNotEmpty(interceptors)) {if(logger.isDebugEnabled()) {
    logger.debug(
    "Execute via interceptors :{}", interceptors);
    }
    return newInterceptingHttpClientRequest(requestClient, interceptors.iterator());
    }
    returnrequestClient;
    }

    HttpClientBeanHolder.getNacosRestTemplate

    典型的双重检查锁。

  • public staticNacosRestTemplate getNacosRestTemplate(HttpClientFactory httpClientFactory) {if (httpClientFactory == null) {throw new NullPointerException("httpClientFactory is null");
    }
    String factoryName
    =httpClientFactory.getClass().getName();
    NacosRestTemplate nacosRestTemplate
    =SINGLETON_REST.get(factoryName);if (nacosRestTemplate == null) {synchronized(SINGLETON_REST) {
    nacosRestTemplate
    =SINGLETON_REST.get(factoryName);if (nacosRestTemplate != null) {returnnacosRestTemplate;
    }
    nacosRestTemplate
    =httpClientFactory.createNacosRestTemplate();
    SINGLETON_REST.put(factoryName, nacosRestTemplate);
    }
    }
    returnnacosRestTemplate;
    }

    而NamingHttpClientFactory是一个AbstractHttpClientFactory的实现类,由于NamingHttpClientProxy没有重写createNacosRestTemplate方法。所以最终引用的也就是AbstractHttpClientFactory的createNacosRestTemplate方法。


    private static final HttpClientFactory HTTP_CLIENT_FACTORY = newNamingHttpClientFactory();publicNacosRestTemplate getNacosRestTemplate() {returnHttpClientBeanHolder.getNacosRestTemplate(HTTP_CLIENT_FACTORY);
    }
    private static class NamingHttpClientFactory extendsAbstractHttpClientFactory {

    @Override
    protectedHttpClientConfig buildHttpClientConfig() {returnHttpClientConfig.builder().setConTimeOutMillis(CON_TIME_OUT_MILLIS)
    .setReadTimeOutMillis(READ_TIME_OUT_MILLIS).setMaxRedirects(MAX_REDIRECTS).build();
    }

    @Override
    protectedLogger assignLogger() {returnNAMING_LOGGER;
    }
    }

    AbstractHttpClientFactory.createNacosRestTemplate


    @OverridepublicNacosRestTemplate createNacosRestTemplate() {
    HttpClientConfig httpClientConfig
    =buildHttpClientConfig();final JdkHttpClientRequest clientRequest = newJdkHttpClientRequest(httpClientConfig);//enable ssl initTls(new BiConsumer<SSLContext, HostnameVerifier>() {
    @Override
    public voidaccept(SSLContext sslContext, HostnameVerifier hostnameVerifier) {
    clientRequest.setSSLContext(loadSSLContext());
    clientRequest.replaceSSLHostnameVerifier(hostnameVerifier);
    }
    },
    newTlsFileWatcher.FileChangeListener() {
    @Override
    public voidonChanged(String filePath) {
    clientRequest.setSSLContext(loadSSLContext());
    }
    });
    return newNacosRestTemplate(assignLogger(), clientRequest);
    }

    JdkHttpClientRequest clientRequest = new JdkHttpClientRequest(httpClientConfig);

    可以看到这里定义了一个JdkHttpClientRequest 。

    再往下跟就到java.net.HttpURLConnection的调用,去请求nacos-server的地址,再往下的就不做分析了,进入了http的通讯层了。

    最终返回了一个结果,如果是200的话,就注册成功了。失败了就会抛出异常。

    基于gRPC http2.0的注册

    这里同样的从gRPC和http的委派来进行分析

    NamingClientProxyDelegate.registerService

    代码上边已经分析过,我们直接进入gRPC的实现。


    @Overridepublic void registerService(String serviceName, String groupName, Instance instance) throwsNacosException {
    getExecuteClientProxy(instance).registerService(serviceName, groupName, instance);
    }

    • NamingGrpcClientProxy.registerService

      redoService.cacheInstanceForRedo 这个从名称上看应该是重试机制,

    • @Overridepublic void registerService(String serviceName, String groupName, Instance instance) throwsNacosException {
      NAMING_LOGGER.info(
      "[REGISTER-SERVICE] {} registering service {} with instance {}", namespaceId, serviceName,
      instance);
      redoService.cacheInstanceForRedo(serviceName, groupName, instance);
      doRegisterService(serviceName, groupName, instance);
      }

      • NamingGrpcRedoService.cacheInstanceForRedo

        这里看起来只是给ConcurrentMap中存放一个redoData,并没有其他的逻辑,后续可能会用到这个。回到主线,继续往下走。

      • private final ConcurrentMap<String, InstanceRedoData> registeredInstances = new ConcurrentHashMap<>();public voidcacheInstanceForRedo(String serviceName, String groupName, Instance instance) {
        String key
        =NamingUtils.getGroupedName(serviceName, groupName);
        InstanceRedoData redoData
        =InstanceRedoData.build(serviceName, groupName, instance);synchronized(registeredInstances) {
        registeredInstances.put(key, redoData);
        }
        }

        • NamingGrpcClientProxy.doRegisterService

          request是根据构造函数封装的一个实例,requestToServer去进行注册。

        • public void doRegisterService(String serviceName, String groupName, Instance instance) throwsNacosException {
          InstanceRequest request
          = newInstanceRequest(namespaceId, serviceName, groupName,
          NamingRemoteConstants.REGISTER_INSTANCE, instance);
          requestToServer(request, Response.
          class);
          redoService.instanceRegistered(serviceName, groupName);
          }

          NamingGrpcClientProxy.requestToServer

          request.putAllHeader推测是跟权限校验相关的,我搭建的没有设置鉴权,所以都是空的。

          然后根据rpcClient去调用request方法。根据超时时间判断的,这2个分支最终都会进入一个方法,默认是3s的超时时间。

          最终返回一个response结果。


          private <T extends Response> T requestToServer(AbstractNamingRequest request, Class<T>responseClass)throwsNacosException {try{
          request.putAllHeader(
          getSecurityHeaders(request.getNamespace(), request.getGroupName(), request.getServiceName()));
          Response response
          =requestTimeout< 0 ?rpcClient.request(request) : rpcClient.request(request, requestTimeout);if (ResponseCode.SUCCESS.getCode() !=response.getResultCode()) {throw newNacosException(response.getErrorCode(), response.getMessage());
          }
          if(responseClass.isAssignableFrom(response.getClass())) {return(T) response;
          }
          NAMING_LOGGER.error(
          "Server return unexpected response '{}', expected response should be '{}'",
          response.getClass().getName(), responseClass.getName());
          }
          catch(Exception e) {throw new NacosException(NacosException.SERVER_ERROR, "Request nacos server failed: ", e);
          }
          throw new NacosException(NacosException.SERVER_ERROR, "Server return invalid response");
          }

          • RpcClient.request

          这里的校验暂且不看,直切主线, response = this.currentConnection.request(request, timeoutMills);

          再进入到request方法。

        • public Response request(Request request, long timeoutMills) throwsNacosException {int retryTimes = 0;
          Response response;
          Exception exceptionThrow
          = null;long start =System.currentTimeMillis();while (retryTimes < RETRY_TIMES && System.currentTimeMillis() < timeoutMills +start) {boolean waitReconnect = false;try{if (this.currentConnection == null || !isRunning()) {
          waitReconnect
          = true;throw newNacosException(NacosException.CLIENT_DISCONNECT,"Client not connected, current status:" +rpcClientStatus.get());
          }
          response
          = this.currentConnection.request(request, timeoutMills);if (response == null) {throw new NacosException(SERVER_ERROR, "Unknown Exception.");
          }
          if (response instanceofErrorResponse) {if (response.getErrorCode() ==NacosException.UN_REGISTER) {synchronized (this) {
          waitReconnect
          = true;if(rpcClientStatus.compareAndSet(RpcClientStatus.RUNNING, RpcClientStatus.UNHEALTHY)) {
          LoggerUtils.printIfErrorEnabled(LOGGER,
          "Connection is unregistered, switch server, connectionId = {}, request = {}",
          currentConnection.getConnectionId(), request.getClass().getSimpleName());
          switchServerAsync();
          }
          }

          }
          throw newNacosException(response.getErrorCode(), response.getMessage());
          }
          //return response. lastActiveTimeStamp =System.currentTimeMillis();returnresponse;

          }
          catch(Exception e) {if(waitReconnect) {try{//wait client to reconnect. Thread.sleep(Math.min(100, timeoutMills / 3));
          }
          catch(Exception exception) {//Do nothing. }
          }

          LoggerUtils.printIfErrorEnabled(LOGGER,
          "Send request fail, request = {}, retryTimes = {}, errorMessage = {}",
          request, retryTimes, e.getMessage());

          exceptionThrow
          =e;

          }
          retryTimes
          ++;

          }
          if(rpcClientStatus.compareAndSet(RpcClientStatus.RUNNING, RpcClientStatus.UNHEALTHY)) {
          switchServerAsyncOnRequestFail();
          }
          if (exceptionThrow != null) {throw (exceptionThrow instanceof NacosException) ?(NacosException) exceptionThrow
          :
          newNacosException(SERVER_ERROR, exceptionThrow);
          }
          else{throw new NacosException(SERVER_ERROR, "Request fail, unknown Error");
          }
          }

          • GrpcConnection.request

          这里的就是封装的rpc请求,和服务端进行交互的逻辑。在这里封装了一个PayLoad类

        • @Overridepublic Response request(Request request, long timeouts) throwsNacosException {
          Payload grpcRequest
          =GrpcUtils.convert(request);
          ListenableFuture
          <Payload> requestFuture =grpcFutureServiceStub.request(grpcRequest);
          Payload grpcResponse;
          try{
          grpcResponse
          =requestFuture.get(timeouts, TimeUnit.MILLISECONDS);
          }
          catch(Exception e) {throw newNacosException(NacosException.SERVER_ERROR, e);
          }
          return(Response) GrpcUtils.parse(grpcResponse);
          }

          2、Nacos注册源码分析-Server端

        • 接收注册

          客户端和服务端之间进行交互的话,一定需要建立一个网络连接。这里的grpc的源码相对来说比较复杂,就简单分析nacos相关的。

          工程名称是nacos-console。

          BaseGrpcServer在启动的时候会绑定很多的Handler。

        • 而基于grpc的通信,会进入server端的InstanceRequestHandler

          InstanceRequestHandler.handle

          从handle方法中可以根据type走到registerInstance中。

          最终进入到EphemeralClientOperationServiceImpl.registerInstance


          public class InstanceRequestHandler extends RequestHandler<InstanceRequest, InstanceResponse>{private finalEphemeralClientOperationServiceImpl clientOperationService;publicInstanceRequestHandler(EphemeralClientOperationServiceImpl clientOperationService) {this.clientOperationService =clientOperationService;
          }

          @Override
          @Secured(action
          =ActionTypes.WRITE)public InstanceResponse handle(InstanceRequest request, RequestMeta meta) throwsNacosException {
          Service service
          =Service
          .newService(request.getNamespace(), request.getGroupName(), request.getServiceName(),
          true);switch(request.getType()) {caseNamingRemoteConstants.REGISTER_INSTANCE://注册 returnregisterInstance(service, request, meta);caseNamingRemoteConstants.DE_REGISTER_INSTANCE://取消注册 returnderegisterInstance(service, request, meta);default:throw newNacosException(NacosException.INVALID_PARAM,
          String.format(
          "Unsupported request type %s", request.getType()));
          }
          }
          privateInstanceResponse registerInstance(Service service, InstanceRequest request, RequestMeta meta)throwsNacosException {//注册实例 clientOperationService.registerInstance(service, request.getInstance(), meta.getConnectionId());
          NotifyCenter.publishEvent(
          newRegisterInstanceTraceEvent(System.currentTimeMillis(),
          meta.getClientIp(),
          true, service.getNamespace(), service.getGroup(), service.getName(),
          request.getInstance().getIp(), request.getInstance().getPort()));
          return newInstanceResponse(NamingRemoteConstants.REGISTER_INSTANCE);
          }
          privateInstanceResponse deregisterInstance(Service service, InstanceRequest request, RequestMeta meta) {
          clientOperationService.deregisterInstance(service, request.getInstance(), meta.getConnectionId());
          NotifyCenter.publishEvent(
          newDeregisterInstanceTraceEvent(System.currentTimeMillis(),
          meta.getClientIp(),
          true, DeregisterInstanceReason.REQUEST, service.getNamespace(),
          service.getGroup(), service.getName(), request.getInstance().getIp(), request.getInstance().getPort()));
          return newInstanceResponse(NamingRemoteConstants.DE_REGISTER_INSTANCE);
          }
          }

          EphemeralClientOperationServiceImpl.registerInstance

          这里的clientManager.getClient(client)说明跳转到下边的建立长连接

        • @Overridepublic void registerInstance(Service service, Instance instance, String clientId) throwsNacosException {
          NamingUtils.checkInstanceIsLegal(instance);
          //获取一个单例的Service,也就是注册的实例 Service singleton =ServiceManager.getInstance().getSingleton(service);if (!singleton.isEphemeral()) {throw newNacosRuntimeException(NacosException.INVALID_PARAM,
          String.format(
          "Current service %s is persistent service, can't register ephemeral instance.",
          singleton.getGroupedServiceName()));
          }
          //这里的Client是客户端的长连接,会进入到ClientManagerDelegate的一个委托,最终进入到connectionBasedClientManager中 Client client =clientManager.getClient(clientId);if (!clientIsLegal(client, clientId)) {return;
          }
          InstancePublishInfo instanceInfo
          =getPublishInfo(instance);//对这个实例进行注册 client.addServiceInstance(singleton, instanceInfo);
          client.setLastUpdatedTime();
          client.recalculateRevision();
          NotifyCenter.publishEvent(
          newClientOperationEvent.ClientRegisterServiceEvent(singleton, clientId));
          NotifyCenter
          .publishEvent(
          new MetadataEvent.InstanceMetadataEvent(singleton, instanceInfo.getMetadataId(), false));
          }

          AbstractClient.addServiceInstance

        • //这个ConcurrentHashMap就是保存实例和发布信息关系的
          protected final ConcurrentHashMap<Service, InstancePublishInfo> publishers = new ConcurrentHashMap<>(16, 0.75f, 1);
          @Override
          public booleanaddServiceInstance(Service service, InstancePublishInfo instancePublishInfo) {if (null ==publishers.put(service, instancePublishInfo)) {if (instancePublishInfo instanceofBatchInstancePublishInfo) {
          MetricsMonitor.incrementIpCountWithBatchRegister(instancePublishInfo);
          }
          else{
          MetricsMonitor.incrementInstanceCount();
          }
          }
          //这里有一个事件,ClientChangeEvent NotifyCenter.publishEvent(new ClientEvent.ClientChangedEvent(this));
          Loggers.SRV_LOG.info(
          "Client change for service {}, {}", service, getClientId());return true;
          }

          ClientServiceIndexesManager

        • //应用Service和clientId的映射,一个应用Service有多个服务,所以会建立多个长连接,用Set来保存clientId
          private final ConcurrentMap<Service, Set<String>> publisherIndexes = new ConcurrentHashMap<>();//应用Service和订阅者clientId的关系
          private final ConcurrentMap<Service, Set<String>> subscriberIndexes = new ConcurrentHashMap<>();
          @Override
          public voidonEvent(Event event) {if (event instanceofClientEvent.ClientDisconnectEvent) {
          handleClientDisconnect((ClientEvent.ClientDisconnectEvent) event);
          }
          else if (event instanceofClientOperationEvent) {
          handleClientOperation((ClientOperationEvent) event);
          }
          }
          private voidhandleClientOperation(ClientOperationEvent event) {
          Service service
          =event.getService();
          String clientId
          =event.getClientId();if (event instanceofClientOperationEvent.ClientRegisterServiceEvent) {//注册 addPublisherIndexes(service, clientId);
          }
          else if (event instanceofClientOperationEvent.ClientDeregisterServiceEvent) {//取消注册 removePublisherIndexes(service, clientId);
          }
          else if (event instanceofClientOperationEvent.ClientSubscribeServiceEvent) {//订阅 addSubscriberIndexes(service, clientId);
          }
          else if (event instanceofClientOperationEvent.ClientUnsubscribeServiceEvent) {//取消订阅 removeSubscriberIndexes(service, clientId);
          }
          }

          建立长连接(这里的过程比较难一些,还在持续学习中)

          GrpcBiStreamRequestAcceptor这个类是建立连接的。

          每一个grpc请求过来后,都会进入到GrpcBiStreamRequestAcceptor.requestBiStream的方法中。

          而会话的长连接id就是这里的ConnectionId。


          @Servicepublic class GrpcBiStreamRequestAcceptor extendsBiRequestStreamGrpc.BiRequestStreamImplBase {
          @Autowired
          ConnectionManager connectionManager;
          private voidtraceDetailIfNecessary(Payload grpcRequest) {
          String clientIp
          =grpcRequest.getMetadata().getClientIp();
          String connectionId
          =CONTEXT_KEY_CONN_ID.get();try{if(connectionManager.traced(clientIp)) {
          Loggers.REMOTE_DIGEST.info(
          "[{}]Bi stream request receive, meta={},body={}", connectionId,
          grpcRequest.getMetadata().toByteString().toStringUtf8(),
          grpcRequest.getBody().toByteString().toStringUtf8());
          }
          }
          catch(Throwable throwable) {
          Loggers.REMOTE_DIGEST.error(
          "[{}]Bi stream request error,payload={},error={}", connectionId,
          grpcRequest.toByteString().toStringUtf8(), throwable);
          }

          }
          @Override
          public StreamObserver<Payload> requestBiStream(StreamObserver<Payload>responseObserver) {

          StreamObserver
          <Payload> streamObserver = new StreamObserver<Payload>() {final String connectionId =CONTEXT_KEY_CONN_ID.get();final Integer localPort =CONTEXT_KEY_CONN_LOCAL_PORT.get();final int remotePort =CONTEXT_KEY_CONN_REMOTE_PORT.get();

          String remoteIp
          =CONTEXT_KEY_CONN_REMOTE_IP.get();

          String clientIp
          = "";

          @Override
          public voidonNext(Payload payload) {

          clientIp
          =payload.getMetadata().getClientIp();
          traceDetailIfNecessary(payload);

          Object parseObj;
          try{
          parseObj
          =GrpcUtils.parse(payload);
          }
          catch(Throwable throwable) {
          Loggers.REMOTE_DIGEST
          .warn(
          "[{}]Grpc request bi stream,payload parse error={}", connectionId, throwable);return;
          }
          if (parseObj == null) {
          Loggers.REMOTE_DIGEST
          .warn(
          "[{}]Grpc request bi stream,payload parse null ,body={},meta={}", connectionId,
          payload.getBody().getValue().toStringUtf8(), payload.getMetadata());
          return;
          }
          if (parseObj instanceofConnectionSetupRequest) {
          ConnectionSetupRequest setUpRequest
          =(ConnectionSetupRequest) parseObj;
          Map
          <String, String> labels =setUpRequest.getLabels();
          String appName
          = "-";if (labels != null &&labels.containsKey(Constants.APPNAME)) {
          appName
          =labels.get(Constants.APPNAME);
          }

          ConnectionMeta metaInfo
          = newConnectionMeta(connectionId, payload.getMetadata().getClientIp(),
          remoteIp, remotePort, localPort, ConnectionType.GRPC.getType(),
          setUpRequest.getClientVersion(), appName, setUpRequest.getLabels());
          metaInfo.setTenant(setUpRequest.getTenant());
          Connection connection
          = newGrpcConnection(metaInfo, responseObserver, CONTEXT_KEY_CHANNEL.get());
          connection.setAbilities(setUpRequest.getAbilities());
          boolean rejectSdkOnStarting = metaInfo.isSdkSource() && !ApplicationUtils.isStarted();//这里会有一个connectionManager.register if (rejectSdkOnStarting || !connectionManager.register(connectionId, connection)) {//Not register to the connection manager if current server is over limit or server is starting. try{
          Loggers.REMOTE_DIGEST.warn(
          "[{}]Connection register fail,reason:{}", connectionId,
          rejectSdkOnStarting
          ? " server is not started" : " server is over limited.");
          connection.request(
          new ConnectResetRequest(), 3000L);
          connection.close();
          }
          catch(Exception e) {//Do nothing. if(connectionManager.traced(clientIp)) {
          Loggers.REMOTE_DIGEST
          .warn(
          "[{}]Send connect reset request error,error={}", connectionId, e);
          }
          }
          }

          }
          else if (parseObj instanceofResponse) {
          Response response
          =(Response) parseObj;if(connectionManager.traced(clientIp)) {
          Loggers.REMOTE_DIGEST
          .warn(
          "[{}]Receive response of server request ,response={}", connectionId, response);
          }
          RpcAckCallbackSynchronizer.ackNotify(connectionId, response);
          connectionManager.refreshActiveTime(connectionId);
          }
          else{
          Loggers.REMOTE_DIGEST
          .warn(
          "[{}]Grpc request bi stream,unknown payload receive ,parseObj={}", connectionId,
          parseObj);
          }

          }

          @Override
          public voidonError(Throwable t) {if(connectionManager.traced(clientIp)) {
          Loggers.REMOTE_DIGEST.warn(
          "[{}]Bi stream on error,error={}", connectionId, t);
          }
          if (responseObserver instanceofServerCallStreamObserver) {
          ServerCallStreamObserver serverCallStreamObserver
          =((ServerCallStreamObserver) responseObserver);if(serverCallStreamObserver.isCancelled()) {//client close the stream. } else{try{
          serverCallStreamObserver.onCompleted();
          }
          catch(Throwable throwable) {//ignore }
          }
          }

          }

          @Override
          public voidonCompleted() {if(connectionManager.traced(clientIp)) {
          Loggers.REMOTE_DIGEST.warn(
          "[{}]Bi stream on completed", connectionId);
          }
          if (responseObserver instanceofServerCallStreamObserver) {
          ServerCallStreamObserver serverCallStreamObserver
          =((ServerCallStreamObserver) responseObserver);if(serverCallStreamObserver.isCancelled()) {//client close the stream. } else{try{
          serverCallStreamObserver.onCompleted();
          }
          catch(Throwable throwable) {//ignore }

          }
          }
          }
          };
          returnstreamObserver;
          }
          }

          • ConnectionManager.register

            这里的connections是用来管理所有的长连接的

          • Map<String, Connection> connections = new ConcurrentHashMap<>();public synchronized booleanregister(String connectionId, Connection connection) {if(connection.isConnected()) {
            String clientIp
            =connection.getMetaInfo().clientIp;if(connections.containsKey(connectionId)) {return true;
            }
            if(checkLimit(connection)) {return false;
            }
            if(traced(clientIp)) {
            connection.setTraced(
            true);
            }
            connections.put(connectionId, connection);
            if (!connectionForClientIp.containsKey(clientIp)) {
            connectionForClientIp.put(clientIp,
            new AtomicInteger(0));
            }
            connectionForClientIp.get(clientIp).getAndIncrement();

            clientConnectionEventListenerRegistry.notifyClientConnected(connection);

            LOGGER.info(
            "new connection registered successfully, connectionId = {},connection={} ", connectionId,
            connection);
            return true;

            }
            return false;
            }

在此系列文章中,我总结了Spring几乎所有的扩展接口,以及各个扩展点的使用场景。并整理出一个bean在spring中从被加载到最终初始化的所有可扩展点的顺序调用图。这样,我们也可以看到bean是如何一步步加载到spring容器中的。


InstantiationAwareBeanPostProcessor

1、概述

public interface InstantiationAwareBeanPostProcessor extends BeanPostProcessor {
    @Nullable
    default Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
        return null;
    }

    default boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
        return true;
    }

    @Nullable
    default PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) throws BeansException {
        return null;
    }
}

Spring框架提供了许多扩展接口,用于在Bean的生命周期中插入自定义逻辑。其中之一是InstantiationAwareBeanPostProcessor接口,它允许我们在Spring容器实例化Bean之前和之后进行一些自定义处理。

InstantiationAwareBeanPostProcessor接口是BeanPostProcessor接口的子接口,它定义了在Bean实例化过程中的扩展点。与BeanPostProcessor接口相比,InstantiationAwareBeanPostProcessor接口提供了更细粒度的控制能力。它在Bean实例化的不同阶段提供了多个回调方法,允许我们在不同的时机对Bean进行自定义处理。

在Spring容器启动过程中,InstantiationAwareBeanPostProcessor接口的方法执行顺序如下:

  1. postProcessBeforeInstantiation方法:在Bean实例化之前调用,如果返回null,一切按照正常顺序执行,如果返回的是一个实例的对象,那么这个将会跳过实例化、初始化的过程
  2. postProcessAfterInstantiation方法:在Bean实例化之后调用,可以对已实例化的Bean进行进一步的自定义处理。
  3. postProcessPropertyValues方法:在Bean的属性注入之前调用,可以修改Bean的属性值或进行其他自定义操作,
    当postProcessAfterInstantiation返回true才执行。
方法 执行顺序 备注
postProcessBeforeInstantiation() 在 Bean 创建前调用 可用于创建代理类,如果返回的不是 null(也就是返回的是一个代理类) ,那么后续只会调用 postProcessAfterInitialization() 方法
postProcessAfterInstantiation() 在 Bean 创建后调用 返回值会影响 postProcessProperties() 是否执行,其中返回 false 的话,是不会执行。
postProcessProperties() 在 Bean 设置属性前调用 用于修改 bean 的属性,如果返回值不为空,那么会更改指定字段的值

2、简单案例

下面是一个示例,演示了TestUser这个Bean内部的执行流程。


// InstantiationAwareBeanPostProcessor扩展实现
@Component
public class MyInstantiationAwareBeanPostProcessor implements InstantiationAwareBeanPostProcessor {
    @Override
    public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
        if(isMatchClass(beanClass)){
            System.out.println("调用 postProcessBeforeInstantiation 方法");
        }
        return null;
    }

    @Override
    public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
        if(isMatchClass(bean.getClass())){
            System.out.println("调用 postProcessAfterInstantiation 方法");
        }
        return true;
    }

    @Override
    public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) throws BeansException {
        if(isMatchClass(bean.getClass())){
            System.out.println("调用 postProcessProperties 方法");
        }
        return pvs;
    }

    private boolean isMatchClass(Class<?> beanClass){
        return TestUser.class.equals(ClassUtils.getUserClass(beanClass));
    }
}

// TestUser测试类
@Component
public class TestUser implements InitializingBean {
    String name;
    String password;

    public TestUser() {
        System.out.println("创建【TestUser】对象");
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        System.out.println("设置【name】属性");
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        System.out.println("设置【password】属性");
        this.password = password;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("所有属性设置完毕");
    }
}

输出:

调用 postProcessBeforeInstantiation 方法
创建【TestUser】对象
调用 postProcessAfterInstantiation 方法
调用 postProcessProperties 方法
所有属性设置完毕

3、源码分析

InstantiationAwareBeanPostProcessor是在对象实例化和初始化前后执行的逻辑,因此主要的代码都在getBean,doGetBean,cerateBean方法中。

  • 在MyBeanFactoryPostProcessor打上断点,启动SpringApplication,可以看到左下角的调用链路。

  • spring的AbstractApplicationContext的refresh方法,执行this.onRefresh()。

  • 在实例化之前,调用postProcessBeforeInstantiation方法入口就在this.resolveBeforeInstantiation(beanName, mbdToUse)中。

  • bean = this.applyBeanPostProcessorsBeforeInstantiation(targetType, beanName)中遍历InstantiationAwareBeanPostProcessor的postProcessBeforeInstantiation方法。
  • 若 this.applyBeanPostProcessorsBeforeInstantiation(targetType, beanName)返回了已实例化的Bean,则执行调用postProcessAfterInitialization方法。

  • 在执行完
    resolveBeforeInstantiation()
    后,调用
    doCreateBean()

  • doCreateBean()
    中先实例化Bean,再调用
    populateBean()
    执行后续的postProcessAfterInstantiation()和postProcessProperties()。

前言

今天分享一个SpringBoot集成腾讯云短信的功能,平常除了工作,很多xdm做自己的小项目都可能用到短信,但自己去看文档挺费劲的,我这边就帮你节省时间,直接把步骤给你列出来,照做就行。

实战

1、申请密钥及签名模板

首先,要使用腾讯云短信,你得先在腾讯云有个账号,申请密钥及签名模板。

1)、找到访问管理-API密钥管理,勿泄漏。

image


2)、签名及模板

要申请,推荐用公众号,描述中写几句赞美腾讯云的话,几分钟后就可以过审了。

image

image


3)、应用SDK APPID

image


4)、短信工具类

应用ID、签名、模板id都从上面找到后改为自己的就行了。

image

2、代码集成

腾讯云短信官方文档:
https://cloud.tencent.com/document/product/382/43194


1)、引入依赖

一般在common模块中引入即可

<!-- 腾讯云短信 -->
<!--请到https://search.maven.org/search?q=tencentcloud-sdk-java查询所有版本,最新版本如下-->
<dependency>
    <groupId>com.tencentcloudapi</groupId>
    <artifactId>tencentcloud-sdk-java</artifactId>
    <version>3.1.714</version>
</dependency>


2)、新增properties配置

一般这种第三方接入的配置使用properties较好,和yml配置做区分。密钥参考前面的说明。

image


3)、新增配置类

package com.imooc.utils;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

@Component
@Data
@PropertySource("classpath:tencentCloud.properties")
@ConfigurationProperties(prefix = "tencent.cloud")
public class TencentCloudProperties {

    private String secretId;
    private String secretKey;

}


4)、短信工具类

从官网拷过来修改即可,记得修改其中的应用ID、签名、模板id。

package com.imooc.utils;

import com.tencentcloudapi.common.Credential;
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
import com.tencentcloudapi.common.profile.ClientProfile;
import com.tencentcloudapi.common.profile.HttpProfile;
import com.tencentcloudapi.sms.v20210666666.SmsClient;
import com.tencentcloudapi.sms.v20210666666.models.SendSmsRequest;
import com.tencentcloudapi.sms.v20210666666.models.SendSmsResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class SMSUtils {
    @Autowired
    private TencentCloudProperties tencentCloudProperties;

    public void sendSMS(String phone, String code) throws Exception {
    
        try {
        
            /* 必要步骤:
             * 实例化一个认证对象,入参需要传入腾讯云账户密钥对secretId,secretKey。
             * 这里采用的是从环境变量读取的方式,需要在环境变量中先设置这两个值。
             * 你也可以直接在代码中写死密钥对,但是小心不要将代码复制、上传或者分享给他人,
             * 以免泄露密钥对危及你的财产安全。
             * CAM密匙查询获取: https://console.cloud.tencent.com/cam/capi
             */
            Credential cred = new Credential(tencentCloudProperties.getSecretId(),
                tencentCloudProperties.getSecretKey());

            // 实例化一个http选项,可选的,没有特殊需求可以跳过
            HttpProfile httpProfile = new HttpProfile();

            // httpProfile.setReqMethod("POST"); // 默认使用POST

            /* 
             * SDK会自动指定域名。通常是不需要特地指定域名的,但是如果你访问的是金融区的服务
             * 则必须手动指定域名,例如sms的上海金融区域名: sms.ap-shanghai-fsi.tencentcloudapi.com 
             */
            httpProfile.setEndpoint("sms.tencentcloudapi.com");

            // 实例化一个client选项
            ClientProfile clientProfile = new ClientProfile();
            clientProfile.setHttpProfile(httpProfile);
            // 实例化要请求产品的client对象,clientProfile是可选的
            SmsClient client = new SmsClient(cred, "ap-nanjing", clientProfile);

            // 实例化一个请求对象,每个接口都会对应一个request对象
            SendSmsRequest req = new SendSmsRequest();
            String[] phoneNumberSet1 = {
                "+86" + phone
            }; //电话号码
            req.setPhoneNumberSet(phoneNumberSet1);
            req.setSmsSdkAppId("xxx"); // 短信应用ID: 短信SdkAppId在 [短信控制台] 添加应用后生成的实际SdkAppId
            req.setSignName("Java分享XX"); // 签名
            req.setTemplateId("xxx"); // 模板id:必须填写已审核通过的模板 ID。模板ID可登录 [短信控制台] 查看

            /* 模板参数(自定义占位变量): 若无模板参数,则设置为空 */
            String[] templateParamSet1 = {
                code
            };
            req.setTemplateParamSet(templateParamSet1);

            // 返回的resp是一个SendSmsResponse的实例,与请求对象对应
            SendSmsResponse resp = client.SendSms(req);
            // 输出json格式的字符串回包
            // System.out.println(SendSmsResponse.toJsonString(resp));
        }
        catch (TencentCloudSDKException e) {
            System.out.println(e.toString());
        }
    }
}


5)、测试效果

在服务中写一个方法测试即可,然后启动网关和user服务,访问
http://127.0.0.1:8000/u/sms
等一会儿就有短信通知了。

@Autowired
private SMSUtils smsUtils;

@GetMapping("sms")
public Object sendSMS() throws Exception {

    smsUtils.sendSMS("159xxxxxxxx", "6752");

    return "sendSMS OK!";
}


总结

集成第三方的短信接口其实很简单,费时间的主要是申请一些东西,以及阅读接口文档。

大家如果想省事,按照我的步骤来就行,接入个短信功能,也花不了什么钱。既可以体验下接入方式,也可以为自己的小项目增加一些亮点。

好了,今天的小知识你学会了吗?


喜欢请点赞+关注↓↓↓,持续分享干货哦~