Mavenでのクラスの重複チェック

ライブラリの管理にMavenやGradleを利用するのが一般的だと思いますが、個人的にいつも憎たらしく思うのがgroupIdやartifactIdがバージョンによって変わってくるケース。例えばこんなの。

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
</dependency>

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>servlet-api</artifactId>
    <version>2.5</version>
</dependency>

何が厄介かというと、groupIdとartifactIdが違うことによって別物と認識されるため、依存関係の連鎖の中で意図せず混入し、同一のFQCN(完全修飾クラス名)を持つクラスが複数存在することになります。

クラス重複の危険性

意図しないartifact、意図しないバージョンのクラスが読み込まれることで、実行時にメソッドがないよと例外が出たり、Slf4Jのアダプタと本家のクラスが混在したようなケースだと想定外の挙動をしたりします。

また、複数同一クラスが存在した場合は先に読み込まれた者勝ちとなり、また読み込み順もクラスパスの順、ファイルシステムでのjarファイルの並び順だったりとで規則性があり、どのクラスが読み込まれるかについては環境が一致していれば大体再現性があります。幸か不幸か、たまたま今までは意図したクラスが読み込まれて正常稼働していた場合でも何らかの環境の変化により、突然意図しないクラスが読み込まれて障害となることもあり、極力危ないものは除外しておきたいところです。

重複しているクラスを持つartifactを見つけるのは面倒

では、重複クラスをどう見つけるか、ですが、servlet-apiの様な例は分かりやすく簡単にチェックできるのですが、groupId/artifactIdが似ても似つかないものになっていたり、他アーティファクトのクラスを同梱してしまっていたりと、巧妙に隠されていることがあります。 1個1個JARを解凍して探すのも手でやるのは非常に面倒なので、簡単なスクリプトを作ってみました。

groovyがない場合でも動かせるよう実行可能JAR化できるようにgradleプロジェクトにしてあります。

github.com

ところで、作る前にも似たようなツールないか調べたのに、作ってから再度調べたらクラス重複チェックしてくれるmaven pluginが存在したようで…。

github.com

Javassistでランタイムでクラスを書き換える

イケてないクローズドなライブラリを使う際にどうしても現在のシステムと合わない箇所があり、なんとかクローズドなライブラリに手を入れられないか?ということでJavassistでランタイムでクラスを書き換えて対応しようというものです。

unk極まりないですが、にっちもさっちもいかない場合に使えるのと、自分も滅多に使わないのでまとめようと思います。

前提

コード例

コード簡略化のため、コマンドから実行するソースとしていますが、同様のコードでアプリケーションサーバ上でも動くはずです。

設計上の注意点として既にクラスローダーに読み込まれてしまったクラスは変更できないため、読み込まれる前に書き換えを行う必要があります。 ですのでWEBアプリであればListenerなど、とにかくデプロイの早い段階で読み込むことを保障することが重要です。

github.com

import javassist.*;

public class Sample {

    public static void main(String... args) throws NotFoundException, CannotCompileException {


        final ClassPool classPool = ClassPool.getDefault();

        // Webアプリではクラスローダーが複数あるので対象のクラスが存在するクラスローダーを
        // ClassPoolに登録しないとうまく動作しない可能性があるとのこと。
        // WARのクラスローダーが欲しいので自分のクラスのローダーを取得して登録している
        // http://jboss-javassist.github.io/javassist/tutorial/tutorial.html
        classPool.insertClassPath(new ClassClassPath(Sample.class));

        CtClass simpleLoggerClass = classPool.get("SimpleLogger");
        // 変更済みなら終える(一度変更済みだと再度の変更はできない)
        if (simpleLoggerClass.isModified()) {
            return;
        }

        // 全コンストラクタを削除
        final CtConstructor[] constructors = simpleLoggerClass.getDeclaredConstructors();
        for (CtConstructor constructor : constructors) {
            simpleLoggerClass.removeConstructor(constructor);
        }

        // デフォルトコンストラクタを生成して追加
        // デフォルトコンストラクタ1つは作っておかないとnewできない
        simpleLoggerClass.addConstructor(CtNewConstructor.defaultConstructor(simpleLoggerClass));


        CtMethod methods = simpleLoggerClass.getDeclaredMethod("log");

        // ここらへん参考に置き換えるソースを記述
        // http://jboss-javassist.github.io/javassist/tutorial/tutorial2.html#alter
        String newSrc = "{"
                + "System.out.println(\"[modified]\" + $1);"
                + "}";

        methods.setBody(newSrc);

        // 読み込みの時と同様の理由で変更後クラスを登録する際にもクラスローダーを指定してあげる
        // http://jboss-javassist.github.io/javassist/tutorial/tutorial.html#load
        simpleLoggerClass.toClass(Sample.class.getClassLoader(), null);

        final SimpleLogger simpleLogger = new SimpleLogger();

        simpleLogger.log("test");
    }
}
import java.util.logging.Logger;

public class SimpleLogger {

    private Logger logger;

    // 消される
    public SimpleLogger() {
        logger = Logger.getLogger(this.getClass().getName());
        logger.info("create SimpleLogger");
    }

    // 標準出力への出力に変更される
    public void log(String text) {
        logger.info(text);
    }
}

実行結果です。以下2点が確認できます。

  • コンストラクタによるログの出力がない
  • logメソッドによるログ出力が変更されている
~/git/javassistsample % ./gradlew run                                                                                                                       (git)-[master]
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:run
[modified]test

BUILD SUCCESSFUL

Total time: 6.625 secs

This build could be faster, please consider using the Gradle Daemon: https://docs.gradle.org/2.13/userguide/gradle_daemon.html

PCとディスプレイをHDMIで繋いでハマったこと

mac book air(2012)用にHDMI出力アダプターを購入してモニター(RDT231WM)につないだところ、どんなに調整しても内蔵ディスプレイと比較して色がでない部分があった。
そこでふと思いついて以下2パターンで試した見たところ、グレーの表示が全然違う…。

  • HDMIを直接つないだ場合
  • HDMI→DVIの変換アダプタでDVI接続した場合

HDMI接続


HDMI→DVI変換接続

どうもディスプレイがHDMI v1.1までの対応なのが原因らしい。
PCはRGBでの出力なのにHDMIv1.1ではRGBに対応していなく、YCbCrのみの対応らしい。
HDMI各バージョンの機能詳細

HDMIのバージョンは気にしておいた方がいいんだろうなぁと思った昨夜。

MySQLを任意の場所にコンパイルする

Unix系の環境では開発用に使うツール類は$HOME/localディレクトリ配下に作る趣味だが、MySQLを$HOME/local/mysqlにインストールする方法が少々特殊だったため、メモ。

コンパイル&インストール

mkdir -p $HOME/local/src
cd $HOME/local/src
tar zxvf ~/Download/mysql-5.5.27.tar.gz
cd mysql-5.5.27
cmake -DCMAKE_INSTALL_PREFIX:PATH=$HOME/local/mysql-5.5.27 . 
make all install

データベースの初期化

cd $HOME/local/mysql-5.5.27
scripts/mysql_install_db --user=username

Windowsマシン上の隠し共有フォルダにアクセスする

Windowsの共有フォルダではフォルダ名の末尾に「$」をつけると隠しフォルダ扱いされ、Windowsからアクセスした場合表示させなくすることができます。
この隠しフォルダをmacからみようとした際に少しはまったのでメモ。

Finderにて[移動]-[サーバへ接続]を開く。
以下だとうまく接続できない。
smb://PC名/drive$

ユーザー名をパスに含めるとうまくつながる。
smb://ユーザー名@PC名/drive$

これで接続できました。

macにhomebrewを入れてgitを入れた

いろいろあってmac book airを買いました。
普段がUbuntuなので簡単に移行できるかと思ったものの、やはりそう甘くはなかったです。
macで試行錯誤したことを書こうかと思います。

さて、macにhomebrewとgitを入れようとして早速軽くつまづいたのでそれについて残しておこうかと思います。

homebrewのインストール

楽勝です。

/usr/bin/ruby -e "$(/usr/bin/curl -fsSL https://raw.github.com/mxcl/homebrew/master/Library/Contributions/install_homebrew.rb)"

以上。

gitのインストール

以下コマンドでgitが入ります。

brew install git

エラーが。。。。

==> Downloading http://git-core.googlecode.com/files/git-1.7.11.1.tar.gz
######################################################################## 100.0%
==> make prefix=/usr/local/Cellar/git/1.7.11.1 CC=/usr/bin/xcrun clang CFLAGS=-O
GIT_VERSION = 1.7.11.1
    * new build flags or prefix
    * new link flags
./generate-cmdlist.sh > common-cmds.h+ && mv common-cmds.h+ common-cmds.h
/usr/bin/xcrun clang -o hex.o -c -MF ./.depend/hex.o.d -MMD -MP  -Os -w -pipe -march=native -Qunused-arguments -I. -DUSE_ST_TIMESPEC -DNO_GETTEXT  -DHAVE_DEV_TTY -DXDL_FAST_HASH -DSHA1_HEADER='<openssl/sha.h>'  -DNO_MEMMEM -DSHELL_PATH='"/bin/sh"'  hex.c
/usr/bin/xcrun clang -o ident.o -c -MF ./.depend/ident.o.d -MMD -MP  -Os -w -pipe -march=native -Qunused-arguments -I. -DUSE_ST_TIMESPEC -DNO_GETTEXT  -DHAVE_DEV_TTY -DXDL_FAST_HASH -DSHA1_HEADER='<openssl/sha.h>'  -DNO_MEMMEM -DSHELL_PATH='"/bin/sh"'  ident.c
/usr/bin/xcrun clang -o kwset.o -c -MF ./.depend/kwset.o.d -MMD -MP  -Os -w -pipe -march=native -Qunused-arguments -I. -DUSE_ST_TIMESPEC -DNO_GETTEXT  -DHAVE_DEV_TTY -DXDL_FAST_HASH -DSHA1_HEADER='<openssl/sha.h>'  -DNO_MEMMEM -DSHELL_PATH='"/bin/sh"'  kwset.c
In file included from ident.c:8:
In file included from kwset.c:37:
In file included from In file included from ./cache.h:4:
./git-compat-util.h./cache.h::93:10In file included from hex.c:1::
In file included from ./cache.h4 :
:./git-compat-util.h4:
./git-compat-util.h:fatal error: 'unistd.h' file not found
93:10: fatal error: 'unistd.h' file not found
:93:10: fatal error: 'unistd.h' file not found
#include <unistd.h>
#include <unistd.h>         ^

         ^
#include <unistd.h>
         ^
/usr/bin/xcrun clang -o levenshtein.o -c -MF ./.depend/levenshtein.o.d -MMD -MP  -Os -w -pipe -march=native -Qunused-arguments -I. -DUSE_ST_TIMESPEC -DNO_GETTEXT  -DHAVE_DEV_TTY -DXDL_FAST_HASH -DSHA1_HEADER='<openssl/sha.h>'  -DNO_MEMMEM -DSHELL_PATH='"/bin/sh"'  levenshtein.c
1 error generated.
1 error generated.
make: *** [hex.o] Error 1
make: *** Waiting for unfinished jobs....
make: *** [ident.o] Error 1
In file included from levenshtein.c:1:
In file included from ./cache.h:4:
./git-compat-util.h:93:10: fatal error: 'unistd.h' file not found
#include <unistd.h>
         ^
1 error generated.
make: *** [kwset.o] Error 1
1 error generated.
make: *** [levenshtein.o] Error 1
==> Build Environment
CPU: quad-core 64-bit dunno
MacOS: 10.7.4-x86_64
Xcode: 4.3.3
CC: /usr/bin/xcrun clang => /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang

CXX: /usr/bin/xcrun clang++ => /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang++

LD: /usr/bin/xcrun clang => /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang

CFLAGS: -Os -w -pipe -march=native -Qunused-arguments
CXXFLAGS: -Os -w -pipe -march=native -Qunused-arguments
MAKEFLAGS: -j4
Error: Failed executing: make prefix=/usr/local/Cellar/git/1.7.11.1 CC=/usr/bin/xcrun\ clang CFLAGS=-Os\ -w\ -pipe\ -march=native\ -Qunused-arguments LDFLAGS= install (git.rb:49)
These existing issues may help you:
    https://github.com/mxcl/homebrew/issues/8643
    https://github.com/mxcl/homebrew/issues/9618
    https://github.com/mxcl/homebrew/issues/10544
    https://github.com/mxcl/homebrew/issues/11481
    https://github.com/mxcl/homebrew/issues/12344
    https://github.com/mxcl/homebrew/issues/12814
    https://github.com/mxcl/homebrew/issues/12897
Otherwise, please report the bug:
    https://github.com/mxcl/homebrew/wiki/reporting-bugs

この場合、Xcodeをインストール・起動し、
[Xcode] - [preference] - [Downloads]からCommand Line Toolsをインストールし、再度gitをインストールします。

JPA2ではEntityManagerなしでCriteriaを作る方法がない?

JPA2では以下のようにしてCriteriaを生成します。

// entityManagerFactory.getCriteriaBuilder(); でもOK
entityManager.getCriteriaBuilder();
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Company> cq = cb.createQuery(Company.class);
Root<Company> root = cq.from(Company.class);

ここで気になるのが以下のようなこと。

  • JPQLのNamedQueryのようなことができないのか?
  • JDBCのPrepearedStatementのようなことができないのか?
  • Criteriaを毎回生成すると地味にコストがかかっているのではないか?

ということでどこか別のところに予めCriteriaを生成して、利用するときにパラメータだけ渡して実行できないのか調べてみました。
結論としては"なさそう"です。

どこかでそんなことができたように記憶していたと思い調べてみたらそれはHibernateのようでした。
Hibernateだと以下のようなことが可能です。
ドキュメントから引用

DetachedCriteria query = DetachedCriteria.forClass(Cat.class)
    .add( Property.forName("sex").eq('F') );
....
Session session = ....;
Transaction txn = session.beginTransaction();
List results = query.getExecutableCriteria(session).setMaxResults(100).list();

JPA2のクライテリアはタイプセーフなのは評価できるけれど、APIのセンスが微妙…。