是時候閉環Java應用了

TemekaMader 8年前發布 | 22K 次閱讀 Java Java開發

你曾經因為部署/上線而痛苦嗎?你曾經因為要去運維那改配置而煩惱嗎? 在我接觸過的一些部署/上線方式中,曾碰到過以下一些問題:

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/sh
echo -------------------------------------------
echo start server
echo -------------------------------------------
# 設置項目代碼路徑
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"
  done
fi

#等待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 5
done

#如果等待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"
  done
fi
tail -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應用的主要目的。

 

來自:http://jinnianshilongnian.iteye.com/blog/2317830

 

Save

 本文由用戶 TemekaMader 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!