java项目打包最佳实践
2020-09-09 本文已影响0人
Nisus_Liu
摸索了一晚上外加多年经验终于有了这个最佳实践.
环境
- assembly
- springboot
- idea
- java8
- logback
需求
- jar包要可执行:
java -jar jar_name
. - 依赖分离: 主业务代码独立成可执行jar, 其他所有依赖放到
./lib
下. - 配置文件分离:
application.yml
,logback.xml
等配置文件不在jar内, 而是在项目根目录下. 这样方便中途查看和修改配置. - 分环境打包: 我这里分了三个环境
dev
,test
,prod
. - 脚本启动,停止,重启应用.
配置参考
pom.xml
<profiles>
<profile>
<!-- 本地开发环境 -->
<id>dev</id>
<properties>
<profileActive>dev</profileActive>
</properties>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<!--<build>
<!–Idea运行时, 需要借助这个配置build指定的配置文件. 打包时排除了. (缺点是打包dev时配置文件不会分离)–>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>-->
</profile>
<profile>
<!-- 测试环境 -->
<id>test</id>
<properties>
<profileActive>test</profileActive>
</properties>
</profile>
<profile>
<!-- 生产环境 -->
<id>prod</id>
<properties>
<profileActive>prod</profileActive>
</properties>
</profile>
</profiles>
<build>
<!--例: 让logback.xml中解析pom.xml的属性变量-->
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<encoding>utf-8</encoding>
<useDefaultDelimiters>true</useDefaultDelimiters>
</configuration>
</plugin>
</plugins>
</pluginManagement>
<finalName>${project.artifactId}-${project.version}</finalName>
<resources>
<!--让xml等资源文件在maven这里过一手, 就可以解析里面的属性值引用了 `${xxx}`-->
<resource>
<directory>src/main/resources</directory>
<!--<includes>-->
<!-- <include>**/*.xml</include>-->
<!--</includes>-->
<!--<excludes>
<!–排除 resources 下所有(assembly将其放到根目录下)–>
<exclude>**/*</exclude>
<!–<exclude>**/*.properties</exclude>–>
<!–<exclude>**/*.yml</exclude>–>
</excludes>-->
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<!--<plugin>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-maven-plugin</artifactId>-->
<!--</plugin>-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<!--<version>2.2-beta-5</version>-->
<configuration>
<descriptors>
<descriptor>src/main/resources/assembly.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<id>make-my-jar-with-dependencies</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<finalName>${project.artifactId}</finalName>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<!--不打包资源文件 !推荐, 这样可以实现打包和Ide运行分开, 打包排除资源文件, Ide运行包含, 保证本地运行成功. 优于 resource 中配置排除.
但需要仔细, 避免将不能排除的文件排除了-->
<excludes>
<exclude>*.xml</exclude>
<exclude>*.yml</exclude>
<exclude>*.properties</exclude>
<exclude>*.sh</exclude>
<exclude>public</exclude>
<exclude>conf</exclude>
</excludes>
<archive>
<manifest>
<!--指定入口类-->
<mainClass>com.gx.app.GxAppApplication</mainClass>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
<!-- manifest是否含时间戳 jar包不包含唯一版本标识 -->
<useUniqueVersions>false</useUniqueVersions>
</manifest>
<manifestEntries>
<!--MANIFEST.MF 中 Class-Path 有当前目录 ! devtools 不能打进依赖, 否则不停的重启 !
当前目录加入classpath便于使用当前目录下的配置文件, 如 logback(就不需要脚本中指定配置文件了)-->
<Class-Path>./</Class-Path>
<!-- <implementation-version>${project.version}</implementation-version>-->
<!-- <implementation-build>${buildNumber}</implementation-build>-->
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
assembly.xml
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
<id>assembly</id>
<formats>
<format>dir</format>
</formats>
<fileSets>
<fileSet>
<directory>${project.basedir}</directory>
<outputDirectory>/</outputDirectory>
<filtered>true</filtered>
<includes>
<include>application.yml</include>
<include>application-${profileActive}.yml</include>
<include>*.xml</include>
</includes>
</fileSet>
<fileSet>
<directory>${project.build.directory}/site</directory>
<outputDirectory>docs</outputDirectory>
</fileSet>
<!-- <fileSet> -->
<!-- <directory>${project.basedir}/src/main/resources</directory> -->
<!-- <outputDirectory>/</outputDirectory> -->
<!-- <filtered>true</filtered> -->
<!-- <fileMode>0755</fileMode> -->
<!-- <includes> -->
<!-- <include>*.properties</include> -->
<!-- </includes> -->
<!-- </fileSet> -->
<fileSet>
<directory>${project.basedir}/src/main/resources</directory>
<outputDirectory>/</outputDirectory>
<filtered>true</filtered>
<fileMode>0775</fileMode>
<includes>
<!--将资源全部移到根目录下-->
<include>**/*</include>
</includes>
</fileSet>
<fileSet>
<directory>${project.basedir}/src/main/resources/conf/${profileActive}</directory>
<outputDirectory>/</outputDirectory>
<filtered>true</filtered>
<fileMode>0755</fileMode>
<includes>
<include>*.properties</include>
</includes>
</fileSet>
</fileSets>
<dependencySets>
<dependencySet>
<outputDirectory>/lib</outputDirectory>
<outputFileNameMapping>
${artifact.artifactId}-${artifact.baseVersion}${dashClassifier?}.${artifact.extension}
</outputFileNameMapping>
<useProjectArtifact>true</useProjectArtifact>
<scope>runtime</scope>
<excludes>
<exclude>${project.groupId}:${project.artifactId}:*</exclude>
</excludes>
</dependencySet>
<dependencySet>
<scope>provided</scope>
<outputDirectory>/</outputDirectory>
<includes>
<include>${project.groupId}:${project.artifactId}:*</include>
</includes>
</dependencySet>
</dependencySets>
</assembly>
logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- Reference Manual http://logback.qos.ch/manual/index.html -->
<configuration>
<!-- 修改部分 如果有job需要监控,单独修改 -->
<property name="LOG_HOME" value="./logs" />
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder charset="UTF-8">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 所有日志 -->
<appender name="RollingFile"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/${project.artifactId}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/rolling/${project.artifactId}_%d{yyyy-MM-dd}.%i.log.zip
</fileNamePattern>
<!-- 保存多少天 -->
<maxHistory>30</maxHistory>
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- ###################### SQL日志监控 ###################### -->
<appender name="SqlStatistics"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/sql/${project.artifactId}_sql_%d{yyyy-MM-dd}.log
</fileNamePattern>
<!-- <maxHistory>3</maxHistory> -->
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="java.sql.Connection" level="INFO">
<appender-ref ref="SqlStatistics" />
</logger>
<logger name="java.sql.Statement" level="INFO">
<appender-ref ref="SqlStatistics" />
</logger>
<logger name="java.sql.PreparedStatement" level="INFO">
<appender-ref ref="SqlStatistics" />
</logger>
<!-- ###################### SQL日志监控 ###################### -->
<!-- ###################### DAO日志监控 ###################### -->
<appender name="DaoStatistics"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/dao/${project.artifactId}_dao_%d{yyyy-MM-dd}.log
</fileNamePattern>
<!-- <maxHistory>3</maxHistory> -->
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- <logger name="**.dao.**" level="INFO"> -->
<logger name="com.jfbank.fincloud.loan.cif.biz.core.dao" level="DEBUG">
<appender-ref ref="DaoStatistics" />
</logger>
<!-- ###################### SQL日志监控 ###################### -->
<appender name="errorFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/error/${project.artifactId}_error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/error/${project.artifactId}_error_%d{yyyy-MM-dd}.log
</fileNamePattern>
<!-- <maxHistory>60</maxHistory> -->
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 测试环境+开发环境. 多个使用逗号隔开. -->
<springProfile name="dev">
<root level="DEBUG">
<appender-ref ref="STDOUT" />
<appender-ref ref="RollingFile" />
<appender-ref ref="errorFile" />
</root>
</springProfile>
<springProfile name="test">
<root level="DEBUG">
<!-- <appender-ref ref="STDOUT" /> -->
<appender-ref ref="RollingFile" />
<appender-ref ref="errorFile" />
</root>
</springProfile>
<!-- 生产环境. -->
<springProfile name="prod">
<root level="INFO">
<!-- <appender-ref ref="STDOUT" /> -->
<appender-ref ref="RollingFile" />
<appender-ref ref="errorFile" />
</root>
</springProfile>
</configuration>
application.yml
spring:
application:
name: GxApp
profiles:
active: @profileActive@
logging:
level:
root: info
com.gx: debug
启动脚本
#!/bin/bash
#此脚本为Linux下启动java程序的通用脚本。(包含启动,停止,重启)
#cd 进入脚本执行的bin目录,sh run.sh start(启动) | stop(停止)| restart(重启)
#
#bin目前路径以及相关目录路径
cd `dirname $0`
BIN_DIR=`pwd`
DEPLOY_DIR=`pwd`
#LOG_BACK=-Dlogback.configurationFile=$DEPLOY_DIR/logback.xml
JAVA_DEBUG_OPTS=" -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=n "
JAVA_JMX_OPTS=" -Dcom.sun.management.jmxremote.port=1099 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -Djava.rmi.server.hostname=$ip "
#拼接全路径jar包名, 为了精确匹配jar包, 能确保准确定位pid
JAR_NAME=$BIN_DIR/`ls ${project.artifactId}*.jar`
cmd=$2
echo "$cmd"
start(){
echo "DEPLOY_DIR=$DEPLOY_DIR"
PIDS=`ps --no-heading -C java -f --width 1000 | grep "$DEPLOY_DIR" |awk '{print $2}'`
if [ -n "$PIDS" ]; then
echo "ERROR: The $SERVER_NAME already started!"
echo "PID: $PIDS"
exit 1
fi
echo -e "Starting the $JAR_NAME ...\c"
nohup java -Xms256M -Xmx1024M -XX:PermSize=128M -XX:+HeapDumpOnOutOfMemoryError -verbose:gc -XX:+PrintGCDetails -Xloggc:./gc.log -XX:+PrintGCDateStamps -jar $JAR_NAME > /dev/null 2>&1 &
COUNT=0
while [ $COUNT -lt 1 ]; do
echo -e ".\c"
sleep 1
COUNT=`ps --no-heading -C java -f --width 1000 | grep "$JAR_NAME" | awk '{print $2}' | wc -l`
if [ $COUNT -gt 0 ]; then
break
fi
done
echo "start OK!"
PIDS=`ps --no-heading -C java -f --width 1000 | grep "$JAR_NAME" | awk '{print $2}'`
echo "PID: $PIDS"
echo $PIDS > .pid
}
stop(){
PIDS=`ps --no-heading -C java -f --width 1000 | grep "$JAR_NAME" |awk '{print $2}'`
if [ -z "$PIDS" ]; then
echo "ERROR: The $SERVER_NAME does not started!"
fi
echo -e "Stopping the $SERVER_NAME ...\c"
for PID in $PIDS ; do
kill $PID > /dev/null 2>&1
done
COUNT=0
while [ $COUNT -lt 1 ]; do
echo -e ".\c"
sleep 1
COUNT=1
for PID in $PIDS ; do
PID_EXIST=`ps --no-heading -p $PID`
if [ -n "$PID_EXIST" ]; then
COUNT=0
break
fi
done
done
echo "stop OK!"
echo "PID: $PIDS"
rm .pid
}
case $1 in
start)
start;
;;
stop)
stop;
;;
restart)
echo "############ Application of '"$JAR_NAME"' restarting....############"
stop;
sleep 2
start;
;;
*)
echo "Usage: startup.sh {start|stop|restart}"
;;
esac
exit 0
目录结构
image.png打包产物
配置文件, 静态资源都在根下.
主程序执行jar包结构
没有配置文件了.
坑点
如何配置不当:
- 打包时配置文件能够隔离. 但IDE里本地运行时, 找不到配置文件, 因为配置文件被排除了.
- spring 的devtools 一定要设置成
scope provided
, 让它不参与打包. 这样可以避免在设置当前目录加入了classpath, 而日志文件又写入当亲目录内时, 应用不停的重启. (这是因为检测到了classpath内内容变化就会热部署
, 就会重启). - 找不到
logback.xml
配置文件. 解决: M1: 加入当前目录到classpath; M2: 启动命令中指定文件.