Springbootを利用したWebアプリケーションの各種サンプル実装を行うサイト.
対象のSpringbootのver.はv2.4.0.RELEASE
Spring Initializrを利用したセットアップ
作成したいアプリの名前でフォルダを作成し,そのフォルダをvscodeで開く
表示->コマンドパレット,を選択し,Spring Initializr:Generate a Gradle Project を実行する
Specify project language: Java
Input Group Id for your project: jp.ac.oit.is.{チーム名をアルファベット小文字で}
例:jp.ac.oit.is.inudaisuki
スペースや特殊文字を含めないこと.すべてアルファベット小文字.
必ずしもjp.ac.oitから始まらなくても良い
Input Artifact Id for your project: {アプリ名をアルファベット小文字で}
例:dogland
スペースや特殊文字を含めないこと(_は入力できるが,不具合を誘発することがあるので使わないほうが良い).すべてアルファベット小文字.また,セットアップ時に作成したフォルダ名と同じにしておくこと.
Specify Spring Boot version.: 2.2.4
Search for dependencies
以下を選択
Spring Web
Thymeleaf
Mybatis Framework
H2 Database
Spring Security
フォルダの選択を行うダイヤログが表示されるので,現在のフォルダのひとつ上(通常is_dev20などになる)を指定して,springbootのためのフォルダを作成させる.
このとき,すでにあるフォルダを上書き(overwrite)してよいか問い合わせがあるので,OKすること.
Successfully Generatedと出ればOK.
.gitignore作成
build.gradleを修正
以下のファイルを修正する
settings.gradle
rootProject.name = 'springbootsamples' の行の'springbootsamples'をアプリ名に変更する.
小文字アルファベットだけから構成されるものにすること(半角スペースや全角文字は不可)
build.gradle
group = 'jp.ac.oit.igakilab' の行の右側を自分たちのグループ名に変更する.
例:'jp.ac.oit.inudaisuki'
小文字アルファベットだけから構成されるものにすること(半角スペースや全角文字は不可)
src\main\java 以下のフォルダ構成とgroup及びrootProject.nameのアプリ名はおなじになるようにしておくこと.src\test\java も同じ
同じくクラスファイルのパッケージの修正も必要
C:\Users\...\oithomes\advanced\springboot_samples\src\main\resources\application.properties
に以下のような設定を追記
server .jetty .accesslog .filename =jetty -access .log
server .jetty .accesslog .enabled =true
server .jetty .accesslog .date -format =yyyy /mm /dd :HH :mm :ss Z
server .jetty .accesslog .time -zone =JST
server .jetty .accesslog .log -server =true
server .port =8000
spring .datasource .sql -script -encoding =UTF -8
プロジェクトのbuild/logsフォルダにアクセスログが保存されるようになる.LogFormat等は今後要検討
ポート番号 server.port=8000
を設定することで, http://localhost:8000/ でSpringBootアプリが動作するようになる
vscodeからターミナル->新しいターミナル,を選択し,bashのターミナルをエディタ下部に開く
build.gradleファイルがあるのと同じディレクトリにいることを確認後,gradle bootRun
を実行するとSpringBootアプリがビルドされ,組み込みjettyで起動する
gradle build
を実行するとbuild/libs/ 以下に作成されるjarを対象に,java -jar ???.jar でもSpringBootWebアプリケーションを起動できる
http://localhost:8000/ にアクセスしたときになにかWebページが表示されていればOK.
HTTP/GET,POSTを利用したWebアプリケーションの作成方法
[Sample1-1]templateを利用したhtmlファイルの表示(HTTP/GET)
特定のURLリクエストに対して静的なhtmlを返すアプリケーション
@Controller
@RequestMapping
[Sample1-2] java-html間の値の受け渡し(HTTP/POST, タイムリーフ)
Webページのフォームで入力した値をjavaにPOSTし,処理した結果をhtmlで受け取って表示する.
@Controller
@RequestMapping (クラス)
@PostMapping 及び @RequestParam
@GetMapping
ModelMapを利用したJavaからHTMLへの値渡し
urlでリクエストをかけると,htmlではなく何らかの値や文字列が返ってくるRestAPIを実装する
[Sample2-1] RestControllerの基本的な利用方法
Classにapiという名前をつけて,メソッドにrest21という名前をつける.
/api/rest21 にGETリクエストすると,Javaのrest21メソッドの返り値がかえってくる(HTMLではない)
@RestController
@RequestMapping
@GetMapping
$ curl -s http://localhost:8000/api/sample21
Hello sample21!
curlの-s
オプションはプログレス情報を表示しないためのもの
[Sample2-2] パラメータを渡してRestAPIを呼ぶ方法
パスパラメータ,クエリパラメータの2種類の方法でRestAPIを呼び,Javaに値を渡す方法
@RestController
@RequestMapping
@GetMapping
パスパラメータ:@PathVariable
クエリパラメータ:@RequestParam
bashターミナルで下記のようになればOK.ブラウザでも確認できる.
上2つがパスパラメータ,3つ目がクエリパラメータ.
$ curl -s http://localhost:8000/api/sample22/hoge
受け取ったパラメータはhogeです
$ curl -s http://localhost:8000/api/sample22/hoge/fuga
受け取ったパラメータはhogeとfugaです
$ curl -s http://localhost:8000/api/sample22? param=ora
受け取ったクエリパラメータはoradayo
curlの-s
オプションはプログレス情報を表示しないためのもの
[Sample3-1]最もシンプルなベーシック認証
特定のURLにアクセスする際にID・パスワードでの認証を行う
ベーシック認証
アカウントの追加
指定したURLリクエストを対象とした認証の追加
@Configuration
@EnableWebSecurity // securityモジュールを利用するためのアノテーション
ブラウザで http://localhost:8000/sample3/sample31
にアクセスする
ベーシック認証のダイアログが表示されるので,user/password と入力する.
Authenticated!
と表示されればOK
[Sample3-2]ベーシック認証時にログインユーザ名を取得する方法
Principalを利用したログインユーザ情報の取得
タイムリーフを利用したModelMapからの値の取得
[Sample4-1]DBのテーブル設定,値登録とselectによる取得
schema.sqlの内容でテーブルを構築し,data.sqlの内容で初期データを登録する.
DBからSELECT文で値を取得し,Fruitsオブジェクトに格納する処理をMybatisのマッパー機能を利用して実装する.
implementation ' org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.1'
runtimeOnly ' com.h2database:h2'
これが記述されているとH2DBの場合はあらゆるDB接続関連の設定をSpringBootが自動でやってくれるようになる.
@Controller
@GetMapping
Mybatisのmapper interface
@Transactional
@Mapper
@Select
Sample4Controllerクラスのsample41GetFruitsメソッド内でDBから値を取得してFruitsオブジェクトに格納し,表示している.
[Sample4-2]DBの値を取得し,GETでHTMLに渡して表示する方法
@Controller
@Transactional
@GetMapping
タイムリーフを利用した値の表示
Sample4Controllerクラスのsample42GetFruitsメソッド内でDBから値を取得してFruitsオブジェクトに格納し,ModelMapオブジェクトに渡してhtmlから参照している.
[Sample4-3] フォームでPOSTしたデータをDBに登録する
オブジェクトごとPOSTでデータをHTMLからJavaに渡し,insert文をmapperインタフェースを利用して実行する
@GetMapping
@PostMapping
@ModelAttribute
@Insert
@Options
@Transactional
@TransactionalをDBに書き込みを行う場合はつけておくと良いらしい(失敗した際に自動でロールバックしてくれる)
http://localhost:8000/sample43 にブラウザでアクセスし,名前に果物の名前を数に数値を入れて「送信」ボタンをクリックするとターミナルに以下のように入力した果物名や数値が表示されればOK
フォームに入力して「送信」すると,id2として(auto increment)送信した内容がDBに登録され,それがそのままフルーツ2としてターミナルに表示される
http://localhost:8000/h2-console にアクセスし,application.propertiesの以下の項目に従って入力する
[Saved Settings] Generic H2(Embedded)
[Driver Class] org.h2.Driver
[JDBR URL] jdbc:h2:mem:testdb
[User Name] sa
Connectをクリックするとjdbc:h2:mem:testdbに接続される.
FRUITSというテーブルが作成されているので選択し,SELECT文などをRunさせると,テーブルにデータがINSERTされていることが確認できる.
Sample3BasicAuthConfiguration.javaに設定したようにCSRFとX-Frameの設定を解除しないとh2-consoleは正しくConnectできない.
spring .datasource .url =jdbc :h2 :mem :testdb
# H2DBを 利用する 場合のドライバ 名,ユーザ 名,パスワード (なし )
spring .datasource .driverClassName =org .h2 .Driver
spring .datasource .username =sa
spring .datasource .password =
[Sample4-4] DBから複数の値をArrayListで取得し,htmlで表示するサンプル
実装:
Sample4Controller.java
FruitsMapper.java
sample44.html
@Select
@GetMapping
Mapperを利用したFruitsオブジェクトのArrayList取得
index:0 id:1 さがほのか 10 5.8 いちご false
index:1 id:2 レモン 100 0.0 false
最初のindexはステータス変数(stat.index)の値.他のステータス変数については参考資料参照.
DBから複数の値をArrayListで取得し,htmlで表示するサンプル
SseEmitterとEventSourceを利用して非同期呼び出しを行うサンプル
参考:
確認1: curl -i -s -N http://localhost:8000/api/streaming?eventNumber=5\&intervalSec=1
-i
はHTTPヘッダを表示するオプション, -s
はダウンロード関連の表示を省略するオプション-N
はバッファを利用しないオプション(このオプションがないとレスポンスが非同期じゃなくまとめて最後に来るようになる)
StreamingControllerからAsyncHelperのstreamingメソッドが非同期に呼び出されて,レスポンスが非同期に返ってくる
確認2: curl -i -s -N http://localhost:8000/api/sse
SseController内の一部の処理が非同期に呼び出される(L28-L43).ただ,一部を非同期にするためにConcurrentパッケージのExecutorServiceが必要なのでおすすめしない
確認3: http://localhost:8000/ajaxFruits.html
普通のAjax呼び出しだが,非同期にはならず,すべての処理が終了してから画面が更新される.非推奨だが,一般的な書き方ということで(同期呼び出しならこれでいい).
確認4: http://localhost:8000/ajaxFruits2.html
EventSourceを利用.非同期に呼び出しが行われ,画面も非同期に更新される.
一度ページを表示すると,StreamingControllerの該当するstreamingメソッドが何度も呼び出されるっぽい.接続が切れると自動的に再接続してるのかも.
ポイント(StreamingController)
SseEmitterのところはResponseBodyEmitterでもいける.curlでの実行結果は同様だが,JSで呼び出す際に,↓のようなエラーがJS側で発生してしまう
EventSource's response has a MIME type ("text/plain") that is not "text/event-stream". Aborting the connection.
これはResponseBodyEmitterだとtext/plainでメッセージが返るため.SseEmitterだとtext/event-streamで返るのでこちらを使うと良い
@RestController
@Autowired
対象クラスのオブジェクトをnewして割り当ててくれる.
ただし,対象クラスに@Component とアノテーションがついていないと駄目.
この例の場合,AsyncHelperクラスに@Componentがついているので,@Autowiredで割当が行われる.
なお,対象クラスを非同期に呼び出したい場合は@Autowiredが必須っぽい.
L35で呼び出しているasyncHelper.streaming()は非同期に呼び出される.すなわち,L35の処理が終了する前にL37が呼び出される.
gradle bootRunを実行したターミナルを確認すると分かる
非同期に呼び出したい別クラスのメソッドには,引数に必ずSseEmitterクラスのオブジェクトを渡す必要がある.また,呼び出し元メソッド(この例の場合はStreamingController.streaming())の返り値をSseEmitterクラスのオブジェクトにする必要がある.
@Component
このオブジェクトを利用するStreamingControllerで@Autowiredするために必要な設定.要はnewをSpringbootにやってもらうため.
@Async
非同期呼び出しをしたいメソッド(要するに指定したメソッドが終了するのを呼び出し元で待たなくて良い)にアノテーションとして付与する
emitter.send()
対象メソッドの引数に与えられたSseEmitterにsend()で引数として他クラスのオブジェクトや文字列を与えることで,javascript側でオブジェクトの場合はjsonオブジェクトとして,文字列の場合は文字列として受け取ることができる.
emitter.complete()
これを呼び出すことで,emitterを利用して呼び出し処理が明示的に終了される.多分呼び出さないとemitterが終了されないままで,再利用できなくなる気がする.
また,これが呼び出されていると,JS側でEventSourceに指定されたapiが何度も呼び出されて,streamingメソッドも何度も呼び出されるが,complete()していないと,streamingメソッドは再度呼び出されることはなくなる(正確には呼ばれていてもemitterが機能しない?).
発生する例外について
asyncHelper.streaming()内の処理を実行している最中にブラウザが終了する等でクライアントとの接続が切れた場合,以下の例外が発生する.
java.lang.IllegalStateException: Calling [asyncError()] is not valid for a request with Async state [MUST_DISPATCH]
EventSource()
sse.onmessage
EventSourceからメッセージが送られてきたら(MessageEvent),function(evt)を処理する.
EventSourceはサーバから接続が切断されても自動的に再接続が行われるらしい.回避しようと思ったら,処理終了時や例外発生時(切断時に例外が発生する)にsse.close()を呼び出すと良い.
sse . onerror = function ( evt ) {
console . log ( "error!!" ) ;
sse . close ( ) ;
}
ブラウザ終了時
組み込みTomcatの場合はjava.lang.IllegalStateException: Calling [asyncError()] is not valid for a request with Async state [MUST_DISPATCH]
という例外が発生する.これはcatchしてログに表示しないようにするのは今の時点ではやり方がわからない
組み込みjettyの場合はorg.eclipse.jetty.io.EofException: null
という例外が発生する.こちらの場合は,AsyncHelper.java
のstreaming()
メソッド内部のemitter.send()
でIOExceptionが発生するので,下記のようにcatchしてやれば切断時の処理を正しく実装できる
@ Async
public void streaming (SseEmitter emitter , long eventNumber , int intervalSec ) throws InterruptedException {
System .out .println ("Start Async processing." );
for (int i = 1 ; i <= eventNumber ; i ++) {
TimeUnit .SECONDS .sleep (intervalSec );
Fruits fruits = new Fruits ();
fruits .setName ("メロン" );
fruits .setNum (i );
fruits .setWeight (i * 25.5 );
try {
emitter .send (fruits );
} catch (IOException e ) {
System .out .println ("IOException!" );
break ;
}
System .out .println ("java:msg" + i );
}
emitter .complete ();
System .out .println ("End Async processing." );
}