【Java】【トラブル】Java で初回の暗号化/複合化処理に時間が掛かる

■ 現象内容

http://blogs.yahoo.co.jp/dk521123/34330480.html
http://blogs.yahoo.co.jp/dk521123/33640872.html
http://blogs.yahoo.co.jp/dk521123/36419973.html
http://blogs.yahoo.co.jp/dk521123/32780473.html
で、Javaの暗号化/複合化処理を実装したが、
以下の「■ サンプル」のように、処理時間を計測してみた。
結果は、以下「出力結果」になるのだが、初回にPCで大体1~1.5秒掛かる。
Raspberry PIスマホなどの非力なデバイス上で実行した場合、更に掛かってしまう。

■ サンプル

以下の関連記事のサンプルを参考に時間計測のログを張ってみる
http://blogs.yahoo.co.jp/dk521123/34330480.html
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;

public class Main {

  public static void main(String[] args) {
    try {
      String key = "1234567890123456";
      String original = "This is a source of string!!";
      String algorithm = "AES";

      System.out.println("[1]");
      long start = System.currentTimeMillis();
      String encrypedResult = CipherHelper.encrypt(original, key, algorithm);
      System.out.println("Time:" + (System.currentTimeMillis() - start) + "[ms]");
      System.out.println("暗号化文字列:" + encrypedResult);

      System.out.println("[2]");
      start = System.currentTimeMillis();
      String decryptedResult = CipherHelper.decrypt(encrypedResult, key, algorithm);
      System.out.println("Time:" + (System.currentTimeMillis() - start) + "[ms]");
      System.out.println("復号化文字列:" + decryptedResult);

      System.out.println("[3]");
      start = System.currentTimeMillis();
      String encrypedResult2 = CipherHelper.encrypt(original, key, algorithm);
      System.out.println("Time:" + (System.currentTimeMillis() - start) + "[ms]");
      System.out.println("暗号化文字列:" + encrypedResult2);

      System.out.println("[4]");
      start = System.currentTimeMillis();
      String decryptedResult2 = CipherHelper.decrypt(encrypedResult, key, algorithm);
      System.out.println("Time:" + (System.currentTimeMillis() - start) + "[ms]");
      System.out.println("復号化文字列:" + decryptedResult2);
    } catch (Exception ex) {
      ex.printStackTrace();
    }
  }

  public static class CipherHelper {
    /**
     * 文字列を16文字の秘密鍵でAES暗号化してBase64した文字列で返す
     */
    public static String encrypt(String originalSource, String secretKey, String algorithm)
        throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException,
        BadPaddingException {

      byte[] originalBytes = originalSource.getBytes();
      byte[] encryptBytes = cipher(true, originalBytes, secretKey, algorithm);
      byte[] encryptBytesBase64 = Base64.getEncoder().encode(encryptBytes);
      return new String(encryptBytesBase64);
    }

    /**
     * Base64されたAES暗号化文字列を元の文字列に復元する
     */
    public static String decrypt(String encryptBytesBase64String, String secretKey, String algorithm)
        throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException,
        BadPaddingException {

      byte[] encryptBytes = Base64.getDecoder().decode(encryptBytesBase64String);
      byte[] originalBytes = cipher(false, encryptBytes, secretKey, algorithm);
      return new String(originalBytes);
    }

    /**
     * 暗号化/複合化の共通部分
     */
    private static byte[] cipher(boolean isEncrypt, byte[] source, String secretKey, String algorithm)
        throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException,
        BadPaddingException {
      byte[] secretKeyBytes = secretKey.getBytes();

      SecretKeySpec secretKeySpec = new SecretKeySpec(secretKeyBytes, algorithm);
      Cipher cipher = Cipher.getInstance(algorithm);
      if (isEncrypt) {
        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
      } else {
        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
      }

      return cipher.doFinal(source);
    }
  }
}

出力結果

[1]
Time:1478[ms] <=★約1~1.5秒かかる★
暗号化文字列:B/UZj3IF6F6tObxiTAqdDsDLxUKGCBuAYjVTQP1GSnI=
[2]
Time:1[ms] <=☆1ミリ秒☆
復号化文字列:This is a source of string!!
[3]
Time:0[ms] <=☆1ミリ秒未満☆
暗号化文字列:B/UZj3IF6F6tObxiTAqdDsDLxUKGCBuAYjVTQP1GSnI=
[4]
Time:0[ms] <=☆1ミリ秒未満☆
復号化文字列:This is a source of string!!

■ 原因

暗号化・複合化で使用している「Cipher.getInstance()」の初回に時間がかかるため。

# cipher.init()も若干時間がかかっている
計測のため、以下のようにログを打ち込む
・・・
long start = System.currentTimeMillis();
byte[] secretKeyBytes = secretKey.getBytes();
System.out.println("Time for secretKey.getBytes():" + (System.currentTimeMillis() - start) + "[ms]");

start = System.currentTimeMillis();
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKeyBytes, algorithm);
System.out.println("Time for new SecretKeySpec():" + (System.currentTimeMillis() - start) + "[ms]");

start = System.currentTimeMillis();
Cipher cipher = Cipher.getInstance(algorithm);
System.out.println("Time for Cipher.getInstance():" + (System.currentTimeMillis() - start) + "[ms]");

start = System.currentTimeMillis();
if (isEncrypt) {
  cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
} else {
  cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
}
System.out.println("Time for Cipher.init():" + (System.currentTimeMillis() - start) + "[ms]");
・・・
出力結果
[1]
Time for Base64.getEncoder().encode:1[ms]
Time for secretKey.getBytes():0[ms]
Time for new SecretKeySpec():3[ms]
Time for Cipher.getInstance():870[ms] <=!!注目!! ★ここに時間が掛かっている★
Time for Cipher.init():78[ms] <=★ここも多少時間が掛かっている★
Time for Cipher.doFinal():1[ms]
Time for Base64.getEncoder().encode:0[ms]
Time:958[ms]
暗号化文字列:B/UZj3IF6F6tObxiTAqdDsDLxUKGCBuAYjVTQP1GSnI=
[2]
Time for Base64.getDecoder().decode:0[ms]
Time for secretKey.getBytes():0[ms]
Time for new SecretKeySpec():0[ms]
Time for Cipher.getInstance():0[ms]
Time for Cipher.init():0[ms]
Time for Cipher.doFinal():0[ms]
Time:1[ms]
復号化文字列:This is a source of string!!
[3]
Time for Base64.getEncoder().encode:0[ms]
Time for secretKey.getBytes():0[ms]
Time for new SecretKeySpec():0[ms]
Time for Cipher.getInstance():0[ms]
Time for Cipher.init():1[ms]
Time for Cipher.doFinal():0[ms]
Time for Base64.getEncoder().encode:0[ms]
Time:1[ms]
暗号化文字列:B/UZj3IF6F6tObxiTAqdDsDLxUKGCBuAYjVTQP1GSnI=
[4]
Time for Base64.getDecoder().decode:0[ms]
Time for secretKey.getBytes():0[ms]
Time for new SecretKeySpec():0[ms]
Time for Cipher.getInstance():0[ms]
Time for Cipher.init():0[ms]
Time for Cipher.doFinal():0[ms]
Time:1[ms]
復号化文字列:This is a source of string!!

■ 対決案

 * システム起動時などにおいて、
   前もって(ダミー文字列でいいから)暗号化又は複合化処理をしておく

関連記事

暗号化/複合化する ~Apache Commonsを使用した場合~

http://blogs.yahoo.co.jp/dk521123/32780473.html

Java で暗号化/複合化する ~Java1.8 標準を使用した場合~

http://blogs.yahoo.co.jp/dk521123/34330480.html

Java で暗号化/複合化する ~Java1.8 標準を使用した場合 / IV使用編~

http://blogs.yahoo.co.jp/dk521123/36419973.html

Java で暗号化/複合化する ~標準ライブラリを使用した場合~

http://blogs.yahoo.co.jp/dk521123/33640872.html