是时候闭环Java应用了

原创 2016-08-16 张开涛 

你曾经因为部署/上线而痛苦吗?你曾经因为要去运维那改配置而烦恼吗?在我接触过的一些部署/上线方式中,曾碰到过以下一些问题:

1、程序代码和依赖都是人工上传到服务器,不是通过工具进行部署和发布;

2、目录结构没有规范,jar启动时通过-classpath任意指定;

3、fat jar,把程序代码、配置文件和依赖jar都打包到一个jar中,改配置文件太费劲;

4、不管是非web应用还是web应用都部署到web容器环境,如Tomcat;

5、web应用还需要先在服务器上安装好环境(如装Tomcat)才能部署,想升级版本或者换个容器太难了;

6、线上参数修改还需要找运维,痛苦。

还有如没有自动部署平台,回滚到上一个版本那可真是天方夜谈;增量包而非全量包,无法自由在在的回滚;前端代码直接覆盖而非版本化,难快速回滚,出问题要清理CDN,痛苦;ngx_lua项目时不按照项目的方式部署,在服务器上随意修改代码,导致某些服务器忘记修改或者版本不一致,排查问题太痛苦。

还有很多部署中不好的方式,但是本文只关注闭环Java应用带来的好处。首先介绍下应该如何部署应用,然后介绍下什么是闭环Java应用,它的好处和如何搭建。

应该如何部署应用

项目

项目中应该包括了所有要执行的代码、启停脚本,比如非web应用

web应用

打包应用后,会按照相应的目录结构构建。如果项目使用maven,可以使用maven-assembly-plugin进行按照相应的目录结构构件。

即项目、打包的应用要按照统一的风格来实施。

自动部署系统

自动部署系统负责打包应用(比如执行mvn相应的命令即可)、抽包(从指定目录抽取要部署的代码,如target/nonweb-example-package目录)、部署代码(发布代码,将代码同步到宿主机器)、启停应用(配置指定的启停脚本并调用)。

自动部署除了这些功能外,应该还有如发布历史管理(回滚)、分组管理(如不同机房不同的配置文件)、配置管理(如要修改启动/停止脚本、修改配置文件[不同机房不同的配置]、参数管理[如jvm参数等])等。

宿主机器

即代码部署到的机器,它应该只安装最小化环境,如只需要装JDK即可,像Tomcat是不需要安装的,由应用决定使用哪个容器。

通过增加自动部署系统可以更好的进行项目的统一发布、管理和回滚。

闭环Java应用

闭环Java应用指Java代码、容器、配置文件、启停脚本等都在同一处维护,修改配置文件、修改环境参数、更改容器类型等都不需要到宿主机器上进行更改。宿主机器只提供基本运行环境,如仅部署JDK环境即可,不需要部署如Tomcat容器,需要什么容器,都是在Java应用中指定。

这样的好处是配置文件修改、JVM参数修改、容器的选择都可以在Java应用中配置,形成闭环。

闭环Java应用的目的主要是让Java应用能自启动,这样程序的控制权就在我们手里,而不是运维手里。而我们更懂我们的程序。

随着微服务概念的流行,spring boot也受到大家的热捧。spring boot能帮助我们快速构建基于spring的应用;其能方便创建自启动应用、可以嵌入各种容器(如Tomcat、Jetty)、提供了一些starter pom用于简化配置文件、自动化配置(只需要引入相关的pom,就自动获得了某些功能)等。

在介绍spring boot之前,我们看下在以前是怎么构建闭环Java应用。

从零构建非web应用

项目结构


本示例演示了构建一个非web应用RPC服务生产者(如Dubbo服务),还可以构建如Worker类型的应用,他们本身不需要web容器,作为普通的java应用启动即可。

maven依赖(pom.xml)

需要自己添加如spring-core、spring-context等相关依赖,此处就不展示了。

打包配置(pom.xml)

nonweb-example\pom.xml

<plugin>    <groupId>org.apache.maven.plugins</groupId>    <artifactId>maven-assembly-plugin</artifactId>    <version>2.6</version>    <configuration>        <descriptor>src/assembly/assembly.xml</descriptor>        <finalName>${project.build.finalName}</finalName>    </configuration>    <executions>        <execution>            <phase>package</phase>            <goals>                <goal>directory</goal>            </goals>        </execution>    </executions></plugin>

使用maven-assembly-plugin进行打包;打包配置如下:

<id>package</id><formats>    <format>dir</format></formats><includeBaseDirectory>false</includeBaseDirectory><fileSets>    <!-- 可执行文件 -->    <fileSet>        <directory>src/bin</directory>        <outputDirectory>bin</outputDirectory>        <includes>            <include>*.bat</include>        </includes>        <lineEnding>dos</lineEnding>    </fileSet>    <fileSet>        <directory>src/bin</directory>        <outputDirectory>bin</outputDirectory>        <includes>            <include>*.sh</include>        </includes>        <lineEnding>unix</lineEnding>        <fileMode>0755</fileMode>    </fileSet>    <!-- classes -->    <fileSet>        <directory>${project.build.directory}/classes</directory>        <outputDirectory>classes</outputDirectory>    </fileSet></fileSets><!-- 依赖jar包 --><dependencySets>    <dependencySet>        <outputDirectory>lib</outputDirectory>        <excludes>            <exclude>com.jd:nonweb-example</exclude>        </excludes>    </dependencySet></dependencySets>

主要有三组配置:

formats:打包格式,此处使用的是dir,还可以是zip、rar等;

fileSet:拷贝文件,本示例主要有bin文件、classes文件需要拷贝;

dependencySets:依赖jar,拷贝到lib目录;

执行mvn package后形成了将得到如下结构:

将该目录通过自动部署抽包并部署到宿主机器即可。然后自动部署系统执行bin下的启停脚本执行即可。

启动类

public class Bootstrap {  public static void main(String[] args) throws Exception {      ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:spring-config.xml");      ctx.registerShutdownHook();      Thread.currentThread().join();  }}

本示例没有使用Java Config方式构建,直接加载spring配置文件启动Java应用。

启动脚本

#!/bin/shecho -------------------------------------------echo start serverecho -------------------------------------------# 设置项目代码路径export CODE_HOME="/export/App/nonweb-example-startup-package"#日志路径export LOG_PATH="/export/Logs/nonweb.example.jd.local"mkdir -p $LOG_PATH# 设置依赖路径export CLASSPATH="$CODE_HOME/classes:$CODE_HOME/lib/*"# java可执行文件位置export _EXECJAVA="$JAVA_HOME/bin/java"# JVM启动参数export JAVA_OPTS="-server -Xms128m -Xmx256m -Xss256k -XX:MaxDirectMemorySize=128m"# 启动类export MAIN_CLASS=com.jd.nonweb.example.startup.Bootstrap

$_EXECJAVA $JAVA_OPTS -classpath $CLASSPATH $MAIN_CLASS &tail -f $LOG_PATH/stdout.log

配置项目代码路径、日志路径、依赖路径、java执行文件路径、JVM启动参数、启动类。

停止脚本

#日志路径export LOG_PATH="/export/Logs/nonweb.example.jd.local"mkdir -p $LOG_PATH# 启动类export MAIN_CLASS=com.jd.nonweb.example.startup.Bootstrap

echo -------------------------------------------echo stop server

#所有相关进程PIDs=`jps -l | grep $MAIN_CLASS | awk '{print $1}'`#停止进程if [ -n "$PIDs" ]; then  for PID in $PIDs; do      kill $PID      echo "kill $PID"  donefi

#等待50秒for i in 1 10; do  PIDs=`jps -l | grep $MAIN_CLASS | awk '{print $1}'`  if [ ! -n "$PIDs" ]; then    echo "stop server success"    echo -------------------------------------------    break  fi  echo "sleep 5s"  sleep 5done

#如果等待50秒还没有停止完,直接杀掉PIDs=`jps -l | grep $MAIN_CLASS | awk '{print $1}'`if [ -n "$PIDs" ]; then  for PID in $PIDs; do      kill -9 $PID      echo "kill -9 $PID"  donefitail -fn200 $LOG_PATH/stdout.log

到此一个闭环非web应用就构建完了,启停脚本、启动类、项目代码都是统一在一处维护,并使用maven-assembly-plugin将这些打包在一起,通过自动部署发布并执行,达到了闭环的目的。

从零构建web应用

项目结构


maven依赖(pom.xml)

需要自己添加如spring-core、spring-context、spring-web、spring-webmvc、velocity等相关依赖,此处就不展示了。


打包配置(pom.xml)

web-example\pom.xml

<plugin>    <groupId>org.apache.maven.plugins</groupId>    <artifactId>maven-assembly-plugin</artifactId>    <version>2.6</version>    <configuration>        <descriptor>src/assembly/assembly.xml</descriptor>        <finalName>${project.build.finalName}</finalName>    </configuration>    <executions>        <execution>            <phase>package</phase>            <goals>                <goal>directory</goal>            </goals>        </execution>    </executions></plugin>

使用maven-assembly-plugin进行打包;打包配置如下:

<id>package</id>
<formats>
    <format>dir</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
    <fileSet>
        <directory>src/bin</directory>
        <outputDirectory>bin</outputDirectory>
        <includes>
            <include>*.sh</include>
        </includes>
        <lineEnding>unix</lineEnding>
        <fileMode>0755</fileMode>
    </fileSet>
    <!-- WEB-INF -->
   
<fileSet>
        <directory>src/main/webapp</directory>
        <outputDirectory></outputDirectory>
    </fileSet>
    <!-- classes -->
   
<fileSet>
        <directory>${project.build.directory}/classes</directory>
        <outputDirectory>WEB-INF/classes</outputDirectory>
    </fileSet>

</fileSets>
<!-- 依赖jar包 -->
<dependencySets>
    <dependencySet>
        <outputDirectory>WEB-INF/lib</outputDirectory>
        <excludes>
            <exclude>com.jd:web-example</exclude>
        </excludes>
    </dependencySet>
</dependencySets>

主要有三组配置:

formats:打包格式,此处使用的是dir,还可以是zip、rar等;

fileSet:拷贝文件,本示例主要有bin文件、classes文件、webapp文件需要拷贝;

dependencySets:依赖jar,拷贝到WEB-INF\lib目录;

执行mvn package后形成了将得到如下结构:

打包的目录结构和普通web结构完全一样;将该目录通过自动部署抽包并发布到宿主机器即可。然后自动部署系统执行bin下的启停脚本执行即可。

启动类

public class TomcatBootstrap {
  private static final Logger LOG = LoggerFactory.getLogger(TomcatBootstrap.class);
  public static void main(String[] args) throws Exception{
    //提升性能(https://wiki.apache.org/tomcat/HowTo/FasterStartUp)
    
System.setProperty("tomcat.util.scan.StandardJarScanFilter.jarsToSkip", "*.jar");
    //System.setProperty("securerandom.source","file:/dev/./urandom");
    
int port =Integer.parseInt(System.getProperty("server.port", "8080"));
    String contextPath = System.getProperty("server.contextPath", "");
    String docBase = System.getProperty("server.docBase", getDefaultDocBase());
    LOG.info("server port : {}, context path : {},doc base : {}",port, contextPath, docBase);
    Tomcat tomcat = createTomcat(port,contextPath, docBase);
    tomcat.start();
    Runtime.getRuntime().addShutdownHook(new Thread() {
        @Override
        public void run(){
            try {
                tomcat.stop();
            } catch (LifecycleException e) {
                LOG.error("stoptomcat error.", e);
            }
        }
    });
    tomcat.getServer().await();
 }

 private static String getDefaultDocBase() {
   File classpathDir = new File(Thread.currentThread().getContextClassLoader().getResource(".").getFile());
   File projectDir =classpathDir.getParentFile().getParentFile();
   return new File(projectDir,"src/main/webapp").getPath();
 }

private static Tomcat createTomcat(int port,String contextPath, String docBase) throws Exception{
    String tmpdir = System.getProperty("java.io.tmpdir");
    Tomcat tomcat = new Tomcat();
    tomcat.setBaseDir(tmpdir);
    tomcat.getHost().setAppBase(tmpdir);
    tomcat.getHost().setAutoDeploy(false);
    tomcat.getHost().setDeployOnStartup(false);
    tomcat.getEngine().setBackgroundProcessorDelay(-1);
    tomcat.setConnector(newNioConnector());
    tomcat.getConnector().setPort(port);
    tomcat.getService().addConnector(tomcat.getConnector());
    Context context =tomcat.addWebapp(contextPath, docBase);
    StandardServer server =(StandardServer) tomcat.getServer();
    //APR library loader. Documentation at /docs/apr.html
   
server.addLifecycleListener(new AprLifecycleListener());
    //Prevent memory leaks due to use of particularjava/javax APIs
   
server.addLifecycleListener(new JreMemoryLeakPreventionListener());
    return tomcat;
  }

//在这里调整参数优化
 
private static Connector newNioConnector() {
    Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
    Http11NioProtocol protocol =(Http11NioProtocol) connector.getProtocolHandler();
    return connector;
 }

}

通过嵌入Tomcat容器启动,这种方式的确定是需要先写Tomcat的启动代码,优点也很明显:以后Tomcat的控制权在我们手中,可以随时进行切换或者优化,不需要改线上的配置文件。

启动脚本

#!/bin/sh
echo -------------------------------------------
echo start server
echo -------------------------------------------
# 设置项目代码路径
export CODE_HOME="/export/App/web-example-web-package"
#日志路径
export LOG_PATH="/export/Logs/web.example.jd.local"
mkdir -p $LOG_PATH
# 设置依赖路径
export CLASSPATH="$CODE_HOME/WEB-INF/classes:$CODE_HOME/WEB-INF/lib/*"
# java可执行文件位置
export _EXECJAVA="$JAVA_HOME/bin/java"
# JVM启动参数
export JAVA_OPTS="-server -Xms128m -Xmx256m -Xss256k-XX:MaxDirectMemorySize=128m"
# 服务端端口、上下文、项目根配置
export SERVER_INFO="-Dserver.port=8090 -Dserver.contextPath=-Dserver.docBase=$CODE_HOME"
# 启动类
export MAIN_CLASS=com.jd.web.example.startup.TomcatBootstrap

$_EXECJAVA $JAVA_OPTS -classpath $CLASSPATH $SERVER_INFO $MAIN_CLASS &
tail -f $LOG_PATH/stdout.log

配置项目代码路径、日志路径、依赖路径、java执行文件路径、JVM启动参数、启动类;相当于非web应用,多了web服务器端口、上下文、项目根路径配置。

停止脚本

和非web的类似就不再重复了。

到此一个闭环web应用就构建完了,启停脚本、启动类、项目代码都是统一在一处维护,并使用maven-assembly-plugin将这些打包在一起,通过自动部署发布并执行。达到了闭环的目的。

Spring Boot构建非web/web应用

项目结构

maven依赖(pom.xml)

spring-boot-example/pom.xml继承spring-boot-starter-parent

<parent>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-parent</artifactId>    <version>1.4.1.BUILD-SNAPSHOT</version></parent>

spring-boot-starter-parent中是一些通用配置,如JDK编码、依赖管理(它又继承了spring-boot-dependencies,这里边定义了所有依赖);

依赖

<dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter</artifactId></dependency><dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-web</artifactId></dependency><dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-velocity</artifactId></dependency><dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-log4j2</artifactId></dependency>

spring-boot-starter是最小化的spring boot环境(spring-core、spring-context等);spring-boot-starter-web是spring mvc环境,并使用Tomcat作为web容器;spring-boot-starter-velocity将自动将模板引擎配置为velocity。此处可以看到starter的好处了,需要什么功能只需要引入一个starter,相关的依赖自动添加,而且会自动配置使用该特性。

打包配置(pom.xml)

spring-boot-example-web\pom.xml添加如下maven插件:

<plugin>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-maven-plugin</artifactId></plugin>

执行mvn package时将得到如下fat jar:

启动类

package com.jd.springboot.example.web.startup;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.context.annotation.ImportResource;@SpringBootApplication(scanBasePackages = "com.jd.springboot.example")@ImportResource("classpath:spring-config.xml")public class Bootstrap {  public static void main(String[] args) {      SpringApplication.run(Bootstrap.class, args);  }}

@SpringBootApplication指定了要扫描的包、可以使用@ImportResource引入xml配置文件。然后可以直接作为普通java应用启动即可,此时自动使用tomcat作为web容器启动。

运行 jar -jar spring-boot-example-1.0-SNAPSHOT.jar即可启动(META-INF\MANIFEST.MF指定了Main-Class)。

个人不太喜欢fat jar的方式。可以使用maven-assembly-plugin配合来打包Java应用。项目结构如下所示:

项目结构和之前的区别是多了assembly和bin。

打包配置(pom.xml)

spring-boot-example-web\pom.xml将如下maven插件

<plugin>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-maven-plugin</artifactId></plugin>

更改为assembly插件

<plugin>    <groupId>org.apache.maven.plugins</groupId>    <artifactId>maven-assembly-plugin</artifactId>    <version>2.6</version>    <configuration>        <descriptor>src/assembly/assembly.xml</descriptor>        <finalName>${project.build.finalName}</finalName>    </configuration>    <executions>        <execution>            <phase>package</phase>            <goals>                <goal>directory</goal>            </goals>        </execution>    </executions></plugin>

assembly.xml和“从零构建非web应用”的类似,就不贴配置了。

执行mvn package时将得到如下打包:

启停脚本也是类似的,在此也不贴配置了。到此基于spring boot的非fat jar方式的自启动Java应用就构建好了。

总结

从零构建非web应用/web应用需要我们查找相关依赖并配置,还需要进行一些配置(Spring配置、容器配置),如果构建一个新的项目还是相对较慢的,但是在公司内大家应该都有自己的“starter pom”,因此实际构建也不会很慢。而如果没有一些项目的积累,使用spring boot可以非常容易而且快速的就能搭建出想要的项目。使用spring boot后:容易添加依赖、启动类不用自己创建、享受到自动配置的好处等;而自带的spring-boot-maven-plugin会生成fat jar,不过可以配合maven-assembly-plugin来实现之前的方式的。

另外因笔者所在公司使用Docker容器,一个宿主机器只部署一个JVM示例,示例中的启停脚本不用考虑单机多JVM实例问题。

创建闭环Java应用,可以更容易的进行如JVM参数调优、修改容器配置文件、非web应用不需要部署到Tomcat容器中;这是笔者想进行闭环Java应用的主要目的。

java 打包插件的更多相关文章

  1. 有时间研究一下Maven打包插件细节

    Maven工作分为多个阶段,具体阶段参考:https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html ...

  2. java打包成window service服务[转]

    1 解释 java project  我说的是main方法作为程序入口的java工程,有别于 web project. 这样的工程 一般都是web project的附属扫描程序或一些独立的执行程序,如 ...

  3. 【Z】扩展阿里巴巴Java开发规约插件

    https://blog.csdn.net/u014513883/article/details/79186893 1.前言 工作中难免会遇到维护别人代码的情况,那么首先就得看懂别人写的代码.如果对方 ...

  4. springboot 打包插件去除jar包瘦身

    1.pom文件配置 <plugin> <groupId>org.springframework.boot</groupId> <artifactId>s ...

  5. 【Gradle】Java Gradle 插件

    Java Gradle 插件 如何应用 apply plugin:'java' Java插件约定的项目结构 Project |--build.gradle |--src |--main |--java ...

  6. 扩展阿里巴巴Java开发规约插件(转)

    转自:https://blog.csdn.net/u014513883/article/details/79186893 1.前言 工作中难免会遇到维护别人代码的情况,那么首先就得看懂别人写的代码.如 ...

  7. java打包遇到问题java.io.IOException: invalid header field

    问题:java打包时报以下错误 $ jar -cvmf main.txt test.jar Shufile1.class java.io.IOException: invalid header fie ...

  8. 阿里巴巴Java规约插件试用

    阿里Java开发规约Eclipse插件介绍 阿里巴巴集团配合<阿里巴巴Java开发手册>PDF终极版开发的IDE插件,目前包括IDEA插件.Eclipse插件. 安装 检查环境 插件要求: ...

  9. Java打包生成exe(使用exe4j和inno setup)

    Java打包生成exe 生成jar 先使用eclipse生成可执行的jar[可执行的jar包含内容更全面,包括指定主类的.mf] Exe4j的使用 一定要可执行jar进行打包. Project typ ...

随机推荐

  1. 从ZOJ2114(Transportation Network)到Link-cut-tree(LCT)

    [热烈庆祝ZOJ回归] [首先声明:LCT≠动态树,前者是一种数据结构,而后者是一类问题,即:LCT—解决—>动态树] Link-cut-tree(下文统称LCT)是一种强大的数据结构,不仅可以 ...

  2. 深入理解QStateMachine与QEventLoop事件循环的联系与区别

    最近一直在倒腾事件循环的东西,通过查看Qt源码多少还是有点心得体会,在这里记录下和大家分享.总之,对于QStateMachine状态机本身来说,需要有QEventLoop::exec()的驱动才能支持 ...

  3. 零售POS开发

    零售POS系统是一款基于离线与在线两种模式的POS系统,能够将门店的销售数据及时准确的同步到企业服务器.离线模式操作更加快捷.稳定.高效:在线操作实时同步会员信息.查看库存.下载最新档案.公文公告等一 ...

  4. C# Json反序列化处理

    最近换工作了 从客户端转到Web端 第一个任务就是去别人的页面上抓取数据 用到的是JSON 因为他们网站json的格式有点怪 所以 就在JSON反序列化上面 花了一点时间 首先用到的工具是http:/ ...

  5. 【Java】Java里String 的equals和==

    Java里面有对象和对象的引用的概念,在String方面,==比较的是引用,equals比较的是对象的具体值. String s1 = new String("abc");Stri ...

  6. 用正则式判断URL是否合法-python

    import sys import re #Make sure we have a single URL argument. if len(sys.argv) != 2: print (sys.std ...

  7. SVN 修改URL路径|SVN 项目路径修改

    在svn的根目录下面右键 输入要修改的地址: 点击ok 搞定... ~~~

  8. 14.7.2 Changing the Number or Size of InnoDB Redo Log Files 改变InnoDB Redo Log Files的数量和大小

    14.7.2 Changing the Number or Size of InnoDB Redo Log Files 改变InnoDB Redo Log Files的数量和大小 改变 InnoDB ...

  9. wcf客户端捕获异常

    直接使用Exception进行捕获,然后在监视器中查看具体是哪一个异常 System.Exception {System.ServiceModel.Security.MessageSecurityEx ...

  10. BZOJ1617: [Usaco2008 Mar]River Crossing渡河问题

    1617: [Usaco2008 Mar]River Crossing渡河问题 Time Limit: 5 Sec  Memory Limit: 64 MBSubmit: 654  Solved: 4 ...