hara.reflect java reflection made easy

Author: Chris Zheng  (z@caudate.me)
Date: 29 June 2017
Repository: https://github.com/zcaudate/hara
Version: 2.5.10

1    Introduction

hara.reflect contains methods for class reflection and method invocation.

1.1    Installation

Add to project.clj dependencies:

[im.chit/hara.reflect "2.5.10"]

All functionality is found contained in the hara.reflect namespace

(use 'hara.reflect)

2    API



apply-element ^

apply the class element to arguments

v 2.1
(defn apply-element
  [obj method args]
  (let [lu (get-element-lookup obj)]
    (if-let [ele (get lu method)]
      (cond (-> ele :modifiers :field)
            (apply ele obj args)

            (:static ele)
            (apply ele args)

            :else
            (apply ele obj args))
      (throw (Exception. (format "Class member not Found for %s - `%s`" (common/context-class obj) method))))))
link
(seq (apply-element "123" "value" [])) => [1 2 3]

class-hierarchy ^

lists the class and interface hierarchy for the class

v 2.1
(defn class-hierarchy
  [obj]
  (let [t (common/context-class obj)]
    (vec (cons t (inheritance/ancestor-tree t)))))
link
(class-hierarchy String) => [java.lang.String [java.lang.Object #{java.io.Serializable java.lang.Comparable java.lang.CharSequence}]]

class-info ^

lists class information

v 2.1
(defn class-info
  [obj]
  (select-keys (element/seed :class (common/context-class obj))
               [:name :hash :modifiers]))
link
(class-info String) => (contains {:name "java.lang.String" :hash anything :modifiers #{:instance :class :public :final}})

context-class ^

if x is a class, return x otherwise return the class of x

v 2.1
(defn context-class
  [obj]
  (if (class? obj) obj (type obj)))
link
(context-class String) => String (context-class "") => String

delegate ^

allow transparent field access and manipulation to the underlying object.

v 2.1
(defn delegate
  [obj]
  (let [fields (->> (map (juxt (comp keyword :name) identity) (q/query-instance obj [:field]))
                    (into {}))]
    (Delegate. obj fields)))
link
(def a "hello") (def >a (delegate a)) (seq (>a :value)) => [h e l l o] (>a :value (char-array "world")) a => "world"

extract-to-ns ^

extracts all class methods into its own namespace.

v 2.1
(defn extract-to-ns
  ([class]
   (extract-to-ns (symbol (.getName *ns*)) class []))
  ([nssym class]
   (extract-to-ns nssym class []))
  ([nssym class selectors]
   (let [eles (q/list-class-elements class selectors)
         methods (distinct (map :name eles))]
     (create-ns nssym)
     (doall (for [method methods]
              (extract-to-var nssym (symbol method) class method selectors))))))
link
(map #(.sym %) (extract-to-ns 'test.string String [:private #"serial"])) => '[serialPersistentFields serialVersionUID]

extract-to-var ^

extracts a class method into a namespace.

v 2.1
(defn extract-to-var
  ([varsym class method]
   (extract-to-var varsym class method []))
  ([varsym class method selectors]
   (let [[nssym varsym] (if-let [nsstr (.getNamespace ^clojure.lang.Symbol varsym)]
                          [(let [nssym (symbol nsstr)
                                  _  (create-ns nssym)]
                             nssym)
                           (symbol (name varsym))]
                          [(.getName *ns*) varsym])]
     (extract-to-var nssym varsym class method selectors)))
  ([nssym varsym class method selectors]
   (let [v  (intern nssym varsym (q/query-class class (cons (str method) (cons :# selectors))))]
      (alter-meta! v (fn [m] (merge m (element-meta @v))))
      v)))
link
(extract-to-var 'hash-without clojure.lang.IPersistentMap 'without []) (with-out-str (eval '(clojure.repl/doc hash-without))) => (str "-------------------------n" "hara.reflect.core.extract-test/hash-withoutn" "[[clojure.lang.IPersistentMap java.lang.Object]]n" " n" "member: clojure.lang.IPersistentMap/withoutn" "type: clojure.lang.IPersistentMapn" "modifiers: instance, method, public, abstractn") (eval '(hash-without {:a 1 :b 2} :a)) => {:b 2}

query-class ^

queries the java view of the class declaration

v 2.1
(defn query-class
  [obj selectors]
  (list-class-elements (common/context-class obj) selectors))
link
(query-class String [#"^c" :name]) => ["charAt" "checkBounds" "codePointAt" "codePointBefore" "codePointCount" "compareTo" "compareToIgnoreCase" "concat" "contains" "contentEquals" "copyValueOf"]

query-hierarchy ^

lists what methods could be applied to a particular instance

v 2.1
(defn query-hierarchy
  [obj selectors]
  (let [grp (args/args-group selectors)
        tcls  (common/context-class obj)]
    (->> (all-instance-elements tcls nil)
         (display/display grp))))
link
(query-hierarchy String [:name #"^to"]) => ["toCharArray" "toLowerCase" "toString" "toUpperCase"]

query-instance ^

lists what methods could be applied to a particular instance

v 2.1
(defn query-instance
  [obj selectors]
  (let [grp (args/args-group selectors)
        tcls (type obj)]
    (->> (all-instance-elements tcls (if (class? obj) obj))
         (display/display grp))))
link
(query-instance "abc" [:name #"^to"]) => ["toCharArray" "toLowerCase" "toString" "toUpperCase"] (query-instance String [:name #"^to"]) => (contains ["toString"])

3    Selectors

The option array takes selectors and filters can be used to customise the results returned by the two query calls.

  • attribute selection
  • name filtering
  • parameter filtering
  • modifier filtering
  • return type filtering

For example

(query-class Long [:name "MIN_VALUE" :#])
=> "MIN_VALUE"

(query-class Long [:params "MIN_VALUE" :#])
=> [java.lang.Class]

(query-class Long [:params :name "MIN_VALUE" :#])
=> {:name "MIN_VALUE", :params [java.lang.Class]}

Additional selector keywords include :container, :hash, :delegate, :origins, :name, :modifiers, :tag and :type and used as follows:

(query-class Long [:params :name :modifiers "MIN_VALUE" :#])
=> {:modifiers #{:public :static :field :final},
    :name "MIN_VALUE",
    :params [java.lang.Class]}

3.1    Name Filtering

We can filter on the name of the class member using two methods - exact matches using strings and regex matchesusing regexs:

(query-class Long [:name "value"])
=> '("value")

(query-class Long [:name #"value"])
=> '("value" "valueOf")

(query-class Long [:name #"VALUE"])
=> '("MAX_VALUE" "MIN_VALUE")

3.2    Parameter Filtering

3.2.1    Number of Inputs

Input parameters can be filtered through specifying the number of inputs:

(query-class Long [:name :params 2])
=> [{:name "compare", :params [Long/TYPE Long/TYPE]}
    {:name "compareTo", :params [Long Long]}
    {:name "compareTo", :params [Long Object]}
    {:name "equals", :params [Long Object]}
    {:name "getLong", :params [String Long]}
    {:name "getLong", :params [String Long/TYPE]}
    {:name "parseLong", :params [String Integer/TYPE]}
    {:name "rotateLeft", :params [Long/TYPE Integer/TYPE]}
    {:name "rotateRight", :params [Long/TYPE Integer/TYPE]}
    {:name "toString", :params [Long/TYPE Integer/TYPE]}
    {:name "toUnsignedString", :params [Long/TYPE Integer/TYPE]}
    {:name "valueOf", :params [String Integer/TYPE]}]

3.2.2    Exact Inputs

Exact inputs can be specified by using a vector with input types:

(query-class Long [:name :params [Long/TYPE]])
=> [{:name "bitCount", :params [Long/TYPE]}
    {:name "highestOneBit", :params [Long/TYPE]}
    {:name "lowestOneBit", :params [Long/TYPE]}
    {:name "new", :params [Long/TYPE]}
    {:name "numberOfLeadingZeros", :params [Long/TYPE]}
    {:name "numberOfTrailingZeros", :params [Long/TYPE]}
    {:name "reverse", :params [Long/TYPE]}
    {:name "reverseBytes", :params [Long/TYPE]}
    {:name "signum", :params [Long/TYPE]}
    {:name "stringSize", :params [Long/TYPE]}
    {:name "toBinaryString", :params [Long/TYPE]}
    {:name "toHexString", :params [Long/TYPE]}
    {:name "toOctalString", :params [Long/TYPE]}
    {:name "toString", :params [Long/TYPE]}
    {:name "valueOf", :params [Long/TYPE]}]

3.2.3    Partial Inputs

Using a vector with :any as the first input will output all functions with any of the types as input arguments

(query-class Long [:name [:any String Long]])
=> ["bitCount" "compare" "decode" "getChars" "getLong" "highestOneBit" "lowestOneBit" "new" "numberOfLeadingZeros" "numberOfTrailingZeros" "parseLong" "reverse" "reverseBytes" "rotateLeft" "rotateRight" "signum" "stringSize" "toBinaryString" "toHexString" "toOctalString" "toString" "toUnsignedString" "valueOf"]

Using a vector with :all as the first input will output all functions having all of the types as input arguments

(query-class Long [:name :params [:all String Long]])
=> [{:name "getLong", :params [String Long/TYPE]}]

3.3    Modifier Filtering

The following are all the modifier keywords that can be used for filtering, most are directly related to flags, four have been defined for completeness of filtering:

:public         1      ;; java.lang.reflect.Modifier/PUBLIC
:private        2      ;; java.lang.reflect.Modifier/PRIVATE
:protected      4      ;; java.lang.reflect.Modifier/PROTECTED
:static         8      ;; java.lang.reflect.Modifier/STATIC
:final          16     ;; java.lang.reflect.Modifier/FINAL
:synchronized   32     ;; java.lang.reflect.Modifier/SYNCHRONIZE
:native         256    ;; java.lang.reflect.Modifier/NATIVE
:interface      512    ;; java.lang.reflect.Modifier/INTERFACE
:abstract       1024   ;; java.lang.reflect.Modifier/ABSTRACT
:strict         2048   ;; java.lang.reflect.Modifier/STRICT
:synthetic      4096   ;; java.lang.Class/SYNTHETIC
:annotation     8192   ;; java.lang.Class/ANNOTATION
:enum           16384  ;; java.lang.Class/ENUM
:volatile       64     ;; java.lang.reflect.Modifier/VOLATILE
:transient      128    ;; java.lang.reflect.Modifier/TRANSIENT
:bridge         64     ;; java.lang.reflect.Modifier/BRIDGE
:varargs        128    ;; java.lang.reflect.Modifier/VARARGS

:plain          0      ;; not :public, :private or :protected
:instance       0      ;; not :static
:field          0      ;; is field
:method         0      ;; is method

3.3.1    Modifier Examples

Find all the fields in java.lang.Long:

(query-class Long [:name :field])
=> ["MAX_VALUE" "MIN_VALUE" "SIZE" "TYPE" "serialVersionUID" "value"]

Find all the static fields in java.lang.Long:

(query-class Long [:name :static :field])
=> ["MAX_VALUE" "MIN_VALUE" "SIZE" "TYPE" "serialVersionUID"]

Find all the non-static fields in java.lang.Long:

(query-class Long [:name :instance :field])
=> ["value"]

Find all public fields in java.lang.Long:

(query-class Long [:name :public :field])
=> ["MAX_VALUE" "MIN_VALUE" "SIZE" "TYPE"]

Find all private members in java.lang.Long:

(query-class Long [:name :private])
=> ["serialVersionUID" "toUnsignedString" "value"]

Find all private fields in java.lang.Long:

(query-class Long [:name :private :field])
=> ["serialVersionUID" "value"]

Find all private methods in java.lang.Long:

(query-class Long [:name :private :method])
=> ["toUnsignedString"]

Find all protected members in java.lang.Long:

(query-class Long [:name :protected])
=> []

Find all members in java.lang.Long with no security attribute:

(query-class Long [:name :plain])
=> ["getChars" "stringSize"]

3.4    Return Type Filtering

Return types signatures can be filtered by giving a class in the options, again all filters can be mixed and matched as needed. In the following example, we query for the name of all methods having a return type of Long/TYPE:

(query-class Long [:name :type :method Long/TYPE])
=> [{:name "highestOneBit", :type Long/TYPE}
    {:name "longValue", :type Long/TYPE}
    {:name "lowestOneBit", :type Long/TYPE}
    {:name "parseLong", :type Long/TYPE}
    {:name "parseLong", :type Long/TYPE}
    {:name "reverse", :type Long/TYPE}
    {:name "reverseBytes", :type Long/TYPE}
    {:name "rotateLeft", :type Long/TYPE}
    {:name "rotateRight", :type Long/TYPE}]