InfoQ 重磅内容产品《中国卓越技术团队访谈录》上新啦! 了解详情
写点什么

开发容器:可重用的开发环境

作者:Avdi Grimm

  • 2022 年 7 月 05 日
  • 本文字数:8064 字

    阅读完需:约 26 分钟

开发容器:可重用的开发环境

拿着 Chromebook 在洗车房做开发

 

那天,我把车开到了洗车场。这是一个高级洗车场,你把车交给工作人员,然后等着他们把车里里外外清洗干净。

 

我要做的就是打发时间了。我还有一些代码要写,但当时我只有一台装在包里的小 Chromebook 和 WiFi 连接。

 

于是,我在GitHub Codespaces中打开了这个项目,然后在上次停下的地方继续,在云端运行我的开发环境。

 


不只是编辑器,而是整个为我的项目定制的虚拟机。

 


我继续开发我的项目。在一台新的(云)机器上配置开发环境所花费的时间:可能只有 5 分钟。

 

现在的 CodeSpaces 太酷了,但本文实际上不是关于它们,甚至不是关于基于云的开发环境。本文将介绍一些技术和实践,让开发人员能够在几秒钟或几分钟内从零开始完成整个项目定制开发环境,例如:

 

  • 在新员工第一天上班的笔记本电脑上;

  • 在第二台旅行用的笔记本电脑上;

  • 在一个设计师的工作站上,他需要在不熟悉后端技术栈的情况下,尝试在本地跨多个内部代码库做出视觉变更;

  • 在一个顾问的笔记本电脑上,同时托管着十几个不相关的代码库;

  • 或者是托管在云端的共享实例中。

 

如果项目的初始设置从一个小小的挑战变成一件轻松自如的事会怎样?如果你可以将开发环境与代码一起打包会怎样?如果你可以在团队中将开发环境标准化,让每一个人都能从中受益,会怎样?如果因为在开发人员的笔记本电脑上项目构建失败而导致的摩擦已经成为过去,会怎样?

 

如果你在洗车时感到无聊,顺便写几行代码呢?

 

这就是开发者体验的未来,而你现在就可以开始感受。实现这一体验的方式就是使用容器进行开发,这些容器有时也被称为开发容器。

什么是开发容器

 

当我们说到“容器”,通常指的是使用 Docker 运行的容器。这也意味着项目可以在 Linux 环境中。当今的大多数 Web 应用程序开发都是这样的。但如果你的项目目标是 iOS,或 Windows 桌面,或其他非 Unix 平台,那么下面的内容可能不太适用于你的项目。

 

这不是一篇介绍 Docker 的文章。由于篇幅的原因,我将假设你对容器化有一定的了解。

 

不过,我们还是有必要简单地讨论一下为什么容器比一些老旧的虚拟化技术(如 Parallels、VirtualBox 或 Vagrant)更适合作为开发环境。

 

简单地说,这是因为容器并不是虚拟化。是的,容器为我们提供了一种看起来像是运行在电脑上的微型电脑的东西。但容器并没有试图模拟计算机,而是通过创建一组独立的命名空间来发挥作用,包括文件系统命名空间、网络端口、进程表和运行操作系统所需的其他命名空间。

 

与虚拟化不同,容器有可能按照原生的速度运行项目代码和工具,而不会让开发机器瘫痪。因为宿主操作系统可以将文件映射到容器命名空间,所以我们可以在容器运行代码的同时使用原生工具编辑源代码。

 

另外,与大多数虚拟化技术不同的是,容器并不是不透明的二进制镜像。我们使用人类可读的配置文件来决定开发容器将包含哪些 Linux 版本、系统包和库、实用程序、文件系统映射、开放端口和支持服务,而且这些配置文件与项目的源代码一起进行版本控制。

 

实际上,开发容器是一种功能齐全的开发环境,它可以被共享、进行版本控制、可重复使用、自文档化,并且只要在使用中,它就是最新的。开发容器就像拉面:只要加入热水就可以吃了。

 

本文也不是教程。构建一个完整的开发容器是一个持续迭代的过程,取决于具体的项目。相反,我将向读者介绍什么是开发容器、开发容器的使用,以及借助开发容器为团队构建可重用的开发环境是一种怎样的体验。

体验开发容器

 

为什么说容器是开发环境的未来?让我们来看一些可以体现开发容器优势的例子。

快速上手

 

最近,我加入了一个为期 6 个月的客户项目。像大多数开发大型旧项目的团队一样,他们在 README 和 Wiki 页上有一系列冗长的初始设置说明和脚本。和往常一样,部分指南的内容要么过时,要么自相矛盾。设置脚本的前提是它们要运行在一台特定时期推出的 MacBook 上,需要安装特定版本的 MacOS,并且这台笔记本将专门用于这个项目的开发。

 

这种令人困惑的、冗长的、过时的初始设置是我见过的大多数团队的常态。在加入项目的第一周,如果你能够运行项目的一部分测试套件,就算很好了!

 

在我加入这个团队的第一个项目中,我创建了一个开发容器配置,将所有这些文档的内容转化为可执行的配置。

 

为此,我创建了一些专门的 Docker 配置文件,与用于创建部署容器的 Docker 配置文件分开。它们位于项目代码库的.devcontainer 目录中。

.devcontainer/├── Dockerfile├── README.md├── devcontainer-load-profile.sh├── devcontainer.json├── docker-compose.yml├── entrypoint.sh├── init-once.sh├── init.sh└── profile.sh
复制代码

 

这里通常会包含一个 docker-compose 配置文件,定义了要启动哪些容器,以及它们如何相互连接并连接到宿主。

version: "3.2"services:app:  user: developer  build:    context: .    dockerfile: Dockerfile  volumes:    - type: bind      source: ..      target: /workspace    - type: bind      source: ${HOME}${USERPROFILE}/.ssh      target: /home/developer/.ssh  working_dir: /workspace  command: sleep infinity  environment:    BUNDLE_PATH: vendor/bundle    INTERFACE: "0.0.0.0"  ports:    - "3000:3000"
复制代码

 

通常还有一个 Dockerfile 文件,用于定制应用程序开发容器。

FROM ruby:2.7.2RUN apt-get update \&& apt-get install -y yarnpkg vim lsof \&& ln -s /usr/bin/yarnpkg /usr/local/bin/yarn \&& rm -rf /var/lib/apt/lists/*

COPY sixmilebridge-load-profile.sh /etc/profile.d/

ARG USERNAME=developerARG USER_UID=1000ARG USER_GID=$USER_UID

RUN groupadd --gid $USER_GID $USERNAME \ && useradd -s /bin/bash --uid $USER_UID --gid $USER_GID -m $USERNAME
复制代码

 

如果我们使用的是 VS Code,可以配置 devcontainer.json 文件。开发容器里还会有一些脚本文件,在容器生命周期的各个阶段发挥作用。

.devcontainer/├── Dockerfile├── README.md├── devcontainer-load-profile.sh├── devcontainer.json├── docker-compose.yml├── entrypoint.sh├── init-once.sh├── init.sh└── profile.sh
复制代码

 

在完成这个开发容器的定义后,我就把它全部提交到项目代码库中。

 

一开始,团队中的一些人说:“我们永远不会用它……你忙你的!”

 

然后,一件有趣的事情发生了。一些新员工用了开发容器,很快就上手开发了。来自另一个团队的一些人用开发容器在他们通常不参与的代码库上创建 PR,再也不需要花一周时间去设置开发环境了。慢慢地,开发容器已经成为我最受夸赞的贡献之一。

 

使用开发容器最明显和最直接的好处之一是简化了项目启动流程,而且并不只是对新员工来说是这样的,甚至连前端团队的员工都能够参与调整后端的应用程序代码。这可能意味着,在三年后,你能够快速修复 Bug。

共享基础工具

 

项目的配置检查清单和脚本很快就会过时,因为一旦我们在一台机器上设置好了项目,就会把它们忘得一干二净。而开发容器会定期进行重新构建。开发容器是可执行的文档,它包含了项目日常开发使用的库、服务、系统配置、开放端口、实用工具等。

 

例如,你的团队是否会使用 ngrok 将本地开发机器开发给远程用户?不要把这个设置说明些在 Wiki 页上,而是将它添加到开发容器中。

RUN wget https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip \  && unzip ngrok-stable-linux-amd64.zip \  && mv ngrok /usr/local/bin \  && rm ngrok-stable-linux-amd64.zip
复制代码

 

这样,所有使用开发容器的人都能在需要时获取到合适的工具。

vscode ➜ /workspace (main ✗) $ ngrok --versionngrok version 2.3.40
复制代码

任何人都可以随时运行所有的测试

 

在很多项目中,如果你能在本地运行单元测试,就会被认为做得很好——但只有 CI 系统能够提供运行集成测试所需的支持服务。在极端情况下,只有少数基础设施人员知道如何在系统测试失败时修复它们,而开发人员在这个时候却什么也做不了。

 

每个人都可以共享开发容器,也可以在 CI 中使用,我们可以提升我们的期望:每个人都可以随时运行所有的测试。虽然它们在 CI 管道中可能执行地更快,但保持集成测试通过率变成了每个人的事情。

 

开发容器能够为整个测试周期提供支持,因为它们能打包所有东西,不仅包括用于开发应用程序的虚拟微型计算机,还包括运行应用程序所需的支持服务。应用程序需要 Redis 服务器和安装了特定扩展的特定版本的 PostgreSQL?docker-compose 配置文件可以确保在开发容器启动时,这些组件都已经是可用的。

 

它甚至可以将 Postgres 专家对数据库的优化变成编码,这些优化可以提升开发数据库的响应性而非可靠性。

redis:  image: redis:${REDIS_VERSION:-6.0.9}postgres:  image: mdillon/postgis:${POSTGRES_VERSION:-9.6}  command: ["-c", "fsync=off", "-c", "full_page_writes=off", "-c", "synchronous_commit=off"]  environment:    POSTGRES_USER: ${POSTGRES_USER:-postgres}    POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-password}
复制代码

在项目之间进行干净的切换

 

说到这里,你是否曾经安装过特定的系统库或 PostgreSQL 版本来满足一个应用程序,结果却破坏了你正在开发的另一个应用程序?有了开发容器,你就可以在一台机器上的多个项目之间进行干净的切换。这对于顾问来说是必不可少的,但也适用于任何拥有多个项目代码库的组织。

 

说到项目切换,如果你习惯使用 Python、Ruby 或 JavaScript 等编程语言,就也习惯了使用 VirtualEnv、RVM 或 NVM 等版本管理器。这些工具可以同时构建、安装和管理多个版本的 Python、Ruby 或 Node,并确保每个项目使用正确的语言运行时版本。在这个过程中,它们增加了一个额外的间接层。它们会成为一个麻烦,因为当你需要应对一个你不熟悉的语言生态系统,它们可能会成为生产力障碍。

 

开发容器消除了对这些工具的依赖。因为一个开发容器专门用于一个项目,所以它可以全局安装指定版本的 Ruby、Python 或 JavaScript。如果运行时是从源代码编译的,那就可以将其整合到开发容器的 Dockerfile 文件中。自从我开始使用开发容器以来,还没有用过语言版本管理器,而且我并不怀念它们!

共享快捷技巧

 

随着时间的推移,许多项目团队发展出了一套“标准”的 shell 和 git 别名,缩短了常见操作命令的长度。其中一些是基本的别名,适用于所有项目,但有一些与一个团队如何开发应用程序有关。

alias gs="git status"alias be="bundle exec"
复制代码

 

通常情况下,这些东西由团队中的某些人分享出来,然后其他人慢慢采用它们。他们先是匹配代码,然后发现他们习惯使用的快捷技巧并不存在,这可能会令人感到不快。

 

这些快捷技巧的存在或不存在也可能导致团队微妙地分化为“酷孩子”(他们总是有最好的 shell 别名)和“不酷的孩子”。

 

如果任何人都可以立即为其他人添加有用的 shell 别名,会怎样?这个使用开发容器就能做到。我们不用在 Slack 中发布 shell 别名的清单,而是创建一个 PR,演示如何在 Slack 中使用它们。因为开发容器包含了一个共享的 UNIX 用户空间,所以你可以确保这些快捷技巧对每个人都有效。

更有效地调试

 

开发容器还提供了一些不太明显的好处。根据定义,容器是一个高度可控的环境,就像是显微镜下有一个微小的计算机网络。本文没有足够的篇幅来详细讨论,但我想说得是,开发容器让调试变得更加容易:监控应用程序的文件写入、网络 I/O,甚至是系统调用,这样就可以准确地了解它在干什么。

重现问题

 

当团队中的大多数人都在使用开发容器时,它的最大好处就会显现出来。你是否曾经遇到过团队中某个开发人员突然遇到了别人都没注意到的问题?最终,在进行了大量的故障排除之后,发现他收到的系统更新与项目所依赖的某个库不兼容。没有人知道怎样帮助他,因为其他人的电脑上没有这个问题!

 

开发容器可以大大减少这种“在我的电脑上没问题”的现象。没有什么东西可以保持每个开发人员的开发环境完全相同,但一个公共的容器定义可以消除大量潜在的变化。在知道了是哪个库更新破坏了项目,就可以很容易地修复它。因为在使用容器时,你可以根据需要获取工具和系统库的版本。

在云端编码

 

在有了开发容器的定义后,你就不局限于在“我的机器”上开发了!

 

使用Gitpod、Amazon Cloud 9JetBrains SpaceGitHub Codespaces等工具进行基于云的开发已经成为一种趋势,而且这种趋势只会越来越明显。

 


基于云的开发环境支持远程结对编程。只要有浏览器,你就能够在任何地方编写代码,即使你不小心把手提电脑包忘在火车上了。如果你有一个可以在本地执行的开发容器定义,也可以用它在云端启动一个 IDE。

 

开发容器也非常适用于开源工作。你有没有想过为开源项目做一点小小的贡献?但是,在拉取代码时,你意识到运行单元测试需要一个漫长而复杂的设置过程。于是,你放弃了,只能在他们的问题追踪器里提了个建议。

 

如果开源项目附带的开发容器可以立即提升业余贡献者的生产力,那会怎样?这可能会更容易吸引到业余贡献者!

 


VS Code 中的开发容器

 

让我们来讨论一下编辑器和 IDE。

 

IDE 开始添加一些特性来支持基于容器的开发。微软的开源编辑器 VS Code 绝对是这一趋势的领头羊。事实上,我对开发容器的很多想法,包括“开发容器”这个术语,都是受 VS Code 提供容器支持的方式的启发。越来越多的编辑器和 IDE 都添加了容器感知功能。

 

开发容器与支持开发容器的 IDE 的紧密集成可以帮助开发人员在开始项目时提升效率。例如,现在的项目中一般都会有针对代码库定制的 Lint 或格式化规则。在以前,新加入的开发人员需要自己安装 Lint 工具,并确保编辑器做了正确的配置。有了容器感知的编辑器配置,开发人员在第一次启动项目时 Lint 和代码格式化就已经可用了。

 

这并不是说开发容器就这样将你与使用完全相同的编辑器配置锁定在一起了。事实并非如此!例如,在使用 VS Code 时,开发容器可以包含一个基础的特定于项目的设置和插件,但你也可以在此基础上添加自己的设置、插件、配色方案、按键映射等。

 

这并不是说团队就只能使用一种编辑器。一个项目可以有支持多个支持开发容器的 IDE 的配置。你可以在开发容器中包含完整的 VIM 设置,包括编辑器本身!

开发容器不是部署容器

 

现在,没有什么工具或技术是万能的。稍后我将讨论一些你可能不想使用开发容器的情况。但在此之前,我想分享关于使用开发容器的一个最大的转折点。

 

我经常会听到这样的建议:“我们已经有了容器定义,为什么不能重用它?”或者完全相反:“开发容器这个东西并不适合我们,因为我们没有使用容器来部署我们的应用程序。”

 

我认为这两个观点都有同一个错误的前提:容器是用来部署的。这种误解是可以理解的。如果你已经在项目中使用了容器,这可能是因为这是你部署应用程序的一种方式。你甚至可能在持续集成基础设施中使用了容器。容器不就是用来装东西的吗?

 

的确,使用容器来部署应用程序是促进容器普及的一个应用场景。但是,不管你是否用容器来部署应用程序,开发容器都很有用!事实上,如果你只是将开发容器视为部署容器,会很容易错过它强大的功能。

 

事情是这样的:用于部署的容器与用于开发的容器有着非常不同的需求。事实上,针对部署容器的许多要求几乎与开发容器完全相反。

 

我们希望部署容器尽可能地小和精简。我们希望它们精简、快速和安全。这意味着要尽量减少不必要的库和工具,所以尽量使用基础镜像,如 Debian Slim,甚至是 Alpine Linux,它们去掉了普通 Linux 发行版中常见的 glibc 库。但是对于开发容器来说,它们需要提供一个完整、舒适的开发环境。这意味着一个像 Ubuntu 这样的 Linux 发行版,需要包含命令行工具、编译器、帮助文档和整个工具包!

 

对于部署,你希望最小化安全横截面。对于开发,你需要打开更多的端口来支持调试!对于部署,不得不与 Honeycomb 或 New Relic 这样的可观测服务通信。对于开发,你不希望向这些服务发送消息,你可能希望“伪造”或模拟一些外部服务。在部署过程中,你想要减少 Docker 构建的层数,而在开发过程中,你可能希望在不需要完全重建镜像的情况下快速添加增量的变更。

 

在很多方面,部署容器和开发容器的目标是相互对立的。这就是为什么当我面对一个新客户并开始构建一个开发容器时,通常会从头开始。我会构建一套全新的容器配置文件,从项目设置指令而不是从已有的 Dockerfile 开始。这为我提供了一个可移植的、可复制的开发环境,但不是为了部署。

 

但这并不意味着开发容器和部署容器的配置就不能共享一些共同的部分。因为本文的篇幅所限,这里不能再展开描述了。不过这里有一个提示:你可以从开发容器开始,并将其简化为部署容器,这比从部署容器开始并将其变成开发环境要容易得多。

关于开发容器的注意事项

 

综上所述,开发容器并不适用于所有项目。

 

我们所讨论的一切都是基于用 Docker 运行的容器。目前,大多数 Web 和企业应用程序都部署在基于 Linux 的服务器上,因此使用开发容器就等于是在接近生产环境的环境中做开发。Android 开发也是如此。但是,如果你的部署目标不是 Linux 或类似 Linux 的系统,你可能就不会想这么做。如果你的目标是 iOS 设备或 Windows 桌面,那么容器开发可能不是最佳选择。

 

此外,到 2021 年,基于 Docker 的桌面开发平台已经有了明显的发展。

 

在基于 Linux 的机器上运行 Docker 可能有着最好的体验,因为 Linux 是容器的原生主机。

 

但令人感到惊讶的是,现在的次优选择是 Windows。这是因为随着 Windows 的 Linux 子系统 2(或“WSL2”)的出现,Windows 现在可以原生运行 Linux。实际上,你可以直接从 Windows 存储库中选择你需要的 Linux 发行版,并直接从 Debian 或 Fedora 存储库中运行 Linux 二进制文件,不需要进行任何重新编译或模拟。

 

Docker Desktop on Windows 使用 WSL2 作为后端。这意味着 Windows 上的 Docker 容器可以有效地运行在原生 Linux 环境中,没有虚拟化性能损失。在我的使用过程中,它们很稳定,能够以原生的速度运行 Rails 项目。

 

MacOS 是以 BSD 为基础构建的,并不是 Linux,而且它没有与 WSL2 等价的工具。这意味着为了运行 Docker,需要进行一定程度的虚拟化。我不再用 Mac 做开发,但我从朋友那里听说,他们在使用 Docker 时遇到了一些古怪的问题,尤其是在文件 I/O 方面。

 

这该怎么办呢?幸运的是,这是一个众所周知的问题,Docker 和苹果公司都有意要去解决。事实上,在我写这篇文章时,Docker 宣布了一些针对 MacOS 性能的重大更新。如果幸运的话,这个问题将很快成为本文最过时的部分!

我们在容器里写代码

 

有一些技术,一旦成熟了,就会永久地改变技术的发展状态。

 

在我编程时,版本控制还没有被普遍接受。一些项目仍然通过定期压缩的代码副本来记录历史。在我后来的职业生涯中,版本控制变得越来越普遍。然后,持续集成从一种新颖的想法变成了一种行业标准。现在,分布式版本控制和持续集成成了桌面开发的筹码:我们几乎无法想象没有它们的软件项目会是什么样子。

 

2021 年,我们正处于容器发展的拐点。五年后,我们会对过去认为在第一次提交代码之前花几天时间安装开发者笔记本电脑是多么正常的事情而嗤之以鼻。但你不需要等那么久,只要稍加努力,你就可以让自己和团队从开发容器中获得好处。

 

你可以有一个可移植的、可复制的开发环境,它可以跟随你从一台机器搬到另一台机器,甚至到云端。你可以在一小时内让新员工上手,而不是几天。你可以更容易地为开源项目做出贡献。你可以确保在 CI 系统中运行的每一个测试也都可以在本地运行。你可以通过 GitHub 与你的队友分享你的开发配置和脚本。你可以通过将开发容器作为项目开发工作流程的一部分来实现这一切。

结论

 

所以,这就是为什么我认为你应该放下一切,为你当前的项目创建一个开发容器定义。不仅如此,你还应该使用开发容器并完善它,直到它变得像家一样舒适。你的合作者会感谢你,未来的你也会感谢现在的你。


作者简介:

 

Avdi Grimm 是一名程序员,著有几本颇受欢迎的 Ruby 编程书籍,并因对 Ruby 社区的贡献而获得 Ruby Hero 奖。他在 Graceful.Dev 上开设了面向对象设计、测试、重构、项目自动化等课程。

 

原文链接

Reproducible Development with Devcontainers

2022 年 7 月 05 日 09:353303

评论

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

原来一条select语句在MySQL是这样执行的《死磕MySQL系列 一》

咔咔

MySQL 数据库

Android SDK 之用户路径采集

神策技术社区

数据 路径规划 分析 行为数据

其实TCP聪明得很!详解TCP常见的五个异常处理场景

Java 编程 架构 程序人生 架构师

硬核分析 Java 内存 Cache 设计与最佳实践 - GuavaCache 篇

普普通通程序员

神策分析 iOS SDK 代码埋点解析 | 数据采集

神策技术社区

程序员 数据 代码 埋点

带你认识MRS CDL架构

华为云开发者联盟

数据库 大数据 FusionInsight MRS MRS CDL 实时同步

MySQL 系列教程之(十二)扩展了解 MySQL 的存储过程,视图,触发器

若尘

MySQL 数据库 8月日更

揭秘环境管理 Noah 的技术实现

Qunar技术沙龙

测试 Dev QA 环境 资源池

2021 年 8 月国产数据库排行榜:秋日胜春朝

墨天轮

数据库 TiDB oceanbase 国产数据库 达梦

写作——开启技术成长之路

神策技术社区

程序员 写作 日志

SphereEx CEO 张亮:数据库上云是大势所趋|初心·问

SphereEx

数据库 开源

FL Studio基本功能介绍

懒得勤快

LeetCode题解:28. 实现 strStr(),暴力法,JavaScript,详细注释

Lee Chen

算法 大前端 LeetCode

从 FFmpeg 性能加速到端云一体媒体系统优化

阿里云视频云

开源 ffmpeg 视频处理 视频流 视频云

神策分析 Web JS SDK 功能介绍

神策技术社区

程序员 代码 埋点

iOS SDK 架构解析

神策技术社区

程序员 数据 埋点

LeetCode刷题07-简单 整数翻转

ベ布小禅

8月日更

容器监控薅光了头发?这篇你再也不能错过!

观测云

json Docker 云计算 Linux 容器

神策 Android 全埋点插件介绍

神策技术社区

程序员 数据分析 埋点

Golang高并发:生产者消费者模型

Regan Yue

Go 语言 8月日更 生产者消费者模型

微信小程序图片流&本地图片转base64处理方案

页面仔小杨

微信小程序

图文并茂的聊聊ReentrantReadWriteLock的位运算

程序猿阿星

ReentrantReadWriteLock 位运算

拿捏!隔离级别、幻读、Gap Lock、Next-Key Lock

艾小仙

MySQL sql 面试 大前端

TronChain波场链智能合约开发详情|智能合约DAPP搭建

量化系统19942438797

智能合约 波场链

前端、后端、测试、研发经理必备技能-ApiPost接口管理工具

CodeNongXiaoW

大前端 测试 后端 接口工具

支持 10 亿日流量的基础设施:当 Apahce APISIX 遇上腾讯

Apache APISIX 中国社区

案例 API网关 APISIX Meetup 腾讯游戏

书单 | 无所不能的Python,从技术到办公,总有一款适合你!

博文视点Broadview

架构实战营 模块六作业

孫影

架构实战营 #架构实战营

架構實戰營 - 畢業設計

Frank Yang

架构实战营

烂大街的Spring循环依赖该如何回答?

Java spring 程序员 架构 面试

开发容器:可重用的开发环境_语言 & 开发_InfoQ精选文章