ブログトップ 記事一覧 ログイン 無料ブログ開設

水まんじゅう

 

2016-12-09

Java EE Webアプリケーションをモジュール化して組み合わせる

これはJava EE Advent Calendarの9日目の記事です。

完全に市民権を得た感じのあるSpring Bootですが、機能は複数のjarに分けて固められており、jarを追加するだけで機能を追加することが出来ます。
それと同じ事をJava EEでもやってみようというお話です。

たとえば、以下のようなクラスを用意します。

@WebFilter(urlPatterns = "/*", filterName = "auth")
public class AuthenticationFilter implements Filter {
    @Override
    public void init(FilterConfig fc) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain fc) throws IOException, ServletException {
        if (!authorize(authKey, req)) {
            sendError(res);
            return;
        }
        fc.doFilter(req, res);
    }

    private void sendError(ServletResponse res) throws IOException {
        HttpServletResponse response = (HttpServletResponse) res;
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
    }

    private boolean authorize(String authKey, ServletRequest req) throws ServletException {
        //実際の認証処理
    }

    @Override
    public void destroy() {
    }
}

これをauthrization.jarにまとめて、認証処理を通したい場合はクラスパスに通して、認証処理を通したくない場合はクラスパスからはずせば認証処理だけモジュール化することが出来ます。

これはテストのときに非常に便利です。
また、各モジュールの祖結合化が進み、可搬性もあがります。


これを応用すると、テストのときと本番のときとで使用するライブラリを入れ替えられることにも気が付くと思います。
aaa-production.jaraaa-test.jarを用意して、それぞれで同じ名称のXXX.javaを作成します。
通常、CDIを利用しているとQualifierを定義してテストのときだけプライオリティを変更して云々とかやって、テストするだけでひどい苦労をするのですが、そういったことよりはもうクラス自体入れ替えてしまえばいいんじゃないかとか。

JAX-WSでエンドポイントから生成したJAX-WSクライアントライブラリについてもテスト環境と本番環境でどうせ違うのはURLだけだったりするので、テストのときだけフックしてURL書き換えてーとかするよりはこちらのほうが簡単になります。


という感じで。

最後にmavenライブラリを切り替える方法だけ説明をしておくと、mavenではprofileの仕組みがあり、profileのところに依存関係を書くことで特定のprofileのみ依存するjarを増やすみたいなことが出来ます。

〜略〜
    <profiles>
        <profile>
            <id>release</id>
            <dependencies>
                <dependency>
                    <groupId>xxxxx</groupId>
                    <artifactId>auth</artifactId>
                    <version>1.0</version>
                </dependency>
            </dependencies>
        </profile>
        <profile>
            <id>development</id>
            <dependencies>
                <dependency>
                    <groupId>xxxxx</groupId>
                    <artifactId>auth-test</artifactId>
                    <version>1.0</version>
                </dependency>
            </dependencies>
        </profile>
    </profiles>
〜略〜


終わり。

2016-12-02

どこからも使用されてないクラスを列挙する

これはJava Advent Calendarの2日目の記事です。

さて、Javaで開発をしているといつの間にかどこからも使用されていないクラスというものが出てきてしまいます。
リファクタリングや仕様変更の結果、呼び出されてなくなったクラスです。

それら、どこからも使用されてないクラスを一覧化してみます。

まず、使用されているクラスを一覧化します。
一覧化にはjdepsを使用します。

jdepsの概要についてはCLOVERの記事を参照してください。

CLOVERの記事ではclassもしくはjarを指定していますが、実はパッケージ(フォルダ)を指定することで、その下のクラスの依存先をすべて出力してくれます。

使用されているクラスの一覧が出来たら、全体のクラスの一覧から引き算してあげます。
package 配下のクラス一覧を取得する方法はいろいろあるようですが、今回はGuavaのClassPathを使用しました。

ということで、以下のような感じです。


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.file.Paths;
import java.util.Set;
import java.util.stream.Collectors;

import com.google.common.reflect.ClassPath;

public class SelectNotUsedClass {

  public static void main(String[] args) throws IOException {

    // buildはコンパイルされたクラスの出力先
    Process process = new ProcessBuilder("jdeps", "-v", Paths.get("build").toAbsolutePath().toString()).start();

    //Charsetは各環境に合わせてください 
    Set<String> dependencies = new BufferedReader(new InputStreamReader(process.getInputStream(), Charset.forName("Windows-31J"))).lines().map(s -> s.trim())
        .filter(s -> s.contains("->")).map(s -> s.substring(s.indexOf("->"))).map(s -> s.replace("->", "").trim().replaceAll("\\s+.*$", ""))
        .collect(Collectors.toSet());

    ClassLoader loader = Thread.currentThread().getContextClassLoader();

    // jp.hoge.xxxxは依存関係を調べたいパッケージの名前
    ClassPath.from(loader).getTopLevelClassesRecursive("jp.hoge.xxxx").stream().map(info -> info.getName()).filter(c -> !dependencies.contains(c))
        .forEach(System.out::println);
  }
}

jdepsが出るまでは.javaファイルのimport行を解析して出してたりしましたが、ずいぶんと楽になりましたね。

2016-12-01

Go言語を完全にマスターする

これはさすかめアドベントカレンダーの一日目です。

○○を完全にマスターしたという言葉はチョットデキルに対抗してさすかめ先生が提案した概念です。
各言語でhello worldが出力できた状態を示します。

ということで、Go言語を完全にマスターしました。

package main

import "fmt"

func main() {
	fmt.Printf("hello, world\n")
}

実行は

C:\gowork\src\hello>go run hello.go
hello, world

ということで、完全にマスターできました。

2016-09-21

JDBCでOracleのchar型のカラムを検索する。

char型は桁数に足りない場合、スペースで勝手に埋められます。*1

検索する場合は、空白スペースを埋めた状態で検索してあげる必要があります。

ここで、OracleJDBCドライバに入っているPreparedStatementの実装、OraclePreparedStatementのメソッドsetFixedCharを使用することで、空白で埋めるのを適切に行ってくれます。

Oracleの公式ドキュメントを見るとPreparedStatementをOraclePreparedStatementにキャストして使用してくださいと記載してあります。


データベース内のCHARデータは、列幅まで埋め込まれます。このため、SELECT文のWHERE句に文字データをバインドするためのsetCHARメソッドの使用に関して、制限が生じます。WHERE句の文字データも、SELECT文で合致させるために、列幅まで埋め込む必要があります。これは特に列幅がわからない場合に問題になります。

これを修正するために、OracleはOraclePreparedStatementクラスにsetFixedCHARメソッドを追加しました。このメソッドは埋込みなしの比較を実行します。

注意:
setFixedCHARメソッドを使用するには、必ずプリコンパイルされたSQLオブジェクトをOraclePreparedStatementにキャストしてください。
INSERT文で、setFixedCHARを使用する必要はありません。データベースは挿入時に、常にそのデータを列幅まで自動的に埋め込みます。


https://docs.oracle.com/cd/E16338_01/java.112/b56281/datacc.htm#BABCHGCH

これには問題があります。
それはPreparedStatementが何かの理由でProxyされている場合にOraclePreparedStatementにキャストが出来ないことがあることです。
アプリケーションサーバー組込のDataSource実装を使用しているとよくあります。

そこで次のようなコードを考えます。

OraclePreparedStatement orapstmt = (OraclePreparedStatement)pstmt.unwrap(PreparedStatement.class);

unwrapはJava 6から増えたメソッドJavaDocには次のような記載があります。

標準以外のメソッド、またはプロキシによって公開されない標準メソッドにアクセスできるようにするために、指定されたインタフェースを実装しているオブジェクトを返します。レシーバがこのインタフェースを実装している場合、結果はレシーバ、またはレシーバのプロキシになります。レシーバがラッパーであり、ラップされたオブジェクトインタフェースを実装している場合、結果はラップされたオブジェクト、またはそのプロキシです。それ以外の場合は、ラップされたオブジェクト、またはその結果のプロキシに対してunwrapを再帰的に呼び出した結果が返されます。レシーバがインタフェースを実装しておらず、ラッパーでもない場合は、SQLExceptionがスローされます。

https://docs.oracle.com/javase/jp/8/docs/api/java/sql/Wrapper.html


これでOKかと思ったら、別の問題がありました。
それは、Wrapper自体がJava 6から実装されたもので、古くからあるDataSourceでは正しく実装されてない場合があるということです。(例えばうぇぶなんとかというアプリケーションサーバーのDataSource実装とか)
そもそも、何らかの理由でラップされているものを直接使ってしまってよいのかという問題もあります。

さて、どうするか

PreparedStatementには値として直接文字列をバインドするsetStringメソッド以外にsetObjectメソッドがあります。
そのsetObjectメソッドは第三引数java.sql.Typesで定義された値を渡すことで、型が何かを指定することができます。
例えば、setStringと同じ挙動をさせるためには、java.sql.Typesで定義されたVARCHARもしくはNVARCHARあたりを渡してあげればよいです。

これでsetFixedCharの挙動をさせたい場合は、999を渡してあげます。

pstmt.setObject(index, "string", 999);

しかしながら、この999自体はjava.sql.Typesには定義されていません。

定数フィールド値一覧
https://docs.oracle.com/javase/jp/8/docs/api/constant-values.html#java.sql.Types.ARRAY


どこで定義されていると、Oracleドライバ内のoracle.jdbc.OracleTypesで定義されています。
https://docs.oracle.com/cd/E16338_01/appdev.112/e13995/oracle/jdbc/OracleTypes.html#FIXED_CHAR

Use this type when binding to a CHAR column in the where clause of a Select statement. A non padded comparision will be done unlike in CHAR and VARCHAR case. Not particularly needed for an insert as the database will pad it. This type is used for bind only. It cannot be used for define and registerOutParameter.

ということで、これを使ってあげるのが一番確実のようです。
もちろんOracle以外では使えませんのでご注意を。

*1:例えば、char(8)で定義したカラムに"abc"を入れると"abc     "になる

2016-08-16

Class#forNameはファイルシステムが大文字小文字の区別をしない場合にNoClassDefFoundErrorを投げることがある

タイトルのとおり。
通常、Class#forNameでクラスが見つからない場合はClassNotFoundExceptionが発生するが、
Windows上では以下のJUnitのテストコードのような挙動を示す。

package test;

import org.junit.Test;

public class TestMain {

  @Test
  public void test1() throws ClassNotFoundException {
    Class.forName("test.TestMain");// このクラスを呼び出す
  }

  @Test
  public void test2() throws ClassNotFoundException {
    Class.forName("java.lang.Thread");// rt.jarに入っているクラスを呼び出す
  }

  @Test(expected = ClassNotFoundException.class)
  public void test3() throws ClassNotFoundException {
    Class.forName("java.lang.thread");// rt.jarに入ってるクラスの大文字小文字の間違い
  }

  @Test(expected = NoClassDefFoundError.class)
  public void test4() throws ClassNotFoundException {
    Class.forName("test.testmain");// このクラスの大文字小文字の間違い
  }

}

まーじーかー・・・・・・
JVMバグじゃないの?

Java 1.8.0_92