2024年7月

一:背景

1. 讲故事

最新版本
1.2402.24001.0
的WinDbg真的让人很兴奋,可以将自己伪装成 GDB 来和远程的 GDBServer 打通来实现对 Linux 上 .NET程序进行调试,这样就可以继续使用熟悉的WinDbg 命令,在这个版本中我觉得 WinDbg 不再是 WinDbg,而是 XDbg 了,画个简图如下:

简图有了,接下来就要付出实践了。

二:实操 Linux 上 .NET调试

1. 测试程序

本想在 CentOS7 上安装 .NET8,不大好装,这里就用一个现存的 .NETCore 3.1 吧,测试代码如下:


    internal class Program
    {
        static void Main(string[] args)
        {
            while (true)
            {
                Console.WriteLine($"{DateTime.Now},tid={Thread.CurrentThread.ManagedThreadId}");
                Thread.Sleep(1000);
            }
        }
    }

代码非常简单,就是1s输出一条记录,接下来编译成x64部署到 Centos7 上。


[root@localhost data]# ls
ConsoleApp7  ConsoleApp7.deps.json  ConsoleApp7.dll  ConsoleApp7.pdb  ConsoleApp7.runtimeconfig.json

2. 安装GDBServer

在 linux 上安装 gdbserver 比较简单,使用 yum 安装即可
yum install gdb-gdbserver
,输出如下:


[root@localhost data]# yum install gdb-gdbserver
Loaded plugins: fastestmirror, langpacks
Loading mirror speeds from cached hostfile
 * base: mirror-hk.koddos.net
 * centos-sclo-rh: ftp.sjtu.edu.cn
 * centos-sclo-sclo: ftp.sjtu.edu.cn
 * epel: mirror.hoster.kz
 * extras: ftp.sjtu.edu.cn
 * updates: mirror-hk.koddos.net
Package gdb-gdbserver-7.6.1-120.el7.x86_64 already installed and latest version
Nothing to do
[root@localhost data]# gdbserver –version
Usage:	gdbserver [OPTIONS] COMM PROG [ARGS ...]
	gdbserver [OPTIONS] --attach COMM PID
	gdbserver [OPTIONS] --multi COMM

COMM may either be a tty device (for serial debugging), or 
HOST:PORT to listen for a TCP connection.

Options:
  --debug               Enable general debugging output.
  --remote-debug        Enable remote protocol debugging output.
  --version             Display version information and exit.
  --wrapper WRAPPER --  Run WRAPPER to start new programs.
  --once                Exit after the first connection has closed.

安装好之后,接下来用 gdbserver 来启动我们的程序,并启动调试端口为 1234,参考如下:


[root@localhost data]# gdbserver 192.168.128.130:1234 dotnet ConsoleApp7.dll
Process dotnet created; pid = 3643
Listening on port 1234

3. 使用 windbg 连接

打开Windbg后,选择
Connect to remote debugger
选项, 在连接字符串中填入
gdb:server=192.168.128.130,port=1234
即可,截图如下:

连接好之后,会有一个初始中断,直接输入g就好了,输出如下:


64-bit machine not using 64-bit API

************* Path validation summary **************
Response                         Time (ms)     Location
Deferred                                       SRV*C:\mysymbols*https://msdl.microsoft.com/download/symbols
Symbol search path is: SRV*C:\mysymbols*https://msdl.microsoft.com/download/symbols
Executable search path is: 
Unknown System Version 0 UP Free x64
System Uptime: not available
Process Uptime: not available
Reloading current modules
ModLoad: 00005555`55554000 00005555`555770cd   /usr/share/dotnet/dotnet
ModLoad: 00007fff`f7bbf000 00007fff`f7dda488   /lib64/libpthread.so.0
ModLoad: 00007fff`f79bb000 00007fff`f7bbe130   /lib64/libdl.so.2
ModLoad: 00007fff`f76b3000 00007fff`f79ba420   /lib64/libstdc++.so.6
ModLoad: 00007fff`f73b1000 00007fff`f76b2138   /lib64/libm.so.6
ModLoad: 00007fff`f719b000 00007fff`f73b0400   /lib64/libgcc_s.so.1
ModLoad: 00007fff`f6dcd000 00007fff`f719a200   /lib64/libc.so.6
ModLoad: 00007fff`f7ddb000 00007fff`f7ffe150   /lib64/ld-linux-x86-64.so.2
ModLoad: 00007fff`f7f72000 00007fff`f7fda288   /usr/share/dotnet/host/fxr/6.0.26/libhostfxr.so
ModLoad: 00007fff`f6b7c000 00007fff`f6dcc3b0   /usr/share/dotnet/shared/Microsoft.NETCore.App/3.1.32/libhostpolicy.so
ModLoad: 00007fff`f63e7000 00007fff`f6b7bac8   /usr/share/dotnet/shared/Microsoft.NETCore.App/3.1.32/libcoreclr.so
ModLoad: 00007fff`f61df000 00007fff`f63e6c38   /lib64/librt.so.1
ModLoad: 00007fff`f57d2000 00007fff`f59dd8c0   /lib64/libnuma.so.1
ModLoad: 00007fff`f3142000 00007fff`f3413dac   /usr/share/dotnet/shared/Microsoft.NETCore.App/3.1.32/libclrjit.so
ModLoad: 00007fff`f2f31000 00007fff`f3141468   /usr/share/dotnet/shared/Microsoft.NETCore.App/3.1.32/System.Native.so
ModLoad: 00007fff`f2d26000 00007fff`f2f30488   /usr/share/dotnet/shared/Microsoft.NETCore.App/3.1.32/System.Globalization.Native.so
ModLoad: 00007fff`f29ad000 00007fff`f2d25fe0   /lib64/libicuuc.so.50
ModLoad: 00007fff`f13da000 00007fff`f29ac030   /lib64/libicudata.so.50
ModLoad: 00007fff`f0fdb000 00007fff`f13d9340   /lib64/libicui18n.so.50
...................
ReadVirtual() failed in GetXStateConfiguration() first read attempt (error == 0.)
Unable to load image /lib64/libpthread.so.0, Win32 error 0n2
*** WARNING: Unable to verify timestamp for /lib64/libpthread.so.0
Unable to load image /usr/share/dotnet/shared/Microsoft.NETCore.App/3.1.32/libcoreclr.so, Win32 error 0n2
*** WARNING: Unable to verify timestamp for /usr/share/dotnet/shared/Microsoft.NETCore.App/3.1.32/libcoreclr.so
libpthread_so!_pthread_cond_timedwait+0x132:
00007fff`f7bcade2 4989c6          mov     r14,rax
0:000> g

有些人可能会好奇,为什么 WinDbg 能伪装成 GDB 来和 GDBServer 来通讯,这其实得益于 WinDbg 是一个宿主,它可以被很多外来的插件无线扩容自己的功能,这和 Linux 的分而治之恰恰相反。。。

接下来可以用
.chain
命令观察插件列表,其中的
GDBServerComposition

ELFBinComposition
让这项功能得到实现。


0:000> .chain
Extension DLL chain:
    GDBServerComposition: image 10.0.27553.1004, API 0.0.0, 
        [path: C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2402.24001.0_x64__8wekyb3d8bbwe\amd64\winext\GDBServerComposition.dll]
    ELFBinComposition: image 10.0.27553.1004, API 0.0.0, 
        [path: C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2402.24001.0_x64__8wekyb3d8bbwe\amd64\winext\ELFBinComposition.dll]
    dbghelp: image 10.0.27553.1004, API 10.0.6, 
        [path: C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2402.24001.0_x64__8wekyb3d8bbwe\amd64\dbghelp.dll]
    uext: image 10.0.27553.1004, API 1.0.0, 
        [path: C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2402.24001.0_x64__8wekyb3d8bbwe\amd64\winext\uext.dll]

接下来就可以做验证了,研究 coreclr 源码,你会发现在 Linux 上 .NET 的 Sleep 函数是借助于底层的
pthread_cond_timedwait
函数,Linux并没有提供类似Windows 的SleepEx这样的系统调用,这就比较坑了,参考如下:


PAL_ERROR CPalSynchronizationManager::ThreadNativeWait(
    ThreadNativeWaitData* ptnwdNativeWaitData,
    DWORD dwTimeout,
    ThreadWakeupReason* ptwrWakeupReason,
    DWORD* pdwSignaledObject)
{
	//...
    while (FALSE == ptnwdNativeWaitData->iPred)
    {
        if (INFINITE == dwTimeout)
        {
            iWaitRet = pthread_cond_wait(&ptnwdNativeWaitData->cond,
                &ptnwdNativeWaitData->mutex);
        }
        else
        {
            iWaitRet = pthread_cond_timedwait(&ptnwdNativeWaitData->cond,
                &ptnwdNativeWaitData->mutex,
                &tsAbsTmo);
        }
    }
	//...
}

不管怎么说,我们用 WinDbg 调试 Linux 的 .NET 程序算是大功告成了。

三:总结

现在的 WinDbg 早已今非昔比,全平台(MacOs,Linux,Windows) 通吃,这也得益于 Windbg 是一个宿主模式的架构体系,给 WinDbg 点赞!
图片名称

从 0 开始搭建一套后台管理系统,成本巨大,所以都会选择一套成熟的组件库,基于此,再堆叠业务逻辑。我们公司的组件库基于
Ant Design
。Ant Design 包含一套完整的后台解决方案,不仅提供了 75 个
组件
,还开源了整套
设计方案
,配色、字体、图标、布局等,还分享了众多的用户体验案例。官方基于 Ant Design 中的组件,还提供了一套开箱即用的中台前端/设计解决方案:
Ant Design Pro
,更贴近于页面研发,缩短开发时间。本文的很多优化思想,都来源于这两套开源库,不过也增加了很多自己的理解。

一、站在巨人的肩膀上

在将系统呈现给用户之前,有必要用专业的眼光来为系统做一次初步的优化。下图是基于上述 2 个库整理的一张思维导图,包含了一个管理后台常用的几个部分,完善这几部分,可以让用户在使用系统时有较好的体验,像图表可视化之类较为特殊的模块并没有列出。

1)基础结构

管理后台的全局结构比较固定,包括侧边顶部的 Logo、侧边菜单、顶部菜单、面包屑导航。

侧边菜单可以隐藏,若要有更好的体验,还可以将宽度变为可自定义,例如左右拖动修改宽度。后台用户的页面权限也会集中到侧边栏,有权限的展示,无权限的隐藏。

在顶部菜单中,不仅仅可以用于导航(修改资料、登出等),还可以增加快捷的检索按钮,例如输入用户 ID,得到一个弹框,展示用户详情,包括基础资料、订单、操作记录、相册、设备等,将这些信息聚合在一起,便于查询。

当一个页面中有比较多的内容时,可以用标签页来分离,一个标签对应一个子页面。

回到顶部,在比较长的页面中比较实用,这个按钮也应该是管理系统不可获取的一个组成部分。

2)反馈

当用户和系统需要交互时,就得为用户在各个阶段提供必要、积极、即时的反馈。但同时要避免过度反馈,以免给用户带来不必要的打扰,对于能够及时看到效果的简单操作,可以省略反馈提示,下面是一组反馈示例。

常见的结果反馈可以分为两种:顶部全局提示和对话框提示(上图所示)。顶部全局提示就是 Toast,比较轻度,过几秒后会自动隐藏。而对话框提示就比较重,会终止用户操作,用于传递非常重要的信息。

表单的操作过程中可通过不同的校验规则给用户提供表单校验提示(上图所示),让他们及时的纠正错误。在表单中,对于比较复杂的字段,可以用气泡卡片的方式提供补充说明(下图所示)。

交互的操作过程中尽可能将状态反馈给用户,即时的响应会给用户增加信赖感。在操作需要较长时间才能完成时,显示该操作的当前进度和状态。若加载时间较长,应提供取消操作。

当目标元素的操作需要用户进一步的确认时,可以通过气泡确认框在目标元素附近弹出浮层提示,以此来询问用户,常见于数据列表操作一列中的按钮。

3)数据列表

数据列表在整个管理系统中占比非常重,其结构也比较固定,最上边是数据过滤(即查询条件),然后是数据统计(可选项),再有是数据列表(即表格区域),其中还包含列表工具栏(新建、刷新、排序等功能),后面是分页栏(图中没有画出),最后是批量操作。

在 Ant Design Pro 中有专门的组件实现
查询条件
,内置查询和重置两个按钮,当条件比较多时,还能自动隐藏。在下图中,就能看到列表工具栏(贴在表格上边),提供的是批量导入、导出数据、创建应用等,这些按钮都可自定义。由于列表的字段较多,还提供了左右滚动,并且第一列和最后一列固定。批量操作是固定在页面底部的,并且会显示选中的数量。

在列表的加载过程中,可以提供骨架屏过渡,加载完成后,显示数据。当没有数据时,要提供空状态占位,而不是空白一片。

数据加载完成后,在悬停时,可以为当前那一行增加底色辨识。

文字链的点击范围受到文字长短影响,可以设置整个单元格为热区,以便用户触发。

分页器可以让用户清楚的知道自己所要浏览的内容有多少、已经浏览了多少、还剩余多少。当信息条目较多的时候,可以允许用户自定义每页的行数,以提高用户查看和检索信息的效率和灵活性,常与表格、卡片搭配使用。下图展示了分页器的各个部分,以及常用的功能。

上述是比较常见的数据列表,还有几类比较特殊的数据列表。第一种是行内可编辑的表格,点击操作列的编辑后,只读的列变为可操作的控件,比较快捷的编辑方式,适合字段较少的列表。图中最下面的添加一行数据,是一种的特殊的复制按钮。

第二种是可拖动排序的表格,将鼠标左键按住第一行的 icon,然后就能拖动这一行了。

第三种是卡片列表,展现形式与常规列表较为不同。在卡片中,可以根据业务侧重元素,例如提供放大图像的功能。

4)表单

表单也是后台系统中不可分割的一部分,现在很多时候不是单独的页面,而是数据列表的一部分。例如点击列表中的某个字段出现浮层表单,浮层的形式可分两种:弹框(Modal)和抽屉(Drawer),弹框适合字段较少的表单,如下所示。

由于抽屉提供的空间比较多,因此能包含更多的字段,如下所示。

除了浮层表单之外,对于较为复杂的表单,可以使用分步表单。将用户需要填写和确认的信息按照线性流程组织,利用步骤条告知用户完整的流程和进度,常常在最后提交前让用户再次确认信息,并在流程结束后给与明确的结果反馈。

登录表单是一种比较特殊的表单,独立于系统的基础布局结构。一般都会简单点,只提供用户名和密码登录,省略短信登录。

常用的表单控件包括输入框、单选框、多选框、下拉框等,这类都比较简单,复杂的有上传、数据联动和复制等。文件上传可以用比较简单的展现形式,如下所示。

而图像上传因为需要预览,所以得提供一块预览区域,常见的就是将按钮区域放大。

还有一种拖动上传,为了更好的体验,在上传的过程中,还需要实时更新进度条。

5)按钮

按钮虽然是一个小元素,但是它遍布整个系统,哪都有它。对于按钮,目前也有了一套成熟的设计规范,主要体现在样式和交互两方面。在下图中,1 是次按钮,2 是主按钮,3 是文字按钮,4 是图标按钮,5 是在按钮中添加图标。

主按钮是主色填充,表示高强调,其余按钮的强调级别依次降低。

红色用于警示用户该操作存在风险,通常删除按钮会被赋予红色。

在移动到按钮位置时,需要有个聚焦效果,例如高亮。在点击按钮时,需要有个过渡效果,例如增加阴影,确认点击成功。在通信时,可以有个加载效果。

6)其它

响应式是为了让后台可以支持移动设备的访问,在没有电脑的户外情况下,也能浏览后台,操作管理。

暗黑模式是一种体验优化,比较适合夜晚办公,也比较符合程序员的审美。在公司内部上线后,很快得到了业务方的肯定,还督促产品完善客户端的夜间模式。

异常页面也是后台系统不可缺少的一部分,权限不足提示 403,页面不存在提示 404,服务器错误提示 500 等。通过异常页,可以解释发生了什么异常,为用户提供相应的建议或操作,避免用户感到迷失和困惑。

二、紧贴业务

虽然开源库已经扫清了许多的体验障碍,但是在实际业务中,还是会出现各类问题,集中体现在流程、功能、性能等方面。例如流程过于繁琐,需要简化;功能太少,需要补充;页面太卡,难以操作等。不过在优化体验之前,需要保证业务进度不受影响,换句话说,就是要有空闲的人力资源去做体验优化这个工作。

1)解放生产力

解放生产力就是为了让更多的人能参与到优化的工作中。我们团队之前在开发管理后台时,会先找一张类似的,然后复制一份,修改文件名称,在此基础上做改动。既然能复制,那就说明有很多共通之处,在整理 200 多张页面后发现,几种常规的布局大概占总页面数的 80% 以上,只有很小一部分的页面需要专门定制,那么接下来就是抽象出常规布局中所包含的组件。

模板组件呼之欲出,经过一周多时间的调试,在组内开始推广。在开发这套组件的时候,预留了许多回调,可根据不同场景做自定义的逻辑。在模板组件上线后,就将页面的开发从3天降低至1天以内,有些简单页面两三个小时就能布局完成。后续在浏览 Ant Design Pro 的组件库时,发现我的模板组件与这些组件类似,但是功能更为的丰富完善,能够适应更多复杂的场景。

这是一次非常典型的降本提效的经历,除此之外,我们还在研发各类工具,让各种业务走可视化的配置,不用再单独研发。例如一个将榜单活动配置化的工具,就是将常用的活动做成可视化配置的形式,目的是减少开发和测试人力,将 2 天的研发时间压缩至 2 小时。这个配置协调了 UI 组、产品组、测试组、前端组、数据组一起,制订出了相关规范,已成功运营了几十场活动。

给我们组自己减负,也是一个目标,那么也需要开发许多趁手的工具。例如设计BFF 平台,在研发完成后的一段时间才开始推广,我们组比较慢热,有段时间,新的业务接口基本都会走此平台,线上已有 70 多个接口在稳定运行着。

为了提升管理后台的开发效率,先后研发了后台页面可视化编辑器(即低代码)第一版和第二版,第一版组员接受度并不理想,第二版已经上线了 2 个菜单。不过,在使用体验上并不理想,后续还需要迭代完善,才能真正使用。
当然,解放生产力和体验优化大部分情况下是并行进行的,在进行一段时间后,获取的收益将会非常高。

2)发现问题

接下来谈谈发现问题,像流程、功能、性能等问题都是在深度使用后台系统后,才能遇到,因此想要发现这类问题,需要与相关人员交流。有条件的话,可以进行一对一的交流,提前通知后,让他们准备下。因为是给他们解决实际问题,所以一般情况下,他们都会积极配合。当时一下子收到了几十个问题反馈,经过整理后,修复比较容易实现的问题,诸如换个字体颜色、加个筛选条件、增加一列等。果断上线后,效果非常好,对你来说是一个小改动,但对业务方来说,却是实打实的提升工作幸福感。

另外一个发现问题的方法是发放满意度问卷,相比上一个方法,受众面更广,成本更低。毕竟一对一是要抽出双方时间的,不能频繁使用。但相对来说,一对一耳闻目染的效果肯定是最好的。如果收到的是高质量的问卷,那么收获也是满满的。

问卷的题目形式包括单选和问答,不要包含太多的专业术语,通俗描述,答案也不一定要用标准的五级量表(非常满意、满意、一般、不满意、非常不满意),可以更具象地去描述答案,方便用户选择。例如:

  • 对XX页面不太满意的原因是什么?
    • 开发人员误解了需求
    • 项目延期
    • BUG太多不能用
    • 其他

当选择其他时,可以提供文本框,输入具体原因。

除了在这类正式场景中获取问题之外,在日常时刻,其实也可以发掘,例如听到有人在抱怨 XX 功能太难用时,就可以去搭话了、某人在群聊中提到某功能有异常时等等。

有些体验问题也可以自己去发掘,例如某个数据列表中的字段由于太多,导致位置变形了,那么你可以主动去增加左右滚动条。筛选条件太少,就增加几个。日常也可以去观察其他开源管理系统的功能,考虑是否可以增加到自己的系统中,提升体验,响应式和暗黑模式就是在浏览其他系统时得到的灵感。

3)解决问题

解决问题的核心本质就是资源成本是否能负担的起。某些比较重要的体验问题,尤其是需要多端协调的,可以将其加到版本迭代中,这样既能引起重视,也能安排一个固定时间分出资源来解决。

不过大部分时候,体验优化上不了台面,提不上日程,优先级比其他需求要低很多。但好在管理后台大部分是由我们全栈管理着,优化工作可以由我们自己承担,不需要找人协作,可控性比较大。可以将那些优化见缝插针,逐个击破。

有些优化难以彻底解决,只能在已有条件下做到最好。例如后台有个审核内容的功能,可以一次性提交 200 条记录,在提交后,服务端会串行的做一系列的逻辑处理,那么不可避免的就会让接口响应会变慢。虽然在接口中,已经做了数据库表的索引,以及尽可能的与数据库少交互,包括一次性从数据库中读取多条记录,将数据缓存到 Redis 中等,但是再要优化,就得改变技术架构,成本会比较大,只能与业务方协商,暂时互相妥协。一般来说,随着业务的迭代,架构也是要跟着调整的,但受限于各种客观条件,有时候不得不搁置。

各个公司的业务都会有所不同,遇到的业务优化问题也会大相径庭,并没有银弹,得根据实际情况分析,在现有资源的前提下,找到最优解(占用的资源最少,得到的效果大家都能认可),这是一个平衡的过程,需要多多尝试。

一、介绍

在实际的软件系统开发过程中,由于业务的需求,在代码层面实现数据的脱敏还是远远不够的,往往还需要在数据库层面针对某些关键性的敏感信息,例如:身份证号、银行卡号、手机号、工资等信息进行加密存储,实现真正意义的数据混淆脱敏,以满足信息安全的需要。

那在实际的业务开发过程中,我们如何快速实现呢?

今天通过这篇文章,我们一起来了解一下如何在 Spring Boot 中快速实现数据的加解密功能。废话不多说了,直接撸代码!

二、方案实践

在 Spring Boot 生态中,有一个非常厉害的开源框架:Apache ShardingSphere。

它是一款分布式 SQL 事务和查询引擎,可通过数据分片、弹性伸缩、加密等能力对任意数据库进行增强。我们可以利用它的数据脱敏模块,快速实现 SQL 字段的加解密操作。

如果当前项目是采用 Spring Boot 开发的,可以实现无缝集成,对原系统的改造会非常少。

下面以用户表为例,一起了解一下
ShardingSphere
的数据加解密的实现过程!

2.1、创建用户表

首先,在数据库中创建一张用户表,示例脚本如下!

CREATE TABLE user (
  id bigint(20) NOT NULL COMMENT '用户ID',
  email varchar(255)  NOT NULL DEFAULT '' COMMENT '邮件',
  nick_name varchar(255)  DEFAULT NULL COMMENT '昵称',
  pass_word varchar(255)  NOT NULL DEFAULT '' COMMENT '二次密码',
  reg_time varchar(255)  NOT NULL DEFAULT '' COMMENT '注册时间',
  user_name varchar(255)  NOT NULL DEFAULT '' COMMENT '用户名',
  salary varchar(255) DEFAULT NULL COMMENT '基本工资',
  PRIMARY KEY (id) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

2.2、创建 springboot 项目并添加依赖包

接着,创建一个 Spring Boot 项目,并添加相关的依赖包,示例如下:

<dependencies>
    <!--spring boot核心-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <!--spring boot 测试-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <!--springmvc web-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--mysql 数据源-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!--mybatis 支持-->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.0.0</version>
    </dependency> 
    <!--shardingsphere数据分片、脱敏工具-->
    <dependency>
        <groupId>org.apache.shardingsphere</groupId>
        <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
        <version>4.1.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shardingsphere</groupId>
        <artifactId>sharding-jdbc-spring-namespace</artifactId>
        <version>4.1.0</version>
    </dependency>
</dependencies>

2.3、添加相关配置


application.properties
文件中,添加
shardingsphere
相关配置,即可实现针对某个表进行脱敏

server.port=8080

logging.path=log

#shardingsphere数据源集成
spring.shardingsphere.datasource.name=ds
spring.shardingsphere.datasource.ds.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.ds.jdbc-url=jdbc:mysql://127.0.0.1:3306/test
spring.shardingsphere.datasource.ds.username=xxxx
spring.shardingsphere.datasource.ds.password=xxxx

#加密方式、密钥配置
spring.shardingsphere.encrypt.encryptors.encryptor_aes.type=aes
spring.shardingsphere.encrypt.encryptors.encryptor_aes.props.aes.key.value=hkiqAXU6Ur5fixGHaO4Lb2V2ggausYwW
#plainColumn表示明文列,cipherColumn表示脱敏列
spring.shardingsphere.encrypt.tables.user.columns.salary.plainColumn=
spring.shardingsphere.encrypt.tables.user.columns.salary.cipherColumn=salary
#spring.shardingsphere.encrypt.tables.user.columns.pass_word.assistedQueryColumn=
spring.shardingsphere.encrypt.tables.user.columns.salary.encryptor=encryptor_aes

#sql打印
spring.shardingsphere.props.sql.show=true
spring.shardingsphere.props.query.with.cipher.column=true


#基于xml方法的配置
mybatis.mapper-locations=classpath:mapper/*.xml

其中有几个的配置信息比较重要,
spring.shardingsphere.encrypt.tables
是指要脱敏的表,
user
是表名,
salary
表示
user
表中的真实列,其中
plainColumn
指的是明文列,
cipherColumn
指的是脱敏列,如果是新工程,只需要配置脱敏列即可!

配置示例如下!

# 用于告诉 ShardingSphere 数据表里哪个列用于存储明文数据
spring.shardingsphere.encrypt.tables.user.columns.salary.plainColumn=
# 用于告诉 ShardingSphere 数据表里哪个列用于存储密文数据
spring.shardingsphere.encrypt.tables.user.columns.salary.cipherColumn=salary
# 用于告诉 ShardingSphere 数据表里哪个列用于存储辅助查询数据
#spring.shardingsphere.encrypt.tables.user.columns.salary.assistedQueryColumn=
# 用于告诉 ShardingSphere 数据表里哪个列使用什么算法加解密
spring.shardingsphere.encrypt.tables.user.columns.salary.encryptor=encryptor_aes

2.4、编写数据持久层

然后,编写一个数据持久层,用于数据的存储和查询操作。

<mapper namespace="com.example.shardingsphere.mapper.UserMapperXml" >

    <resultMap id="BaseResultMap" type="com.example.shardingsphere.entity.UserEntity" >
        <id column="id" property="id" jdbcType="BIGINT" />
        <result column="email" property="email" jdbcType="VARCHAR" />
        <result column="nick_name" property="nickName" jdbcType="VARCHAR" />
        <result column="pass_word" property="passWord" jdbcType="VARCHAR" />
        <result column="reg_time" property="regTime" jdbcType="VARCHAR" />
        <result column="user_name" property="userName" jdbcType="VARCHAR" />
        <result column="salary" property="salary" jdbcType="VARCHAR" />
    </resultMap>

    <select id="findAll" resultMap="BaseResultMap">
        SELECT * FROM user
    </select>
    
    <insert id="insert" parameterType="com.example.shardingsphere.entity.UserEntity">
        INSERT INTO user(id,email,nick_name,pass_word,reg_time,user_name, salary)
        VALUES(#{id},#{email},#{nickName},#{passWord},#{regTime},#{userName}, #{salary})
    </insert>
</mapper>
public interface UserMapperXml {


    /**
     * 查询所有的信息
     * @return
     */
    List<UserEntity> findAll();

    /**
     * 新增数据
     * @param user
     */
    void insert(UserEntity user);
}
public class UserEntity {

    private Long id;

    private String email;

    private String nickName;

    private String passWord;

    private String regTime;

    private String userName;

    private String salary;

    //省略set、get...

}

2.5、单元测试

最后,我们编写一个单元测试,验证一下代码的正确性。

编写启用服务程序

@SpringBootApplication
@MapperScan("com.example.shardingsphere.mapper")
public class ShardingSphereApplication {

    public static void main(String[] args) {
        SpringApplication.run(ShardingSphereApplication.class, args);
    }
}

编写单元测试

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = ShardingSphereApplication.class)
public class UserTest {

    @Autowired
    private UserMapperXml userMapperXml;

    @Test
    public void insert() throws Exception {
        UserEntity entity = new UserEntity();
        entity.setId(3l);
        entity.setEmail("123@123.com");
        entity.setNickName("阿三");
        entity.setPassWord("123");
        entity.setRegTime("2021-10-10 00:00:00");
        entity.setUserName("张三");
        entity.setSalary("2500");
        userMapperXml.insert(entity);
    }

    @Test
    public void query() throws Exception {
        List<UserEntity> dataList = userMapperXml.findAll();
        System.out.println(JSON.toJSONString(dataList));
    }
}

插入数据后,如下图,数据库存储的数据已被加密!

我们继续来看看,运行查询服务,结果如下图,数据被成功解密!

采用配置方式,最大的好处就是直接通过配置脱敏列就可以完成对某些数据表字段的脱敏,非常方便。

三、小结

当需要对某些数据表字段进行脱敏处理的时候,可以采用 Apache ShardingSphere 框架快速实现。

但是有个细节很容易遗漏,那就是字段类型,例如
salary
字段,根据常规,很容易想到使用数字类型,但是却不是,要知道加密之后的数据都是一串乱码,数字类型肯定是无法存储字符串的,因此在定义的时候,这个要留心一下。

希望以上的案例,能帮助到大家!

示例代码:
spring-boot-example-shardingsphere

热点随笔:

·
求救信:救下园子,保住这块开发者的天地
(
博客园团队
)
·
给博客园的寄语
(
☆№忧忧★♂
)
·
36岁,大龄剩男,2024上半年总结......
(
久曲健
)
·
我不应该用JWT的!
(
sum墨
)
·
线程池遇到父子任务,有大坑,要注意!
(
why技术
)
·
架构与思维:微服务架构的思想本质
(
Hello-Brand
)
·
.NET科普:.NET简史、.NET Standard以及C#和.NET Framework之间的关系
(
dax.net
)
·
将传统应用带入浏览器的开源先锋「GitHub 热点速览」
(
削微寒
)
·
C# 使用模式匹配的好处,因为好用所以推荐~
(
万雅虎
)
·
使用ML.NET训练一个属于自己的图像分类模型,对图像进行分类就这么简单!
(
追逐时光者
)
·
拯救SQL Server数据库事务日志文件损坏的终极大招
(
桦仔
)
·
Microsoft宣布将在开发人员会议上专注于.NET Aspire
(
张善友
)

热点新闻:

·
VIP不够SVIP来凑,会员“套娃”套路有多深?
·
专为Excel而生的大模型来了!
·
雷军万字回忆录:为了造车,我学会了「漂移」
·
“鸿蒙 PC 版”现身华为开发者官网
·
“6元5公里”只是熟悉的推广配方?无人驾驶出租车没那么便宜
·
我卖货到俄罗斯,被拖款四个月,情况变了
·
传娃哈哈严重内斗、宗馥莉动了核心利益被踢出局,此前已博弈一个多月
·
马斯克自曝猛料:曾被两次暗杀!
·
周鸿祎总能踩中时代的节点
·
老人被中国移动多收 8 年来显费引热议,每月 6 元累计费用不可退
·
小米 Buds 5 半入耳式耳机官宣:支持无损音质,7 月 19 日晚发布
·
让萝卜快跑再跑一会儿

论文提出了一种可扩展的多数据集目标检测器(
ScaleDet
),可通过增加训练数据集来扩大其跨数据集的泛化能力。与现有的主要依靠手动重新标记或复杂的优化来统一跨数据集标签的多数据集学习器不同,论文引入简单且可扩展的公式来为多数据集训练产生语义统一的标签空间,通过视觉文本对齐进行训练,能够学习跨数据集的标签语义相似性来进行标签分配。经过训练后,
ScaleDet
可以很好地泛化任意具有可见和不可见类的上游和下游数据集

来源:晓飞的算法工程笔记 公众号

论文: Training data-efficient image transformers & distillation through attention

Introduction


计算机视觉的重大进步是由大规模数据集推动的,大规模数据集对于训练具有良好泛化能力的识别模型至关重要。但收集大量带标注的数据集既费钱又费时,为了在没有额外标注成本的情况下利用更多训练数据,最近的研究集中于统一多个数据集。从更多视觉类别和更多样化的视觉领域中学习,然后进行检测和分割。

要跨多个数据集训练目标检测器,需要应对几个挑战:

  • 多数据集训练需要统一跨数据集的异构标签空间,来自两个数据集的标签可能指代相同或相似的对象。
  • 数据集之间的训练设置可能不一致,不同大小的数据集通常需要不同的数据采样策略和学习计划。
  • 多数据集模型应该比单数据集模型表现更好,但异构的标签空间、数据集之间的域差异以及对较大数据集的过拟合风险使得这一目标的实现更难。

为了解决上述挑战,现有研究大多手动重新标记类或训练多个特定于数据集的分类器。但这些方法缺乏可扩展性,随着数据集的增加,手动重新标记工作量和训练多个分类器的复杂性迅速增加。

与上述研究不同,
ScaleDet
是可扩展的多数据集目标检测器,主要有两个创新点:

  • 可扩展的公式统一多个标签空间。
  • 新颖的损失公式学习跨数据集的硬标签和软标签分配:硬标签用于消除类标签的歧义,而软标签作为正则化器关联相似类标签。

总体而言,论文的贡献如下:

  • 论文提出了一种用于目标检测的新型可扩展多数据集训练方法,利用文本编码根据语义相似性来统一和关联跨数据集的标签,通过视觉文本对齐训练单个分类器来学习硬标签分配和软标签分配。
  • 论文通过大量实验证明
    ScaleDet
    在多数据集训练中具有令人信服的可扩展性、通用性以及性能。
  • 论文评估了
    ScaleDet
    在具有挑战性的
    Object Detection in the Wild
    基准上的可转移性,证明其在下游数据集上具有不错的泛化能力。

ScaleDet: A Scalable Multi-Dataset Detector


ScaleDet
通过统一不同的标签集以形成统一的标签语义空间(图2顶部)进行跨数据集学习,并通过硬标签和软标签分配实现视觉文本对齐来进行训练(图2底部)。

Preliminaries and problem formulation

  • Standard object detection

典型的对象检测器旨在预测对象的
\(b_{i}\in{\mathbf{R}}^{4}\)
边界位置以及在给定
\(n\)
个类中的类标签
\(c_i \in \mathbb{R}^n\)
。给定图像
\(I\)
,检测器的图像编码器(例如 CNN 或 Transformer)提取框特征和视觉特征,将其送到边界框回归器
\(B\)
和视觉分类器
\(C\)
进行预测。检测器通过最小化边界框损失
\(\mathcal{L}_{b b o x}\)
和分类损失
\(\mathcal{L}_{cls}\)
来学习边界框的预测以及框特征和视觉特征对应的类标签,即,

\[\mathcal{L}_{D e t}=\mathcal{L}_{b b o.}+\mathcal{L}_{c l s}
\]

现有的目标检测器通常采用一级或二级框架,其中可能包含额外损失项。单级检测器使用回归损失来回归对象位置的属性,如中心性,两阶段检测器则改为使用包含专用损失函数的
RPN
网络来预测每个框是目标的概率。

在这项工作中,论文专注于重新制定分类损失
\(\mathcal{L}_{cls}\)
,在两级检测器之上解决多数据集训练问题。

  • Multi-dataset object detection

给定一组
\(K\)
数据集
\(\{D_1, D_2, \dots, D_K\}\)
及标签空间
\(\{L_{1},L_{2},\dots,L_{K}\}.\)
,论文的目标是训练一个可扩展的多数据集检测器,该检测器可以很好地泛化上游和下游检测数据集。

之前的多数据集学习器手动将跨数据集的相似标签关联或合并到联合标签,而论文提出了一个简单但可扩展的公式来进行标签统一,无需手动合并任何标签。

Scalable unification of multi-dataset label space

如图2上部分所示,每次训练都从多个训练集中随机抽取一小批图像一起提取视觉特征
\(\{v_1, v_2, \ldots, v_j\}\)
,其中
\(v_{i}\in{\mathbf{R}}^{D}\)

\(D\)
维向量。每个视觉特征 $ v_{i}$ 通过标签分配与一组文本编码
\(\{t_{1},t_{2},\ldots,t_{n}\}\)
进行匹配。

  • Define labels with text prompts

论文用扩展的文本提示来表示每个类标签
\(l_{i}\)
,例如,标签

可以用文本提示
一个人的照片
来表示。论文从预训练的
CLIP

OpenCLIP
的文本编码器中提取提示文本的编码
\(t_{i}\)
,然后将所有文本编码进行均值操作。

  • Unify label spaces by concatenation

给定来自所有数据集的类标签的文本编码,多数据集训练的一个关键问题是统一不相同的标签空间
\(\{L_{1},L_{2},\ldots,L_{K}\}\)
,这可以通过将相似的标签关联并合并来解决。然而,如果没有仔细的人工检查,标签定义的模糊性会导致模型训练中传播错误的风险。因此,论文不进行跨数据集的标签合并,而是先直接通过并集来统一不同的标签空间:

\[L=L_{1}\coprod\dots\coprod L_{K}=\{l_{1,1},l_{1,2},\dots,l_{K,1},l_{K,2},\dots\}
\]

其中
\(\coprod\)
表示并集,
\(l_{k,i}\)
是来自数据集
\(k\)
的标签
\(i\)
。除了简单之外,这个统一语义标签空间
\(L\)
最大限度地保留了所有标签的语义,从而为训练提供了更丰富的词汇。

  • Relate labels by semantic similarities

当使用文本编码来表示类标签时,可以在统一标签空间中关联相似语义的标签。为了展示跨数据集的标签关系,论文基于提示文本编码来计算语义相似性。对于给定的类标签
\(l_{i}\)
,用余弦相似性计算与所有标签的语义相似性,并在
0

1
之间归一化:

\[\begin{array}{l}
{{\operatorname*{sim}(l_{i},l_{j})=\displaystyle\frac{\cos(t_{i},t_{j})-\alpha_{i}}{\beta_{i}-\alpha_{i}},}}
\\
{{ \alpha_{i}=\operatorname*{min}\{\cos(t_{i},t_{j})\}_{j=1}^{n},}}
\\
{{\beta_{i}=\operatorname*{min}\{\cos(t_{i},t_{j})\}_{j=1}^{n}=\cos(t_{i},t_{i})=1,}}
\end{array}
\]

其中
\(\operatorname*{sim}(l_{i},l_{j})\)
是两个标签
\(l_{i},l_{j}\)
的文本编码
\(t_{i},t_{j}\)
之间的语义相似度。

编码所有类标签之间的标签关系,得到标签语义相似度矩阵
\(S\)

\[S=\left[\begin{array}{r c r}{{1}}&{{\cdot\cdot\cdot}}&{{\operatorname{sim}(l_{1},l_{n})}}\\ {{\vdots}}&{{\ddots}}&{{\vdots}}\\ {{\operatorname{sim}\left(l_{n},l_{1}\right)}}&{{\cdot\cdot\cdot}}&{{1}}\end{array}\right]=\left[\begin{array}{c}{{{\bf s}_{1}}}\\ {{\vdots}}\\ {{\bf{\bf s}_{n}}}\end{array}\right],
\]

其中
\(S\)
是一个
\(n \times n\)
矩阵,每个行向量
\(\mathbf{S}_{i}\)
编码标签
\(l_{i}\)
相对于所有
\(n\)
类标签的语义关系。

有了这些标签语义相似性,论文可以引入显式约束,使检测器能够在具有编码标签语义相似性的统一语义标签空间上学习。重要的是,相似性和标签空间都是离线计算的,这不会为训练和推理增加任何计算成本,在扩大训练数据集的数量时也不需要重新制定模型。

Training with visual-language alignment

为了在统一语义标签空间
\(\{l_1, l_2,\ldots, l_n\}\)
上进行训练,论文通过硬标签和软标签分配将视觉特征与文本编码
\(\left\{t_{1},t_{2},\ldots,t_{n}\right\}\)
对齐。

  • Visual-language similarities

给定对象区域提案的视觉特征
\(v_{i}\)
,论文首先计算
\(v_{i}\)
和所有文本编码
\(\{t_{1},t_{2},\ldots,t_{n}\}\)
之间的余弦相似度:

\[\mathbf{c}_{i}=[\mathrm{cos}(v_{i},t_{1}),\mathrm{cos}(v_{i},t_{2}),\ldots,\mathrm{cos}(v_{i},t_{n})]
\]

有了这些相似度分数,论文可以根据以下损失项将视觉特征
\(v_i\)
与的文本编码对齐。

  • Hard label assignment

每个视觉特征
\(v_{i}\)
都有其真实标签
\(l_{i}\)
,因此可以通过硬标签分配与的文本编码
\(t_{i}\)
匹配:

\[\mathcal{L}_{h l}=\mathrm{BCE}\big(\sigma_{s g}\big(\mathbf{c}_{i}\big/\tau\big),{l}_{i}\big),
\]

其中
\(\mathrm{BCE}(\cdot)\)
是二元交叉熵损失,
\(\sigma_{s g}{\big(}\cdot)\)
是 sigmoid 激活函数,
\(\tau\)
是温度超参数。

上述公式虽然确保视觉特征
\(v_{i}\)
与文本嵌入
\(t_{i}\)
对齐,但没有明确地学习跨数据集的标签关系。因此,论文引入软标签分配来学习语义标签关系。

  • Soft label assignment

论文通过语义相似度分数将单个标签与所有标签关联,同样地,视觉特征也可以通过使用语义相似度分数与所有文本编码关联。为此,论文在视觉特征
\(v_{i}\)
上引入了软标签分配:

\[\mathcal{L}_{s l}=\mathrm{MSE}(\mathbf{c}_{i},\mathbf{s}_{i})
\]

其中,
\(\mathrm{MSE}(\cdot)\)
是均方误差,
\(\mathbf{s}_{i}\)
表示标签
\(l_{i}\)
和所有
\(n\)
类标签之间的语义相似性(标签语义相似性矩阵
\(S\)
的第
\(i\)
行)。

  • Remark

硬标签分配可以在概率空间消除不同类别标签的歧义,而软标签分配则可以在语义相似性空间中将每个视觉特征以不同的语义相似度分配给不同的文本编码,充当正则化器来关联跨数据集的相似类标签。

  • Training with semantic label supervision

基于硬标签和软标签分配,论文通过将视觉特征与统一语义标签空间中的文本编码对齐来对不同区域提议进行分类,从而训练检测器。即将原来检测器中的分类损失
\(\mathcal{L}_{c l,s}\)
被替换为:

\[\mathcal{L}_{l a n g}=\mathcal{L}_{h l}+\lambda\mathcal{L}_{s l}
\]

其中
\(\lambda\)
是平衡超参数。由于上述损失使用语言监督将图像映射到文本,可以实现对不可见标签的零样本检测。

  • Overall objective

论文不改变原检测器中的检测损失
\(\mathcal{L}_{b b o x}\)
,训练
ScaleDet
的总体目标是:

\[\mathcal{L}_{S c a l e D e t}=\mathcal{L}_{b b o x}+\mathcal{L}_{l a n g}
\]

使用
\(\mathcal{L}_{S c a l e D e t}\)
进行训练后,
ScaleDet
可部署在包含可见或未见类的任何上游或下游数据集上。对于任何给定的测试数据集的标签空间,替换统一标签空间
\(L\)
后,
ScaleDet
可以根据视觉语言相似性分配标签。当测试数据集包含未见过的类时,整体评估设置即为
zero-shot
检测或
open-vocabulary
对象检测。在任何给定的数据集上进行测试时,可以直接评估
ScaleDet
或在评估之前对其进行微调。

Experiments


Training with a growing number of datasets


表 1 展示了在增加数据集数量时对上游数据集的影响:1) 增加训练数据集的数量始终会带来更好的模型性能。2)多数据集使用
ScaleDet
进行训练通常优于单数据集训练。这表明
ScaleDet
在异构标签空间、不同数据集的不同领域中学习得很好,并且不会过度拟合任何特定数据集。

图 3 展示了在
ODinW
基准测试中直接迁移的性能。值得注意的是,扩大
ScaleDet
训练数据集的数量显着提高了下游数据集的准确性。

图 4 进一步可视化了
ScaleDet

ODinW
中的一些下游数据集上的性能。这些数据集要么包含看不见的类,要么来自与用于训练的那些非常不同的视觉域。重要的是,
ScaleDet
在这两种情况下都表现良好。

表 2 展示了使用不同的骨干和文本编码的测试结果。

Comparison to SOTA multi-dataset detectors

表 3 展示了遵循
UniDet
的设置并在相同的数据集上训练
ScaleDet
的性能。
UniDet
训练了多个特定于数据集的分类器,而
ScaleDet
则由一个分类器使用语义标签进行训练。

在表 4 展示了遵循
Detic
的设置执行多数据集训练的性能对比。在
Detic
中,
LVIS

COCO
的统一标签空间包含
1203
个类标签,通过将两个标签集与
wordnet
同义词集合并获得,而
ScaleDet
将它们的标签(
1203+80
)“扁平化”为
1283

Comparison to SOTA detectors on COCO

表 5 展示了基于
LVIS

COCO

O365

OID
训练论文的
ScaleDet
与其他模型的检测性能对比,其中所有模型都使用
ResNet50
主干训练。

表 6 展示了使用
Swin Transformers
作为主干网络的性能对比。

Comparison of SOTA on ODinW

表 7 展示了 3 种检测器在
ODinW
上的性能比较。

Ablation study

表 8 展示了
ScaleDet
的组件的消融实验结果。

Conclusion


论文介绍了一种简单但可扩展且有效的多数据集目标检测训练方法
ScaleDet
,在统一的语义标签空间中跨多个数据集学习,通过硬标签和软标签分配进行优化以对齐视觉和文本编码。
ScaleDet
在多个上游数据集(
LVIS

COCO

Objects365

OpenImages
)和下游数据集(
ODinW
)上实现了最新的性能。



如果本文对你有帮助,麻烦点个赞或在看呗~
更多内容请关注 微信公众号【晓飞的算法工程笔记】

work-life balance.