Đã đến lúc hoàn thiện quy trình triển khai ứng dụng Java
Trong quá trình làm việc, bạn có bao giờ gặp khó khăn khi triển khai ứng dụng không? Bạn có bao giờ phải nhờ đến bộ phận vận hành để thay đổi cấu hình không? Trong số các phương pháp triển khai mà tôi đã gặp, có một số vấn đề sau:
- Mã nguồn và các thư viện phụ thuộc được tải lên máy chủ bằng tay, không thông qua công cụ triển khai tự động;
- Cấu trúc thư mục không chuẩn, khởi động jar thông qua -classpath tùy ý;
- Fat jar, gói mã nguồn, tệp cấu hình và các thư viện phụ thuộc vào một file jar duy nhất, thay đổi tệp cấu hình rất khó khăn;
- Cả ứng dụng không web và web đều được triển khai trên môi trường web container như Tomcat;
- Ứng dụng web cần cài đặt môi trường trên máy chủ (như cài Tomcat) trước khi triển khai, việc nâng cấp phiên bản hoặc thay đổi container rất khó khăn;
- Thay đổi tham số trực tuyến vẫn cần nhờ bộ phận vận hành, rất mệt mỏi.
Còn nhiều phương pháp triển khai không tốt khác, nhưng bài viết này chỉ tập trung vào lợi ích của việc tạo ứng dụng Java "đóng gói". Trước hết, tôi sẽ giới thiệu cách triển khai ứng dụng, sau đó giới thiệu về khái niệm ứng dụng Java "đóng gói", lợi ích và cách xây dựng nó.
Cách triển khai ứng dụng
Dự án
Dự án nên bao gồm tất cả mã nguồn cần thực thi, các script khởi động/dừng, ví dụ như ứng dụng không web
Ứng dụng web
Sau khi gói ứng dụng, sẽ được xây dựng theo cấu trúc thư mục tương ứng. Nếu dự án sử dụng Maven, có thể sử dụng maven-assembly-plugin để xây dựng theo cấu trúc thư mục tương ứng.
Tức là dự án và ứng dụng đã gói cần được thực hiện theo phong cách thống nhất.
Hệ thống triển khai tự động
Hệ thống triển khai tự động chịu trách nhiệm gói ứng dụng (ví dụ: thực thi lệnh mvn tương ứng), trích xuất (từ thư mục chỉ định trích xuất mã nguồn cần triển khai, như target/nonweb-example-package), triển khai mã nguồn (phát hành mã nguồn, đồng bộ hóa mã nguồn đến máy chủ đích), khởi động/dừng ứng dụng (cấu hình script khởi động/dừng chỉ định và gọi).
Ngoài các chức năng này, hệ thống triển khai tự động còn có các chức năng như quản lý lịch sử phát hành (quay lại phiên bản trước), quản lý nhóm (ví dụ: các tệp cấu hình khác nhau cho các phòng máy khác nhau), quản lý cấu hình (ví dụ: thay đổi script khởi động/dừng, thay đổi tệp cấu hình [các tệp cấu hình khác nhau cho các phòng máy khác nhau], quản lý tham số [ví dụ: tham số jvm, v.v.]).
Máy chủ đích
Là máy mà mã nguồn được triển khai, nó chỉ cần cài đặt môi trường tối thiểu, ví dụ: chỉ cần cài đặt JDK, không cần cài đặt Tomcat, ứng dụng sẽ quyết định sử dụng container nào.
Bằng cách thêm hệ thống triển khai tự động, có thể triển khai, quản lý và quay lại dự án tốt hơn.
Ứng dụng Java "đóng gói"
Ứng dụng Java "đóng gói" có nghĩa là mã nguồn Java, container, tệp cấu hình, script khởi động/dừng, v.v. đều được duy trì tại cùng một nơi, thay đổi tệp cấu hình, thay đổi tham số môi trường, thay đổi loại container, v.v. đều không cần thay đổi trên máy chủ đích. Máy chủ đích chỉ cung cấp môi trường chạy cơ bản, ví dụ: chỉ cần triển khai môi trường JDK, không cần triển khai container như Tomcat, loại container cần thiết sẽ được chỉ định trong ứng dụng Java.
Lợi ích của việc này là tệp cấu hình, tham số JVM, lựa chọn container đều có thể được cấu hình trong ứng dụng Java, tạo thành một vòng lặp khép kín.
Mục đích chính của ứng dụng Java "đóng gói" là để ứng dụng Java có thể tự khởi động, do đó quyền kiểm soát chương trình nằm trong tay chúng ta, không phải trong tay bộ phận vận hành. Và chúng ta hiểu rõ hơn về chương trình của mình.
Với sự phổ biến của khái niệm microservices, Spring Boot cũng được nhiều người yêu thích. Spring Boot giúp chúng ta xây dựng ứng dụng dựa trên Spring một cách nhanh chóng; nó có thể tạo ứng dụng tự khởi động dễ dàng, có thể nhúng nhiều container (như Tomcat, Jetty), cung cấp một số starter pom để đơn giản hóa tệp cấu hình, tự động cấu hình (chỉ cần giới thiệu pom liên quan, sẽ tự động có được một số chức năng) v.v.
Trước khi giới thiệu Spring Boot, chúng ta hãy xem cách xây dựng ứng dụng Java "đóng gói" trước đây.
Xây dựng ứng dụng không web từ đầu
Cấu trúc dự án
Ví dụ này trình bày cách xây dựng một nhà cung cấp dịch vụ RPC (ví dụ: dịch vụ Dubbo), cũng có thể xây dựng các ứng dụng loại Worker, chúng không cần container web, chỉ cần khởi động như một ứng dụng Java thông thường.
Phụ thuộc Maven (pom.xml)
Cần tự thêm các phụ thuộc như spring-core, spring-context, v.v., ở đây không trình bày.
Cấu hình gói (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>
Sử dụng maven-assembly-plugin để gói; cấu hình gói như sau:
<id>package</id>
<formats>
<format>dir</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<!-- Tệp thực thi -->
<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>
<!-- Thư viện phụ thuộc -->
<dependencySets>
<dependencySet>
<outputDirectory>lib</outputDirectory>
<excludes>
<exclude>com.jd:nonweb-example</exclude>
</excludes>
</dependencySet>
</dependencySets>
Có ba nhóm cấu hình chính:
formats: định dạng gói, ở đây sử dụng dir, cũng có thể là zip, rar, v.v.;
fileSet: sao chép tệp, ví dụ này chủ yếu cần sao chép tệp bin, tệp classes;
dependencySets: thư viện phụ thuộc, sao chép vào thư mục lib;
Sau khi thực thi mvn package, sẽ có được cấu trúc sau:
Đưa thư mục này vào hệ thống triển khai tự động để trích xuất và triển khai lên máy chủ đích. Sau đó hệ thống triển khai tự động thực thi script khởi động/dừng trong thư mục bin.
Lớp khởi động
public class StartupManager {
public static void main(String[] args) throws Exception {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:spring-config.xml");
ctx.registerShutdownHook();
Thread.currentThread().join();
}
}
Ví dụ này không sử dụng cách xây dựng Java Config, trực tiếp tải tệp cấu hình Spring để khởi động ứng dụng Java.
Script khởi động
#!/bin/sh
echo -------------------------------------------
echo start server
echo -------------------------------------------
# Đặt đường dẫn mã nguồn dự án
export CODE_HOME="/export/App/nonweb-example-startup-package"
# Đường dẫn log
export LOG_PATH="/export/Logs/nonweb.example.jd.local"
mkdir -p $LOG_PATH
# Đặt đường dẫn phụ thuộc
export CLASSPATH="$CODE_HOME/classes:$CODE_HOME/lib/*"
# Vị trí tệp java thực thi
export _EXECJAVA="$JAVA_HOME/bin/java"
# Tham số khởi động JVM
export JAVA_OPTS="-server -Xms128m -Xmx256m -Xss256k -XX:MaxDirectMemorySize=128m"
# Lớp khởi động
export MAIN_CLASS=com.jd.nonweb.example.startup.StartupManager
$_EXECJAVA $JAVA_OPTS -classpath $CLASSPATH $MAIN_CLASS &
tail -f $LOG_PATH/stdout.log
Cấu hình đường dẫn mã nguồn dự án, đường dẫn log, đường dẫn phụ thuộc, đường dẫn tệp java thực thi, tham số khởi động JVM, lớp khởi động.
Script dừng
# Đường dẫn log
export LOG_PATH="/export/Logs/nonweb.example.jd.local"
mkdir -p $LOG_PATH
# Lớp khởi động
export MAIN_CLASS=com.jd.nonweb.example.startup.StartupManager
echo -------------------------------------------
echo stop server
# Tất cả các tiến trình liên quan
PIDs=`jps -l | grep $MAIN_CLASS | awk '{print $1}'`
# Dừng tiến trình
if [ -n "$PIDs" ]; then
for PID in $PIDs; do
kill $PID
echo "kill $PID"
done
fi
# Chờ 50 giây
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
# Nếu sau 50 giây vẫn chưa dừng xong, dừng ngay lập tức
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
Đến đây, một ứng dụng không web "đóng gói" đã được xây dựng xong, script khởi động/dừng, lớp khởi động, mã nguồn dự án đều được duy trì thống nhất tại một nơi, và sử dụng maven-assembly-plugin để gói chúng lại, phát hành và thực thi thông qua hệ thống triển khai, đạt được mục đích "đóng gói".
Xây dựng ứng dụng web từ đầu
Cấu trúc dự án
Phụ thuộc Maven (pom.xml)
Cần tự thêm các phụ thuộc như spring-core, spring-context, spring-web, spring-webmvc, velocity, v.v., ở đây không trình bày.
Cấu hình gói (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>
Sử dụng maven-assembly-plugin để gói; cấu hình gói như sau:
<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>
<!-- Thư viện phụ thuộc -->
<dependencySets>
<dependencySet>
<outputDirectory>WEB-INF/lib</outputDirectory>
<excludes>
<exclude>com.jd:web-example</exclude>
</excludes>
</dependencySet>
</dependencySets>
Có ba nhóm cấu hình chính:
formats: định dạng gói, ở đây sử dụng dir, cũng có thể là zip, rar, v.v.;
fileSet: sao chép tệp, ví dụ này chủ yếu cần sao chép tệp bin, tệp classes, tệp webapp;
dependencySets: thư viện phụ thuộc, sao chép vào thư mục WEB-INF\lib;
Sau khi thực thi mvn package, sẽ có được cấu trúc sau:
Cấu trúc thư mục gói và cấu trúc web thông thường hoàn toàn giống nhau; đưa thư mục này vào hệ thống triển khai tự động để trích xuất và phát hành lên máy chủ đích. Sau đó hệ thống triển khai tự động thực thi script khởi động/dừng trong thư mục bin.
Lớp khởi động
public class WebContainerBootstrap {
private static final Logger LOG = LoggerFactory.getLogger(WebContainerBootstrap.class);
public static void main(String[] args) throws Exception{
// Tối ưu hóa hiệu suất(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("stop tomcat 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(createNioConnector());
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 particular java/javax APIs
server.addLifecycleListener(new JreMemoryLeakPreventionListener());
return tomcat;
}
// Tại đây điều chỉnh các tham số để tối ưu hóa
private static Connector createNioConnector() {
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();
return connector;
}
}
Khởi động bằng cách nhúng container Tomcat, nhược điểm của phương pháp này là cần viết mã khởi động Tomcat trước, ưu điểm cũng rất rõ ràng: sau này quyền kiểm soát Tomcat nằm trong tay chúng ta, có thể thay đổi hoặc tối ưu hóa bất cứ lúc nào, không cần thay đổi tệp cấu hình trực tuyến.
Script khởi động
#!/bin/sh
echo -------------------------------------------
echo start server
echo -------------------------------------------
# Đặt đường dẫn mã nguồn dự án
export CODE_HOME="/export/App/web-example-web-package"
# Đường dẫn log
export LOG_PATH="/export/Logs/web.example.jd.local"
mkdir -p $LOG_PATH
# Đặt đường dẫn phụ thuộc
export CLASSPATH="$CODE_HOME/WEB-INF/classes:$CODE_HOME/WEB-INF/lib/*"
# Vị trí tệp java thực thi
export _EXECJAVA="$JAVA_HOME/bin/java"
# Tham số khởi động JVM
export JAVA_OPTS="-server -Xms128m -Xmx256m -Xss256k -XX:MaxDirectMemorySize=128m"
# Cấu hình cổng, đường dẫn context, đường dẫn gốc dự án
export SERVER_INFO="-Dserver.port=8090 -Dserver.contextPath=-Dserver.docBase=$CODE_HOME"
# Lớp khởi động
export MAIN_CLASS=com.jd.web.example.startup.WebContainerBootstrap
$_EXECJAVA $JAVA_OPTS -classpath $CLASSPATH $SERVER_INFO $MAIN_CLASS &
tail -f $LOG_PATH/stdout.log
Cấu hình đường dẫn mã nguồn dự án, đường dẫn log, đường dẫn phụ thuộc, đường dẫn tệp java thực thi, tham số khởi động JVM, lớp khởi động; tương tự như ứng dụng không web, thêm cấu hình cổng, đường dẫn context, đường dẫn gốc dự án của máy chủ web.
Script dừng
Tương tự như ứng dụng không web, không lặp lại ở đây.
Đến đây, một ứng dụng web "đóng gói" đã được xây dựng xong, script khởi động/dừng, lớp khởi động, mã nguồn dự án đều được duy trì thống nhất tại một nơi, và sử dụng maven-assembly-plugin để gói chúng lại, phát hành và thực thi thông qua hệ thống triển khai. Đạt được mục đích "đóng gói".
Xây dựng ứng dụng web/không web bằng Spring Boot
Cấu trúc dự án
Phụ thuộc Maven (pom.xml)
spring-boot-example/pom.xml kế thừa 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 là một số cấu hình chung, ví dụ: mã hóa JDK, quản lý phụ thuộc (nó kế thừa spring-boot-dependencies, nơi định nghĩa tất cả các phụ thuộc); phụ thuộc
<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 là môi trường spring boot tối thiểu (spring-core, spring-context, v.v.); spring-boot-starter-web là môi trường spring mvc, sử dụng Tomcat làm container web; spring-boot-starter-velocity sẽ tự động cấu hình template engine thành velocity. Ở đây có thể thấy lợi ích của starter, chỉ cần chức năng gì, chỉ cần giới thiệu một starter, các phụ thuộc liên quan sẽ tự động được thêm, và sẽ tự động cấu hình sử dụng tính năng đó.
Cấu hình gói (pom.xml)
spring-boot-example-web\pom.xml thêm plugin Maven sau:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
Thực thi mvn package sẽ nhận được fat jar như sau:
Lớp khởi động
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 ApplicationBootstrap {
public static void main(String[] args) {
SpringApplication.run(ApplicationBootstrap.class, args);
}
}
@SpringBootApplication chỉ định gói cần quét, có thể sử dụng @ImportResource để giới thiệu tệp cấu hình xml. Sau đó có thể khởi động như một ứng dụng Java thông thường, tại thời điểm này tự động sử dụng tomcat làm container web để khởi động.
Chạy jar -jar spring-boot-example-1.0-SNAPSHOT.jar để khởi động (META-INF\MANIFEST.MF chỉ định Main-Class).
Cá nhân không thích phương pháp fat jar. Có thể sử dụng maven-assembly-plugin để gói ứng dụng Java. Cấu trúc dự án như sau:
Cấu trúc dự án khác với trước đây là thêm assembly và bin.
Cấu hình gói (pom.xml)
spring-boot-example-web\pom.xml thay đổi plugin Maven sau
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
Thành 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 tương tự như "Xây dựng ứng dụng không web từ đầu", không trình bày cấu hình ở đây.
Sau khi thực thi mvn package, sẽ nhận được gói như sau:
Script khởi động/dừng cũng tương tự, không trình bày cấu hình ở đây. Đến đây, ứng dụng Java tự khởi động dựa trên spring boot không sử dụng fat jar đã được xây dựng xong.