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プロジェクトにしてあります。
ところで、作る前にも似たようなツールないか調べたのに、作ってから再度調べたらクラス重複チェックしてくれるmaven pluginが存在したようで…。
Javassistでランタイムでクラスを書き換える
イケてないクローズドなライブラリを使う際にどうしても現在のシステムと合わない箇所があり、なんとかクローズドなライブラリに手を入れられないか?ということでJavassistでランタイムでクラスを書き換えて対応しようというものです。
unk極まりないですが、にっちもさっちもいかない場合に使えるのと、自分も滅多に使わないのでまとめようと思います。
前提
- JavaAssist 3.20.0-GA使用
- アプリケーションサーバにデプロイしてWEBアプリとして動作
コード例
コード簡略化のため、コマンドから実行するソースとしていますが、同様のコードでアプリケーションサーバ上でも動くはずです。
設計上の注意点として既にクラスローダーに読み込まれてしまったクラスは変更できないため、読み込まれる前に書き換えを行う必要があります。 ですのでWEBアプリであればListenerなど、とにかくデプロイの早い段階で読み込むことを保障することが重要です。
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で繋いでハマったこと
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
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のセンスが微妙…。