20201207のJavaに関する記事は17件です。

Javaでプログラムしてますけど何か?

前書き

最近オワコンとか言われているJavaプログラマーです。
オワコンと言われても言われなくてもぜんぜん嬉しくないし・・・
Javaで仕事でやって行くためにやれるだけやってみようっていうコーナーです。
下記の書籍を主にソースコードリーディングをしていこうと思います。

エディター

使用エディターは一旦見るだけなので Visual Studio Codeにする。

参考書籍等

Javaフレームワーク開発入門
https://www.amazon.co.jp/dp/B00U17815S/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1

ソースコードリーディングから学ぶ Javaの設計と実装
https://www.amazon.co.jp/ソースコードリーディングから学ぶ-Javaの設計と実装-WINGSプロジェクト-佐藤-匡剛/dp/477412950X

シラバス

●目次
1章 フレームワークとは
2章 メタプログラミングを学ぶ
3章 デザインパターンを学ぶ
4章 DI×AOPを学ぶ
5章 実習編
6章 フレームワーク作成時に考慮すべき点

アクション1

ソースコードをダウンロードして見てみる。
4章のソースが読めるようなのでしばらく読む。
→ Springを学ぶ時に吸収すればいいような気がする。
5章を中心に読むことにする。

http://dodododo.jp/book/9784797353402/index.html

アクション2

Strutsを動かしながら内部に抱えているサンプルを動かしたりしながら
ソースを見て見ようかと思う。 動かすだけならそれほど難しくないはず、Eclipseでデバックかけられれば
いいが、ベストエフォートでやることにする。
→ http://dbflute.seasar.org/ja/tutorial/handson/section01.html
しばらくDBfluteのハンズオンを読ませていただく。こういう例を出してくれてるってありがたいので
よく読もうと思います。本当は実際に問題を解いたりするべきなんでしょうが、すみません。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ArchUnit 実践:あなたの common パッケージは 本当に common ?

// 実行環境
* AdoptOpenJDK 11.0.9.1+1
* JUnit 5.7.0
* ArchUnit 0.14.1

アーキテクチャテストのモチベーション

特定の業務知識をもたない汎用的な値オブジェクトクラスやユーティリティクラスを common パッケージに置いていたら1、いつのまにか common パッケージ以外に依存してしまっていたなんてことはないでしょうか?これらの汎用クラスは外部から依存されることはあっても、外部に依存してはいけないはずです。

アーキテクチャテストの実装

package com.example;

import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import org.junit.jupiter.api.Test;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;

class ArchitectureTest {

    // 検査対象のクラス
    private static final JavaClasses CLASSES =
            new ClassFileImporter()
                    .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
                    .importPackages("com.example");

    @Test
    void commonパッケージは特定の業務ドメインに依存しない() {
        noClasses().that().resideInAPackage("com.example.domain.common..")
            .should()
            .dependOnClassesThat(new DescribedPredicate<>("common でないパッケージに属する") {
                /**
                 * @param clazz 依存先のクラス
                 * @return 依存先のクラスが common でないパッケージに属するクラスである場合、true
                 */
                @Override
                public boolean apply(final JavaClass clazz) {
                    if (! clazz.getPackageName().startsWith("com.example")) {
                        // サードパーティーライブラリなどへの依存はOK
                        return true;
                    }

                    return ! clazz.getPackageName().startsWith("com.example.domain.common");
                }
            })
            .check(CLASSES);
    }
}

  1. そもそも common というくくりでパッケージングすることの是非も... 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【受取法】APIのレスポンスがJson形式の場合

APIのレスポンスがJson形式の場合の受取り方

SpringBootでは以下のように受け取る

@JsonProperty("image_urls")
private List<String> imageUrls;

後はGetter、Setterで値を操作しましょう

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Androidで起動時、deplhiで作成したアプリを自動起動にする。

delphi 2020 アドベントカレンダー 7日目です

こんにちは
やましょうです。

Delphiで作ったアプリをAndrid起動時に自動起動。

1.まず検索

1.classes.dexを入れ替えてパッケージを作る方法
https://dannywind.nl/auto-start-delphi-xe5-android-app-after-boot/

2.jar ファイルを自分でつくる方法
https://blog.csdn.net/tanqth/article/details/74357209
お金で解決がここは中華口座が必要。

結果jarファイルで行う方が無難だと思う。

2.javaでソースを仕方なく書く

package com.qa65000;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Intent;
import android.content.Context;
import android.util.Log;

public class BootReceiver extends BroadcastReceiver
{

    @Override
    public void onReceive(Context context, Intent intent) 
    {
            Log.d("test_TAG", "onRecive()");
        if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
            Log.d("test_TAG", "Booo........t Complated()");
           Intent launchintent = new Intent();
            launchintent.setClassName(context, "com.embarcadero.firemonkey.FMXNativeActivity");           
            launchintent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivity(launchintent);  
        }
    }
}

3.batファイルを作ってコンパイルする。

echo.
echo Compiles your Java code into classes.dex
echo Verified to work for Delphi XE6
echo.
echo Place this batch in a java folder below your project (project\java)
echo Place the source in project\java\src\com\dannywind\delphi
echo If your source file location or name is different, please modify it below.
echo This assumes a Win64 system with the 64-bit Java installed by the Delphi XE6 
echo installer in C:\Program Files\Java\jdk1.70.0_25
echo.

setlocal

set ANDROID_JAR="C:\Users\Public\Documents\Embarcadero\Studio\20.0\PlatformSDKs\android-sdk-windows\platforms\android-26\android.jar"
set DX_LIB="C:\Users\Public\Documents\Embarcadero\Studio\20.0\PlatformSDKs\android-sdk-windows\build-tools\28.0.2\lib"
set EMBO_DEX="C:\Program Files (x86)\Embarcadero\Studio\20.0\lib\android\debug\classes.dex"
set PROJ_DIR=%CD%
set VERBOSE=0
set JAVASDK="C:\Program Files\Java\jdk1.8.0_60\bin"
set DX_BAT="C:\Users\Public\Documents\Embarcadero\Studio\20.0\PlatformSDKs\android-sdk-windows\build-tools\28.0.2\dx.bat"

echo.
echo Compiling the Java source files
echo.
pause
mkdir output 2> nul
mkdir output\classes 2> nul
if x%VERBOSE% == x1 SET VERBOSE_FLAG=-verbose
%JAVASDK%\javac %VERBOSE_FLAG% -classpath %ANDROID_JAR% -d    bin\classes src\com\qa65000\BootReceiver.java
%JAVASDK%\javac %VERBOSE_FLAG% -classpath %ANDROID_JAR% -d output\classes src\com\qa65000\BootReceiver.java

jar cvf bin\BootReceiver.jar -C bin\classes jp

echo. ここから不要だと思う(classes.dexを書き換える場合のコンパイル)
echo Creating jar containing the new classes
echo.
pause
mkdir output\jar 2> nul
if x%VERBOSE% == x1 SET VERBOSE_FLAG=v
%JAVASDK%\jar c%VERBOSE_FLAG%f output\jar\test_classes.jar -C output\classes com

echo.
echo Converting from jar to dex...
echo.
pause

mkdir output\dex 2> nul
if x%VERBOSE% == x1 SET VERBOSE_FLAG=--verbose
call %DX_BAT% --dex %VERBOSE_FLAG% --output=%PROJ_DIR%\output\dex\test_classes.dex --positions=lines %PROJ_DIR%\output\jar\test_classes.jar

echo.
echo Merging dex files
echo.
pause
%JAVASDK%\java -cp %DX_LIB%\dx.jar com.android.dx.merge.DexMerger %PROJ_DIR%\output\dex\classes.dex %PROJ_DIR%\output\dex\test_classes.dex %EMBO_DEX%
echo.
echo Now use output\dex\classes.dex instead of default classes.dex
echo And add broadcastreceiver to AndroidManifest.template.xml
echo.

:Exit

endlocal


                                      javac -classpath "C:\Users\Public\Documents\Embarcadero\Studio\15.0\PlatformSDKs\adt-bundle-windows-x86-20131030\sdk\platforms\android-19\android.jar" -d bin\classes src\com\example\hello\Hello.java

C:\Program Files\Java\jdk1.8.0_60\bin"\javac  -Xlint:all -class

4.作成したBootReciver.jarをプロジェクトに追加する。

(10.3で64bitは追加できない。泣)

SnapCrab_NoName_2020-12-7_20-3-8_No-00.png

.5 コンパイルして転送する。

たぶん動くと思う(夏にやった奴なのでうごかなかったらごめんなさいです。)

本家のコードが
一応気になる人は
https://www.youtube.com/watch?v=4_CkU9L2mCo&t=173s
をみてください。

以上
やましょうでした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Java】文字列を置換する方法

プログラミング勉強日記

2020年12月7日
文字列を置換するときにはreplaceメソッド、replaceAllメソッド、replaceFirstメソッドを使うが、それぞれの使い方をまとめる。

replaceメソッド

 replaceメソッドは対象の文字列から第1引数で指定した文字列を検索して、一致した文字列を第2引数で指定した文字列で置換する。置換後の文字列を戻り値として返す。

replaceメソッドの書き方
対象の文字列.replace(置換される文字列, 置換する文字列)

replaceメソッドのサンプルコード

サンプルコード
String str1 = "abc123abc123";
String str2 = str1.replace("abc", "0");
System.out.println(str2); 
実行結果
01230123

replaceAllメソッド

 正規表現を使う場合にreplaceAllメソッドを使う。replaceAllメソッドは対象の文字列から第1引数で指定した正規表現のパターンで文字列を検索して、一致した文字列を第2引数で指定した文字列で置換する。置換後の文字列を戻り値として返す。
 正規表現についてはこちらの記事で詳しく扱っている。

replaceAllメソッドの書き方
対象の文字列.replaceAll(正規表現, 置換する文字列)

replaceAllメソッドのサンプルコード

サンプルコード
String str3 = "abc123abc123";
// "[a-z]+" : 小文字のアルファベット
String str4 = str3.replaceAll("[a-z]+", "0");
System.out.println(str4); 
実行結果
01230123

replaceFirstメソッド

 replaceFirstメソッドは対象の文字列の中から最初に一致した文字列だけを置換する。対象の文字列から第1引数で指定した正規表現のアターンで文字列を検索して、最初に一致した文字列のみを第2引数で指定した文字列で置換する。置換後の文字列を戻り値として返す。

replaceFirstメソッドの書き方
対象の文字列.replaceFirst(正規表現, 置換する文字列)

replaceFirstメソッドのサンプルコード

サンプルコード
String str5 = "abc123abc123";
// "[a-z]+" : 小文字のアルファベット
String str6 = str5.replaceFirst("[a-z]+", "0");
System.out.println(str6); 
実行結果
0123abc123

まとめ

 Javaで文字列を置換する方法を紹介した。

  • replaceメソッドは正規表現を使わない
  • replaceAllメソッドとreplaceFirstメソッドは正規表現を使う
  • すべて置換する場合はreplaceAllメソッドを使う
  • 最初の1つだけ置換する場合はreplaceFirstメソッドを使う

参考文献

2分で理解!Javaで文字列を置換するreplaceFirst,replaceAll【Stringクラス】
Javaで文字列を置換する:replace(), replaceAll(), replaceFirst()

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【業務系】世の中に逆行するJavaのWebアプリでの画面フレームワーク選定

これは何?

  • Java Advent Calendar 2020 を見て、記事の少なさ (後で見たら参加者増えてた!)、よい記事なのにLGTMの少なさに驚愕して、少しでもJava言語を盛り上げたく記事を書きます。
  • Webな業務アプリにおいて画面を作る画面系フレームワークの選択肢について、偏見たっぷりに書いてみます:relaxed:
    • 想定読者:Javaで画面フレームワーク?ReactとかVueとかじゃねーの?っていう人向け。

業務系 && Webアプリ で画面フレームワークといったら?(前置き)

普段はBtoBでJavaで業務アプリを開発しています。Spring Framework(Boot)だったり、JavaEE(JakartaEE)にお世話になっております。
今時の定番でいえば、やはり、Spring MVCでしょうか?
更にモダンになると、RESTfulAPI + Vue.js とか ReactJs とか Angularみたいな組合せになるのでしょうか?

ココではあえて、世の中のトレンドから外れた選択肢の紹介と考察を加えてみたいと思います。
そして、ここであえて、独断と偏見により、世の中に逆行する点上げておきます。(選定理由に大きく影響します)

  • 画面はなるべくJavaの世界で完結したい。そうSwingやAWTのように!
  • JavaScriptのエコシステムとJavaScriptが大嫌い。
    • 理由は、「Java言語と混同されがちなネーミング(Javaを汚している!)」、「進化が早さすぎてちょっと前の開発エコシステムが維持できないこと」、「TypeScriptによってだいぶマシになったものの書き方のバリエーションや言語仕様の理解がしにくく、いつまで経ってもちゃんと書ける気がしない」の三点です。異論は認めない!
  • Spring MVCのようにアクションベースの画面設計が大嫌い。楽して画面を作りたいので、コンポーネントベースの画面フレームワークが好きです。むしろ、Vue.jsやReactもコンポーネントベースじゃないですか!?

業務系アプリ(と言ったも色々ありますが)の画面的特徴として下記のパターンでほぼほぼ画面要求を網羅できると感じています。

  • 一覧(&検索)画面 ・・・ データの一覧と検索
  • 詳細編集画面 ・・・ データの一覧から遷移した個別の詳細画面。編集(削除)もセットでできることも
  • 新規作成画面 ・・・ データの新規作成画面。

もちろん例外もありコールセンター業務のように一度に様々な情報を表示することが必須となる画面、グラフがいっぱい表示されたりするダッシュボード的な画面など当てはまらないケースもあったりしますが、基本的にはデータに対する検索、更新、削除、追加の観点が揃っている画面がセットで開発されていく感じだと思います。

上記のような嗜好と業務系アプリ画面の特徴を踏まえて、オープンソースの画面フレームワークを紹介します。

定番:Java Server Faces(JSF)

一言で説明しろ: とにかく定番。デザインはHTMLで書いて、ロジックはJavaで書くみたいな感じ。HTMLの知識は必須。

もうちょっと詳しく:
JSFは、仕様なので、実装として、MyFaces や Mojarra といった実装があります。(なので他と違って公式サイトといった様なものを挙げにくい事情があります)
JSFは、画面の構造をサーバ側に保持しておき状態変化に応じて常に同期(更新)していくステートフルな仕組みです(クライアントに置く方式もありますが構造を保持する点は同じ)。そのため、ボタン押下や画面更新の都度に、サーバ側との通信が発生することでVue.jsやReactJsに比べて動作はもっさりしますし、BtoCのようなスケーラブルなアプリケーションで利用するには工夫が必要(セッションを共有化するとかセッションに応じてアクセスするアプリケーションサーバを固定化する等)なフレームワークです。ある程度利用者が限定できるBtoBの業務システムなどに向いていると考えています。
また、悪いことにドキュメントが少なく(特に日本語では)、とっつき易いフレームワークか、と言われれば否です。とはいえ、StackOverFlowのコミュニティは活発で、質問すればだいたい数時間でまともな返答がかえってきて頼もしいです。

JSF単独で利用するというより、UIライブラリを組み合わせて使うのが定番です。
UIコンポーネント PrimeFaces
画面テーマ: AdminFaces

デモ

第二候補: Vaadin

一言で説明しろ: Web版Swing/AWT or Android画面の作り方のWeb版(基本的に画面もロジックもJavaで完結!)

もうちょっと詳しく:
2007年頃までは、裏側はGWTで構築されていたが、近年Vue.jsとかをバックエンドに持つようになった。
Javaで書いた内容が、Vue.jsとかのフレームワークの実装に変換されてWebアプリとして構成される感じ。
なので、動作にはnodeとか(大嫌いな)javascirptのエコシステムは裏側で動いている。

JSFに比べてクライアントサイドで完結する部分があり、動作が速い。
元々準備されているUIコンポーネントが、モダンで、いい感じで、キレイ!テンション上がる。

でも、UIコンポーネントにハマらない画面を作ろうとすると途端に大変に。コンポーネントの良さが失われる。
その点、JSFの方が、最悪なんとかなる感じはしている。

デモ:
https://demo.vaadin.com/invoice-editor-app/

第三候補: CUBA Platform

一言で説明しろ: Vaadinの皮を被った業務用アプリケーションフレームワークと開発エコシステム(もちろんJava)

もうちょっと詳しく:
VaadinのUIをもう少し業務アプリケーションで使えるようにカスタマイズした感じ。
根っこの部分は、Vaadinと変わらない。

Spring + Vaadin をベースにして、業務アプリのためにがっつりフレームワークとして作り込んでいるイメージ。
また、業務アプリ開発に対応したGUIツールをIntellij IDEAのプラグインとして提供していて、かなり使えそう。
あいにく、ドキュメント群が英語しかないのが難点だが・・・

個人的にはイチオシしたい!
画面フレームワークという立ち位置で語るのは少し微妙で、適切な立ち位置は、業務アプリケーションフレームワークという感じだろうか?
例えば、下記のような面々が競合になるだろう。

  • JHipster - アプリのひな形生成
  • Apache Isis - 業務アプリのUIのひな形生成
  • OpenXava - EntityからのCRUDアプリケーションの生成
  • iPLass - 業務システム系の開発プラットフォーム

デモ:

落選?: GWT:Google Web Toolkit

一言で説明しろ: 一昔前にGoogle App Engineの登場と共に一斉を風靡したあのフレームワーク。書き方的には、WicketやVaadinに近いか?

もうちょっと詳しく:
最近のリリースもあるし、全然廃れていない。悪くない。

ただ、マイナス点として・・・

  • Spring Frameworkと組合せて動かす場合に、情報も少なく、かなりハマってしまいきつかった・・・
  • 情報量が少ない
    • きっと知る人は知る的な状態?

デモサイト:

落選?:Apache Wicket

一言で説明しろ:
JSF+Vaadinを足して2で割った様なフレームワーク。JSFよりはSwing的にコンポーネントをコードで書いていくやり方だが、若干のHTMLを書く必要がある。

もうちょっと詳しく:
コードの見通し、書き方、いずれをとっても、悪くない。むしろ使いたい。(むしろJSFより好き)
でも、JSFのPrimeFacesに相当するような実用的なUIコンポーネントライブラリなどが乏しく、
業務的に使っていくには周辺ライブラリを整備したりと、かなり時間を掛けて手を入れていく必要があると感じています。
エコシステムの成熟がイマイチ感がある。

知らないだけかもしれない・・・が、それなりにがっつり調べた過去があり、採用を見送った経緯がある。

デモ:

まとめ

モダンな選択肢以外にも手堅い、面白い選択肢があったのではないでしょうか?
もちろん、BtoCのWeb系サービスのようにUI/UXの要求が厳しいところに対して使うのは難しい側面があります。
銀の弾丸はありません。選択肢は多く知っておいた方が良いのではないかと思う次第です。

それでは。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javaプログラムがパソコンに理解され実行されるまでの流れ

はじめに

ザックリとした概要を掴みやすいように、省いた表現や正確でない記載が含まれております。正確な詳細な情報を好む方は別の記事を参照することをお勧めします。

目的

Javaプログラムがどのようにして動いているのか概要を把握する

概要

Javaで書いたプログラムは当然人間が書いたものですので、そのままだとパソコンは理解することができない。そこでパソコンが理解できるようにJavaファイルを変換する必要がある。

変換していく順番は下記の通り。

  • Javaファイル(人間が書いたソースコード)
  • クラスファイル(コンパイラが生成した中間コード、特定の環境に依存しない)
  • ネイティブコード(コンピュータが理解できるマシン語)

JavaのソースコードはJavaのコンパイラを用いてクラスファイルを生成する。このクラスファイルを元にJVM(JITコンパイラ)がネイティブコードに変換しながらプログラムを実行する。

JVMとは

Java仮想マシン(以降、JVM)はJavaで作ったプログラムを動かすための計算機のようなもの。クラスファイルを各プラットフォームに応じたネイティブコードに変換して実行してくれる。

JVMがプログラムを処理する流れ

JVM1.png

JVMはスタックを中心に処理を進める。処理をする際は複数のスレッドがあり、各スレッドにはプログラムカウンタとスタックを1つづつ用意する。

また、各スタックは複数のフレームでできており、各フレームはローカル変数とオペランドスタックで構成されている。

スレッドとスタック上の動き

JVM2.png

Javaのスレッドを起動すると、まず初めにJVMはメモリ上にスタックを割り当てる。スタックとはデータを後入れ先出し(LIFO: Last In First Out)する構造のメモリ領域のことを指す。

そして、ディスク上にあるクラスファイルからメソッドの内容やローカル変数をコピーして、このスタックにプッシュする。この時プッシュされたデータの一つ一つの塊のことをフレームと呼ぶ。

メソッドの起動順序

JVM3.png

フレームBのメソッドBにメソッドCを呼び出す記述があったとする。するとJVMはメソッドCが記述されたフレームCを新たにプッシュしてその処理を実行する。

このようにJVMはスタックの一番最後に追加したフレーム(以降、カレントフレーム)を処理するようにできている。

そして、カレントフレームの処理がすべて終わるとそのフレームを削除して、呼び出し元のフレームの処理を再開する。

引数と戻り値

JVM4.png

例えば、フレームBのメソッドの内容に別メソッドを呼ぶ処理が記述されていたとする。その場合、フレームCがスタックにプッシュされますが、このとき呼び出し元であるフレームBのローカル変数を参照したいとしても、フレームを越えてデータを参照することはできない。

そのため、フレームCを作成する際にフレームBのローカル変数のデータをフレームCにコピーしておく。このフレームCからフレームBへのデータのコピーのことを「引数」と呼ぶ。

また、フレームCの処理結果をフレームBにて使用したい場合については、フレームCを削除する前にフレームBに結果をコピーしておく。このフレームCからフレームBへの結果のコピーのことを「戻り値」と呼ぶ。

参照渡し

JVM5.png

インスタンスなどの大きなオブジェクトについては生成するとヒープに置かれる。フレーム間でこのインスタンスを受け渡す場合はヒープ上の番地(以降、参照)を渡している。このことを「参照渡し」と呼ぶ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javaの "IncompatibleClassChangeError: Found class ... , but interface was expected" というエラー

大学のJavaの課題をやっていて、インターフェースを抽象クラスに書き直した際に遭遇したエラーです。

ディレクトリ

.
├── Main.java
├── Main.class
├── A.java
├── A.class
├── B.java
└── B.class

Aがインターフェース、BがAを実装したクラス、Mainが動作確認用のクラスです。
この後、Aをインターフェースではなく抽象クラスに書き直してコンパイルし、実行しようとしました。

$javac Main.java
$java Main

すると次のようなエラーが出ました。

Exception in thread "main" java.lang.IncompatibleClassChangeError: Found class A, but interface was expected

このエラー文を和訳すると「Aというクラスは見つかったけど、それはインターフェースじゃないとダメだよ」となります。そしてIncompatibleClassChangeErrorというのは

クラス定義に互換性のない変更があった場合にスローされます。現在実行中のメソッドが依存しているクラスの定義が、実行開始後に変更されています。
 (Java(tm) Platform, Standard Edition 8 API仕様 から引用)

解決方法

A.class, B.class, Main.class の3つのファイルを削除しコンパイルするとうまく実行できました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Java】IF文のネストを浅くする方法

目的

IF文のネストを浅くする方法のまとめ

概要

IF文がネストしていると様々なデメリットが発生する。

  • 可読性が落ちる
  • テスト工数が増える
  • バグを埋め込みやすくなる

下記のテクニックを駆使してIF文のネストを浅くする

  • 条件の逆転
  • 条件の分割
  • ド・モルガンの法則を利用
  • アーリーリターンを利用

題材のソースコード

Sample.java
if(hoge != null && hoge != "")) {
    if(fuga == 1){
        // 処理A
    }else{
        // 処理B
    }
}else{
    // 処理C
}

条件の逆転

Sample.java
if(!(hoge != null && hoge != ""))) {
    // 処理C
}else{
    if(fuga == 1){
        // 処理A
    }else{
        // 処理B
    }
}

逆転させただけだとあまり変わり映えが無い。

条件の分割

Sample.java
if(!(hoge != null && hoge != ""))) { // -- (1)
    // 処理C
}
if(fuga == 1){ // -- (2)
    // 処理A
}else{
    // 処理B
}

IF文を分割する。ELSEの中からIF文を外に出す。この段階でネストが消える。

ド・モルガンの法則を利用(※対象IF文箇所のみ抜粋)

否定(!)が多いと可読性が落ちるのでド・モルガンの法則を利用して、否定(!)を減らす。

Sample.java
if(!(hoge != null && hoge != ""))) {
Sample.java
// 1.否定(!)を外に出して比較演算子を && → || に変更
if(!(!(hoge == null || hoge == ""))) {
Sample.java
// 2.重複している否定(!)を削除
if(hoge == null || hoge == "") {
Sample.java
if(hoge == null || hoge == "") {
    // 処理C
}
if(fuga == 1){
    // 処理A
}else{
    // 処理B
}

アーリーリターンを利用

Sample.java
if(hoge == null || hoge == "") {
    // 処理C
    return;
}
if(fuga == 1){
    // 処理A
}else{
    // 処理B
}

もし、処理Cを実施後、後続処理を実施したくない場合は処理Cの最後にreturn文を入れればよい。このことをアーリーリターンと呼ぶ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Java起動オプション標準化

この記事について

長年、Javaを使ったIT開発の仕事をしていますが、Javaの起動オプション決定は毎回試行錯誤している気がします。
GCの特性毎にHeap,Metaspace,New領域(Eden/Survivor),Old領域などの複雑なメモリ管理を調べつつ、-Xオプションを設定してみる
この作業、10年前から変わってない気がします。
細かいJavaのメモリ管理については別の記事に譲るとして、ここでは完結に
ここだけいじればオーケー
的な情報を示したいと思います。
ご意見などあれば、是非コメントお願いします。

前提

ここでは、常駐して利用されるJavaVM(OpenJDK 8u20以降のHotspot JavaVM)の起動オプションを想定しています。
常駐とは・・・

  • アプリケーションを起動したら、停止命令を送るまで起動したままの物
  • 代表例はTomcatやJetty,WildFlyなどのアプリケーションサーバ
  • Spring-bootを使ったアプリケーション
  • もちろん自作のJavaSEアプリケーションでも常駐するものならOK 

対象外を言えばよかったかも

  • 一回実行して、処理が終了したらJavaVMも終了するものは対象外

商用環境JavaVM 推奨起動オプション

ここでは起動オプション(例)を先に示し、それぞれの項目の意味と設定内容を説明します。
この設定をベースにヒープサイズファイル出力先ぐらいを変更(チューニング?)すればよいでしょう。

起動オプション決定方針

JavaVM起動オプション様々な設定が存在しますが、可能な限りエルゴノミクス・デフォルト(※)を利用し、最低限の設定を行うものとします。
エルゴノミクス・デフォルト:JavaVMが利用環境(CPUコア数やOS、物理メモリ量などの情報)から最適な値を自動設定します。要するに、できる限りJavaVMにおまかせすると言うことです。

JavaVMオプション(例)

> java -Xms8192M
       -Xmx8192M
       -XX:+CrashOnOutOfMemoryError
       -XX:+HeapDumpOnOutOfMemoryError
       -XX:HeapDumpPath=/AAA/BBB/CCC/hprof-dumps
       -Xlog:gc:file=/AAA/BBB/CCC/gclogs/gc_%p_%t.log:time:filecount=10:size=100M
    (以降、メインクラス名 アプリケーション固有の起動引数)

起動オプションの内容

  • GCアルゴリズム=G1GCを使用(デフォルト)
    • 最大停止時間目標 200ms(デフォルト)
    • metaspace,SurvivorRatio/NewRatioなどは、JavaVMにおまかせ
  • ヒープサイズ8192Mバイト 最大=最小
  • OutOfMemoryErrorが発生した時はプロセスを停止し、クラッシュレポートを出力
  • OutOfMemoryErrorが発生した時に、ヒープダンプを出力
  • ヒープダンプの出力先:/AAA/BBB/CCC/hprof-dumps
  • gcログ関連
    • gcログファイルは /AAA/BBB/CCC/gclogs/に格納する
    • ファイル名はgc_[プロセスID]_[時間].log
    • 1ファイルのサイズ=100Mバイト
    • ファイル世代数=10 でローテーション
    • gcログに各行にタイムスタンプを出力する

JavaVMオプションまとめ

よく使うJavaVM起動オプションの説明と、設定値の決め方を以下にまとめます。

起動オプション(-Xlog:gc以外)

起動オプション 設定値 設定値の例 説明(決め方)
-Xms ヒープサイズ最小 8192M 初期のヒープサイズ
物理メモリの量からOSが利用するメモリ量を引いた値を設定 -Xmxと同じ値にする
-Xmx ヒープサイズ最大 8192M 最大ヒープサイズ
物理メモリの量からOSが利用するメモリ量を引いた値を設定 -Xmsと同じ値にする
-XX:MaxGCPauseMillis 停止してもよい時間(ミリ秒) 200 GCによる最大停止時間の目標。省略時は200ms。GCが頻発する場合は増やしてみる。
-XX:+CrashOnOutOfMemoryError - - OutOfMemory発生時にプロセスを終了し、クラッシュレポートを出力する。これはいつも設定すべき
-XX:+HeapDumpOnOutOfMemoryError - - OutOfMemory発生時にヒープダンプを出力する。ヒープが大きい場合は、ヒープダンプファイルも大きくなるため、出力先に注意する。見るつもりがないなら出力しない
-XX:HeapDumpPath= ヒープダンプの出力先 /.../hprof-dumps 上記ヒープダンプの出力先
-Dcom.sun.management.jmxremote= JMXリモート接続の許可 true 監視ツールなどからJMXリモート接続する場合に設定
-Dcom.sun.management.jmxremote.port= 上記接続ポート 7091 上記接続時のポート番号
-Dcom.sun.management.jmxremote.authenticate= 上記接続時に認証するか false 上記接続時に認証するか
-Dcom.sun.management.jmxremote.ssl= 上記接続時にSSL通信するか false 上記接続時にSSL通信するか

起動オプション(-Xlog:gc)詳細にgcログを出力したい場合は -Xlog:gc*

-Xlog:gc:[ファイル名]:[修飾]:filecount=[世代数]:size=[ファイルサイズ]

設定項目 設定例 説明(決め方)
[ファイル名] /gclogs/gc_%p_%t.log /gclogsディレクトリに格納
%p=プロセスID,%t=タイムスタンプでファイル名を付ける
 コンテナ環境で実行する場合は、"stdout" と記載することにより、標準出力を利用することができる
[修飾] time,tags time=gcログにタイムスタンプを出力
tags=ログにタグを設定 これはコンテナ環境などで標準出力を利用する際に便利
[世代数] 10 ファイルの世代数
[ファイルサイズ] 100M 1ファイルのサイズ

終わりに

この記事にある設定値だけであれば楽に理解できると思います。このほかに監視エージェントへの接続設定があるかもしれませんが、それ以外の難しい設定を、意味が分からず呪い(まじない)のように使うのだけは止めましょう。状況を悪化させるだけでなので、おとなしくアプリケーションの実装を見直すべきだと思います。(長い経験から)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

デシジョンテーブルの概要把握と書き方の基本

目的

デシジョンテーブルの概要把握と書き方の基本のまとめ

概要

デシジョンテーブルとは「条件」と「条件の組み合わせに応じた動作」を一覧表にまとめたもの。

下記タイミングでの利用が考えられる。

  • 要件定義や設計にて仕様を整理する
  • 製造や試験にてメソッドのIn/OutやIF文のパターンを網羅する

デシジョンテーブルの構成

デシジョンテーブルのひな形.png
デシジョンテーブルは下記の構成で成り立っている。

  • 条件箇所(条件を記載する)
  • 条件指定箇所(条件に対して、その条件を適用するorしないを指定する)
  • 動作箇所(動作を記載する)
  • 動作指定箇所(指定された条件に対してどの動作をするか指定する)

機械的に全パターン記載する

デシジョンテーブル1.png
一旦、機械的に全パターン記載する。
上記の場合、条件の数が3つで、それぞれに対してYesとNoの2通りですから、ルール数は8通り(2×2×2)

矛盾しているルールを削除する

デシジョンテーブル2.png
条件の組み合わせとして矛盾しているルールについては削除する。上記の場合、分かりやすいようにグレーアウトして残している。

仕様確認などのために保留しておきたい場合は動作指定箇所に「N/A」を記載しておくこともできる。

無駄なルールを削除する

デシジョンテーブル3.png
ルール1とルール2は「残金が1000円未満」の場合、満腹かどうかは関係なく「ラーメンを食べない」ということが分かるのでルール2を削除する。

完成したデシジョンテーブル

デシジョンテーブル4.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

時々Thread.dumpStackデバッグ

TL;DR

Javaでデバッグの方法はいろいろあるけど、Thread#dumpStackを使う(スタックトレースを表示する)、みたいなことも知っておくと便利ですよ、ということで。

デバッグ方法?

Javaアプリケーションのデバッグって、どんな感じでやっているでしょうか?

  • System.out.printlnでしょ
  • 各種ログライブラリで、ふつうにログ埋め込みますよ
  • IDEのデバッガー使ってます

まあ、いろいろあるんじゃないかと思います。

デバッガーも使うけど、めんどうな時はSystem.out.printlnしてる、みたいなこともあるでしょう。

スタックトレースを使う

確認したい内容によっては、スタックトレースを表示すると楽に情報が見れることがあります。

たとえば、

  • ある処理の呼び出しを、どういう経路で、誰が行っているか知りたい
  • 共通的な処理を入れ込んだ時に、ちゃんと呼び出しが行われているか知りたい

などなど。

前者は、「これ、誰が呼び出してるんだろう?」とか「なんでこの処理が呼び出されたんだろう?」ということを確認したい時などに。後者は、AOPやサーブレットフィルターなどを追加した時に、「本当に組み込まれているか?」を確認したい時などに。

ログや処理結果に反映されるなどで、デバッグをするための処理を仕込まなくてもわかるのが1番ですけどね。それがわからない時、確認しなくてはいけない時、トレースしなくてはいけない時などに知っておくと便利です。

例外のインスタンスは必ずスローしなければならない、というわけでもない

Javaで例外(Exception)を目にする機会というのは

  • なにかしら問題が起こった時に、スタックトレースとともに表示される
  • try-catchとprintStackTraceを知り、「これでスタックトレースというものが表示される」と覚える
  • 例外というものを覚え、throwで例外をスローできることを知る(で、catchする)

という感じで、およそ「エラーの発生」、「エラーハンドリング」と合わせて意識に刷り込まれます。

なので「エラーが起こるとスタックトレースが出る」とか、スタックトレースを見ると「エラーになった」みたいな拒絶反応に近いことになる人も、まあまあ見ます。

ですが、Exception(というか、その上位のThrowable)もインスタンスですし、throwできるだけであってここからスタックトレースをオブジェクトとして取得することもできます。

Throwable#getStackTrace

これを標準エラー出力に書き出しているのが、おなじみprintStackTraceです。

Throwable#printStackTrace

https://github.com/openjdk/jdk/blob/jdk-11+9/src/java.base/share/classes/java/lang/Throwable.java#L638-L673

なのでcatchを使わずともスタックトレースを出力することができます。

こんな感じですね。

        new Exception().printStackTrace();

とはいえ、例外をnewしてthrowしないのはちょっと気持ち悪いかもしれません。

こんな時にはThread#dumpStackを使ってみるとよいでしょう。

        Thread.dumpStack();

まあ、実際の中身はこんな感じなんですが。

    public static void dumpStack() {
        (new Exception("Stack trace")).printStackTrace();
    }

https://github.com/openjdk/jdk/blob/jdk-11+9/src/java.base/share/classes/java/lang/Thread.java#L1423-L1425

結局は、Exceptionのインスタンスを生成しているんですけどね。

サンプル

実際に、例を見てみましょう。

確認環境はこちらです。

$ java --version
openjdk 11.0.9.1 2020-11-04
OpenJDK Runtime Environment (build 11.0.9.1+1-Ubuntu-0ubuntu1.20.04)
OpenJDK 64-Bit Server VM (build 11.0.9.1+1-Ubuntu-0ubuntu1.20.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f)
Maven home: /home/charon-r13b/.sdkman/candidates/maven/current
Java version: 11.0.9.1, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-56-generic", arch: "amd64", family: "unix"

お題は、簡単にSpring Bootでいきます。

$ curl https://start.spring.io/starter.tgz -d dependencies=web,jdbc,h2 \
           -d bootVersion=2.4.0 -d baseDir=demo | tar -xzvf -
$ cd demo

こんなRestControllerを作成。

src/main/java/com/example/demo/HelloController.java

package com.example.demo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    @GetMapping("hello")
    public String hello() {
        return "Hello World!!";
    }
}

起動。

$ mvn spring-boot:run

確認。

$ curl localhost:8080/hello
Hello World!!

まずは動いているのが確認できたので、RestControllerを以下のように修正してみます。

@RestController
public class HelloController {
    public HelloController() {
        Thread.dumpStack();
    }

    @GetMapping("hello")
    public String hello() {
        Thread.dumpStack();
        return "Hello World!!";
    }
}

コンストラクタと、リクスエストを受け付けるメソッドにThread#dumpStackを仕込みました。

すると、mvn spring-boot:run(というかアプリケーションの起動時)に、こんな内容が標準エラー出力に書き出されます。

java.lang.Exception: Stack trace
    at java.base/java.lang.Thread.dumpStack(Thread.java:1388)
    at com.example.demo.HelloController.<init>(HelloController.java:9)
    at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:490)
    at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:212)
    at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:87)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1310)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1216)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:571)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:531)
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:944)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:925)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:588)
    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:144)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:767)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:759)
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:426)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:326)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1309)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1298)
    at com.example.demo.DemoApplication.main(DemoApplication.java:10)

今回のコードだと、アプリケーションのmainメソッドからの呼び出し先の時点で、すでにRestControllerのインスタンスが作成されていることがわかります。

    at com.example.demo.DemoApplication.main(DemoApplication.java:10)

また、リクエストを送ってみると

$ curl localhost:8080/hello
Hello World!!

こんなスタックトレースが得られます。

java.lang.Exception: Stack trace
    at java.base/java.lang.Thread.dumpStack(Thread.java:1388)
    at com.example.demo.HelloController.hello(HelloController.java:14)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:197)
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:141)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:106)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:893)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:807)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1061)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:961)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:626)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:733)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374)
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1590)
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.base/java.lang.Thread.run(Thread.java:834)

ちょっと長いので、まあまあ見そうな部分を残すと、こんなところでしょうか。

java.lang.Exception: Stack trace
    at java.base/java.lang.Thread.dumpStack(Thread.java:1388)
    at com.example.demo.HelloController.hello(HelloController.java:14)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:197)
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:141)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:106)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:893)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:807)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1061)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:961)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:626)

Spring FrameworkがどのようなクラスをたどってControllerを呼び出しているかの雰囲気が、少しわかります。

では、次に@Transactionalアノテーションを付けてみましょう。

    @Transactional
    @GetMapping("hello")
    public String hello() {
        Thread.dumpStack();
        return "Hello World!!";
    }

すると、スタックトレースがこう変化します。

java.lang.Exception: Stack trace
    at java.base/java.lang.Thread.dumpStack(Thread.java:1388)
    at com.example.demo.HelloController.hello(HelloController.java:16)
    at com.example.demo.HelloController$$FastClassBySpringCGLIB$$71860334.invoke(<generated>)
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:771)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:371)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:134)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691)
    at com.example.demo.HelloController$$EnhancerBySpringCGLIB$$84c39e5d.hello(<generated>)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:197)
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:141)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:106)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:893)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:807)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1061)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:961)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:626)

リフレクションでの呼び出しを抜粋すると、@Transactionalがない時はこれくらいシンプルだったのが

java.lang.Exception: Stack trace
    at java.base/java.lang.Thread.dumpStack(Thread.java:1388)
    at com.example.demo.HelloController.hello(HelloController.java:14)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

一気に呼び出し階層が増えます。

java.lang.Exception: Stack trace
    at java.base/java.lang.Thread.dumpStack(Thread.java:1388)
    at com.example.demo.HelloController.hello(HelloController.java:16)
    at com.example.demo.HelloController$$FastClassBySpringCGLIB$$71860334.invoke(<generated>)
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:771)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:371)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:134)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691)
    at com.example.demo.HelloController$$EnhancerBySpringCGLIB$$84c39e5d.hello(<generated>)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

トランザクションに関係しそうな処理が織り込まれているのが確認できますね。

    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:371)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:134)

わかりやすい例として、AOPを使用したわけですが。

@Transactionalの場合はコミット、ロールバックの実際の結果で確認すればよい、という話はもちろんあります。ですが、「そもそも呼ばれてる?」とか「なにが変わるんだろう?」みたいな疑問にはこういう視点で確認することもできるかな、と。

注意点

こういうスタックトレースを扱う方法を知ると、Exceptionのインスタンスを作ってスタックトレースを得ればメソッド名とかソースコードの行番号をログに出力できるのでは?とか思うことがあります。

ですが、これはやらない方が懸命です。

Exceptionのインスタンスの生成は重い処理になるため、ログ出力の高頻度で使われる用途に組み込んでしまうとパフォーマンスがとても悪くなります。

あくまで、例外は「例外的な状況で使いましょう」、という原則は変わりません。

今回のような使い方は、一時的なデバッグ用途のため、知りたい情報が確認できたら削除されるような代物のはずです。

というわけで

使えそうなところがあれば、デバッグの手段としておひとつどうぞ。

あと、自分たちが作るクラスもスタックトレースに含まれることになるので、「なにをするためのクラスやメソッドなのか、名前から推測がしやすいもの」とすることを心がけたいですね。

そうすれば、スタックトレースを見るとだいたいどんな処理が行われていそうか、雰囲気をつかみやすくなるでしょう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

念のためもう一度トルコ語(等)で起こる問題について整理する

Qiita Advent Calendar 2020のJava Advent Calendarの12月7日のエントリです。

トルコ語問題については、ご存知の方はご存知だと思いますが、聞いたことがない人にはなかなか理解できない問題なので2020年の今、もう一回おさらいしておくのもいいかと思い書いています。(ほかにネタがなかった、とも言う。)

トルコ語の何が特殊なのか

トルコ語には、dotted-iとdotless-iの二つのアルファベットがあります。

https://en.wikipedia.org/wiki/Dotted_and_dotless_I

通常私たちは「i」の大文字が「I」であり、「I」の小文字が「i」であると理解しています。しかし、トルコ語ロケール(とアゼルバイジャン語ロケール)では「I」はドットなしIであるとみなされ、その小文字は「ı」になります。逆に「i」の大文字はドットありの「İ」なのです。

このため、以下のようなことが起こります。

    Locale trLocale = new Locale("tr");
    String largeDot = "i".toUpperCase(trLocale);  // --> "\u130"
    System.out.println("I".equals(largeDot));              // --> false

引数なしのtoUpperCase()/toLowerCase()はデフォルトのロケールを参照しますので、システム全体がトルコ語ロケールで動作している場合とそれ以外で以下のコードは異なる結果を返すというわけです。

    String s1 = "Application-ID";
    String s2 = "APPLICATION-ID";
    System.out.println(s1.toUpperCase().equals(s2));  // --> true/false

たとえば、ヘッダの項目名とかで大文字小文字の区別のない定義済み文字列について一致を見たいケースなどありますよね。そのようなケースでシステムのロケールが変わると動かなくなるという問題が発生するわけです。

java.util.jar.Attributes$Name#equals()

分かりやすいかどうかは分からないですが、典型的な例としてピンポイントでこのメソッドを追いかけて見ます。JarファイルのManifestをハンドリングするときに、属性名の一致判定がトルコ語で失敗してしまい落ちるという話です。

JDK-4624534 : JarEntry.getCertificates() returns null on Turkish locale (tr_TR)

JDK1.4.2のソースコードです。java.util.jar.Attributesクラスの内部クラスであるNameのequals()メソッドの定義です。

Attributes.java
        /**
         * Compares this attribute name to another for equality.
         * @param o the object to compare
         * @return true if this attribute name is equal to the
         *         specified attribute object
         */
        public boolean equals(Object o) {
            if (o instanceof Name) {
                return name.equalsIgnoreCase(((Name)o).name);
            } else {
                return false;
            }
        }

単純な大文字小文字無視の一致判定であるString#equalsIgnoreCase()を呼び出しています。実はequalsIgnoreCase()は、JDK1.4.2の頃はロケールに依存した文字列比較を実行していました。つまりトルコ語ロケール下で"i"と"I"をequalsIgnoreCase()するとfalseが返っていました。

そこで、ここをまずは直したようです。

JDK1.5のソース。

Attributes.java
        /**
         * Compares this attribute name to another for equality.
         * @param o the object to compare
         * @return true if this attribute name is equal to the
         *         specified attribute object
         */
        public boolean equals(Object o) {
            if (o instanceof Name) {
                Comparator c = ASCIICaseInsensitiveComparator.CASE_INSENSITIVE_ORDER;
                return c.compare(name, ((Name)o).name) == 0;
            } else {
                return false;
            }
        }

sun.misc.ASCIICaseInsensitiveComparator.CASE_INSENSITIVE_ORDERは、ASCIICaseInsensitiveComparatorクラスのstaticインスタンスです。これのcompare()メソッドは、

ASCIICaseInsesitiveComparator.java
    public int compare(Object o1, Object o2) {
        String s1 = (String) o1;
        String s2 = (String) o2;
        int n1=s1.length(), n2=s2.length();
        int minLen = n1 < n2 ? n1 : n2;
        for (int i=0; i < minLen; i++) {
            char c1 = s1.charAt(i);
            char c2 = s2.charAt(i);
            assert c1 <= '\u007F' && c2 <= '\u007F';
            if (c1 != c2) {
                c1 = (char)toLower(c1);
                c2 = (char)toLower(c2);
                if (c1 != c2) {
                    return c1 - c2;
                }
            }
        }
        return n1 - n2;
    }

なので、ASCIIの範囲外のcharを含んだ文字列を引き渡すとassertionで落ちるのですが、JDK1.5の上記の箇所では「i」が含まれる文字列と「I」が含まれている文字列を比較しているだけなのでそこは大丈夫です。

この修正と関連しつつ独立して、以下のバグの記録のところで、当時UNICODE 3.0の規約での大文字小文字比較に準拠しているかどうか、みたいなコメントが出ていて、toUpperCase()、toLowerCase()から、equalsIgnoreCase()、compareToIgnoreCase()まで、このあたりの振舞を修正しながらバグをコツコツ直していったようです。

JDK-6208680 : Doc: Clarify issues with toLowerCase/toUpperCase and Turkish

なので、途中でString#equalsIgnoreCase()がドットあり/なしのIの比較で(Unicodeの振舞に準拠するために)trueを返すようになったりはしているのですが、こちらにはそのことは反映されず、このあとしばらくは(JDK8まで)このコードが継承されていました。

しかし、安易にsun.misc.ASCIICaseInsensitiveComparatorを呼ぶコードが広がるのも問題です。JDK9では、そもそもjigsawの流れからも、sun.misc.*を呼ぶのはよろしくないよね、ということでしょうか、さらなる改訂が行われました。

JDK-8151384 : Improve String.CASE_INSENSITIVE_ORDER and remove sun.misc.ASCIICaseInsensitiveComparator

ということでJDK9では以下のようになりました。

http://hg.openjdk.java.net/jdk9/jdk9/jdk/rev/c82be424393e

Attributes.java
        /**
         * Compares this attribute name to another for equality.
         * @param o the object to compare
         * @return true if this attribute name is equal to the
         *         specified attribute object
         */
        public boolean equals(Object o) {
            if (o instanceof Name) {
                Comparator<String> c = String.CASE_INSENSITIVE_ORDER;
                return c.compare(name, ((Name)o).name) == 0;
            } else {
                return false;
            }
        }

ということで現在Manifestの属性名の一致判定でトルコ語で問題が発生することはなくなっています。

これで終わりではない

ひとまずjarファイルのManifestのハンドリングのところだけの変遷を見てきましたが、トルコ語問題は、かなり初期から存在しており、しかもばらばらに発見されてはつぶされるという経緯をたどってきました。

JDK-6972385: issues with String.toLowerCase/toUpperCase
JDK-7059542 : JNDI name operations should be locale independent

正直に言うとまだまだ潜在する同種の問題が存在しているような気配です。常に気をつけていないといけないという意味ではかなり注意が必要なバグパターンであると思います。

当然ですが、Javaのランタイムに存在する問題をすべて解決してしまえば終わりというわけではなくライブラリが同じ問題を抱えていたりアプリ側の処理が不十分で問題を引き起こすこともあります。

Unicodeの文字列比較のルールは複雑ですし、バージョン改訂に伴って振舞が変わることもありえますので、十分に気をつけてハンドリングすることが必要です。

Tipsとしては、

  • とにかく文字列比較なんて分かってるから、みたいな態度はいったん捨てる。
  • toUpperCase()/toLowerCase()してからequals()/compareTo()を呼ぶのは本当に大丈夫なときだけ。できるだけ、equalsIgnoreCase()/compareToIgnoreCase()を呼ぶコードに変更する。
  • toUpperCase()/toLowerCase()をデフォルトロケールで呼んでよいかどうかは十分吟味する。ロケールに依存しない変換が必要ならLocale.ROOTを使う。(大丈夫ならLocale.ENGLISHでもよい。)
  • いきなりASCIICaseInsensitiveComparator.CASE_INSENSITIVE_ORDERやString.CASE_INSENSITIVE_ORDERが出てきても慌てない。
  • 逆にロケール依存の文字列比較・一致判定が必要な場合、Collatorクラスを使うことも考慮する。

ちなみに、当然ですがこれはJavaだけの問題ではありません。ほかの言語を使っておられる方も参考にされてください。(とここで書いてもしょうがないけど。)

上にLDAPの例が出ていますが、筆者はSMBで類似件に当たったことがあります。通信プロトコルは大文字小文字を無視する系が多いので要注意です。(しかもサーバは直せないのでクライアントの実装が割を食う例が多い。)

もう一つ、ここではASCII前提だった処理にトルコ語・アゼルバイジャン語が混ざるのでトラブルになる例ですが、ベース処理の仮定する文字体系と入力される文字体系がミスマッチを起こすことで問題になる可能性はいろんなものの中にあります。CJKでもありそうだな、とちょっと嫌な予感がしています。(今のところはその種の話は聞いていないですが。)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【ダレトク】Java 版 Minecraft のコマンドの境地 NBT を頼りに、ついでに IT 用語に触れる記事

はじめに

この記事は IT 用語に触れる記事です。マイクラ好きな人が多いみたいなので、マイクラを頼りに IT の勉強でもしてみませんか?
楽しいマイクラの NBT とは何なのかを考えながらコマンドへの恐怖心を無くし、そのついでに IT 用語を少しかじって成長して行く。そんな感じの無駄話がツマッタ記事を載せておきます。
(マイクラのコマンドを求めてる方は、この記事では紹介していないので、いつか書くかもしれない別の記事をお待ちください。)

この記事の中では、以下の IT 用語の一端に触れることができます。

  • JSON
  • API

この記事は10分程度で読めます。

1. NBT とは

入り口はマイクラからにして、まずは NBT について紹介します。NBT は Named Binary Tag の略で、極論を言ってしまうとユニクロへ行ったときに新品の靴下に付いてる "タグ" と同じです。靴下のサイズとか値段とかメーカーとか色々な「情報」がタグに書かれています。それです。
マイクラで NBT という単語を使うタイミングは2種類くらいあります。1つはコマンドで「データタグ」を入力するとき、もう1つは自分達が普段遊んでいるワールドデータやセーブデータなどのマイクラが取り扱っているほぼ全ての情報(実は中身は NBT の集まり)の中に潜んでいます。勘違いすると良くないので最初だけしっかり説明します。

2. コマンド

2.1. データタグ

NBT をコマンドで見かけるタイミングは以下の画像の通りです。ここでは "名前" が 「私は防具立てです。」という防具立てを召喚し、更に "名前を遠くからでも見える" ように補足情報を与えようとしています。
nbt1.png
ゲーム内では画像の通り <nbt> と表示されているため、NBT と呼ばれていても特に違和感はありませんし、間違いでもありませんが、この長ったらしい文字列には NBT ではなく「データタグ」という名称があります。上記のコマンドの中で中括弧(波括弧)から中括弧までの文字列全ての事を「データタグ」と呼びます。この中括弧部分を見やすく整形すると以下の表記になります。

{
    CustomName:"\"私は防具立てです。\"",
    CustomNameVisible:1
}

これを更に一般化すると以下のような形式になります。

{
    <tag name>:<value>,
    <tag name>:<value>,
    ・・・
    <tag name>:<value>
}

改行しない場合を考えてみると分かりますが、最後の <value> の後ろにカンマは付かないので気を付けて下さい。要するに、{品名:靴下,価格:300} のような情報の集まりで、コロン(:)の左と右に何かの文字が書かれていて、それをカンマ(,) 区切りで繋げただけです。出てきた用語の説明は下記の通りです。

 <tag name>  タグ名:値を識別する名前(項目名)
 <value>     値:持っている情報、数字や文字列などで表される。例えば300円なら「300」など。

それぞれ任意の文字列を記載可能ですが、マイクラの中で意味のないものは無視されます。例えばですが、オオカミはヘルメットを装着することができないため、<tag name> に "頭装備" などと書いてあっても無視されます。

ここで話を一旦 NBT に戻しますが、マイクラで NBT を取り扱う2つ目の事例として、ワールドデータやセーブデータという話がありました。上記のデータタグというのは、コマンドを見栄え良く中括弧で囲った表記のことであり、データタグの表記を使って、マイクラのワールドデータ(中身は NBT の集まり)を書き換えたり、読み出すことができます。データタグとは NBT を読み書きする命令だと考えれば分かりやすいでしょうか。

もう少しデータタグを見てみたい方は、以下のコマンドを入力してみて下さい。プレイヤーが持っている「情報」が一覧表示されます。

/data get entity @s

nbt2.png

2.2. データタグの記法

さて、データタグと NBT の関係について分かったところで、データタグの記法について簡単に説明します。データタグの記法について理解しておくと、今まで見るたびに恐怖を感じていたコマンドが幾分か易しく見えるかも知れません。
データタグが中括弧と <tag name> と <value> からできているところまでは説明しましたが、記法にはルールがあります。<tag name> には、それが何なのかを識別するための名前を書けば良いとして、<value> には以下の値を記載することができます。

<value> 説明
数値 整数、小数の記載が可能。数字の後ろに b,s,l,f,d などとアルファベットを1つ付けることで整数/小数、表現可能な数字の範囲を決めることもできるがここでは詳細は説明しない。
真偽値 整数の1種。1b または 1 で true(真、有効、ON)、0b または 0 で false(偽、無効、OFF)を表す。
文字列 ダブルクォート(")で両端を囲って文字の羅列を記載できる。ダブルクォート自体を文字に含めたい場合にはバックスラッシュとダブルクォート(\")を並べて記載する。日本語を記載するときには前後を(\")で囲う必要がある。
配列 大括弧で囲んだ中に <value> をカンマ区切りで複数記載できる。但し、最初の値が数値だった場合は2つ目以降の値も数値である必要がある。最初が文字列なら、2つ目以降も文字列が必要となる。
データタグ <value>の中に更に中括弧から始まり中括弧で終わるデータタグを記載できる。

例を1つ記載しておきます。

{
 id:1,
 name1:"Taro",
 name2:"\"太郎\"",
 "\"身長\"":172.4f,
 favorite:[
  "manga",
  "\"カレー\""
 ],
 "\"更にデータタグを記載する例\"":{
   address:"Tokyo",
   age:30
 }
}

整数、小数、文字列、そして文字列の中でも日本語の記載ルール、それから複数の要素を並べる配列、配列の中には文字列を書いたり数字を書いたり、更に <value> にデータタグ自体を書いて、データタグの階層構造にしたりすることができます。

括弧で始まったら括弧で終わる、ダブルクォートで始まったらダブルクォートで終わる、コロンの前後にはタグ名と値がある、つなぎ目にはカンマがある、最後の要素の後ろにはカンマは付かない、文法はたったのこれだけです。英語の文法を勉強するよりはるかに楽ですね。

中括弧が多いと呪文のように見えてしまうのですが、あとはマイクラ特有の項目名を覚えてあげれば、動かない村人や強い装備を持ったゾンビなどが作れるようになります。
上の例を summon コマンドなどの後ろにそのまま貼り付ければエラー無く実行できますが、意味のある項目名が無いため、データタグとしては無視されます。

2.3. JSON

ここで1つ IT 用語について触れておきます。恐らくはデータタグの元ネタになった JSON(JavaScript Object Notation:ジェイソン)と呼ばれるものについてですが、私が知る限り JSON が登場したのは2000年以降です。
2000年台の初期は掲示板、チャットサイト、Web サイト上で動くチープな育成ゲームなど「動的に閲覧中のWeb サイトを動かす手法」が乱立し終わった時代だったと記憶しています。その中で徐々にクライアント(Web ブラウザや自分のPC内のアプリ)とサーバ(Web サイトなど)の間で「効率的にデータをやりとりする手法」や「Web サイト自体を効率的に作成する手法」が登場し始めていました。

JSON は名前の通り、JavaScript を発祥としていますが別に JavaScript に限らず、色々なところで使われています。JavaScript がたまたま Web ページを綺麗にお手軽に動かすための手法の1つだったというだけであり、一方 JSON は情報の伝達手段(表現方法)の1つであり、また「効率が良い」というだけです。
「効率が良い」というのは、同じ情報をより短い情報で表現できる。という観点で評価することができます。
例えば、Mojang Studios がマインクラフトのプレイヤー情報を Web から参照できる「窓口」を用意してくれているので、以下URLにアクセスしてみて下さい。

[Mojang API] https://api.mojang.com/users/profiles/minecraft/JohnikiJoestar

これは JohnikiJoestar というプレイヤーを世界で1つのユニークな ID で表現する UUID を取得するサイトです。見て頂ければわかりますが、名前と ID の他に余分な情報は中括弧とダブルクォート、コロン、カンマだけであり、有名どころではこれ以上短く表す記法は存在しません。(書こうと思えば書けますが読みにくいだけです。)

マインクラフトは Java という言語で 200X 年頃に開発されていますが、その時代に JSON が丁度成熟していたからなのか、開発者の Notch さんが JSON を好きだったからなのかは定かではありませんが、上の URL から得られる ID はデータタグではなく JSON で表されています。

2.4. JSON の記法

最初に説明したデータタグとほとんど同じ記法です。一般化すると以下のようになります。

{
    <key>:<value>,
    <key>:<value>,
    ・・・
    <key>:<value>
}

データタグとの違いに注目しつつ説明すると、

 <key>       キー:値を識別する名前(項目名)、ダブルクォートで囲った文字列でなければならない。
 <value>     値:持っている情報、数字や文字列などで表される。例えば300円なら「300」など。

ただ呼び名が違うだけですが、<tag name> の代わりに <key> という呼び名に変わったことと、<key> はダブルクォート(")で囲われた文字列である必要があります。<\value> について、データタグと違う箇所を赤文字にしておきました。以下の通りです。

<value> 説明
数値 整数、小数の記載が可能。小数は 42.195e3(42.195 × 1000)のような指数表記も使ってよい。
ヌル 何もないことを表すときには小文字半角で null と記載する。
真偽値 英半角小文字で true(真、有効、ON)、false(偽、無効、OFF)を表現する。
文字列 ダブルクォート(")で両端を囲って文字の羅列を記載できる。ダブルクォート自体を文字に含めたい場合にはバックスラッシュとダブルクォート(\")を並べて記載する。
配列 大括弧で囲んだ中に <value> をカンマ区切りで複数記載できる。最初の値が数値だったとしても2つ目以降が数値である必要はない。
JSON <value>の中に更に中括弧から始まり中括弧で終わる JSON を記載できる。JSON 自身の事をオブジェクトや JSON オブジェクトなどとも呼ぶ

JSON の例を1つ紹介しておきます。

{
  "id": 1,
  "name1": "Taro",
  "name2": "太郎",
  "身長": 172.4,
  "favorite": [
    "manga",
    "カレー",
    1,
    null,
    true
  ],
  "更にJSONを記載する例": {
    "address": "Tokyo",
    "age": 30
  }
}

データタグとそんなに違いはないですね。

3. セーブデータ

3.1. NBT の可視化

マイクラの中でもう1つの NBT を使っている場所ですが、それは目にみえる所にはありません。Java 版マインクラフトの「saves」フォルダの中を NBTExplorer などの特別なツールで閲覧すると以下のような画面を見ることができます。
nbt3.png

ツールを使うことで NBT を閲覧したり、NBT を書き換えることで、バグったブロックを取り除いたり、ワールドの中から好きな情報を取り出したりと「セーブデータを直接編集する」こともできます。

3.2. セーブデータの中身

そもそもセーブデータとは何なのかという疑問に答えないといけません。まずは、マインクラフトの世界の中であなたが思い浮かべるものをずらずらと列挙してみて下さい。時系列でも良いですし、あいうえお順でも良いです。思いつくもの全てを挙げてみた結果「この情報は次ログインしたときに消えていると何か変だな。」と思ったものは大体セーブデータに含まれているはずです。

例えば、ワールドを作ってから以降、時系列順に適当にいくつか考えてみるとすると、

  • 初期スポーン地点
  • プレイヤーの現在地
  • プレイヤーの持っているアイテム
  • プレイヤーの体力、空腹ゲージ、経験値
  • チェストの中身
  • 村人の立っている場所
  • 村人の交易結果
  • 置いたブロックの場所
  • 天気、時刻
  • リスポーン地点

まだまだありますが、とりあえず10個だけ挙げてみました。この中に座標に関する情報がいくつかあることに気付くと思いますが、座標は X(東西)、Y(上下)、Z(南北) の3つの情報で表現できます。
天気はどうでしょう?天気は "晴れ" とか "雨" かも知れませんし、"晴れ" の代わりに "A"、"雨" の代わりに "B" などと書いてあるかも知れませんが、いずれにしても1つの情報で表現できます。
ではここで、天気が "晴れ"、村人1の座標(X=100、Y=68、Z=-50)、村人2の座標(X=90、Y=67、Z=-40) の3種をできるだけ短い文字数で表すことを考えてみて下さい。

例えば以下のような書き方を思いつきます。

{"天気":"A",{"id":1,"座標":[100,68,-50]},{"id":2,"座標":[90,67,-40]}}
A 100 68 -50 90 67 -40
A100068-50090067-40

1つ目は JSON で書いてみました。情報そのままですね。2つ目はなるべく省略して、文字の間にスペースを開けてみました。3つ目はどうでしょう?天気は1桁、村人の数は2人まで、座標は3桁までだと仮定してスペースを無くしました。それぞれに良い点、悪い点があります。

1つ目の形式は "id" という箇所を見てあげれば、どの村人の座標か分かるため、村人1と村人2の情報は順不同だという特徴があります。
2つ目と3つ目の形式は文字数が少ないという特徴がありますが、村人の情報が順不同ではないということや、仮に後ろにプレイヤーの座標があったとすると、それが3人目の村人なのかプレイヤーなのか表現できない問題があります。3つ目については座標が4桁になったらセーブデータが壊れてしまいますね。

マインクラフトJava版というゲームがセーブデータを読み込むときに、1桁目は天気、そこから9桁ずつ村人の情報が2人分入っていると知っていれば、3つ目の例でもセーブデータを読み込むことができます。
2つ目3つ目の例は極端に短くし過ぎましたが、必要な場所にはタグを付けてあげて、場合によっては村人が2人いるという情報や、座標の情報は3桁が上限です、などと決めてあげればセーブデータとしては成立しそうです。

という感じで、タグ付きの情報というのはセーブデータの形式としてとても優秀で、区切り文字が中括弧なのかスペースなのか固定長の桁数区切りなのか、それだけ決めてあげれば、それをそのままファイルに書くだけでセーブデータにすることができます。
後は情報を短く書く工夫をしてあげればセーブデータを小さくすることができますが、それはまた別の話。

余談

余談ですが、あるアプリケーションが何らかの情報(先の Mojang Studios の例であれば UUID)を読み書きする「窓口」を提供していることがありますが、これを API (Application Programming Interface)と呼びます。
Mojang の場合は Mojang API でした。Web から利用できる場合には WebAPI などと呼び、WebAPI の流行りはここ10年くらいで誕生したものです。

自分の作ったアプリケーションをみんなに使ってもらいたいときに全部を提供するのはハードルが高いことが有ります。そこで一部の機能だけでも、みんなが API を使って提供しあうことで新たなサービスが生まれることがあります。
例えば、SNS からフォロワー情報を提供する API と、住所に関連しそうな API と、食べログ API を組み合わせたら、深夜3時に呟く飯テロサービスができました。というふうに WebAPI の考え方が普及し出すと共に、色んな企業がインターネットをベースにしたサービスをどんどん作り始めました。

今までは御堅い体質のあった金融業界でも最近ようやく法律が整って API が普及し始めていて、Web 系のエンジニアが大量に必要になっているような状況です。10年くらい前に FinTech とかの単語が騒がれましたが、その流れの1つですね。あ、これは来る。って思った人や企業は当時から Web 系のエンジニアを育成していたんだろうと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Java・SpringBoot】宣言的トランザクション処理(SpringBootアプリケーション実践編20)

ホーム画面からユーザー一覧画面に遷移し、ユーザーの詳細を表示するアプリケーションを作成して、Spring JDBCの使い方について学びます⭐️
今回はトランザクションの基本的な処理である宣言的トランザクションを学びます^^
構成は前回/これまでの記事を参考にしてください

⭐️前回の記事

【Java・SpringBoot】NamedParameterJdbcTemplateでCRUD操作(SpringBootアプリケーション実践編19)

Springでのトランザクション

  • トランザクションとは、関連する複数の処理を大きな1つの処理として扱うこと
    • →関連した処理が正常に完了しない場合、すべての変更が取り消され、データの不整合を防ぐことができる!
  • 宣言的トランザクション(実践ではこれを使う)
    • あるメソッドを呼び出したときにトランザクション処理する
    • @Transactionalアノテーションを付けるだけでOK
      • 以下ではトランザクションでロールバック(途中処理結果を捨てて、やる前の状態に戻す作業)ができているかを確認するために例外を投げる
  • 明示的トランザクション(あまり使わないので、参考程度。次回紹介します)

リポジトリクラス修正

  • 更新処理でわざと例外を投げます
UserDaoJdbcImpl.java
//略(全文は下記参考)
    // Userテーブルを1件更新.
    @Override
    public int updateOne(User user) throws DataAccessException {

        //1件更新
        int rowNumber = jdbc.update("UPDATE M_USER"
                + " SET"
                + " password = ?,"
                + " user_name = ?,"
                + " birthday = ?,"
                + " age = ?,"
                + " marriage = ?"
                + " WHERE user_id = ?",
                user.getPassword(),
                user.getUserName(),
                user.getBirthday(),
                user.getAge(),
                user.isMarriage(),
                user.getUserId());

        //トランザクション確認のため、わざと例外をthrow
                if (rowNumber > 0) {
                    throw new DataAccessException("トランザクションテスト") {
                    };
                }

        return rowNumber;
    }
//略(全文は下記参考)

サービスクラス修正

@Transactional

  • トランザクションを使うためには、クラスに@Transactinalアノテーションを付ける (一般的には、ビジネスロジックを担当するサービスクラス)
  • 引数を付けることでトランザクションのレベルを設定できる
UserService.java
//略(全文は下記参考)
@Transactional
@Service
public class UserService {
    @Autowired
    @Qualifier("UserDaoJdbcImpl")
    UserDao dao;

//略(全文は下記参考)
}

コントローラクラス修正

  • try~catchでDBで例外発生しても引き続き画面を表示する
HomeController.java
//略(全文は下記参考)

    /**
     * ユーザー更新用処理.
     */
    @PostMapping(value = "/userDetail", params = "update")
    public String postUserDetailUpdate(@ModelAttribute SignupForm form,
            Model model) {

        System.out.println("更新ボタンの処理");

        //Userインスタンスの生成
        User user = new User();

        //フォームクラスをUserクラスに変換
        user.setUserId(form.getUserId());
        user.setPassword(form.getPassword());
        user.setUserName(form.getUserName());
        user.setBirthday(form.getBirthday());
        user.setAge(form.getAge());
        user.setMarriage(form.isMarriage());

        try {

            //更新実行
            boolean result = userService.updateOne(user);

            if (result == true) {
                model.addAttribute("result", "更新成功");
            } else {
                model.addAttribute("result", "更新失敗");
            }

        } catch (DataAccessException e) {

            model.addAttribute("result", "更新失敗(トランザクションテスト)");

        }

        //ユーザー一覧画面を表示
        return getUserList(model);
    }

//略(全文は下記参考)

SpringBootを起動してホーム画面確認!

  • http://localhost:8080/home
  • ユーザ一覧からユーザ詳細画面に移り、ユーザー名更新
  • 更新しても、ユーザ名が変わっていない(更新失敗と表示)、トランザクションがロールバックできていることがわかりました〜〜!^^

スクリーンショット 2020-11-29 11.46.11.png

スクリーンショット 2020-11-29 11.45.56.png

(参考)コード全文

UserDaoJdbcImpl.java
package com.example.demo.login.domain.repository.jdbc;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import com.example.demo.login.domain.model.User;
import com.example.demo.login.domain.repository.UserDao;

@Repository("UserDaoJdbcImpl")
public class UserDaoJdbcImpl implements UserDao {

    @Autowired
    JdbcTemplate jdbc;

    // Userテーブルの件数を取得.
    @Override
    public int count() throws DataAccessException {

        //全件取得してカウント
        int count = jdbc.queryForObject("SELECT COUNT(*) FROM m_user", Integer.class);

        return count;
    }

    // Userテーブルにデータを1件insert.
    @Override
    public int insertOne(User user) throws DataAccessException {

        //1件登録
        int rowNumber = jdbc.update("INSERT INTO m_user(user_id,"
                + " password,"
                + " user_name,"
                + " birthday,"
                + " age,"
                + " marriage,"
                + " role)"
                + " VALUES(?, ?, ?, ?, ?, ?, ?)",
                user.getUserId(),
                user.getPassword(),
                user.getUserName(),
                user.getBirthday(),
                user.getAge(),
                user.isMarriage(),
                user.getRole());

        return rowNumber;
    }

    // Userテーブルのデータを1件取得
    @Override
    public User selectOne(String userId) throws DataAccessException {

        // 1件取得
        Map<String, Object> map = jdbc.queryForMap("SELECT * FROM m_user"
                + " WHERE user_id = ?", userId);

        // 結果返却用の変数
        User user = new User();

        // 取得したデータを結果返却用の変数にセットしていく
        user.setUserId((String) map.get("user_id")); //ユーザーID
        user.setPassword((String) map.get("password")); //パスワード
        user.setUserName((String) map.get("user_name")); //ユーザー名
        user.setBirthday((Date) map.get("birthday")); //誕生日
        user.setAge((Integer) map.get("age")); //年齢
        user.setMarriage((Boolean) map.get("marriage")); //結婚ステータス
        user.setRole((String) map.get("role")); //ロール

        return user;

    }

    // Userテーブルの全データを取得.
    @Override
    public List<User> selectMany() throws DataAccessException {

        // M_USERテーブルのデータを全件取得
        List<Map<String, Object>> getList = jdbc.queryForList("SELECT * FROM m_user");

        // 結果返却用の変数
        List<User> userList = new ArrayList<>();

        // 取得したデータを結果返却用のListに格納していく
        for (Map<String, Object> map : getList) {

            //Userインスタンスの生成
            User user = new User();

            // Userインスタンスに取得したデータをセットする
            user.setUserId((String) map.get("user_id")); //ユーザーID
            user.setPassword((String) map.get("password")); //パスワード
            user.setUserName((String) map.get("user_name")); //ユーザー名
            user.setBirthday((Date) map.get("birthday")); //誕生日
            user.setAge((Integer) map.get("age")); //年齢
            user.setMarriage((Boolean) map.get("marriage")); //結婚ステータス
            user.setRole((String) map.get("role")); //ロール

            //結果返却用のListに追加
            userList.add(user);
        }

        return userList;
    }

    // Userテーブルを1件更新.
    @Override
    public int updateOne(User user) throws DataAccessException {

        //1件更新
        int rowNumber = jdbc.update("UPDATE M_USER"
                + " SET"
                + " password = ?,"
                + " user_name = ?,"
                + " birthday = ?,"
                + " age = ?,"
                + " marriage = ?"
                + " WHERE user_id = ?",
                user.getPassword(),
                user.getUserName(),
                user.getBirthday(),
                user.getAge(),
                user.isMarriage(),
                user.getUserId());

        //トランザクション確認のため、わざと例外をthrowする
                if (rowNumber > 0) {
                    throw new DataAccessException("トランザクションテスト") {
                    };
                }

        return rowNumber;
    }

    // Userテーブルを1件削除.
    @Override
    public int deleteOne(String userId) throws DataAccessException {

        //1件削除
        int rowNumber = jdbc.update("DELETE FROM m_user WHERE user_id = ?", userId);

        return rowNumber;
    }

    //SQL取得結果をサーバーにCSVで保存する
    @Override
    public void userCsvOut() throws DataAccessException {

        // M_USERテーブルのデータを全件取得するSQL
        String sql = "SELECT * FROM m_user";

        // ResultSetExtractorの生成
        UserRowCallbackHandler handler = new UserRowCallbackHandler();

        //SQL実行&CSV出力
        jdbc.query(sql, handler);
    }
}
UserService.java
package com.example.demo.login.domain.service;

import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.dao.DataAccessException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.example.demo.login.domain.model.User;
import com.example.demo.login.domain.repository.UserDao;

@Transactional
@Service
public class UserService {
    @Autowired
    @Qualifier("UserDaoJdbcImpl")

    UserDao dao;

    /**
     * insert用メソッド.
     */
    public boolean insert(User user) {
        // insert実行
        int rowNumber = dao.insertOne(user);
        // 判定用変数
        boolean result = false;

        if (rowNumber > 0) {
            // insert成功
            result = true;
        }
        return result;
    }

    /**
     * カウント用メソッド.
     */
    public int count() {
        return dao.count();
    }

    /**
     * 全件取得用メソッド.
     */
    public List<User> selectMany() {
        // 全件取得
        return dao.selectMany();
    }

    /**
     * 1件取得用メソッド.
     */
    public User selectOne(String userId) {
        // selectOne実行
        return dao.selectOne(userId);
    }

    /**
     * 1件更新用メソッド.
     */
    public boolean updateOne(User user) {

        // 判定用変数
        boolean result = false;

        // 1件更新
        int rowNumber = dao.updateOne(user);

        if (rowNumber > 0) {
            // update成功
            result = true;
        }

        return result;
    }

    /**
     * 1件削除用メソッド.
     */
    public boolean deleteOne(String userId) {

        // 1件削除
        int rowNumber = dao.deleteOne(userId);

        // 判定用変数
        boolean result = false;

        if (rowNumber > 0) {
            // delete成功
            result = true;
        }
        return result;
    }
    // ユーザー一覧をCSV出力する.

    public void userCsvOut() throws DataAccessException {
        // CSV出力
        dao.userCsvOut();
    }

    /**
     * サーバーに保存されているファイルを取得して、byte配列に変換する.
     */
    public byte[] getFile(String fileName) throws IOException {

        // ファイルシステム(デフォルト)の取得
        FileSystem fs = FileSystems.getDefault();

        // ファイル取得
        Path p = fs.getPath(fileName);

        // ファイルをbyte配列に変換
        byte[] bytes = Files.readAllBytes(p);

        return bytes;
    }
}

HomeController.java
package com.example.demo.login.controller;

import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;

import com.example.demo.login.domain.model.SignupForm;
import com.example.demo.login.domain.model.User;
import com.example.demo.login.domain.service.UserService;

@Controller
public class HomeController {

    @Autowired
    UserService userService;

    //結婚ステータスのラジオボタン用変数
    private Map<String, String> radioMarriage;

    /**
     * ラジオボタンの初期化メソッド(ユーザー登録画面と同じ).
     */
    private Map<String, String> initRadioMarrige() {

        Map<String, String> radio = new LinkedHashMap<>();

        // 既婚、未婚をMapに格納
        radio.put("既婚", "true");
        radio.put("未婚", "false");

        return radio;
    }

    /**
     * ホーム画面のGET用メソッド
     */
    @GetMapping("/home")
    public String getHome(Model model) {

        //コンテンツ部分にユーザー詳細を表示するための文字列を登録
        model.addAttribute("contents", "login/home :: home_contents");

        return "login/homeLayout";
    }

    /**
     * ユーザー一覧画面のGETメソッド用処理.
     */
    @GetMapping("/userList")
    public String getUserList(Model model) {

        //コンテンツ部分にユーザー一覧を表示するための文字列を登録
        model.addAttribute("contents", "login/userList :: userList_contents");

        //ユーザー一覧の生成
        List<User> userList = userService.selectMany();

        //Modelにユーザーリストを登録
        model.addAttribute("userList", userList);

        //データ件数を取得
        int count = userService.count();
        model.addAttribute("userListCount", count);

        return "login/homeLayout";
    }

    /**
     * ユーザー詳細画面のGETメソッド用処理.
     */
    @GetMapping("/userDetail/{id:.+}")
    public String getUserDetail(@ModelAttribute SignupForm form,
            Model model,
            @PathVariable("id") String userId) {

        // ユーザーID確認(デバッグ)
        System.out.println("userId = " + userId);

        // コンテンツ部分にユーザー詳細を表示するための文字列を登録
        model.addAttribute("contents", "login/userDetail :: userDetail_contents");

        // 結婚ステータス用ラジオボタンの初期化
        radioMarriage = initRadioMarrige();

        // ラジオボタン用のMapをModelに登録
        model.addAttribute("radioMarriage", radioMarriage);

        // ユーザーIDのチェック
        if (userId != null && userId.length() > 0) {

            // ユーザー情報を取得
            User user = userService.selectOne(userId);

            // Userクラスをフォームクラスに変換
            form.setUserId(user.getUserId()); //ユーザーID
            form.setUserName(user.getUserName()); //ユーザー名
            form.setBirthday(user.getBirthday()); //誕生日
            form.setAge(user.getAge()); //年齢
            form.setMarriage(user.isMarriage()); //結婚ステータス

            // Modelに登録
            model.addAttribute("signupForm", form);
        }

        return "login/homeLayout";
    }

    /**
     * ユーザー更新用処理.
     */
    @PostMapping(value = "/userDetail", params = "update")
    public String postUserDetailUpdate(@ModelAttribute SignupForm form,
            Model model) {

        System.out.println("更新ボタンの処理");

        //Userインスタンスの生成
        User user = new User();

        //フォームクラスをUserクラスに変換
        user.setUserId(form.getUserId());
        user.setPassword(form.getPassword());
        user.setUserName(form.getUserName());
        user.setBirthday(form.getBirthday());
        user.setAge(form.getAge());
        user.setMarriage(form.isMarriage());

        try {

            //更新実行
            boolean result = userService.updateOne(user);

            if (result == true) {
                model.addAttribute("result", "更新成功");
            } else {
                model.addAttribute("result", "更新失敗");
            }

        } catch (DataAccessException e) {

            model.addAttribute("result", "更新失敗(トランザクションテスト)");

        }

        //ユーザー一覧画面を表示
        return getUserList(model);
    }

    /**
     * ユーザー削除用処理.
     */
    @PostMapping(value = "/userDetail", params = "delete")
    public String postUserDetailDelete(@ModelAttribute SignupForm form,
            Model model) {

        System.out.println("削除ボタンの処理");

        //削除実行
        boolean result = userService.deleteOne(form.getUserId());

        if (result == true) {

            model.addAttribute("result", "削除成功");

        } else {

            model.addAttribute("result", "削除失敗");

        }

        //ユーザー一覧画面を表示
        return getUserList(model);
    }

    /**
     * ログアウト用処理.
     */
    @PostMapping("/logout")
    public String postLogout() {

        //ログイン画面にリダイレクト
        return "redirect:/login";
    }

    /**
     * ユーザー一覧のCSV出力用処理.
     */
    @GetMapping("/userList/csv")
    //public String getUserListCsv(Model model) {
    public ResponseEntity<byte[]> getUserListCsv(Model model) {

        //ユーザーを全件取得して、CSVをサーバーに保存する
        userService.userCsvOut();

        byte[] bytes = null;

        try {

            //サーバーに保存されているsample.csvファイルをbyteで取得する
            bytes = userService.getFile("sample.csv");

        } catch (IOException e) {
            e.printStackTrace();
        }

        //HTTPヘッダーの設定
        HttpHeaders header = new HttpHeaders();
        header.add("Content-Type", "text/csv; charset=UTF-8");
        header.setContentDispositionFormData("filename", "sample.csv");

        //sample.csvを戻す
        return new ResponseEntity<>(bytes, header, HttpStatus.OK);

    }
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Java・SpringBoot】宣言的トランザクション処理(SpringBootアプリケーション実践編20)

ホーム画面からユーザー一覧画面に遷移し、ユーザーの詳細を表示するアプリケーションを作成して、Spring JDBCの使い方について学びます⭐️
今回はトランザクションの基本的な処理である宣言的トランザクションを学びます^^
構成は前回/これまでの記事を参考にしてください

⭐️前回の記事

【Java・SpringBoot】NamedParameterJdbcTemplateでCRUD操作(SpringBootアプリケーション実践編19)

Springでのトランザクション

  • トランザクションとは、関連する複数の処理を大きな1つの処理として扱うこと
    • →関連した処理が正常に完了しない場合、すべての変更が取り消され、データの不整合を防ぐことができる!
  • 宣言的トランザクション(実践ではこれを使う)
    • あるメソッドを呼び出したときにトランザクション処理する
    • @Transactionalアノテーションを付けるだけでOK
      • 以下ではトランザクションでロールバック(途中処理結果を捨てて、やる前の状態に戻す作業)ができているかを確認するために例外を投げる
  • 明示的トランザクション(あまり使わないので、参考程度。次回紹介します)

リポジトリクラス修正

  • 更新処理でわざと例外を投げます
UserDaoJdbcImpl.java
//略(全文は下記参考)
    // Userテーブルを1件更新.
    @Override
    public int updateOne(User user) throws DataAccessException {

        //1件更新
        int rowNumber = jdbc.update("UPDATE M_USER"
                + " SET"
                + " password = ?,"
                + " user_name = ?,"
                + " birthday = ?,"
                + " age = ?,"
                + " marriage = ?"
                + " WHERE user_id = ?",
                user.getPassword(),
                user.getUserName(),
                user.getBirthday(),
                user.getAge(),
                user.isMarriage(),
                user.getUserId());

        //トランザクション確認のため、わざと例外をthrow
                if (rowNumber > 0) {
                    throw new DataAccessException("トランザクションテスト") {
                    };
                }

        return rowNumber;
    }
//略(全文は下記参考)

サービスクラス修正

@Transactional

  • トランザクションを使うためには、クラスに@Transactinalアノテーションを付ける (一般的には、ビジネスロジックを担当するサービスクラス)
  • 引数を付けることでトランザクションのレベルを設定できる
UserService.java
//略(全文は下記参考)
@Transactional
@Service
public class UserService {
    @Autowired
    @Qualifier("UserDaoJdbcImpl")
    UserDao dao;

//略(全文は下記参考)
}

コントローラクラス修正

  • try~catchでDBで例外発生しても引き続き画面を表示する
HomeController.java
//略(全文は下記参考)

    /**
     * ユーザー更新用処理.
     */
    @PostMapping(value = "/userDetail", params = "update")
    public String postUserDetailUpdate(@ModelAttribute SignupForm form,
            Model model) {

        System.out.println("更新ボタンの処理");

        //Userインスタンスの生成
        User user = new User();

        //フォームクラスをUserクラスに変換
        user.setUserId(form.getUserId());
        user.setPassword(form.getPassword());
        user.setUserName(form.getUserName());
        user.setBirthday(form.getBirthday());
        user.setAge(form.getAge());
        user.setMarriage(form.isMarriage());

        try {

            //更新実行
            boolean result = userService.updateOne(user);

            if (result == true) {
                model.addAttribute("result", "更新成功");
            } else {
                model.addAttribute("result", "更新失敗");
            }

        } catch (DataAccessException e) {

            model.addAttribute("result", "更新失敗(トランザクションテスト)");

        }

        //ユーザー一覧画面を表示
        return getUserList(model);
    }

//略(全文は下記参考)

SpringBootを起動してホーム画面確認!

  • http://localhost:8080/home
  • ユーザ一覧からユーザ詳細画面に移り、ユーザー名更新
  • 更新しても、ユーザ名が変わっていない(更新失敗と表示)、トランザクションがロールバックできていることがわかりました〜〜!^^

スクリーンショット 2020-11-29 11.46.11.png

スクリーンショット 2020-11-29 11.45.56.png

(参考)コード全文

UserDaoJdbcImpl.java
package com.example.demo.login.domain.repository.jdbc;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import com.example.demo.login.domain.model.User;
import com.example.demo.login.domain.repository.UserDao;

@Repository("UserDaoJdbcImpl")
public class UserDaoJdbcImpl implements UserDao {

    @Autowired
    JdbcTemplate jdbc;

    // Userテーブルの件数を取得.
    @Override
    public int count() throws DataAccessException {

        //全件取得してカウント
        int count = jdbc.queryForObject("SELECT COUNT(*) FROM m_user", Integer.class);

        return count;
    }

    // Userテーブルにデータを1件insert.
    @Override
    public int insertOne(User user) throws DataAccessException {

        //1件登録
        int rowNumber = jdbc.update("INSERT INTO m_user(user_id,"
                + " password,"
                + " user_name,"
                + " birthday,"
                + " age,"
                + " marriage,"
                + " role)"
                + " VALUES(?, ?, ?, ?, ?, ?, ?)",
                user.getUserId(),
                user.getPassword(),
                user.getUserName(),
                user.getBirthday(),
                user.getAge(),
                user.isMarriage(),
                user.getRole());

        return rowNumber;
    }

    // Userテーブルのデータを1件取得
    @Override
    public User selectOne(String userId) throws DataAccessException {

        // 1件取得
        Map<String, Object> map = jdbc.queryForMap("SELECT * FROM m_user"
                + " WHERE user_id = ?", userId);

        // 結果返却用の変数
        User user = new User();

        // 取得したデータを結果返却用の変数にセットしていく
        user.setUserId((String) map.get("user_id")); //ユーザーID
        user.setPassword((String) map.get("password")); //パスワード
        user.setUserName((String) map.get("user_name")); //ユーザー名
        user.setBirthday((Date) map.get("birthday")); //誕生日
        user.setAge((Integer) map.get("age")); //年齢
        user.setMarriage((Boolean) map.get("marriage")); //結婚ステータス
        user.setRole((String) map.get("role")); //ロール

        return user;

    }

    // Userテーブルの全データを取得.
    @Override
    public List<User> selectMany() throws DataAccessException {

        // M_USERテーブルのデータを全件取得
        List<Map<String, Object>> getList = jdbc.queryForList("SELECT * FROM m_user");

        // 結果返却用の変数
        List<User> userList = new ArrayList<>();

        // 取得したデータを結果返却用のListに格納していく
        for (Map<String, Object> map : getList) {

            //Userインスタンスの生成
            User user = new User();

            // Userインスタンスに取得したデータをセットする
            user.setUserId((String) map.get("user_id")); //ユーザーID
            user.setPassword((String) map.get("password")); //パスワード
            user.setUserName((String) map.get("user_name")); //ユーザー名
            user.setBirthday((Date) map.get("birthday")); //誕生日
            user.setAge((Integer) map.get("age")); //年齢
            user.setMarriage((Boolean) map.get("marriage")); //結婚ステータス
            user.setRole((String) map.get("role")); //ロール

            //結果返却用のListに追加
            userList.add(user);
        }

        return userList;
    }

    // Userテーブルを1件更新.
    @Override
    public int updateOne(User user) throws DataAccessException {

        //1件更新
        int rowNumber = jdbc.update("UPDATE M_USER"
                + " SET"
                + " password = ?,"
                + " user_name = ?,"
                + " birthday = ?,"
                + " age = ?,"
                + " marriage = ?"
                + " WHERE user_id = ?",
                user.getPassword(),
                user.getUserName(),
                user.getBirthday(),
                user.getAge(),
                user.isMarriage(),
                user.getUserId());

        //トランザクション確認のため、わざと例外をthrowする
                if (rowNumber > 0) {
                    throw new DataAccessException("トランザクションテスト") {
                    };
                }

        return rowNumber;
    }

    // Userテーブルを1件削除.
    @Override
    public int deleteOne(String userId) throws DataAccessException {

        //1件削除
        int rowNumber = jdbc.update("DELETE FROM m_user WHERE user_id = ?", userId);

        return rowNumber;
    }

    //SQL取得結果をサーバーにCSVで保存する
    @Override
    public void userCsvOut() throws DataAccessException {

        // M_USERテーブルのデータを全件取得するSQL
        String sql = "SELECT * FROM m_user";

        // ResultSetExtractorの生成
        UserRowCallbackHandler handler = new UserRowCallbackHandler();

        //SQL実行&CSV出力
        jdbc.query(sql, handler);
    }
}
UserService.java
package com.example.demo.login.domain.service;

import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.dao.DataAccessException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.example.demo.login.domain.model.User;
import com.example.demo.login.domain.repository.UserDao;

@Transactional
@Service
public class UserService {
    @Autowired
    @Qualifier("UserDaoJdbcImpl")

    UserDao dao;

    /**
     * insert用メソッド.
     */
    public boolean insert(User user) {
        // insert実行
        int rowNumber = dao.insertOne(user);
        // 判定用変数
        boolean result = false;

        if (rowNumber > 0) {
            // insert成功
            result = true;
        }
        return result;
    }

    /**
     * カウント用メソッド.
     */
    public int count() {
        return dao.count();
    }

    /**
     * 全件取得用メソッド.
     */
    public List<User> selectMany() {
        // 全件取得
        return dao.selectMany();
    }

    /**
     * 1件取得用メソッド.
     */
    public User selectOne(String userId) {
        // selectOne実行
        return dao.selectOne(userId);
    }

    /**
     * 1件更新用メソッド.
     */
    public boolean updateOne(User user) {

        // 判定用変数
        boolean result = false;

        // 1件更新
        int rowNumber = dao.updateOne(user);

        if (rowNumber > 0) {
            // update成功
            result = true;
        }

        return result;
    }

    /**
     * 1件削除用メソッド.
     */
    public boolean deleteOne(String userId) {

        // 1件削除
        int rowNumber = dao.deleteOne(userId);

        // 判定用変数
        boolean result = false;

        if (rowNumber > 0) {
            // delete成功
            result = true;
        }
        return result;
    }
    // ユーザー一覧をCSV出力する.

    public void userCsvOut() throws DataAccessException {
        // CSV出力
        dao.userCsvOut();
    }

    /**
     * サーバーに保存されているファイルを取得して、byte配列に変換する.
     */
    public byte[] getFile(String fileName) throws IOException {

        // ファイルシステム(デフォルト)の取得
        FileSystem fs = FileSystems.getDefault();

        // ファイル取得
        Path p = fs.getPath(fileName);

        // ファイルをbyte配列に変換
        byte[] bytes = Files.readAllBytes(p);

        return bytes;
    }
}

HomeController.java
package com.example.demo.login.controller;

import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;

import com.example.demo.login.domain.model.SignupForm;
import com.example.demo.login.domain.model.User;
import com.example.demo.login.domain.service.UserService;

@Controller
public class HomeController {

    @Autowired
    UserService userService;

    //結婚ステータスのラジオボタン用変数
    private Map<String, String> radioMarriage;

    /**
     * ラジオボタンの初期化メソッド(ユーザー登録画面と同じ).
     */
    private Map<String, String> initRadioMarrige() {

        Map<String, String> radio = new LinkedHashMap<>();

        // 既婚、未婚をMapに格納
        radio.put("既婚", "true");
        radio.put("未婚", "false");

        return radio;
    }

    /**
     * ホーム画面のGET用メソッド
     */
    @GetMapping("/home")
    public String getHome(Model model) {

        //コンテンツ部分にユーザー詳細を表示するための文字列を登録
        model.addAttribute("contents", "login/home :: home_contents");

        return "login/homeLayout";
    }

    /**
     * ユーザー一覧画面のGETメソッド用処理.
     */
    @GetMapping("/userList")
    public String getUserList(Model model) {

        //コンテンツ部分にユーザー一覧を表示するための文字列を登録
        model.addAttribute("contents", "login/userList :: userList_contents");

        //ユーザー一覧の生成
        List<User> userList = userService.selectMany();

        //Modelにユーザーリストを登録
        model.addAttribute("userList", userList);

        //データ件数を取得
        int count = userService.count();
        model.addAttribute("userListCount", count);

        return "login/homeLayout";
    }

    /**
     * ユーザー詳細画面のGETメソッド用処理.
     */
    @GetMapping("/userDetail/{id:.+}")
    public String getUserDetail(@ModelAttribute SignupForm form,
            Model model,
            @PathVariable("id") String userId) {

        // ユーザーID確認(デバッグ)
        System.out.println("userId = " + userId);

        // コンテンツ部分にユーザー詳細を表示するための文字列を登録
        model.addAttribute("contents", "login/userDetail :: userDetail_contents");

        // 結婚ステータス用ラジオボタンの初期化
        radioMarriage = initRadioMarrige();

        // ラジオボタン用のMapをModelに登録
        model.addAttribute("radioMarriage", radioMarriage);

        // ユーザーIDのチェック
        if (userId != null && userId.length() > 0) {

            // ユーザー情報を取得
            User user = userService.selectOne(userId);

            // Userクラスをフォームクラスに変換
            form.setUserId(user.getUserId()); //ユーザーID
            form.setUserName(user.getUserName()); //ユーザー名
            form.setBirthday(user.getBirthday()); //誕生日
            form.setAge(user.getAge()); //年齢
            form.setMarriage(user.isMarriage()); //結婚ステータス

            // Modelに登録
            model.addAttribute("signupForm", form);
        }

        return "login/homeLayout";
    }

    /**
     * ユーザー更新用処理.
     */
    @PostMapping(value = "/userDetail", params = "update")
    public String postUserDetailUpdate(@ModelAttribute SignupForm form,
            Model model) {

        System.out.println("更新ボタンの処理");

        //Userインスタンスの生成
        User user = new User();

        //フォームクラスをUserクラスに変換
        user.setUserId(form.getUserId());
        user.setPassword(form.getPassword());
        user.setUserName(form.getUserName());
        user.setBirthday(form.getBirthday());
        user.setAge(form.getAge());
        user.setMarriage(form.isMarriage());

        try {

            //更新実行
            boolean result = userService.updateOne(user);

            if (result == true) {
                model.addAttribute("result", "更新成功");
            } else {
                model.addAttribute("result", "更新失敗");
            }

        } catch (DataAccessException e) {

            model.addAttribute("result", "更新失敗(トランザクションテスト)");

        }

        //ユーザー一覧画面を表示
        return getUserList(model);
    }

    /**
     * ユーザー削除用処理.
     */
    @PostMapping(value = "/userDetail", params = "delete")
    public String postUserDetailDelete(@ModelAttribute SignupForm form,
            Model model) {

        System.out.println("削除ボタンの処理");

        //削除実行
        boolean result = userService.deleteOne(form.getUserId());

        if (result == true) {

            model.addAttribute("result", "削除成功");

        } else {

            model.addAttribute("result", "削除失敗");

        }

        //ユーザー一覧画面を表示
        return getUserList(model);
    }

    /**
     * ログアウト用処理.
     */
    @PostMapping("/logout")
    public String postLogout() {

        //ログイン画面にリダイレクト
        return "redirect:/login";
    }

    /**
     * ユーザー一覧のCSV出力用処理.
     */
    @GetMapping("/userList/csv")
    //public String getUserListCsv(Model model) {
    public ResponseEntity<byte[]> getUserListCsv(Model model) {

        //ユーザーを全件取得して、CSVをサーバーに保存する
        userService.userCsvOut();

        byte[] bytes = null;

        try {

            //サーバーに保存されているsample.csvファイルをbyteで取得する
            bytes = userService.getFile("sample.csv");

        } catch (IOException e) {
            e.printStackTrace();
        }

        //HTTPヘッダーの設定
        HttpHeaders header = new HttpHeaders();
        header.add("Content-Type", "text/csv; charset=UTF-8");
        header.setContentDispositionFormData("filename", "sample.csv");

        //sample.csvを戻す
        return new ResponseEntity<>(bytes, header, HttpStatus.OK);

    }
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Quarkusについて調べてみたらハマった話

今年も参加してみました。
滑り込みアウトですが、ALH Advent Calendar 2020の6日目です。

前日は「なんかDB遅いな。調べておいて~」って振られた時に見ること-wolさんでした。

Quarkusとは何ぞや

image.png

きっかけ

普段、マイクロサービスやコンテナ周りの技術習得を目指し勉強しているのですが、Kubernetes向け軽量コンテナを実現するスタックとしてRedHatが何か作っているらしいぞ、という情報を耳にしたため調べてみました。

Quarkus

Quarkus - SUPERSONIC SUBATOMIC JAVA
名前からしてそそられますね。

A Kubernetes Native Java stack tailored for OpenJDK HotSpot and GraalVM, crafted from the best of breed Java libraries and standards.

  • コンテナファースト:コンテナでの実行に最適化された最小フットプリント(リソース占有が少ない)のJavaアプリケーション
  • クラウドネイティブ:Kubernetes環境のようにThe Twelve-Factor Appのアーキテクチャを採用している
  • 手続き型とリアクティブ型の統合:1つのプログラミングモデルの下で、命令型と非ブロッキングの開発スタイルを実現する
  • スタンダードに基づく:Java標準と各種フレームワークに対応(RESTEasy and JAX-RS, Hibernate ORM and JPA, Netty, Eclipse Vert.x, Eclipse MicroProfile, Apache Camel...)
  • マイクロサービスファースト:非常に高速な起動時間
  • 開発者にやさしい:妥協のない開発中心のエクスペリエンス

引用:https://github.com/quarkusio/quarkus

OpenJDK HotSpotおよびGraalVM用に調整されたKubernetesネイティブJavaスタックだそうです。
Javaはアプリケーションの起動が遅いなんてよく言われますが、アプリケーションのコンテナ化が主流になってきた中で、リソース弾力性のあるアプリが求められる中、スケールアウト時の挙動が遅いと瞬間的なアクセスに対応できません。

GraalVMという仮想マシンを使って起動のリードタイム短縮するためのスタックが生まれた、という背景があるようですね。

そもそもGraalVMって何?

その前にJVMの話

JavaはJVM上で動くという、初歩的な話から進めます。

最近は、Webアプリケーション開発から入ってEclipse上で開発だ!が主流だとクラスファイル*.classが生成されてjarとかwarで固められる、ぐらいの理解はあっても、実際にjavacを使ってコンパイルする、という機会はほとんどないと思います。
ひと昔前はAntビルド、最近はMavenが主流ですね。

知らなくてもよいことを知る必要がない、のはライブラリなどに代表されるようにカプセル化・隠ぺいされることはよいことなのですが、パフォーマンスチューニングとか障害発生時とか、仕組みを知っていないと困る時もあります。
初級プログラマーを抜けていくためのステップですね。プログラマにコンピュータサイエンスが必要かどうかという話もありますがこの話はここでは割愛します。
また、知らないことは必要になったときに調べればよいですが、何も知らないと調べること自体が大変になります。

Javaはコンパイラ言語ということは知ってると思いますが、コンパイラにも種類があります。

コンパイラの種類 何から 何へ
Javaコンパイラ javaソースコード(*.java) クラスファイル(*.class)
ネイティブコンパイラ javaソースコード
またはバイトコード
ネイティブコードの実行ファイル
動的コンパイラ(JIT) メモリ上のjavaバイトコード メモリ上のネイティブコード

image.png

JITコンパイラはデフォルトで有効になっており、Javaメソッドが呼び出されるとアクティブになります。 JITコンパイラは、そのメソッドのバイトコードをネイティブコードにコンパイルし、実行するために"just in time"コンパイルします。

ここでGraalVM

以上を踏まえてGraalVMの特徴は、GraalというJITコンパイラが実装されています。JVM Compiler Interfaceを利用してC2コンパイラを置き換えます。

初めはC1を使い、HotSpotがさらに多くの呼び出しを検知するとメソッドはC2を使って再度コンパイルされます。ほとんどのJavaアプリケーションにとって、C2コンパイラが環境のもっとも重要なパーツの1つであり、これがプログラムのもっとも重要な部分に対し、高度に最適化されたマシンコードを生成するからです。

そのC2の元々c++で実装されている箇所が、Javaで置き換えられ、最適化技術に改良が加えられ、パフォーマンスが向上しました。

image.png

Native Image

GraalVMではJavaのコードをAOTコンパイルすることによりNative Image化を行なうことができます。AOTコンパイルとはJIT "just in time"(実行時)コンパイルではなく、"ahead of time"事前コンパイルを行うことです。

Native Image化を行なうと以下のような特徴を得ることができます。

  • アプリ起動時にクラスロードや初期化処理が不要になるため、起動が早くなる
  • メモリフットプリント(リソース占有)を削減することができる

以上から、スケールする軽量コンテナと相性がいいというわけですね。

その他

GraalVMの特徴に多言語に対応できるTruffleという仕組みがありますが、本題ではないのでここでは割愛します。
GraalVM上ではJavaScriptやRubyも実行できます。

困らない程度のJDK入門 - slideshare
Getting to Know Graal, the New Java JIT Compiler - InfoQ
JVM JITコンパイラの仕組み - Qiita
GraalVMに入門する - Uzabase Tech Blog
GraalVM の概要と、Native Image 化によるSpring Boot 爆速化の夢 - slideshare

GraalVMのNativeImage化をやってみた

よしよし、なんとなく仕組みはわかりました。なので実際にGraalVMのNative Image化してみましょう。

前提

  • macOS Catalina
  • zsh
  • Java 11
  • graalvm-ce

準備

GraalVMのインストール

公式の手順に従って設定していきます。

https://www.graalvm.org/docs/getting-started-with-graalvm/macos/

以下のリリースからパッケージをダウンロードします。
https://github.com/graalvm/graalvm-ce-builds/releases
image.png

解凍します。

$ tar -xvf graalvm-ce-java11-darwin-amd64-20.30.0.tar.gz

macOSの場合、Javaのシステムディレクトリに移動させます。

$ sudo mv graalvm-ce-java11-20.3.0 /Library/Java/JavaVirtualMachines

※パスワード入力が求められます。

インストールしたGraalVMが表示されることを確認します。

$ /usr/libexec/java_home -V
Matching Java Virtual Machines (3):
    12, x86_64: "Java SE 12"    /Library/Java/JavaVirtualMachines/jdk-12.jdk/Contents/Home
    11.0.9, x86_64: "GraalVM CE 20.3.0" /Library/Java/JavaVirtualMachines/graalvm-ce-java11-20.3.0/Contents/Home
    11.0.2, x86_64: "Java SE 11.0.2"    /Library/Java/JavaVirtualMachines/jdk-11.0.2.jdk/Contents/Home

/Library/Java/JavaVirtualMachines/jdk-12.jdk/Contents/Home

以下、環境によって違いますが、プロファイルでPATHの変更をします。
参考:MacのBrewで複数バージョンのJavaを利用する + jEnv

当環境はzshなので.zsh_profileに以下追記します

export JAVA_HOME=/Library/Java/JavaVirtualMachines/graalvm-ce-java11-20.3.0/Contents/Home
# インストールしているJavaバージョンによっては以下でも可能です
# export JAVA_HOME=`/usr/libexec/java_home -v "11"`
PATH=${JAVA_HOME}/bin:${PATH}

PATH変更前

$ java -version
java version "11.0.2" 2019-01-15 LTS
Java(TM) SE Runtime Environment 18.9 (build 11.0.2+9-LTS)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.2+9-LTS, mixed mode)

PATH変更後

$ java -version
openjdk version "11.0.9" 2020-10-20
OpenJDK Runtime Environment GraalVM CE 20.3.0 (build 11.0.9+10-jvmci-20.3-b06)
OpenJDK 64-Bit Server VM GraalVM CE 20.3.0 (build 11.0.9+10-jvmci-20.3-b06, mixed mode, sharing)

Native Imageのインストール

NativeImageビルドの機能を利用したいので、こちらもインストールします。
GraalVMをインストールすると、GraalVM Updater Toolもインストールされるので以下のコマンドを実行します。

terminal
$ gu install native-image

コーディング

GraalVM公式のリファレンスに沿ってNativeImageを試しに実装してみます。
再帰呼び出しにより文字列を反転させます。

terminal
$ mkdir graalvm
$ cd graalvm 
Sample.java
public class Sample {
    public static void main(String[] args) {
        String str = "Native Image is awesome";
        String reversed = reverseString(str);
        System.out.println("reverse: " + reversed); // reverse: mosewa si egamI evitaN
    }

    private static String reverseString(String str) {
        if (str.isEmpty())
            return str;
        return reverseString(str.substring(1)) + str.charAt(0);
    }
}

実行ファイルのビルドの分だけ時間がかかりますが、このレベルのクラスの実行速度でも全然違いますね!

javac
$ time javac Sample.java
javac Sample.java  1.09s user 0.20s system 106% cpu 1.210 total

$ time java Sample
reverse: emosewa si egamI evitaN
java Sample  0.15s user 0.08s system 43% cpu 0.534 total
native-image
$ time javac Sample.java
# コンパイルまでは同様のため割愛

$ time native-image Sample
[sample:22975]    classlist:   1,220.44 ms,  0.96 GB
[sample:22975]        (cap):   3,556.39 ms,  0.96 GB
[sample:22975]        setup:   4,944.21 ms,  0.96 GB
[sample:22975]     (clinit):     157.30 ms,  1.22 GB
[sample:22975]   (typeflow):   4,157.74 ms,  1.22 GB
[sample:22975]    (objects):   3,755.36 ms,  1.22 GB
[sample:22975]   (features):     200.18 ms,  1.22 GB
[sample:22975]     analysis:   8,530.65 ms,  1.22 GB
[sample:22975]     universe:     299.25 ms,  1.22 GB
[sample:22975]      (parse):   1,030.67 ms,  1.22 GB
[sample:22975]     (inline):     923.63 ms,  1.67 GB
[sample:22975]    (compile):   6,578.55 ms,  2.28 GB
[sample:22975]      compile:   9,004.62 ms,  2.28 GB
[sample:22975]        image:   1,189.74 ms,  2.28 GB
[sample:22975]        write:     305.47 ms,  2.28 GB
[sample:22975]      [total]:  25,641.41 ms,  2.28 GB
native-image Sample  107.22s user 3.87s system 415% cpu 26.733 total

$ time ./sample
reverse: emosewa si egamI evitaN
./sample  0.00s user 0.00s system 1% cpu 0.343 total
実行方法 コンパイル時間 ビルド時間 起動時間
クラスファイル 1.09秒 - 0.15秒
ネイティブコード 同上 107.22秒 0.00秒

msecでないと測れないようです。

Quarkusを使ってみる

ここから本題。今まではGraalVMの説明でした。では、実際にQuarkusを使ってみましょう。

GraalVMとしては、SpringはDIコンテナをはじめとして、リグレクションやダイナミックプロキシによる動的なクラス生成を多用しているため、Native Image化とは相性が悪い=対応していないようです。
2019年時点では対応中ですとのことでしたが、2020年も厳しいようです。

Spring Native for GraalVM 0.8.3 available now - spring.io
※2020/11/23時点
正式版ではないですが開発は進められているようなので期待しましょう!詳細はこちら

それとは別にQuarkusでもSpring対応しているようなので、今回はSpringWebを試してみたいと思います。

Archetypeの取得

MavenのArchetypeが用意されているので取得します。

terminal
$ mvn io.quarkus:quarkus-maven-plugin:1.10.2.Final:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=spring-web-quickstart \
    -DclassName="org.acme.spring.web.GreetingController" \
    -Dpath="/greeting" \
    -Dextensions="spring-web"

pom.xmlを見てみると専用のプラグインも用意されているようですね。

pom.xml
      <plugin>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-maven-plugin</artifactId>
        <version>${quarkus-plugin.version}</version>
        <extensions>true</extensions>
        <executions>
          <execution>
            <goals>
              <goal>build</goal>
              <goal>generate-code</goal>
              <goal>generate-code-tests</goal>
            </goals>
          </execution>
        </executions>
      </plugin>

Controllerがいるのでとりあえず実行してみます。

GreetingController.java
package org.acme.spring.web.spring.web;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/greeting")
public class GreetingController {

    @GetMapping
    public String hello() {
        return "Hello Spring";
    }
}

ビルド

公式リファレンスには開発モードでの実行が紹介されていますが、せっかくなのでdockerで動かしてみます。

spring-web-quickstart/src/dockerDockerfile.nativeというファイルがあるのでこれに従ってビルドします。

terminal
$ ./mvnw package -Pnative

イメージビルドします。

terminal
$ docker build -f src/main/docker/Dockerfile.native -t quarkus/spring-web-quickstart .

$ docker images
REPOSITORY                                    TAG                 IMAGE ID            CREATED              SIZE
quarkus/spring-web-quickstart                 latest              df12a35b655b        About a minute ago   138MB

起動します。

terminal
$ docker run -i --rm -p 8080:8080 quarkus/spring-web-quickstart
standard_init_linux.go:211: exec user process caused "exec format error"

あら。。。エラーで立ち上がりません。

調べてみると、Rasberry-PIでdockerが起動しない場合と同様、ビルド環境と実行環境が同一CPUアーキテクチャでないとダメなようです。ネイティブコードの時点で確かにそうですね。。

Linux上の仮想環境上でビルドデプロイしないとなので、今回はタイムアップで次回試してみようと思います、というところでお茶を濁しちゃいます。

感想

結論、QuarkusのメリットであるGraalVMを使用したNativeImage化までできなかったのですが、SpringスタックもNativeImageに対応するそうなので、今後注視していきたいと思います。

まだ新しめの技術スタックなので、今後のRedHatにも期待ですが、現行システムでRHELの場合は、RedHat公式イメージ(ubi)もあり相性はよいのではと思ってます。

参考

spring-graal-nativeでSpring BootをGraalVM native imageにしてみる - Qiita
Quarkus: コンテナ上で Java アプリを高速起動する新しい手法のご紹介
JavaアプリをNativeコンパイルして爆速で起動するQuarkusを試してたら利用例にプルリクエストがマージされた - HatenaBlog

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む