部署一个基于 Docker 的 Java 远程开发环境

远程开发的优势

  1. 统一的,预制的开发环境,非常方便修改和部署,新同事不用再花一整天配置开发环境;
  2. 独立的,专有的开发环境,不受本地环境或设备影响,居家办公不用重新配置开发环境;
  3. 集中的,可扩展的开发环境,可以最大化利用硬件资源,减少浪费降低研发投入;

配置介绍

  1. 本地设备系统 Windows 10;
  2. 服务器系统 Centos 7;
  3. Docker 版本 19.03.12;
  4. Visual Studio Code 版本 1.67.0。

部署步骤

创建镜像

  1. 首先新建 Dockerfile 并在镜像中安装 JDK 及一些必要的工具;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    FROM ubuntu:20.04

    RUN set -eux \
    && apt-get update \
    && DEBIAN_FRONTEND="noninteractive" apt-get -y install --no-install-recommends \
    ca-certificates \
    locales \
    tzdata \
    wget \
    curl \
    git \
    openjdk-17-jdk \
    openjdk-17-source \
    openssh-server \
    && rm -rf /var/lib/apt/lists/* \
    && mkdir /var/run/sshd

    EXPOSE 22

    CMD ["/usr/sbin/sshd", "-D"]
  2. 使用 build 命令创建镜像。

    1
    docker build -t codeserver-java:1.0.0-jdk-17 .

使用 Docker swarm 部署

  1. 在服务器以你的姓名部署目录;

    1
    mkdir -p ~/docker/stacks/yourname
  2. 上传本地的 Maven settings.xml 文件到部署目录,settings.xml 可以用于设置镜像仓库或内部仓库,如果不需要设置可略过;

  3. 上传本地的 .gitconfig 和 .git-credentials 到部署目录;

  4. 上传本地的 SSH 公钥到部署目录,如果没有则使用 ssh-keygen 命令创建一个;

  5. 初始化 Docker swarm;

    1
    docker swarm init
  6. 新建 dockers-compose.yml;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    version: "3.8"
    services:

    mymariadb:
    image: mariadb:10.4.12-bionic
    environment:
    MYSQL_ROOT_PASSWORD: my-secret-pw
    TZ: Asia/Shanghai
    volumes:
    - mariadb-data:/var/lib/mysql

    myredis:
    image: redis:6.0.1
    environment:
    TZ: Asia/Shanghai
    volumes:
    - redis-data:/data

    codeserver:
    image: codeserver-java:1.0.0-jdk-17
    environment:
    TZ: Asia/Shanghai
    ports:
    - "3600:22"
    configs:
    - source: id_rsa.pub
    target: /root/.ssh/authorized_keys
    mode: 0600
    - source: maven-settings.xml
    target: /root/.m2/settings.xml
    - source: gitconfig
    target: /root/.gitconfig
    - source: git-credentials
    target: /root/.git-credentials
    volumes:
    - workspaces:/root/workspaces
    - maven-repository:/root/.m2/repository

    configs:
    id_rsa.pub:
    file: ./id_rsa.pub
    maven-settings.xml:
    file: ./settings.xml
    gitconfig:
    file: ./gitconfig
    git-credentials:
    file: ./git-credentials

    volumes:
    mariadb-data:
    redis-data:
    workspaces:
    maven-repository:
    vscode-server-extensions:
    vscode-server-data:
  7. 部署 stack;

    1
    docker stack deploy -c docker-compose.yml yourname
  8. 检查服务是否启动成功,如果 REPLICAS 列为 1/1 则说明服务启动成功。

    1
    docker service ls

安装 Remote : SSH 扩展

打开 VS Code,在侧边栏选择 Extensions,在搜索框输入 “Remote - SSH”,分别安装搜索结果中的 Remote - SSH 和 Remote - SSH: Editing Configuration Files 扩展。

搜索 Remote - SSH 扩展

安装成功后在侧边栏会出现 “Remote Explorer” 菜单。

连接远程开发环境

  1. 修改 ~/.ssh/config 添加远程开发服务器信息

    1
    2
    3
    4
    5
    Host Codeserver
    Hostname 192.168.0.12
    Port 3600
    User root
    IdentityFile ~/.ssh/id_rsa
  2. 打开 VS Code,在侧边栏选择 Remote Explorer,SSH config 中的主机会被加载的到界面中,鼠标移动到上一步添加的 “Codeserver”,点击右侧出现的按钮后,VS Code 会在新窗口连接主机。配置中的 “Codeserver” 可以理解为是一个远程主机的别名,可以根据需要自行修改。

    准备连接

  3. 在弹出的新窗口上方提示框中选择 Linux,如果未设置公钥或公钥有误会弹出密码输入框,此时应检查公钥或进入容器设置密码。

    连接中

设置远程开发环境

  1. 打开 VS Code,在侧边栏选择 Extensions,在搜索框输入 “Extension Pack for Java”,点击 “Install in SSH: Codeserver”,这是一个 JAVA 开发的扩展包,点击后会安装 6 个 JAVA 开发相关的扩展。

    安装 Java 开发扩展

  2. 设置用户 settings.xml 路径。点击侧边栏最下方 Manage 菜单,选择 Settings,在搜索框入手 “Maven”,在 Java › Configuration › Maven: User Settings 选项中输入 “/root/.m2/settings.xml”,则文件已在 dockers-compose.yml 文件创建,部署后副本就存在于 /root/.m2/settings.xml。

    设置 Maven 配置路径

  3. 常用的 Java 开发扩展还有:

    • Quarkus
    • Spring Boot Tools
    • Spring Initializr Java Support
    • SonarLint
    • Lombok Annotations Support for VS Code

开发微服务

创建 Spring Boot 工程

  1. VS Code 连接到远程开发环境后,按快捷键 “Ctrl + Shift + p” 唤出命令面板,选择或输入 Java: Create Java Project;

    创建 Java 项目

  2. 选择 Spring Boot 项目,如果未安装 Spring Initializr Java Support 扩展会提示安装此扩展,安装后重新执行上一步或在命令面板选择或输入 Spring Initializr: Create Maven Project;

    创建 Spring boot 项目

  3. 依次按回车跳到选择依赖页面,搜索并添加 “Spring Web”,“Spring Data JPA”,“MariaDB Driver”,按回车继续,将项目保存到 /root/workspaces,点击 OK;

    选择依赖

  4. 项目生成成功后右下角会通知提示,点击 Open 即可打开项目;

    打开项目

  5. 首次打开项目会弹出提示框询问是否信任则文件夹内文件的作者,勾选信任上级目录 “workspaces” 下所有文件的作者,然后点击 Yes;

    信任目录

  6. Java 扩展开始导入项目并从 Maven 仓库下载依赖(虽然我们没有在镜像中安装 Maven,但是 Java 扩展的 M2Eclipse 模块能够完成构建、依赖管理等操作),如果设置了镜像仓库下载会很快完成,如果没有则需要等待一段时间;

    导入项目

编写代码

  1. 在 application.properties 文件中添加数据库配置;

    1
    2
    3
    4
    5
    spring.datasource.url=jdbc:mariadb://mymariadb:3306/school?createDatabaseIfNotExist=true
    spring.datasource.username=root
    spring.datasource.password=my-secret-pw

    spring.jpa.hibernate.ddl-auto=update

JDBC URL 中的 mymariadb 是在 Compose file 定义的 mariadb 服务,创建服务时会使用一个与定义服务时一样的名称作为网络别名供其它服务访问。

  1. 编写代码

    创建实体类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    package com.example.demo.model;

    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.GenerationType;
    import javax.persistence.Id;

    @Entity
    public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;

    private String sex;

    public Long getId() {
    return id;
    }

    public void setId(Long id) {
    this.id = id;
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    public String getSex() {
    return sex;
    }

    public void setSex(String sex) {
    this.sex = sex;
    }

    }

    编写 Repository

    1
    2
    3
    4
    5
    6
    7
    8
    package com.example.demo.repository;

    import com.example.demo.model.Student;
    import org.springframework.data.repository.CrudRepository;

    public interface StudentRepository extends CrudRepository<Student, Long> {

    }

    编写 Service

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    package com.example.demo.service;

    import com.example.demo.model.Student;

    public interface StudentService {

    Iterable<Student> findAll();

    Student save(Student student);

    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    package com.example.demo.service.impl;

    import com.example.demo.model.Student;
    import com.example.demo.repository.StudentRepository;
    import com.example.demo.service.StudentService;

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;

    @Service
    public class StudentServiceImpl implements StudentService {

    @Autowired
    private StudentRepository studentRepository;

    public Iterable<Student> findAll() {
    return studentRepository.findAll();
    }

    public Student save(Student student) {
    return studentRepository.save(student);
    }

    }

    编写 Controller,由于篇幅原因,此示例直接使用实体类作为 Controller 方法的参数,真实的开发为了规避可能的恶意攻击通常会使用 DTO 作为方法参数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    package com.example.demo.controller;

    import com.example.demo.model.Student;
    import com.example.demo.service.StudentService;

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;

    @RestController
    @RequestMapping("/students")
    public class StudentController {

    @Autowired
    private StudentService studentService;

    @GetMapping
    public Iterable<Student> finaAll() {
    return studentService.findAll();
    }

    @PostMapping
    public Student save(@RequestBody Student student) {
    return studentService.save(student);
    }
    }

运行微服务

打开 DemoApplication.java,点击 main 方法上方的 run 即可启动应用。

运行项目

测试微服务

打开 Ports 视图,输入 8080 端口后按 tab 键即可将远程的应用端口映射到本地。

转发端口

使用 Postman 添加一条数据

添加数据

使用 Postman 获取数据

获取数据

总结

通过以上操作可发现远程开发可以将配置和数据从开发环境中分离出来,一些常用的组件也可以和开发服务一起部署,可以起到方便维护管理开发环境的目的,通过 VS Code 扩展连接到远程开发环境的编码过程和体验与本地开发区别不大,远程开发最大的问题就是极度依赖网络,但是经我们测试,同一个城市网络延时基本不影响开发工作,毕竟我们只是在远程的机器上写代码,而不是在远程的机器上玩游戏。