前回もクラスローダのあたりを書いたがあんまりしっくりしないのでもうちょっと調べてみる. 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
ではクラス名を指定するため,proxy
やreify
とは異なります.
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を
作ります.
これはdeftype
やdefrecord
のためでなくreify
やfn
も同様です.
(.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)