mavenmaven-assembly-pluginor-toolsspring-boot-maven-plugin

Adding a JNI library to a spring boot (maven) jar


I'm using Google Or-Tools library over a Java-Spring-Boot app, Windows 10 OS and Intellij IDE. To make it work over intellij I did the following:

  1. Install Microsoft Visual C++ Redistributable for Visual Studio 2019 (required according to the installation instructions).

  2. Downloaded and extracted the OR-Tools library for Java (included 2 jars and a 1 dll file).

  3. In Intellij, I added those jars as module dependencies (under a folder called lib).

  4. Added the lib library path to VM options in Intellij run configurations.

  5. Loaded the library statically in my code:

    static {System.loadLibrary("jniortools");}

Now I can run the project successfully form Intellij. Next I would like to pack everything to a spring boot jar that can run over any windows machine.

My folders structure is:

enter image description here

My pom file is pretty basic, a few dependencies with a standard spring-boot-maven-plugin:

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

As I'm trying to pack the code using mvn install I'm getting package com.google.ortools.sat does not exist.

How can I make sure maven packs those 3rd party jars to the executable spring-boot jar?

UPDATE

I added to my pom file:

<dependency>
    <groupId>com.google.ortools</groupId>
    <artifactId>ortools</artifactId>
    <version>LATEST</version>
    <type>jar</type>
    <scope>system</scope>
    <systemPath>${project.basedir}/lib/com.google.ortools.jar</systemPath>
</dependency>
<dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java</artifactId>
    <version>LATEST</version>
    <type>jar</type>
    <scope>system</scope>
    <systemPath>${project.basedir}/lib/protobuf.jar</systemPath>
</dependency>


<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-antrun-plugin</artifactId>
            <version>1.7</version>
            <executions>
                <execution>
                    <phase>generate-test-sources</phase>
                    <configuration>
                        <tasks>
                            <mkdir dir="${project.basedir}/target/lib"/>
                            <echo message="Creating lib folder..."/>
                            <copy todir="${project.basedir}/target/lib">
                                <fileset dir="${project.basedir}/lib">
                                    <include name="**/**"/>
                                </fileset>
                            </copy>
                        </tasks>
                    </configuration>
                    <goals>
                        <goal>run</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

In addition adding to library path:

static {
    try {
        String orToolsDllLibrary = System.getProperty("user.dir") + "\\lib";
        addLibraryPath(orToolsDllLibrary);
        System.loadLibrary("jniortools");
    } catch (Exception e) {
        e.printStackTrace();
    }
}

public static void addLibraryPath(String pathToAdd) throws Exception {
    final Field usrPathsField = ClassLoader.class.getDeclaredField("usr_paths");
    usrPathsField.setAccessible(true);

    //get array of paths
    final String[] paths = (String[]) usrPathsField.get(null);

    //check if the path to add is already present
    for (String path : paths) {
        if (path.equals(pathToAdd)) {
            return;
        }
    }

    //add the new path
    final String[] newPaths = Arrays.copyOf(paths, paths.length + 1);
    newPaths[newPaths.length - 1] = pathToAdd;
    usrPathsField.set(null, newPaths);
}

And now when running command java -jar myApp-0.0.1-SNAPSHOT.jar getting an exception:

Caused by: java.lang.NoClassDefFoundError: com/google/ortools/sat/CpSolver
        at java.base/java.lang.Class.getDeclaredMethods0(Native Method) ~[na:na]
        at java.base/java.lang.Class.privateGetDeclaredMethods(Class.java:3167) ~[na:na]
        at java.base/java.lang.Class.getDeclaredMethods(Class.java:2310) ~[na:na]
        at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:463) ~[spring-core-5.2.6.RELEASE.jar!/:5.2.6.RELEASE]
        ... 29 common frames omitted
Caused by: java.lang.ClassNotFoundException: com.google.ortools.sat.CpSolver
        at java.base/java.net.URLClassLoader.findClass(URLClassLoader.java:471) ~[na:na]
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:588) ~[na:na]
        at org.springframework.boot.loader.LaunchedURLClassLoader.loadClass(LaunchedURLClassLoader.java:129) ~[solver-0.0.1-SNAPSHOT.jar:0.0.1-SNAPSHOT]
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521) ~[na:na]
        ... 33 common frames omitted

Solution

  • I am not sure how you added the library to your project? You don't seem to have done it through Maven, did you?

    In the past I took the approach of adding it via using system scope in Maven (see here. This would give you something like this in your pom:

    <dependency>
        <groupId>com.google.ortools</groupId>
        <artifactId>ortools</artifactId>
        <version>1.0</version>
        <scope>system</scope>
        <systemPath>${basedir}/src/main/resources/lib/com.google.ortools.jar</systemPath>
    </dependency>
    

    However, this approach can also be a pain especially if you have to work multi-platform. Recently, I found this repo and that made my life much easier dealing with OR-tools. Hope this helps.

    UPDATE: I strongly recommend using the updated method below as it is much less of a headache:

    <repositories>
        ...
        <repository>
            <id>bintray</id>
            <url>https://dl.bintray.com/magneticflux/maven</url>
        </repository>
        ....
    </repositories>
    
    <dependencies>
      <dependency>
           <groupId>com.skaggsm.ortools</groupId>
           <artifactId>ortools-natives-all</artifactId>
           <version>7.7.7810</version>
      </dependency>
      <!-- OR-tools needs protobuf       -->
       <dependency>
           <groupId>com.google.protobuf</groupId>
           <artifactId>protobuf-java</artifactId>
           <version>3.12.2</version>
       </dependency>
    </dependencies>
    

    Then you can do a static load of the library:

    static {
        OrToolsHelper.loadLibrary()
    }
    

    Make sure to work with JDK >= 11 as elaborated here.