'Clojure - compiling project with Java classes that are potentially not available

I am wrapping a java library in Clojure. Depending on the java library version, some classes exist or not, so my library can fail to even compile if it can't find the java classes. My idea was to use Reflector to use the string name of classes.

Example of what I'm trying to do:

(java.time.LocalDateTime/parse "2020-01-01")

would become

(if right-version?
  (clojure.lang.Reflector/invokeStaticMethod "java.time.LocalDate" "parse" (into-array ["2020-01-01"]))

This works but is slower by a factor of 20x. Is there a better way to achieve the same? Can I use a macro that will define the correct function at compile time, depending on the version of the underlying library?

Thanks,



Solution 1:[1]

I have been using a macro solution to this problem for 6+ years in the Tupelo Library. It allows you to write code like:

(defn base64-encoder []
  (if-java-1-8-plus
    (java.util.Base64/getEncoder)
    (throw (RuntimeException. "Unimplemented prior to Java 1.8: "))))

The macro itself is quite simple:

     (defmacro if-java-1-11-plus  
       "If JVM is Java 1.11 or higher, evaluates if-form into code. Otherwise, evaluates else-form."
       [if-form else-form]
       (if (is-java-11-plus?)
         `(do ~if-form)
         `(do ~else-form)))

     (defmacro when-java-1-11-plus  
       "If JVM is Java 1.11 or higher, evaluates forms into code. Otherwise, elide forms."
       [& forms]
       (when (is-java-11-plus?)
         `(do ~@forms)))

and the version testing functions look like

     ;-----------------------------------------------------------------------------
     ; Java version stuff
     (s/defn version-str->semantic-vec :- [s/Int]
       "Returns the java version as a semantic vector of integers, like `11.0.17` => [11 0 17]"
       [s :- s/Str]
       (let [v1 (str/trim s)
             v2 (xsecond (re-matches #"([.0-9]+).*" v1)) ; remove any suffix like on `1.8.0-b097` or `1.8.0_234`
             v3 (str/split v2 #"\.")
             v4 (mapv #(Integer/parseInt %) v3)]
         v4))

     (s/defn java-version-str :- s/Str
       [] (System/getProperty "java.version"))

     (s/defn java-version-semantic :- [s/Int]
       [] (version-str->semantic-vec (java-version-str)))

     (s/defn java-version-min? :- s/Bool
       "Returns true if Java version is at least as great as supplied string.
       Sort is by lexicographic (alphabetic) order."
       [tgt-version-str :- s/Str]
       (let [tgt-version-vec    (version-str->semantic-vec tgt-version-str)
             actual-version-vec  (java-version-semantic)
             result             (increasing-or-equal? tgt-version-vec actual-version-vec)]
         result))

    (when-not (java-version-min? "1.7")
      (throw (ex-info "Must have at least Java 1.7" {:java-version (java-version-str)})))


     (defn is-java-8-plus? [] (java-version-min? "1.8")) ; ***** NOTE: version string is still `1.8` *****
     (defn is-java-11-plus? [] (java-version-min? "11"))  
     (defn is-java-17-plus? [] (java-version-min? "17"))  

The advantage of using the macro version is that you can refer to a Java class normally via the symbol java.util.Base64. Without macros, this will crash the compiler for older versions of Java even if wrapped by an if or when, since the symbol will be unresolved before the if or when is evaluated.

Since Java doesn't have macros, the only workaround in that case is to use the string "java.util.Base64" and then Class/forName, etc, which is awkward & ugly. Since Clojure has macros, we can take advantage of conditional code compilation to avoid needing the powerful (but awkward) Java Reflection API.

Instead of copying or re-writing these functions into your own code, just use put

[tupelo "22.05.04"]

into your project.clj and away you go!


P.S.

You do not need to throw an exception if you detect an older version of Java. This example simply elides the code if the Java version is too old:

(t/when-java-1-11-plus

  (dotest
    (throws-not? (Instant/parse "2019-02-14T02:03:04.334Z"))
    (throws-not? (Instant/parse "2019-02-14T02:03:04Z"))
    (throws-not? (Instant/parse "0019-02-14T02:03:04Z")) ; can handle really old dates w/o throwing

...)

Solution 2:[2]

I could not tell from your question if your code uses Reflector on every call to the parse method. If so, you could instead define the Method once to use later many times:

(def right-version? true)   ; set as appropriate

(def ^java.lang.reflect.Method parse-method
  (when right-version?
    (.getMethod (Class/forName "java.time.LocalDateTime")
                "parse"
                (into-array [java.lang.CharSequence]))))

(defn parse-local-date-time [s]
  (when parse-method
    (.invoke parse-method nil (into-array [s]))))
(parse-local-date-time "2020-01-01T14:30:00")
;; => #object[java.time.LocalDateTime 0x268fc120 "2020-01-01T14:30"]

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2