前言
上期讲了如何在windows平台搭建Java后端的开发环境,并给出了一个简单的hello world级别的多模块代码示例。但上期仅仅是在IDEA中运行,和正式的生产环境完全不同。
本期将讲解,如何配置SpringBoot多模块项目的maven打包,并分离出lib和resource。
真实的项目,不可能是运行在IDEA里的。实际的生产,一般有两种模式。一则把写好的项目打包成Jar包,通过命令启动jar包;另一种则是把项目打包成一个docker镜像,使用镜像启动。我们这里先讨论Jar包启动的情况。
一、打包配置
1.1 默认情况
我们直接在根目录点击package,会在各自模块生成一个target的文件夹,里面就有我们打包好的包。
我们随便找一个包,打开看看,会发现我们打的包异常的小,只有几十k。
拿我们的主业务模块general-test来说,包只包含3个模块,我们的代码,maven相关配置以及静态资源。application.yml放在最外层。
毫无疑问,这样的包是无法直接通过java -jar命令运行的。
1.2 springboot 打包插件
在根pom中,添加打包插件的版本引用
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
</plugin>
</plugins>
</pluginManagement>
</build>
而后,在general-test的pom下,添加如下内容
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
再次clean,package后,观察现象。我们首先可以看到,包的大小已经正常了
使用winRar打开,可以发现如下目录
org中,放了SpringFramework的相关内容,
META-INF放了maven的pom引用以及jar包服务信息
BOOT-INF下结构如下
lib放置了项目引用需要的包,包括自己写的core-common,core-enum-memo
而classes下,则放着本包的代码。
我们在打好包的位置,单击右键,打开命令行。
java -jar general-test-1.0.0.jar
即可启动服务器。
如果出现找不到favicon.ico的错误,可以在resources/static下放一个你喜欢的美少女头像,记得转成ico格式哦。
1.3 资源和配置文件的分离
2.2这样做,我们就会发现一个问题,配置文件被打到jar包里,运维人员无法轻易的修改配置文件。那样,配置文件的意义在哪里呢?还不如直接写到Java文件里。
在生产上,一般有2种做法。一种是使用配置中心,不过这要整上一套SpringCloud的东西(当然用K8S也可以实现)。我们这里讲一个简单的方法,通过修改打包方式来解决。
首先,在Resource中,把想要分离的文件拷出去:
<build>
<resources>
<!--打包时,把这些文件拷贝到外面-->
<resource>
<directory>src/main/java/indi/zhifa/study2024/class002/busy/generalTest/business/report</directory>
<includes>
<include>**/*.xml</include>
</includes>
<targetPath>${project.build.directory}/resources/mybatis</targetPath>
</resource>
<resource>
<directory>src/main/resources/static</directory>
<includes>
<include>**/*.*</include>
</includes>
<targetPath>${project.build.directory}/resources/static</targetPath>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>*.yml</include>
<include>*.properties</include>
</includes>
<targetPath>${project.build.directory}/config</targetPath>
</resource>
<!--打包时,为了idea能启动,还要向classes里拷贝一份-->
<resource>
<directory>src/main/java/indi/zhifa/study2024/class002/busy/generalTest/business/report</directory>
<includes>
<include>**/*.xml</include>
</includes>
<targetPath>${project.build.directory}/classes/mybatis</targetPath>
</resource>
<resource>
<directory>src/main/resources/static</directory>
<includes>
<include>**/*.*</include>
</includes>
<targetPath>${project.build.directory}/classes/static</targetPath>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>*.yml</include>
<include>*.properties</include>
</includes>
<targetPath>${project.build.directory}/classes</targetPath>
</resource>
</resources>
......
</build>
这样打包已经实现了需求,但把包解压开后发现,资源仍然存在于包中(相当于浪费了空间)
那,我们再加个打包的配置,排除这些文件
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<finalName>${jar-name}</finalName><!--主义properties里定义该变量,打包后jar包的名字-->
<!--排除掉配置,资源等,毕竟放到外面了-->
<excludes>
<exclude>*.yml</exclude>
<exclude>*.properties</exclude>
<exclude>mybatis/**/*.xml</exclude>
<exclude>static/**/*</exclude>
<exclude>templates/**/*</exclude>
</excludes>
</configuration>
</plugin>
<!--把${jar-name}包再打进包含springframework的包中-->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<finalName>${jar-name}</finalName>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
如此,我们再点击package,打包出的内容就会如图所示:
1.4 启动jar包
把jar包,config文件夹,resources文件夹站到linux系统的相应位置,我这里是/WORK/APP/study2024-class003
打开8080~8089的防火墙
ufw allow 8080/tcp
...
输入启动命令
java -jar nbr.jar --spring.config.additional-location=config/ study2024-class003-001
可以看出,程序启动成功了。
访问192.168.0.64/doc.html,即可看到swagger页面,查询一个报表接口,发现没有问题。
1.5 把lib包分离
其实有一说一,把lib包分离在实际的生产中,意义其实没有那么大。唯一的作用就是在公司的测试部署机放在外网,减少打包部署的带宽消耗。但这也会造个坑,就是当我们升级包时,可能会忘记把更新的包拷过去,导致莫名其妙的报错,这非常搞心态。
pom做如下修改:
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<finalName>${jar-name}</finalName>
<addResources>true</addResources>
<includes>
<include>
<groupId>nothing</groupId>
<artifactId>nothing</artifactId>
</include>
</includes>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<finalName>${jar-name}</finalName>
<archive>
<!-- 指定资源文件目录,与打包的jar文件同级目录 -->
<manifestEntries>
<Class-Path>resources/</Class-Path>
</manifestEntries>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
</manifest>
</archive>
<!--排除掉配置,资源等,毕竟放到外面了-->
<excludes>
<exclude>*.yml</exclude>
<exclude>*.properties</exclude>
<exclude>mybatis/**/*.xml</exclude>
<exclude>static/**/*</exclude>
<exclude>templates/**/*</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib/</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
再进行打包,观察现象
我们看到,我们的包重新变小了。
把新打的包重新拷到相应的地方。而后再输入如下命令:
java -jar nbr.jar -D loader.path=./lib --spring.config.additional-location=config/ --logging.config=config/logback.xml study2024-class003-001
发现毫无问题。
二、部署环境
2.1 通常的4种环境
不考虑那种有中台的千人大厂情况,一般开发团队,通常会有4套部署环境。
- local
本地开发环境,即IDEA的环境,用户个人调试 - dev
开发环境,部署在公司内部的开发服务器中,用于前后端联调。该分支服务通常不稳定,经常发生代码变更。
更严谨点的做法,dev也可以认为是一个测试环境,公司内部服务器部署dev,dev-feature1,dev-feature2…feature表示某个新增功能,通常有1个或几个程序员共同开发。测试先测试通过feature分支,而后把feature合并到dev上。当测试发现dev上有bug,则即刻要求开发进行修复,拉取部署bugfix分支,交由测试测试通过后,合并到dev分支上。
但测试的最终验收,以test分支为主。
dev环境下所用的数据库等中间件地址,很多时候是与我们本地开发不同的。但有时候为了方便,也采用相同的数据。 - test
测试环境,部署在公司内部的测试服务器中,也可以直接部署在外网的云服务器上。该服务用于交付测试,代码不经常变动。测试通过后打tag,可发到线上环境。
该环境的数据库等中间件,与dev环境也不同。
个人观点,这个环境下的数据,不要让开发人员污染。放有意义的测试数据。最好由专业的测试人员提供录入。 - prod
线上部署环境。通常,线上环境所用的数据库等中间件,与测试环境也是不同的。
2.2 profile配置
正因为不同的环境,可能有不同的配置。打包时要自动区分这点,所以通常会在pom文件中声明这4种环境。
由于我们这里是学习配置,只保留local和test,local表示在IDEA里跑,test表示放linux机器上(没linux的可以用WSL,我前面的文章介绍过)
<profiles>
<!--本地-->
<profile>
<id>local</id>
<properties>
<!-- 环境标识,需要与配置文件的名称相对应 -->
<spring.profiles.active>local</spring.profiles.active>
</properties>
<activation>
<!-- 默认环境 -->
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<!--测试-->
<profile>
<id>test</id>
<properties>
<spring.profiles.active>test</spring.profiles.active>
</properties>
</profile>
</profiles>
2.3 把先前的yml配置文件和日志xml分环境
我们现在把这种配置文件这样
为了能够正常运行,以及把对应文件拷到外面的config文件夹中,xml可以做如下配置
<resource>
<directory>src/main/config/${spring.profiles.active}</directory>
<includes>
<include>*.yml</include>
<include>*.properties</include>
<include>logback.xml</include>
</includes>
<targetPath>${project.build.directory}/config</targetPath>
</resource>
<resource>
<directory>src/main/config/${spring.profiles.active}</directory>
<includes>
<include>*.yml</include>
<include>*.properties</include>
<include>logback.xml</include>
</includes>
<targetPath>${project.build.directory}/classes</targetPath>
</resource>
点击compile,观察现象
我想,这样就实现了
三、maven打包命令
实际开发时,可不会在IDEA里打包,而是在jenkins中,在linux下使用命令打包。
我们先在windows中尝试一下:
mvn clean package -pl busy/general-test -am -Ptest
嗯,这就成功了。
四、创建一个启动和关闭脚本
实际开发中,我们不可能用java -jar来启动服务。另外,同一个jar包,可能会部署多个实例。这时,我们可以写一个shell脚本来做启动和关闭。
在如图位置创建这俩脚本
在resource的配置中,添加拷贝脚本的配置
<resource>
<directory>src/main/bin/${spring.profiles.active}</directory>
<includes>
<include>*.sh</include>
<include>*.bat</include>
</includes>
<targetPath>${project.build.directory}/bin</targetPath>
</resource>
4.1 启动脚本startup.sh
#!/bin/bash
cygwin=false
darwin=false
os400=false
case "`uname`" in
CYGWIN*) cygwin=true;;
Darwin*) darwin=true;;
OS400*) os400=true;;
esac
error_exit ()
{
echo "ERROR: $1 !!"
exit 1
}
#参数解析
VERSION=''
appName=''
PORT='8081'
ARGS=`getopt -o v:a:p: --long version:,appName:,port: -n "$0" -- "$@"`
if [ $? != 0 ]; then
echo "Terminating..."
exit 1
fi
echo ARGS=[$ARGS]
eval set -- "${ARGS}"
while true
do
case "$1" in
-v|--version)
case "$2" in
"")
VERSION=''
shift 1;
;;
*)
VERSION=$2
shift 2;
;;
esac
;;
-a|--appName)
case "$2" in
"")
appName='main'
shift 1;
;;
*)
appName=$2
shift 2;
;;
esac
;;
-p|--port)
case "$2" in
"")
PORT='8081'
shift 1;
;;
*)
PORT=$2
shift 2;
;;
esac
;;
--) shift ; break ;;
*) echo "Internal error!" ; exit 1 ;;
esac
done
SERVER="nbr"
if [[ -z "$VERSION" ]]; then
SERVER_JAR="$SERVER.jar"
else
SERVER_JAR="$SERVER-$VERSION.jar"
fi
SPACE="study2024"
SERV_NAME="general-test"
PROFILE="test"
which java
JAVA="$JAVA_HOME/bin/java"
echo $JAVA
BASE_DIR=`cd $(dirname $0)/..; pwd`
CUSTOM_SEARCH_LOCATIONS=file:${BASE_DIR}/config/
#===========================================================================================
# JVM Configuration
#===========================================================================================
#JAVA_OPT="${JAVA_OPT} -Xms512m -Xmx512m -Xss128k -XX:MaxGCPauseMillis=300 -Xlog:gc:../logs/gc.log"
JAVA_OPT="${JAVA_OPT} ${JAVA_OPT_EXT}"
JAVA_OPT="${JAVA_OPT} -jar ${BASE_DIR}/${SERVER_JAR}"
JAVA_OPT="${JAVA_OPT} -D loader.path=./lib"
JAVA_OPT="${JAVA_OPT} --spring.profiles.active=${PROFILE}"
JAVA_OPT="${JAVA_OPT} --server.port=${PORT}"
JAVA_OPT="${JAVA_OPT} --spring.config.additional-location=${CUSTOM_SEARCH_LOCATIONS}"
JAVA_OPT="${JAVA_OPT} --logging.config=${BASE_DIR}/config/logback.xml"
JAVA_MAJOR_VERSION=$($JAVA -version 2>&1 | sed -E -n 's/.* version "([0-9]*).*$/\1/p')
if [ ! -d "${BASE_DIR}/logs" ]; then
mkdir ${BASE_DIR}/logs
fi
echo "$JAVA ${JAVA_OPT}"
# start
echo "$JAVA ${JAVA_OPT}" > ${BASE_DIR}/logs/start.out 2>&1 &
nohup $JAVA ${JAVA_OPT} ${SPACE}.${SERV_NAME}-${appName} >> ${BASE_DIR}/logs/start.out 2>&1 &
echo "${SPACE}.${SERV_NAME}-${appName} is starting,you can check the ${BASE_DIR}/logs/start.out"
echo $! > pid.txt
4.2 关闭脚本shutdown.sh
#!/bin/bash
SPACE="study2024"
SERV_NAME="general-test"
PROFILE="test"
#参数解析
appName='app001'
ARGS=`getopt -o a: --long appName: -n "$0" -- "$@"`
if [ $? != 0 ]; then
echo "Terminating..."
exit 1
fi
#echo ARGS=[$ARGS]
eval set -- "${ARGS}"
#echo formatted parameters=[$@]
while true
do
case "$1" in
-a|--appName)
case "$2" in
"")
appName='main'
shift 1;
;;
*)
appName=$2
shift 2;
;;
esac
;;
--) shift ; break ;;
*) echo "Internal error!" ; exit 1 ;;
esac
done
#cd `dirname $0`/..
target_dir=`pwd`
pid=$(cat "$target_dir/pid.txt")
if [[ -z "$pid" ]]; then
pid=`ps ax | grep -i "$SPACE.$SERV_NAME-${appName}" | grep ${target_dir} | grep java | grep -v grep | awk '{print $1}'`
fi
if [ -z "$pid" ] ; then
echo "No $SPACE.$SERV_NAME-${appName} running."
exit 0;
fi
rm pid.txt
echo "The ${SPACE}.${SERV_NAME}-${appName}(${pid}) is running..."
kill ${pid}
echo "Send shutdown request to ${SPACE}.${SERV_NAME}-${appName}(${pid}) OK"
4.3 启动关闭实验
把该脚本拖到linux,如图所示
cd进bin中,把这俩脚本赋予可执行权限
cd bin
chmod +x startup.sh
chmod +x shutdown.sh
启动命令
./startup -p 8083
查看是否启动成功
lsof -i:8083
可见,程序启动成功了。
关闭程序
./shutdown.sh
再次查看是否进程还在
lsof -i:8083
观察到已经没有。
五、代码展示
不多讲,还是移步我的码云吧