基于Map调用Java setter的Clojure宏?

11

我正在为Braintree Java库编写Clojure包装器,以提供更简洁和符合惯用语的接口。我希望提供快速和简洁地实例化Java对象的函数,例如:

(transaction-request :amount 10.00 :order-id "user42")

我知道我可以明确地执行此操作,如此问题所示:

(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-from-map的宏版本,但是我的宏编程能力不够强。根据这里的解释,它可能需要使用eval

有没有一种方法可以在运行时调用Java方法,而不使用反射?

谢谢您提前!

更新的解决方案:

根据Joost的建议,我成功地使用了类似的技术解决了问题。一个宏在编译时使用反射来识别类具有哪些setter方法,然后输出形式以检查参数是否在map中,并使用它的值调用方法。

以下是该宏和示例用法:

; 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
  ))

没有反射? 几乎肯定不行。 - Louis Wasserman
2
好的,可以使用宏将地图转换为方法调用,而不需要使用反射。我只有在意识到宏无法接受保存地图的符号,而只能接受原始地图本身时才使用了反射。我应该更清楚地表明我想避免在运行时使用反射,就像@joost-diepenmaat在下面所描述的那样。 - bkirkbri
2个回答

8

谢谢,这真的很有帮助。我需要颠倒我的方法,而不是在运行时获取任何映射参数,而是在编译时枚举设置器。额外的好处是只检查有效的参数。 - bkirkbri

1
宏可能非常简单,例如:
(defmacro set-obj-map [a & r] `(doto (~a) ~@(partition 2 r)))

但这会让你的代码看起来像这样:

(set-obj-map TransactionRequest. .amount 10.00 .orderId "user42")

我猜这不是你想要的 :)


是的,这种语法不太理想,而且也不能被函数的调用者使用。你仍然需要想办法将调用者的参数映射到这种语法中。 - bkirkbri

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接