我为Braintree
Java库编写了一个Clojure包装器,以提供更简洁和惯用的界面.我想提供功能来快速简洁地实例化Java对象,如:
(transaction-request :amount 10.00 :order-id "user42")
我知道我可以明确地做到这一点,如this question所示:
(defn transaction-request [& {:keys [amount order-id]}] (doto (TransactionRequest.) (.amount amount) (.orderId order-id)))
但是,对于许多类来说这是重复的,当参数是可选的时,它变得更加复杂.使用反射,可以更简洁地定义这些功能:
(defn set-obj-from-map [obj m] (doseq [[k v] m] (clojure.lang.Reflector/invokeInstanceMethod obj (name k) (into-array Object [v]))) obj) (defn transaction-request [& {:as m}] (set-obj-from-map (TransactionRequest.) m)) (defn transaction-options-request [tr & {:as m}] (set-obj-from-map (TransactionOptionsRequest. tr) m))
显然,如果可能,我想避免反思.我试图定义一个宏映射的set-obj从地图,但我的宏夫不够强大.这可能需要eval,如here所述.
提前致谢!
更新解决方案:
根据Joost的建议,我能够使用类似的技术来解决问题.一个宏在编译时使用反射来识别类有哪些setter方法,然后在表中查找param,并用该值调用该方法.
这里是宏和一个例子:
; Find only setter methods that we care about (defn find-methods [class-sym] (let [cls (eval class-sym) methods (.getMethods cls) to-sym #(symbol (.getName %)) setter? #(and (= cls (.getReturnType %)) (= 1 (count (.getParameterTypes %))))] (map to-sym (filter setter? methods)))) ; Convert a Java camelCase method name into a Clojure :key-word (defn meth-to-kw [method-sym] (-> (str method-sym) (str/replace #"([A-Z])" #(str "-" (.toLowerCase (second %)))) (keyword))) ; Returns a function taking an instance of klass and a map of params (defmacro builder [klass] (let [obj (gensym "obj-") m (gensym "map-") methods (find-methods klass)] `(fn [~obj ~m] ~@(map (fn [meth] `(if-let [v# (get ~m ~(meth-to-kw meth))] (. ~obj ~meth v#))) methods) ~obj))) ; Example usage (defn transaction-request [& {:as params}] (-> (TransactionRequest.) ((builder TransactionRequest) params) ; some further use of the object ))
解决方法
你可以在编译时使用反射〜只要你知道你正在处理的类,然后〜找出字段名称,并从中生成“静态”设置器.我写了一些代码,对于getter来说,你可能会发现有趣的.见
https://github.com/joodie/clj-java-fields(特别是
https://github.com/joodie/clj-java-fields/blob/master/src/nl/zeekat/java/fields.clj中的def-fields宏).