前回もクラスローダのあたりを書いたがあんまりしっくりしないのでもうちょっと調べてみる. JavaのClassLoaderの仕組みが分かっていなかった. まずStack OverflowのHow does clojure class reloading work?から.

質問

私はclojureでクラスのリロードの仕組みについてコードやドキュメントを 調べてきました. 多くのwebサイト,例えばhttp://tutorials.jenkov.com/java-reflection/dynamic-class-loading-reloading.html によると,クラスのロードは本質的に どんなデータ構造であれバイト列を得ることでそれを defineClassによりclassClassのインスタンスに変換し, resolveClassによりそのclassをresolve(link)することであるということでした. (defineClassは暗黙的にresolveClassを呼んでいるのでしょうか?) どんなclassloaderでもclassのリンクは一度だけに限定されています. もし既存のclassへリンクしようとした場合でも何も行われないようです. これでは新しいclassインスタンスをリンクできないという問題があり, classをリロードする際は毎度classloaderのインスタンスを作る必要があります.

clojureに注目してみると,clojureには複数のクラス定義方法があります.

匿名クラス: reify proxy
名前付きクラス: deftype defrecord(内部ではdeftypeを使っている) gen-class

これらに関するコードは最終的にclojure/src/jvm/clojure/lang/DynamicClassLoader.java へたどり着きます. DynamicClassLoader/defineClassではclassインスタンスを作り,キャッシュしておきます. クラスをロードし利用する場合はforNameを呼び,またそれはDynamicClassLoader/findClassを 呼びます. DynamicClassLoader/findClassは,キャッシュを検索しその後基底クラスを検索します. (通常のclassloaderでは逆に基底クラスを先に検索するようです.)

混乱の元になっているのは,

  • forNameはclassをリンクする前に返す(returnする)とドキュメントにある
  • これは既存のDynamicClassLoaderではclassのリロードが行えない
  • 代わりに新しくDynamicClassLoaderのインスタンスを作らねばならないがそのコードが見つからない

ということです. proxyやreifyは匿名クラスなのでクラス名が異なっていても問題ありません. しかし,名前付きクラスの場合はそうはいきません.

DynamicClassLoaderの仕組みを教えてください. 最終的にはjavacでコンパイルされた.classファイルをロード,リロードさせたいのです.

回答

以下全てが同じ技術を使っているわけではありません.

  • proxy

proxyマクロは基底クラスやインタフェースから名付けられたクラスを作ります. それぞれのメソッドはインスタンス内でclojureのfnとして扱われるため, マクロの内部が同じか否かに依らず, 同じインタフェースが継承される場合は同じproxyのクラスが使われます. 実際にクラスのリロードは行われません.

  • reify

reifyの場合は,メソッド本体が直接classの中にコンパイルされるため, proxyのトリックは使えません. 代わりに,formがコンパイルされる度に新たなクラスができるため, もし本体を編集してリロードしたとしても完全に新たなクラス(新しい名前)が作られます. これも実際のクラスのリロードは行われません.

  • gen-class

gen-classではクラス名を指定するため,proxyreifyとは異なります. gen-classマクロは枠組み(spec)のみを持ち,メソッド本体は持ちません. つまりproxyと似て,clojureのfnを参照するということです. しかしproxyと違いクラス名が枠組み(spec)と結びつくため,本体を修正しリロード ということは出来ません. 従ってgen-classはAOTでのみ利用可能でJVMの再起動が必要です.

  • deftypeとdefrecord

これらでは実際に動的なクラスのリロードが行われます. そのクラスを含むコードをコンパイル場合やforNameが呼ばれた場合など クラス名を解決する必要があるときは,DynamicClassLoader/findClass が呼ばれます.

(deftype T [a b])  ; define an original class named T
(def x (T. 1 2))   ; create an instance of the original class
(deftype T [a b])  ; load a new class by the same name
(cast T x)         ; cast the old instance to the new class -- fails
; ClassCastException   java.lang.Class.cast (Class.java:2990)

新たなクラス定義のためにclojureはトップレベルのformで新しくDynamicClassLoaderを 作ります. これはdeftypedefrecordのためでなくreifyfnも同様です.

(.getClassLoader (class x))
;=> #<DynamicClassLoader clojure.lang.DynamicClassLoader@337b4703>

(.getClassLoader (class (T. 3 4)))
;=> #<DynamicClassLoader clojure.lang.DynamicClassLoader@451c0d60>

Tクラスを新しく定義していない場合は同じクラスローダを持ちます.

(.getClassLoader (class (T. 4 5)))
;=> #<DynamicClassLoader clojure.lang.DynamicClassLoader@451c0d60>

まとめ

さらにここからMLの方でもこの話が続いていてクラスのロードの度に DynamicClassLoaderのインスタンスを作っていくと対応できました. というかDynamicClassLoaderのコードでスタック構造を読み取れるし そんなに不思議なことでもなかった. それにしてもこんなにクラス定義の方法があると混乱する. 何か書籍を買ったほうが早い気がしてきた. programming clojureあたり.

(ns test.core
  (:use [clojure.stacktrace])
  (:require [clojure.contrib.io :as io]))

(def class-name "hello")
(def file-path "src/test/hello.class")

(defn get-byte-array
  [file-path]
  (io/to-byte-array
    (io/file file-path)))

(defn reload-class
  [class-name file-path]
  (.defineClass (clojure.lang.DynamicClassLoader.)
                class-name
                (get-byte-array file-path)
                nil))

;(reload-class class-name file-path)

(def x (reload-class class-name file-path))
(.getName x)
(def xo (.newInstance x))

;; hello.javaを修正,リコンパイル

(def y (reload-class class-name file-path))
(.getName y)
(def yo (.newInstance y))

(.hi xo)
(.hi yo)