开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

C#网络编程整理

发表于 2021-06-18

这是我参与更文挑战的第18天,活动详情查看: 更文挑战

引言

不好意思各位,最近公司项目比较忙,鸽了好久的文章,现在重新拾起来。

我个人比较愿意将工作过程中学习的知识,问题的解决思路,通用的处理方式等做一个总结,方便自己,也照亮他人。为什么是工作过程中的,因为只有真实的场景下碰到的问题才是有价值的内容,我愿意分享,但是我也做不到像某些大佬一样熬夜肝文章,本着身体是第一位的原则,适当做一些总结是我现在做事的思路。

项目叙述

这次要讲的不是什么复杂的项目,是一个基于WebSocket的通信服务程序。

在做这个项目前,我其实没有接触过通信服务程序这一块内容,之前主要都是写的WebApi和后台管理系统接口。当然我也知道Tcp是面向连接的可靠的,Udp是不可靠的这些基于知识,但在实际的应用场景下使用这还是第一次,可能有些人觉得这种简单的东西有什么好讲的,那这样你可以选择关闭网页,合适的东西应该给合适的人看,这是我的观点。

开发语言c#,开发一个基于WebSocket的通信程序,客户端按照规定的报文格式发送,服务端可以正常解析。

报文格式如下

名称 帧头 数据长度 CRC 校验 数据段
长度(字节) 2 2 2 N
说明 TT

所有数据帧, 皆为高字节在前模式, 即小端模式。
CRC 的校验范围为: 从包头至包尾巴。 校验初始, CRC 校验位置要先清零。
CRC 校验方式:
初始校验值为 0xAA, 所有字节依次累加, 最后求和

项目技术预演

没有先例如何开发一个稳定的Tcp通信服务?

在开发本任务前我并没有做通信服务的经验,为了最快成本的减少踩坑和开发顺畅,我选择了使用现成的框架。在综合后我选择了RRMQSocket。

日月之行,若出其中;星汉灿烂,若出其里。

RRQMSocket是一个整合性的、超轻量级的网络通信服务框架。它具有 高并发连接 、 高并发处理 、 事件订阅 、 插件式扩展 、 多线程处理 、 内存池 、 对象池 等特点,让使用者能够更加简单的、快速的搭建网络框架。

选择他的理由其实比较简单,开源【gitee 推荐项目】、有文档、持续维护。

我通过作者提供的文档首先对通信服务有了一个基本了解,然后使用框架编写一个最基本案例:一个简单的tcp通信服务,增加自己对框架的了解。

到此,对框架的工作暂时高一段落。

什么是小端模式

在计算机中一般讲字节序分为两类:Big-Endian(大端字节序) 和 Little-Endian(小端字节序)
a) Little-Endian 高位字节在前,低位字节在后。
b) Big-Endian 低位字节在前,高位字节在后。

网络字节序:TCP/IP各层协议将字节序定义为Big-Endian,因此TCP/IP协议中使用的字节序通常称之为网络字节序。
举个小例子:
整数127(十进制)在计算机(64位)中大/小端字节序

img

c#中对字节序的处理

首先默认的 C# 使用的是小端序。直接上案例,大家可以按照上图进行分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
ini复制代码using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;

namespace ConsoleApplication2
{
class Program
{
static void Main(string[] args)
{
byte[] b = { 1, 2, 3, 4 };
Console.WriteLine(bytes2string(b));

//x1是小端模式值:x1=67305985=0x 04 03 02 01
int x1 = BitConverter.ToInt32(b, 0);
Console.WriteLine(x1.ToString("X2"));

//x2是大端模式值:x2=16909060=0x 01 02 03 04
int x2 = IPAddress.NetworkToHostOrder(x1);

Console.WriteLine(x2.ToString("X2"));
}
}
}

如何进行大端序和小端序的转换?

在上面的案例中细心的服友肯定已经发现了处理方式

推荐方式

1
2
arduino复制代码System.Net.IPAddress.HostToNetworkOrder(本机到网络转换)
System.Net.IPAddress.NetworkToHostOrder(网络字节转成本机)

其他方式

字节数组reverse反转,即得到相反的字节序

1
scss复制代码byte[].Reverse().ToArray();

代码示例

short表示短整形,short 6 占2个字节,内存表现为 06 00 值=06

使用HostToNetworkOrder将x转换为大端字节表现形式,b=1536 ,内存表现形式00 06

最调用`BitConverter.GetBytes’将大端字节数1536转为大端字节数组表现形式 00 06

1
2
3
4
ini复制代码short x = 6;
byte[] aa = System.BitConverter.GetBytes(x); //默认的小端字节序字节数组。
short b = System.Net.IPAddress.HostToNetworkOrder(x); //把x转成相应的大端字节数
byte[] bb = System.BitConverter.GetBytes(b); //这样直接取到的就是大端字节序字节数组。

image-20210511154830146

对于字符串型:使用 System.Text.Encoding.Default.GetBytes();

直接取字串对应字节数组。

CRC校验是什么,有什么作用

crc定义

CRC即循环冗余校验码:是数据通信领域中最常用的一种查错校验码,其特征是信息字段和校验字段的长度可以任意选定。循环冗余检查(CRC)是一种数据传输检错功能,对数据进行多项式计算,并将得到的结果附在帧的后面,接收设备也执行类似的算法,以保证数据传输的正确性和完整性。

crc校验作用

在信息的传输过程中,有时会发生误码。

例如,传送1001,接收到1000,这就产生了误码,但接收方并不知道产生了误码。

而当发送方与接收方使用同一标准的CRC校验,就能够判断在信息传输的过程中是否发生了误码。

进制处理相关

c# 在进行进制处理时有一些常用的方法,记录下来方便自己后期回顾

单个10进制转为16进制字符串
1
2
3
4
5
6
vbscript复制代码X.ToString("X2")即转化为大写的16进制。
X.ToString("x2")即转化为小写的16进制。
2表示输出两位,不足2位的前面补0,如 0x0A 如果没有2,就只会输出0xA

Console.WriteLine(10.ToString("X")); //输出A
Console.WriteLine(10.ToString("X2"));//输出0A

借着这个机会我又重温了一下 string.Format函数

1
2
arduino复制代码Console.WriteLine(string.Format("{0:X}", 12)); //输出A
Console.WriteLine(string.Format("{0:X2}", 12));//输出OA
字节数组转为字符串,其实就是一个循环
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码public static string BytesTostring(byte[] bytes)
{
string hexString = string.Empty;
if (bytes != null)
{
StringBuilder strB = new StringBuilder();

for (int i = 0; i < bytes.Length; i++)
{
strB.Append(bytes[i].ToString("X2"));
}
hexString = strB.ToString();
}
return hexString;
}
官方库自带的字节处理帮助类

using namespace System

BitConverter.方法

参考

C# 大端与小端

C#中string.format用法详解

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Mysql 数据同步到 ES 的技术方案选型和思考

发表于 2021-06-18

MySQL 到 ES 数据同步本质上是数据去规范化的一种。本节我们展开 “MySQL 到 ES 数据迁移同步” 的技术解决方案,通过比较他们的优缺点和应用场景给读者提供一些思路。

去规范化
数据库规范化 是指关系数据库中通过一系列数据库范式来减少冗余、增强数据一致性的策略。

但是为什么要进行去规范化呢?

规范化在带来我们看得见的好处(利于事物操作性能、存储成本降低)的同时,伴随数据规模扩大、并发度提高、复杂度上升,他的弊端也慢慢展现。

这个时候通过数据库去规范化能够一定程度满足这些挑战,总体思路是通过一系列降低写入性能的操作例如更多的数据冗余、数据分组等来提升数据库读取的性能。

去规范化的时机
数据去规范化动机多样,当出现因数据复杂操作影响系统稳定性、业务响应/并发要求不满足等都是触发因素。

业务稳定性问题:面向 C 端的互联网应用特征是并发量较高,SQL 偏向点查点写,相对简单,但是沉淀下来的数据需要做运营往往涉及传统企业级应用对于数据库的操作特征。大范围数据删查、表关联、排序等实时操作,以及满足报表/BI 等更加复杂的数据库需求。通过去规范化和负载分离是较合理的选择。

复杂查询性能问题:企业级应用经常涉及表关联、聚合、多维筛选、排序等操作,并常常带来性能问题。通过去规范化的一些方式,如下文提到的数据冗余和预计算方式,显著改善性能。

去规范化的几种实现方式

列级处理 主查询表冗余字段

通过在主表冗余计算好的数据,可避免频繁重复计算数据。如下场景适合在主数据表内冗余数据:

  • 应用系统需要经常获取计算好的数据
  • 冗余的原始数据不经常变化

优点:方法简单,容易实现

缺点:侵入业务逻辑,拖慢业务代码性能的同时,长期迭代所产生的变化可能会有稳定性风险

表级处理 宽表预构建 / Cube 预构建

主要操作就是构建宽表,或者构建数据立方体(Data Cube)。构建好的宽表包含了用户查询时需要的所有维度、度量信息。

常见的表级处理包括:应用多写、数据库自身实现的物化视图、数据迁移同步。

应用多写

在主数据相同数据库内创建宽表,应用写入数据的同时也想宽表写入数据(事务保证一致性),复杂查询即可从该表进行。

优点:实现简单、低成本

缺点:对主数据库造成更大的读写压力,外加业务改造成本

RDBMS 物化视图

物化视图(快照)是包括一个查询结果的数据库只读对象,它是远程数据的本地副本,或者用来生成基于数据表求和的汇总表。通过数据冗余与预计算减少 join、聚合,从而提升查询性能。

优点:数据库引擎自身支持,使用成本较低

缺点:RDBMS 实现的方式有自己的局限性,比如生成物化视图的数据需要做一些业务紧相关变换就无法满足,或者某些数据库并没有完整实现该能力

数据迁移同步

借助数据同步工具,准实时将主数据表数据组织变换(包括按照业务逻辑变换)形成普通表或大宽表,写入第三方存储引擎。复杂查询直接在预构建好的表或者 cube 上执行,从而达到良好的性能。数据迁移工具的选择较多,总体上按照其侧重点,可以分为如下几类:

大数据类、流计算类、消息类、数据库类、云厂商类、专业数据迁移同步工具

优点:

主库更稳定:异步化解耦业务系统事务查询和复杂查询,避免复杂查询对主数据库产生影响。

易运维、链路稳定:数据迁移同步链路有标准化产品支撑,和主业务系统、主库读写解耦。整体架构上职责清晰,易于维护和问题追踪。

缺点:需要对纷繁多样的数据迁移同步工具、承载复杂查询数据库产品选型。

MySQL 到 ES 数据实时同步技术架构
在此介绍 “MySQL 到 ES 数据迁移同步” 的技术解决方案

为什么选 Mysql?

高并发能力:MySQL 内核特征特别适合高并发简单 SQL 操作,链接轻量化(线程模式),优化器、执行器、事务引擎相对简单。

稳定性好:主数据库最大的要求就是稳定、不丢数据。MySQL 主备系统在应对崩溃的情况下快速切换,innodb 存储引擎也保障了 MySQL 下盘稳定。

操作便捷:良好、便捷的用户体验,容易上手,学习成本低。

开源生态:MySQL 开源,让上下游厂商围绕其构建工具相对简单,HA proxy、分库分表中间件让其实用性大大加强,同时开源的特质让其有大量的用户。

为什么是 ES?

ES 的几个显著特点,能有效补足 MySQL 在企业级数据操作场景的缺陷

文本搜索能力:ES 是基于倒排索引实现的搜索系统,配合多样的分词器,在文本模糊匹配搜索上表现得比较好,业务场景广泛。

多维筛选性能好:亿级规模数据使用宽表预构建(消除 join),配合全字段索引,使 ES 在多维筛选能力上具备压倒性优势,加上文本搜索能力,独此一家。

开源和商业并行:ES 开源生态非常活跃,具备大量的用户群体,同时其内后也有独立的商业公司支持,而这让用户根据自身特点有了更加多样、渐进的选择。

为什么是数据迁移同步方式?

稳定性好:迁移同步对主数据库的操作主要是进行数据和日志的顺序读取,同时并发小,对主数据库稳定性影响较小(较多的下游订阅可能在网络层面存在影响,一般用消息解决)。另外日志(Binlog/WAL/Redo 等)可重放特质,让下游丢数据的可能性大大减小(处理好幂等的情况下)

业务解耦:一般而言主数据库更多承载事务型操作,而下游数据系统承载运营等层面的业务。

业务侵入小:数据迁移同步对业务无侵入,双端对接标准数据库(源),可以便利地找到开源、商业、云等各个方向的成熟解决方案或产品。

业务适配性好:某些数据迁移同步产品能够嵌入业务逻辑,让下游获取到更加贴近业务的数据,从而让数据服务更加有效和便捷。

数据迁移同步模型选择

订阅消费

image.png
优点:

堆积能力:由于引入了消息队列,所以整个链路是具备变更数据的堆积能力的。假设变更数据消费的比较慢,MySQL 本地较老的 binlog 文件由于磁盘空间的不足而被删除时,消息队列的数据依然存在,数据同步依然可以进行。

数据分发能力:引入消息队列后可以支持多方订阅。如果下游多个应用都依赖源端的变更数据,可以订阅同一份 topic 即可。

数据加工能力:由于变更数据是由下游消费者订阅,因此订阅后可以灵活的做一些数据加工。例如从外部调用微服务接口或者反查一些数据来做数据加工都是比较方便的。

缺点:

运维成本相对较高:包含了较多的组件和应用,运维保障相对复杂。

稳定性风险较高:一环出问题会导致整个数据同步链路的稳定性收到影响。而且排查和定位问题也会比较困难。

端对端直连

image.png

优点:

低延迟:端对端的直接同步,链路较短,延迟低

稳定性好:链路组件少,出问题概率较低,定位排查均比较容易。适合对数据精确性高的严苛场景。

功能拓展性强:对端写入消息系统,模拟订阅模式,可拓展性强

运维部署检点:链路组件少,部署运维更简单

数据迁移同步模型选择总结

如果没有众多的下游数据订阅,建议采用直连模式。数据同步链路往往置于在线业务中,随着业务规模以及重要性逐渐加大,链路稳定性更为重要些。

另外端到端模式只要支持对端数据源为消息中间件,可立即实现订阅模式,数据加工能力在某些数据迁移同步产品上可通过上传业务代码运行的方式解决。

数据架构在满足业务需求的同时,简洁和清晰能够让系统更加易于维护和排查,当遇到链路每天同步大量数据、偶尔丢几条需要排查,或同步链路卡住不同步等情况,端对端方式往往体现出相当大的优势。

MySQL 关联表在 ES 上的设计
关系型数据库中的表 join 关系在 ES 可以用几种数据类型来表达,包括 objected,nested,join 三种。

objected

object 类型可以存储嵌套结构。

优点:表示主 field 和 object 内部 field 之间的一对多关系,支持 doc 的 join 查询。由于所有查询时依赖的关联数据也都在一个文档内,避免了 ES 内部的 join,查询效率较高。

缺点:一对多关系只能保留一层,多于一层的会被打平,会丢失嵌套 field 内部的关联关系。

nested

nested 类型可以存储嵌套结构,表示一对多关系,是 object 类型的拓展。

优点:

不会出现 object 的缺点,整个嵌套关系是完整维护的,子文档内部关联关系保存是完整的

关联数据通过实现上自然关联到主文档上,搜索时性能较好(相对于 join)

缺点:

一个 nested field 只能属于一个主文档

在 nested 类型中,子文档和主文档之间强绑定,主文档更新的时候会强制更新子文档。在写多读少的场景,新能开销较大。

child 文档的查询必须通过父文档再找到子文档

子文档频繁修改的话会影响别的子文档和父文档,因为本质上在 lucene 实现上是父文档下的冗余存档

join

join 类型可以配置父子文档,通过父子文档来实现一对多的能力,一个索引只能建一个。相比 nested 类型,该类型更加灵活。父子文档之间通过 parentId 来关联,实际实现上他们就是独立的文档。

优点:

子文档更新不影响父文档和其他子文档

一个子文档可以单独搜索

一个文档在作为子文档时可以自己选择属于哪个父文档。通过 relation 可以指定不同的 join 列

缺点:

需要建个全局序数,用于服务于父子文档的关联关系,这个会影响搜索性能

join 和 nested 类型比较

join 适合写多读少场景,更加适合关注索引性能的场景。这意味着更新的生效会更快,但是搜索时的开销也相对大些

nested 适合读多写少的场景,更加关注搜索的性能

去规范化实现
下面将介绍几种数据同步去规范化的几种方式:

主表冗余数据

业务侧将一些查询时需要的关系数据提前冗余在源表的一个字段中。

image.png
优点:处理模式能应对各种一对多的关联关系,对数据同步工具的功能要求低,配置简单,只需要支持单同步到 ES 即可。

缺点:

索引、搜索性能非最佳:提供给 ES 的不是构建好的宽表数据。这种实现方式会有索引、搜索性能方面的额外开销,不是性能最佳的实现方式。

业务系统入侵:业务系统写主数据的时候需要额外写入信息。

主数据库表冗余过多数据:关系型数据库表冗余了过多其他表的信息,可能存在存储和性能开销。

总结: 不太推荐该方式

多表订阅并预构建宽表数据

数据同步工具同时订阅搜索时依赖的所有表,先到的数据先进到 ES,没有数据过来的字段为空。

image.png

优点:

数据同步工具配置同步任务较为简单,无业务入侵,不耦合业务系统逻辑。

对数据同步工具要求低,除了同步以外,不需要其他额外的功能特性。

基于预构建宽表的方式在 ES 上也有较好的索引和查询性能。

同步链路不会因为宽表某些列缺失数据阻塞整个数据链路的同步。

缺点:基于事实表主动触发式的方式来进行宽表的构建。源端订阅的表,如果更新很少或者从来不更新产生 binlog,则对端的文档中的列值可能一直不完整,导致时效性会比较差。搜索的时候有一些列的数据会缺少。

总结:

适合构成宽表的事实表数据写入有事务保证一起落盘的场景,这样可以避免对端 ES 搜素到不完整的数据。

适合构建宽表不需要业务加工处理的场景,构建宽表只是单数的将多张表的列拼接在一起,形成宽表。

同步过程回查预构建

数据同步工具订阅的表称为主表。数据同步过程中,反查数据库查询的表称为从表。利用数据同步工具自身的能力,在订阅主表期间,自动通过回查的方式,填补宽表中的列,形成完整的宽表行数据。

image.png

优点:

基于反查的方式构建宽表灵活性好,可以在生成宽表前基于主表的数据对从表数据做一些轻度的数据加工。

一条主表的数据,通过反查生成宽表行,可以配合数据加工生成多条宽表行数据。

基于反查的方式可以比较轻松的实现跨实例的 join,从而生成宽表行。

基于宽表预构建的方式在 ES 有较好的索引、查询性能。

缺点:

反查时数据可能没有准备好,导致数据确实

需要数据同步工具在数据反差、数据加工方面进行支持

总结:

适合构建宽表涉及数据加工的场景

引用
MySQL 数据实时同步到 Elasticsearch 的技术方案选型和思考

物化视图和普通视图的区别

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring Boot 2x基础教程:使用LDAP来管理用

发表于 2021-06-18

很多时候,我们在做公司系统或产品时,都需要自己创建用户管理体系,这对于开发人员来说并不是什么难事,但是当我们需要维护多个不同系统并且相同用户跨系统使用的情况下,如果每个系统维护自己的用户信息,那么此时用户信息的同步就会变的比较麻烦,对于用户自身来说也会非常困扰,很容易出现不同系统密码不一致啊等情况出现。

如果此时我们引入LDAP来集中存储用户的基本信息并提供统一的读写接口和校验机制,那么这样的问题就比较容易解决了。尤其在一些内部管理系统的开发和搭建时,往往我们的内部系统一开始并不全是自己开发的,还有很多第三方产品支持,比如:OA系统、财务系统等,如果自己开发一套用户管理系统,那么这些系统对接还得二次开发,成本很大。由于LDAP并不是什么新技术,大部分成熟软件都支持用LDAP来管理用户,所以时至今日,LDAP的应用依然可以经常看到。

下面我们就具体来看看,当使用Spring Boot开发的时候,如何来访问LDAP服务端。

LDAP简介

LDAP(轻量级目录访问协议,Lightweight Directory Access Protocol)是实现提供被称为目录服务的信息服务。目录服务是一种特殊的数据库系统,其专门针对读取,浏览和搜索操作进行了特定的优化。目录一般用来包含描述性的,基于属性的信息并支持精细复杂的过滤能力。目录一般不支持通用数据库针对大量更新操作操作需要的复杂的事务管理或回卷策略。而目录服务的更新则一般都非常简单。这种目录可以存储包括个人信息、web链结、jpeg图像等各种信息。为了访问存储在目录中的信息,就需要使用运行在TCP/IP 之上的访问协议—LDAP。

LDAP目录中的信息是是按照树型结构组织,具体信息存储在条目(entry)的数据结构中。条目相当于关系数据库中表的记录;条目是具有区别名DN (Distinguished Name)的属性(Attribute),DN是用来引用条目的,DN相当于关系数据库表中的关键字(Primary Key)。属性由类型(Type)和一个或多个值(Values)组成,相当于关系数据库中的字段(Field)由字段名和数据类型组成,只是为了方便检索的需要,LDAP中的Type可以有多个Value,而不是关系数据库中为降低数据的冗余性要求实现的各个域必须是不相关的。LDAP中条目的组织一般按照地理位置和组织关系进行组织,非常的直观。LDAP把数据存放在文件中,为提高效率可以使用基于索引的文件数据库,而不是关系数据库。类型的一个例子就是mail,其值将是一个电子邮件地址。

LDAP的信息是以树型结构存储的,在树根一般定义国家(c=CN)或域名(dc=com),在其下则往往定义一个或多个组织 (organization)(o=Acme)或组织单元(organizational units) (ou=People)。一个组织单元可能包含诸如所有雇员、大楼内的所有打印机等信息。此外,LDAP支持对条目能够和必须支持哪些属性进行控制,这是有一个特殊的称为对象类别(objectClass)的属性来实现的。该属性的值决定了该条目必须遵循的一些规则,其规定了该条目能够及至少应该包含哪些属性。例如:inetorgPerson对象类需要支持sn(surname)和cn(common name)属性,但也可以包含可选的如邮件,电话号码等属性。

LDAP简称对应

  • o:organization(组织-公司)
  • ou:organization unit(组织单元-部门)
  • c:countryName(国家)
  • dc:domainComponent(域名)
  • sn:surname(姓氏)
  • cn:common name(常用名称)

以上内容参考自:LDAP快速入门

入门示例

在了解了LDAP的基础概念之后,我们通过一个简单例子进一步理解!

  • 创建一个基础的Spring Boot项目(如果您还不会,可以参考这篇文章:快速入门1)
  • 在pom.xml中引入两个重要依赖
1
2
3
4
5
6
7
8
9
10
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-ldap</artifactId>
</dependency>

<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<scope>test</scope>
</dependency>

其中,spring-boot-starter-data-ldap是Spring Boot封装的对LDAP自动化配置的实现,它是基于spring-data-ldap来对LDAP服务端进行具体操作的。

而unboundid-ldapsdk主要是为了在这里使用嵌入式的LDAP服务端来进行测试操作,所以scope设置为了test,实际应用中,我们通常会连接真实的、独立部署的LDAP服务器,所以不需要此项依赖。

  • 在src/test/resources目录下创建ldap-server.ldif文件,用来存储LDAP服务端的基础数据,以备后面的程序访问之用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
makefile复制代码dn: dc=didispace,dc=com
objectClass: top
objectClass: domain
objectclass: extensibleObject
dc: didispace

dn: ou=people,dc=didispace,dc=com
objectclass: top
objectclass: organizationalUnit
ou: people

dn: uid=ben,ou=people,dc=didispace,dc=com
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: didi
sn: zhaiyongchao
uid: didi
userPassword: {SHA}nFCebWjxfaLbHHG1Qk5UU4trbvQ=

这里创建了一个基础用户,真实姓名为zhaiyongchao,常用名didi,在后面的程序中,我们会来读取这些信息。更多内容解释大家可以深入学习LDAP来理解,这里不做过多的讲解。

  • 在application.properties中添加嵌入式LDAP的配置
1
2
properties复制代码spring.ldap.embedded.ldif=classpath:ldap-server.ldif
spring.ldap.embedded.base-dn=dc=didispace,dc=com
  • 使用spring-data-ldap的基础用法,定义LDAP中属性与我们Java中定义实体的关系映射以及对应的Repository
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@Data
@Entry(base = "ou=people,dc=didispace,dc=com", objectClasses = "inetOrgPerson")
public class Person {

@Id
private Name id;
@DnAttribute(value = "uid", index = 3)
private String uid;
@Attribute(name = "cn")
private String commonName;
@Attribute(name = "sn")
private String userName;
private String userPassword;

}

public interface PersonRepository extends CrudRepository<Person, Name> {

}

通过上面的定义之后,已经将Person对象与LDAP存储内容实现了映射,我们只需要使用PersonRepository就可以轻松的对LDAP内容实现读写。

  • 创建单元测试用例读取所有用户信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@Slf4j
@SpringBootTest
public class ApplicationTests {

@Autowired
private PersonRepository personRepository;

@Test
public void findAll() {

personRepository.findAll().forEach(p -> {
System.out.println(p);
});

}

}

启动该测试用例之后,我们可以看到控制台中输出了刚才维护在ldap-server.ldif中的用户信息:

1
ini复制代码Person(id=uid=ben,ou=people,dc=didispace,dc=com, uid=ben, commonName=didi, userName=zhaiyongchao, userPassword=123,83,72,65,125,110,70,67,101,98,87,106,120,102,97,76,98,72,72,71,49,81,107,53,85,85,52,116,114,98,118,81,61)

添加用户

通过上面的入门示例,如果您能够独立完成,那么在Spring Boot中操作LDAP的基础目标已经完成了。

如果您足够了解Spring Data,其实不难想到,这个在其下的子项目必然也遵守Repsitory的抽象。所以,我们可以使用上面定义的PersonRepository来轻松实现操作,比如下面的代码就可以方便的往LDAP中添加用户:

1
2
3
4
5
6
java复制代码Person person = new Person();
person.setUid("uid:1");
person.setSuerName("AAA");
person.setCommonName("aaa");
person.setUserPassword("123456");
personRepository.save(person);

如果还想实现更多操作,您可以参考spring-data-ldap的文档来进行使用。

连接LDAP服务端

在本文的例子中都采用了嵌入式的LDAP服务器,事实上这种方式也仅限于我们本地测试开发使用,真实环境下LDAP服务端必然是独立部署的。

在Spring Boot的封装下,我们只需要配置下面这些参数就能将上面的例子连接到远端的LDAP而不是嵌入式的LDAP。

1
2
3
4
properties复制代码spring.ldap.urls=ldap://localhost:1235
spring.ldap.base=dc=didispace,dc=com
spring.ldap.username=didispace
spring.ldap.password=123456

关注我,后面更新如何与Spring Security结合使用!

本系列教程《Spring Boot 2.x基础教程》点击直达!。学习过程中如遇困难,建议加入Spring技术交流群,参与交流与讨论,更好的学习与进步!

代码示例

本文的相关例子可以查看下面仓库中的chapter2-8目录:

  • Github:github.com/dyc87112/Sp…
  • Gitee:gitee.com/didispace/S…

如果您觉得本文不错,欢迎Star支持,您的关注是我坚持的动力!

欢迎关注我的公众号:程序猿DD,分享外面看不到的干货与思考!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

使用Docker申请和自动续期基于阿里云的Let's Enc

发表于 2021-06-18

1. 安装和配置Docker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ruby复制代码yum install docker

# 关闭seLinux
setenforce 0
usermod -G root dockerroot
// 配置镜像源
vi /etc/docker/daemon.json
{
"registry-mirrors": ["镜像源地址"],
"log-driver":"json-file",
"log-opts": {"max-size":"100m", "max-file":"2"}
}

systemctl daemon-reload
systemctl restart docker

一定要执行setenforce和usermod命令,否则后面申请证书时会报权限错误

2. 编写Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
bash复制代码# VERSION 1.0
# Author: xxxx

#基础镜像
FROM certbot/certbot

#作者
MAINTAINER xxxx <xxxx@xxxx.com>

RUN pip install --upgrade pip \
&& pip install certbot-apache certbot-dns-aliyun \
&& mkdir -p /project/conf/aliyun \

3. 构建docker镜像

在Dockerfile所在的目录下执行docker build -t aliyun-certbot:v1.0 .命令,构建过程中会出现红色的错误,不用理会,不影响正常使用,最后会出现Successfully built代表镜像构建成功,执行docker images命令进行查看

4. 申请并配置阿里云DNS访问密钥

前往ram.console.aliyun.com 申请子账号并配置AliyunDNSFullAccess权限。然后为子账号配置AccessKey并记录。

5. 创建certbot-dns-aliyun的配置文件credentials.ini

1
2
3
4
bash复制代码cat > /opt/aliyun-dns/credentials.ini <<EOF
certbot_dns_aliyun:dns_aliyun_access_key = 上一步申请的AccessKey
certbot_dns_aliyun:dns_aliyun_access_key_secret = 上一步申请的AccessSecret
EOF

6. 申请证书

1
2
3
4
5
6
7
8
9
bash复制代码docker run -it --rm -v /opt/testdomain:/etc/letsencrypt \
-v /opt/testdomain:/var/log/letsencrypt \
-v /opt/aliyun-dns:/project/conf/aliyun \
aliyun-certbot:v1.0 certonly \
-v \
-a certbot-dns-aliyun:dns-aliyun \
--certbot-dns-aliyun:dns-aliyun-credentials /project/conf/aliyun/credentials.ini \
--register-unsafely-without-email \
-d *.tomcat.test.abc.com

/opt/testdomain证书和日志存放的地方,/opt/aliyun-dns阿里云DNS配置文件存放地方

注意申请的泛域名证书应是通配符后的直接域名,比如你的域名为8100.tomcat.test.abc.com,如果申请的证书为*.abc.com,配置好后,会提示证书有问题,应该申请的证书为:*.tomcat.test.abc.com

6. 配置apache httpd

在/etc/httpd下新建common_conf/ssl_common.conf文件,文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
vbnet复制代码SSLEngine on
SSLProtocol all -SSLv2 -SSLv3

SSLCipherSuite HIGH:3DES:!aNULL:!MD5:!SEED:!IDEA

SSLCertificateFile "/etc/letsencrypt/live/tomcat.test.abc.com/fullchain.pem"

SSLCertificateKeyFile "/etc/letsencrypt/live/tomcat.test.abc.com/privkey.pem"

SSLCertificateChainFile "/etc/letsencrypt/live/tomcat.test.abc.com/fullchain.pem"

# 反向代理配置,根据需求选择
SSLProxyEngine On
ProxyRequests off
ProxyPreserveHost on
<Proxy *>
Order allow,deny
Allow from all
</Proxy>

配置http服务

1
2
3
4
5
6
7
8
9
10
perl复制代码<VirtualHost *:443>
ServerName 您的域名.tomcat.test.abc.com

Include common_conf/ssl_common.conf

ProxyPass / http://localhost:xxxx/
ProxyPassReverse / http://localhost:xxxx/
CustomLog logs/abcsss_log \
"%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b"
</VirtualHost>

重启httpd服务

1
复制代码systemctl restart httpd.service

7. 编写自动续期脚本

1
2
3
4
5
6
7
8
9
10
11
12
bash复制代码#!/bin/bash

docker run -it --rm -v /opt/testdomain:/etc/letsencrypt/live \
-v /opt/testdomain:/var/log/letsencrypt \
-v /opt/aliyun-dns:/project/conf/aliyun \
aliyun-certbot:v1.0 renew \
-v \
-a certbot-dns-aliyun:dns-aliyun \
--certbot-dns-aliyun:dns-aliyun-credentials /project/conf/aliyun/credentials.ini \
--register-unsafely-without-email

echo "SSL续期成功" | mail -s "`date +%Y%m%d`SSL续期" xxxx@qq.com

添加定时任务,每天晚上凌晨1点执行

1
2
3
javascript复制代码crontab -e

0 1 1 * * /opt/certbot/renew.sh > /opt/certbot/renew.log 2>&1

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

写yml,写的腿疼。

发表于 2021-06-18

原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,转载请保留出处。

我非常羞耻的发现,配置文件界,已经被下面三种所统治:yaml,toml和json,这让一直使用properties文件的javaer深深的埋下了头。

不要担心,当你读到文章最后,你也会羞愧的埋下头。

像各种人工智能调参数,k8s调参师,都已经成功升级为yml配置大师。作为一个常年使用yml文件的SpringBoot框架使用者,有时候对yml的表现形式竟然显露出了困惑,这不由得让人羞愧又加了一层。

YAML,竟然是XML的一个子集,所以它的复杂是有源头的,最早诞生于2009年。

使用yml文件,首先遇到的问题,就是它的缩进问题。就如同python语言一样,yml文件的表现层次,是靠嵌套的缩进来完成的。它并不使用TAB,而是使用空格表示缩进。

要命的是,空格的多少,并不重要,只要相同级别元素左侧能够对齐就行。这对于CV党来说,不得不说是一个噩梦哈哈。

那一个配置文件,要解决哪些问题呢?Redis已经做出了回答。就像你学习一门新的语言一样,解决了它的字符串和集合的表示方法,基本上写代码就没问题了。那我们就挨个来看一下。

以下方法以SpringBoot的yml文件格式为准,其他场景的解析器会有些许差异。为了能够debug这些值,我们简单的写了一个测试类,然后再设值完成之后打印以下就可以了。

1
2
3
4
5
6
7
8
9
10
java复制代码@EnableAutoConfiguration
@Configuration
public class TestConfig implements InitializingBean {
@Value("${str1}")
String str1;
@Override
public void afterPropertiesSet() throws Exception {
System.out.println(this);
}
}
  1. 字符串

字符串是最简单的配置,也是最常见的配置。再spring中,字符串可以代引号,也可以不带引号。所以下面三行的配置效果,是一样的。

1
2
3
yaml复制代码str1: ksdfjsdlkfjdsf skdfljs
str1: 'ksdfjsdlkfjdsf skdfljs'
str1: "ksdfjsdlkfjdsf skdfljs"

那么,如何支持多行文本呢?毕竟有些需求,就是这么作死。写法如下:

1
2
3
4
yaml复制代码str1: |
ksdfjsdlkfjdsf skdfljs
ksdfjsdlkfjdsf skdfljs
ksdfjsdlkfjdsf skdfljs

注意,后面不需要有其他的画蛇添足的结束表示,一切都是靠缩进来证明的。当然,你也可以把 |换成>,效果是一样的。

1
2
3
4
yaml复制代码str1: >
ksdfjsdlkfjdsf skdfljs
ksdfjsdlkfjdsf skdfljs
ksdfjsdlkfjdsf skdfljs

要命的是,它还有第三种写法。

1
2
3
yaml复制代码str1: "ksdfjsdlkfjdsf skdfljs
ksdfjsdlkfjdsf skdfljs
ksdfjsdlkfjdsf skdfljs"
  1. 数字

当我们的接收者,是一个数字的时候,比如下面这个。

1
2
java复制代码@Value("${a}")
int a ;

那么,你即使把配置文件写成了字符串,它也会强制转成数字。

1
yaml复制代码a: "014"

此时,a的数值,就会被设置成整数14。

神奇的是,如果你把引号去掉,也就是下面这样。

1
yaml复制代码a: 014

此时,a的数值,竟然变成了12!

我就曾碰到过这样的极品bug,浪费了不少脑细胞,wtf。因为以0开头,代表的是八进制,解析器中间做了一层转换。所以,按照这个逻辑,0x14就是20,使用时一定要注意这一点。机灵的同学可以拿来埋坑哦。

这里也有一些特殊的写法。

1
2
3
4
5
6
7
8
9
yaml复制代码float: 1.23e+3     # 浮点数
fixed: 13.67 # 固定小数
minmin: -.inf # 表示负无穷
notNumber: .NaN # 无效数字

boolean: [true, false] # 布尔值
string: '12345' # 字符串

date: 2021-06-03 # 日期
  1. 字典

再来看一下常见的字典。其实,把所有的配置罗列开来,本身就是一个字典,也就是kv配置。

它是以:进行分割的,所以左半部分要求不能有特殊字符,否则就晕菜了。不不不,它没有晕菜,因为它把乱七八糟的字符,正确的识别了出来。比如下面的yml配置。

1
yaml复制代码a&& xk@71: 0x14

这样的代码接收。

1
2
java复制代码@Value("${a&& xk@71}")
int a ;

嗯,容易被打死的写法。所以,你懂的。

还是我太幼稚了,yml文件根本就没规定key不允许有特殊字符,它允许你这么做。

  1. 对象

由字典,很容易可以扩展到对象。因为对象,也是一堆属性的集合。json已经证明,这些属性,就是一堆KV,我们的yaml也是如此。

假设有如下的代码,我们需要构造dog中的数据。

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Data
public static class Dog{
private String xjjdog1;
private String xjjdog2;
}

@Bean
@ConfigurationProperties(prefix = "dog")
public Dog getDog(){
return new Dog();
}

第一种yml的写法,是这样。

1
2
3
yaml复制代码dog:
xjjdog1: i am xjjdog1
xjjdog2: i am xjjdog1++

而另一种方式,是把json数据直接给写到文件里。

1
yaml复制代码dog: {xjjdog1: 'i am xjjdog1',xjjdog2: 'i am xjjdog++'}

当然,多个层次,可以在一行之中平铺开。比如prefix是super.dog,那么yml文件就可以这么写。

1
yaml复制代码super.dog: {xjjdog1: 'i am xjjdog1',xjjdog2: 'i am xjjdog++'}
  1. 列表支持

列表,就是list,我们可以使用数组接收,也可以使用List等。

它也有两种写法。这是最常见的一种。

1
2
3
4
yaml复制代码animal:
- dog
- cat
- monkey

当然,也可以放在一行。

1
yaml复制代码animal: [dog,cat,monkey]

这没什么问题,关键是yml文件支持嵌套。比如List里嵌套Map,或者Map里嵌套List。当嵌套层次比较深的时候,或者缩进没什么规律的时候,就显得非常的乱。

比如下面这个k8s的pod配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
yaml复制代码apiVersion: v1
kind: Pod
metadata:
name: xjjdog-Pod
labels:
app: front-web
spec:
containers:
- name: front-web
image: nginx
ports:
- containerPort: 80
- name: front-app
image: xjjdog/frontapp
ports:
- containerPort: 14000
storages:
...

比较复杂的是spec,里面有containers、storages等配置。其中containers是一个列表,列表之间是一个map,map中其中的ports属性,又是一个列表…如此嵌套,如果配置文件比较长的化,不熟悉业务属性的同学就会容易晕菜。

  1. 特殊数据

即使是这样,yaml也比xml简单的多。它也有很多特殊的写法。

比如这个。

1
yaml复制代码str1: !!str 2021-06-03

它的意思是,把2021-06-04,强制转化成字符串。这样的强制转化有很多,但大多数时候你不会用。但如果你想要把你的yaml文件变得复杂,让别人不敢动,那就可以这么做。

1
2
3
4
5
6
7
8
9
10
11
yaml复制代码 !!int               # 整数类型
!!float # 浮点类型
!!bool # 布尔类型
!!str # 字符串类型
!!binary # 也是字符串类型
!!timestamp # 日期时间类型
!!null # 空值
!!set # 集合
!!omap, !!pairs # 键值列表或对象列表
!!seq # 序列,也是列表
!!map # 键值表

既然yml文件有这么多复杂的写法,那么我们就可以去玩一把。比如下面的写法。

1
2
yaml复制代码from: &d !!str 2021-06-04
str1: *d

这个配置,和上面的配置,效果是一样的,&的意思是标记,我们给它起了个名字,叫做d;*的意思是引用,我们在需要它的地方引用一把就可以了。

yml中的key,竟然也可以用对象或者复杂的结构作为key。为了标识是一个特殊的key,我们还要做一点处理。

1
yaml复制代码?[blue, reg, green]: Color

上面这个配置的?,就是说,我下面要进行一个比较复杂的配置了,你准备好了么?

  1. End

学会了这些招数的你,是不是跃跃欲试了?想要在你的SpringBoot项目里搞一点有意思的东西?为了让你的基础架构部门无法扫描出你的配置,为什么不呢?

这是我改造的一个普通datasource的配置文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
yaml复制代码h2: &sa !!str sa
driver: &driver !!str org.h2.Driver
defaults: &defaults
?username: *sa
?password:
?driverClassName: *driver
spring:
datasource:
<<: *defaults
?url: !!str >
jdbc:h2:mem:h2test;
DB_CLOSE_DELAY=-1;
DB_CLOSE_ON_EXIT=FALSE

你觉得美么?我反正腿挺疼的。

作者简介:小姐姐味道 (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。我的个人微信xjjdog0,欢迎添加好友,​进一步交流。​

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【SpringCloud系列】- Hystrix选择线程池策

发表于 2021-06-18

Hystrix选择线程池策略实现资源隔离

这是我参与更文挑战的第3天,活动详情查看: 更文挑战

上一篇文章-【SpringCloud系列】- Hystrix隔离策略选择
提到Hystrix实现资源隔离两种策略,接下来主要关注Hystrix采用线程池策略实现资源隔离

资源隔离

把某一个依赖服务的所有调用请求,全部隔离在同一份资源池内,不会去用其它资源

解决核心业务场景:

将多个依赖服务的调用分别隔离到各自的资源池内,避免对某一个依赖服务的调用,因为依赖服务的接口调用的延迟或者失败,导致服务所有的线程资源全部耗费在这个服务的接口调用上,一旦某个服务的线程资源全部耗尽,就可能导致服务崩溃,甚至这种故障不断蔓延。

例如:会员服务同时发起调用量到1000,但是线程池内就10个线程,最多就只会调用10个线程去执行,而不会对会员服务,因为接口调用延时,将tomcat内部所有的线程资源全部耗尽。

如何设定command key、command group、command thread pool的相关值?

1
2
3
4
5
6
7
java复制代码private static final Setter cachedSetter = Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("Gxin"))
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("GxinPool"));
public GxinCommand(String name){
super(cachedSetter);
this.name=name;
}

上一篇文章了解到command key代表了底层的依赖服务的一个接口,command group代表了代表了某一个底层的依赖服务,而ThreadPoolKey代表了一个HystrixThreadPool,用来进行统一监控、统计、缓存。默认的ThreadPoolKey就是command group的名称。每个command 都会跟它的ThreadPoolKey对应的ThreadPool绑定在一起

线程池关键参数:

  • coreSize设置线程池的大小,默认是10
1
java复制代码HystrixThreadPoolProperties.Setter().withCoreSize(int value);
  • queueSizeRejectionThreshold
    线程池中的10个线程都在工作中,没空闲的线程来做任务,此时再有请求过来,会先进入队列积压,如果说队列积压满了,再有请求过来,就直接reject,拒绝请求,执行fallback降级的逻辑,快速返回。

任务投递流程图如下:

截图.png

控制queue满了之后reject的threshold,因为maxQueueSize不允许热修改,提供这个参数可热修改,控制队列的最大大小

1
java复制代码HystrixThreadPoolProperties.Setter().withQueueSizeRejectionThreshold(int value);

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

私有镜像仓库 Harbor 的安装与配置

发表于 2021-06-18

在云原生时代,各种系统服务都以 Docker 容器的方式运行着。镜像仓库,顾名思义就是用来存放 Docker 镜像的地方,它是云原生架构的核心之一。目前,最为流行的私有镜像仓库便是 CNCF 的毕业生之一的 Harbor(中文含义:港口)。

本文将主要介绍如何在 CentOS 7 上安装和配置 Harbor。

安装 Docker 和 Docker Compose

配置阿里的 yum 源,并安装 Docker CE 版本。注:当前最新版本为 19.03.6:

1
2
3
ruby复制代码$ yum install -y yum-utils device-mapper-persistent-data lvm2
$ yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
$ yum install -y docker-ce docker-ce-cli

创建 Docker 守护进程的配置文件,包括使用阿里镜像仓库提高下载速度,并重启 Docker:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
javascript复制代码$ mkdir /etc/docker
$ cat << EOF > /etc/docker/daemon.json
{
"exec-opts": ["native.cgroupdriver=systemd"],
"log-driver": "json-file",
"log-opts": {
"max-size": "100m"
},
"registry-mirrors": ["https://umqjaxg5.mirror.aliyuncs.com"],
"storage-driver": "overlay2",
"storage-opts": [
"overlay2.override_kernel_check=true"
]
}
EOF
$ systemctl daemon-reload
$ systemctl start docker

继续安装 docker-compse 命令:

1
2
shell复制代码$ curl -L "https://github.com/docker/compose/releases/download/1.25.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
$ chmod +x /usr/local/bin/docker-compose

安装 Harbor

下载 Habor 离线安装包并解压:

1
2
ruby复制代码$ wget https://github.com/goharbor/harbor/releases/download/v1.10.1/harbor-offline-installer-v1.10.1.tgz
$ tar -xvf harbor-offline-installer-v1.10.1.tgz -C /opt/

打开 /opt/harbor/harbor.yml 文件,修改 hostname 域名、https 证书等配置信息,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yaml复制代码# Configuration file of Harbor
# The IP address or hostname to access admin UI and registry service.
# DO NOT use localhost or 127.0.0.1, because Harbor needs to be accessed by external clients.
hostname: registry.k8sadm.xinhua.tech
# http related config
http:
# port for http, default is 80. If https enabled, this port will redirect to https port
port: 80
# https related config
https:
# https port for harbor, default is 443
port: 443
# The path of cert and key files for nginx
certificate: /opt/certs/registry.k8sadm.xinhua.tech.cert
private_key: /opt/certs/registry.k8sadm.xinhua.tech.key

接着执行如下命令进行安装:

1
shell复制代码$ ./install.sh --with-clair

以上安装命令同时安装了 Clair 服务,一个用户镜像漏洞静态分析的工具。如果不需要,可以省略该选项。

安装成功后,可以通过 docker login 命令来测试仓库的连通性,看到如下字样即表示安装成功(也可以通过浏览器访问 Web UI):

1
2
3
4
5
makefile复制代码$ docker login registry.k8sadm.xinhua.tech
Username: admin
Password: Passw0rd

Login Succeeded

至此,私有镜像仓库 Harbor 安装完毕。

试一试 Harbor

在 Harbor 中,镜像均以项目的形式组织。我们在页面上创建一个名为 foo 的测试项目,然后将从 Docker Hub 上拉取的 busybox 镜像推送到自己的仓库中:

1
2
3
shell复制代码$ docker pull busybox:1.31.1
$ docker tag busybox:1.31.1 registry.admtest.xinhua.tech/foo/busybox:1.31.1
$ docker push registry.admtest.xinhua.tech/foo/busybox:1.31.1

最后,可以在项目页面查看到刚刚上传的 busybox 镜像:

Harbor Web 页面

其他

如果需要重启 Harbor 服务,需要进入其安装目录,执行如下命令:

1
2
3
shell复制代码$ cd /opt/harbor
$ docker-compse down -v
$ docker-compse up -d

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

玩转Java注解---元注解、内置注解、自定义注解的原理和实

发表于 2021-06-18

这是我参与更文挑战的第18天,活动详情查看: 更文挑战


相关文章

Java注解和反射系列:Java注解和反射系列


前言

Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制。
重点:和 Javadoc 不同,Java 标注可以通过反射获取标注内容。
大话空话不用说太多,简而言之,在编译器生成类文件时,标注 可以被嵌入到字节码中。Java 虚拟机可以保留标注内容,在运行时可以获取到标注内容 。 当然它也支持自定义 Java 标注。
反射+注解,是不是感觉Java变成一种动态语言?哈哈哈!

我觉得注解可以分为三个部分来讲:内置注解,元注解,自定义注解。

一、内置注解

1、@Override 重写

概念:检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。

1
2
3
4
5
6
7
8
java复制代码//这个extends 不要在意,我写上去只是为了更加方便直观的去理解,Object是万物之源,不写也会默认是其子类,不用解释过多吧?
public class Annotation1 extends Object{

@Override
public String toString (){
return "我是重新定义过的toString方法";
}
}

@Override(重写),这个大家应该很熟悉,重写父类的方法。
我们可以看下Object类中toString()是什么样子的。
在这里插入图片描述

那么显而易见,使用了@Override(重写)注解,方法名、方法参数必须得和父类保持一致,否则会报错。
如下图所示:

在这里插入图片描述

如果不加@Override(重写)注解,则正常编译。

在这里插入图片描述

2、@Deprecated 过期警告

概念:标记过时方法。如果使用该方法,会报编译警告。
在开发中,我们经常能遇到这样的情况,如下图:

在这里插入图片描述

在jdk中有大量这样的方法,我就不举例了,自己写一个可能会更加方便理解。

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class Annotation1 extends Object{
public static void main(String[] args) {
testDeprecated.toString1();
}
}
class testDeprecated {
@Deprecated
public static String toString1(){
return "我是重新定义过的toString方法";
}
}

在这里插入图片描述

注意点: 这个不是报错,只是警告,提醒我们这个方法可能会有问题,可能有更好的方法来实现!

3、@SuppressWarnings 忽略警告

概念:指示编译器去忽略注解中声明的警告。

平时开发中,我们会遇到这样的情况,如下图:

在这里插入图片描述

这也不是错误,这是提醒我们,该方法没有使用到,警告提醒的作用。
加上@SuppressWarnings注解后。

1
2
3
4
5
6
7
8
9
java复制代码public class Annotation1 extends Object{
public static void main(String[] args) {

}
@SuppressWarnings("all")
public static void testSuppressWarnings(){
System.out.println("测试+testSuppressWarnings忽略警告!");
}
}

方法成功高亮起来,并且没有警告提示了!

在这里插入图片描述
我们可以点进去看下这个注解为什么需要参数?

在这里插入图片描述
看这里,这个不是方法哦,这是参数。

在注解中的参数格式:calss + 参数名 + ()!
这个需要强行记忆哦,回头我们自定义注解时也需要用到。
换一种写法加深理解!如下图:

在这里插入图片描述

注意点:当注解中只有一个参数时,我们无需加上参数名,注解会自动帮我们匹配的。

具体参数参考点击这里哦

二、元注解

概念:顾名思义,元注解就是给注解使用的注解!

1、@Retention 作用域-(常用)

概念:表示在什么级别保存该注解信息。
在实际开发中,我们一般都写RUNTIME,除非项目有特殊需求!
我们看下@Retention的源码。

在这里插入图片描述

可以看到,需要一个参数,进参数瞅瞅。

在这里插入图片描述

SOURCE: 源代码时有用。

CLASS: class文件中有用,但会被jvm丢弃。

RUNTIME: 运行时有用。

关系:RUNTIME>CLASS>SOURCE

后面我们自定义注解时,每个都需要用该注解!

2、@Documented 作用文档

概念:将此注解包含在 javadoc 中 ,它代表着此注解会被javadoc工具提取成文档。

老规矩看下源码:

在这里插入图片描述

无参的注解,作用域为RetentionPolicy.RUNTIME,运行时有用!这个只是用来作为标记,了解即可,在实际运行后会将该注解写入javadoc中,方便查看。

3、@Target 目标-(常用)

概念: 标记这个注解应该是使用在哪种 Java 成员上面!

在这里插入图片描述

参数源码:

在这里插入图片描述

注意这里时数组格式的参数,证明可以传多个值。

@Target(ElementType.TYPE)——接口、类、枚举、注解

@Target(ElementType.FIELD)——字段、枚举的常量

@Target(ElementType.METHOD)——方法

@Target(ElementType.PARAMETER)——方法参数

@Target(ElementType.CONSTRUCTOR) ——构造函数

@Target(ElementType.LOCAL_VARIABLE)——局部变量

@Target(ElementType.ANNOTATION_TYPE)——注解

@Target(ElementType.PACKAGE)——包

我们来试一下:

在这里插入图片描述

目标不对会报错的哦!我们将其改成方法上!编译即正常通过。

在这里插入图片描述

其他的作用域大家可以去自行尝试,篇幅问题,无法做到每个都去试一遍!

4、@Inherited 继承

概念:标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)。

在这里插入图片描述

这个很简单,就是当@InheritedAnno注解加在某个类A上时,假如类B继承了A,则B也会带上该注解。

4、新注解-(了解即可)

从 Java 7 开始,额外添加了 3 个注解:

@SafeVarargs - Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。

@FunctionalInterface - Java 8 开始支持,标识一个匿名函数或函数式接口。

@Repeatable - Java 8 开始支持,标识某注解可以在同一个声明上使用多次。

三、自定义注解

我们来定义一个属于自己的注解。

1
2
3
4
5
6
7
8
9
java复制代码@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = ElementType.METHOD)
@Inherited
@interface myAnnotation {
String name() default "";
int age() default 18;
String like();
String IDCard() default "";
}

格式:修饰符(pulic)+ @interface +注解名+ {参数等}

可利用default 设置默认值,设定了默认值后使用注解时不传值也不会报错,反之报错!

在这里插入图片描述

我们只需要传没有默认值的参数即可。

如果不传则报错:

在这里插入图片描述

总结

主要就是要注意元注解的使用,因为我们自定义注解时必须得用到!
其实注解主要配合反射来用,在此就不展开来叙述了,下一篇文章讲解反射时,会结合注解详细讲解!

在这里插入图片描述


路漫漫其修远兮,吾必将上下求索~

如果你认为i博主写的不错!写作不易,请点赞、关注、评论给博主一个鼓励吧~hahah

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Apitest 接口自动化测试工具

发表于 2021-06-18

安装

Apitest工具是单可执行文件,不需要安装,放到PATH路径下面就可以直接运行

1
2
3
4
5
6
7
8
9
10
11
12
ruby复制代码# linux
curl -L -o apitest https://github.com/sigoden/apitest/releases/latest/download/apitest-linux
chmod +x apitest
sudo mv apitest /usr/local/bin/

# macos
curl -L -o apitest https://github.com/sigoden/apitest/releases/latest/download/apitest-macos
chmod +x apitest
sudo mv apitest /usr/local/bin/

# npm
npm install -g @sigodenjs/apitest

开始使用

编写测试文件 httpbin.jsona

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
css复制代码{
test1: {
req: {
url: "https://httpbin.org/anything",
query: {
k1: "v1",
},
},
res: {
body: { @partial
args: {
"k1": "v2", // 注意,这儿应该是"v1", 我们故意写"v2"以测试Apitest的反应
},
url: "https://httpbin.org/anything?k1=v1",
}
}
}
}

执行如下命令测试接口

1
复制代码apitest httpbin.jsona

其结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
css复制代码main
test1 (2.554) ✘
main.test1.res.body.args.k1: v2 ≠ v1

{
"req": {
"url": "https://httpbin.org/anything",
"query": {
"k1": "v1"
}
},
"res": {
"headers": {
"date": "Thu, 17 Jun 2021 15:01:51 GMT",
"content-type": "application/json",
"content-length": "400",
"connection": "close",
"server": "gunicorn/19.9.0",
"access-control-allow-origin": "*",
"access-control-allow-credentials": "true"
},
"status": 200,
"body": {
"args": {
"k1": "v1"
},
"data": "",
"files": {},
"form": {},
"headers": {
"Accept": "application/json, text/plain, */*",
"Host": "httpbin.org",
"User-Agent": "axios/0.21.1",
"X-Amzn-Trace-Id": "Root=1-60cb63df-1b8592de3767882a6e865295"
},
"json": null,
"method": "GET",
"origin": "119.123.242.225",
"url": "https://httpbin.org/anything?k1=v1"
}
}
}

Apitest 发现了k1的值异常 main.test1.res.body.args.k1: v2 ≠ v1 并打印错误,同时还打印了接口请求响应详情。

如果我们修改 main.test1.res.body.args.k1 值 v2 => v1 后再执行测试。

1
复制代码apitest httpbin.jsona

其结果如下

1
2
css复制代码main
test1 (1.889) ✔

Apitest 报告测试通过了。

原理

Apitest 执行测试文件时会加载全部测试用例,逐一执行,其执行过程可以描述为:根据 req 部分构造请求发送给服务器,收到响应后依据 res 校验响应数据,然后打印结果。

Apitest 中的用例文件格式是 JSONA。 JSONA是JSON的超集,减轻了一些JSON语法限制(不强制要求双引号,支持注释等),再添加了一个特性:注解。上面例子中的@partial就是注解。

为什么使用JSONA?

接口测试的本质的就是构造并发送req数据,接收并校验res数据。数据即是主体又是核心,而JSON是最可读最通用的数据描述格式。
接口测试还需要某些特定逻辑。比如请求中构造随机数,在响应中只校验给出的部分数据。

JSONA = JSON + Annotation(注解)。JSON负责数据部分,注解负责逻辑部分。完美的贴合接口测试需求。

特性

  • 跨平台
  • DSL
    • 类JSON,没有学习难度
    • 编写简单,阅读容易
    • 不要求编写者会编程
  • 数据即断言
  • 数据可访问
  • 支持Mock
  • 支持Mixin
  • 支持CI
  • 支持TDD
  • 支持用户定义函数
  • 跳过,延时,重试和循环
  • 支持Form,文件上传,GraphQL

示例

下面的示例会用到一些注解,不明白的地方请查看README

全等校验

默认请求下,Apitest 进行全等校验。

  • 简单类型数据(null,boolean,string,number)完全相等
  • object数据属性和属性值完全相等,字段顺序可以不一致
  • array数据元素长度和各元素完全相等,元素顺序也要一致
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
js复制代码{
test1: { @client("echo")
req: {
any: null,
bool: true,
str: "string",
int: 3,
float: 0.3,
obj: {a:3, b:4},
arr: [3,4],
},
res: {
any: null,
bool: true,
str: "string",
int: 3,
float: 0.3,
obj: {a:3, b:4},
// obj: {b:4, b:3}, object类数据字段顺序可以不一致
arr: [3,4],
}
}
}

Apitest 保证:只有当实际接收到的 res 数据与我们用例中描述的 res 数据全等,测试才会通过。

数组校验技巧

Apitest 默认全等校验,而接口返回的array数据可能几十上百条,怎么办?

通常接口数据是结构化的,我们可以只校验数组第一个元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
js复制代码{
test1: { @client("echo")
req: {
arr: [
{name: "v1"},
{name: "v2"},
{name: "v3"},
]
},
res: {
arr: [ @partial
{
name: "", @type
}
],
}
}
}

如果array数据的长度也很关键呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
js复制代码{
test1: { @client("echo")
req: {
arr: [
{name: "v1"},
{name: "v2"},
{name: "v3"},
]
},
res: {
arr: [ @every
[ @partial
{
name: "", @type
}
],
`$.length === 3`, @eval
],
}
}
}

对象校验技巧

Apitest 默认全等校验,而接口返回的object数据的属性很多,我们只关注其中部分属性?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
js复制代码{
test1: { @client("echo")
req: {
obj: {
a: 3,
b: 4,
c: 5,
}
},
res: {
obj: { @partial
b: 4,
}
}
}
}

查询字符串

通过 req.query 传入QueryString

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
js复制代码{
test1: {
req: {
url: "https://httpbin.org/get",
query: {
k1: "v1",
k2: "v2",
}
},
res: {
body: { @partial
url: "https://httpbin.org/get?k1=v1&k2=v2",
}
}
}
}

当然你可以把QueryString直接写在req.url中

1
2
3
4
5
6
7
8
9
10
11
12
css复制代码{
test1: {
req: {
url: "https://httpbin.org/get?k1=v1&k2=v2",
},
res: {
body: { @partial
url: "https://httpbin.org/get?k1=v1&k2=v2",
}
}
}
}

路径变量

通过 req.params 传入路径变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
js复制代码{
test1: {
req: {
url: "https://httpbin.org/anything/{id}",
params: {
id: 3,
}
},
res: {
body: { @partial
url: "https://httpbin.org/anything/3"
}
}
}
}

请求头/响应头

通过 req.headers 传入请求头,通过 res.headers 校验响应头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
js复制代码{
setCookies: { @describe("response with set-cookies header")
req: {
url: "https://httpbin.org/cookies/set",
query: {
k1: "v1",
k2: "v2",
},
},
res: {
status: 302,
headers: { @partial
'set-cookie': [
"k1=v1; Path=/",
"k2=v2; Path=/",
],
},
body: "", @type
}
},
useCookies: { @describe("request with cookie header")
req: {
url: "https://httpbin.org/cookies",
headers: {
Cookie: `setCookies.res.headers["set-cookie"]`, @eval
}
},
res: {
body: { @partial
cookies: {
k1: "v1",
k2: "v2",
}
}
},
},
}

用例数据变量导出与引用

凡是执行过的用例其数据均可以当做已自动导出变量,它们均可以被后续用例引用。

Apitest 中可以使用 @eval 注解引用用例数据。

比如上面例子中setCookies.res.headers["set-cookie"],就是引用前面setCookies用例的set-cookie响应头数据。

表单: x-www-form-urlencoded

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
js复制代码{
test1: { @describe('test form')
req: {
url: "https://httpbin.org/post",
method: "post",
headers: {
'content-type':"application/x-www-form-urlencoded"
},
body: {
v1: "bar1",
v2: "Bar2",
}
},
res: {
status: 200,
body: { @partial
form: {
v1: "bar1",
v2: "Bar2",
}
}
}
},
}

表单: multipart/form-data

结合 @file 注解实现文件上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
js复制代码{
test1: { @describe('test multi-part')
req: {
url: "https://httpbin.org/post",
method: "post",
headers: {
'content-type': "multipart/form-data",
},
body: {
v1: "bar1",
v2: "httpbin.jsona", @file
}
},
res: {
status: 200,
body: { @partial
form: {
v1: "bar1",
v2: "", @type
}
}
}
}
}

GraphQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
js复制代码{
test1: { @describe("test graphql")
req: {
url: "https://api.spacex.land/graphql/",
body: {
query: `\`query {
launchesPast(limit: ${othertest.req.body.count}) {
mission_name
launch_date_local
launch_site {
site_name_long
}
}
}\`` @eval
}
},
res: {
body: {
data: {
launchesPast: [ @partial
{
"mission_name": "", @type
"launch_date_local": "", @type
"launch_site": {
"site_name_long": "", @type
}
}
]
}
}
}
}
}

http(s)代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
js复制代码{
@client({
name: "default",
type: "http",
options: {
proxy: "http://localhost:8080",
}
})
test1: {
req: {
url: "https://httpbin.org/ip",
},
res: {
body: {
origin: "", @type
}
}
}
}

Apitest 支持通过 HTTP_PROXY HTTPS_PROXY 环境变量开全局代理

多个接口服务地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
js复制代码{
@client({
name: "api1",
type: "http",
options: {
baseURL: "http://localhost:3000/api/v1",
}
})
@client({
name: "api2",
type: "http",
options: {
baseURL: "http://localhost:3000/api/v2",
}
})
test1: { @client("api1")
req: {
url: "/signup", // => http://localhost:3000/api/v1/signup
}
},
test2: { @client("api2")
req: {
url: "/signup", // => http://localhost:3000/api/v2/signup
}
}
}

自定义超时

你可以设置客户端超时,影响所有使用该客户端的接口

1
2
3
4
5
6
7
8
9
js复制代码{
@client({
name: "default",
type: "http",
options: {
timeout: 30000,
}
})
}

你也可以为某个用例设置超时

1
2
3
4
5
js复制代码{
test1: { @client({options:{timeout: 30000}})

}
}

环境变量传递数据

1
2
3
4
5
6
7
8
9
js复制代码{
test1: {
req: {
headers: {
"x-key": "env.API_KEY", @eval
}
}
}
}

mock数据

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码{
login1: {
req: {
url: "/signup",
body: {
username: 'username(3)', @mock
password: 'string(12)', @mock
email: `req.username + "@gmail.com"`, @eval
}
}
}
}

Apitest 支持近40个mock函数。下面列些常用的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
js复制代码{
test1: {
req: {
email: 'email', @mock
username: 'username', @mock
integer: 'integer(-5, 5)', @mock
image: 'image("200x100")', @mock
string: 'string("alpha", 5)', @mock
date: 'date', @mock // iso8601格式的当前时间 // 2021-06-03T07:35:55Z
date2: 'date("","2 weeks ago")', @mock // 2周前
sentence: 'sentence', @mock
cnsentence: 'cnsentence', @mock // 中文段落
}
}
}

用例组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
js复制代码{
@describe("这是一个模块")
@client({name:"default",kind:"echo"})
group1: { @group @describe("这是一个组")
test1: { @describe("最内用例")
req: {
}
},
group2: { @group @describe("这是一个嵌套组")
test1: { @describe("嵌套组内的用例")
req: {
}
}
}
}
}

上面的测试文件打印如下

1
2
3
4
5
markdown复制代码这是一个模块
这是一个组
最内用例 ✔
这是一个嵌套组
嵌套组内的用例 ✔

跳过用例(组)

1
2
3
4
5
6
7
8
9
js复制代码{
test1: { @client("echo")
req: {
},
run: {
skip: `othertest.res.status === 200`, @eval
}
}
}

延时执行用例(组)

1
2
3
4
5
6
7
8
9
js复制代码{
test1: { @client("echo")
req: {
},
run: {
delay: 1000, // 延时毫秒
}
}
}

重试用例(组)

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码{
test1: { @client("echo")
req: {
},
run: {
retry: {
stop:'$run.count> 2', @eval // 终止重试条件
delay: 1000, // 重试间隔毫秒
}
},
}
}

重复执行用例(组)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
js复制代码{
test1: { @client("echo")
req: {
v1:'$run.index', @eval
v2:'$run.item', @eval
},
run: {
loop: {
delay: 1000, // 重复执行间隔毫秒
items: [ // 重复执行数据
'a',
'b',
'c',
]
}
},
}
}

如果不在意数据,只想重复执行多少次的话,可以这样设置

1
2
3
4
5
6
7
8
js复制代码{
test1: {
run: {
delay: 1000,
items: `Array(5)`, @eval
}
}
}

强制打印详情

常规模式下,接口如果没有出错是不会打印数据详情的。通过设置run.dump为true强制打印详情数据。

1
2
3
4
5
6
7
8
9
js复制代码{
test1: { @client("echo")
req: {
},
run: {
dump: true,
}
}
}

抽离公用逻辑以复用

首先创建一个文件存储Mixin定义的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
js复制代码// mixin.jsona
{
createPost: { // 抽离路由信息到mixin
req: {
url: '/posts',
method: 'post',
},
},
auth1: { // 抽离鉴权到minxin
req: {
headers: {
authorization: `"Bearer " + test1.res.body.token`, @eval
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
js复制代码@mixin("mixin") // 引入 mixin.jsona 文件

{
createPost1: { @describe("写文章1") @mixin(["createPost", "auth1"])
req: {
body: {
title: "sentence", @mock
}
}
},
createPost2: { @describe("写文章2,带描述") @mixin(["createPost", "auth1"])
req: {
body: {
title: "sentence", @mock
description: "paragraph", @mock
}
}
},
}

越是频繁用到的数据越适合抽离到Mixin。 

自定义函数

某些情况下,Apitest 内置的注解不够用,你可以使用自定义函数。

编写函数lib.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
js复制代码
// 创建随机颜色
exports.makeColor = function () {
const letters = "0123456789ABCDEF";
let color = "#";
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}

// 判断是否是ISO8601(2021-06-02:00:00.000Z)风格的时间字符串
exports.isDate = function (date) {
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(date)
}

使用函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
js复制代码@jslib("lib") // 引入js文件

{
test1: {
req: {
body: {
color: 'makeColor()', @eval // 调用 `makeColor` 函数生成随机颜色
}
},
res: {
body: {
createdAt: 'isDate($)', @eval // $ 表示须校验字段,对应响应数据`res.body.createdAt`

// 当然你可以直接使用regex
updatedAt: `/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test($)`, @eval
}
}
}
}

后记

这里列举了一下Apitest使用示例,详细说明请点击github.com/sigoden/api…查看。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

会员中心小程序的主要模块

发表于 2021-06-18

这是我参与更文挑战的第18天,活动详情查看: 更文挑战

这篇文章我和大家一起来说说一套会员中心小程序,具体会涉及到哪些主要模块方面。

1. 会员模块

线下用户通过小程序完成会员的注册与绑定,成为线上会员。会员绑定步骤不难,功能实现也不难,难的是怎样能引导线下消费者进行会员注册、怎样推广小程序。这里面就涉及到运营者的运营能力,如何举办多种多样的线下运营活动,在活动中引导用户关注成为粉丝和注册成为会员,在线下引流的同时,也要注意降低会员流失率和提高粉丝留存度。这就涉及到我下一个要介绍的模块。

2. 积分模块

要想稳定会员留存度,需要有相应的激励措施,而对于不同会员等级也应该设置不同的积分奖励。积分可以兑换实物,如果小程序规模不大,可以固定几种积分礼品。如果规模比较大,可以考虑接入EC商城,在EC商城里实现积分兑礼。

对于线下有专场活动或服务的品牌方来说,可以利用小程序做专场预约报名的工作,这就对应着我下一个要说的模块了。

3. 活动中心

小程序运营者定期维护服务活动数据,包括线下活动的活动介绍、举办时间、地点等等。会员在小程序中进行活动的预约报名。

这里要深入介绍的一点是关于活动的推广力度和参与热度。运营者在前期活动推广,以及活动期间的互动时,能够借助微信公众号和小程序的消息功能尽可能多的完成人群触达。做法包括但不限于:

  • 针对不同模型人群包,有针对性的发送活动推文,激发粉丝参与线下活动的兴趣;
  • 善于利用小程序订阅消息功能,在合适时间提醒潜在消费者参与活动。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…639640641…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%