Quantcast
Channel: システム開発メモ
Viewing all 100 articles
Browse latest View live

ローカルイントラネット内でのSeleniumとIE11とwindow.openの相性の悪さ

$
0
0

前提:Internet Explorer11, Windows7, Selenium WebDriver2.53.0
注意:ローカルイントラネットの話なので、会社の環境によって以下の内容が常に真になるか不明。

ローカルイントラネットでInternet Explorer11を使うという日本の業務システムにありがちな構成でSelenium WebDriverを動かしたとき、window.openで開いた子画面がSeleniumで取得できないという問題にあたった。
window.openで開く箇所は以下のようになっている。

<script>
var childWindowProp = {
	   width : 805
	   ,height : 580
	   ,createOptions : function() {
	       return "scrollbars=yes,resizable=no,width=" + this.width + ",height=" + this.height + ",top=" + ((screen.height - this.height) / 2) + ",left=" + ((screen.width - this.width) / 2);
	   }
   };
</script>
<a href="javascript:void(0)" onclick="window.open('/child_window_url', 'child_window_name', childWindowProp.createOptions());return false;">子画面を開く</a>

Selenium WebDriverで子画面を取得する箇所は以下のようになっている。

public class ChildWindow {

	protected WebDriver driver;
	private String parentWindowId;
	private String childWindowId;

	public ChildWindow(WebDriver driver) {
		this.driver = driver;
		this.parentWindowId = driver.getWindowHandle();

		int maxRetry = 5;
		while (childWindowId == null && maxRetry > 0) {
			maxRetry--;
			try {
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
			}
			for (String windowId : driver.getWindowHandles()) {
				if (!windowId.equals(parentWindowId)) {
					this.childWindowId = windowId;
				}
			}
		}
		driver.switchTo().window(childWindowId);
	}
}

同Webシステムをインターネット経由で利用したときには

driver.getWindowHandles()
で親画面と子画面の二つのwindowHanleが必ず取得できたが、イントラネット経由で利用したときには親画面のwindowHandleしか取得できなかった。

インターネットとイントラネットでのIE11の設定の違いが原因だろうと当たりをつけて、「インターネットオプション」の「セキュリティ」タブでインターネットとイントラネットの違いを確認した。
「このゾーンのセキュリティのレベル」欄の「保護モードを有効にする」項目でインターネットの場合はチェックがついているが、イントラネットの場合はチェックがついていなかったため、イントラネットでもチェックをつけてからSeleniumを実行した。
結果はイントラネットでもうまく動くようになり成功だったが、「保護モードを有効にする」の意味を深く理解できていないので根本原因は分からなかった。


MavenからJUnitを実行すると文字化けが発生する件の対応方法

$
0
0

SpringBootで作成したWebアプリのIntegration Test目的でSelenium WebDriverを組み込んだJUnitをMavenから実行したとき、初期データ構築のために発行されたINSERT文のSQLが、SQLファイルをUTF-8で保存しているにもかかわらずMS932として解釈されて実行されていた。
※ちなみにこのSQLファイルは、SpringBootが起動時に実行してくれるdata.sql。

このせいでWeb画面の検索フォームで日本語を入力してDBを検索するというテストでは、アプリケーションに渡る文字は正しい日本語なのに対してDBに保存されている日本語が文字化けしているため、必ず失敗するようになってしまった。
このテストはEclipseの「実行」→「JUnitテスト」から起動するときは問題なく成功する。

環境は以下の通り。
Windows7, Java8, JUnit4, Maven3.3.3, SpringBoot1.3.3

問題の根本原因は、Windows7でJavaを実行したときのデフォルトのfile.encodingはMS932なので、UTF-8の日本語がうまく解釈できていないことにある。

System.out.println(System.getProperty("file.encoding"));
// MS932

WindowsでMavenを動かすときの文字エンコード絡みの注意点としては、Web上に情報が出回っている通り、pom.xmlでpropertiesのproject.build.sourceEncodingに設定をしてあげれば良い。

<properties>
	<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	<java.version>1.8</java.version>
</properties>

しかし今回の問題はproject.build.sourceEncodingを設定しても解決しない。
JUnitがMavenの実行プロセスとは別で起動されるということが原因で起こるからだ。

project.build.sourceEncodingとは別に、maven-surefire-pluginで文字エンコードの設定をする必要がある。maven-surefire-pluginはUnit Testにおいて使われるプラグインで、

${basedir}/target/surefire-reports
にレポートを出力するのが主な役割になる。
今回の設定で見るべきは、JVM引数を渡している
<argLine>-Dfile.encoding=UTF-8</argLine>
になる。
<build>
	<plugins>
		<plugin>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-maven-plugin</artifactId>
			<configuration>
				<executable>true</executable>
			</configuration>
		</plugin>
		<plugin>
			<groupId>org.apache.maven.plugins</groupId>
			<artifactId>maven-surefire-plugin</artifactId>
			<configuration>
				<junitArtifactName>junit:junit</junitArtifactName>
				<encoding>UTF-8</encoding>
				<inputEncoding>UTF-8</inputEncoding>
				<outputEncoding>UTF-8</outputEncoding>
				<argLine>-Dfile.encoding=UTF-8</argLine>
				<skipTests>true</skipTests>
			</configuration>
		</plugin>
	</plugins>
</build>

【余談】
この解決方法は、日本人でも中国人でもなくイタリア人のサイト(https://carlobertoldi.wordpress.com/2012/03/12/maven-unit-tests-and-those-funny-characters/)で見つけた。

Java8のdefaultメソッドでthisを使う、そして例外へ応用する

$
0
0

Java8からdefaultメソッドを使用することで、interfaceに実装が持てるようになった。
defaultメソッド内で

this
を使うとthisは個別実装クラスを指すことになる。
public interface SampleInterface {
	default void sampleDefault() {
		System.out.println(this.getClass().getSimpleName());
	}
}

public class SampleImpl implements SampleInterface {
	public void original() {
	}
}

public class Caller {
	public void call() {
		SampleImpl impl = new SampleImpl();
		impl.sampleDefault();
		// SampleImplと出力される。
	}
}

これは例外のStackTraceを補完する情報として利用できる。
sampleDefault()で例外を投げたとすると、StackTraceには次のようになる。

Exception
at SampleInterface.sampleDefault(SampleInterface.java:行数)
at Caller.call(Caller.java:行数)

この情報だけではSampleImplが実装クラスであるとわからず、Caller.javaの該当行数を見ないといけない。

sampleDefaultの中でthisを使い、Exceptionの引数messageに入れてあげるとすると、StackTraceにSampleImplの情報を表示することができる。

LinuxとWindowsバッチでワイルドカードで指定したファイル名を変数に入れる方法の違い

$
0
0

Linuxのシェルならワイルドカードで指定したファイル名を変数に入れるのは、コマンド置換を使えば簡単だ。

// 現在のディレクトリで*.logに一致するファイルのうち最後1件のファイル名を変数filenameにセットする
filename=`find . -maxdepth 1 -name "*.log" -printf '%f\n' | tail -1`

これをWindowsバッチで行う方法はFORコマンドを使用する。
※FORコマンド参考:バッチとPowerShellで昨日の日付を取得する

FOR /F %%a in ('dir /B *.log') DO SET filename=%%a

dir /B
でdirコマンドの実行結果をファイル名の表示だけにできる。またFORコマンドでループしているため、変数filenameには最後に代入されたファイル名が入る。

今の例ではファイル名のうち一番最後のファイル名を変数にセットするというものだったが、ワイルドカードで一致する全ファイルを対象に処理をしたいのであれば、DOの後を()でくくってあげれば良い。

FOR /F %%a in ('dir /B *.log') DO (
	SET filename=%%a
	echo %filename%
	echo %filename%
)

Linuxで全ファイルを対象に処理をするには、

xargs -I {} -sh "コマンド群"
を使う。
find . -maxdepth 1 -name "*.log" -printf '%f\n' | xargs -I {} -sh "echo {}; echo {}"

Windows batファイルでゼロパディング

$
0
0

Linux bashであればprintfを使用した書式設定で済むゼロパディング。
これをWindows batファイルで実現する方法の考え方は、元の数字の頭に最大桁数分の0を付けて、必要な桁数分だけ右側から抜き出すというもの。

: for文の中で!変数!で変数参照できるようにする。
setlocal enabledelayedexpansion

: 0がpad個ある文字列pad_numを作る。
set pad=3
set pad_num=
for /L %%i in (1, 1, %pad%) do (
	set pad_num=0!pad_num!
)
echo %pad_num%
: pad_numと数字を文字結合して、右からpad桁分だけ抜き出す。
set start=1
set end=10
for /L %%i in (%start%, 1, %end%) do (
	set num=!pad_num!%%i
	set num=!num:~-%pad%!

	echo !num! is zero padded. original number is %%i
)

Spring BootのFully Executable Jarをサーバにリリースする方法

$
0
0

Spring Bootはバージョン1.3.0以降、Executable JarとしてJarファイルに固めるだけでなく、実行スクリプトがJarファイル内に埋め込まれたFully Executable Jarが作成できる。
サーバに常駐させるため、普通のJarファイルであれば起動スクリプトを書く必要があるが、Fully Executable Jarであれば不要となる。
通常のJarファイルの起動スクリプトが公式に提供されていない状況だが、Fully Executable Jarの登場によって、実質的に起動スクリプトが公式に提供されたことになる。
これによって、簡単にserviceコマンドでアプリをdeamon起動することができるようになった。

ここでは、ビルド方法と次の環境にリリースする場合の構築手順と解説を記載する。
※基本はhttp://docs.spring.io/spring-boot/docs/1.3.5.RELEASE/reference/htmlsingle/#deployment-installに書いてある通り。

目次

環境
ビルド方法
構築手順
解説

環境

CentOS 6.8
Java8
Spring Boot 1.3.5
アプリ名:myapp.jar
アプリ用ユーザ名:myappusr

ビルド方法

pom.xmlにspring-boot-maven-pluginというbuildプラグインを追加し、executableをtrueにする。
falseを設定するとExecutable Jarファイルを作成し、trueを設定するとFully Executable Jarファイルを作成してくれる。

<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<executable>true</executable>
				</configuration>
			</plugin>
		</plugins>
	</build>

構築手順

[root@host ~]# #アプリ用に任意のディレクトリを作成する。jarにworking directoryとして利用される。
[root@host ~]# mkdir /usr/local/bin/myapp
[root@host ~]# chown myappusr.myappusr /usr/local/bin/myapp
[root@host ~]#
[root@host ~]# #アプリを配置する。
[root@host ~]# cp /tmp/myapp.jar /usr/local/bin/myapp/
[root@host ~]# #myapp.jarの所有者がmyapp.jarの実行ユーザになるので、rootではなくアプリ用ユーザにする。※1
[root@host ~]# chown myappusr.myappusr /usr/local/bin/myapp/myapp.jar
[root@host ~]# #Fully Executable Jarはスクリプトが内蔵されているため、実行可能ファイルである必要があり、最低限、権限xを所有者につける。
[root@host ~]# chmod 500 /usr/local/bin/myapp/myapp.jar
[root@host ~]#
[root@host ~]# #jarファイルと同名のconfファイルを作成し、環境変数やJVMに渡すパラメータを設定する。※2
[root@host ~]# echo -e 'export LANG="ja_JP.UTF-8"\nJAVA_OPTS="-Dspring.profiles.active=development"' > /usr/local/bin/myapp/myapp.conf
[root@host ~]# #confファイルの中身確認
[root@host ~]# cat /usr/local/bin/myapp/myapp.conf
export LANG="ja_JP.UTF-8"
JAVA_OPTS="-Dspring.profiles.active=development"
[root@host ~]#
[root@host ~]# #deamon起動のため/etc/init.d/に登録
[root@host ~]# ln -s /usr/local/bin/myapp/myapp.jar /etc/init.d/myapp
[root@host ~]# #自動起動のためchkconfigに登録
[root@host ~]# chkconfig --add myapp
[root@host ~]# #起動
[root@host ~]# service myapp start
Started [3061]
[root@host ~]#
[root@host ~]# #起動確認
[root@host ~]# service myapp status
Running [3061]
[root@host ~]# #起動確認2
[root@host ~]# ps -ef | grep jar
myappusr  3061     1  3 11:58 ?        00:01:19 /usr/bin/java -Dsun.misc.URLClassPath.disableJarChecking=true -Dspring.profiles.active=development -jar /usr/local/bin/myapp/myapp.jar
[root@host ~]#
[root@host ~]# #ログ確認※3
[root@host ~]# ll /var/log/myapp.log
-rw-r--r--. 1 myappusr root 402  6月 23 12:36 2016 /var/log/myapp.log
[root@host ~]# #PID確認※4
[root@host ~]# ll /var/run/myapp/ -d
drwxr-xr-x. 2 myappusr root 4096  6月 23 12:36 2016 /var/run/myapp/
[root@host ~]# ll /var/run/myapp/myapp.pid
-rw-r--r--. 1 myappusr root 5  6月 23 12:36 2016 /var/run/myapp/myapp.pid

解説

※1)myapp.jarの所有者がmyapp.jarの実行ユーザになるので、rootではなくアプリ用ユーザにする。

http://docs.spring.io/spring-boot/docs/1.3.5.RELEASE/reference/htmlsingle/#deployment-initd-service-securingにセキュリティ面を考慮した設定方法が列挙されている。
次の二点は、構築手順で達成できている。
・myapp.jarの所有者をrootからmyappusrに変更する
・myapp.jarの権限を500に変更する
・myapp.confの所有者をrootにする(rootで構築しているため、自動で所有者がrootになっている)

しかし、それ以外は行っていないので、必要に応じてセキュリティを高めていく必要がある。
・myappusrのシェルを/usr/sbin/nologinにする
・jarファイルにchattr +i
・confファイルにchmod 400

※2)jarファイルと同名のconfファイルを作成し、環境変数やJVMに渡すパラメータを設定する。

jarファイルと同じディレクトリに同名のconfファイルを置くと、起動時に読み込んでくれる。
読み込む方法は、jarファイルの初めの部分がシェルスクリプトになっているので、jarファイルをlessして見るとわかる。

[myappusr@host myapp]$ less myapp.jar
#!/bin/bash
#
#    .   ____          _            __ _ _
#   /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
#  ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
#   \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
#    '  |____| .__|_| |_|_| |_\__, | / / / /
#   =========|_|==============|___/=/_/_/_/
#   :: Spring Boot Startup Script ::
#

中略

# Source any config file
configfile="$(basename "${jarfile%.*}.conf")"
# shellcheck source=/dev/null
[[ -r "${jarfolder}/${configfile}" ]] && source "${jarfolder}/${configfile}"

中略

# Build actual command to execute
command="$javaexe -Dsun.misc.URLClassPath.disableJarChecking=true $JAVA_OPTS -jar $jarfile $RUN_ARGS $*"

中略

    if [ $USE_START_STOP_DAEMON = true ] && type start-stop-daemon > /dev/null 2>&1; then
      arguments=(-Dsun.misc.URLClassPath.disableJarChecking=true $JAVA_OPTS -jar $jarfile $RUN_ARGS "$@")
      start-stop-daemon --start --quiet \
        --chuid "$run_user" \
        --name "$identity" \
        --make-pidfile --pidfile "$pid_file" \
        --background --no-close \
        --startas "$javaexe" \
        --chdir "$working_dir" \
        -- "${arguments[@]}" \
        >> "$log_file" 2>&1
      await_file "$pid_file"
    else
      su -s /bin/sh -c "$command >> \"$log_file\" 2>&1 & echo \$!" "$run_user" > "$pid_file"
    fi

source "${jarfolder}/${configfile}"
となっているため、環境変数を設定したければexportを、JVMに渡す引数を設定したければJAVA_OPTSに値を設定すればいい。

今回は以下の二つを設定した。

export LANG="ja_JP.UTF-8"
JAVA_OPTS="-Dspring.profiles.active=development"

JAVA_OPTS=”-Dspring.profiles.active=development”

Spring Bootの実行環境をJVM引数で渡すことができ、propertiesファイルの切り替え等ができるため、おそらくどのプロジェクトでも必要になる引数だろう。
開発環境であればdevelopment、ステージング環境であればstaging、本番環境であればproductionなどの引数を渡す。

export LANG=”ja_JP.UTF-8″

文字コードを設定した理由は、今回の環境がCentOSというところにある。
/etc/init.dにあるスクリプトを直接実行する場合と異なり、manによれば、serviceコマンドで実行するとプログラムを起動する際に環境変数LANGとTERM以外が引き継がれない。

ENVIRONMENT
LANG, TERM
The only environment variables passed to the init scripts.

しかし、CentOSではこの記述が当てはまらず、LANGも引き継がれない。
原因は/sbin/serviceスクリプトを見るとわかるが、env -iで環境変数をクリアしており、LANGを再設定してないから。
参考:http://heartbeats.jp/hbblog/2013/06/service-start-stop.html

LANG=”ja_JP.UTF-8″が設定されないと、Javaが日本語のファイルを扱う際に文字化けを起こしてしまうため、RedHatやFedoraも含めCentOS系のディストリを使用する場合は、confファイル内でLANG変数をexportしなければならない。
Ubuntu系を使用する場合でも、LANGの設定があることでバグを起こすわけではないので、一律この設定を入れてもいいかもしれない。

他にもconfファイルで設定できる項目はたくさんあるので、公式ドキュメントを確認してほしい。
http://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/html/deployment-install.html#deployment-script-customization

※3)ログ確認
※4)PID確認

ログファイルとPIDファイルは所定の場所に出力される。あらかじめPIDファイルが格納されるディレクトリを作る必要はない。
出力場所を変えたければconfファイルで設定ができる。

WindowsでAnsibleを実行する

$
0
0

AnsibleはLinux、Macでは動くがWindowsでは動かない。Windowsで実行するためには、仮想でLinuxを立ち上げて、そこにAnsibleをインストールする必要がある。

Windowsで『初めてのAnsible』の「1.9 テスト用サーバーのセットアップ」相当の内容を実行する方法を、次の構成で確かめていく。
・Windows 7
・VirtualBox 5.0.20
・Vagrant 1.8.4
・CentOS 6.7
・Ansible 2.1.0
初めてのAnsible 初めてのAnsible

目次

1.WindowsにVirtualBoxとVagrantをインストールする
2.VagrantでCentOSを2台起動する
3.Ansible用CentOSにAnsibleをインストールする
4.別のCentOSにAnsibleでアクセスする

1.WindowsにVirtualBoxとVagrantをインストールする

WindowsにVirtualBoxVagrantをインストールする。

2.VagrantでCentOSを2台起動する

コマンドプロンプトを開いて、VagrantにCentOSのBOXを追加する。

>vagrant box add bento/centos-6.7

Windows上に今回のテスト用のディレクトリ(test)を作成する。

>cd test

test内にVagrantfileを作成する。
Vagrantfile

Vagrant.configure("2") do |config|

  config.ssh.insert_key = false

  config.vm.define "vagrant0" do |vagrant0|
    vagrant0.vm.box = "bento/centos-6.7"
    vagrant0.vm.network :private_network, ip: "192.168.11.30"
  end

  config.vm.define "vagrant1" do |vagrant1|
    vagrant1.vm.box = "bento/centos-6.7"
    vagrant1.vm.network :private_network, ip: "192.168.11.31"
  end

end

config.ssh.insert_key = false
は『初めてのAnsible』でも説明されているが、Vagrantの各ホストごとに異なるSSH鍵を使うというデフォルトの設定をOFFにして、すべてのホストで同じSSH鍵を使う昔の動作にするために行っている。

vagrant0とvagrant1の2台の設定を行い、vagrant0をAnsible用、vagrant1をAnsibleによる構成管理対象用とする。
private_networkの設定を行っている理由は、Vagrantマシン間でSSH通信をできるようにするため。
※参考:vagrantのネットワークについて

Vagrantファイルを保存したら、vagrant upを実行してvagrant0とvagrant1の起動を行う。

>vagrant up
Bringing machine 'vagrant0' up with 'virtualbox' provider...
Bringing machine 'vagrant1' up with 'virtualbox' provider...
==> vagrant0: Checking if box 'bento/centos-6.7' is up to date...
==> vagrant0: Clearing any previously set forwarded ports...
==> vagrant0: Clearing any previously set network interfaces...
==> vagrant0: Preparing network interfaces based on configuration...
    vagrant0: Adapter 1: nat
    vagrant0: Adapter 2: hostonly
==> vagrant0: Forwarding ports...
    vagrant0: 22 (guest) => 2222 (host) (adapter 1)
==> vagrant0: Booting VM...
==> vagrant0: Waiting for machine to boot. This may take a few minutes...
    vagrant0: SSH address: 127.0.0.1:2222
    vagrant0: SSH username: vagrant
    vagrant0: SSH auth method: private key
    vagrant0: Warning: Remote connection disconnect. Retrying...
    vagrant0: Warning: Remote connection disconnect. Retrying...
    vagrant0: Warning: Remote connection disconnect. Retrying...
    vagrant0: Warning: Remote connection disconnect. Retrying...
    vagrant0: Warning: Remote connection disconnect. Retrying...
    vagrant0: Warning: Remote connection disconnect. Retrying...
==> vagrant0: Machine booted and ready!
==> vagrant0: Checking for guest additions in VM...
==> vagrant0: Configuring and enabling network interfaces...
==> vagrant0: Mounting shared folders...
    vagrant0: /vagrant => C:/test
==> vagrant0: Machine already provisioned. Run `vagrant provision` or use the `--provision`
==> vagrant0: flag to force provisioning. Provisioners marked to run always will still run.
### vagrant1は省略

Warning: Remote connection disconnect. Retrying…が出力されても、しばらく待つと接続できるようになる。
vagrant0にアクセスできるかどうか確認する。
確認方法はvagrant sshコマンドを使用するか、Tera Term等で接続するかの2通りがある。
・vagrant sshコマンドの場合

>vagrant ssh vagrant0

・Tera Termの場合

ホスト:192.168.11.30
ポート:22
ユーザ:vagrant
パスワード:パスフレーズにvagrant、あるいはRSA/DSA鍵にC:\Users\%USERNAME%\.vagrant.d\insecure_private_keyを設定

Windowsからvagrant0にアクセスできたら、vagrant0からvagrant1にSSHでアクセスできるように、C:\Users\%USERNAME%\.vagrant.d\insecure_private_keyをvagrant0にアップロードし、id_rsaとして保存する。

# scp等であらかじめWindows端末からvagrant0にinsecure_private_keyをコピーした状態とする
$ mv insecure_private_key ~/.ssh/id_rsa
$ chmod 600 ~/.ssh/id_rsa
#接続できるか確認
$ ssh 192.168.11.31
$ exit

3.Ansible用CentOSにAnsibleをインストールする

vagrant0にAnsibleをインストールする。CentOSではyumでAnsibleをインストールする場合、エンタープライズLinux用の拡張パッケージ(EPEL) を使えるようにする必要がある。

$ sudo yum localinstall http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm
#もしくは
$ sudo rpm -ivh http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm

$ sudo yum install ansible

4.別のCentOSにAnsibleでアクセスする

vagrant0からvagrant1にAnsibleでアクセスできるか、『初めてのAnsible』の「1.9.2 テストサーバーのことをAnsibleに知らせる」に沿って確認する。

$ mkdir playbooks
$ cd playbooks
$ echo testserver ansible_ssh_host=192.168.11.31 ansible_ssh_user=vagrant > hosts
$ ansible testserver -i hosts -m ping
testserver | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

次は『初めてのAnsible』の「1.9.3 ansible.cfgによる簡略化」に沿って確認する。

$ echo "[defaults]
hostfile = hosts
remote_user = vagrant
host_key_checking = False" > ansible.cfg
$ echo testserver ansible_ssh_host=192.168.11.31 > hosts
$ ansible testserver -m ping
testserver | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

大きな数値をJSONとして返す時に注意すること(Spring Bootでの実装例)

$
0
0

結論を一文で初めにいうと、Spring BootではJSONで返したいJavaBeansのfieldに必要に応じて

@JsonSerialize(using = ToStringSerializer.class)
を付けようということ。
以下で理由や現象を、Spring Bootで実装する場合の方法を知ることを目標として、説明する。

目次

JavaScriptの数値についての知識
JSONを返すRest APIの例
JSONを返すRest APIの例の解決策

JavaScriptの数値についての知識

JavaScriptで扱える数値は2の53乗 – 1までである。
JavaScriptの一部をベースに作られているJSONも同様に、2^53(9007199254740992)未満の数しか扱えない。
2^53と2^54は、たまたま計算がうまくいくが、2^55からは下位桁が0に丸められてしまい、2^70からは指数表示されてしまう。

JavaScriptでは,内部的に数値を「IEEE754」という規格に従って「64ビット倍精度」で保管している。

この規格で処理・表現できる最大値が,2の53乗 – 1なのだ。
最大値をオーバーした瞬間,正確さは保証されなくなる。

一番上の桁の正確さ(=有効数字)を保とうとする結果,一番下の桁から正確さが失われていく。

※参考および引用:http://language-and-engineering.hatenablog.jp/entry/20150513/JavaScriptIeee754OutOfRangeError

Number.prototype.toFixed([digits])
メソッドを使用すれば正確な文字列表記が取得できるが、サーバとブラウザ間で気軽にJSONを扱いたいのに毎回.toFixed()を呼ぶのは大変である。
※参考:MSD:Number.prototype.toFixed()

JSONを返すRest APIの例

Rest APIとして、桁数の多い数値型のIDをJSONで返すようなコードを書くとき、DB上でNumber型等になっていれば、Java上ではBigIntegerやBigDecimalでもつだろう。

例えば、ビル名からビル情報を曖昧検索してJSONで返す検索機能を次のようなJavaBeansとRestControllerで実装するとする。
JavaBeansはDBの検索結果を受け取る用途とJSONに変換される用途を兼用している。

import java.math.BigInteger;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Building {

	private BigInteger objectId;

	private String name;
}

// import文は省略

@RestController
@RequestMapping("/rest")
public class MasterInfoController {

	@Autowired
	MasterInfoService masterInfoService;

	@RequestMapping(value = "/buildings", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
	public List<Building> buildings(@Valid @ModelAttribute BuildingSearchForm buildingSearchForm, BindingResult result) {
		if (result.hasErrors()) {
			return Collections.emptyList();
		}

		List<Building> buildings = masterInfoService.findBuildingsByNameLike(buildingSearchForm.getName());
		return buildings;
	}
}

リクエストを受けるとサーバのJavaの世界では次のようなJSONが生成される。

[{"objectId":8144587379213241111,"name":"六本木ヒルズ"}]

ブラウザが受け取るResponse Bodyも上と同じものになる。
しかし、受け取ったJSONをJavaScriptがパースした時点で、8144587379213241111は8144587379213240000となってしまう。

JSONを返すRest APIの例の解決策

解決策は、DBやJavaの世界では数値型で保持するも、JSONとして応答するときには文字列に変換すること。

Springの@RequestMappingのproducesでJSONを指定し、JavaのオブジェクトをJSONに自動で変換してレスポンスを返す場合、内部ではJacksonライブラリが使われている。
Jacksonには@JsonSerializeというアノテーションがあり、getterやfieldに付けることでJSONにするときの値の変換ロジックを与えることができる。
独自に作った変換ロジックを持つクラスを指定してもいいのだが、今回のBigIntegerをStringにしたいというようなよくある例であれば既にJacksonが用意したクラス(ToStringSerializer.class)がある。

アノテーションを付けたBuildingクラス

import java.math.BigInteger;

import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Building {

	@JsonSerialize(using = ToStringSerializer.class)
	private BigInteger objectId;

	private String name;
}

MasterInfoControllerクラスには変更は一切入らない。

この対応をとるとobjectIdが文字列としてJSONに変換され、Javaがレスポンスを返す。

// 変更前:
[{"objectId":8144587379213241111,"name":"六本木ヒルズ"}]
// 変更後:
[{"objectId":"8144587379213241111","name":"六本木ヒルズ"}]

JavaScriptがこれをパースしても、文字列なので8144587379213241111が8144587379213240000に変わることはない。


『初めてのAnsible』読書ノート

$
0
0

初めてのAnsible』を読んだので、学んだ点をまとめる。
初めてのAnsible 初めてのAnsible

1.2 Ansibleが役立つこと

Ansibleが役立つこと
・設定管理
・デプロイ
・オーケストレーション(DB→APのデプロイ順、APは一つずつロードバランサから外すetc)
・プロビジョニング(新しい仮想マシンの起動)

1.9 テスト用サーバーのセットアップ

WindowsでAnsibleを実行するでアレンジしながら実践した。

・インベントリファイル
インベントリファイルをplaybooks/hostsとすると、

$ ansible サーバエイリアス -i hosts -m ping
でpingモジュールが呼び出せる。

・ansible.cfg
デフォルト設定のためのファイル。
ansible.cfgによりインベントリファイルが簡略化できる。
カレントディレクトリにplaybookと一緒に置き、共にバージョン管理するのがBEST。

ansible.cfg

[default]
hostfile = hosts

ansible.cfgで設定すると、次のように簡素に呼び出せる。

ansible.cfgなし $ ansible サーバエイリアス -i hosts -m ping
ansible.cfgあり $ ansible サーバエイリアス -m ping

2.2 ごくシンプルなPlaybook

yes/no/True/Falseの使い分け
・モジュール引数:yes/no
・Playbookのその他:True/False

2.5.1 Play

playは次の二つが最小構成要素。
・設定するホスト(hosts)
・タスクのリスト(tasks)

3.4 グループとグループとグループ

インベントリにはグループが設定可能。

playbooks/hosts

[web]
web[1:10].example.com
vagrant ansible_ssh_host=127.0.0.1

自動的にall(あるいは*)グループが定義される。

$ ansible all -a "date"

3.5 ホストとグループ変数:インベントリの内部

ホストに対して任意の変数を定義できる。
環境ごとに異なる設定値(ex, DBの接続情報)はグループに対する変数として定義するのが良い。

[all]
ntp-server=ntp-ubuntu.com

[production:vars]
db_primary_host=prodb.example.com
db_primary_port=5432
db_name=dbname
db_user=dbuser
db_password=pFmMxcyDjFc6)6

[staging:vars]
db_primary_host=stgdb.example.com
db_primary_port=5432
db_name=dbname
db_user=dbuser
db_password=L@4Ryz8cRUXedj

3.8 複数ファイルへのインベントリの分割

通常のインベントリファイルと動的インベントリファイルを同じディレクトリに置く。
ansible.cfgに

hostfile = ディレクトリ名
とする。
4.1 Playbook内での変数の定義

Playbook内で変数を定義する箇所
・varsセクション
・vars_filesセクション

4.2 変数の値の表示

-debug: var=変数名

4.3 変数の登録

register: 変数名

4.4.1 サーバーに関連づけられたすべてのファクトの表示

setupモジュールでファクト収集が手動でできる。

$ ansible server名 -m setup

フィルタリングはグロブを指定することで実現可能。

$ ansible server名 -m setup -a 'filter=ansible_eth*'

4.6.1 hostvars

すべてのホストに対して定義されたすべての変数を含む辞書。

ファクトにアクセスしようと思えば次のようにする。

hostvars['ホスト名'].ansible_eth1.ipv4.address

6.4 イテレーション(with_items)

Ansibleはループでのイテレーション変数名として必ずitemを使う。

apt: pkg={{item}}
become: True
with_items:
	-git
	-nginx
	-postgresql

6.9 タスク中の複雑な引数

・YAMLの行折り返し

pip: >
	name={{item.name}}
	version={{item.version}}

・キーを変数名とする辞書

pip:
	name:"{{item.name}}"
	version:"{{item.version}}"

7.1 コントロールマシン上でのタスクの実行

local_action節

7.2 ホスト以外のマシン上でのタスクの実行

delegate_to節

7.4 一度に一つのホストでの実行

serial節
playを並列実行するホスト数を設定する。
max_fail_percentage節を一緒に使え、すべてのホストでタスクが失敗する前にplay全体を失敗させる。

7.5 1回だけの実行

run_once節

7.8 Vaultによるセンシティブなデータの暗号化

センシティブ情報の扱い方

  1. センシティブ情報専用ファイルに切り出す。
    playbook.yml

    vars_files:
    	-secrets.yml

  2. Vaultを使用してセンシティブ情報専用ファイルを暗号化する。

    #暗号化
    $ ansible-vault encrypt secrets.yml
    #新規作成
    $ ansible-vault create secrets.yml

  3. Vaultでの暗号化の際に入力したパスワードを外部ファイルに書いて、playbookを実行する。

    $ ansible-playbook playbook.yml --vault-password-file ~/password.txt

7.12 ルックアップ

lookup('file', '/path/to/file.txt')
でファイルの中身を取り出せる。
file以外にpipi, env, csvfileなどがある。
7.13 さらに複雑なループ

with_items以外にwith_lines, with_fileglobなどがある。

8 ロール:プレイブックのスケールアップ

ロールは、playbookを複数のファイルに分割するための主要な仕組み。
ロールを1つ以上のホストに割り当てる。

8.1 ロールの基本構造

ロールに関連づけられたファイルは、

roles/ロール名/
ディレクトリに置かれる。
roles/database/tasks/main.yml
roles/database/files/
etc...

rolesディレクトリ自体は、次の場所である。
・playbookが置かれているディレクトリ内のroles
・システム全体にわたるロールは、/etc/ansible/roles

8.3 Playbooks内でのロールの利用

playbook中にロールのセクションを置く。
ロールを呼び出す際に変数が渡せる。(今回の例ではdatabase_name, database_userの2変数)

roles:
	-role: databse
	 database_name: "{{db_name}}"
	 database_user: "{{db_user}}"

8.6 Mezzanineのデプロイのための”mezzanine”ロール

Ansibleには、複数のロール間で使える名前空間の記法がない。
→ほかのロールで定義された変数やplaybookの他の部分で定義された変数がどこからでもアクセス可能。
→ロールの変数に接頭語をつけるのがいい。
・mezzanine_venv_home
・mezzanine_venv_path
 など

14.4.1 構文チェック

–syntax-check

$ ansible --syntax-check playbook.yml

14.4.4 チェックモード

-C or –check
dry run。
実際にサーバーに変更を加えずplaybookを実行する。

$ ansible -C playbook.yml

チェックモードの難点:playbookのそれ以前の部分が成功した時のみ、ある部分が成功する場合、チェックモードではタスクが失敗する。

14.4.5 Diff(ファイルの差分の表示)

-D or –diff
–checkと併せて使う。

$ ansible -D --check playbook.yml

14.5.1 step

各タスクの実行前に y/n/c を聞いてくれる。
cを選ぶと、playbookの残りをプロンプトの表示をせずに実行する。

14.5.2 start-at-task

–start-at-task タスク名

『はじめよう! 要件定義』読書ノート

$
0
0

はじめよう! 要件定義 ~ビギナーからベテランまで』を読んだので、学んだ点をまとめる。

はじめよう!  要件定義 ~ビギナーからベテランまで はじめよう! 要件定義 ~ビギナーからベテランまで

要件とは

04 3つの要素の定め方
要件定義の三要素
  • UIを決める
  • 機能を決める
  • データを決める

要件定義の前に

06 企画を確認する

ソフトウェア作成のゴールを確認するための企画書を探す。
なければ作る。

企画書の構成
  1. プロジェクトの名称
  2. なぜ
  • 目的
  • 何を
    • 作るもの
    • 作るものの説明
    • 利用者
    • 利用者が得られる便益
  • どのように
    • 体制
    • 期限
    07 全体像を描こう

    全体像とは、「何を」の部分をビジュアルに表現したもの。

    企画書:プロジェクトの紹介


    全体像:ソフトウェアの紹介

    ソフトウェア、(システム管理者や連携システムを含めた)利用者、利用者の行うことを書く。

    08 大まかに区分けしよう

    サブブロック図を作成する。
    全体像に描かれているソフトウェアを分割する。

    09 実装技術を決めよう

    Web or ネイティブ、言語/フレームワーク/DB以外にも要件に深くかかわる部分がある。

    • ユーザアクションの検知方法(マウス, タッチ or 音声入力?)
    • 画面の遷移方法
    • 通信プロトコル、データフォーマット
    10 実現したいことを整理整頓しよう

    やりたいことを一覧にする。要求一覧。

    11 利用者の行動シナリオを書こう。
    行動シナリオから得られるもの。
    • ソフトウェアを利用するタイミング
    • ソフトウェアを利用する理由
    • ソフトウェアを利用することで達成できる仕事
    行動シナリオの書き方

    重要なのは、UIの出現場所。
    誰が何をするためにソフトウェアを必要とするのかが明確になりさえすればいい。
    要求一覧を吟味しながらシナリオを作る。

    12 概念データモデルを作る。

    行動シナリオを材料に名詞、動詞を抽出する。大雑把なものでOK。

    要件定義のメイン

    13 UIを考えよう
    UIの構成要素
    • データ項目
    • 操作項目
    • レイアウト

    各行動シナリオの仕事(ワークセット)単位で、「何をどうする」の形式でワイヤーフレーム、モックアップ等を作る。

    • 必要な項目の列挙
    • 動線
    • 操作と機能
    16 要件定義の仕上げ
    権限別のワークセット、行動シナリオ

    権限ごとにワークセットを分けるのが基本。
    要件を分けることと実装の共通化は別の話。

    17 要件定義、その後に

    他人に理解してもらえるように、要件定義成果物の一覧を作る。
    全体から詳細へ、WhyからWhat、WhatからHowへ流れる一覧。

    1. 企画書
    2. 全体像
    3. 要求一覧
    4. 行動シナリオ一覧
    5. 行動シナリオ
    6. ワークセット一覧
    7. 概念データモデル
    8. モックアップ
    9. 画面遷移図
    10. 項目の説明
    11. 機能の入出力定義
    12. 機能の処理定義
    13. ERD

    『プロジェクトを成功させる技術!』読書ノート

    $
    0
    0

    『図解とマンガでわかるリーダーになったら最初に読む プロジェクトを成功させる技術!』を読んだので、学んだ点をまとめる。
    図解とマンガでわかるリーダーになったら最初に読む プロジェクトを成功させる技術! (マジビジPRO) 図解とマンガでわかるリーダーになったら最初に読む プロジェクトを成功させる技術! (マジビジPRO)

    02 プロジェクトは5つのプロセスで出来ている
    • 立ち上げ
    • 計画
    • 実行
    • 監視とコントロール
    • 終結
    05 まず、「課題ログ」を作ってみよう
    • 課題管理
    • タスクをメンバーにアサインする
    06 プロジェクトの目的と成功基準をはっきりさせる

    目的と成功基準がプロジェクトの範囲を決める。
    範囲が勝手に広がらないように基準を作るのが大切。

    07 プロジェクトに関わる顧客の声を聞きにいく

    プロジェクトは顧客のニーズを満たせて初めて成功といえる。

    • 誰に聞くのか?
    • 何を聞くのか?
    • いつ聞くのか?
    11 アウトプットのイメージをしておく

    プロジェクトとして、何を作らなければいけないか。
    大まかなWBSを作り、以下を明らかにする。

    • 最終アウトプット
    • 中間成果物
    • 作成に必要な作業

    ※上から順にブレイクダウンしていく。

    14 誰にどんな情報を展開するかを合意する
    • ステークホルダーの特定
    • 影響のある情報の展開
    • 情報展開の相手は誰かについての合意
    18 プロジェクトチャーターを作ってみる

    プロジェクトのビジョンを示し、ベクトルを合わせる。

    プロジェクトチャーター項目一覧
    • プロジェクトの名前
    • プロジェクトミッション
    • プロジェクトの目的、ニーズ
    • プロジェクトの目標
    • チームメンバー
    • プロジェクトリーダー
    • プロジェクトスポンサー
    20 キックオフミーティングを開く
    目的
    • プロジェクトが承認されたことを伝える
    • プロジェクトリーダーとして認めてもらう
    • 組織としての支援を働きかける
    • プロジェクトのビジョン、方向性を共有する
    21 チームはこうやってできていく
    1. 成立期
      • 強力なリーダーシップが必要
    2. 動乱期
      • 各自の役割が理解される
      • 不満が出やすい
        →コミュニケーションの促進が必要
    3. 安定期
      • 権限をチームで共有する
      • チームとして問題解決をする
    4. 遂行期
      • 各自がリーダーシップを発揮
        →各自に任せ、見守りと調整に徹する
    25 やるべきことを3つの関係で整理する
    3つの関係
    • 作業(プロセス)
    • インプット
    • アウトプット

    作業と作業の依存関係を見える化する。

    PFD(プロセスフローダイアグラム)

    29 余裕は後ろにまとめて、時間枠で管理する

    見積もりを余裕といっしょくたにしない。
    バッファはバッファとして後ろにあつめて管理する。

    30 プロジェクトの現状を手間をかけずに把握する
    • 期日で管理しない。バッファを後ろに集めて、時間枠で管理する
    • バッファの消化枠と計画と実績の差の推移を視る
    • 計画の変更をある程度吸収できるようにする
    33 リスクリストを作成する
    リスク管理
    • リスクの洗い出し
    • リスク評価(発生確率と影響度)
    • リスク対策(回避、軽減、転嫁、受容)
    • リスクの追跡

    Spring Boot + Thymeleafでエラー画面をカスタマイズする

    $
    0
    0

    Spring Boot + Thymeleafの構成でエラー画面をカスタマイズする方法。

    検証version
    • Spring Boot 1.3.5
    • Thymeleaf 2.1.4
    • Bootstrap 3.3.6
    Whitelabel Error Page

    Spring Bootで404等のエラーが発生したときに表示されるエラーページはWhitelabel Error PageというSpringがデフォルトで用意している味気ない固定のエラーページになる。

    Whitelabel Error Page

    This application has no explicit mapping for /error, so you are seeing this as a fallback.

    Fri Jul 22 18:11:48 JST 2016
    There was an unexpected error (type=Not Found, status=404).
    No message available

    error.html

    Thymeleafを組み合わせている場合、エラー画面を独自に定義するには、error.htmlを用意すればよい。

    /src/main/resources/templates/error.html

    <?xml version='1.0' encoding='UTF-8' ?>
    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org"
    	xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4"
    	xmlns:layout="http://www.myproject/web/thymeleaf/layout"
    	layout:decorator="common/layout">
    
    <head>
    </head>
    
    <body>
    	<div class="container" layout:fragment="content">
    		<div id="information" class="alert alert-danger alert-dismissible">
    			エラーが発生しました。
    		</div>
    	</div>
    
    </body>
    </html>

    エラー種別ごとの詳細カスタマイズ

    404エラーの時は別のエラー画面を表示するなどのカスタマイズを行うには、EmbeddedServletContainerCustomizerインタフェースをimplementしたクラスとErrorController.javaを実装し、さらに404用のHTMLを用意すればよい。

    /src/main/java/com/sample/system/servletcontainer/Customizer.java

    import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
    import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
    import org.springframework.boot.context.embedded.ErrorPage;
    import org.springframework.http.HttpStatus;
    import org.springframework.stereotype.Component;
    
    @Component
    public class Customizer implements EmbeddedServletContainerCustomizer {
    
    	@Override
    	public void customize(ConfigurableEmbeddedServletContainer container) {
    		container.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/404"));
    	}
    
    }

    /src/main/java/com/sample/web/error/ErrorController.java

    import org.springframework.http.HttpStatus;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.ResponseStatus;
    
    @Controller
    public class ErrorController {
    
    	@RequestMapping(value = "/404", method = RequestMethod.GET)
    	@ResponseStatus(HttpStatus.NOT_FOUND)
    	public String notFound() {
    		return "404";
    	}
    
    }

    /src/main/resources/templates/404.html

    <?xml version='1.0' encoding='UTF-8' ?>
    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org"
    	xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4"
    	xmlns:layout="http://www.myproject/web/thymeleaf/layout"
    	layout:decorator="common/layout">
    
    <head>
    </head>
    
    <body>
    	<div class="container" layout:fragment="content">
    		<div id="information" class="alert alert-danger">
    			404 Not Found<br/>
    			ページが見つかりません。
    		</div>
    	</div>
    
    </body>
    </html>

    Spring BootでLogging用のFilterクラスを実装する

    $
    0
    0

    静的ページ(JavaScriptやCSS等)は除いて、Spring Bootでリクエストを受けたものについて、以下をログに出力したい。

    • 処理開始時刻、処理終了時刻、処理時間
    • URL
    • すべてのリクエストパラメータ
    • レスポンスステータス

    また、一つ一つのリクエストを一意に特定しやすくするため、ログインユーザ名とUUIDを組み合わせた文字列を、ログの先頭に識別子として必ず付与する。

    LoggingFilter.java

    import java.io.IOException;
    import java.util.Arrays;
    import java.util.Map;
    import java.util.Map.Entry;
    import java.util.UUID;
    
    import javax.servlet.Filter;
    import javax.servlet.FilterChain;
    import javax.servlet.FilterConfig;
    import javax.servlet.ServletException;
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.springframework.stereotype.Component;
    
    import lombok.extern.slf4j.Slf4j;
    
    @Component
    @Slf4j
    public class LoggingFilter implements Filter {
    
    	@Override
    	public void init(FilterConfig filterConfig) throws ServletException {
    	}
    
    	@Override
    	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    		HttpServletRequest httpReq = ((HttpServletRequest)request);
    		String uri = httpReq.getRequestURI();
    		if (isStatic(uri)) {
    			chain.doFilter(request, response);
    			return;
    		}
    
    		long start = System.currentTimeMillis();
    		UUID uuid = UUID.randomUUID();
    		String userName = httpReq.getRemoteUser();
    		String requestIdentifier = "[" + userName + "]" + "[" + uuid + "]";
    
    		log.info(String.format("%s start", requestIdentifier));
    		log.info(String.format("%s URI: %s", requestIdentifier, uri));
    
    		Map<String, String[]> params = httpReq.getParameterMap();
    		for (Entry<String, String[]> param : params.entrySet()) {
    			log.info(String.format("%s PARAM_KEY: %s, PARAM_VALUE: %s"
    					, requestIdentifier
    					, param.getKey()
    					, Arrays.toString(param.getValue())));
    		}
    
    		chain.doFilter(request, response);
    
    		int status = ((HttpServletResponse)response).getStatus();
    
    		log.info(String.format("%s end in %d millisec. STATUS %d", requestIdentifier, System.currentTimeMillis() - start, status));
    	}
    
    	private boolean isStatic(String uri) {
    		return uri.contains("/js/")
    		|| uri.contains("/css/")
    		|| uri.contains("/fonts/");
    	}
    
    	@Override
    	public void destroy() {
    	}
    }

    ソースコード解説
    ログ項目の取得方法

    処理開始時刻、処理終了時刻、処理時間は通常のJavaコードである。

    URL、リクエストパラメータ、レスポンスステータスは、SpringのモジュールではなくJavaServletでおなじみのHttpServletRequest、HttpServletResponseを利用して取得する。

    Loggerオブジェクト

    Loggerオブジェクトを取得するためには、通常

    private static final Logger log = LoggerFactory.getLogger(Sample.class);
    のようなコードを書く必要があるが、Lombokを使用しているため、
    @Slf4j
    でクラスをアノテートすることで、logという名前の Loggerオブジェクトが使えるようなる。
    Filter実装クラスのSpring Bootへの登録方法

    @Component
    をFilter実装クラスにつけるだけで、Spring BootがFilterとして扱ってくれる。

    今回はFilterはただ一つLoggingFilterのみを登録すればよかったのでこの方法を採っているが、Filter実装クラスを複数登録し、さらにFilterの実行順序を制御するのであれば、FilterRegistrationBeanを使用する必要があるだろう。

    FilterRegistrationBeanの使用方法はこちらを参考:Spring Boot で複数の Filter を定義する

    Spring Boot製アプリをSelenium WebDriverでIntegrationテストする

    $
    0
    0

    Spring BootにはJUnitとSelenium WebDriverを組み合わせてEnd to Endテストできる機能が組み込まれている。
    Selenium部分は通常のJavaを使ったPageObjectパターンで作成するとして、JUnit部分をどのように設定していけばいいかを記載する。

    検証version

    • Spring Boot 1.3.5
    • Selenium WebDriver 2.53.0

    Annotation設定

    Port

    試験を実行するときのPortをランダムで決定するように設定する。
    決め打ちでもいいが同一端末上で別のアプリが動いていたりすることがあるので、空Portを使用してくれるランダムPort設定のほうがBetter。

    設定方法は、クラスに

    @WebIntegrationTest(randomPort = true)
    を、フィールドに
    @Value("${local.server.port}")
    を付与する。

    @Valueはプロパティファイルの値をフィールドに設定してくれる。

    実装具体例

    @WebIntegrationTest(randomPort = true)
    public class IntegrationTest {
    }

    @Value("${local.server.port}")
    int port;

    Contextパス

    プロパティファイルでアプリのルートURLとなるContextパスを設定している場合、@ValueでContextパスを変数に設定する。

    @Value("${server.context-path}")
    String context;

    JUnit設定

    @RunWith(SpringJUnit4ClassRunner.class)
    をクラスに付与する。

    Spring BootのConfig設定

    @SpringApplicationConfiguration(classes = Application.class)
    をクラスに付与することで、通常実行時と同じように起動する。
    src/test/resources
    にapplication.propertiesを置くことで、テスト用プロパティファイルでオーバーライド可能。
    application.propertiesでDBへのデータセット用SQLを設定できるので、テスト用データの管理にSQLを使うことができる。

    spring.datasource.schema=schema.sql
    spring.datasource.data=data.sql

    setupとdestroy

    JUnitのsetupとdestroyでは、Selenium WebDriverの起動と終了を管理する。
    今回後掲するソースではsetupではFirefoxProfileの設定を行っているが、Seleniumでファイルダウンロードして、その後検証する方法で解説した機能の設定と同じ。ファイルダウンロード機能がないアプリケーションであれば不要となる。

    ソース

    import static org.junit.Assert.*;
    
    import java.io.IOException;
    import java.nio.file.Files;
    import java.nio.file.Path;
    import java.nio.file.Paths;
    
    import org.junit.After;
    import org.junit.Before;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.openqa.selenium.WebDriver;
    import org.openqa.selenium.firefox.FirefoxDriver;
    import org.openqa.selenium.firefox.FirefoxProfile;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.boot.test.SpringApplicationConfiguration;
    import org.springframework.boot.test.WebIntegrationTest;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    import lombok.SneakyThrows;
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringApplicationConfiguration(classes = Application.class)
    @WebIntegrationTest(randomPort = true)
    public class IntegrationTest {
    	@Value("${local.server.port}")
    	int port;
    
    	@Value("${server.context-path}")
    	String context;
    
    	WebDriver driver;
    
    	Path tempDir = Paths.get(System.getProperty("java.io.tmpdir"), "seleniumtest", Long.toString(System.currentTimeMillis()));
    
    	@SneakyThrows(IOException.class)
    	@Before
    	public void setup() {
    		Files.createDirectories(tempDir);
    		FirefoxProfile profile = new FirefoxProfile();
    		profile.setPreference("browser.download.folderList", 2);
    		profile.setPreference("browser.download.dir", tempDir.toString());
    		profile.setPreference("browser.download.useDownloadDir", true);
    		profile.setPreference("browser.helperApps.neverAsk.saveToDisk", "text/html, text/plain, application/vnd.ms-excel, text/csv, application/zip, text/comma-separated-values, application/octet-stream");
    		driver = new FirefoxDriver(profile);
    	}
    
    	@After
    	public void destroy() {
    		if (driver != null) {
    			driver.quit();
    		}
    	}
    
    	@Test
    	public void EndToEndシナリオ() {
    		LoginPage loginPage = LoginPage.to(driver, port, context);
    		MainPage mainPage = loginPage.login("user1", "password1", MainPage.class);
    
    		// エラーパターン
    		mainPage.download();
    		assertEquals("このフィールドは必須です。", mainPage.getResultError().getText());
    		・・・略
    	}
    }

    Copyright © 2016 システム開発メモ All Rights Reserved.

    シェルスクリプトでAtomicなファイル操作をする

    $
    0
    0

    シェルスクリプトでAtomicなファイル操作が必要になる例を次の2点あげ、どのコマンドを使用すればいいのか記載する。

    • ロックファイルの作成
    • 複数のプロセスが複数のファイルを処理する

    ロックファイルの作成をシェルスクリプトで実現する方法

    ロックファイルを使いたいということは、複数プロセスからロックファイルの存在確認と(存在しない場合)ファイル作成が行われるということなので、存在確認と作成を同時にAtomicに行ってくれるコマンドが必要になる。

    シンボリックリンクを作成するコマンドが適任だ。

    シンボリックリンクは、すでにファイルが存在するとエラーとなる。シンボリックリンクをロックファイルとして扱えば、ロックファイルが存在すればエラーが発生し、存在しなければシンボリックリンクとして作成される。
    さらにシンボリックリンクの都合のいいことは、リンク先のファイルが存在しなくてもいいということだ。適当な文字列をリンク先としておけばいい。1でもいいし、$$としてシェルスクリプトのプロセスIDでもいい。

    if ! ln -s $$ file.lock; then
        exit 0
    fi

    複数のプロセスが複数のファイルを処理する

    複数のプロセスが複数のファイルを処理していく、という題材。ロックファイルを使用してもいいのだが、Atomicなファイル操作さえできればロックファイルを使用しなくても実現できる。

    ファイルがあるディレクトリに大量にあり、各プロセスはループして次々とファイルを処理していく場合、プロセス間で処理するファイルがぶつからないようにする必要がある。この場合、ロックファイルを作成して、処理対象のファイルを決める瞬間を一プロセスに限定するというような処理を書いてもいいが、mv コマンドを使用すればロックファイルを使用するまでもない。

    mv コマンドは、移動元と移動先が同一ファイルシステム上にある場合、rename()システムコールが呼ばれるが、renameはAtomicにファイルの置き換えを行ってくれる。
    処理対象のファイル名が(a.txt, b.txt, …)だった場合、

    mv a.txt a.txt.done
    とすれば、複数のプロセスが同時にa.txtを触ることはできなくなる。mv に成功したプロセスのみa.txtを処理することができ、失敗したプロセスは次のファイルを mv してみればいい。

    Copyright © 2016 システム開発メモ All Rights Reserved.


    ArrayList#getでIndexOutOfBoundsExceptionとArrayIndexOutOfBoundsExceptionが返る違い

    $
    0
    0

    ArrayList#get(int index)
    の引数indexにArrayListのsizeの範囲を超える数字を入れると、
    IndexOutOfBoundsException
    もしくは
    ArrayIndexOutOfBoundsException
    が発生する。indexによって異なる例外が発生する。indexが負数のときがArrayIndexOutOfBoundsExceptionで、index >= sizeのとき(greater than or equal to, gte)がIndexOutOfBoundsExceptionとなる。

    発生する例外に違いが出る原因の実装調査と、このような実装になっている根本理由の調査、そしてStack OverFlowで質問して合点が行くまでの経緯をまとめる。

    目次

    rangeCheckメソッド

    indexが負数かgteかで変わる原因を調べる。ArrayList#getの中でまず初めにrangeCheckメソッドを呼び出していた。rangeCheckを呼び出しているのはget以外にもsetとremoveがあったが、どれもまず初めに呼び出していた。

    /**
     * Checks if the given index is in range.  If not, throws an appropriate
     * runtime exception.  This method does *not* check if the index is
     * negative: It is always used immediately prior to an array access,
     * which throws an ArrayIndexOutOfBoundsException if index is negative.
     */
    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
    
    public E get(int index) {
        rangeCheck(index);
    
        return elementData(index);
    }

    rangeCheckのJavaDoc及び実装を見ると、indexが負数の時はチェックせず、ArrayListが内部で持つ配列にアクセスした時の検査に任せ、配列アクセス時にArrayIndexOutOfBoundsExceptionを投げるようになっていた。

    ArrayIndexOutOfBoundsExceptionはIndexOutOfBoundsExceptionのサブクラスなので、投げられる例外がこのように異なっていても両方ともIndexOutOfBoundsExceptionでcatchできるから問題ないのだが、なぜわざわざ負数のときはチェックせずに配列の検査機構に任せているのかという理由を探ることにした。

    負数をチェックしない理由

    そもそもJDKの実装で負数をチェックしないのはArrayList導入時からではなく、Java1.4.2からのようだ。
    JDK-4819074 : Remove the index < 0 test from RangeCheck in ArrayListで、配列の検査機構でもできる負数のチェックを行うのはredundant、つまり冗長で、頻繁に呼び出されるgetのようなメソッドで不要なチェックをしていることになると主張され、1.4.2になるときに負数チェックを行わないようになっている。

    get, setメソッドは頻繁に呼び出されるのでパフォーマンス重視なのはわかるが、Java Language Specificationで、配列の検査機構はindexが負数の時だけでなく、indexと配列のlengthを比べて大なりイコールであった場合もArrayIndexOutOfBoundExceptionを投げると定義されているので、それではArrayListのrangeCheckメソッド自体要らないのではないかと思ってしまった。

    Stack OverFlowでの解決

    rangeCheckメソッドの存在意義がわからなくなってしまったのでStack OverFlowで聞いてみた。
    Why does ArrayList#rangeCheck not check if the index is negative?

    回答はすぐに貰えて、しかも当たり前すぎる事実が意識から抜けていたのが悩みの原因だったとわかった。

    ArrayListが内部で保持する配列(the backing array)がArrayListの現在のサイズよりも大きいかもしれないから、上限チェックは行わなければならないということだった。

    ArrayListは現在のサイズを超えると上限を拡張してくれるので、内部保持の配列のlengthは当然sizeよりも常に等しいか大きい。
    size <= lengthなので、配列の検査機構を使ってしまうと、index < lengthは保障されても、index < sizeが保障されなくなってしまう。

    Copyright © 2016 システム開発メモ All Rights Reserved.

    Mavenリポジトリにないライブラリをpom.xmlで指定する方法

    $
    0
    0

    Mavenのセントラルリポジトリに登録されていないライブラリ(Oracleのojdbc.jarや自作Jar)をpom.xmlで指定できるようにする方法を書く。
    方法はローカルリポジトリにインストールすることになるのだが、その方法を二点記載する。またあるサイトでsystemスコープを指定することで簡単に実現できると書いてあったが、2016/08/21時点では、mvn installするとWARNINGが発生してライブラリが最終成果物のJarファイルに含まれないという事態に陥ったので、その点も記載する。

    目次

    ローカルリポジトリにインストールする方法その1

    1. Mavenのinstallコマンドを実行する
    2. カレントディレクトリにインストールしたいライブラリがあるとする。ojdc6.jarや自作ライブラリmy_common.jarを mvn install:install-file して、ローカルリポジトリにインストールするには、次のコマンド実行方法の通りにする。

      # コマンド実行方法
      # mvn install:install-file -Dfile=Jarファイルへのパス -DgroupId=グループID -DartifactId=アーティファクトID -Dversion=バージョン -Dpackaging=jar
      # ojdbc6.jarの場合
      mvn install:install-file -Dfile=ojdbc6.jar -DgroupId=com.oracle -DartifactId=ojdbc6 -Dversion=11.2.0 -Dpackaging=jar
      # 自作ライブラリの場合
      mvn install:install-file -Dfile=my_common.jar -DgroupId=me -DartifactId=my_common -Dversion=1.0.0 -Dpackaging=jar

      参考:Guide to installing 3rd party JARs

    3. プロダクトのpom.xmlでライブラリを指定する。
    4. # ojdbc6.jarの場合
      <dependency>
        <groupId>com.oracle</groupId>
        <artifactId>ojdbc6</artifactId>
        <version>11.2.0</version>
      </dependency>

    5. 確認
    6. ライブラリのリンク先を確認する。
      Eclipseを使っていれば、Maven Dependenciesのojdbc6.jarが.m2\repository\com\oracle\ojdbc6\11.2.0となっている。
      実際に.m2\repository\com\oracle\ojdbc6の中を見てみると、maven-metadata-local.xmlが出力され、さらに11.2.0の中には、Jarファイルやpomファイルが入っている。

    systemスコープを指定する方法とその問題点

    Mavenリポジトリで提供されていないサードパーティJarをどうするかに書いてあった方法で、一番簡潔でいいかと思われたが、mvn install時に問題が発生した。最終成果物のJarファイルに今回追加したいライブラリが含まれないのだ。
    Mavenリポジトリで提供されていないサードパーティJarをどうするかで示されている方法を今回の例に合わせて記載したうえで、問題点を見ていく。

    1. プロダクトのpom.xmlに<scope><systemPath>をつけて書く
    2. # ojdbc6.jarの場合
      <dependency>
        <groupId>com.oracle</groupId>
        <artifactId>ojdbc6</artifactId>
        <version>11.2.0</version>
        <scope>system</scope>
        <systemPath>${basedir}/lib/ojdbc6.jar</systemPath>
      </dependency>

      <scope>ではsystemを指定し、<systemPath>ではpom.xmlが置かれているディレクトリまでのフルパスを示す${basedir}を用いてライブラリのフルパスを指定する。

    3. 確認
    4. ライブラリのリンク先を確認する。
      Eclipseを使っていれば、Maven Dependenciesのojdbc6.jarが.workspace\プロダクト名\libになっている。

    5. 問題点
    6. Eclipse上では問題なく開発ができるようになった。開発に区切りがつき、Jarファイルにまとめてサーバで実行しようとmvn installコマンドを打つと次のWARNINGが発生する。

      [WARNING] 'dependencies.dependency.systemPath' for com.oracle:ojdbc6:jar should not point at files within the project directory, ${basedir}/lib/ojdbc6.jar will be unresolvable by dependent projects @ line 79, column 16
      [WARNING] It is highly recommended to fix these problems because they threaten the stability of your build.
      [WARNING] For this reason, future Maven versions might no longer support building such malformed projects.

      WARNINGがでてもビルド自体は成功し、BUILD SUCCESSと出力される。しかし、生成されたJarファイルを解凍してみるとojdbc6.jarは含まれていなかった。
      <systemPath>を使い、サードパーティ製Jarファイルを指定すべきではないようだ。

    ローカルリポジトリにインストールする方法その2

    ローカルリポジトリへのインストールを採用する場合は長いmvn install:install-fileコマンドを打たないといけないのに対して、systemスコープを指定する場合はバージョン管理システムからEclipseにImportするだけでよく、シンプルだと思ったが、mvn installできないので採用するわけにはいかない。しかし${basedir}を用いてライブラリのフルパスをpom.xmlに書くという点は、ローカルリポジトリへのインストールにも応用でき、長いmvn install:install-fileコマンドを打たなくてもmvn cleanだけで良くなる。

    1. プロダクトのpom.xmlでライブラリを指定する
    2. # ojdbc6.jarの場合
      <dependency>
        <groupId>com.oracle</groupId>
        <artifactId>ojdbc6</artifactId>
        <version>11.2.0</version>
      </dependency>

    3. プロダクトのpom.xmlでmaven-install-pluginを設定する。
    4. mvn install:install-fileで書いたことと同じことをpom.xmlで書いているだけである。コマンド引数を全てpom.xmlに書くことで毎回コマンドを打たなくて良くなる。

      <build>
       <plugins>
           <plugin>
               <groupId>org.apache.maven.plugins</groupId>
               <artifactId>maven-install-plugin</artifactId>
               <executions>
                   <execution>
                       <id>install-external</id>
                       <phase>clean</phase>
                       <configuration>
                           <file>${basedir}/lib/ojdbc6.jar</file>
                           <repositoryLayout>default</repositoryLayout>
                           <groupId>com.oracle</groupId>
                           <artifactId>ojdbc6</artifactId>
                           <version>11.2.0</version>
                           <packaging>jar</packaging>
                           <generatePom>true</generatePom>
                       </configuration>
                       <goals>
                           <goal>install-file</goal>
                       </goals>
                   </execution>
               </executions>
           </plugin>
       </plugins>
      </build>

      参考:Maven and adding JARs to system scope

    5. mvn cleanコマンドを実行する
    6. maven-install-pluginの設定で<phase>clean</phase>と設定したため、mvn cleanを実行することで、maven-install-pluginが動いてくれる。

    7. 確認
    8. Eclipse 4.6 Neonで確認した限りでは、mvn cleanを実行した後もpom.xmlにはMissing artifact com.oracle:ojdbc6:jar:11.2.0というエラーが出ていた。これはコマンドラインでmvn cleanしてもEclipseのRun AsからMaven Cleanを選んでも同じ。
      pom.xmlを再保存するとエラーは消えた。
      ローカルリポジトリにインストールする方法その1と同様、Eclipseを使っていれば、Maven Dependenciesのojdbc6.jarが.m2\repository\com\oracle\ojdbc6\11.2.0となっている。
      実際に.m2\repository\com\oracle\ojdbc6の中を見てみると、maven-metadata-local.xmlが出力され、さらに11.2.0の中には、Jarファイルやpomファイルが入っている。
      またmvn installやmvn packageを行っても生成されたJarファイルにはojdbc6.jarが入っていた。

    まとめ

    ローカルリポジトリにインストールする方法であれば、どちらの方法を採用しても問題ないが、pom.xmlにmvn install:install-fileの引数情報が全てつまっている方法2の方がお勧め。

    Copyright © 2016 システム開発メモ All Rights Reserved.

    ObjectOutputStreamを使ってファイルのやり取りをSpring Bootで実装する

    $
    0
    0

    ObjectOutputStreamObjectInputStreamを使うことで、Serializable JavaオブジェクトのHTTPを通した送信、受信ができる。オブジェクトにbyte[]が含まれていれば、バイナリファイルのアップロードとダウンロードもできることになる。

    ObjectOutputStream/ObjectInputStreamでJavaオブジェクトを送受信する場合、サーバサイドだけでなくクライアントもJava実装を強いられるため、複数のクライアントが対応しようとするときの障壁になる。基本は、JSONでのやりとりやmultipart/form-data形式で送受信した方がブラウザをはじめとして多種多様なクライアントとのやり取りが簡単になるので、Javaオブジェクトを送受信しないほうがいい。

    今回、他社に作られたObjectOutputStream/ObjectInputStreamで送受信するシステムと接続することになり、こちらの環境でその外部システムのスタブを作ることになったので、それをSpring Bootで実装してみた。

    version
    Spring Boot 1.4.0

    ファイルの送受信の種類

    ObjectOutputStream/ObjectInputStreamで作るのは今回のような前提がない限り勧められないので、Spring Bootでの実装に移る前に、送受信の種類を一覧にしておく。特に理由がない限り各項目の上位に記載されている方法から順に実装を検討するといい。

    送信

    • multipart/form-data形式(HTML)
    • Base64 EncodeしてJSONにセットする
    • ObjectOutputStream/ObjectInputStream

    受信

    • ダウンロード形式(Content-Disposition: attachment)
    • Base64 EncodeしてJSONにセットする
    • ObjectOutputStream/ObjectInputStream

    ダウンロード形式ではファイルのバイナリデータ以外にデータを送ることができないが、ファイル名であればContent-Dispositionヘッダにセットされる。

    Content-Disposition: attachment; filename*=UTF-8''ファイル名

    参考:Spring Bootで日本語ファイル名のファイルダウンロード

    ファイル名以外にも大量に情報を受信するのには向かないが、一つ二つ追加したいくらいであれば独自ヘッダを定義してヘッダに情報を書くのもいいだろう。

    Spring Bootでの実装

    Spring Bootでのサーバサイド実装

    ※サンプルのためエラーは全て握りつぶす。
    new ObjectInputStream(req.getInputStream())readObject(), new ObjectOutputStream(res.getOutputStream())writeObject(ret)がObjectOutputStream/ObjectInputStreamを使う上でキーになるコード。

    package jp.co.sample.web;
    
    import java.io.IOException;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    import java.nio.file.Files;
    import java.nio.file.Paths;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.PostMapping;
    
    import jp.co.sample.SampleObject;
    
    @Controller
    public class SampleController {
    
        @PostMapping(value = "/upload")
        public String upload(HttpServletRequest req, HttpServletResponse res) {
            // Javaオブジェクトをリクエストから取り出し
            SampleObject sampleObject = null;
            try (ObjectInputStream ois = new ObjectInputStream(req.getInputStream())) {
                sampleObject = (SampleObject) ois.readObject();
    
                if (sampleObject == null) {
                    res.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                    return null;
                }
            } catch (IOException | ClassNotFoundException e) {
                res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                return null;
            }
    
            // 取り出したJavaオブジェクトを使って仕事をする
            byte[] docBody = sampleObject.getDocumentBody();
            String fileName = sampleObject.getDocumentName();
            SampleObject ret = new SampleObject();
            try {
                Files.write(Paths.get(fileName), docBody);
            } catch (IOException e) {
                ret.setErrorFlg("1");
            }
    
            // Javaオブジェクトをレスポンスにする
            try (ObjectOutputStream oos = new ObjectOutputStream(res.getOutputStream())) {
    
                oos.writeObject(ret);
            } catch (IOException e) {
                res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                return null;
            }
    
            return null;
        }
    }

    URLConnectionを使ったクライアント実装

    Java標準ライブラリのURLConnectionを使ってクライアントを書く。
    uc.setRequestProperty("Content-Type", "application/octet-stream");がないと、サーバでreq.getInputStream()をしたときにEOFExceptionが発生してしまった。

    // 送信するJavaオブジェクトの作成
    SampleObject sampleObject = new SampleObject();
    sampleObject.setDocumentBody(Files.readAllBytes(Paths.get("sampledirectory", "sample.txt")));
    sampleObject.setDocumentName("sample.txt");
    
    // サーバ接続
    URL url = new URL(connectUrl);
    URLConnection uc = url.openConnection();
    uc.setDoOutput(true);
    uc.setRequestProperty("Content-Type", "application/octet-stream");
    
    // リクエスト送信
    OutputStream os = uc.getOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(os);
    oos.writeObject(sampleObject);
    oos.close();
    
    // レスポンス受信
    InputStream is = uc.getInputStream();
    ObjectInputStream ois = new ObjectInputStream(is);
    SampleObject returned = (SampleObject) ois.readObject();

    Copyright © 2016 システム開発メモ All Rights Reserved.

    SAStrutsでHTML5の複数ファイルアップロードを行う

    $
    0
    0

    HTML5で<input type="file" />multiple属性が追加され、ブラウザのファイル選択画面で複数のファイルが一度に選択でき、サーバへの送信も一度にできるようになった。
    SAStrutsはHTML5時代に作られたものではないので、ActionFormでのファイル受取はファイルが一つしか来ないものとして実装されている。フレームワーク部分を拡張してファイルを複数受け取れるようにする。

    jspとActionForm

    sample.jsp

    <input type="file" name="fileDatas" multiple="multiple" />

    SampleForm.java

    public class SampleForm implements Serializable {
    
        private FormFile[] fileDatas; // getter, setter 略
    }

    ファイルを受け取る箇所は、FormFileの配列にする。

    S2MultipartRequestHandler

    リクエスト内容をActionFormに詰めるための前処理としてリクエスト内容の分解と解釈を、SAStrutsで用意されているS2MultipartRequestHandlerクラスが行っている。addTextParameteraddFileParameterというメソッドで、それぞれリクエストの文字列とファイルをActionFormに詰めるための前処理を行っている。文字列のほうは配列に対応できるように全て配列として扱っているのだが、ファイルの方は単体としてしか扱っていないため、addTextParameterを参考にしてaddFileParameterをOverrideする。

    import org.apache.commons.fileupload.FileItem;
    import org.apache.struts.upload.FormFile;
    import org.seasar.struts.upload.S2MultipartRequestHandler;
    
    public class MyS2MultipartRequestHandler extends S2MultipartRequestHandler {
    
        @SuppressWarnings("unchecked")
        @Override
        protected void addFileParameter(FileItem item) {
            String name = item.getFieldName();
            FormFile value = new S2FormFile(item);
    
            FormFile[] oldArray = (FormFile[]) elementsFile.get(name);
            FormFile[] newArray;
            if (oldArray != null) {
                newArray = new FormFile[oldArray.length + 1];
                System.arraycopy(oldArray, 0, newArray, 0, oldArray.length);
                newArray[oldArray.length] = value;
            } else {
                newArray = new FormFile[] { value };
            }
            elementsFile.put(name, newArray);
            elementsAll.put(name, newArray);
        }
    
    }

    S2RequestProcessor

    S2RequestProcessorは、数あるActionのうち何を使うか等を解決し、processActionPerformメソッドで実際にActionのメソッドを呼び出す。SAStrutsでの基幹部分となるクラスだ。
    S2MultipartRequestHandlerS2RequestProcessorprocessPopulateメソッドに呼び出されている。このメソッドの終盤でsetPropertyメソッドが呼ばれ、ActionFormの各変数にS2MultipartRequestHandlerで前処理された値をセットしている。

    S2MultipartRequestHandler#addFileParameterをOverrideしたため、S2RequestProcessor#setProperty内においても、FormFileを単体として扱っていたものが配列として扱わなければいけなくなった。setPropertyで実際に値をセットしている箇所は次の三つのメソッドに切り出されている。

    • setSimpleProperty
    • setMapProperty ※setSimplePropertyから呼ばれる
    • setIndexedProperty

    これらのメソッドでString配列がどのように処理されているのかを確認し、String[]の処理と同様にFormFile[]の処理を実装するようOverrideする。
    継承したクラスとOverrideしたメソッドは以下の通り。既存の処理にelse if (value instanceof FormFile[])を追加している。
    ※setIndexedPropertyについては、String[]の処理は行われていないため、Overrideしていない。

    public class MyS2RequestProcessor extends S2RequestProcessor {
    
        /**
         * 単純なプロパティの値を設定します。
         *
         * @param bean
         *            JavaBeans
         * @param name
         *            パラメータ名
         * @param value
         *            パラメータの値
         * @throws ServletException
         *             何か例外が発生した場合。
         */
        @SuppressWarnings("unchecked")
        @Override
        protected void setSimpleProperty(Object bean, String name, Object value) {
            if (bean instanceof Map) {
                setMapProperty((Map) bean, name, value);
                return;
            }
            BeanDesc beanDesc = BeanDescFactory.getBeanDesc(bean.getClass());
            if (!beanDesc.hasPropertyDesc(name)) {
                return;
            }
            PropertyDesc pd = beanDesc.getPropertyDesc(name);
            if (!pd.isWritable()) {
                return;
            }
            if (pd.getPropertyType().isArray()) {
                pd.setValue(bean, value);
            } else if (List.class.isAssignableFrom(pd.getPropertyType())) {
                List<String> list = ModifierUtil.isAbstract(pd.getPropertyType()) ? new ArrayList<String>()
                        : (List<String>) ClassUtil
                                .newInstance(pd.getPropertyType());
                list.addAll(Arrays.asList((String[]) value));
                pd.setValue(bean, list);
            } else if (value == null) {
                pd.setValue(bean, null);
            } else if (value instanceof String[]) {
                String[] values = (String[]) value;
                pd.setValue(bean, values.length > 0 ? values[0] : null);
            } else if (value instanceof FormFile[]) {
                FormFile[] values = (FormFile[]) value;
                pd.setValue(bean, values.length > 0 ? values[0] : null);
            } else {
                pd.setValue(bean, value);
            }
        }
    
        /**
         * Mapの値を設定します。
         *
         * @param map
         *            マップ
         * @param name
         *            キー名
         * @param value
         *            値
         */
        @SuppressWarnings("unchecked")
        @Override
        protected void setMapProperty(Map map, String name, Object value) {
            if (value instanceof String[]) {
                String[] values = (String[]) value;
                map.put(name, values.length > 0 ? values[0] : null);
            } else if (value instanceof FormFile[]) {
                FormFile[] values = (FormFile[]) value;
                map.put(name, values.length > 0 ? values[0] : null);
            } else {
                map.put(name, value);
            }
        }
    }

    ※ActionForm内でFormFileを配列で定義しているときは、S2MultipartRequestHandlerの拡張をするだけでも動くのだが、FormFileを単体で定義すると動かなくなってしまう。上に書いた拡張を行えば単体でも動く。

    struts-config.xml

    S2MultipartRequestHandlerS2RequestProcessorstruts-config.xmlcontrollerタグで設定することになっているが、独自クラスを継承して作ったため、明示的に設定が必要になる。

    略
    
    <controller
        maxFileSize="250M"
        bufferSize="1024"
        processorClass="jp.co.sample.framework.processor.MyS2RequestProcessor"
        multipartClass="jp.co.sample.framework.handler.MyS2MultipartRequestHandler"
    />
    
    略

    processorClassmultipartClassにそれぞれ継承実装したクラスを設定しているのは見ての通りとして、maxFileSizeについては注意が必要となる。
    maxFileSizeはアップロードされるファイルのサイズを制限する数値だが、ファイルを複数アップロードした時はファイル一つ一つに制限値が適用されるのではなく、複数まとめたサイズに対して適用される。
    ファイルを一つだけアップロードした時には十分だった値も、ファイルを複数アップロードするとなると途端に制限が厳しすぎるということになりかねないので、適切な値に設定し直す必要がある。

    Copyright © 2016 システム開発メモ All Rights Reserved.

    多重度を指定してAjaxを並列実行する

    $
    0
    0

    複数のAjaxを実行すると、各処理の終了を待たずして非同期でAjaxが動くため、同時に処理をしてしまう。
    一つずつ処理をするには、async : falseを設定して同期処理に変更したり、Ajax成功時のコールバック部分でAjaxを再帰呼び出ししたりなどの方法がある。

    大量のAjax処理を全て同時に実行するのは避けつつも、効率化のため一つずつではなくある程度の数を並列化したいとなった場合に、どのように実装するかを書く。

    例題

    今回の処理を実装するにあたり、例題を設定する。

    要件

    • ファイル名と処理状態の二つの列をもつ一覧がある。
    • 処理状態の候補値は次の通り。
      • 準備中
      • 処理中
      • 成功
      • 失敗
    • 実行ボタンを押下すると、一覧を上から順に次のように処理する。
      • 処理状態が「準備中」でない場合、次の行へスキップする。
      • 処理状態を「処理中」へ変更する。
      • ファイル名をAjaxでPOSTし、サーバ側である程度時間のかかる処理を行う。
      • Ajaxのレスポンスでjson.result == 'OK'と返ってくれば処理状態を「成功」に、返らなければ「失敗」に変更する。
    • 一覧のすべての処理が完了したら、alert('完了');を実行する。
    • 処理の多重度を指定できるようにして、処理速度を高速化する。

    実装

    一覧部分の実装

    Ajaxの多重度を指定した並列実行という本筋とは関係ないが、まずはじめに一覧部分がどのようになっているのかを示す。
    一覧はjqGridで実装されているものとする。

    $(function(){
        $('#grid1').jqGrid({
            datatype: 'json',
            mtype: 'GET',
            url: '/loadFiles',
            rowNum: '',
            shrinkToFit:false,
            height: 160,
            width: 788,
            hidegrid : false,
            viewrecords: false,
            altRows : true,
            altclass : 'jqgridAltRows',
            colNames:[ // ヘッダ行
                        'ファイル名',
                        '処理状態'
                    ],
            colModel :[ // 明細行
                        {name:'file', resizable:true, sortable:false, width:520, align:'left'},
                        {name:'status', resizable:true, sortable:false, width:185, align:'left'}
                     ],
            gridComplete: function() {
                    },
        });
    });

    多重度の制御機能

    多重度の制御機能を実現するため、変数の状態を記憶し、また変数をprivateにできるクロージャを使ったクラスベースオブジェクト指向で実装する。

    ファイル一覧が処理対象なので、ファイル一覧の状態を管理し、またファイル一覧に対しての処理を行うメソッドをもつクロージャが必要になる。
    FileManager(concurrentLimit)と定義し、引数concurrentLimitで、多重度の限界値を指定できるようにする。

    function FileManager(concurrentLimit) {
        var fileList = [];
        var statusList = [];
        var ids = $('#grid1').jqGrid('getDataIDs');
        for (var i = 0; i < ids.length; i++) {
            var rowData = $('#grid1').jqGrid('getRowData', ids[i]);
            fileList.push(rowData['file']);
            statusList.push(rowData['status']);
        }
    
        var registerAjax = function(idx) {
            var file = fileList[idx];
            $.ajax({
                type : 'post',
                dataType : 'json',
                url : '/register',
                cache : false,
                data : {
                    "file" : file
                },
                success : function(json){
                    if (json.result == 'OK') {
                        changeStatus(idx, '成功');
                    } else {
                        changeStatus(idx, '失敗');
                    }
                },
                error : function(jqXHR ,test,error){
                    changeStatus(idx, '失敗');
                }
            });
        };
    
        var changeStatus = function(idx, txt) {
            statusList[idx] = txt;
            $('#grid1').jqGrid('setCell', ids[idx], 'status', txt);
        };
    
        return {
            // return true when limited.
            limited : function() {
                var count = 0;
                for (var i = 0; i < statusList.length; i++) {
                    if (statusList[i] == '処理中') {
                        count++;
                        if (count >= concurrentLimit) {
                            return true;
                        }
                    }
                }
                return false;
            },
    
            // return idx. If nothing, return -1.
            get : function() {
                var idx = $.inArray('準備中', statusList);
                if (idx == -1) {
                    return -1;
                }
                changeStatus(idx, '処理中');
                return idx;
            },
    
            execute : function(idx) {
                registerAjax(idx);
            },
    
            // return true when finished.
            finished : function() {
                for (var i = 0; i < statusList.length; i++) {
                    if (statusList[i] == '準備中' || statusList[i] == '処理中') {
                        return false;
                    }
                }
                return true;
            },
        };
    }

    各メソッドの説明

    メソッド 説明
    limited 一覧の中で「処理中」の個数を数え、多重度の限界値に達しているかを判定する。
    get 一覧から一番最初に出てくる「準備中」の行のIndexを返す。「処理中」に変更する。
    execute Ajaxを実行する。成否によって「成功」、「失敗」に変更する。
    finished 一覧の処理状態を全て確認し、Ajaxがすべて終了したかを判定する。

    FileManagerにおけるエッセンスは多重度を判定するlimitedだ。

    ループ & スリープによる呼び出し

    FileManagerをループとスリープを使いながら呼び出す関数を作る。実行ボタンはクリック時、この関数を呼び出す。

    まずはじめに、多重度つきの並列実行のエッセンスに着目するため、今回の要件の内、「一覧のすべての処理が完了したら、alert('完了');を実行する。」を抜かして実装する。

    var manager;
    function register() {
        var sleep = 1000;
        manager = FileManager(3);
        var loop = setInterval(function() {
            if (!manager.limited()) {
                // 処理対象取得
                var idx = manager.get();
                // 処理対象がなくなれば
                if (idx == -1) {
                    clearInterval(loop);
                    return;
                }
    
                // 登録処理実行
                manager.execute(idx);
            }
        }, sleep);
    }

    JavaScriptにはsleep関数がないので、setInterval, clearIntervalを使ってループとスリープ、そしてbreakを実現している。
    ループしながら並列数の限界値を超えていないかどうかlimitedで確認し、超えていなければget, executeと処理を行っていく。executeは非同期処理なのでレスポンスが返る前にメソッドが終了する。これでスリープする時間分のタイムラグはあるものの、並列実行ができる。

    次に、抜かした要件部分も実装を行う。「ajax処理終了判定」をclearInterval(loop);後に実行する。いつレスポンスが返ってくるかわからないので、ループとスリープを使って判定をしなければいけない。構造は外側を囲っているループと同じ。

    var manager;
    function register() {
        var sleep = 1000;
        manager = FileManager(3);
        var loop = setInterval(function() {
            if (!manager.limited()) {
                // 処理対象取得
                var idx = manager.get();
                // 処理対象がなくなれば
                if (idx == -1) {
                    clearInterval(loop);
    
                    // ajax処理終了判定
                    var finishLoop = setInterval(function() {
                        if (manager.finished()) {
                            clearInterval(finishLoop);
                            alert('完了');
                        }
                    }, sleep);
    
                    return;
                }
    
                // 登録処理実行
                manager.execute(idx);
            }
        }, sleep);
    }

    以上で多重度を指定してAjaxを並列実行する処理は完成である。
    理解補助のためsleepメソッドのあるJavaで書くと、次のような処理になる。

    @SneakyThrows
    void register() {
        int sleep = 1000;
        manager = new FileManager(3);
    
        while (true) {
            if (!manager.limited()) {
                // 処理対象取得
                int idx = manager.get();
                // 処理対象がなくなれば
                if (idx == -1) {
                    // ajax処理終了判定
                    while (true) {
                        if (manager.finished()) {
                            System.out.println("完了");
                            return;
                        }
                        Thread.sleep(sleep);
                    }
                }
    
                manager.execute(idx);
            }
            Thread.sleep(sleep);
        }
    }

    Copyright © 2016 システム開発メモ All Rights Reserved.

    Viewing all 100 articles
    Browse latest View live