Сегодняшняя статья будет нацелена на интересную, но довольно мощную концепцию: упаковывать ваше приложение в один исполняемый файл JAR, также известный как один или толстый JAR.
Мы привыкаем к большим архивам WAR, которые содержат все зависимости, упакованные вместе в какую-то общую структуру папок. С JAR-подобной упаковкой история немного другая: для того, чтобы сделать ваше приложение работоспособным (через java -jar ), все зависимости должны быть предоставлены через параметр classpath или переменную окружения. Обычно это означает, что там будет какая-нибудь папка lib со всеми зависимостями и какой-нибудь исполняемый скрипт, который будет выполнять работу по созданию classpath и запуску JVM. Плагин Maven Assembly хорошо известен для распространения приложений такого рода.
Несколько иной подход заключается в том, чтобы упаковать все зависимости вашего приложения в один и тот же файл JAR и сделать его работоспособным без каких-либо дополнительных параметров или сценариев. Звучит здорово, но … это не сработает, если вы не добавите немного волшебства: познакомьтесь с проектом One-JAR .
Давайте кратко опишем проблему: мы пишем отдельное приложение Spring, которое можно запустить, просто набрав java -jar <our-app.jar> .
Как всегда, давайте начнем с нашего файла POM, который будет довольно простым
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemalocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelversion>4.0.0</modelversion>
<groupid>com.example</groupid>
<artifactid>spring-one-jar</artifactid>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-one-jar</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceencoding>UTF-8</project.build.sourceencoding>
<org.springframework.version>3.1.1.RELEASE</org.springframework.version>
</properties>
<dependencies>
<dependency>
<groupid>cglib</groupid>
<artifactid>cglib-nodep</artifactid>
<version>2.2</version>
</dependency>
<dependency>
<groupid>org.springframework</groupid>
<artifactid>spring-core</artifactid>
<version>${org.springframework.version}</version>
</dependency>
<dependency>
<groupid>org.springframework</groupid>
<artifactid>spring-context</artifactid>
<version>${org.springframework.version}</version>
</dependency>
</dependencies>
</project>
Наше примерное приложение загрузит контекст Spring , получит некоторый экземпляр компонента и вызовет для него метод. Наш компонент называется SimpleBean и выглядит так:
package com.example;
public class SimpleBean {
public void print() {
System.out.println( "Called from single JAR!" );
}
}
Falling in love with Spring Java configuration, let us define our context as annotated AppConfig POJO:
package com.example.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.example.SimpleBean;
@Configuration
public class AppConfig {
@Bean
public SimpleBean simpleBean() {
return new SimpleBean();
}
}
And finally, our application Starter with main():
package com.example;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import com.example.config.AppConfig;
public class Starter {
public static void main( final String[] args ) {
ApplicationContext context = new AnnotationConfigApplicationContext( AppConfig.class );
SimpleBean bean = context.getBean( SimpleBean.class );
bean.print();
}
}
Adding our main class to META-INF/MANIFEST.MF allows to leverage Java capabilities to run JAR file without explicitly specifying class with main() method. Maven JAR plugin can help us with that.
<build>
<plugins>
<plugin>
<groupid>org.apache.maven.plugins</groupid>
<artifactid>maven-jar-plugin</artifactid>
<configuration>
<archive>
<manifest>
<mainclass>com.example.Starter</mainclass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
Trying to run java -jar spring-one-jar-0.0.1-SNAPSHOT.jar will print the exception to the console: java.lang.NoClassDefFoundError. The reason is pretty straightforward: even such a simple application as this one already required following libraries to be in classpath.
aopalliance-1.0.jar
cglib-nodep-2.2.jar
commons-logging-1.1.1.jar
spring-aop-3.1.1.RELEASE.jar
spring-asm-3.1.1.RELEASE.jar
spring-beans-3.1.1.RELEASE.jar
spring-context-3.1.1.RELEASE.jar
spring-core-3.1.1.RELEASE.jar
spring-expression-3.1.1.RELEASE.jar
Let’s see what One-JAR can do for us here. Thanks to availability of onejar-maven-plugin we can add one to the plugins section of our POM file.
<plugin>
<groupid>org.dstovall</groupid>
<artifactid>onejar-maven-plugin</artifactid>
<version>1.4.4</version>
<executions>
<execution>
<configuration>
<onejarversion>0.97</onejarversion>
<classifier>onejar</classifier>
</configuration>
<goals>
<goal>one-jar</goal>
</goals>
</execution>
</executions>
</plugin>
Also, pluginRepositories section should contain this repository in order to download the plugin.
<pluginrepositories>
<pluginrepository>
<id>onejar-maven-plugin.googlecode.com</id>
<url>http://onejar-maven-plugin.googlecode.com/svn/mavenrepo</url>
</pluginrepository>
</pluginrepositories>
As the result, there will be another artifact available in the target folder, postfixed with one-jar: spring-one-jar-0.0.1-SNAPSHOT.one-jar.jar. Running this one with java -jar spring-one-jar-0.0.1-SNAPSHOT.one-jar.jar will print to the console:
Called from single JAR!
Fully runnable Java application as single, redistributable JAR file! The last comment: though our application looks pretty simple, One-JAR works perfectly for complex, large applications as well without any issues. Please, add it to your toolbox, it’s really useful tool to have.
Thanks to One-JAR guys!