Building Closed‑Loop Java Applications: From Zero to Deployment with Maven and Spring Boot
This article explains the common pitfalls of manual Java deployment, introduces the concept of a closed‑loop Java application, and provides step‑by‑step Maven‑assembly and Spring Boot configurations, scripts, and code examples for both non‑web and web projects to achieve automated, reproducible deployments.
The author, a senior Java engineer at JD.com, shares a practical guide for eliminating painful manual deployment steps by creating a closed‑loop Java application that bundles code, configuration, container, and start‑stop scripts together.
Typical problems in traditional deployments include manual uploading of code and dependencies, inconsistent directory structures, fat‑jar packaging that makes configuration changes difficult, reliance on external web containers like Tomcat, and the need to involve operations staff for every change.
A closed‑loop Java application keeps all artefacts in a single location, allowing configuration files, JVM parameters, and container choices to be modified without touching the host machine; the host only needs a minimal JDK runtime.
Deployment workflow : the project contains the application code and start‑stop scripts, an automated deployment system packages the project (using Maven), extracts the package on the host, and runs the scripts. The Maven‑assembly‑plugin is used to create a directory‑based package that includes binaries, classes, and dependencies.
Non‑web example – Maven assembly configuration :
<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>The assembly.xml defines three groups of configuration: formats (using dir ), fileSets for binaries and classes, and dependencySets for external JARs.
Startup class for the non‑web application :
public class Bootstrap {
public static void main(String[] args) throws Exception {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:spring-config.xml");
ctx.registerShutdownHook();
Thread.currentThread().join();
}
}Start script (shell) :
#!/bin/sh
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/*"
export _EXECJAVA="$JAVA_HOME/bin/java"
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.logStop script (shell) :
# Logs path
export LOG_PATH="/export/Logs/nonweb.example.jd.local"
mkdir -p $LOG_PATH
export MAIN_CLASS=com.jd.nonweb.example.startup.Bootstrap
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
# Wait up to 50 seconds
for i in 1 10; do
PIDs=`jps -l | grep $MAIN_CLASS | awk '{print $1}'`
if [ ! -n "$PIDs" ]; then echo "stop server success"; break; fi
echo "sleep 5s"; sleep 5
done
# Force kill if still running
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.logThe same approach is applied to a web application, with the only difference being the inclusion of a web‑app directory and the use of an embedded Tomcat container.
Web example – Maven assembly configuration (similar to the non‑web one, but with additional fileSet entries for src/main/webapp and WEB-INF/lib ).
Embedded Tomcat startup class :
public class TomcatBootstrap {
private static final Logger LOG = LoggerFactory.getLogger(TomcatBootstrap.class);
public static void main(String[] args) throws Exception {
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(() -> {
try { tomcat.stop(); } catch (LifecycleException e) { LOG.error("stop tomcat error.", e); }
}));
tomcat.getServer().await();
}
// helper methods omitted for brevity
}Web start script (shell) :
#!/bin/sh
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/*"
export _EXECJAVA="$JAVA_HOME/bin/java"
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 $SERVER_INFO -classpath $CLASSPATH $MAIN_CLASS &
tail -f $LOG_PATH/stdout.logSpring Boot approach : using spring-boot-starter-parent and the spring-boot-maven-plugin (or the assembly plugin for a non‑fat‑jar). The pom includes starters such as spring-boot-starter , spring-boot-starter-web , and spring-boot-starter-velocity . The bootstrap class is minimal:
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);
}
}Running java -jar spring-boot-example-1.0-SNAPSHOT.jar starts the embedded Tomcat automatically. The author prefers the assembly‑plugin approach to avoid large fat‑jars, especially when deploying inside Docker containers where only a single JVM runs per host.
Conclusion : Building a closed‑loop Java application with Maven assembly (or Spring Boot) provides a reproducible, automated deployment pipeline, simplifies JVM tuning and container configuration, and removes the need for operations staff to modify files on the host.
Qunar Tech Salon
Qunar Tech Salon is a learning and exchange platform for Qunar engineers and industry peers. We share cutting-edge technology trends and topics, providing a free platform for mid-to-senior technical professionals to exchange and learn.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.