javamavenclojureclojure-java-interop

How to include Clojure dependencies to a Java project with Maven


I'm very new to all things JVM and want to start a Java project that involves a Clojure library as a dependency. I've seen this question on how to run Clojure code from Java, but when I try to run the jar file after mvn package, I get cannot find symbol for variable Clojure. My code looks like this so far:

package org.example;

import clojure.java.api.Clojure;
import clojure.lang.IFn;

public class App 
{
    public static void main( String[] args )
    {

        IFn plus = Clojure.var("clojure.core", "+");
    }
}

So far, my pom file looks like this:

<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/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>org.example</groupId>
  <artifactId>project</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>poi</name>
  <properties>
    <maven.compiler.source>1.6</maven.compiler.source>
    <maven.compiler.target>1.6</maven.compiler.target>
  </properties>
  <url>http://maven.apache.org</url>
  <repositories>
    <repository>
      <id>clojars</id>
      <url>https://repo.clojars.org/</url>
      <snapshots>
        <enabled>true</enabled>
      </snapshots>
      <releases>
        <enabled>true</enabled>
      </releases>
    </repository>
  </repositories>
  <build>
  <plugins>
    <plugin>
      <groupId>com.theoryinpractise</groupId>
      <artifactId>clojure-maven-plugin</artifactId>
      <version>1.8.3</version>
      <extensions>true</extensions>
    </plugin>
  </plugins>
  </build>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.clojure</groupId>
      <artifactId>clojure</artifactId>
      <version>1.10.1</version>
    </dependency>
    <dependency>
      <groupId>clj-python</groupId>
      <artifactId>libpython-clj</artifactId>
      <version>1.45</version>
    </dependency>
  </dependencies>
</project>

The clojure-maven-plugin seemed to download the dependencies (I watched the usual downloads fly up the screen), but still no luck on invoking Clojure after an import.

Ultimately, I hope to be able to reference libpython-clj from within Java.

Update

I tried Alan Thompson's answer and needed to run lein pom to get a pom.xml file. Then I needed to add the following to the pom at the project level to get it to mvn -q compile <properties> <maven.compiler.source>1.6</maven.compiler.source> <maven.compiler.target>1.6</maven.compiler.target> </properties>

However, at mvn -q exec gives me long stack trace ending with

[ERROR] Failed to execute goal org.codehaus.mojo:exec-maven-plugin:3.0.0:java (default-cli) on project demo: An exception occured while executing the Java class. example.Main -> [Help 1]
org.apache.maven.lifecycle.LifecycleExecutionException: Failed to execute goal org.codehaus.mojo:exec-maven-plugin:3.0.0:java (default-cli) on project demo: An exception occured while executing the Java class. example.Main
    at org.apache.maven.lifecycle.internal.MojoExecutor.execute(MojoExecutor.java:216)
    at org.apache.maven.lifecycle.internal.MojoExecutor.execute(MojoExecutor.java:153)
    at org.apache.maven.lifecycle.internal.MojoExecutor.execute(MojoExecutor.java:145)
    at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject(LifecycleModuleBuilder.java:116)
    at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject(LifecycleModuleBuilder.java:80)
    at org.apache.maven.lifecycle.internal.builder.singlethreaded.SingleThreadedBuilder.build(SingleThreadedBuilder.java:51)
    at org.apache.maven.lifecycle.internal.LifecycleStarter.execute(LifecycleStarter.java:120)
    at org.apache.maven.DefaultMaven.doExecute(DefaultMaven.java:347)
    at org.apache.maven.DefaultMaven.execute(DefaultMaven.java:154)
    at org.apache.maven.cli.MavenCli.execute(MavenCli.java:582)
    at org.apache.maven.cli.MavenCli.doMain(MavenCli.java:214)
    at org.apache.maven.cli.MavenCli.main(MavenCli.java:158)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.codehaus.plexus.classworlds.launcher.Launcher.launchEnhanced(Launcher.java:289)
    at org.codehaus.plexus.classworlds.launcher.Launcher.launch(Launcher.java:229)
    at org.codehaus.plexus.classworlds.launcher.Launcher.mainWithExitCode(Launcher.java:415)
    at org.codehaus.plexus.classworlds.launcher.Launcher.main(Launcher.java:356)
Caused by: org.apache.maven.plugin.MojoExecutionException: An exception occured while executing the Java class. example.Main
    at org.codehaus.mojo.exec.ExecJavaMojo.execute(ExecJavaMojo.java:311)
    at org.apache.maven.plugin.DefaultBuildPluginManager.executeMojo(DefaultBuildPluginManager.java:132)
    at org.apache.maven.lifecycle.internal.MojoExecutor.execute(MojoExecutor.java:208)
    ... 19 more
Caused by: java.lang.ClassNotFoundException: example.Main
    at java.base/java.net.URLClassLoader.findClass(URLClassLoader.java:471)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:588)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
    at org.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:246)
    at java.base/java.lang.Thread.run(Thread.java:834)
[ERROR]
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException

Update 2

It seems running mvn clean install && java -jar target/<whatever-it's called>.jar works when you add the following snippet to the pom.xml within the <plugins> section.

  <plugin>
    <artifactId>maven-assembly-plugin</artifactId>
    <configuration>
      <archive>
        <manifest>
          <mainClass>demo.Main</mainClass>
        </manifest>
      </archive>
      <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>

Solution

  • Intro

    I have a working demo for you using lein to build. For the Maven part, the example project at the end.


    Using lein to build

    Files:

    ~/expr/demo > ls -ldF  **/*.{java,clj}
    -rwxr-xr-x  1 alanthompson  staff  904 Jul 24 13:25 project.clj*
    -rw-r--r--  1 alanthompson  staff  130 Jul 24 13:24 src/clj/demo/core.clj
    -rw-r--r--  1 alanthompson  staff  373 Jul 24 13:17 src/java/demo/Main.java
    -rw-r--r--  1 alanthompson  staff  129 Jul 24 13:20 test/clj/tst/demo/core.clj
    

    project.clj

    (defproject demo "0.1.0-SNAPSHOT"
      :license {:name "Eclipse Public License"
                :url  "http://www.eclipse.org/legal/epl-v10.html"}
      :dependencies [[org.clojure/clojure "1.10.2-alpha1"]
                     [prismatic/schema "1.1.12"]
                     [tupelo "20.07.01"]]
        
      :profiles {:uberjar {:aot :all}}
    
      :global-vars {*warn-on-reflection* false}
      :main demo.core   ;  when use ^:skip-aot   ???
    
      :source-paths            ["src/clj"]
      :java-source-paths       ["src/java"]
      :test-paths              ["test/clj"]
      :target-path             "target/%s"
      :compile-path            "%s/class-files"
      :clean-targets           [:target-path]
    
      :jvm-opts ["-Xms500m" "-Xmx4g" ]
    )
    

    Java source

    package demo;
    import clojure.java.api.*;
    import clojure.lang.IFn;
    
    public class Main {
      public static double add2(double x, double y) {
        return (x + y);
      }
    
      public static void main(String[] args) {
        System.out.println("java main - enter");
        IFn plus = Clojure.var("clojure.core", "+");
        plus.invoke(1, 2);
        System.out.println("java main - leave");
      }
    }
    

    Clojure main

    (ns demo.core
      (:use tupelo.core)
      (:gen-class))
    
    (defn -main [& args]
      (println :clj-main-enter)
      (println :clj-main-leave))
    

    Clojure test

    (ns tst.demo.core
      (:use tupelo.core tupelo.test)
      (:import [demo Main])
      (:gen-class))
    
    (dotest
      (spyx (Main/add2 2 3)))
    

    The Clojure part is straightforward using lein:

    ~/expr/demo > lein clean; lein run
    :clj-main-enter
    :clj-main-leave
    
    ~/expr/demo > lein test
    
    ------------------------------------------
       Clojure 1.10.2-alpha1    Java 14.0.1
    ------------------------------------------
    
    lein test tst.demo.core
    (Main/add2 2 3) => 5.0
    
    Ran 2 tests containing 0 assertions.
    0 failures, 0 errors.
    

    We will use lein to build the uberjar:

    ~/expr/demo > lein uberjar
    Compiling demo.core
    Created /Users/alanthompson/expr/demo/target/uberjar/demo-0.1.0-SNAPSHOT.jar
    Created /Users/alanthompson/expr/demo/target/uberjar/demo-0.1.0-SNAPSHOT-standalone.jar
    

    then run either Clojure main using java -jar or Java main using java -cp

    # Entrypoint controlled by `:main` key in `project.clj` => clojure `demo.main/-main` function
    ~/expr/demo > java -jar /Users/alanthompson/expr/demo/target/uberjar/demo-0.1.0-SNAPSHOT-standalone.jar
    :clj-main-enter
    :clj-main-leave
    
    # ***** notice `demo.Main` Java class name *****
    ~/expr/demo > java \
      -cp /Users/alanthompson/expr/demo/target/uberjar/demo-0.1.0-SNAPSHOT-standalone.jar  \
      demo.Main   
    java main - enter
    java main - leave
    

    Update

    Just tried Stuart Halloway's Clojure Maven example.

    It will crash with Java 14, so beware!

    Results:

    ~/expr/demo/clojure-from-java > java -version
    openjdk version "1.8.0_252"
    OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_252-b09)
    OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.252-b09, mixed mode)
    
    ~/expr/demo/clojure-from-java > mvn  -q clean
    ~/expr/demo/clojure-from-java > mvn  -q compile
    ~/expr/demo/clojure-from-java > mvn  -q exec:java  -Dexec.mainClass=example.Main
    fn says hello
    file filter returns false
    object toString returns <object created Fri Jul 24 13:55:11 PDT 2020>
    

    Update #2

    You can fix the problem with Java 14 if you update the pom.xml to output Java 1.8 features. Excerpt:

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.8.1</version>
                    <configuration>
                        <!-- put your configurations here -->
                        <source>1.8</source>
                        <target>1.8</target>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
        <sourceDirectory>src/java</sourceDirectory>
        <resources>
            <resource>
                <directory>src/clojure</directory>
            </resource>
        </resources>
    </build>
    

    The part that matters is adding 1.8 here:

    <source>1.8</source>
    <target>1.8</target>
    

    Enjoy!