タグ「Java」が付けられているもの

画像分類の学習モデルを作るツールをJavaで作っていたのですが、あるJPEGファイルをImageIOで読もうとすると以下のエラーが出ました。

Exception in thread "main" java.awt.color.CMMException: Invalid image format
    at sun.awt.color.CMM.checkStatus(CMM.java:131)
    at sun.awt.color.ICC_Transform.(ICC_Transform.java:89)
    at java.awt.image.ColorConvertOp.filter(ColorConvertOp.java:516)
    at com.sun.imageio.plugins.jpeg.JPEGImageReader.acceptPixels(JPEGImageReader.java:1169)
    at com.sun.imageio.plugins.jpeg.JPEGImageReader.readImage(Native Method)
    at com.sun.imageio.plugins.jpeg.JPEGImageReader.readInternal(JPEGImageReader.java:1137)
    at com.sun.imageio.plugins.jpeg.JPEGImageReader.read(JPEGImageReader.java:948)
    at javax.imageio.ImageIO.read(ImageIO.java:1422)
    at javax.imageio.ImageIO.read(ImageIO.java:1282)
    at Test.main(Test.java:31

JAIだと読み込めるというので、処理速度が落ちるけどそっちを使うようにしてみた。

BufferedImage img = null;
try {
    img = ImageIO.read(file);
} catch (java.awt.color.CMMException e) {
    img = JAI.create("fileload", file.getAbsolutePath()).getAsBufferedImage();
} catch (IOException e) {
    // ちゃんと処理しようね!
}

正しい使い方としては、JAIをきちんとセットアップしてImageIO経由で使えるようにし、ImageReaderを列挙して順番に試すというのが正統な方法なのかなと。今回は手抜き処理。

JAIだけでも良いんだけど、速度が・・。

参考サイト

 

今回はAvocadoDBの簡単な概念とJavaからの扱い方を説明します。そういえばAvocadoDBはmrubyの人柱に名乗りを上げています。mrubyを使ってみる理由は彼らのBlogに書いてあります。前回の記事でマルチスレッドですと書きましたが、V8をマルチスレッドで扱うのめんどくね?とちょっと思っていたのですが、彼らも思っていたようですw またAvocadoDBのQueryLanguageはRFC化を目指しているようです。 とっても意欲的ですね。

※Version0.3.12を元に記載していますので、今後変わる可能性があります。

さて、まずはAvocadoDBの用語説明をします。全部は解説しません。一部だけです。

用語 説明
Collection RDBで言うところのテーブルです
Collection Identifier コレクションを識別するための内部ID(整数型です)
Collection Name コレクションを識別するための名前です(文字列です)。Collection Identifierと内部的に1:1に対応しています。命名規則は先頭が英字で他は英数かアンダーバー、ハイフンで構成されます。ハイフンを除けば常識的なプログラミング言語の変数名の規則と同じです。
Document RDBで言うところのレコードです
Document Identifier Documentを識別するための内部ID(整数型です)。これを直接使うことはほとんどありません。↓のDocumentHandleを通常は使います。
Document Handle Documentを識別するためのHandle名(文字列です)。書式はCollectionID + "/" + DocumentIDとなります。
Document Revision MVCCのMV(Multi Version)のバージョン番号にあたるものです。整数型です。これとDocument Etagを併せて説明しないといけないのですが、今回は省略します・・。

HTTP-RESTのInterfaceではCollectionNameもCollectionIDも両方サポートしています。またDocumentの実際の値はJSONになります。JSONObjectでも、JsonArrayでも大丈夫ですし、AvocadoDBはKVSでもあるのでJSONの要素(文字列、整数、Boolean、NULL)も受け付けます。

ちょっとだけGraphDBとしてのAvocadoDBも説明します。
Graph構造はVertexとEdgeで構成されます。VertexはDocumentに相当します。Edgeは専用で作るAPIが存在します。有向グラフか無向グラフ(要はエッジの向きがあるか無いか)はEdgeを作るときには指定できません。基本は有効グラフとしてEdgeを作ります。Edgeを取得する時にany, in, outを指定して取ります。VertexはDocumentなので任意の属性を保存する事ができ、Edgeも同様に任意の属性を保存する事ができます。 

Indexの種類は今は3つあります。Hash Index, Geo Index, Skip Listsです。(B-treeが無い理由はVideo見ればわかったりするのかな?)。複合インデックスに出来ますが、Geo Indexは要素は2つまでの制約があります。Indexはユニーク制約を付けることができます。HashとSkipListsの特性の違いの説明は必要ありませんよね。

CollectionやDocumentを作るときに、Diskへの書き込みを同期化するかのオプションがあります。InnoDBのFlushパラメータみたいなのですが、これは基本的にAsyncにしないと性能が出ません。(※性能テストはRoadMapの1.0betaで行う事になっているので、今の段階で性能云々を言うのはちょっとお門違いです)

さて、次はJavaで扱う方法です。Javaのドライバはこちらにあります。JARファイル作っていません。作った方がいいかな?
※prototypeですので、サーバが落ちている時のハンドリングを入れていません。

今できる事は、HTTP-RESTを叩いてCollection, Document, Cursor, Index, Edgeの操作ができます。

使い方はREADMEかExampleに書いてあります。単体テストを見るのが良いと思います。
以下はDocumentを1000個適当に作ってクエリで検索するサンプルです。Cursorを扱いやすくするためにResultSetを作ってみました。

public class Example1 {

  public static class ExampleEntity {
    public String name;
    public String gender;
    public int age;
  }

  public static void main(String[] args) {

    AvocadoConfigure configure = new AvocadoConfigure();
    AvocadoDriver driver = new AvocadoDriver(configure);

    try {
      for (int i = 0; i < 1000; i++) {
        ExampleEntity value = new ExampleEntity();
        value.name = "TestUser" + i;
        switch (i % 3) {
        case 0: value.gender = "MAN"; break;
        case 1: value.gender = "WOMAN"; break;
        case 2: value.gender = "OTHER"; break;
        }
        value.age = (int) (Math.random() * 100) + 10;
        driver.createDocument("example_collection1", value, true, null, null);
      }

      HashMap<String, Object> bindVars = new HashMap<String, Object>();
      bindVars.put("gender", "WOMAN");

      CursorResultSet<ExampleEntity> rs = driver.executeQueryWithResultSet(
          "select t from example_collection1 t where t.age >= 20 && t.age < 30 && t.gender == @gender@",
          bindVars, ExampleEntity.class, true, 10);

      System.out.println(rs.getTotalCount());
      for (ExampleEntity obj: rs) {
        System.out.printf("  %15s(%5s): %d%n", obj.name, obj.gender, obj.age);
      }

    } catch (AvocadoException e) {
      e.printStackTrace();
    } finally {
      driver.shutdown();
    }

  }

}

グラフも扱えます。 グラフの扱い方は以下のようになります。ただし効果的にトラバーサルする方法がまだ提供されていません。TinkerPopのBluePrintsとGremlinに対応するらしいので、そのあたりで出来ることは将来的にできるようになると思います。(私はGremlinでGraphを操作した事がないので詳細はわかりません。)
以下のサンプルは、Vertexを10個作って、エッジを3つ(e1=v0->v1, e2=v0->v2, e3=v2->v3)作り、v0のエッジを求めています。 

public class Example2 {

  public static class TestEdgeAttribute {
    public String a;
    public int b;
    public TestEdgeAttribute(){}
    public TestEdgeAttribute(String a, int b) {
      this.a = a;
      this.b = b;
    }
  }
  public static class TestVertex {
    public String name;
  }

  public static void main(String[] args) {

    AvocadoConfigure configure = new AvocadoConfigure();
    AvocadoDriver driver = new AvocadoDriver(configure);

    final String collectionName = "example";
    try {

      // CreateVertex
      ArrayList<DocumentEntity<TestVertex>> docs = new ArrayList<DocumentEntity<TestVertex>>();
      for (int i = 0; i < 10; i++) {
        TestVertex value = new TestVertex();
        value.name = "vvv" + i;
        DocumentEntity<TestVertex> doc = driver.createDocument(collectionName, value, true, false, null);
        docs.add(doc);
      }

      // 0 -> 1
      // 0 -> 2
      // 2 -> 3

      EdgeEntity<TestEdgeAttribute> edge1 = driver.createEdge(
          collectionName, docs.get(0).getDocumentHandle(), docs.get(1).getDocumentHandle(),
          new TestEdgeAttribute("edge1", 100));

      EdgeEntity<TestEdgeAttribute> edge2 = driver.createEdge(
          collectionName, docs.get(0).getDocumentHandle(), docs.get(2).getDocumentHandle(),
          new TestEdgeAttribute("edge2", 200));

      EdgeEntity<TestEdgeAttribute> edge3 = driver.createEdge(
          collectionName, docs.get(2).getDocumentHandle(), docs.get(3).getDocumentHandle(),
          new TestEdgeAttribute("edge3", 300));

      EdgesEntity<TestEdgeAttribute> edges = driver.getEdges(collectionName, docs.get(0).getDocumentHandle(), Direction.ANY, TestEdgeAttribute.class);
      System.out.println(edges.size());
      System.out.println(edges.get(0).getAttributes().a);
      System.out.println(edges.get(1).getAttributes().a);

    } catch (AvocadoException e) {
      e.printStackTrace();
    } finally {
      driver.shutdown();
    }

  }

}

MurmurHash Java版

リハビリのためにMurmurHashをJavaに移植してみました。
Version1,2,3全てありますので、32bit,64bit,128bitのハッシュを求めることができます。

ソースはこちらです。
https://github.com/tamtam180/MurmurHash-For-Java

オリジナルと同じ結果を返すことは確認済みです。

MurmurHashってなんぞという人はオリジナルのサイトをどうぞ。
https://sites.google.com/site/murmurhash/
http://code.google.com/p/smhasher/

github便利だけど、バージョン管理して配布するような場合ってGoogle Projectの方が良いような気がしてる。パッケージをダウンロードさせたい場合、Githubではどうやるのかわかんないや。

HiveのUDF一覧を自動生成してみた

発熱でDOWNしているタムタムです。
インフルエンザのようなすごい高熱というわけでもなく、プリン食べて家でしょぼーんとしてます。

今回はHiveのUDF一覧を自動生成してみました。(Hive0.8)
https://mt.orz.at/archives/hive_udf_0.8.html
(追記) Hive0.8用はURLを変更して、こちらにtrunk(rev1293519)のバージョンを起きました。

reflect関数とかWikiには載っていないと思います。
builtinと書いてあるやつはFunctionRegistryのstaticイニシャライザで関数登録しているやつです。Alias名がある関数はそれぞれ1つの項目に作ってあります。

作り方は至ってシンプルで、UDF関数はDescriptionというアノテーションが定義されています。desc function xxxxという構文で表示される説明は、このアノテーションの情報を表示しているだけです。というわけで、SpringFrameworkのClassPathScanningCandidateComponentProviderを使ってClassPath中のDescriptionアノテーションが定義してあるクラスを取得して情報を拾っているだけです。

こんな感じです。

ClassPathScanningCandidateComponentProvider scanner =
        new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AnnotationTypeFilter(Description.class));
Set<BeanDefinition> beans = scanner.findCandidateComponents("");

取得したBean定義を回して、リフレクション使ってAnnotation情報を取得します。
nameをカンマで区切っているのは、Aliasがある場合はカンマ区切りで入っているからです。あとはdescから情報を普通に引っこ抜けます。

for (BeanDefinition def : beans) {
    Class<?> clazz = Class.forName(def.getBeanClassName());

    Description desc = AnnotationUtils.findAnnotation(clazz, Description.class);
    String[] funcNames = desc.name().split(",");
}

簡単ですね (=ω=)b

JavaのsubListの罠

本当は食べ物の写真とか載せたいんですけどね。。
ついこの前やらかしちゃった事を備忘録としてメモメモ。

Javaでは基本的にSerializableを実装していないと(Externalizableでもいいけど)、Objectをシリアライズできません。例えばMemcachedにそのままObjectをまるっと保存する場合は、ObjectOutputStreamを介して保存する事が多いと思います。(MsgPackとかJSONICとかを使う方が良いかもしれませんが)。

で、この前やらかしてしまったのは、JavaのListインターフェースにあるsubListで返ってくるリストはSerializableインターフェースを実装していないListなので、キャッシュに保存できないでエラーが出まくりでした。と書くと語弊があるので正確に書くと、ArrayListとLinkedListはAbstractListを継承しており、subListの実装はAbstractListが持っています。AbstractList#subListの実装は、RandomAccessを実装しているクラスの場合はRandomAccessSubListを、実装していない場合はSubListのObjectを返します。RandomAccessSubListもSubListもSerializableは実装されていません。

public List<E> subList(int fromIndex, int toIndex) {
    return (this instanceof RandomAccess ?
            new RandomAccessSubList<E>(this, fromIndex, toIndex) :
            new SubList<E>(this, fromIndex, toIndex));
}

ちなみに、subListは要素をコピーして作っているわけでは無く、単純に元リストのラッパークラスのように振る舞っているだけなので、そこも注意なのです。subListで返ってきたリストに対して操作するとどうなるかのサンプルを書きました。

import java.util.ArrayList;
import java.util.List;

public class Test1 {
	public static void main(String[] args) throws Exception {

		// サンプルデータを作る
		ArrayList<Integer> srcList = new ArrayList<Integer>();
		for (int i = 0; i < 10; i++) {
			srcList.add(i);
		}
		System.out.println("src=" + srcList);
		// サブリストを作ってみる
		List subList = srcList.subList(5, 8);
		System.out.println("sub=" + subList);
		// サブリストを操作する
		subList.add(100);

		System.out.println("=====after=====");
		System.out.println("sub=" + subList);
		System.out.println("src=" + srcList);

		System.out.println(subList instanceof Serializable);
	}
}

実行結果

src=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
sub=[5, 6, 7]
=====after=====
sub=[5, 6, 7, 100]
src=[0, 1, 2, 3, 4, 5, 6, 7, 100, 8, 9]
false

 

JavaのClassLoaderの話をちょっとだけ。意外にClassLoaderを知らない人が居るようです。

ざっくりと説明をしますと、
クラスローダーにはBootstrapクラスローダーやシステムクラスローダー、コンテキストクラスローダーがあります。(あと拡張クラスローダーですかね)。そんでもって、クラスローダーは親子関係を持っています。委譲モデルと言って、あるクラスローダーにクラスをロードする依頼があると、親に依頼を委譲します。最初に発見されたクラスが実際にロードされるわけです。

1. Bootstrap (rt.jar)
2. 親クラスローダーA
3. 子クラスローダーB

こんなクラスローダーがあるとしてまだロードされていないクラスXをロードしようとした場合は次のように動作します。

  1. B.loadClass(X)
  2. Aに委譲
  3. Bootstrapに委譲
  4. BoostrapにXは存在しない
  5. AにXは存在しない
  6. 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使えばいいじゃんとかそういう野暮な事は言わないの。)

 

この前u15が出たばっかりなのに、JDK6u16がリリースされていました。

リリースノートを見ると、修正内容は1件。
ピンポイントでブレークポイントのバグが直ってた!!

http://java.sun.com/javase/6/webnotes/6u16.html

(ちなみに、日本語のページはまだJDKu15でした)

さっそく試してみなければ。

Eclipse3.5を入れて、CTRL+クリックでジャンプする時にInterfaceかImplemented Classかを選択する事ができるのがすごい便利なんです。特にInterfaceベースのDIをしまくるプログラムを書いていると重宝します。

で、ブレークポイントを設定しても止まってくれないのです。正確には、最初の1回だけは止まります。その後、全てのブレークポイントが無視されますΣ(゚д゚lll) なんでかなーと思ったら、同じ症状の人を発見しました。

どうやらEclipseは関係なく、JDK6u14のバグらしいです。u14から圧縮ポインタ(?)や新しいGCアルゴリズムが追加されているのですが、GCが悪さをしているのかなぁ。。BugID:6862295で登録されています。仕方ないのでJDK6u13を探そうとしたら、JDK6u15がリリースされていました。日本語のリリースノートも出ています。それによると、、

Java ™ Virtual Machine Tool Interface (JVM TI) のブレークポイントは、並列スカベンジガベージコレクタ (-XX:+UseParallelGC) または並列圧縮ガベージコレクタ (-XX:+UseParallelOldGC) が使用されている場合のみ信頼できます。

ほかのコレクタが使用されている場合は、ブレークポイントが機能しなくなることがあり、フル GC 処理の実行後に JVM TI オブジェクトタグが使用不可になることがあります。Java ™ Debug Interface (JDI) ThreadReference には、JVM TI オブジェクトタグに依存する埋め込みのスレッド ID があるため、この埋め込みのスレッド ID が予期せず変更されることがあります。これにより、スレッドベースの JDI イベントに混乱が発生する可能性があります。

シリアルガベージコレクタ (-XX:+UseSerialGC) はこの問題に対して脆弱ですが、一部のプラットフォームではデフォルトで選択されることに注意してください。この問題を回避するには、コマンド行オプション -XX:+UseParallelGC を使用して並列スカベンジコレクタを明示的に選択します。

との事です。(´ε`;)ウーン…。

JDK6u15ではタイムゾーン関連が更新されているようです。
tzdata2009iはバングラディッシュ関連??
それよりも「ヨルダンの新規則により、tzdata2009h 以降の SimpleTimeZone 解析に障害が発生する」これでしょう(;´Д`) タイムゾーンや夏時間は本当にめんどくさいですね(´・ω・)

追記:JDK6u16がさっそくリリースされてピンポイントで修正されていました。

前の記事で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));

svnkitを使って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データを一元管理できるので、マルチプロセスで動作するアプリ側でデータの整合性を気にする必要はなくなるヽ( `・ω・)ノ

 

すごく手抜き処理の動けばいいやー程度のやっつけコードです。
よい子は真似しちゃだめですよ(;´Д`)

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

最近は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とか使えば良いんですが。

そんなわけで、重要なログを別ファイルに追加出力をしてみたいと思います。

 

 

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の取得に困ります。

  1. 名簿テーブルにデータをINSERTする。(IDは自動生成で1が入ったとする)
  2. 自動生成された1を取得するために以下の方法で取得する
    1. SELECTを発行する
    2. LAST_INSERT_ID()をコールする
  3. 係り活動テーブルにデータをINSERTする。

SELECTの場合、MAX関数使った場合はマルチスレッド、マルチプロセスで破綻します。
LAST_INSERT_ID()の場合、発行する順番に気をつければ同一セッション内で値が保持されている・・・?
詳しくはわかりません(´・ω・`)

またAUTO_INCREMENTをやめてシーケンステーブルを作成した場合、SELECTは上記と同じ理由で破綻(ロックかけるなら別ですけど。)、LAST_INSERT_IDは上手くいくが、どちらにせよ2往復している事になります。

もしDBがSEQUENCEをサポートしている場合(名簿テーブルのIDはAUTO_INCREMENTではありません)、

  1. シーケンスを進めて値を取得する(仮に1とする)
  2. 名簿テーブルにデータをINSERTする
  3. 係り活動テーブルにデータをINSERTする

こんな感じで済みます。

とりあえず、2往復は効率が悪そう。でMySQLはシーケンスをサポートしていない。となれば作るしかなさそうです。

INSERTの戻り値として自動生成された値を取得する場合、PERLではDBIのmysql_insertidを使えば出来、JDBCの場合はStatement#getGeneratedKey()を使う事で取得が可能になるようです。ちなみにOracleの場合はgetGeneratedKey()でカラムを取得してもRowId型になった気がします。(誰かのBLOGに載っていた予感)

というわけで、早速コードを書いてみました。 

 

※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>タグの中身がエスケープされていないものがありました。

みんな結構いい加減だなぁ(´・ω・)