Spring Boot + Spring MVC + MyBatis 版本的 Real World 实现

Real World 是由 thinkster 这样一个在线编程教育机构发起的一个前后端分离的项目规范。用以展示并作为教材教大家用 react、angular 等不同的前端框架或者 rails、django、spring boot 等不同的后端框架实现同一个项目时的实践是什么样子的。我觉得这个主意非常的好,它让大家对技术的讨论有了一个共同的主题,在采用不同的技术栈以及设计思路解决这个共同的问题的时候我们可以更确切的看到不同的方案之间的优劣,从而更切实的(而不是零散的代码和想象)了解不同框架、语言、设计思路在实现一个项目时的差异,从而帮助我们更好的选择项目的解决方案。当然,从单个技术栈来看,它提供了一个做出完整项目需要都需要哪些具体的知识点,可以当做某一个技术栈的入门小项目来学习和借鉴。

虽然这个项目叫做 real world,相比 todomvc 这样的 hello world 确实复杂了不少,但是很显然它的复杂度还仅仅是一个人几个小时就能完成的水平,当然不能全面的反映出一个框架的水平,仅做参考。

本篇对项目做一个整体的介绍,后续会有一些细节的介绍。文章中会涉及到一些 DDD(领域驱动设计) 相关的概念,想要更多的了解建议看看最下面相关材料中的链接。

项目功能

conduit 是 realworld 要实现的一个博客系统。具备一下的功能:

  1. 用户的注册和登录
  2. 用户可以发表、编辑文章
  3. 用户可以对文章添加评论、点赞
  4. 用户可以关注别的用户,关注的用户的文章会展示在用户的 feed 中
image

这是一个前后端分离的项目,其提供了后端 api 的规范。这里,我们不评论其 API 设计的好坏,要完全遵循其设计并实现它。当然,对于不同的语言和框架实现都有其 API 设计的偏好,既然这里定死了一种规范,那么在实现的过程中难免会有一些 tricky 的地方需要我们去克服。

目录结构

如标题所述,这里我提供了一个 spring boot + spring mvc + mybatis 的实现。其大概的结构如下:

.
├── JacksonCustomizations.java
├── MyBatisConfig.java
├── RealworldApplication.java
├── api
├── application
├── core
└── infrastructure
  1. api 是 web 层,实现了和 spring mvc 的 web 接口
  2. core 是业务层,包含了最关键的业务实体和领域服务以及其各个实体之间的交互
  3. application 是对外的服务层,由于这个项目本身的业务并不复杂,这里处理的基本都是各种信息的查询
  4. core 中定义的大量接口在 infrastructure 包含了其具体的实现,比如 data mapper 的实现,具体的密码加密的实现等
  5. 其他则是一些整体的配置类,如主类 RealworldApplication 数据库配置类 MyBatisConfig

六边形架构

六边形架构 或者说是 洋葱架构 其实不是一个什么新东西,因为分层架构会导致其最底层的实现是数据库,而之前很多的业务逻辑和数据库是揉在一起的(事实上很多项目也确实这样,大量的存储过程中包含着业务的逻辑,业务和数据库紧密的结合在了一起);但实际上,一个应用最核心的东西应该是业务逻辑,而业务逻辑是不应该和技术细节有强关联的,数据库实现和视图层一样,是某种技术细节,不应该和将其与业务逻辑绑定,所以应该用应该强调内部和外部:内部是我的业务逻辑,而外部与外界沟通的基础设施和技术细节,比如具体的数据库存储,比如 restful 的 api,再比如 html 的视图。

image

通过这样的思考方式,我们可以认为 mysql 数据库实现仅仅是众多数据库实现中的一个而已,我们可以在不同的环境中轻易的替换掉它,尤其是为对业务的测试提供了可能:我们可以采用内存数据库或者 mock 轻松的实现业务测试。

我所实现的这个 realworld 项目也基本遵循这个架构,首先在 core 中定义了我们的业务实体 User Article 已经各种 Repository 的接口。他们定义了这个项目核心实体的关系以及交互行为。其中具体的 Repository 的实现以及 web 接口的实现都与它无关。对于比较简单的数据创建等行为我们直接在 web 层中处理了,而相对比较麻烦的查询业务我们按照用例在 application 中提供。

在 api 层直接创建用户:

@RequestMapping(path = "/users", method = POST)
public ResponseEntity createUser(@Valid @RequestBody RegisterParam registerParam,
                                 BindingResult bindingResult) {
    checkInput(registerParam, bindingResult);

    User user = new User(
        registerParam.getEmail(),
        registerParam.getUsername(),
        encryptService.encrypt(registerParam.getPassword()),
        "",
        defaultImage);
    userRepository.save(user);
    UserData userData = userQueryService.findById(user.getId()).get();
    return ResponseEntity.status(201).body(
                userResponse(new UserWithToken(userData,
                                               jwtService.toToken(user))));
}

在 application 层创建一个 findRecentArticles 的服务,用于处理相对比较复杂的查询:

public ArticleDataList findRecentArticles(String tag, 
                                          String author, 
                                          String favoritedBy, 
                                          Page page, 
                                          User currentUser) {
    List<String> articleIds = articleReadService.queryArticles(tag, 
                                                               author, 
                                                               favoritedBy, 
                                                               page);
    int articleCount = articleReadService.countArticle(tag, author, favoritedBy);
    if (articleIds.size() == 0) {
        return new ArticleDataList(new ArrayList<>(), articleCount);
    } else {
        List<ArticleData> articles = articleReadService.findArticles(articleIds);
        fillExtraInfo(articles, currentUser);
        return new ArticleDataList(articles, articleCount);
    }
}

注意 这里之所以没有为创建数据的用例在 application 中创建 service 纯粹是因为它们比较简单,在面向复杂的场景时是可以提供的。

CQRS

在 DDD 中 Repository 主要负责数据的持久化:它的任务非常的简单:要么是将内存中的 aggregate 储到数据库中,要么是从数据库中将指定 id 的实体从数据库中重新在内存中构建起来。它实际上是不负责那种复杂的查询业务的,比如获取被喜爱最多的 50 篇文章。更多的内容可以看这篇文章

CQRS 全称 Command Query Responsibility Segregation,强调一个系统的读模型和写模型是分离的。其中 DDD 所实现的是读模型,保证了业务的实现以及数据的一致性。而读模型则纯粹是利用底层数据库的优势将用户需要的数据拼装起来,完全不涉及到实体。这样的好处在于我们可以完全实现界面所需要的数据模型和真正的业务模型的独立演进,不会因为一个界面上数据展示的变化而导致本身的业务模型出现变更。当然,这里所谓的界面就是我们的 json API 规格。对于查询业务,我们在 application/data 下提供了单独的 Data Transfer Object

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ArticleData {
    private String id;
    private String slug;
    private String title;
    private String description;
    private String body;
    private boolean favorited;
    private int favoritesCount;
    private DateTime createdAt;
    private DateTime updatedAt;
    private List<String> tagList;
    @JsonProperty("author")
    private ProfileData profileData;
}

可以看到 ArticleData 就是 DTO 或者说是 Presentation Model 它和 API 文档中对数据的格式要求完全对齐而不考虑 Article 和 Author 到底应不应当属于一个聚合。

而下面则是在 core 下的实体:

@Getter
@NoArgsConstructor
@EqualsAndHashCode(of = {"id"})
public class Article {
    private String userId;
    private String id;
    private String slug;
    private String title;
    private String description;
    private String body;
    private List<Tag> tags;
    private DateTime createdAt;
    private DateTime updatedAt;
    ...
}

很显然,这里 Article 中并不包含 Author 的概念,因为它们并不属于一个聚合,Article 只能保存另外一个聚合的 Id (userId)。

具体的代码见 GitHub 欢迎 star、fork、报 bug、提供 PR。

相关资料

  1. Real World
  2. CQRS
  3. DDD Repository
  4. Some tips about DDD
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容