java项目打包最佳实践

摸索了一晚上外加多年经验终于有了这个最佳实践.

环境

  • 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>
                &lt;!&ndash;Idea运行时, 需要借助这个配置build指定的配置文件. 打包时排除了. (缺点是打包dev时配置文件不会分离)&ndash;&gt;
                <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>
                    &lt;!&ndash;排除 resources 下所有(assembly将其放到根目录下)&ndash;&gt;
                    <exclude>**/*</exclude>
                    &lt;!&ndash;<exclude>**/*.properties</exclude>&ndash;&gt;
                    &lt;!&ndash;<exclude>**/*.yml</exclude>&ndash;&gt;
                </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

打包产物
配置文件, 静态资源都在根下.

image.png

主程序执行jar包结构
没有配置文件了.

image.png

坑点

如何配置不当:

  1. 打包时配置文件能够隔离. 但IDE里本地运行时, 找不到配置文件, 因为配置文件被排除了.
  2. spring 的devtools 一定要设置成 scope provided, 让它不参与打包. 这样可以避免在设置当前目录加入了classpath, 而日志文件又写入当亲目录内时, 应用不停的重启. (这是因为检测到了classpath内内容变化就会热部署, 就会重启).
  3. 找不到 logback.xml 配置文件. 解决: M1: 加入当前目录到classpath; M2: 启动命令中指定文件.
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335