Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Boot uber jar packaging classes to root instead of BOOT-INF/classes

Hi Spring Boot Experts -

I am trying to create a spring boot uber jar that needs to be deployed to a apache storm cluster. But, the catch is that Storm is expecting all the class files in the root of the jar while the packaged app files are under "BOOT-INF/classes" when packaged using the "spring-boot-maven-plugin".

Is there a way I can have my app classes packaged directly under the root instead of "BOOT-INF/classes"?

I tried using the "maven-assembly-plugin" with the "spring-boot-maven-plugin" as shown below which creates the Uber jar with all the class files from the dependency jars packaged at the root of the uber jar, but the app classes are still at BOOT-INF/classes.

<plugins>
    <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
            <excludes>
                <exclude>
                    <groupId>org.apache.storm</groupId>
                    <artifactId>storm-core</artifactId>
                </exclude>
            </excludes>
            <requiresUnpack>
                <dependency>
                    <groupId>com.myorg</groupId>
                    <artifactId>my-app-artifact</artifactId> <!-- This does not help! :( -->
                </dependency>
            </requiresUnpack>
        </configuration>
    </plugin>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-assembly-plugin</artifactId>
        <version>2.4</version>
        <configuration>
            <appendAssemblyId>false</appendAssemblyId>
            <descriptorRefs>
                <descriptorRef>jar-with-dependencies</descriptorRef>
            </descriptorRefs>
        </configuration>
        <executions>
            <execution>
                <id>make-assembly</id>
                <phase>package</phase>
                <goals>
                    <goal>single</goal>
                </goals>
            </execution>
        </executions>
    </plugin>
</plugins>
like image 648
Satya Avatar asked Nov 11 '16 22:11

Satya


2 Answers

So, for my future self or for anyone who is trying to find an answer for a similar question. Here are the different things that I realized during my research for this -

  1. Storm wants an executable java jar file
  2. Spring Boot provides a custom jar packaging. While it confirms with java jar packaging, Spring Boot loads the classes from the BOOT-INF/classes

So, to make a Spring Boot jar work on the storm cluster while behaving as Spring Boot - we would need to create a copy of all the classes from BOOT-INF/classes to the root of the jar file.

Is this possible? and the answer is yes.

Using the approach describe here, I was able to create a Spring Boot jar with the BOOT-INF/classes copied to the root of the Spring Boot jar. This approach requires ant build.xml, ivy settings and an ivy.xml as shown below. (disclaimer: config tested only till packaging on not on the storm cluster)

Since we are able to create a Spring Boot Jar hacked with classes at the root -

Should we do it? NO.

Here are the reasons -

  1. Spring strongly advises not taking this approach to not end up with unwanted class overwrite and class versioning issues for classes with same names across jar files and with different versions.

  2. Spring Boot Jar packaging is not a format intended for using as a dependency jar. Read the first line here. Hence for dependency use cases, you need to stick with your plain old java modules. Spring Boot is for more of standalone executables or for deployment on containers like tomcat.

Good luck!

build.xml

<project
        xmlns:ivy="antlib:org.apache.ivy.ant"
        xmlns:spring-boot="antlib:org.springframework.boot.ant"
        name="spring-boot-sample-ant"
        default="build">

    <description>
        Sample ANT build script for a Spring Boot executable JAR project. Uses ivy for
        dependency management and spring-boot-antlib for additional tasks. Run with
        '$ ant -lib ivy-2.2.jar spring-boot-antlib.jar' (substitute the location of your
        actual jars). Run with '$ java -jar target/*.jar'.
    </description>

    <property name="spring-boot.version" value="1.4.2.RELEASE" />
    <property name="lib.dir" location="${basedir}/target/lib" />
    <property name="start-class" value="com.my.main.class" />

    <target name="resolve" description="--> retrieve dependencies with ivy">
        <ivy:retrieve pattern="${lib.dir}/[conf]/[artifact]-[type]-[revision].[ext]" />
    </target>

    <target name="classpaths" depends="resolve">
        <path id="compile.classpath">
            <fileset dir="${lib.dir}/compile" includes="*.jar" />
        </path>
    </target>

    <target name="init" depends="classpaths">
        <mkdir dir="target/classes" />
    </target>

    <target name="compile" depends="init" description="compile">
        <javac srcdir="src/main/java" destdir="target/classes" classpathref="compile.classpath" />
    </target>

    <target name="clean" description="cleans all created files/dirs">
        <delete dir="target" />
    </target>

    <target name="build" depends="compile">
        <spring-boot:exejar destfile="target/${ant.project.name}-${spring-boot.version}.jar" classes="target/classes">
            <spring-boot:lib>
                <fileset dir="${lib.dir}/runtime" />
            </spring-boot:lib>
        </spring-boot:exejar>
    </target>

    <target name="unjar_dependencies" depends="compile">
        <unzip dest="target/classes">
            <fileset dir="${lib.dir}/compile">
                <include name="my-app-common-0.1-SNAPSHOT.jar" />
            </fileset>
        </unzip>
    </target>

    <!-- Manual equivalent of the build target -->
    <target name="manual" depends="compile, unjar_dependencies">
        <jar destfile="target/manual/${ant.project.name}-${spring-boot.version}.jar" compress="false">
            <mappedresources>
                <fileset dir="target/classes" />
                <globmapper from="*" to="BOOT-INF/classes/*"/>
            </mappedresources>
            <mappedresources>  <!-- **** this mapped resources block does what I was looking for **** -->
                <fileset dir="target/classes" />
                <globmapper from="*" to="/*"/> 
            </mappedresources>
            <mappedresources>
                <fileset dir="src/main/resources" erroronmissingdir="false"/>
                <globmapper from="*" to="BOOT-INF/classes/*"/>
            </mappedresources>
            <mappedresources>
                <fileset dir="${lib.dir}/runtime" />
                <globmapper from="*" to="BOOT-INF/lib/*"/>
            </mappedresources>
            <zipfileset src="${lib.dir}/loader/spring-boot-loader-jar-${spring-boot.version}.jar" />
            <manifest>
                <attribute name="Main-Class" value="org.springframework.boot.loader.JarLauncher" />
                <attribute name="Start-Class" value="${start-class}" />
            </manifest>
        </jar>
    </target>
</project>

ivysettings.xml

<ivysettings>
    <settings defaultResolver="chain" />
    <resolvers>
        <chain name="chain" returnFirst="true">
            <!-- NOTE: You should declare only repositories that you need here -->
            <filesystem name="local" local="true" m2compatible="true">
                <artifact pattern="${user.home}/.m2/repository/[organisation]/[module]/[revision]/[module]-[revision].[ext]" />
                <ivy pattern="${user.home}/.m2/repository/[organisation]/[module]/[revision]/[module]-[revision].pom" />
            </filesystem>
            <ibiblio name="ibiblio" m2compatible="true" />
            <ibiblio name="spring-milestones" m2compatible="true" root="http://repo.spring.io/release" />
            <ibiblio name="spring-milestones" m2compatible="true" root="http://repo.spring.io/milestone" />
            <ibiblio name="spring-snapshots" m2compatible="true" root="http://repo.spring.io/snapshot" />
        </chain>
    </resolvers>
</ivysettings>

ivy.xml

<ivy-module version="2.0">
    <info organisation="org.springframework.boot" module="spring-boot-sample-ant" />
    <configurations>
        <conf name="compile" description="everything needed to compile this module" />
        <conf name="runtime" extends="compile" description="everything needed to run this module" />
        <conf name="loader" description="Spring Boot loader used when manually building an executable archive" />
    </configurations>
    <dependencies>
        <dependency org="org.springframework.boot" name="spring-boot-starter" rev="${spring-boot.version}" conf="compile">
                <exclude org="ch.qos.logback" name="logback-classic"/>
        </dependency>

        <dependency org="org.springframework.boot" name="spring-boot-loader" rev="${spring-boot.version}" conf="loader->default" />

        <dependency org="org.apache.storm" name="storm-core" rev="1.0.2">
            <exclude org="org.apache.logging.log4j" name="log4j-slf4j-impl"/>
            <exclude org="org.apache.logging.log4j" name="log4j-core"/>
        </dependency>

        <dependency org="com.mycompany" name="app-common" rev="0.1-SNAPSHOT"/>

        <dependency org="org.apache.storm" name="storm-kafka" rev="1.0.2"/>

        <dependency org="org.apache.kafka" name="kafka_2.10" rev="0.10.1.0"/>

        <dependency org="org.apache.kafka" name="kafka_2.10" rev="0.10.1.0"/>

        <dependency org="org.apache.httpcomponents" name="httpcomponents-client" rev="4.5.2"/>

        <dependency org="org.eclipse.paho" name="org.eclipse.paho.client.mqttv3" rev="1.1.0"/>

        <dependency org="com.amazonaws" name="aws-java-sdk-s3" rev="1.11.53"/>

        <dependency org="com.jcraft" name="jsch" rev="0.1.54"/>

        <dependency org="io.netty" name="netty-handler" rev="3.7.0.Final"/>


    </dependencies>
</ivy-module>
like image 107
Satya Avatar answered Oct 23 '22 23:10

Satya


Is there a way I can have my app classes packaged directly under the root instead of "BOOT-INF/classes"?

Yes, you just need to use Spring Boot 1.3. Back to maven... in your pom.xml if you declare your parent like this:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.3.5.RELEASE</version>
</parent>

then your classes (and other files) will be placed at the root level. This is the "old way" for spring boot.

In version 1.4 they changed the spring boot jar structure to use the BOOT-INF directory. So, if you use <version>1.4.1.RELEASE</version> for example, then your classes will be under BOOT-INF/classes. An undesirable side effect is that your configuration files (e.g., application.properties, application-myprofile.properties, etc.) will also be under BOOT-INF/classes, even though they are not Java classes.

like image 41
Paulo Merson Avatar answered Oct 24 '22 01:10

Paulo Merson