架构实战篇(十六):Spring Boot Assembly服务化打包

偷懒的程序员 2018-09-25 14:56:21 ⋅ 917 阅读

在使用assembly来打包springboot微服务项目前,我想说一说,目前springboot项目的几种常见的部署方式。

  1. 使用docker容器去部署,将springboot的应用构建成一个docker image,然后通过容器去启动镜像 ,这种方式在需要部署大规模的应用和应用扩展时是非常方便的,属于目前工业级的部署方案,但是需要掌握docker的生态圈技术。

  2. 使用fatjar直接部署启动,这是很多初学者或者极小规模情况下的一个简单应用部署方式。

    本文主要针对第二种部署方式提供一种更加友好的打包方案,是部署管理更加轻松,第一种方式可能未来我会在自己博客中写。

一、为什么要将springboot服务化打包 ?

  最近我看到一个项目团队,他们在采用springboot开发完项目构建交互给运维团队就是一个spring boot 的fatjar。而且这种原始打出的包在传统型项目开发公司,对于运维人员来说无疑是很致命的,项目交付后整个配置文件都被隐藏到打成的jar中,针对不同的环境修改配置文件就变成了一件很困难的事情。因此,我们在公司引入任何新技术时,一定要考虑怎么去做服务化和工程化,如果仅仅引用技术框架,很多时候可能只需要加入几个依赖,看下api写几行代码就能跑起来。

   针对上面的这种问题,要去做服务化和工程化,大致要解决两点问题:
  1. 让springboot能够加载jar外的配置文件。

  2. 提供一个服务化的启动脚本,这个脚本一般是shell或者windows下的bat ,有了springboot的应用服务脚本后,就可以很容易的去启动和停止springboot的应用了。

二、打包后的springboot应用结构图

      这里先来看下使用assembly将springboot服务化打包后的效果图。
image

三、服务化打包重要步骤

    下面是打包springboot的详细步骤。

3.1 加入assembly打包插件

<plugin>
    <artifactId>maven-assembly-plugin</artifactId>
    <version>3.0.0</version>
    <configuration>
        <descriptors>
        <descriptor>src/main/assembly/assembly.xml</descriptor>
        </descriptors>
    </configuration>
    <executions>
        <execution>
        <id>make-assembly</id>
        <phase>package</phase>
        <goals>
            <goal>single</goal>
        </goals>
        </execution>
    </executions>
</plugin>

从上面代码看出了我把assembly的配置放在main目录下,这个是习惯,可以不放这里也可以,下面就是一个assembly在项目中的大致结构图:

image

3.2 assembly.xml配置

    assembly的配置不同的应用和下面配置也差不多,无非就是打包服务脚本、jar、配置文件等。从下面的代码中config配置就会发现, assembly将配置文件打到了config下。

format 支持多个 zip 、tar、tar.gz 、tgz、tar.bz2、tbz2、tar.snappy、tar.xz、txz、jar、dir、war 如果配置了多个,也会相应的打出多个不同的包

fileMode 在UNIX 系统里面的文件许可,读、写、执行

fileSet 可以有多个打包的时候包含一些自定义文件,一般会把bin执行文件和配置包含在里面

<assembly>
    <id>1.0</id>
    <formats>
        <format>tar.gz</format>
    </formats>
    <fileSets>
        <fileSet>
            <directory>src/main/assembly/bin</directory>
            <outputDirectory>bin</outputDirectory>
            <fileMode>0755</fileMode>
        </fileSet>
        <fileSet>
            <directory>src/main/assembly/config</directory>
            <outputDirectory>config</outputDirectory>
            <fileMode>0644</fileMode>
        </fileSet>
        <fileSet>
            <directory>target</directory>
            <outputDirectory>lib</outputDirectory>
            <includes>
                <include>*.jar</include>
            </includes>
        </fileSet>
        <fileSet>
            <directory>src/main/resources</directory>
            <outputDirectory>logs</outputDirectory>
            <fileMode>0755</fileMode>
            <excludes>
                <exclude>**/*</exclude>
            </excludes>
        </fileSet>
    </fileSets>
</assembly>

3.3 编写服务脚本

 现在写linux环境的脚本。  

第一个:start.sh启动脚本
#!/bin/bash

SERVER_NAME='spring-vue'
# jar名称
JAR_NAME='springboot-vue.jar'
cd `dirname $0`
BIN_DIR=`pwd`
cd ..
DEPLOY_DIR=`pwd`
CONF_DIR=$DEPLOY_DIR/config
# SERVER_PORT=`sed '/server.port/!d;s/.*=//' config/application.properties | tr -d '\r'`
# 获取应用的端口号
SERVER_PORT=`sed -nr '/port: [0-9]+/ s/.*port: +([0-9]+).*/\1/p' config/application.yml`

PIDS=`ps -f | grep java | grep "$CONF_DIR" |awk '{print $2}'`
if [ "$1" = "status" ]; then      
    if [ -n "$PIDS" ]; then
        echo "The $SERVER_NAME is running...!"
        echo "PID: $PIDS"
        exit 0
    else
        echo "The $SERVER_NAME is stopped"
        exit 0
    fi
fi

if [ -n "$PIDS" ]; then
    echo "ERROR: The $SERVER_NAME already started!"
    echo "PID: $PIDS"
    exit 1
fi

if [ -n "$SERVER_PORT" ]; then
    SERVER_PORT_COUNT=`netstat -tln | grep $SERVER_PORT | wc -l`
    if [ $SERVER_PORT_COUNT -gt 0 ]; then
        echo "ERROR: The $SERVER_NAME port $SERVER_PORT already used!"
        exit 1
    fi
fi

LOGS_DIR=$DEPLOY_DIR/logs
if [ ! -d $LOGS_DIR ]; then
    mkdir $LOGS_DIR
fi
STDOUT_FILE=$LOGS_DIR/stdout.log

JAVA_OPTS=" -Djava.awt.headless=true -Djava.net.preferIPv4Stack=true "
JAVA_DEBUG_OPTS=""
if [ "$1" = "debug" ]; then
    JAVA_DEBUG_OPTS=" -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=n "
fi

JAVA_JMX_OPTS=""
if [ "$1" = "jmx" ]; then
    JAVA_JMX_OPTS=" -Dcom.sun.management.jmxremote.port=1099 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false "
fi

JAVA_MEM_OPTS=""
BITS=`java -version 2>&1 | grep -i 64-bit`
if [ -n "$BITS" ]; then
    JAVA_MEM_OPTS=" -server -Xmx512m -Xms512m -Xmn256m -XX:PermSize=128m -Xss256k -XX:+DisableExplicitGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:LargePageSizeInBytes=128m -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 "
else
    JAVA_MEM_OPTS=" -server -Xms512m -Xmx512m -XX:PermSize=128m -XX:SurvivorRatio=2 -XX:+UseParallelGC "
fi

CONFIG_FILES=" -Dlogging.path=$LOGS_DIR -Dlogging.config=$CONF_DIR/log4j2.xml -Dspring.config.location=$CONF_DIR/application.properties "
echo -e "Starting the $SERVER_NAME ..."
nohup java $JAVA_OPTS $JAVA_MEM_OPTS $JAVA_DEBUG_OPTS $JAVA_JMX_OPTS $CONFIG_FILES -jar $DEPLOY_DIR/lib/$JAR_NAME > $STDOUT_FILE 2>&1 &

COUNT=0
while [ $COUNT -lt 1 ]; do
    echo -e ".\c"
    sleep 1
    if [ -n "$SERVER_PORT" ]; then
        COUNT=`netstat -an | grep $SERVER_PORT | wc -l`
    else
        COUNT=`ps -f | grep java | grep "$DEPLOY_DIR" | awk '{print $2}' | wc -l`
    fi
    if [ $COUNT -gt 0 ]; then
        break
    fi
done

echo "OK!"
PIDS=`ps -f | grep java | grep "$DEPLOY_DIR" | awk '{print $2}'`
echo "PID: $PIDS"
echo "STDOUT: $STDOUT_FILE"

脚本用例:

# 启动应用

./start.sh

# 以debug方式启动
./start debug

# 启动任务并开启jmx监控

./start jmx
# 获取当前的运行状态
./start status

停止脚本:stop.sh

#!/bin/bash
cd `dirname $0`
BIN_DIR=`pwd`
cd ..
DEPLOY_DIR=`pwd`
CONF_DIR=$DEPLOY_DIR/config

SERVER_NAME=$DEPLOY_DIR

PIDS=`ps -ef | grep java | grep "$CONF_DIR" |awk '{print $2}'`
if [ -z "$PIDS" ]; then
    echo "ERROR: The $SERVER_NAME does not started!"
    exit 1
fi

if [ "$1" != "skip" ]; then
    $BIN_DIR/dump.sh
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 -f -p $PID | grep java`
        if [ -n "$PID_EXIST" ]; then
            COUNT=0
            break
        fi
    done
done

echo "OK!"
echo "PID: $PIDS"

windows环境的启动脚本:

echo off

set APP_NAME=springboot-vue.jar
set CONFIG= -Dlogging.path=../logs -Dlogging.config=../config/log4j2.xml -Dspring.config.location=../config/application.yml 

set DEBUG_OPTS=
if ""%1"" == ""debug"" (
   set DEBUG_OPTS= -Xloggc:../logs/gc.log -verbose:gc -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=../logs 
   goto debug
)

set JMX_OPTS=
if ""%1"" == ""jmx"" (
   set JMX_OPTS= -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9888 -Dcom.sun.management.jmxremote.ssl=FALSE -Dcom.sun.management.jmxremote.authenticate=FALSE 
   goto jmx
)

echo "Starting the %APP_NAME%"
java -Xms512m -Xmx512m -server %DEBUG_OPTS% %JMX_OPTS% %CONFIG% -jar ../lib/%APP_NAME%
goto end

:debug
echo "debug"
java -Xms512m -Xmx512m -server %DEBUG_OPTS% %CONFIG% -jar ../lib/%APP_NAME%
goto end

:jmx
java -Xms512m -Xmx512m -server %JMX_OPTS% %CONFIG% -jar ../lib/%APP_NAME%
goto end

:end
pause

对于不同的springboot项目,只需要适当修改一下脚本就可以了,为了节约篇幅这里就不列出其他的脚本了,可以参考我提交的demo:https://github.com/Shalousun/springboot-vue.git,对于demo如有疑问可添加该群:170651381

ps:以上脚本参考自dubbo官方。其实对于dubbo项目的轻量化构建也是类似的

重点:上面讲了这么多脚本,其实每次创建项目添加一堆assembly的配置也是比较麻烦的,而且需要去修改脚本中的jar名称,修改的不一致还可能出错,因此可以采用本人开源的项目框架搭建脚手架https://gitee.com/sunyurepository/ApplicationPower来自动创建这些配置然后复制到您的项目中,您就只需要安安心心写写业务代码。

四、打包后应用的日志路径处理

   在第二节的图中可以看到打包的应用日志一般统一输出到logs目录中,但是对于不同的系统平台,虽然配置的日志输出路径是一样的,但是最后不一定输出到logs中。经过测试在windows平台中使用相对的日志路径../logs是没有问题的,但是对于linux系统下使用相对路径就不能输出到logs下,因此建议在linux平台下就写绝对路径吧。不过在我提供的脚本中设置输出日志的路径
-Dlogging.path=../logs

因此结合log4j2的强大解析能力完全可以设置log42的日志路径(开发时则手动指定路径):

<property name="LOG_HOME">${sys:logging.path}</property>

但是对于springboot应用的访问日志在linux下似乎只能使用绝对路径了。

# server config
server:
  port: 8080
  undertow:
   accesslog:
    enabled: true
    dir: /usr/xxx/logs
logging:
  path: /usr/xxx/logs

当然后面有使用配置解决的同学可以提醒纠正下。

总结:这个方案本身并没有带来什么新东西,甚至脚本大多数是参考了dubbo官方的脚本,只是在上面做了些完善。但是重要的一点是怎么去根据实际的技术应用场景,思考使用这项技术需要做的服务化和工程化。

参考 官网文档 网络文献

关注我们



全部评论: 0

    我有话说:

    架构实战七):Spring Boot Assembly 整合 thymeleaf

    如何让服务器上的 sprig boot 项目升级变的方便快捷

    架构实战):Spring Boot RestTemplate的使用

    RestTemplate是Spring提供的用于访问Rest服务的客户端,RestTemplate提供了多种便捷访问远程Http服务的方法,能够大大提高客户端的编写效率。

    服务架构实战):Spring boot2.x 集成阿里大鱼短信接口详解与Demo

    Spring boot2.x 集成阿里大鱼短信接口,发送短信验证码及短信接口详解。

    架构实战):Spring Boot 集成 Dubbo

    Dubbo是阿里巴巴SOA服务治理方案的核心框架,一个分布式服务框架,致力于提供高性能和透明的RPC远程服务调用方案。

    架构实战一):Spring Boot 集成企业级搜索引擎 SolrCloud

    Solr是以Lucene为基础实现的文本检索应用服务。Solr部署方式有单机方式、多机Master-Slaver方式、Cloud方式。

    架构实战五):Spring Boot 解耦之事件驱动

    通过使用spring 事件来解决业务代码的耦合

    架构实战(三)-Spring Boot架构搭建RESTful API案例

    之前分享了Spring Boot 整合Swagger 让API可视和前后端分离架构 受到了大家一致好评 ,本节就接着上节的代码做了详细的查询代码的补充和完善并搭建RESTful API架构案例。

    码云推荐:一个优秀的分布式spring boot/Spring Cloud API限流框架,特别适合微服务架构

    一个优秀的分布式spring boot/Spring Cloud API限流框架,特别适合微服务架构.

    架构实战(七):Spring Boot Data JPA 快速入门

    Spring Data JPA 是Spring Data 的一个子项目,它通过提供基于JPA的Repository极大了减少了操作JPA的代码。

    架构实战四):Spring Boot 多缓存实战

    多场景下的不同缓存策略解决方案

    架构实战(一)-Spring Boot+MyBatis基础架构搭建

    Spring的追求一定是简单点简单点,让java的开发变得更加简单、容易。瞧瞧的告诉你们直接copy就能用哦~~~

    架构实战三):Spring Boot Logback 邮件通知

    日志对于应用程序来说是非常重要的,当你的程序报错了,而你又不知道是多么可怕的一件事情,本文使用logback把程序报错信息邮件到开发者

    服务架构学习笔记:gRPC Spring Boot Starter 2.2.0 发布,及使用步骤

    gRPC Spring Boot Starter 项目是一个 gRPC 的 Spring Boot 模块。内嵌一个 gRPC Server 对外提供服务,并支持 Spring Cloud 的服务发现

    架构实战(五):Spring Boot 表单验证和异常处理

    为了让API 能够更好的提供服务,表单数据验证和异常的处理是必不可少的,让我们来看看怎么处理......

    架构实战二):Spring Boot 分布式Session共享Redis

    分布式Web网站一般都会碰到集群session共享问题,小编整理了一套解决方案,内附GitHub 源码地址哦~~~

    架构实战(二)-Spring Boot整合Swagger,让你的API可视

    你还在跟前端对接上花费很多的时间而没有效果吗?你还在为写接口文档而烦恼吗?今天就教大家一个接口对接神器...

    架构实战:认识一下微服务架构

    服务是一个新兴的软件架构,就是把一个大型的单个应用程序和服务拆分为数个的支持微服务