JAVA 项目工程化实践
项目概述
公司前后端开发人员各两名。后端采用 Java 开发,主要使用 springboot + dubbo 技术栈;前端使用 vue 技术栈。项目使用前后端分离开发的方式。
技术栈
- 后端:Spring Boot + Dubbo
- 前端:Vue
- 部署工具:
- 前端:SFTP
- 后端:Docker Swarm + Portainer
- CI/CD:云端构建(使用 Dockerfile-Maven 插件)
项目架构与部署
架构设计
- 前后端分离:前端为 Vue 开发的静态页面,后端提供接口服务。
- 统一入口:使用 Nginx 作为服务的统一入口,基于 URL 路径分发请求到前端或后端。
前端静态页面直接放在 nginx html 目录;后端接口服务使用 docker 容器运行。使用多台阿里云 ECS 搭建 docker swarm 集群。
部署方式
- 前端部署:
- 静态页面直接放置在 Nginx 的 HTML 目录中。
- 使用 SFTP 进行文件传输。
- 后端部署:
- 使用 Docker 容器运行。
- 多台阿里云 ECS 搭建 Docker Swarm 集群。没有采用 Kubernetes,因为当前项目规模较小,Docker Swarm 更简单易用。
环境管理
项目环境
- 测试环境:用于内部测试和开发联调。
- 正式环境:线上用户实际使用的生产环境。
Git 分支管理策略
- 主分支
dev
:对应测试环境,不能直接提交代码,只能从其他分支合并。release
:对应正式环境,不能直接提交代码,只能从其他分支合并。
- 功能分支
feature/*
:新需求开发分支。每个独立功能需求对应一个feature
分支,基于最新的release
创建。- 开发完成后先合并到
dev
用于前端联调。 - 测试验证通过后合并到
release
上线。
- 开发完成后先合并到
- 修复分支
fix/*
:线上 Bug 修复分支。基于最新的release
创建。- 修复完成后先合并到
dev
提交测试验证。 - 验证通过后再合并到
release
上线。
- 修复完成后先合并到
- 分支生命周期
fix
和feature
分支在上线后即可删除。
项目构建方式
没有采用 Jenkins 或本地构建方案,而是选择云端构建 ,使用 dockerfile-maven-plugin
插件,在 Maven 打包时自动构建 Docker 镜像并推送到远程仓库。
关键配置 (Maven pom.xml
)
<properties><dockerfile.plugin.version>1.4.13</dockerfile.plugin.version><dockerfile.skip>true</dockerfile.skip><dockerfile.timestamp.skip>true</dockerfile.timestamp.skip><docker.images.tag>latest</docker.images.tag><docker.repository.prefix>registry-vpc.cn-shenzhen.aliyuncs.com/test</docker.repository.prefix><maven.build.timestamp.format>yyyyMMddHHmmss</maven.build.timestamp.format>
</properties><build><pluginManagement><plugins><!--为了统一管理各个maven模块的Dockerfile,这里通过maven-resources-plugin的copy-resources在maven的compile阶段将Dockerfile文件复制到target目录中。这里的resource配置的顺序是copy当前模块中的Dockerfile文件,再copy父级模块中的Dockerfile,因为copy-resources目标的overwrite属性默认为false,所以会优先使用当前模块的Dockerfile。--><plugin><artifactId>maven-resources-plugin</artifactId><executions><execution><id>copy-dockerfile</id><phase>compile</phase><goals><goal>copy-resources</goal></goals><configuration><outputDirectory>${basedir}/target/</outputDirectory><resources><resource><directory>./</directory><includes><include>Dockerfile</include></includes></resource><resource><directory>../</directory><includes><include>Dockerfile</include></includes></resource></resources></configuration></execution></executions></plugin><plugin><groupId>com.spotify</groupId><artifactId>dockerfile-maven-plugin</artifactId><version>${dockerfile.plugin.version}</version><executions><!-- 在build阶段创建image、tag、push --><execution><id>build-tag-push</id><phase>package</phase><goals><goal>build</goal><goal>tag</goal><goal>push</goal></goals><configuration><tag>${docker.images.tag}</tag></configuration></execution><!-- tag 一个以时间为版本号并push --><execution><id>tag-push-latest</id><phase>package</phase><goals><goal>tag</goal><goal>push</goal></goals><configuration><tag>${maven.build.timestamp}</tag><skip>${dockerfile.timestamp.skip}</skip></configuration></execution></executions><configuration><skip>${dockerfile.skip}</skip><repository>${docker.repository.prefix}/${project.build.finalName}</repository><contextDirectory>${basedir}/target</contextDirectory><verbose>true</verbose><!-- 注意在本地.m2/settings.xml中设置Registries账号和密码 --><useMavenSettingsForAuth>true</useMavenSettingsForAuth><buildArgs><ARTIFACT_NAME>${project.build.finalName}</ARTIFACT_NAME></buildArgs></configuration></plugin></plugins></pluginManagement>
</build>
项目构建脚本
#!/bin/bash
# 开头加上set -e,这句语句告诉bash如果任何语句的执行结果不是true则应该退出
set -e
# 项目名称
projectName="接口服务"
# git仓库地址
gitUrl="git@gitee.com:xxx/xxx.git"
# git 分肢
branch="dev"
# 本地工作空间路径
workspace="/home/build-xx"
# maven 参数
mvnParam=""echo "开始构建【${projectName}】"
starttime=`date +'%Y-%m-%d %H:%M:%S'`# 工作目录是否存在
if [ ! -d "$workspace" ]; thenecho "创建工作目录: $workspace"mkdir $workspaceecho "开始拉取代码...."git clone ${gitUrl} $workspace
fi# 进入工作空间
cd $workspace# 读入环境
read -p "请选择环境(dev,prod),默认[dev],回车跳过: " env
if [ -z "$env" ] || [ ${env} == "dev" ]; thenbranch='dev'
elif [ ${env} == "prod" ]; thenbranch="release"mvnParam="-Ddocker.images.tag=release -Ddockerfile.timestamp.skip=false"
elseecho "无效的环境,只支持dev,prod"exit 1
fimm="git checkout $branch"
echo "切换分支: $mm"
eval $mm# 拉取分支
mm="GIT_TRACE=2 GIT_CURL_VERBOSE=2 git pull origin $branch"
echo "将远程分支merge到当前分支: $mm"
eval $mm# 读入构建的maven模块
read -p "请输入需要构建的maven模块,默认全部构建,多个模块可以使用\",\"隔开;直接回车跳过:" modules
if [ -n "$modules" ]; thenmodules="-pl ${modules} -am"
fimm="mvn clean package ${mvnParam} -Dmaven.test.skip=true -Ddockerfile.skip=false ${modules}"
echo $mm
eval $mmendtime=`date +'%Y-%m-%d %H:%M:%S'`
start_seconds=$(date --date="$starttime" +%s);
end_seconds=$(date --date="$endtime" +%s);
echo "本次运行时间: "$((end_seconds-start_seconds))"s"
镜像标签规则
- 测试环境:Docker 镜像的 tag 为
latest
。 - 正式环境:Docker 镜像的 tag 为
release
,同时以构建时间(yyyyMMddHHmmss
)指定另一个 tag。
云端构建环境
直接在 docker swarm 中部署一个容器,容器需要包括的工具有:git、jdk、maven、docker-ce-cli。
Dockerfile
FROM centos:7
MAINTAINER "xx@126.com"
ENV TZ=Asia/Shanghai
WORKDIR /home
COPY ./jdk-17_linux-x64_bin.rpm jdk-17_linux-x64_bin.rpm
RUN rpm -ivh jdk-17_linux-x64_bin.rpm && rm jdk-17_linux-x64_bin.rpm
RUN curl -o /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-7.repo \&& curl -o /etc/yum.repos.d/docker-ce.repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo \&& yum makecache \&& yum install java-1.8.0-openjdk* git maven docker-ce-cli -y \&& yum install -y glibc-common kde-l10n-Chinese fonts-chinese \&& yum clean packages \&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \&& localedef -c -f UTF-8 -i zh_CN zh_CN.UTF-8&& echo "Asia/Shanghai" > /etc/timezone
# 设置默认语言环境
ENV LANG zh_CN.UTF-8
ENV LANGUAGE zh_CN.UTF-8
ENV LC_ALL zh_CN.UTF-8
ENTRYPOINT ["/sbin/init"]
构建命令
docker build -t registry-vpc.cn-shenzhen.aliyuncs.com/xx/java-server-build:release .
应用部署
构建环境部署(Docker Compose)
version: "3.7"
services:build:image: registry-vpc.cn-shenzhen.aliyuncs.com/xx/java-server-build:releaseworking_dir: /home/bin/volumes:- /var/run/docker.sock:/var/run/docker.sock # 将宿主机上的 Docker 套接字(socket)文件挂载到容器中- /java-server-build/.m2:/root/.m2/ # maven settings.xml 和 repository 目录- /java-server-build/.ssh:/root/.ssh/ # git 拉取代码的密钥信息- /java-server-build/bin:/home/bin/ # 项目构建脚本deploy:mode: replicatedreplicas: 1resources:limits:cpus: '2'memory: 4greservations:cpus: '0.5'memory: 200M
应用服务部署(Docker Compose)
version: "3.7"
networks:xx-net: external: true
services: biz:image: registry-vpc.cn-shenzhen.aliyuncs.com/xx/api-server:latestcontainer_name: api-servernetworks:- xx-netdeploy:replicas: 1update_config:parallelism: 1delay: 5sorder: start-firstresources:limits:cpus: '1'memory: 1greservations:cpus: '0.25'memory: 700Mrestart_policy:condition: on-failuredelay: 5smax_attempts: 2window: 90sdepends_on:- redishealthcheck:test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]interval: 30stimeout: 2sretries: 3start_period: 60senvironment:- SPRING_PROFILES_ACTIVE=docker-dev
网关(nginx)部署(Docker Compose)
version: "3.7"
networks:xx-net:external: true
services:nginx:image: nginx:1.19.2ports:- target: 80published: 80protocol: tcpmode: host- target: 443published: 443protocol: tcpmode: hostrestart: alwayscontainer_name: nginxlogging: options: max-size: "2g" networks:- xx-netdeploy:resources:limits:cpus: '1'memory: 2greservations:cpus: '0.5'memory: 50Mmode: globalvolumes:- /nginx/nginx.conf:/etc/nginx/nginx.conf:ro- /nginx/uc/conf.d:/etc/nginx/conf.d:ro- /nginx/html:/etc/nginx/html:roenvironment:TZ : 'Asia/Shanghai'