unhurried

コンピュータ関連ネタがほとんど、ときどき趣味も…

Spring Bootの依存性注入(DI)設定方法

Spring Frameworkの依存性注入(Dependency Injection)を設定する方法をまとめました。

コンポーネントのスキャン対象指定

デフォルトでは @ComponentScan が付与されたクラスのパッケージと同階層以下のパッケージをスキャンする。その他のクラスをスキャン対象にしたい場合はscanBasePackageにパッケージ名を指定する。

// @SpringBootApplicationなどの複合アノテーションに指定することもできる。
@ComponentScan(scanBasePackages={"com.example"})

依存性注入(DI)設定

Springアノテーションを使う場合

依存するオブジェクトを private final なメンバとして定義して、コンストラクタで依存オブジェクトを引数で受け取るコンストラクタを定義する。Lombokを使う場合は下記のように記載できる。

コンポーネント定義
@Component
class ComponentA { ...
DI設定
@RequiredArgsConstructor
class ClassA {
    // @Autowiredは省略できる。
    private final ComponentA componentA;
    ...

以前はfinalでないメンバもしくはそのSetterに@Autowiredを指定するのが主流であった。

class ClassA {
    @Autowired
    private ComponentA componentA;
    ...
class ClassA {
    private ComponentA componentA;
    @Autowired
    public void setComponentA(ComponentA componenA) {
        ...

コンストラクタを利用することには以下のメリットがあるため、現在はこちらが推奨されている。

  • DIなしでも利用可能なモジュールになる。(利用側でコンストラクタで依存関係を設定すればよい。)
  • final 修飾子を付けられる。(DIオブジェクトを格納する変数を書き換えることは通常ない。)

JSR-330アノテーションを使う場合

Java標準なので他のDIコンテナに乗り換えやすい。

コンポーネント定義
@Named // もしくは@ManagedBean
class ComponentA { ... }
DI設定

コンストラクタ(もしくはSetter)に@Injectを指定する。(Setterでも可。)

@RequiredArgsConstructor(onConstructor=@__(@Inject))
class ClassA {
    private final ComponentA componentA;
    ...

もしくは、メンバに@Injectを指定する。

class ClassA {
    @Inject
    private ComponentA componentA;
    ...

参考

Javaカバレッジ計測ライブラリ

Javaのテストカバレッジを計測できるオープンソースライブラリの現状を調査してみました。

要約

Coberturaは開発が停止しているため、今後はJaCoCoもしくはCloverが主流となりそう。ただしCloverは最近オープンソース化されたため、今後の継続開発(=コミュニティの活発度)はあまり見えていない。

主要な3ライブラリの概要

JaCoCo

  • 最新版は2018年1月リリースのバージョン0.8.0(2018年3月時点)。
  • 実行時に計測(on-the-fly instrumentation)できないクラスに対して、クラスファイルに計測処理を事前に追加する(offline instrumentation)ことができる。

Cobertura

  • 開発は2015年2月で止まっている。
  • Java 8に未対応でLambda式などの記法があるとその部分の計測ができない。
  • 他のライブラリでは対応していない複数のモジュールから構成されるプロジェクトに対する計測レポートの集約に対応している。

Clover

その他のライブラリなど

EclEmma

JMockit

参考

Spring Boot DBコネクションプール

Spring Boot(Spring Data JPA)でコネクションプールを設定する方法をまとめました。

ライブラリの自動選択

コネクションプールライブラリは以下の順序で選択される。(クラスパスにライブラリがあるか確認し、あればそのライブラリを選択し、なければ次のライブラリを確認する。)

  1. HikariCP
  2. Tomcat JDBC Connection Pool
  3. Commons DBCP2

spring-boot-starter-jdbc もしくは spring-boot-starter-data-jpa を利用している場合は、HikariCPが依存関係として解決されるため、何も設定していなければHikariCPが選択される。

ライブラリの指定方法

application.properties(もしくはapplications.yaml)の spring.datasource.type プロパティに利用したいライブラリのDataSource( javax.sql.DataSource 継承クラス)を指定することで変更ができる。

HikariCPの場合

設定可能な項目はConfigurationを参照。

spring:
  datasource:
    driverClassName: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost/request_log
    username: user
    password: password
    type: com.zaxxer.hikari.HikariDataSource
    hikari:
      maximum-pool-size: 20
      minimum-idle: 10
Tomcat JDBC Connection Poolの場合

設定可能な項目はCommon Attributesを参照。

spring:
  datasource:
    driverClassName: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost/request_log
    username: user
    password: password
    type: org.apache.tomcat.jdbc.pool.DataSource
    tomcat:
      max-active: 20
      min-idle: 10

参考

Tomcatのlogging.properties設定方法

Tomcat 7/8のログ出力設定ファイル(logging.properties)の定義方法をまとめています。

Tomcatjava.util.logging API実装(JULI)での設定方法拡張(一部抜粋)

  • .handlersプロパティを設定することでルートLoggerのHandlerが定義できる。
  • Handler名にプレフィックスを付けることで同じHandlerクラスを複数回利用できる。
    • プレフィックスは数字で始まり.で終わること。
  • あるLoggerにHandlerが設定されている場合は親のLoggerの処理は実施されない(設定で変更可能)。

デフォルトのlogging.properties設定内容

# 利用するHandlerを指定する。この設定方法はjava.util.loggingと同様。
# org.apache.juli.FileHandlerクラスを"1catalina"、"1localhost"、"3manager"というプレフィックスをつけることで3つのHandlerとして利用している。
handlers = 1catalina.org.apache.juli.FileHandler, \
           2localhost.org.apache.juli.FileHandler, \
           3manager.org.apache.juli.FileHandler, \
           java.util.logging.ConsoleHandler

# ルートLoggerが利用するHandlerを指定する。
# Loggerに個別にHandlerを指定していない場合は、ルートLoggerのHandlerに渡される。
.handlers = 1catalina.org.apache.juli.FileHandler, java.util.logging.ConsoleHandler

# 各Handlerに対して設定値を指定する。
# level:出力するログレベル
# directory:ログファイルを出力するディレクトリ(FileHandler)
# prefix:ログファイルのファイル名のプレフィックス(FileHandler)
# bufferSize:ログ出力処理のバッファサイズ(FileHandler)
# formatter:ログの整形を行うクラス

1catalina.org.apache.juli.FileHandler.level = FINE
1catalina.org.apache.juli.FileHandler.directory = ${catalina.base}/logs
1catalina.org.apache.juli.FileHandler.prefix = catalina.

2localhost.org.apache.juli.FileHandler.level = FINE
2localhost.org.apache.juli.FileHandler.directory = ${catalina.base}/logs
2localhost.org.apache.juli.FileHandler.prefix = localhost.

3manager.org.apache.juli.FileHandler.level = FINE
3manager.org.apache.juli.FileHandler.directory = ${catalina.base}/logs
3manager.org.apache.juli.FileHandler.prefix = manager.
3manager.org.apache.juli.FileHandler.bufferSize = 16384

java.util.logging.ConsoleHandler.level = FINE
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter

# 各Loggerに対して設定値を指定する。
# level:記録対象となるログのログレベル
# handlers:Loggerが利用するHandler

org.apache.catalina.core.ContainerBase.[Catalina].[localhost].level = INFO
org.apache.catalina.core.ContainerBase.[Catalina].[localhost].handlers = \
   2localhost.org.apache.juli.FileHandler

org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/manager].level = INFO
org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/manager].handlers = \
   3manager.org.apache.juli.FileHandler

参考

MySQL 存在しないユーザー削除時のエラー回避方法

MySQLで存在しないユーザーを削除したときにエラーになるのを回避する方法を調べました。

MySQL 5.7.6以降

MySQL 5.7.6以降ではDROP USER構文でIF EXISTSオプションが利用できる。

DROP USER IF EXISTS user

MySQL 5.7.5以前

IF EXISTSオプションが使えないため、DROP USER構文で存在しないユーザーを削除しようとするとエラーになる。これを回避するために、GRANT構文でユーザー権限を権限なし(USAGE)に変更(ユーザーが存在しない場合には作成される)してからDROP USERを実行する。

GRANT USAGE ON *.* TO user IDENTIFIED BY password;
DROP USER user;

※ このSQLMySQL 5.7.6以降では以下の警告が出る。

1287 Using GRANT statement to modify existing user's properties other than privileges is deprecated and will be removed in future release. Use ALTER USER statement for this operation.
参考

BigIntegerでバイナリ値のビット演算を行う

Javaにはバイナリ値を扱う便利な機能があまり用意されていないないため、バイナリ値が格納されたbyte配列に対してビット演算を行うには、自力でビット(配列)間の値引き渡し処理を実装する必要があります。

よく使われる方法には、byte配列をint型やlong型などの整数型に一度格納してからビット演算を行い、再度byte配列に戻すというものがありますが、この方法では最大でlong型の8byteまでしか扱えません。

BigIntegerクラスを使うことで任意のサイズのbyte配列を扱えますが、データ変換に少し工夫が必要となりますので、ポイントをまとめてみました。

BigIntegerでbyte配列のビット演算を行う方法

BigIntegerを使ったbyte配列のビット演算は以下の手順でできる。

  1. byte配列を正数としてBigIntegerに読み込む。
    • BigInteger(byte[] val)コンストラクタで読み込んだ後に、符号ビットをマスク処理で0に変える。
  2. BigIntegerインスタンスのメソッドを使って各種演算を行う。
    • インクリメント・ディクリメント:addメソッド、subtractメソッド
    • 論理演算:andメソッド、orメソッドなど
    • シフト演算:shiftRightメソッド、shiftLeftメソッド ※ これらは算術シフトであるが、論理シフトと同じ結果になる。(ポイント1参照)
    • 各演算の後には毎回マスク処理を行い、BigIntegerの値の正数化と不要な上位バイトの各ビットの0への変換を行う。
  3. BigIntegerをbyte配列に再び変換する。
    • toByteArrayメソッドでbyte配列に変換し、バイト数が不足する場合は補完処理、バイト数が多い場合は削減処理を行う。

実装例

ポイント1:BigIntegerを常に正数の状態に保つ

BigIntegerの値が負になると、上手くいかないケースがある。

論理シフトの算術シフトでの代用

BigIntegerには算術シフトしか実装されていない。(そもそもBigIntegerはバイト列を意識することなく数値を扱うためのクラスなので、論理シフトが意味を持たないため。)このため、算術シフトを論理シフトで代用する必要があるが、負数に対する右シフトは算術シフトと論理シフトで結果が異なる。

1010 1010 -- 1bit論理右シフト --> 0101 0101
1010 1010 -- 1bit算術右シフト --> 1101 0101 # 論理シフトと結果が異なる。

そこで、BigIntegerの値が常に正数となるように変換する必要がある。この変換処理は最初のbyte配列読み込みと各演算の後に毎回行う必要がある。

1010 1010
  -- 正数としてBigIntegerに変換 --> 0000 1010 1010
  -- 1bit算術右シフト --> 0000 0101 0101 # 論理シフトと同じ結果になる。

BigIntegerを正数にする方法には、符号ビットを指定してbyte配列を読み込む方法と、マスク処理で符号ビットを1に変更する2通りあるが、後述のポイント2の目的でも利用できるマスク処理をする方法が簡単である。

// 入力バイト配列: c0 00
byte[] input = new Bytes(new byte[] {(byte) 0xc0, (byte) 0x00}).toByteArray();

// 正数(符号ビットを0)として読み込む方法
// 00 c0 00
new BigInteger(1, input).toByteArray();

// 符号ビットをマスク処理で0に変える方法
// ff c0 00 (入力) AND 00 ff ff (マスク) => 00 c0 00
// ※ バイト配列にしたときにサイズの異なるBigIntegerのAND演算では、
//    サイズの小さい方がサイズの大きい方に合うように拡張される。
BigInteger mask = new BigInteger(1, new Bytes(new byte[] {(byte) 0xff, (byte) 0xff}).toByteArray());
new BigInteger(input).and(mask).toByteArray();

ポイント2:余分な上位バイトの各ビットを0に保つ

BigIntegerインスタンスに対してインクリメント・ディクリメントをしたときに元のバイト数で表現できる範囲を超えてしまった場合や、左シフト演算を行った場合に、本来のバイト数よりも上位のバイトにデータが残ってしまうことがある。

この余分な上位バイトのデータは以降の処理でシフト演算をするときに影響を与えてしまうので、各演算の後に各ビットを0にクリアする必要がある。これは、ポイント1で述べたマスク処理で同時に行える。

// 入力バイト配列: c0 00
byte[] input = new Bytes(new byte[] {(byte) 0xc0, (byte) 0x00}).toByteArray();
BigInteger mask = new BigInteger(1, new Bytes(new byte[] {(byte) 0xff, (byte) 0xff}).toByteArray());
// 1bit左シフト、1bit右シフトの順でシフト演算する。
// 本来は最上位1bitが捨てられて 80 00 となるが、元の c0 00 に戻ってしまう。
new BigInteger(input).and(mask).shiftLeft(1).shiftRight(1);

ポイント3:BigIntegerをbyte配列に戻すときにバイト数を調整する

toByteArrayメソッドはBigIntegerに格納されている数字の表現に必要な最小の要素数のbyte配列を返却するため、元のbyte配列よりもサイズの小さなbyte配列となることがある。

// 入力バイト配列: 00 7f
byte[] input = new Bytes(new byte[] {(byte) 0x00, (byte) 0x7f}).toByteArray();
// 出力バイト配列: 7f
// BigIntegerに変換すると1バイトになる。(数字としては127で1バイトで表現できるため。)
byte[] output = new BigInteger(input).toByteArray();

また、各演算により元のbyte配列よりもサイズの大きなbyte配列になることもある。

// 入力バイト配列: 40 00
byte[] input = new Bytes(new byte[] {(byte) 0x40, (byte) 0x00}).toByteArray();
// 1bit左算術シフト後の出力バイト配列: 00 80 00
byte[] output = new BigInteger(input).shiftLeft(2).toByteArray();

このため、byte配列に戻すときには元のバイト数と一致するように調整する必要がある。

// 変換対象のBigIntegerインスタンス
BigInteger bi;
// 変換するバイト配列のサイズ
int byteSize;

byte[] ba = bi.toByteArray();
ByteBuffer bb = ByteBuffer.allocate(byteSize);
// 余分な上位バイトを取り除く。
if (ba.length >= byteSize) {
  bb.put(ba, ba.length-byteSize, byteSize);
// 不足するbyte数分ByteBufferを先頭から埋める。
} else {
  int byteSizeToFill = byteSize - ba.length;
  for(int i=0; i<byteSizeToFill; i++) {
    bb.put((byte) 0);
  }
  bb.put(ba);
}

// 変換後のバイト配列
byte[] output = bb.array();

厳密にはBigIntegerが負数の場合は、不足バイトを0ではなく1で埋める必要があるが、ポイント1の対応でBigIntegerの値は常に正数になるように調整しているため、負数の場合を考慮する必要はない。

// 入力バイト配列: ff ff
byte[] input = new Bytes(new byte[] {(byte) 0xff, (byte) 0xff}).toByteArray();
// 出力バイト配列: ff
// 負数の場合には不足する上位ビットを1で埋める必要がある。
byte[] output = new BigInteger(input).toByteArray();

シェルスクリプトでの正規表現を使ったバリデーション

exprコマンドを使う方法

target='aa bbcc'

# 正規表現をクォートする。
# 変数展開時はワード分割を避けるためにクォートする。
if expr "${target}" : "^a\+ [b-c]\{4\}$" > /dev/null; then
    echo "match"
fi

# 正規表現にマッチしない条件分岐をするには!を使う。
if ! expr "${target}" : "^a\+ [b-c]\{3\}$" > /dev/null; then
    echo "mismatch"
fi

[[ ]] 構文を使う方法(bashzshなど)

target='aa bbcc'

# bash: 正規表現をクォートしない。
# zsh: 正規表現をクォートする。
# 変数展開時のクォートは不要。
if [[ ${target} =~ ^a+' '[b-c]{4}$ ]]; then
    echo "match"
fi

# 正規表現にマッチしない条件分岐をするには!を使う。
if [[ ! ${target} =~ ^a+' '[b-c]{4}$ ]]; then
    echo "mismatch"
fi

参考