Java Archive
ClassLoaderを使って実行時にクラスを更新する方法(リロード処理)
JavaのClassLoaderの話をちょっとだけ。意外にClassLoaderを知らない人が居るようです。
ざっくりと説明をしますと、
クラスローダーにはBootstrapクラスローダーやシステムクラスローダー、コンテキストクラスローダーがあります。(あと拡張クラスローダーですかね)。そんでもって、クラスローダーは親子関係を持っています。委譲モデルと言って、あるクラスローダーにクラスをロードする依頼があると、親に依頼を委譲します。最初に発見されたクラスが実際にロードされるわけです。
1. Bootstrap (rt.jar)
2. 親クラスローダーA
3. 子クラスローダーB
こんなクラスローダーがあるとしてまだロードされていないクラスXをロードしようとした場合は次のように動作します。
- B.loadClass(X)
- Aに委譲
- Bootstrapに委譲
- BoostrapにXは存在しない
- AにXは存在しない
- BにXが存在するのでロードする。
ちなみにClassLoader#findClassをオーバーライドして自前の処理で上書きしてしまえば、この委譲モデルを破壊する事ができます。まぁそんな事は非推奨ですけど(´・ω・)
JVMプロセス内においては異なるClassLoaderで読み込んだクラスは例え同じバイナリでも異なるクラスとして扱われます。ClassLoaderAによってロードされたクラスXと、ClassLoaderBによってロードされたクラスXは別物です。
※クラスAとBが親子関係を持っていて、AがXをロードしている時にBでロードしても委譲モデルでAが既にロードしてあるXを使うので、こういう場合は同じ定義を指します。
TomcatのWebApps1とWebApps2の関係を想像すると理解しやすいと思います。
この特性を生かすとリロード処理が実現できるわけです。
ClassLoaderの細かい話はDeveloperWorksの記事が丁寧なのでそちらを参照した方が良いです。今回はTomcatにも実装されているリロード処理のサンプルを作ってみたいと思います。TomcatのドキュメントにTomcatのClassLoaderの戦略が書いてあるのでそれを参考にすると分かりやすいと思います。
例えばRPCサーバやスケジューラーを考えます。これらを設計する時には大きく二つに分けることができ、RPCサーバではサーバ本体と各命令、スケジューラーでも本体と各タスクという感じです。Tomcatの場合はServletエンジンとビジネスロジックを書いたServlet。(ビジネスロジックはServletに書くものではありませんけどね。)
RPCの命令、スケジューラーのタスク、ServletコンテナのServletは抽象化する事ができ、インターフェースを切ることが多いです。(HttpServletとかね。)
擬似コードだとこんな感じですね。
class MyServer {
void run() {
ITask task = getTask();
task.execute();
}
}
このMyServerとITaskを本体側のClassLoaderに、ITaskを実装したクラスを別のClassLoaderに読み込ませます。
例えば本体はCLASSPATHを指定してシステムクラスローダーに読み込ませます。(TomcatはBoostrap用のクラスを挟んでいますけどね。)
タスクは独自のクラスローダーをインスタンス化(このオブジェクトをc1とする)して(URLClassLoaderを直接でも良い)、そのClassLoaderを使ってクラス定義を読み込ませます。C言語だとdlopenみたいなイメージです。
異なるクラスローダーから読み込んだクラスは別のクラスになるので、リロードしたい場合はClassLoaderをもう1回インスタンス化(c2)してあげればいいのです。
文章だけで説明してもアレなので簡単なサンプルを書いてみました。
Thread#setContextClassLoaderを使うともっと簡単に書けると思います。
サンプルの内容は定期的にタスクを実行するだけのものです。Reload処理はJMX経由で行うようにしました。
Eclipse上で作る時はタスククラスは別プロジェクトで作るのが良いです。同じプロジェクトに入れてしまうとシステムクラスローダーによってタスクもロードされてしまうので。色々適当です。Sleepの代わりにSemaphoreを使っている理由はsleepの場合Future#cancelで割り込みが発生しなかったからです。Semaphoreだと割り込みが入ってすぐに終了できます。
tasklist.propertiesに登録したいタスクのクラス名を書く必要があります。まぁこんな事をしなくてもLunarClassLoaderに手を加えて特定のインターフェースを実装しているクラスを検索(列挙)すれば良いだけです。今回はそこまで余力がありませんでした。 タスク用のクラスパスはソースに直書きしています。自分の環境に合わせて修正してください。VMの引数に-Dhogehogeを指定してシステムプロパティとして読み込むのが一般的な方法かと思います。
package at.orz.lunar;
import java.io.File;
import java.lang.management.ManagementFactory;
import java.net.URL;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javax.management.MBeanServer;
import javax.management.ObjectName;
public class LunarBootstrap {
public static void main(String[] args) throws Exception {
URL[] urls = new URL[]{
new File("C:/usr/local/eclipse3.5/workspace/lunar-subsystem/lunar-task.jar").toURI().toURL(),
new File("C:/usr/local/eclipse3.5/workspace/lunar-subsystem/bin").toURI().toURL()
};
final ExecutorService service = Executors.newCachedThreadPool();
final LunarServer lunarServer = LunarServer.newInstance(urls, Thread.currentThread().getContextClassLoader());
final Future<?> future = service.submit(lunarServer);
service.shutdown(); // 新規追加STOP
MBeanServer mbServer = ManagementFactory.getPlatformMBeanServer();
mbServer.registerMBean(new ServerOperationMXBean() {
@Override
public void stop() {
lunarServer.stop();
future.cancel(true);
if (!service.isShutdown()) {
service.shutdown();
}
}
@Override
public void reload() {
lunarServer.reload();
}
}, new ObjectName("at.orz.lunar.LunarBootstrap:name=LunarServer"));
}
}
シングルインスタンスモデルにするか、プロトタイプモデルにするか迷ったのでclsMapとかでClass定義を保持しているわけです。今回はシングルインスタンスモデルにしたので実はclsMapは必要ありません。
package at.orz.lunar;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Properties;
public class LunarClassLoader extends URLClassLoader {
private HashMap<String, Class<? extends LunarTask>> clsMap = new HashMap<String, Class<? extends LunarTask>>();
private HashMap<String, LunarTask> insMap = new HashMap<String, LunarTask>();
public LunarClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
init();
}
@SuppressWarnings("unchecked")
private void init() {
try {
Enumeration<URL> en = getResources("tasklist.properties");
while (en.hasMoreElements()) {
URL url = en.nextElement();
Properties prop = new Properties();
InputStream is = url.openStream();
prop.load(is);
is.close();
for (String clsName: prop.stringPropertyNames()) {
try {
Class<?> c = loadClass(clsName);
if (LunarTask.class.isAssignableFrom(c)) {
Class<? extends LunarTask> cls = (Class<? extends LunarTask>) c;
clsMap.put(clsName, cls);
System.out.printf("register class:%s%n", clsName);
LunarTask task = cls.newInstance();
task.init();
insMap.put(clsName, task);
System.out.printf("init class:%s%n", clsName);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
void destroy() {
for (LunarTask task : insMap.values()) {
try {
task.destroy();
} catch (Exception e) {
e.printStackTrace();
}
}
insMap.clear();
clsMap.clear();
}
public LunarTask getTask(String className) {
return insMap.get(className);
}
public Iterable<LunarTask> taskIterator() {
return new HashMap<String, LunarTask>(insMap).values();
}
}
package at.orz.lunar;
import java.net.URL;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class LunarServer implements Runnable, ServerOperationMXBean {
private LunarClassLoader loader;
private volatile boolean running;
private LunarServer() {
}
@Override
public void run() {
ExecutorService service = Executors.newFixedThreadPool(3);
running = true;
while (running) {
for (final LunarTask task : loader.taskIterator()) {
service.submit(new Runnable() {
@Override
public void run() {
task.execute();
}
});
}
try {
Semaphore sema = new Semaphore(0);
sema.tryAcquire(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
service.shutdown();
}
@Override
public void reload() {
LunarClassLoader curLoader = loader;
LunarClassLoader newLoader = new LunarClassLoader(curLoader.getURLs(), curLoader.getParent());
this.loader = newLoader;
curLoader.destroy();
}
@Override
public void stop() {
running = false;
}
public static LunarServer newInstance(URL[] urls, ClassLoader parent) {
LunarServer server = new LunarServer();
server.loader = new LunarClassLoader(urls, parent);
return server;
}
}
package at.orz.lunar;
public interface LunarTask {
public void init();
public void destroy();
public void execute();
}
package at.orz.lunar;
public interface ServerOperationMXBean {
public void stop();
public void reload();
}
ここからタスクです。Eclipse上では別プロジェクトにした方が良いです。今回は2つのクラスを登録します。
package at.orz.lunar.task;
import at.orz.lunar.LunarTask;
public class SampleTask1 implements LunarTask {
@Override
public void destroy() {
System.out.println("SampleTask1.destroy()");
}
@Override
public void execute() {
System.out.println("☆SampleTask1.execute()");
}
@Override
public void init() {
System.out.println("SampleTask1.init()");
}
}
package at.orz.lunar.task;
import at.orz.lunar.LunarTask;
public class SampleTask2 implements LunarTask {
@Override
public void destroy() {
System.out.println("SampleTask2.destroy()");
}
@Override
public void execute() {
System.out.println("★SampleTask2.execute()");
}
@Override
public void init() {
System.out.println("SampleTask2.init()");
}
}
クラスパスのルートにtasklist.propertiesを作成します。
at.orz.lunar.task.SampleTask1 at.orz.lunar.task.SampleTask2
実行した後にSampleTask2の内容を変更(★を★★★★★に変更)した後にjconsoleからreloadを呼び出した結果が↓です。
register class:at.orz.lunar.task.SampleTask2 SampleTask2.init() init class:at.orz.lunar.task.SampleTask2 register class:at.orz.lunar.task.SampleTask1 SampleTask1.init() init class:at.orz.lunar.task.SampleTask1 ★SampleTask2.execute() ☆SampleTask1.execute() ★SampleTask2.execute() ☆SampleTask1.execute() ☆SampleTask1.execute() ★SampleTask2.execute() ☆SampleTask1.execute() ★SampleTask2.execute() register class:at.orz.lunar.task.SampleTask2 SampleTask2.init() init class:at.orz.lunar.task.SampleTask2 register class:at.orz.lunar.task.SampleTask1 SampleTask1.init() init class:at.orz.lunar.task.SampleTask1 SampleTask1.destroy() SampleTask2.destroy() ☆SampleTask1.execute() ★★★★★SampleTask2.execute() ☆SampleTask1.execute() ★★★★★SampleTask2.execute()
リロード処理の所にログ仕込むの忘れたのでリロードが発端になっているかは確認できませんが、リロード処理は呼ばれています(; ・`д・´)
そしてリ新しいクラスの内容が読み込まれているのも確認できます。
※途中から書くのが面倒になったのがソースからわかっちゃうと思いますが、サンプルなので気にしない(・ε・)
ただし、このような処理はちょっとミスするとPermGenが解放されずにOutOfMemoryErrorを引き起こす原因になりやすいので気をつけましょー。(IBMのVM使えばいいじゃんとかそういう野暮な事は言わないの。)
Struts2のJspTemplateEngineで文字化けを起こさないようにする初期設定
前の記事でIncludeクラスのエンコード設定がおかしいため、JspTemplateEngineで文字化けが発生する問題を取り上げましたが、今回はそれを回避するコードを書いてみました。
IncludeクラスのInstanceを1回作成し、setDefaultEncodingを実行すればstaticなフィールドにデフォルトエンコードが設定されるのでContextListenerの中でそういったコードを書きます。
ContextListenerの中でDispatcherへListenerを追加します。そしてそのListenerの中で初期化処理を行います。なぜこういう風にするのかと言うと、Struts2のコントローラーはFilterで実装されています。ContextListenerで直接初期化処理を書いてしまうと、Filterの初期化よりも前に実行されてしまい、Struts2の初期化処理が行われていない状態で実行されてしまう事になるからです。
public class DefaultContextListener implements ServletContextListener {
private DispatcherListener dispatchListener = new DispatcherListener() {
private ValueStackFactory valueStackFactory;
private String defaultEncoding;
@SuppressWarnings("unused")
@Inject
public void setValueStackFactory(ValueStackFactory valueStackFactory) {
this.valueStackFactory = valueStackFactory;
}
@Inject(StrutsConstants.STRUTS_I18N_ENCODING)
@SuppressWarnings("unused")
public void setDefaultEncoding(String val) {
defaultEncoding = val;
}
@Override
public void dispatcherInitialized(Dispatcher du) {
du.getContainer().inject(this);
ValueStack stack = valueStackFactory.createValueStack();
new Include(stack, null, null).setDefaultEncoding(defaultEncoding);
}
@Override
public void dispatcherDestroyed(Dispatcher du) {
}
};
@Override
public void contextInitialized(ServletContextEvent sce) {
Dispatcher.addDispatcherListener(dispatchListener);
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
Dispatcher.removeDispatcherListener(dispatchListener);
}
}
月末日を求めるプログラム
検索すると結構見つかるものですが、JavaのCalendar#getActualMaximumを使った間違ったサンプルを多く見かけるので正しいやり方をメモメモ。相手の力量をはかるためのクイズとして使っても良いかもしれませんね。
カレンダーがgetInstance()じゃない事は言及しません。
以下は2006年2月の月末日を求めるコードです。
●とりあえず間違ったコード
GregorianCalendar c = new GregorianCalendar(Locale.JAPAN); c.set(Calendar.YEAR, 2006); c.set(Calendar.MONTH, 2 - 1); System.out.println(c.getActualMaximum(Calendar.DAY_OF_MONTH));
何がいけないか。
月末に実行してみるとわかると思います。
仮に今日が2009年3月31日だとします。
まず1行目でCの値は2009年3月31日になります。(時間は省略)
次に2行目でCの値は2006年3月31日になります。
3行目でCの値は2006年2月31日になります。※
→2006年3月3日になります。
2006年3月の月末日は31日のため、31が表示されます。
というわけで、
●正しいコード
GregorianCalendar c = new GregorianCalendar(Locale.JAPAN); c.set(Calendar.YEAR, 2006); c.set(Calendar.MONTH, 2 - 1); c.set(Calendar.DAY_OF_MONTH, 1); System.out.println(c.getActualMaximum(Calendar.DAY_OF_MONTH));
JspTemplateEngineのBug(Struts2.1.6)
結論から言うと、JSPを使ったカスタムタグを作るときにEncodingの指定を読み込めず(システムのデフォルトの文字コード)を使うようになって文字化けが発生するというバグが存在しました。
Struts2.1.6でカスタムタグを作るときにデフォルトではFreeMarkerを使うようになっていますが、VelocityやJSPもテンプレートとして使うことができます。struts.xmlでデフォルトのエンジンを変更するか、自作のカスタムタグでテンプレート名を明示的に拡張子付きで指定してあげます。(例:hogehoge.jsp)
さて、JspTemplateEngineはRequestDispatcherのIncludeを使って実装されています。
このIncludeをする部分はカスタムタグのInclude(<s:include>)のstaticなメソッドであるincludeを呼んでいます。
ソースをひもといていくと、、、
private static String defaultEncoding;
@Inject(StrutsConstants.STRUTS_I18N_ENCODING)
public void setDefaultEncoding(String encoding) {
defaultEncoding = encoding;
}
Σ(´Д`lll)エエ!!
しかも、defaultEncodingとは別にencodingってフィールドも持っているのですがこれもstaticなわけです。
これはひどい実装だ・・。 分けている意味が分からない。。。defaultがstaticでencodingがnon-staticだったら意味はわかるのですが・・・。
しかも、struts-default.xmlのBeanの定義には↓こう書いてある。
<bean class="org.apache.struts2.components.Include" static="true" />
staticなinjectionをしますよと書いてあるのにかかわらず、Setterはstaticではないのです!!
誰がこんなコードを何のために修正したんだろうと思ってレポジトリ追ってみた。
ここで変更されたみたい。
ちなみに、先に<s:include>タグを実行すると、インスタンス化されて普通に@Injectが走ってstaticフィールドに設定されて以降はその文字コードを使うようにします。WTPだとデフォルトでfile.encoding=UTF-8をつけるみたいで気がつきませんでした。
(○口○*) はぁ・・
svnkitを使ってSubversionを操作する
- 2009年1月27日 02:26
- Java | 日記2009前期 | 開発
- Tags: Java Subversion
例えばツールで、何かをバージョン管理したい時はバックエンドにSVNを使っていたりするのですが、Davによる自動コミットがONになっていない環境だとファイルの更新が面倒です。ファイルの追加はSVNコマンドでIMPORTを使えば可能ですが、既に存在するファイルの更新は事前にローカルのワークファイルが存在しないとコミットできません。
というわけで、既に登録されているファイルを、チェックアウトしなくても更新できるプログラムを書いてみました。
本当はきちんとクラス化してあるのですが、長くなるので主要な部分を切り貼りしていきます。
// SVNKitの初期化 DAVRepositoryFactory.setup();
// 引数にはSVNのURLを指定します
// 認証とかの設定をしてレポジトリオブジェクトを返します
private SVNRepository getRepos(String baseUrl) throws SVNException {
SVNURL url = SVNURL.parseURIDecoded(baseUrl);
SVNRepository repos = SVNRepositoryFactory.create(url);
if (userName != null) {
ISVNAuthenticationManager authManager =
SVNWCUtil.createDefaultAuthenticationManager(
this.userName,
this.password == null ? "" : this.password
);
repos.setAuthenticationManager(authManager);
}
return repos;
}
まずはファイルをSVNに追加するコードです。
/**
* ファイルをSVNに追加する。
* 実際に追加される場所は、baseUrl + filePath
* @param baseUrl SVNのベースのURL
* @param filePath ファイル名
* @param fileData 追加するデータの内容
* @param logMessage コミットログの内容
* @return コミットログ
*/
public SVNCommitInfo addFile(String baseUrl, String filePath, byte[] fileData, String logMessage) {
SVNCommitInfo cInfo = null;
try {
SVNRepository repos = getRepos(baseUrl);
ISVNEditor editor = repos.getCommitEditor(logMessage, null, true, null);
try {
editor.openRoot(-1);
editor.addFile(filePath, null, -1);
editor.applyTextDelta(filePath, null);
SVNDeltaGenerator deltaGenerator = new SVNDeltaGenerator();
String checksum = deltaGenerator.sendDelta(filePath, new ByteArrayInputStream(fileData), editor, true);
editor.closeFile(filePath, checksum);
editor.closeDir();
cInfo = editor.closeEdit();
} catch (SVNException e) {
editor.abortEdit();
throw e;
}
} catch (SVNException e) {
throw new RuntimeException(e);
}
return cInfo;
}
次はSVNのファイルの内容を更新するコードです。 上の登録のコードもそうですが、基本は差分(DELTA)を計算してそれを送信しているだけです。
/**
* SVNのファイルの内容を更新する(変更する)。
* 実際に変更されるファイルの場所は、baseUrl + filePath
* @param baseUrl SVNのベースのURL
* @param filePath ファイル名
* @param fileData 変更後のデータの内容
* @param logMessage コミットログの内容
* @return コミットログ
*/
public SVNCommitInfo updateFile(String baseUrl, String filePath, byte[] fileData, String logMessage) {
SVNCommitInfo cInfo = null;
try {
SVNRepository repos = getRepos(baseUrl);
// 古いコンテンツを取得する
ByteArrayOutputStream oldOut = new ByteArrayOutputStream();
repos.getFile(filePath, -1, SVNProperties.wrap(Collections.EMPTY_MAP), oldOut);
byte[] oldData = oldOut.toByteArray();
ISVNEditor editor = repos.getCommitEditor(logMessage, null, true, null);
try {
editor.openRoot(-1);
editor.openFile(filePath, -1);
editor.applyTextDelta(filePath, null);
SVNDeltaGenerator deltaGenerator = new SVNDeltaGenerator();
String checksum = deltaGenerator.sendDelta(
filePath,
new ByteArrayInputStream(oldData),
0,
new ByteArrayInputStream(fileData),
editor,
true
);
editor.closeFile(filePath, checksum);
editor.closeDir();
cInfo = editor.closeEdit();
} catch (SVNException e) {
editor.abortEdit();
throw e;
}
} catch (SVNException e) {
throw new RuntimeException(e);
}
return cInfo;
}
複数ファイルを変更する場合、上記のメソッドを複数回呼び出すと別々のコミットになってしまいます。 まぁeditor.closeEdit()を複数回呼んでいるので、当然と言えば当然ですが。
というわけで、複数のファイルを1回で追加・更新するコードを書いてみます。
その前にファイルが存在するかを確認するメソッドを追加します。
public boolean exists(String baseUrl, String filePath) {
try {
SVNRepository repos = getRepos(baseUrl);
SVNNodeKind nodeKind = repos.checkPath(filePath, -1);
if (nodeKind == SVNNodeKind.NONE || nodeKind == SVNNodeKind.DIR) {
return false;
}
return true;
} catch (SVNException e) {
throw new RuntimeException(e);
}
}
複数のファイルを良い感じに更新するメソッドです。
BLOGにコードを書き写した時に手動で一部のメソッドを書き換えているのでコピペしても動かないかもしれません。
事前にexists等を求めているのはlockメソッドでエラーが出るからです。SVNKitのバグかどうかは知りません。
public static class SvnRequestEntry {
public String filePath;
public byte[] fileData;
boolean exists;
byte[] oldData;
}
/**
* 複数のファイルをSVNに追加する。既にファイルが存在する場合は更新される。
* また、コミットは1回で行われる。
* @param baseUrl SVNのベースURL
* @param entries 追加するデータのリスト
* @param logMessage コミットログの内容
* @return コミットログ
*/
public SVNCommitInfo putFiles(String baseUrl, List<SvnRequestEntry> entries, String logMessage) {
SVNCommitInfo cInfo = null;
try {
SVNRepository repos = getRepos(baseUrl);
// 事前にチェックを行う
for (SvnRequestEntry entry: entries) {
SVNNodeKind nodeKind = repos.checkPath(entry.filePath, -1);
entry.exists = !(nodeKind == SVNNodeKind.NONE || nodeKind == SVNNodeKind.DIR);
// 古いコンテンツを取得する
if (entry.exists) {
ByteArrayOutputStream oldOut = new ByteArrayOutputStream();
repos.getFile(entry.filePath, -1, null, oldOut);
entry.oldData = oldOut.toByteArray();
}
}
ISVNEditor editor = repos.getCommitEditor(logMessage, null, true, null);
try {
editor.openRoot(-1);
for (SvnRequestEntry entry: entries) {
String filePath = entry.filePath;
byte[] fileData = entry.fileData;
boolean exists = entry.exists;
byte[] oldData = entry.oldData;
// 存在チェック
if (!exists) {
// 追加する
editor.addFile(filePath, null, -1);
editor.applyTextDelta(filePath, null);
SVNDeltaGenerator deltaGenerator = new SVNDeltaGenerator();
String checksum = deltaGenerator.sendDelta(filePath, new ByteArrayInputStream(fileData), editor, true);
editor.textDeltaEnd(filePath);
editor.closeFile(filePath, checksum);
} else {
// 更新する
editor.openFile(filePath, -1);
editor.applyTextDelta(filePath, null);
SVNDeltaGenerator deltaGenerator = new SVNDeltaGenerator();
String checksum = deltaGenerator.sendDelta(
filePath,
new ByteArrayInputStream(oldData),
0,
new ByteArrayInputStream(fileData),
editor,
true
);
editor.textDeltaEnd(filePath);
editor.closeFile(filePath, checksum);
}
}
editor.closeDir();
cInfo = editor.closeEdit();
} catch (SVNException e) {
editor.abortEdit();
throw e;
}
} catch (SVNException e) {
throw new RuntimeException(e);
}
return cInfo;
}
使い方はこんな感じです。他のメソッドも同じ感じです。
SVNCommitInfo info1 = addFile( baseUrl, "test01.txt", "test01_init_data".getBytes(), "プログラムからからファイルを追加してみる\nテスト。" );
putFilesを呼び出すときは、SvnRequestEntryのfilePathとfileDataに値を入れて、 更新したいファイルの数だけList化すれば良いです。
Luceneの殴り書きメモ
全文検索エンジンLuceneのメモです。((φo(´・ω・`*)
バージョンはこの前リリースされたばかりの2.4を使います。
2.4では、Hitsクラスなどが非推奨になりました。Luceneのマイルストーンとしては、次は2.9。そして3.0になります。3.0の時点で現在非推奨となっているメソッドは全て削除されてしまいます。ですので、今回は非推奨のメソッドやクラスは使わないようにしました。
CJKAnalyzerが検索の取りこぼしをするのでまずはAnalyzerを作ります。
package at.orz.tools;
import java.io.Reader;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.ngram.NGramTokenizer;
public class MyAnalyzer extends Analyzer {
private int minGram;
private int maxGram;
public MyAnalyzer(int minGram, int maxGram) {
this.minGram = minGram;
this.maxGram = maxGram;
}
@Override
public TokenStream tokenStream(String fieldName, Reader reader) {
return new NGramTokenizer(reader, minGram, maxGram);
}
}
INDEXの作成をします。
IndexWriter out = new IndexWriter("lucene-idx", new MyAnalyzer(1,3), true, MaxFieldLength.UNLIMITED);
Document doc = new Document();
doc.add(new Field("msg", "新機能aabbccを使う", Store.YES, Index.ANALYZED));
out.addDocument(doc);
out.close();
"lucene-index"ディレクトリにMyAnalyzer(1, 3)なので、1文字~3文字のNGramのIndexを新規作成します。
Documentの"msg"フィールドに適当に文字を入れて、Store.YESなので実体も入れます。Index.ANALYZEDなので解析した状態で値をつっこみます。ここら辺の違いは、実際に作成してツールでIndexの中身を見るのがいいと思います。(現時点でLucene2.4に対応しているツールがあるのかは不明ですが・・・。少なくともLukeは対応していませんでした)
検索をしてみる。
IndexSearcher s = new IndexSearcher("lucene-idx");
Query q = new QueryParser("msg", new MyAnalyzer(1,1)).parse("aa");
TopDocs td = s.search(q, 2);
System.out.println(td.totalHits);
System.out.println(s.doc(td.scoreDocs[0].doc));
検索する時は、MyAnalyzer(1,1) (Uni-gram)にします。これを1,3にしてしまうと取りこぼしが発生します。というのも、QueryParserがTokenの位置を使わないでPhraseQueryを作っているのが原因らしいです。(ソース見てないので知りませんが・・)
s.searchの第2引数は、先頭の何件を取得するかです。2なので、td.scoreDocs配列は最大2になります。totalHitsは何件ヒットしたか。100件ヒットしたら100が返ります。(ただし、2を指定しているのでデータは2件分のみ)
td.scoreDocs[x]がScoreDocクラスを返し、そのdocフィールドが何番のDocumentかを示しています。
なのでIndexSearcherからそのDocumentを取得する。
DBと連携をする時は、二重に文章データを持ちたくないのであれば、Sotre.NOで作成すればINDEXのみ作成する(という認識だけど、試してはいない)。フィールドにDBレコードのPKを入れておけば参照はできる。そしてRPCにしておけばINDEXデータを一元管理できるので、マルチプロセスで動作するアプリ側でデータの整合性を気にする必要はなくなるヽ( `・ω・)ノ
JavaでISO単位付きの文字列を整数に変換する
すごく手抜き処理の動けばいいやー程度のやっつけコードです。
よい子は真似しちゃだめですよ(;´Д`)
public long toLong(String str) {
long unit = 1L;
long head;
char tail = str.charAt(str.length()-1);
switch (tail) {
case 'G':
case 'g':
unit *= 1024;
case 'M':
case 'm':
unit *= 1024;
case 'K':
case 'k':
unit *= 1024;
head = Long.parseLong(str.substring(0, str.length()-1));
break;
default:
head = Long.parseLong(str);
}
return head * unit;
}
Sequenceをエミュレート(MySQL+iBatis版)
GeneratedKeyを使う方法は以前書きましたが、今回はiBatisを使った場合の方法です。
1回の命令でシーケンスをエミュレートするやりかたを書いておきます。
まず、iBatisは以下の事が言えます。
- Insertの時だけSelectKeyで値を返すことができる(Updateの時は戻り値は更新件数)
- InsertタグでもUpdate文を発行する事は可能
(JDBCレベルではexecuteUpdateを発行しているため)
これらの事をふまえると、
Insertタグの中にUpdate文を記述して、SelectKeyも併せて書くことでインクリメントした値を返すことができます。
さっそくサンプルを。
シーケンステーブルの準備
CREATE TABLE SEQUENCE (
NAME VARCHAR(32) NOT NULL,
SEQ INTEGER NOT NULL,
CONSTRAINT PK_SEQUENCE PRIMARY KEY(NAME)
) Type=InnoDB;
INSERT INTO SEQUENCE (NAME, SEQ) VALUES('IMGID', 0);
iBatisの設定ファイル
<insert id="GetNextSequence" parameterClass="java.lang.String"> UPDATE SEQUENCE SET SEQ = LAST_INSERT_ID(SEQ+1) WHERE NAME = #name# <selectKey resultClass="int"> SELECT LAST_INSERT_ID() </selectKey> </insert>
Javaプログラム
int id = (Integer) sqlMap.insert("GetNextSequence", "IMGID");
Generics未対応のメソッドをそれっぽくするユーティリティクラス
最近はJDK5, 6も主流になってきているのでGenericsを普通に使っていると思います。
しかし、HttpServletRequest#getAttributeやSpringのBeanFactory#getBean、iBatisのSqlMapClient#queryForObectなどは戻り値がObjectです。なので、明示的にキャストをするわけですが、Eclipseで開発していると「安全なキャストを保証しないよ」みたいな警告が出ます。
アノテーションの@SuppressWarnings("unchecked")を入れてしまえばいいのですが毎回つけるのも面倒です。
というわけで、私は以下のサポートクラスを使ってます。
public class CastUtils {
@SuppressWarnings("unchecked")
public static <T> T cast(Object obj) {
return (T) obj;
}
}
Log4jでログレベルによって出力先を変更する
いろいろな方法がありますが、そのうちの一つということで。
InfoLogやErrorLogが混在していると見づらいです。もちろんchainsawとか使えば良いんですが。
そんなわけで、重要なログを別ファイルに追加出力をしてみたいと思います。
SequenceをGeneratedKeyでエミュレート(MySQL+JDBC版)
OracleやPostgreSQLにはSequenceがあるのですが、MySQLにはありません。
AUTO_INCREMENTは便利なのですが、外部キーの参照先となるような場合に困ってしまいます。
具体的には次のような感じです。
テーブル設計や正規化云々の話は面倒なので言及しません。
| 名簿テーブル | ||
|---|---|---|
| ID | NAME | YOMI |
| 1 | 陽菜 | ひな |
| 2 | 葵 | あおい |
| 3 | さくら | さくら |
| 4 | 結衣 | ゆい |
| 5 | 結菜 | ゆうな |
| 係り活動テーブル | |||
|---|---|---|---|
| ID | NEN | GAKKI | KAKARI |
| 1 | 1 | 1 | いきもの |
| 1 | 1 | 2 | 図書 |
| 1 | 1 | 3 | 保健 |
| 1 | 2 | 1 | 風紀 |
名簿テーブルのIDがAUTO_INCREMENTの場合に係り活動テーブルに入れるIDの取得に困ります。
- 名簿テーブルにデータをINSERTする。(IDは自動生成で1が入ったとする)
- 自動生成された1を取得するために以下の方法で取得する
-
- SELECTを発行する
- LAST_INSERT_ID()をコールする
- 係り活動テーブルにデータをINSERTする。
SELECTの場合、MAX関数使った場合はマルチスレッド、マルチプロセスで破綻します。
LAST_INSERT_ID()の場合、発行する順番に気をつければ同一セッション内で値が保持されている・・・?
詳しくはわかりません(´・ω・`)
またAUTO_INCREMENTをやめてシーケンステーブルを作成した場合、SELECTは上記と同じ理由で破綻(ロックかけるなら別ですけど。)、LAST_INSERT_IDは上手くいくが、どちらにせよ2往復している事になります。
もしDBがSEQUENCEをサポートしている場合(名簿テーブルのIDはAUTO_INCREMENTではありません)、
- シーケンスを進めて値を取得する(仮に1とする)
- 名簿テーブルにデータをINSERTする
- 係り活動テーブルにデータをINSERTする
こんな感じで済みます。
とりあえず、2往復は効率が悪そう。でMySQLはシーケンスをサポートしていない。となれば作るしかなさそうです。
INSERTの戻り値として自動生成された値を取得する場合、PERLではDBIのmysql_insertidを使えば出来、JDBCの場合はStatement#getGeneratedKey()を使う事で取得が可能になるようです。ちなみにOracleの場合はgetGeneratedKey()でカラムを取得してもRowId型になった気がします。(誰かのBLOGに載っていた予感)
というわけで、早速コードを書いてみました。
sun.reflect. GeneratedSerialization ConstructorAccessor と __JVM_DefineClass__
※sun.reflect.GeneratedSerializationConstructorAccessor と __JVM_DefineClass__
タイトルが長いとデザイン崩れるのでタイトルに区切り入れました。
CommonsBeanUtilsとリフレクションを使ったコードを書いたプログラムを稼動させていると、以下のメッセージがコンソールに出るようになりました。
[Loaded sun.reflect.GeneratedSerializationConstructorAccessor2600 from __JVM_DefineClass__]
[Loaded sun.reflect.GeneratedSerializationConstructorAccessor2601 from __JVM_DefineClass__]
[Loaded sun.reflect.GeneratedSerializationConstructorAccessor2602 from __JVM_DefineClass__]
[Unloading class sun.reflect.GeneratedSerializationConstructorAccessor2600]
[Unloading class sun.reflect.GeneratedSerializationConstructorAccessor2601]
[Unloading class sun.reflect.GeneratedSerializationConstructorAccessor2602]
はてさて・・・こういうときはGoogle先生に聞いてみるのが一番です・・。
海外のメーリングリストを見ると結構出てきました。殆どのフレームワークがリフレクションを使っているのでフレームワーク関連のフォーラムやMLを漁ったら一杯出てきました(;´∀`)
http://marc.info/?l=tomcat-user&m=117932092932418&w=2
http://www.techienuggets.com/Comments?tx=8367
http://forum.java.sun.com/thread.jspa?threadID=5159979&messageID=9608761
斜め読みすると、正常動作っぽいです。
http://java.sun.com/javase/technologies/hotspot/gc/index.jsp
ここを見ろ!との事なので、ちょっとお勉強します・・。
裏で何が起こっているのか全くわからないのは嫌なので(´Д⊂グスン
そもそも何をすれば表示される状態になるのか、その状態にはどういう事をすればその状態になるのか全くわかりません。
誰か日本語で解説してくれないかなー。
Linuxのカーネルソースなら追うんだけど、JavaのJREのソースって公開してましたっけ・・・(´・ω・)?
ライセンスをGPLにして公開した部分ってなんだったっけ・・。
久しぶりに (´・ω・)しょぼーん
Javaでも#identを実現する方法
CVSやSubversionでは$Id$と記述しておくと、
"$Id: AppDao.java 258 2007-12-17 08:45:36Z tamtam $"
のように変換してくれます。
ソースレベルで見ると誰が最後に弄ったのかとソースのバージョンが分かるので便利なのですが、コンパイルしてバイナリファイルになるとこの情報は失われてしまいます。
例えば、test1.c と test2.c というソースから test.so というファイルを作るとしましょう。
test.soがポツーンと置いてある場合、「このsoはどのバージョンのソースで作られたものだろう・・・?」というときに、gccでは#identマクロを使うことで解決できます。
各ソースに↓を記述します。
#ident "$Id$"
そして↓を実行。
ident test.so
test.so:
$Id: test1.c 124 2007-10-24 04:43:13Z tamtam $
$Id: test2.c 124 2007-10-24 04:43:13Z tamtam $
このように、どのソースから作られたバイナリかを特定することができます。
普通はsoのファイル名にバージョン番号も入れるのが普通なのですがね・・。
これをJavaで実現する方法です。
ちなみに、Subversionやtracを使ってきちんとプロジェクト管理をしていれば全く問題ないと思います。
JARファイルのファイル名とマニフェストファイルにバージョン番号入れるだろうし。
というわけで、あくまでも方法論です。
RSSライブラリInformaとBOM
JavaのRSSライブラリにInformaというものがあります。
日付の書式のパースの書式が違うというバグがあるのですが、それ以外にも一つ見つけました。
BOMつきのRSSを解釈できません・・。
Google先生に聞いてみるとJ2SE1.4の標準XMLパーサはBOM付きXMLをパースできないからXercesとか使いなさいね。って事でした。しかし、今使っているのJDKはヴァージョン6・・。しかもInformaはjdomを使っている様子。
・・・ん?jdom?
まさかこいつが・・!
調査めんどくさい・・・。
先頭読み込んでBOM(0xFEFF)だったら飛ばす処理をいれて応急処置っとφ(`д´)。
で、このBOMをつけてくるRSSがGOOの新着BLOGのRSSなわけですが、それ以前にGOOのRSSバグってませんか?
XMLなのに、CDATAでもないのに、&をエスケープしてないんですが・・・。
勘弁してください・・(;´Д`)
追記(2007/12/17)
DTIBLOGのRSSもXMLとして壊れているようです。
FC2も<creater>タグの中身がエスケープされていないものがありました。
みんな結構いい加減だなぁ(´・ω・)
PreparedStatementのキャッシュ
Wikiにも書きましたが、PreparedStatementをキャッシュするとスループットの向上が望めます。
parse, prepare処理が内部でどういった手順で行われているかは以下のサイトが参考になると思います。
- http://www.atmarkit.co.jp/fjava/rensai2/webopt11/webopt11.html
- http://www.atmarkit.co.jp/fdb/rensai/orasql06/orasql06_1.html
実際にプログラムを書いて動作を確認したところ、確かにOracleでは30%ほど速くなりました。
MySQL(MyISAM)では200%ほど速くなりました。
INSERTしか確認していないので、実験の信頼性は低いと思いますが、気になる方は自分で試してみると良いかもしれません。
ちなみにWikiはこちらです。実験の結果も載せてます。
Commons-DBCP
Commons-DBCPの使い方です。
Commons-DBCPはデータベースのコネクションプールとPreparedStatementのキャッシュをしてくれます。TomcatやStruts1, Springなど有名なアプリケーションサーバやフレームワークが採用しているライブラリですが、これを直接使ってみたいと思います。
ちなみに、こんな記事を見つけました。 かなり前の内容のようなので現状はわかりませんが・・・。
それはさておき、簡単なサンプルコードは以下になります。そもそも、OracleのJDBCドライバには、 適切な(?)ファイナライザが実装されていないらしく、 Connectionにしても、Statementにしても、ResultSetにしても、 closeし忘れるとメモリリークが起こるという不具合というか仕様があるらしいです。 でもって、Oracleのコネクションプーリング実装でclose忘れされた日には、 どうにも開放する手段がなく、メモリリークしてしまいます。 その辺りを確かめるべく、 Jakarta Commons DBCPも交えつつ、 色々実験してました。 すると、 ・OracleのJDBCドライバをそのまま使用 → 高負荷時にConnectionを取得できないことがあるが、メモリリークはなし。 ・OracleのJDBCドライバのプーリングを使用 → 高負荷時にConnectionを取得できないことがあり、メモリリークもある。 ・Commons DBCP&OracleのJDBCドライバを使用 → 高負荷時もConnectionを取得でき、メモリリークはなし。 (ただし、必要最小限のメモリは使用していると思われる。) という結果が得られました。 というわけで、 OracleのJDBCドライバに入ってるコネクションプーリング実装は、 「百害あって一利なし」 ということが判明しました。