MENU

【代码札记】基于 Merkle DAG 的文件存储服务 P4 API接口 & SpringBoot初入门

October 24, 2021 • Read: 108 • 瞎折腾

上文中实现了一个简单的概念原型,但是每次要用都得打开IDEA改代码,给人一种不可靠的感觉。经过一番搜索,我发现IPFS也提供基于HTTP的接口,我决定仿照IPFS也弄一个HTTP API。



虽然我是写Java的,可我从来没用过Spring相关的东西(Spring框架是一系列简化开发的库的合集,而当Spring这套框架变得愈加复杂时,人们构建了SpringBoot来简化Spring框架的使用)。我一直觉得对于个人的小项目来说用Spring过于庞大了,所以以前我比较青睐诸如Javalin和Ktor这样的框架,用于快速构建一个HTTP API,但是他们仅仅是一个提供HTTP API的库,当程序需要可配置性的时候,通常需要一个yml(我个人比较青睐这个格式作为配置文件)文件进行配置,于是解析配置、拉起服务等操作都需要我自己写代码。一个两个还行,等项目多了,或者说,一个项目里的配置多了起来,自己维护这一套代码就变得极其繁琐。最近我在考虑为这个系列做一个HTTP API,找了一圈,这些库虽然说小巧好用,但跑不掉自己要做好多铺垫/善后。最终还是找到了Spring,与几年前不同,现在可以直接通过官方的网页版交互式选择你需要的配件,然后自动生成项目模板。我一直不愿意碰Spring的原因之一就是它本身过于庞大,给人一种无从下手的感觉。至少SpringBoot在这方面减轻了我启动项目的痛苦。

项目总览

经过大约三四天的编码,我成功的将原来的代码迁移到SpringBoot框架中,其中重构了不少东西,并且大部分东西都搞成用Java编写了。SpringBoot对于Kotlin的支持还比较有限,一些东西和Kotlin混合起来之后会引发一些奇怪的问题,所以我比较青睐的做法是大部分基础设施使用Java编写,比如配置和数据库实体什么的,然后一些工具类或者数据模型使用Kotlin,后者在写法上会方便不少(尤其得益于data class及不严格要求一个public class独占一个文件)。通过大约上千次谷歌搜索,我终于让Gradle(with Kotlin DSL)、protobuf、lombok、SpringBoot和Kotlin能够在一个项目中融洽相处。现在的项目中有这么多文件:

屏幕截图 2021-10-24 134521.png

对比之下,原来的工程全部展开也只有这么一点文件:

屏幕截图 2021-10-24 134713.png

并且虽然这些组件可以在一个工程中和睦相处,但实际上lombok和Kotlin还是没有办法一起工作。因为Hibernate使用代理类来监测实体,而Kotlin默认的类和成员全都是final,因此使用Kotlin来编写数据库实体就不可避免的要滥用open关键字,同时Hibernate依赖setter来更新数据,因此数据库中具有不可变语义的列被声明为val时将会导致Hibernate无法为查询结果赋值,而声明为var就失去了Kotlin中不可变成员变量的优势。因此最终决定还是使用Java来编写数据库实体类,然后使用Lombok处理Getter和Setter。但是因为Kotlin要先于Java编译(否则Java编译时找不到Kotlin依赖),而此时Lombok的注解处理器来没有开始工作,因此使用@Getter@Setter标注的类,这时候还没有对应的方法被生成。所以在Kotlin中没办法使用依赖Lombok的Java类。但好在需要Lombok的地方只有数据库实体,而与数据库相关的操作可以封装到由Java实现的服务里,通过巧妙的设计可以让Kotlin和Lombok互不见面,从而避免产生问题。

项目结构

在切换到SpringBoot之后我发现原来的一些设计并不能直接拿来用。在Spring中引入了控制反转(Invert of Control),即由框架调用你的代码。在传统的编程中都是你的程序调用框架来完成一些事情,比如你的程序调用Jackson或者Gson将一个字符串转换为一个对象,这个是控制。而在处理HTTP请求时,别人的代码调用你的代码,这个就有点反转控制的意思了。控制反转更严格的说法是基于对象的,在Java中所有的数据和操作都封装在对象中,因此当你的代码需要利用Jackson或者Gson做什么的时候,至少需要先实例化一个ObjectMapper或者Gson对象,然后拿着这个对象去做事。这个过程中你的代码控制这个对象如何实例化;在控制反转中,你的代码接受一个对象作为入参,而不必(也无法)控制这个对象在何时何地如何实例化的,只管用就是了。好在原来的程序中我设计了不少接口,所以迁移起来还是比较方便的。反转控制带来的好处就在于程序代码之间只经过接口约定,而不必假定对方是如何实现的。大多数情况下接口是一个相对比较强的约束(但没办法程序化的约定实现效果——程序员只能将要求的实现效果,诸如原子性,写在JavaDoc中并祈祷子类这样实现)。

在重构后的设计中一共有三个服务:ProtoMetaServiceProtoStorageServiceProtoService。Meta和Storage都只面向不同的后端存储元数据和数据,并不关心这些数据如何去重,如何与其他数据联系起来,干就完了。元数据存储还好说,目前的同一实现是使用JPA在关系型数据库上提供持久化存储(不使用NoSQL的原因在于这东西没必要用NoSQL存,反正都已经SpringBoot了,RDB不可避免,索性就完全拥抱RDB了),配合Redis实现分布式锁,保证同一时间同一个键只有一个调用者可以修改(这样做的原因是为了拥抱Hibernate提供的ORM特性,同时不必自己写SQL,并且在Redis上竞争锁的性能会更高一些)。得益于JPA的抽象,元数据并不挑数据库,无论是MySQL还是PostgreSQL,只要能接到JPA上就可以,而分布式锁也就是一个Redis,没什么好挑的。而数据存储就不一样了,目前开发用只是将数据存在文件里,之后为了实用性需要接入AWS S3,或者为了花哨一些而接入IPFS。在面对不同的存储后端时只考虑如何读何如写,这种纯粹性能带来非常高的编码效率——试想一下不光要处理如何读如何写,还得面向不同的存储后端解决怎么去重/复用,想想就头皮发麻。

至于数据切片、去重和重建,我把这些工作全都交给了ProtoService,它会利用Meta和Storage(当然,实现者也可以不用,他/她开心就好)来完成底层数据的存储而不必关心实现,只专注于如何去重,如何重建。还是那句话,设计的纯粹性能带来非常高的效率,虽然在上文中实现快速原型时大可不必如此,但从长期的可维护性来看,这样设计受益会更高(翻译:日后维护不会很痛苦)。

关于其他SpringBoot的基建就不多赘述了——诸如JpaRepository、Entity描述什么的。这里简单说说给我眼前一亮的地方。还记得开头说的使用Jackson解析配置文件吗?SpringBoot能帮我把这部分自动化了,然后配合依赖注入,写程序真的太爽了。一个很现实的需求就是要根据不同的配置文件来决定使用哪一个存储后端。通过SpringBoot的自动配置就可以直接把枚举值写到application.yml里,然后对应的Java代码中加几个注解,配置文件里的值就能自动映射到对象中的成员变量上,最后通过依赖注入获取这个对象,然后解析内容构建对应的存储后端,把这个构建的对象返回给Spring。在之后这个对象(Bean)就可以注入到其他代码里作为存储后端使用。一气呵成,非常舒服,再也不用自己写代码解析yml了。

接口设计

上面吹了一波Spring的好处,但是我觉得还是得自己先不用Spring写几个项目,再去接触Spring才会有所感受,才能理解他这里面为什么要这么设计。对于接口方面,比起使用Javalin或者Ktor自己注册Handler,Spring的Controller让代码更加结构化(也就是MVC里面的C)。关于Proto的接口设计如下:

/proto
 + /chunk
 | + Post: Upload chunk of data and get links to blob or list
 | + Get:  Download chunk of data from a link to blob or list
 |
 + /tree
 | + Post: Create a tree obj from list of links
 | + Get:  Get the list of links from a link to a tree obj
 |
 + /commit
 | + Post: Create a commit obj from the given info
 | + Get:  Display the info of a link to commit obj
 |
 + /restore
 | + Post: Restore and return all involved blob object links from a list of links
 |
 + /status
 | + Get:  Query the info of given links

根据Proto已有的操作,chunk接口能够操作bloblist类型,用于读写整块数据。而treecommit就分别操作树和快照对象。而restore则用于告诉存储后端恢复/准备某些proto以供读取,并返回这些proto涉及到的其他proto,如果选择不进行操作,该接口也可以单纯用于列出这个proto所涉及的子proto。最后status则是用于查询给定Proto的信息,比如类型、文件信息、存储状态等。

之后还会有一个接口,面向用户界面提供一个类似文件系统的功能,然后利用前端调用proto接口进行数据的上传、修改等。

项目地址

由于我比较懒,并不想详细的总结什么东西,因此这篇文章写的也比较草率,放一个项目链接吧:GitHub。在这里可以看到这个项目的全部代码,并且可以看到我用了一些比较花哨的东西,比如OpenAPI之类的东西。目前代码量算是一半一半:

屏幕截图 2021-10-24 164521.png

总之这篇文章就先到这里了。周末突然发现这个月的更新还完全没写,于是赶紧摸了一篇。

-全文完-


生活不易,一点广告


知识共享许可协议
【代码札记】基于 Merkle DAG 的文件存储服务 P4 API接口 & SpringBoot初入门天空 Blond 采用 知识共享 署名 - 非商业性使用 - 相同方式共享 4.0 国际 许可协议进行许可。
本许可协议授权之外的使用权限可以从 https://www.skyblond.info/about.html 处获得。

Archives QR Code Tip
QR Code for this page
Tipping QR Code
Leave a Comment

2 Comments
  1. wys wys

    曾经对编程感过兴趣,被现实打了几次后,彻底放弃了。

    1. @wys编程这东西,我感觉最难的还是入门。早年间花了好大功夫试图去理解计算机和代码是怎么运行的,后来理解了之后,无论是C,Java,Python还是其他什么语言,大部分都可以直接上手,与语言本身有关的特性简单搜索一下看看文档,一周之内也都能上手写。不过我个人觉得不同的语言有各自的设计精髓,就和说英语一样,说Chinglish也算英语,但不地道。同样,按照Java的风格写C++也能写,就是不地道。