AICon 上海站|日程100%上线,解锁Al未来! 了解详情
写点什么

不影响开发体验,如何将单体 Node.js 变成 Monorepo

作者:Adrien Joly

  • 2022-12-08
    北京
  • 本文字数:9046 字

    阅读完需:约 30 分钟

不影响开发体验,如何将单体Node.js变成Monorepo

将单体拆分成服务会带来维护多个存储库(每个服务一个存储库)的复杂性,每个存储库都有独立(但相互依赖)的构建流程和版本控制历史。Monorepo 已经成为一种降低复杂性的流行解决方案。


尽管 Monorepo 工具开发商有时会提供建议,但在现有代码库中配置 Monorepo 并不容易,尤其是单体代码库。更重要的是,迁移到 Monorepo 可能会给代码库开发团队带来巨大影响。例如,需要将大多数文件移动到子目录中,这会与团队当前正在进行的其他更改产生冲突。


本文将探讨如何平滑地将单体 Node.js 代码库变成 Monorepo,并将可能带来的影响和风险降到最低。


简介:单体代码库


假如存储库包含两个 Node.js API 服务器:api-server 和 back-for-front-server。它们是用 TypeScript 编写的,并转译为 JavaScript 在生产环境中运行。这两个服务器共用一套开发工具(用于检查、测试、构建和部署服务器)和 npm 依赖。它们还共用 Dockerfile 打成一个包,运行哪个 API 服务器要通过指定不同的入口点来选择。


迁移之前的文件结构:

├─ .github│  └─ workflows│     └─ ci.yml├─ .yarn│  └─ ...├─ node_modules│  └─ ...├─ scripts│  ├─ e2e-tests│  │  └─ e2e-test-setup.sh│  └─ ...├─ src│  ├─ api-server│  │  └─ ...│  ├─ back-for-front-server│  │  └─ ...│  └─ common-utils│     └─ ...├─ .dockerignore├─ .eslintrc.js├─ .prettierrc.js├─ .yarnrc.yml├─ docker-compose.yml├─ Dockerfile├─ package.json├─ README.md├─ tsconfig.json└─ yarn.lock
复制代码


迁移之前的 Dockerfile(经过简化):

FROM node:16.16-alpineWORKDIR /backendCOPY . .COPY .yarnrc.yml .COPY .yarn/releases/ .yarn/releases/RUN yarn installRUN yarn buildRUN chown node /backendUSER nodeCMD exec node dist/api-server/start.js
复制代码


在共享存储库中维护多个服务器有以下好处。


  • 开发工具(TypeScript、ESLint、Prettier……)的配置和部署过程是共享的,这减少了维护工作,而且可以保证所有贡献团队的做法一致。

  • 方便开发人员跨服务器重用模块,例如日志模块、数据库客户端、外部 API 封装器等。

  • 版本控制简单,因为所有服务器共用版本,任何服务器的任何更新都会产生新版本的 Docker 镜像,其中包含所有服务器。

  • 也很容易编写覆盖多个服务器的端到端测试,并将它们包含在存储库中,因为所有东西都在一个地方。遗憾的是,这些服务器的源代码是单体的。我的意思是,各服务器的代码是分不开的。为其中一个服务器编写的代码(例如 SQL 适配器)最终也会被其他服务器导入。因此,要防止服务器 A 的代码更改也影响到服务器 B,这非常复杂,可能会导致意想不到的回归。而且,随着时间的推移,代码的耦合度会变得越来越高,代码会越来越脆弱,越来越难维护。


“Monorepo 结构”是一个有趣的折衷方案:在共享存储库的同时将代码库分割成包。这种划分使得接口更加清晰,因此,可以有意识的选择包之间的依赖关系。它还实现了一些工作流优化,例如,只在更改过的包上构建和运行测试。


如果代码库很大,集成了很多工具(例如代码分析、转译、打包、自动化测试、持续集成、基于 Docker 的部署……),那么将单体代码库迁移到 Monorepo 很快就会变得困难和反复。此外,由于存储库做了结构更改,所以在迁移期间,操作任何 Git 分支都会导致冲突。让我们看下将代码库转换为 Monorepo 的必要步骤,最大限度减少迁移问题。


所需的更改


将代码库迁移到 Monorepo 需要遵循以下步骤。


  1. 文件结构:一开始,创建包含所有源代码的惟一包,这样,所有文件都将被移动。

  2. Node.js 模块解析的配置:使用 Yarn 工作空间来实现包之间的相互导入。

  3. Node.js 项目和依赖的配置:package.json (包括 npm/yarn 脚本)将被拆分:主脚本在根目录,然后每个包里有一个。

  4. 开发工具的配置:tsconfig.json、.eslintrc.js、 .prettierrc.jsjest.config.js 也将拆分成两部分:一个“基础”部分,然后每个包里有一个对它的扩展。

  5. 持续集成工作流的配置:.github/workflows/ci.yml 需要做多处调整,例如,确保其中的步骤会针对每个包运行,多个包的指标(如测试覆盖率)会合并成一个。

  6. 构建和部署流程的配置:优化 Dockerfile,使其只包含要构建的服务器所需的文件和依赖。

  7. 跨包脚本的配置:使用 Turborepo 编排影响多个包的 npm 脚本的执行(如构建、测试、分析)。迁移之后的文件结构:

├─ .github│  └─ workflows│     └─ ci.yml├─ .yarn│  └─ ...├─ node_modules│  └─ ...├─ packages│  └─ common-utils│     └─ src│        └─ ...├─ servers│  └─ monolith│     ├─ src│     │  ├─ api-server│     │  │  └─ ...│     │  └─ back-for-front-server│     │     └─ ...│     ├─ scripts│     │  ├─ e2e-tests│     │  │  └─ e2e-test-setup.sh│     │  └─ ...│     ├─ .eslintrc.js│     ├─ .prettierrc.js│     ├─ package.json│     └─ tsconfig.json├─ .dockerignore├─ .yarnrc.yml├─ docker-compose.yml├─ Dockerfile├─ package.json├─ README.md├─ turbo.json└─ yarn.lock
复制代码


由于 Node.js 及其工具生态系统非常灵活,所以共享一个通用的方法会很复杂,因此请记住,为了让开发人员的体验至少与迁移前一样好,将需要进行大量的优化迭代。


如何将影响降至最低


所幸,虽然迭代优化可能需要几周的时间,但影响最大的是第一步:更改文件结构。


如果你的团队借助 Git 分支并行开发,那么这一步骤将导致这些分支发生冲突,在合并到存储库的主分支时解决冲突就会非常麻烦。


因此,我们有三方面的建议,特别是当需要就迁移到 Monorepo 说服整个团队时。


  • 提前计划(短时间的)代码冻结:为了避免迁移时发生冲突,定义一个日期和时间,到时所有分支都必须合并。提前计划,以便开发人员可以做出适当的调整。但在可行的迁移计划确认前,不要选定日期。

  • 将迁移计划中最关键的部分编写 bash 脚本,这样就可以确保开发工具在迁移前后都能工作,包括在持续集成管道上。这样应该可以打消怀疑者的疑虑,在代码冻结的实际日期和时间上获得更大的灵活性。

  • 在团队的帮助下,列出他们日常工作所需的所有工具、命令和工作流(包括 IDE 的特性,如代码导航、代码分析和自动补全)。这个需求列表(或验收标准)将帮助我们检查将开发体验迁移到 Monorepo 设置的步骤。这有助于确保在迁移时不会忘掉重要事项。以下是我们决定满足的需求列表:

  • yarn install 仍然安装依赖;

  • 所有自动化测试仍能运行并通过;

  • yarn lint 仍然能够发现代码风格违规的情况(如果有的话);

  • eslint 错误(如果有的话)仍然会在 IDE 中报告;

  • prettier 仍然会在 IDE 保存文件对其进行格式化;

  • IDE 仍然会发现错误的导入和 / 或违反tsconfig.json 文件中定义的 TypeScript 规则的情况(如果有的话);

  • 在使用外部包暴露的符号时,如果它被声明为依赖,那么 IDE 仍然能够提出导入正确模块的建议;

  • 生成的 Docker 镜像在部署后仍然能够启动且和预期一样正常运行;

  • 生成的 Docker 镜像大小仍然(大致)一样;

  • 整个 CI 工作流都可以通过,而且不会消耗更多的时间;

  • 集成的第三方代码分析器(SonarCloud)仍然能够和预期一样工作。下面是迁移脚本示例:

# 这个脚本使用 Yarn 工作空间和 Turborepo 将存储库转换为 Monorepo
set -e -o pipefail # stop in case of error, including for piped commands
NEW_MONOLITH_DIR="servers/monolith" # 第一个工作空间的路径:"monolith"
# 清理临时目录,即没有存储在 Git 中的那些rm -rf ${NEW_MONOLITH_DIR} dist
# 创建目标目录mkdir -p ${NEW_MONOLITH_DIR}
# 将文件和目录从 root 移动到 ${NEW_MONOLITH_DIR}目录# ……除了那些绑定到 Yarn 和 Docker 的(目前)mv -f \ .eslintrc.js \ .prettierrc.js\ README.md \ package.json \ src \ scripts \ tsconfig.json \ ${NEW_MONOLITH_DIR}
# 将新文件复制到 root 目录cp -a migration-files/. . # 包括 turbo.json, package.json, Dockerfile, # 和 servers/monolith/tsconfig.json
# 更新路径sed -i.bak 's,docker\-compose\.yml,\.\./\.\./docker\-compose\.yml,g' \ ${NEW_MONOLITH_DIR}/scripts/e2e-tests/e2e-test-setup.shfind . -name "*.bak" -type f -delete # delete .bak files created by sed
unset CI # to let yarn modify the yarn.lock file, when script is run on CIyarn add --dev turbo # 安装 Turboreporm -rf migration-files/echo "✅ You can now delete this script"
复制代码


我们在持续集成工作流中添加了一个作业(GitHub Actions),用于检查测试和其他常规 Yarn 脚本在迁移之后是否仍然可以正常工作:


jobs:  monorepo-migration:    timeout-minutes: 15    name: Test Monorepo migration    runs-on: ubuntu-latest    steps:      - uses: actions/checkout@v2      - run: ./migrate-to-monorepo.sh        env:          YARN_ENABLE_IMMUTABLE_INSTALLS: "false" # 允许 yarn.lock 变化      - run: yarn lint      - run: yarn test:unit      - run: docker build --tag "backend"      - run: yarn test:e2e
复制代码


从单体的源代码转换生成第一个包


看看迁移之前我们唯一的package.json 文件是什么样子:


{  "name": "backend",  "version": "0.0.0",  "private": true,  "scripts": {    /* 所有 npm/yarn 脚本... */  },  "dependencies": {    /* 所有运行时依赖 ... */  },  "devDependencies": {    /* 所有开发依赖 ... */  }}
复制代码


以下片段摘自迁移之前 TypeScript 配置文件tsconfig.json


{    "compilerOptions": {        "target": "es2020",        "module": "commonjs",        "lib": ["es2020"],        "moduleResolution": "node",        "esModuleInterop": true,        /* ... 多条让 TypeScript 更严谨的规则 */    },    "include": ["src/**/*.ts"],    "exclude": ["node_modules", "dist", "migration-files"]
复制代码


在将单体拆分成包时,我们必须:


  • 告诉包管理器(这里是 Yarn)代码库包含多个包;

  • 更明确地指出可以在哪里找到这些包。为了使包可以作为其他包的依赖项导入(也就是workspaces),我们建议使用 Yarn 3 或其他支持工作空间的包管理器。


所以我们在package.json中添加了"packageManager": "yarn@3.2.0" ,并在其旁边创建了一个.yarnrc.yml 文件:


nodeLinker: node-modulesyarnPath: .yarn/releases/yarn-3.2.0.cjs
复制代码


根据 Yarn 迁移路径 的建议:


  • 提交.yarn/releases/yarn-3.2.0.cjs 文件;

  • 我们还是坚持使用node_modules目录,至少目前如此。在将单体代码库(包括package.jsontsconfig.json)移动到 servers/monolith/之后,在项目的根目录下新建一个package.json 文件,其中 workspaces 属性列出了工作空间的位置:


{  "name": "@myorg/backend",  "version": "0.0.0",  "private": true,  "packageManager": "yarn@3.2.0",  "workspaces": [    "servers/*"  ]}
复制代码


从现在开始,每个工作空间必须有自己的package.json 文件,用于指定其包名和依赖。截至目前,我们只有一个工作空间“monolith”。在servers/monolith/package.json文件中使用组织名作为其名称的前缀,明确标明它现在是一个 Yarn 工作空间:


{  "name": "@myorg/monolith",  /* ... */}
复制代码


在运行完yarn install 之后,我们又修复了一些路径:


  • yarn build 及其他 npm 脚本(从 servers/monolith/运行时)应用仍然有效;

  • Dockerfile 应该仍然可以生成一个有效的构建;

  • 所有的 CI 检查应该仍然可以通过。

提取第一个包:common-utils


到目前为止,我们的 Monorepo 只定义了一个“monolith”工作空间。它在servers目录下,这表明它无意让其他工作空间导入其模块。


让我们定义一个可以被这些服务器导入的包。为了更好地传达这种差异,我们在servers目录旁增加了一个packages目录。要提取一个包的话,目录common-utils(来自servers/monolith/common-utils)是首选,因为“monolith”工作空间的多个服务器都使用了它的模块。当每个服务器都在自己的工作空间中定义时,common-utils包将被声明为两个服务器的依赖项。


现在,我们将common-utils 目录从servers/monolith/ 移动到新建的目录packages/


为了将其转换成一个包,创建packages/common-utils/package.json 文件,其中包含所需的依赖和构建脚本:


{  "name": "@myorg/common-utils",  "version": "0.0.0",  "private": true,  "scripts": {    "build": "swc src --out-dir dist --config module.type=commonjs --config env.targets.node=16",    /* 其他脚本 ... */  },  "dependencies": {    /* common-utils 的依赖 ... */  },}
复制代码


注意:我们使用swc 将 TypeScript 转译为 JavaScript,但使用tsc 应该也可以获得类似的效果。此外,我们尽力让它的配置(使用命令行参数)与servers/monolith/package.json 中的配置一致。确保包会按预期构建:


$ cd packages/common-utils/$ yarn$ yarn build$ ls dist/ # 应该包含 src/ 中所有文件的.js 构建
复制代码


接下来,更新根package.json 文件,将packages/ 的所有子目录(包括common-utils)也声明为工作空间:


{  "name": "@myorg/backend",  "version": "0.0.0",  "private": true,  "packageManager": "yarn@3.2.0",  "workspaces": [    "packages/*",    "servers/*"  ],  /* ... */}
复制代码


common-utils 添加为服务器包monolith 的依赖:$ yarn workspace @myorg/monolith add @myorg/common-utils


你可能已经注意到,Yarn 创建了一个到packages/common-utils/ (源代码就在这里)的符号链接node_modules/@myorg/common-utils


完成此操作后,我们必须修复所有有问题的common-utils 导入。实现这一目标的一种低成本方法是在servers/monolith/中重新引入common-utils目录,并使用一个从新生成的包@myorg/common-utils导出函数的文件:


export { hasOwnProperty } from "@myorg/common-utils/src/index"


更新服务器的Dockerfile ,以便构建包并包含在镜像中:


# 使用以下命令从项目根目录构建:# $ docker build -t backend -f servers/monolith/Dockerfile .
FROM node:16.16-alpine
WORKDIR /backendCOPY . .COPY .yarnrc.yml .COPY .yarn/releases/ .yarn/releases/RUN yarn install
WORKDIR /backend/packages/common-utilsRUN yarn build
WORKDIR /backend/servers/monolithRUN yarn build
WORKDIR /backendRUN chown node /backendUSER nodeCMD exec node servers/monolith/dist/api-server/start.js
复制代码


这个Dockerfile 必须从根目录构建,那样它才能访问yarn 环境和那里的文件。注意:可以通过在Dockerfile 中将yarn install 替换为yarn workspaces focus --production来从 Docker 镜像中除去开发依赖,这要感谢 plugin-workspace-tools 插件,参考“使用 Yarn 3 和 Turborepo 编排和 Docker 化 Monorepo”一文中的介绍。


至此,我们已经成功地从单体中提取出了一个可导入的包,但是:


  • 生产构建因为Cannot find module 错误运行失败;

  • common-utils 的导入路径过于冗长。

修复开发和生产环境的模块解析


我们从@myorg/types-helpers导入函数的方法是有问题的,因为 Node.js 从子目录src/中查找模块,即使它们被转译到子目录dist/中。


我们宁愿采用一种子目录无关的方式导入函数:


import { hasOwnProperty } from "@myorg/common-utils"


即使我们在包的package.json 文件里指定"main": "src/index.ts" ,在运行转译构建时路径仍然会被破坏。


作为补救使用 Node 的 条件导入,以使包的入口点可以适配运行时上下文:


 {    "name": "@myorg/common-utils",    "main": "src/index.ts",+   "exports": {+     ".": {+       "transpiled": "./dist/index.js",+       "default": "./src/index.ts"+     }+   },    /* ... */  }
复制代码


简而言之,增加一个exports配置项,关联包根目录的两个入口点:


  • default条件指定 ./src/index.ts 为包的入口点;

  • transpiled条件指定./dist/index.js 为包的入口点。根据 Node 的文档,default 条件应该始终放在最后。transpiled条件是自定义的,所以你可以随意指定其名称。


为了让这个包在转译后的运行时上下文中运行,需要修改相应的 node 命令,指定自定义条件。例如,在Dockerfile中:


- CMD exec node servers/monolith/dist/api-server/start.js+ CMD exec node --conditions=transpiled servers/monolith/dist/api-server/start.js
复制代码


确保开发工作流和以前一样


现在,我们有了一个 Monorepo。它包含两个工作空间,每一个都可以从另一个导入模块、构建并运行。


但是,每增加一个工作空间,就需要更新Dockerfile ,因为必须针对每个工作空间手动运行yarn build 命令。


此时,像 Turborepo 这样的 Monorepo 编排器就派上用场了:我们可以让它根据声明好的依赖关系递归地构建包。


在将 Turborepo 作为 Monorepo 的开发依赖项添加以后(命令:$ yarn add turbo --dev ),可以在turbo.json中定义一个构建管道:


{    "pipeline": {        "build": {            "dependsOn": ["^build"]        }    }}
复制代码


这个管道定义的意思是,对于任何包,$ yarn turbo build 会从它依赖的包开始构建,以此类推。这样就可以简化Dockerfile


# 使用以下命令从项目根目录构建:# $ docker build -t backend -f servers/monolith/Dockerfile .
FROM node:16.16-alpineWORKDIR /backendCOPY . .COPY .yarnrc.yml .COPY .yarn/releases/ .yarn/releases/RUN yarn installRUN yarn turbo build # builds packages recursivelyRUN chown node /backendUSER nodeCMD exec node --conditions=transpiled servers/monolith/dist/api-server/start.js
复制代码


注意:可以利用 Docker 多阶段构建和turbo prune 来优化构建时间和镜像大小,但在本文写作时,生成的yarn.lock 文件与 Yarn 3 还不兼容。(关于这个问题,可以查看 这个 pull 请求 了解最新进展。)借助 Turborepo,在定义好管道后(和构建时类似),只需一条命令(yarn turbo test:unit )就可以运行所有包的单元测试。


也就是说,大多数开发工作流的依赖项和所依赖的配置文件都移到了servers/monolith/目录下,因此,它们大部分都无法正常工作了。


我们可以把这些依赖项和文件留在根目录一级,那样所有包都可以共用。或者在每个包中复制一份。当然,还有更好的方法。


将通用配置提取到包中并扩展它


现在,最关键的构建和开发工作流已经可以正常工作了,接下来,要让测试执行器、代码分析器和格式化器在针对不同的包执行时行为一致,同时还要留出定制空间。


一种方法是创建保存基础配置的包,然后让其他包扩展它。


就像我们对common-tools所做的那样,创建以下包:


├─ packages│  ├─ config-eslint│  │  ├─ .eslintrc.js│  │  └─ package.json│  ├─ config-jest│  │  ├─ jest.config.js│  │  └─ package.json│  ├─ config-prettier│  │  ├─ .prettierrc.js│  │  └─ package.json│  └─ config-typescript│     ├─ package.json│     └─ tsconfig.json├─ ...
复制代码


然后,把它们作为依赖项添加到每个包含源代码的包中,并创建配置文件扩展它们:


packages/*/.eslintrc.js:
module.exports = { extends: ["@myorg/config-eslint/.eslintrc"], /* ... */}
packages/*/jest.config.js:
module.exports = { ...require("@myorg/config-jest/jest.config"), /* ... */}
packages/*/.prettierrc.js:
module.exports = { ...require("@myorg/config-prettier/.prettierrc.js"), /* ... */}
packages/*/tsconfig.json:
{ "extends": "@myorg/config-typescript/tsconfig.json", "compilerOptions": { "baseUrl": ".", "outDir": "dist", "rootDir": "." }, "include": ["src/**/*.ts"], /* ... */}
复制代码


可以使用像 plop 这样的样板文件生成器来简化使用这些配置文件设置新包的过程,加快设置速度。


下一步:每个服务器一个包


我们已经逐项核对了“如何将影响降至最低”一节所列出的所有需求,现在可以冻结代码贡献、运行迁移脚本、并将更改提交到源代码存储库了。


从现在起,该存储库可以正式称为“Monorepo”了!所有开发人员都应该能够创建自己的包,并在单体中导入它们,而不是直接向其中新增代码。基础已经打好,可以开始将单体拆分成多个包了,就像我们对common-tools 所做的那样。


我们不打算讨论实现这一目标的详细步骤,但这里有一些关于如何做好拆分准备的建议:


  • 从提取小的实用程序包开始,例如类型库、日志记录、错误报告、API 封装器等;

  • 然后,提取计划跨所有服务器共享的代码的其他部分;

  • 最后,复制不计划共享但不只一个服务器依赖的部分。这些建议的目标是逐步解耦各服务器。以此为基础将每个服务器提取成一个包应该和提取common-utils 一样简单。


此外,在这个过程中,你应该可以利用以下几项特性优化构建、开发和部署工作流的持续时间:


  • Docker 多阶段构建(参见 Dockerfile 文件编制最佳实践) ;

  • 重用主机的 Yarn 缓存(参见 Docker Build Mounts);

  • Turborepo 的 远程缓存。

小结


我们已经把一个单体 Node.js 后端变成了 Monorepo,同时将对团队的影响和风险降到最低:


  • 将单体拆分为多个相互依赖的、解耦的包;

  • 跨包共享通用 TypeScript、ESLint、Prettier 和 Jest 配置;

  • 安装 Turborepo 优化开发和构建工作流。使用迁移脚本让我们可以在准备和测试迁移时避免代码冻结和 Git 冲突,确保构建和开发工具不会因为迁移脚本添加 CI 作业而遭到破坏。


感谢 Renaud Chaput (Notos 联合创始人、CTO)、Vivien Nolot(Choose 软件工程师)和 Alexis Le Texier (Choose 软件工程师)在这次迁移中的通力合作。

原文链接:https://www.infoq.com/articles/nodejs-monorepo/
相关阅读:

Node.js 基于区块链的游戏应用的首选

【异常】window 10 安装 node.js 时遇到 2502 2503 错误解决方法

JXcore 打包在企业级项目里的合理运用和模块系统以及网络的配置详解【node.js】

2022-12-08 16:568271

评论

发布
暂无评论
发现更多内容

Sentienl 动态数据源架构设计理念与改造实践,阿里P8大牛手把手教你

Java 程序员 后端

Sentinel:万字详解微服务的哨兵机制,我跪了,mysql编程入门教程

Java 程序员 后端

Shiro等权限管理框架本质很简单,一个注解+拦截器就可实现

Java 程序员 后端

Spring Boot Redis 实现分布式锁,真香,kalilinux入侵教程

Java 程序员 后端

spring boot 整合Swagger2 构建API文档,linux学习路线图

Java 程序员 后端

Redis源码剖析——客户端和服务器,springboot入门程序

Java 后端

RPC服务和HTTP服务对比,java基础实验报告总结

Java 程序员 后端

RPC框架编写实践——服务治理的基石,这位阿里P7大牛分析总结的属实到位

Java 程序员 后端

Seata 新特性,APM 支持 SkyWalking,java流式编程原理

Java 程序员 后端

Servlet+JSP(七,java界面开发的三层架构技术

Java 程序员 后端

linux 环境安装Flutter

坚果

flutter 安装 11月日更

spring boot增删改查,javassm框架面试重点

Java 程序员 后端

Spring cloud stream【入门介绍】,java开发实例大全云盘

Java 程序员 后端

Redis持久化--Redis宕机或者出现意外删库导致数据丢失--解决方案

Java 程序员 后端

Spring Boot 谷粒学院、谷粒商城项目问题汇总,tomcat面试题

Java 程序员 后端

Spring AOP 源码分析——创建代理对象,绝对干货

Java 程序员 后端

Spring Boot 2(1),蛙课网java教程资源库

Java 程序员 后端

spring boot 使用Spring Cache集成Redis,java编程基础实验报告小结

Java 程序员 后端

Spring Cloud 分布式事务详解及LCN解决方案,mybatis底层原理

Java 程序员 后端

Redis的各种用途以及使用场景,mybatis技术原理

Java 程序员 后端

shiro(三)shiro实战,java面试题项目中的难点

Java 程序员 后端

【Flutter 专题】13 图解最基础的 http 请求方式

阿策小和尚

Flutter 小菜 0 基础学习 Flutter Android 小菜鸟 11月日更

Spring Cloud Stream 编程模型的基础知识,很多老司机都不知道

Java 程序员 后端

002|CocoaPods 优化知多少?

棒棒彬👻

CocoaPods 认知偏差 工程能力 开源软件

Spring Boot 实战(9) springboot 整合 JPA,2021必看

Java 程序员 后端

Spring Cloud Gateway限流实战,万字详解微服务的哨兵机制

Java 程序员 后端

redis数据迁移之redis-shake,java高级技术经理面试题

Java 程序员 后端

RocketMQ ACL版本升级过程中的曲折经历(大厂线上环境大规模MQ升级开启ACL实战)

Java 程序员 后端

Rpc与RMI服务,java面试笔试题代码

Java 程序员 后端

Spring Boot 实战(11)整合MyBatis-Plus,mysql原理相关文章

Java 程序员 后端

Spring Boot核心技术之Rest映射以及源码的分析,java从入门到放弃

Java 程序员 后端

不影响开发体验,如何将单体Node.js变成Monorepo_大前端_InfoQ精选文章