刚接触 CI/CD 或者被"手动打包、上传、重启、盯着日志"这套流程折磨过的人,这篇文章就是给你准备的。项目标题里的"Jenkins+Docker"是当前 Java 后端部署最主流、也是上手成本最低的一套组合,核心就一件事:把代码从提交到运行整个链路自动化,你只需要 push 代码,剩下交给流水线。这篇我直接给出一套能在真实服务器上落地的最简流程,不绕弯子,不说废话,先讲清楚为什么要这么搭,再逐步拆解每一步的配置和实操,最后把容易踩的坑和排查思路一起整理出来。
很多人看到 Jenkins、Docker、Pipeline 这几个词就觉得复杂,实际拆开看,链路就这么短:开发机 push 代码到代码仓库,Jenkins 感知到变更后拉取源码,在项目里用 Dockerfile 构建镜像,跑容器启动,完事。没有 K8s,没有微服务编排,纯粹的单机或少量服务器部署场景,这套最省心。
1. 整体思路拆解:为什么选 Jenkins+Docker 而不是手动部署
先聊为什么不用传统的"打包-上传-重启"方式。我自己早期部署 SpringBoot 应用是这样的:本地执行mvn clean package,然后 scp 上传 jar 到服务器,再登录服务器执行kill -9杀掉旧进程,最后nohup java -jar启动。单机部署一个服务还好,一旦有两个、三个服务需要发版,这套流程的痛点会被迅速放大。
痛点集中在几处:一是操作链路长且无记录,每次发版执行过哪些命令全凭记忆,要么靠聊天记录,要么靠终端滚动屏;二是发布动作完全依赖人,谁发布、发布哪一版代码、发布是否成功,没有统一入口;三是回滚成本高,一旦发布的新版本有问题,你得找到旧包、确认旧包还能用、重新上传启动,整个过程手忙脚乱;四是环境差异,在开发机打包出来的东西,到生产服务器上因为 JDK 版本、Tomcat 内嵌版本或操作系统差异,可能起不来。这里最典型的例子就是你在本地用 JDK 17 编译的 class 文件,放到 JDK 8 的服务器上直接报 UnsupportedClassVersionError。
Jenkins+Docker 这套组合把上面的问题一次解决。Jenkins 承担的是流水线中枢的角色,它负责拉取代码、执行构建、调用 Docker;Docker 承担的是运行时标准化角色,把应用连同 JDK、环境变量、启动参数全部固化到镜像里,保证任何一个装了 Docker 的服务器上构建出来的运行环境完全一致。
用一个生活化的类比:手动部署就像你每次搬家用纸箱零散打包,每次搬到新家都要重新分类、拆箱、摆放,出错了还不知道哪一箱的东西放错了地方;Jenkins+Docker 则是建立了一套标准集装箱方案,代码、运行环境、启动命令全部封在镜像里,Jenkins 是那个负责运输调度的人,任何一个环节出错都能追溯到具体操作日志。
这套方案的好处总结下来就是:推送代码即触发发布;构建和部署全程脚本化,任何人操作结果都一致;回滚时直接换镜像版本重启容器,几秒钟完成;服务器不需要安装 JDK 和 Maven,只需要 Docker 运行环境,因为 Jenkins 会自动在容器或内置环境里完成编译和镜像创建。这个"无需在生产环境安装 JDK"的效果,很多人第一次体验时会觉得非常舒服,因为生产机上的软件管理一下子从"装一堆工具链"变成了"只跑容器"。
2. 服务器环境准备:从零安装 Docker 与 Jenkins
2.1 Docker 安装与基础配置
不管 Jenkins 装在哪台机器上,第一步都是准备 Docker。这里以 Linux 服务器为例,操作系统普遍是 7.x 或 8.x 系列。安装方式很多,常见的是通过官方脚本或直接配置 yum 源安装。我更推荐直接使用系统自带的包管理工具安装,因为后续维护升级方便,而且能自动处理依赖。
安装过程我就不贴长串命令了,只说几个关键点:安装完成后,必须把当前登录用户加入 docker 用户组,这样后面执行 docker 命令时不需要每次都 sudo。这一步很多人会忽略,然后后面配置 Jenkins pipeline 时发现权限问题,排查老半天。简单执行:
sudo usermod -aG docker jenkins如果是普通用户操作 docker,就改成自己的用户名。执行完需要重新登录会话才能生效,这是第一个容易踩的坑。
启动 Docker 服务并设置开机自启,命令是sudo systemctl enable docker --now,检查是否安装成功可以跑docker version,看到 Client 和 Server 两段信息都正常,就说明 Docker 引擎已经可用了。
这里多说一句镜像加速的问题,国内服务器拉取 Docker Hub 镜像经常超时,如果遇到 pull 镜像特别慢或者直接失败,需要去 Docker 守护进程配置文件里添加 registry-mirrors 配置。改完后执行sudo systemctl restart docker重启生效。具体地址实际使用中经常变化,我之前用过的几个后面失效了,这里重点是提醒你保留这个处理方式的认知,遇到问题知道从哪里入手,不要卡在这一步。
2.2 Jenkins 的安装方式选择
Jenkins 最早的安装方式是下载 war 包扔进 Tomcat 运行,或者用官方 yum 源安装到系统里。现在已经不是最佳选择了,我推荐直接用一个独立容器跑 Jenkins,原因很现实:Jenkins 本身依赖的 JDK 版本、插件兼容性、配置文件散落位置,用传统方式装很容易污染宿主机环境。用容器装,Jenkins 的运行环境是隔离的,升级和迁移都简单。
启动 Jenkins 容器的方式,核心命令如下:
docker run -d \ --name jenkins \ -p 8080:8080 \ -p 50000:50000 \ -v /home/jenkins_home:/var/jenkins_home \ -v /var/run/docker.sock:/var/run/docker.sock \ -v $(which docker):/usr/bin/docker \ --restart=always \ jenkins/jenkins:lts-jdk17这条命令有几个地方值得细讲。
-v /home/jenkins_home:/var/jenkins_home是把 Jenkins 的数据目录挂载到宿主机。Jenkins 的配置、插件、构建记录都在这,如果不挂载,容器一删数据全没。我见过有人在测试环境没挂载数据目录,容器重启后整个 Jenkins 配置全丢,教训深刻。
-v /var/run/docker.sock:/var/run/docker.sock是让 Jenkins 容器可以通过 Docker 主机的套接字调用宿主机 Docker 进程。这句话是整个 Jenkins+Docker 玩法里最核心的机制。简单解释,容器里运行 docker 命令时,虽然二进制文件是容器内的,但实际执行动作用的还是宿主机 Docker 引擎的能力。
-v $(which docker):/usr/bin/docker是把宿主机的 docker 命令挂载进 Jenkins 容器,这样 Jenkins 容器内部才能执行 docker 相关命令。如果你不挂这个文件,进入 Jenkins 容器执行docker version会直接提示 command not found。
-p 50000:50000这个端口是 Jenkins 用于和 Agent 节点通信的,单机使用不涉及构建代理的话可以不映射,但建议保留,后续如果增加构建节点会用到。
启动完成后,浏览器访问http://服务器IP:8080,会看到 Jenkins 的解锁页面。初始管理员密码在容器日志里,执行docker logs jenkins可以看到一段随机生成的 key,或者在挂载目录的secrets/initialAdminPassword文件里查看。拿到密码解锁后,选择安装推荐插件即可。插件安装会耗时几分钟,耐心等一下。后续还需要在 Jenkins 插件管理里额外安装 Maven Integration、Pipeline、Docker Pipeline、Git Parameter 等插件,推荐插件列表里不一定包含全部,建议确认一下。
2.3 全局工具与凭据配置
Jenkins 核心管理配置里需要准备几样东西:JDK、Maven、Git 凭据和 Docker Hub 或私有仓库凭据。
JDK 和 Maven 可以在 Jenkins 的 Global Tool Configuration 里配置自动安装,也可以用已存在的路径。如果 Jenkins 容器内部没有 JDK 和 Maven,你可以在容器里执行apt-get install安装,或者更省事的方式是使用 Jenkins 的自动安装功能。自动安装需要服务器能联网下载,选择版本号后保存即可。实践中我更推荐容器方式跑一个含 JDK 和 Maven 的 Jenkins 镜像,或者直接在 Global Tool Configuration 配置挂载到容器里的 JDK/Maven 路径,这个路径是容器内部的路径,不是宿主机路径,这个区分经常让人混淆。
凭据管理是自动化流程里永远绕不开的一环。你需要把代码仓库的账号密码或 SSH 密钥存进 Jenkins 凭据中心,这样流水线里拉代码时不需要手动输入密码。同样的,推送到镜像仓库的账号密码也要存进去。在 Manage Jenkins 的 Credentials 里添加 Username with password 类型的凭据即可。
凭据就像一个保险柜,流水线脚本里通过credentials('凭据ID')引用,不会在日志里暴露明文密码。这一点非常重要,不要把密码硬编码到 pipeline 脚本里,否则 Jenkins 的日志会把密码打印出来,等于把服务器钥匙贴在大门上。
3. SpringBoot 项目的构建配置:Dockerfile 与镜像仓库准备
3.1 编写项目 Dockerfile
环境准备好之后,回到代码项目本身。SpringBoot 应用要 Docker 化,关键是写一份合适的 Dockerfile。SpringBoot 项目打包产物就是一个可执行 jar,所以 Dockerfile 的核心任务就是"把 jar 放进去,告诉容器怎么启动"。
一个最简的 Dockerfile 内容如下:
FROM openjdk:17-jdk-alpine MAINTAINER devops@example.com WORKDIR /app COPY target/*.jar app.jar EXPOSE 8080 ENV JAVA_OPTS="" ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]这里有几个点要展开讲。
基础镜像选择openjdk:17-jdk-alpine,alpine 版本体积小,适合在生产环境使用。注意 SpringBoot 应用实际编译用的是 JDK 17,如果你的代码是基于 JDK 8 写的,就换成openjdk:8-jdk-alpine,保持一致即可,否则运行时会因为字节码版本问题直接崩溃。
COPY target/*.jar app.jar这行依赖上一阶段 Maven 构建的输出。也就是说,先跑 Maven 打包出 jar,再构建 Docker 镜像。如果 Jenkins 里 Maven 构建失败,后续镜像构建就不会执行。
ENV JAVA_OPTS=""用环境变量预留调整 JVM 参数的入口。部署过程中需要调整堆内存、GC 参数时,不用重新构建镜像,通过docker run -e JAVA_OPTS="-Xmx512m"直接传参,这种灵活性在调试阶段特别有用。
如果你想让镜像更小,可以用多阶段构建,把 Maven 构建和运行时镜像分开。第一阶段用 maven 基础镜像编译项目,第二阶段只从第一阶段拷贝 jar 文件。这样最终的运行镜像里没有 Maven 和编译残留,体积能减掉不少。但要注意多阶段构建在 Jenkins 里的构建时间会变长,因为每次都要重新下载 Maven 依赖,具体取舍看你们团队的实际情况。
Dockerfile 写好后,本地可以先手动构建一次验证:docker build -t demo-app:test .,构建成功后跑一个容器测试接口是否正常响应。这一步在本地验证好,能避免后面 Jenkins 链路出问题时分不清是代码问题还是流水线配置问题。
3.2 镜像仓库选型与推送准备
镜像构建完成后要推送到一个仓库,Jenkins 部署时从仓库拉取。可选方案是 Docker Hub 私有仓库和自建 Registry。Docker Hub 的私有仓库对个人项目比较方便,但国内服务器拉取时网络不稳定;自建 Registry 适合内网环境,部署速度快。
如果选 Docker Hub,在 Jenkins 里配置一个 docker 凭据即可。如果自建 Registry,需要考虑配置 HTTP 访问或证书信任问题,这个细节很多人会卡,我后面在常见问题里细说。
推到 Docker Hub 的流程是:本地或管道内执行docker login,然后docker tag 镜像名 用户名/仓库名:tag,再docker push 用户名/仓库名:tag。Jenkins 流水线里会用到凭据引用,避免明文密码:
withCredentials([usernamePassword(credentialsId: 'docker-hub', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS')]) { sh "echo '$DOCKER_PASS' | docker login --username $DOCKER_USER --password-stdin" }4. Jenkins Pipeline 核心实现:一键自动化部署
4.1 创建流水线任务
现在到了最核心的部分。在 Jenkins 首页点击新建任务,填写任务名,选择类型为 Pipeline。不建议选 Freestyle project,Freestyle 的配置项虽然在界面里排列得很直观,但很多配置无法代码化,迁移和备份都不方便。Pipeline 的脚本即代码,全流程写入 Jenkinsfile 可以直接跟着代码走,版本可追溯,换一台 Jenkins 服务立刻能把任务重建起来,你只需维护代码仓库里的 Jenkinsfile 文件就行。
在 Pipeline 配置页面的 Pipeline 区域,Definition 选择 "Pipeline script from SCM",SCM 选择 Git,填写仓库地址和凭据,Branches to build 填*/main或*/master。Script Path 默认是Jenkinsfile。保存即可。
这里有一个值得注意的点。很多人第一次配置时,不知道 Jenkinsfile 是可以和代码一起版本管理的。Script Path 就是指代码仓库根目录下的 Jenkinsfile 文件。这意味着流水线的逻辑变更不需要去 Jenkins 界面里手工修改,直接在代码仓库里改 Jenkinsfile,提交后 Jenkins 执行的就是新逻辑。这份文件沉淀到项目仓库里,团队任何人都能 review 部署逻辑,这是 Freestyle 风格完全做不到的。
4.2 Jenkinsfile 最小可用版本
下面给一个真正可用的 Pipeline 脚本,这是整套流程的骨架。代码里每一段都有它必须存在的理由,我按阶段拆解。
pipeline { agent any environment { DOCKER_REGISTRY = 'your-registry-address' IMAGE_REPO = 'demo-app' IMAGE_TAG = "${BUILD_NUMBER}" CONTAINER_NAME = 'demo-app-container' PORT_MAPPING = '8080:8080' } stages { stage('拉取代码') { steps { checkout scm } } stage('Maven 构建') { steps { script { def jdkTool = tool name: 'JDK17', type: 'jdk' def mvnTool = tool name: 'Maven3', type: 'maven' sh """ export JAVA_HOME=${jdkTool} export PATH=${jdkTool}/bin:${mvnTool}/bin:\$PATH mvn clean package -DskipTests """ } } } stage('构建并推送镜像') { steps { script { sh "docker build -t ${DOCKER_REGISTRY}/${IMAGE_REPO}:${IMAGE_TAG} ." withCredentials([usernamePassword(credentialsId: 'docker-hub', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS')]) { sh "echo '$DOCKER_PASS' | docker login --username $DOCKER_USER --password-stdin ${DOCKER_REGISTRY}" sh "docker push ${DOCKER_REGISTRY}/${IMAGE_REPO}:${IMAGE_TAG}" } } } } stage('部署容器') { steps { script { sh """ docker stop ${CONTAINER_NAME} || true docker rm ${CONTAINER_NAME} || true docker run -d \ --name ${CONTAINER_NAME} \ -p ${PORT_MAPPING} \ -e JAVA_OPTS='-Xmx512m' \ ${DOCKER_REGISTRY}/${IMAGE_REPO}:${IMAGE_TAG} """ } } } } post { success { echo '部署成功,应用已启动' } failure { echo 'Pipeline 执行失败,请检查构建日志' } } }逐段说下设计意图。
environment这块把镜像仓库地址、镜像名、tag、容器名等变量统一收拢。tag 用BUILD_NUMBER,也就是 Jenkins 每次构建的递增序号,这样每次部署的镜像版本都是唯一的,很方便回滚时定位指向哪次构建。
第一个阶段checkout scm是最基础的步骤,从配置的 Git 仓库拉取当前分支代码。如果你的项目用分支策略区分环境,比如 develop 分支对应测试环境、main 分支对应生产环境,后续可以扩展成按分支参数化构建,多分支 Pipeline 是另一个话题。
第二个阶段 Maven 构建,需要注意的事比较细腻。工具链选择这里我用tool关键字引用全局工具配置里已配置的 JDK 和 Maven,然后显式设置JAVA_HOME和PATH。很多人第一次跑 Jenkins 流水线会遇到 Maven 构建时提示找不到 JAVA_HOME 或 mvn 命令不存在,往往就是环境变量没有正确设置。每个 sh 步骤行尾的\是换行拼接,\$PATH中反斜杠转义是为了让 Jenkins 在 Groovy 字符串插值之后,由 shell 自己解析原来的 PATH 变量。
-DskipTests跳过了测试,如果你的工程已经配套了完善的自动化测试,建议改成-Dmaven.test.failure.ignore=false而不是直接跳过测试,否则测试失败不会打断发布,等于给你埋了个大雷。当然在最简流程里,跳过测试是简化链路的一种方式,实际项目大家按需调整。
第三个阶段是最有 Jenkins+Docker 风格的一段。构建镜像直接在工作空间执行docker build,它会自动读取当前目录下的 Dockerfile。构建完成后,用凭据中心保存的账号密码登录镜像仓库,然后 push。这里的登录动作每次构建都会执行一次,逻辑上是有些冗余,但好处是简单可靠,不会出现凭据因为时间过期导致的推送失败。
第四个阶段是部署。这里的策略非常直接粗暴:先停掉旧容器,删除旧容器,再用新镜像启动新容器。|| true的作用是,如果当前没有运行的容器导致 docker stop 或 docker rm 报错,脚本不会因为非零退出码直接终止,而是继续往下走。这在"首次部署"和"后续滚动升级"两种场景下都适用。
如果你对快速找回旧版本有要求,可以在删容器之前把当前运行的镜像 tag 记下来,例如执行docker inspect获取当前镜像信息,回滚时直接指定旧 tag 再次部署即可。
4.3 从提交到部署的完整时序
Pipeline 配置好之后,用户视角看整个流程是这样的。开发者在本地写完代码,提交并 push 到 Git 仓库。Jenkins SCM 配置的轮询或 webhook 机制感知到有新提交,触发流水线执行。流水线依次执行拉取代码、Maven 构建、镜像构建与推送、容器部署。最后 Jenkins 界面展示蓝绿状态条,看到蓝色代表构建成功,红色则代表失败并输出失败日志。
触发方式默认是 Jenkins 定时轮询远端仓库,默认最小间隔是 1 分钟。更快的方式是配置代码仓库的 Webhook,提交代码后立即触发,这需要代码仓库平台支持同时 Jenkins 可以被外部访问。对初学者来说,轮询就够用了,等流程跑顺了再上 Webhook。
这条链路跑通之后,你会发现发布这个动作的性质变了——之前发布是一个"惊险操作",现在发布是一个"可重复的标准动作"。同一份代码,无论谁来触发,无论哪一台配置好的机器来执行,最终产物是一致的,应用启动后的行为是一致的。
5. 常见问题与排查技巧实录
这部分内容全部来自实操中的踩坑记录,整理了最常碰到的几类问题,每一项我都告诉你问题长什么样,以及怎么定位。
首先是最常见的"Jenkins 容器内 docker 命令不存在"。现象是流水线执行到 docker build 阶段报错,错误消息类似docker: command not found。原因基本就是启动 Jenkins 容器时没有挂载宿主机 docker 二进制。解决办法就是重新创建 Jenkins 容器,把-v $(which docker):/usr/bin/docker加上。这里补充一个细节,有些新版 docker 命令是动态链接,容器里可能还缺少依赖库,执行会报cannot execute binary file,这种情况下需要挂载整个 docker 客户端相关的库目录,或者换用挂载 Docker.sock 配合容器内附加 docker client 的方式。
第二个高频问题是在 Jenkins 宿主机的端口冲突。容器启动时提示:
docker: Error response from daemon: driver failed programming external connectivity或者端口占用错误。因为 SpringBoot 默认监听 8080,如果你同一台服务器上还有另一个服务也占用了 8080,部署会直接失败。处理办法有两个方向:要么改 PORT_MAPPING 把宿主机端口换成别的,比如8081:8080;要么把旧容器先停掉再启动新容器。推荐在早期就把端口规划好,避免多个服务互相冲撞。
第三个问题是镜像仓库是自建的 HTTPS 或 HTTP 信任。如果你用自建 Registry 且是 HTTP 协议,Jenkins 推送镜像时会报:
http: server gave HTTP response to HTTPS client这是因为 Docker 默认要求 Registry 走 HTTPS。解决办法是在 Docker 守护进程配置文件里,insecure-registries中把仓库地址加入白名单。注意这是配在宿主机 Docker 的/etc/docker/daemon.json中,然后重启 docker 服务,很多人在 Jenkins 界面里找配置项,方向完全错了。
第四个问题是时区不对。容器启动的 Java 应用打印日志时间总是比北京时间早 8 小时,这是容器默认使用 UTC 时区。解决办法是在 docker run 时增加-e TZ=Asia/Shanghai,或者在 Dockerfile 里设置时区。我通常两者都加,确保万无一失。
第五个问题是构建产物没有更新。改了代码重新构建,启动后发现还是旧逻辑。这类问题极大概率出在 Maven 缓存上。回滚思路是:看 Jenkins 控制台输出 Maven 阶段是否有 UP-TO-DATE 或 SKIPPED 字样,如果依赖没变但代码变了,要确认mvn clean package是否真的执行了 clean。另外就是确认 Dockerfile 的 COPY 路径里拿到的是不是这次构建的新 jar。我也踩过快照版本依赖不更新引发的"伪旧包"问题,这属于业务侧依赖管理范畴,建议发布前在测试环境做一次完整验证。
第六个是 Jenkins 工作空间磁盘占满。SpringBoot 项目的 target 目录、Jenkins 的构建记录、Docker 的镜像层叠加,时间久了容易把磁盘塞满。做好定期清理docker system prune -f或清理 Jenkins 旧构建记录,这是运维习惯问题,但会直接影响构建稳定性。
第七个是docker build慢的问题。每次构建都要重新下载 Maven 依赖,这个时间可能比代码编译还长。常见的方式是在本地 maven 仓库上做镜像或挂载共享存储,也可以配置流水线缓存机制,例如用-v /root/.m2:/root/.m2挂载 Maven 本地仓库到宿主机。这个方案最直接,效果也最明显,第二次构建的依赖下载时间会大幅缩短。
我把上面几个高频问题和对应处理方案整理成速查表:
| 问题现象 | 根因 | 处理方式 |
|---|---|---|
| docker command not found | Jenkins 容器未挂载 docker 命令 | 重新创建容器并挂载 docker 二进制与 docker.sock |
| 端口占满导致容器启动失败 | 宿主机端口冲突 | 调整端口映射,先停旧容器再启新容器 |
| 推镜像报 HTTPS 错误 | 自建仓库未配置信任 | 在 Docker 守护进程配置 insecure-registries |
| 应用日志时间晚了 8 小时 | 容器时区为 UTC | docker run 时加 TZ=Asia/Shanghai |
| 构建的镜像仍是旧代码 | Maven 缓存或 COPY 旧 jar | 确保执行 clean,镜像构建前清理 target |
| 构建越来越慢 | 依赖和镜像层缓存失效 | 挂载 Maven 本地仓库,清理无用镜像 |
最后说一个从部署效率角度很有用的技巧:部署阶段不要总是彻底删除容器再新建,可以改成先拉镜像,然后用docker-compose up -d或docker service update这类方式滚动更新。最简流程里用 stop/rm/run 这套是因为它直白、问题容易定位。我实际用久之后,会倾向于在流水线里判断老容器镜像与当前要部署的镜像是否一致,一致就直接跳过重启,不一致才更新,这样可以节省非常多的时间。
另一个从安全性角度的建议:确保 Jenkins 的权限控制合理。Jenkins 如果不做配置,默认是任何登录用户都有管理权限。建议在系统管理中开启基于角色的授权策略,普通开发人员只给 Job 执行与查看权限,管理员才给系统配置权限。流水线里的凭据也按需最小化授权,避免把生产服务器的 Docker 能力暴露给所有账号。这个问题在团队协作时尤其重要,你给一个人开了登录权限,就等于可能给了整个部署链路的核心控制权,权限收得越紧,容错空间越大。
这条自动化的部署链路,前前后后我帮不同团队搭过很多次,也给几个内部项目从手动部署迁移过来。每次走完这套流程,大家的普遍反馈都是"以后发版不用再提心吊胆了"。如果你正在经历手动部署的痛苦,照着这篇文章的路径走一遍,把 Jenkins+Docker 这条链路在自己机器上跑通,你会明显感觉到发版这件事开始变得省力和规范。第一次搭建的时候不用追求一步到位,能满足"代码提交后自动完成构建、镜像、部署"就算成功,后面的细节优化和更复杂的发布策略,都是在跑通这条主线之后慢慢迭代的事。