Search code examples
javareflectioninterfaceclojureclasspath

Clojure, reflection: Find classes that implement an interface


This seems to be a more difficult in Clojure than in Java and Scala. What I want to do is:

  1. Define an interface in Java
  2. Implement it in a class in Clojure (Bonus: with a macro)
  3. Find it using the classloader and reflection

This is what I have so far:

The Java interface

package hello.interfaces;
public interface Test {
    String doSomething(String input);
}

The Clojure definition

(ns hello.impl.test
  (:gen-class
    :implements [hello.interfaces.Test]))

(defn doSomething [v] (str "hello " v))

Searching for the classes:

(ns hello.core
  (:import
    (org.reflections Reflections)
    (org.reflections.util ConfigurationBuilder))
  (:gen-class))

(defn -instances []
  (let [paths (into-array Object ["hello"])]
      (.getSubTypesOf (Reflections. paths) hello.interfaces.Test)))

(defn -main [& args]
  (println (-instances)))

I am using org.reflections. This works if I search for classes that are in the classpath (e.g. in the org.reflections jar) but it does not work for my previously defined class, therefore I think that the problem is not in the last snippet of code but in the previous one, or maybe in the usage, e.g. it requires pre-compiling it.

How can I define classes in Clojure that I can find later with reflection?


Solution

  • I'm not familiar with org.reflections, but if you just want a list of loaded classes, you can get it with the below code:

    (let [classloader (.getClassLoader clojure.main)
          classes-field (.getDeclaredField java.lang.ClassLoader "classes")]
      (.setAccessible classes-field true)
      (let [class-list (.get classes-field classloader)
            class-vec (reduce conj [] class-list)] ; copy everything into a new vector rather than working directly with the classloader's private field
        class-vec))
    

    It sounds like you're familiar with Java, so I guess you can see the above is basically just translated Java. It will only give you the classes which have been loaded with the same class loader as that for the class clojure.main, but if you haven't done any customisation with your class loaders, that should be enough.

    Once you have that list, you can search/filter it however you want. Of course, the class you're looking for does have to have been loaded first. If that's not the case, you'll have to search the classpath instead.

    === UPDATE to respond to your comment ===

    OK I see, you're asking how to create a class. The first thing to say is that you don't generally need to create named classes when you're writing Clojure, unless you specifically want to use some existing Java code which requires you to do so. If you're writing pure Clojure, you just write your functions and work with them directly.

    However, you can of course do so. The first part of the doc for gen-class states:

    => (doc gen-class)

    clojure.core/gen-class

    ([& options])

    Macro

    When compiling, generates compiled bytecode for a class with the given package-qualified :name (which, as all names in these parameters, can be a string or symbol), and writes the .class file to the compile-path directory. When not compiling, does nothing.

    So, you need to compile your namespace. I don't normally do this so I don't know if there's a way to do it without creating .class files, and just creating the classes directly in memory, but the below does what you want, if I've understood you correctly:

    (ns wip.test)
    
    ; looks for wip/himplement.clj on the classpath, and compiles it into .class files
    ; requires that ../bin is in the classpath
    (binding [*compile-path* "../bin"]
         (compile 'wip.himplement))
    
    ; loads the wip.himplement class from the .class files
    (Class/forName "wip.himplement")
    
    
    ; create a list of all loaded classes (could presumably also be done with org.reflections)
    (def classes-list (let [classloader (.getClassLoader clojure.main)
                            classes-field (.getDeclaredField java.lang.ClassLoader "classes")]
                        (.setAccessible classes-field true)
                        (java.util.ArrayList. (.get classes-field classloader))))
    
    ; Outputs all loaded classes which implement HInterface. Output is:
    ; (wip.hello.HInterface wip.himplement)
    (println (filter #(isa? % wip.hello.HInterface) classes-list))