20210802のJavaに関する記事は5件です。

JavaGold学習メモ(第一章)

主旨 JavaGold資格取得に向けて学習したことの内容メモ(黒本をまずは読んでみる) 記憶の怪しい項目や知らなかったことをまとめる あとで見返せる程度で備忘録を残す(多分意味が自分しか理解できない記載も多い) 完全なメモなので誤字などもあるかもしれないが細かいことはひとまず気にしないでおく Javaのクラス設計について ■カプセル化 ・外部に公開するアクセス手段のメソッド→アクセサメソッド o r アクセサ ・finalやstaticを実装することとカプセル化は無関係 ・トップレベルクラス→ネストされたクラスでない通常のクラス→ファイル名がFoo.javaでクラスがFooの場合、トップレベルクラス ・一つのソースにおいて、宣言できるpublicなトップレベルクラスは一つだけ ■継承 ・is-a関係(AはBである) or kind -of-a関係(AはBの一種である) ・has-a関係(AはBを持っている)は、集約の関係。構成集約とも呼ばれる。 ・集約は関係するオブジェクトのライフライクルが同じではない→親クラスと学生クラス。親クラスを削除しても子クラスは残る。 ・構成集約は関係するオブジェクトのライフサイクルが同じ。Is-a-part-of関係とも呼ぶ→家クラスと部屋クラス。部屋クラスは家クラスがなくなると存在できない。依存関係 ・is-a→継承 ・has-a(Aggregation:白の◇)は集約 ・has-a(Composition:黒の?)は依存 ■ポリモーフィズム ・コンパイル時に決定する静的な型 ・実行時に複数の型を扱うことができる仕組み ・継承関係で持ちいることが多い(is-a関係) ・インターフェイスの実装、クラスやインターフェイスの継承 ・インターフェイスで宣言されている抽象メソッドを実装する場合にも@Overrideアノテーションを記述できるが、正しく実装できていない場合はアノテーションの有無に関わらずコンパイルエラーになる。 ■java.lang.Objectクラス ・equals(Object obj)、int hashCode()、String toString()メソッドが主要なメソッド。 ■System.out.printlnメソッド ・toStringメソッドをオーバーライドしていない場合→[完全修飾クラス@ハッシュ値の16進数の文字列 ・[は配列、L完全修飾クラス名は要素の型を表す ・全てのクラスにおいてtoStringメソッドを適切にオーバーライドすることが推奨 ・戻り値として文字列にフィールドの値を含むなど ■equalsメソッド ・全てのオブジェクトのequalsメソッドの比較はメモリアドレスの比較 ・StringやIntegerはequalsメソッドをオーバーライドしており、フィールドの値の比較を行う ・StringやIntegerクラスは同じ値を保持しておれば、インスタンスの実態が異なっても同じと扱う ・null以外の参照値xにおいて、x.equals(nulll)はfalseを返却 ■hashCodeメソッド ・呼ばれたインスタンスのハッシュ値を返却する→メモリアドレスを基にした値 ・ハッシュ値の代表的なものとしてMD5やSHAがある→不可逆性、一方向性→理論上は元データを復元できない ・オブジェクトを検索する時にパフォーマンスを向上させる ・equalsメソッドによって等しいオブジェクトは必ず同じ値になる ・equalsメソッドによって等しくないオブジェクトは必ず同じ値を返さないといけないわけではない ・異なるオブジェクトは異なるハッシュ値を返すことが理想→通常検索で用いるので ■final ・クラス→継承不可→abstractは不可、コンパイルエラー ・フィールド→再代入不可 ・メソッド→オーバーライド不可→abstractは不可、コンパイルエラー ・メソッドの引数、ローカル変数で使用することは可→引数でうけとった値を変更する必要がないときには推奨 ■クラスとインスタンス ・クラスは静的→コンパイル時に決定 ・インスタンスは動的→プログラム実行中にnewされヒープ領域(メモリ)に生成される ・staticフィールドはfinalをつけることが推奨 ・クラス名.staticフィールド名、クラス名.staticメソッド名で使用する ・staticメンバーはクラス名による参照が推奨、 ■static変数をコード・ブロックで初期化 ・static初期化子を用いる static { // 初期化コード } ・クラスがロードされたタイミングで使用 ■シングルトンを実装するには ・自身をフィールドとして保持。private staticフィールド ・コンストラクタをprivate ・自身のインスタンスを返却するpublic staticメソッド pubilc class A { private static final A a = new A(); private A() {} public static A getInstance() { return a;} } ■不変クラス ・Immutable Classという ・生成されたインスタンスのフィールド値を変更できないように設計されたクラス ・java.lang.Stringクラスなど ・クラスはfinal ・フィールドはprivate final ・メソッドはgetterのみ ■インターフェイス ・抽象メソッドの他にdefaultメソッドとstaticメソッドの宣言が可 ・defaultメソッド→インターフェイス名.メソッドで呼び出す。実装クラスを用いて呼び出せない(コンパイルエラー)、実装クラスにおいて最初から実行が可能 ・関数型インターフェイスは抽象メソッドを1つだけ保持できる ・同じシグニチャ(メソッド名、引数などが同じ)のdefalutメソッドを持つインターフェイスを複数実装した場合にはコンパイルエラー→防ぐにはオーバーライド ・defaultメソッドが型階層(インターフェイスを継承して、実装するなど)においては先にオーバーライドされた内容が適応される ■抽象クラス ・インスタンス生成不可 ・具象メソッドと抽象メソッドの両方の宣言が可 ・実装を記述することはできない ・抽象クラスを継承する場合はメソッドをオーバーライド or 自身も抽象クラスにする ・実装しないメソッドを継承する場合抽象クラスにしないとコンパイルエラー ■入れ子クラス ・メンバークラス、staticメンバークラス、ローカルクラス ・static以外のクラスを内部クラスといい、匿名クラス(関数の引数として使う場合に、その場で宣言して利用する)として利用できる ■内部クラス ・(例) Outer.Inner inner = new Outer().new Inner();→外側のクラス.内部クラス 変数名;→他のクラスから呼ぶ場合 ・同じクラスのメソッド内では通常通り呼べる ・staticは内部クラスはそのまま呼べない→まずは自身のクラスをnewしてから呼ぶ必要あり ■匿名クラス ・インターフェイスの実装をその場で行うときなどに用いる ・new クラス名() { //抽象メソッドの実装 } ■列挙型(Enum) ・修飾子 enum 識別子 {列挙定数1, 列挙定数2, ...} ・トップレベル、入れ子クラスの中で使える→メソッド内は不可 ・列挙型はクラスなのでフィールドやメソッドをメンバーに保持できる ・列挙定数には修飾子をつけることはできない。 ・他のクラスを継承できない ・インターフェイスの実装は可能 ・列挙型はfinalクラスに暗黙的にコンパイルされるので、final, abstractは使用不可 ・トップレベルではpubilcもしくはデフォルトのみ修飾子として可→ネストされている場合はstatic扱い ・列挙定数名を取得するnameメソッドがある→final扱い ・toStringメソッドのオーバーライドの実装も提供している→推奨(カスタマイズした文字列を返却できるから) ・文字列から列挙型インスタンスを取得するにはvalueOfメソッドを使用→存在しない場合はIllegalArgumentExceptionがスローされる ・ordinal()メソッド→列挙定数の宣言された順番の数値を返す→0から開始 ・values()メソッドは列挙型の配列を返却 ・コンストラクタはprivateのみ ふりかえり 列挙型や内部クラスはこれまで扱ってきてなかったので、意味や書き方のルールなどをもっと学習する必要があると感じた。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JSFを環境構築してみた!

仕事でJSFを使うので、独学でも触りたいと思い、環境構築しました。 開発環境 Windows10 home Java SE8 netbeans 8 GlassFish 4.1.1 Javaのインストール こちらのサイトから、Java SE 8をインストールします。 最新版のJDKだと、netbeansをインストールする際にエラーが排出されてしまう為、SE 8をインストールしましょう。 環境変数の設定 次に、「コントロールパネル」→「システムとセキュリティ」→「システム」の「システムの詳細設定」から環境変数の設定を行います。 「システム環境変数」の中に「Path」という変数があるので、編集ウィンドウから新規ボタンを押してJDKをインストールした場所のbinディレクトリのパスを追加します。 「Path」に追加後、コマンドプロンプトを立ち上げ、javacとコマンドを打って、きちんと環境変数が設定できているか、確認します。 以下のコマンドが表れれば、OKです。 Picked up _JAVA_OPTIONS: -Djava.net.preferIPv4Stack=true 使用方法: javac <options> <source files> 使用可能なオプションには次のものがあります。 @<filename> ファイルからの読取りオプションおよびファイル名 -Akey[=value] 注釈プロセッサに渡されるオプション --add-modules <module>(,<module>)* 初期モジュールに加えて解決するルート・モジュール、または<module>が ALL-MODULE-PATHである場合はモジュール・パスのすべてのモジュール。 --boot-class-path <path>, -bootclasspath <path> ブートストラップ・クラス・パスの位置をオーバーライドする --class-path <path>, -classpath <path>, -cp <path> ユーザー・クラス・ファイルおよび注釈プロセッサを検索する位置を指定する -d <directory> 生成されたクラス・ファイルを格納する位置を指定する -deprecation 推奨されないAPIが使用されているソースの位置を出力する --enable-preview プレビュー言語機能を有効にします。-sourceまたは--releaseとともに使用されます。 -encoding <encoding> ソース・ファイルが使用する文字エンコーディングを指定する -endorseddirs <dirs> 推奨規格パスの位置をオーバーライドする -extdirs <dirs> インストール済み拡張機能の位置をオーバーライドする -g すべてのデバッグ情報を生成する -g:{lines,vars,source} いくつかのデバッグ情報のみを生成する -g:none デバッグ情報を生成しない -h <directory> 生成されたネイティブ・ヘッダー・ファイルを格納する場所を指定する --help, -help, -? このヘルプ・メッセージを出力します --help-extra, -X 追加オプションのヘルプを出力します -implicit:{none,class} 暗黙的に参照されるファイルについてクラス・ファイルを生成するかどうかを指定する -J<flag> <flag>を実行システムに直接渡す --limit-modules <module>(,<module>)* 参照可能なモジュールの領域を制限します --module <module>(,<module>)*, -m <module>(,<module>)* 指定したモジュールのみコンパイルし、タイムスタンプを確認する --module-path <path>, -p <path> アプリケーション・モジュールを検索する位置を指定する --module-source-path <module-source-path> 複数モジュールの入力ソース・ファイルを検索する位置を指定する --module-version <バージョン> コンパイルするモジュールのバージョンを指定します -nowarn 警告を発生させない -parameters メソッド・パラメータにリフレクション用のメタデータを生成します -proc:{none,only} 注釈処理やコンパイルを実行するかどうかを制御します。 -processor <class1>[,<class2>,<class3>...] 実行する注釈プロセッサの名前。デフォルトの検出処理をバイパス --processor-module-path <path> 注釈プロセッサを検索するモジュール・パスを指定する --processor-path <path>, -processorpath <path> 注釈プロセッサを検索する位置を指定する -profile <profile> 使用されているAPIが指定したプロファイルで使用可能かどうかを確認します --release <release> 指定されたJava SEリリースに対してコンパイルします。サポートされているリリース: 7, 8, 9, 10, 11, 12, 13, 14, 15 -s <directory> 生成されたソース・ファイルを格納する場所を指定する --source <release>, -source <release> 指定されたJava SEリリースとソースの互換性を保持します。サポートされているリリース: 7, 8, 9, 10, 11, 12, 13, 14, 15 --source-path <path>, -sourcepath <path> 入力ソース・ファイルを検索する位置を指定する --system <jdk>|none システム・モジュールの位置をオーバーライドする --target <release>, -target <release> 指定されたJava SEリリースに適したクラス・ファイルを生成します。サポートされているリリース: 7, 8, 9, 10, 11, 12, 13, 14, 15 --upgrade-module-path <path> アップグレード可能なモジュールの位置をオーバーライドする -verbose コンパイラの動作についてメッセージを出力する --version, -version バージョン情報 -Werror 警告が発生した場合にコンパイルを終了する 続いて、システム環境変数に「JAVA_HOME」という変数を追加します。 値はJDKをインストールした場所のパスになります。 追加後、コマンドプロンプトでset JAVA_HOMEと入力し、以下のようになればOKです。 JAVA_HOME=C:\Program Files\Java\jdk1.8.0_181 netbeansをインストール こちらのサイトから、netstat8をインストールします。 GlassFishをインストール 続いて、webサーバーであるGlassFishをこちらのサイトから、4.1.1のzipファイルをダウンロードします。 zipファイルは特定の場所に解凍すればOKです。 Java Webアプリケーションの設定 netbeansを立ち上げ、いよいよプロジェクトの作成!...の前に、以下の設定を行います。 まず、「ツール」バーから「プラグイン」を選択します。 「使用可能なプラグインタブ」を開き、「Java EE」と検索し、Java EE Baseのボックスにチェックを入れ、「インストール」をクリックします。 インストールが完了したら、再起動を促すメッセージが表れるので、netbeansを再起動させます。 プロジェクトを作成 再起動したら、いよいよプロジェクトの作成です。 ファイル->新規プロジェクトを選択し、カテゴリとプロジェクトを下図のように選択します。 「次」を押すと、プロジェクト名とプロジェクトを格納するパスを指定します。 続いて、サーバーを指定します。 ここでは、GlassFishを使用します。 「次」を押すと、サーバーの場所を指定します。 ここで、GlassFishをインストールした場所を指定するのですが、\glassfish4\glassfishになるように指定し、「ライセンス契約を読んで同意しました」にチェックを入れます。 最後にドメイン名を指定し、「終了」を押せば、サーバーの設定は完了です。 続いて、フレームワークを指定します。 ここでは「Java Server Faces」にチェックを入れます。 これで、プロジェクトの作成は完了です。 Hello World的なものを表示させる プロジェクトバーにあるプロジェクト名を右クリックし、「実行」を押せば、サーバーが立ち上がり、プロジェクトが実行されます。 無事、「Hello from Facelets」というテキストが表示されました! まとめ JSFは古めのフレームワークだからか、書籍やネットの記事がかなり少ない為、初心者にはとっつきにくいと思います。 しかし、使う現場はある程度あったり、MVC構造の勉強になるので、使ってみてはいかがでしょうか。 参考文献と引用 【Win10】NetBeansでJSFの開発環境を構築する GlassFish 4.0のインストールとNetBeans 8.0の連携 NetBeansでJava Webアプリケーションを作成できません
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Jackson】JSONから特定プロパティだけデシリアライズする

やりたいこと 以下のようなJSONから、valueプロパティだけを、型を付けてデシリアライズします。 { "value": "10", // このプロパティは有ったりなかったりする /* 他にも沢山のフィールド */ } やり方 以下のような読み出し関数を作ることでできます。 サンプルコードはKotlinですが、Javaでも問題なく動くと思います。 import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper val mapper: ObjectMapper = jacksonObjectMapper() /** * @param json 読み出し対象JSON * @param targetPropertyName 読み出し対象プロパティ名 * @param dst デシリアライズ先クラス */ fun <T> readProperty(json: JsonNode, targetPropertyName: String, dst: Class<T>): T? { // プロパティの読み出し(getの場合無ければnullが返ってくる) val readedNode: JsonNode? = json.get(targetPropertyName) // プロパティを読み出せた場合、dstに指定した型へデシリアライズする return readedNode?.let { mapper.treeToValue(it, dst) } } 使い方は以下のとおりです。 サンプルコードでは、valueプロパティを文字列や整数として読み出せることと、値がnullのプロパティも問題なく読み出せることを示しています。 val rawJson = """ { "value": "10", "hoge": null, "fuga": "aaa", "piyo": 0 } """.trimIndent() val json: JsonNode = mapper.readTree(rawJson) // 文字列として読み出し val asString: String = readProperty(json, "value", String::class.java)!! // "10" // 整数として読み出し val asInt: Int = readProperty(json, "value", Int::class.java)!! // 10 // nullも読み出せる val asNull: Int? = readProperty(json, "hoge", Int::class.java) 補足1: 再帰的にプロパティを見つけたい場合 ネストしたJSONで、深い階層に有るプロパティも処理対象としたい場合、findValueでの読み出しが使えます。 - val readedNode: JsonNode? = json.get(targetPropertyName) + val readedNode: JsonNode? = json.findValue(targetPropertyName) 補足2: カスタムデシリアライザについて サンプルコード内で利用しているObjectMapperにカスタムデシリアライザを登録することで、treeToValueの処理時にデシリアライザを適用することが可能です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TomcatとJerseyでトランザクション管理してみる

はじめに Tomcat + Jersey という構成の Web アプリケーションの動作を理解するために、Tomcat Embed を使って実験をしていきます。 いまどきこんな構成で開発を始めることは少ないかと思いますが、レガシーソフトウェアと戦う人たちの助けになれば幸いです。 関連記事の一覧(予定) 埋め込みTomcatでJerseyを動かしてみる HK2でDIしてみる BeanValidationで入力値検証してみる TomcatとJerseyでトランザクション管理してみる ← イマココ リポジトリ データベースを準備する 本記事では検証用のデータベースとしてインメモリの H2 Database を使用します。 build.gradle に以下の依存を追加します。 tomcat-dbcp は埋め込み Tomcat でデータベースにアクセスするために必要です。 build.gradle ... implementation group: 'com.h2database', name: 'h2', version: H2_VERSION implementation group: 'org.apache.tomcat', name: 'tomcat-dbcp', version: TOMCAT_VERSION ... データベースへの接続設定を context.xml に記述し、 META-INF 内に配置します。1 名前は jdbc/h2db としました。 context.xml <Context> <Resource name="jdbc/h2db" auth="Container" type="javax.sql.DataSource" factory="org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory" driverClassName="org.h2.Driver" url="jdbc:h2:mem:test" /> </Context> ファイル構成 src/main ├── (省略) └── webapp ├── META-INF │ └── context.xml └── index.html アプリケーションの起動時にはデータベースは空の状態です。Web アプリケーションの初期化時に実行されるイベントリスナーを登録し、データベースにテーブルを作成します。 さきほど定義した jdbc/h2db は java:comp/env/ 以下に見つかります。 DatabaseInitializer.java ... public class DatabaseInitializer implements ApplicationEventListener { @Override public void onEvent(ApplicationEvent event) { if (event.getType().equals(ApplicationEvent.Type.INITIALIZATION_START)) { createTable(); } } private void createTable() { try { var ds = (DataSource) InitialContext.doLookup("java:comp/env/jdbc/h2db"); try (var con = ds.getConnection(); var stmnt = con.createStatement()) { stmnt.execute("create table positive(value integer)"); stmnt.executeUpdate("insert into positive values(0)"); } } catch (NamingException | SQLException e) { throw new RuntimeException(e); } } @Override public RequestEventListener onRequest(RequestEvent requestEvent) { return null; } } positive テーブルを作成し、初期値として value カラムに 0 を挿入しました。 リソースにデータベース接続を渡す リソースからデータベースにアクセスするために、 Connection を供給する DisposableSupplier を作成します。2 動作の様子がわかるように、コンソールに状態を書き出しています。 ConnectionSupplier.java ... public class ConnectionSupplier implements DisposableSupplier<Connection> { @Override public Connection get() { try { var ds = (DataSource) InitialContext.doLookup("java:comp/env/jdbc/h2db"); var con = ds.getConnection(); con.setAutoCommit(false); System.out.println("Supplied"); return con; } catch (NamingException | SQLException e) { throw new RuntimeException(e); } } @Override public void dispose(Connection instance) { System.out.println("Closing..."); try { if (!instance.isClosed()) { instance.close(); } } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException(e); } } } この ConnectionSupplier を Connection のファクトリとして登録します。 スコープには RequestScoped を設定しないと、なぜか dispose が呼ばれないようです。なお、この dispose はリソースで予期しない例外が発生した場合にも Connection をクローズできることを想定して実装しています。 AppConfig.java @ApplicationPath("app") public class AppConfig extends ResourceConfig { public AppConfig() { packages(getClass().getPackage().getName()); register(DatabaseInitializer.class); register(new AbstractBinder() { @Override protected void configure() { bindFactory(ConnectionSupplier.class).to(Connection.class) .in(RequestScoped.class); } }); } } リソースに現在の値を返す GET メソッドを作成し、データベースにアクセスできることを確認してみます。 Positive.java @Path("positive") public class Positive { private static final String SELECT_VALUE_FROM_POSITIVE = "select value from positive"; @Inject private Connection connection; @GET public String query() throws SQLException { try (var con = connection; var select = con.prepareStatement(SELECT_VALUE_FROM_POSITIVE)) { var result = select.executeQuery(); result.next(); var current = result.getInt("value"); return getMessage(current); } } private String getMessage(int value) { return String.format("Current value is %d. Never be negative!", value); } } Web アプリケーションを起動して、アクセスしてみましょう。 Terminal curl localhost:8080/app/positive/ # Current value is 0. Never be negative! 初期値 0 を取得できていますね。サーバーのログも見てみましょう。 サーバー 情報: Starting ProtocolHandler ["http-nio-8080"] Supplied Closing... リクエストごとに Connection のクローズ処理が実行されていることがわかります。 DI でトランザクション 注入された Connection を使ってデータベースを更新した後に例外が発生したとき、トランザクションがロールバックされるように変更してみましょう。 リソースに PUT メソッドを定義して値を更新できるようにします。ここで、現在値と入力値の合計が負数になるとき、あえてレコードを合計値で更新してから例外を発生させます。 Positive.java ... @PUT public String add(String n) throws SQLException { try (var select = connection.prepareStatement(SELECT_VALUE_FROM_POSITIVE); var update = connection.prepareStatement("update positive set value = ?")) { var result = select.executeQuery(); result.next(); var current = result.getInt("value"); var sum = current + Integer.parseInt(n); update.setInt(1, sum); update.executeUpdate(); if (sum < 0) { throw new RuntimeException(); } System.out.println("Commit"); connection.commit(); connection.close(); return getMessage(sum); } } ... add メソッドでは try-with-resources 文に Connection を渡していません。呼び出しの途中で例外が発生した場合 Connection は ConnectionSupplier#dispose でクローズされますが、この前にロールバックを追加します。3 ConnectionSupplier.java ... @Override public void dispose(Connection instance) { System.out.println("Closing..."); try { if (!instance.isClosed()) { System.out.println("Roll back"); // 追加 instance.rollback(); // 追加 instance.close(); } ... リソースの中で Connection がクローズされなかった場合に、ロールバックが実行されるようになりました。 Web アプリケーションを再起動して、 PUT リクエストで値を更新してみましょう。 Terminal curl -X PUT localhost:8080/app/positive -d 1 # Current value is 1. Never be negative! curl -X PUT localhost:8080/app/positive -d -3 # <!doctype html><html lang="en"><head><title>HTTP Status 500 – Internal Server Error</title>... curl localhost:8080/app/positive/ # Current value is 1. Never be negative! 合計が負数になるリクエストで例外が発生していますが、例外発生前の 1 が保持されていることがわかります。 サーバーのログを見てみましょう。 サーバー 情報: Starting ProtocolHandler ["http-nio-8080"] Supplied Commit Closing... Supplied Closing... Roll back ... java.lang.RuntimeException ... Supplied Closing... 例外が発生したリクエストで、ロールバックが実行されたことがわかります。 DI と AOP でトランザクション DI を使ったトランザクションでは、データベースを更新する呼び出しのなかで明示的にコミットを実行する必要がありました。次はこの規約を廃し、単に呼び出しが正常終了すればコミット、例外発生時にはロールバックが実行される仕組みを実現します。 トランザクション境界を示すアノテーションを定義します。独自に実装しても良いのですが、ここでは JTA で使われている @Transactional を流用します。 build.gradle ... implementation group: 'javax.transaction', name: 'javax.transaction-api', version: '1.3' ... 実際にコミット・ロールバック処理を行う MethodInterceptor を実装します。 メソッドの呼び出しが MethodInvocation として引数に与えられるので、その実行の前後に目的の処理を定義します。 TransactionInterceptor.java public class TransactionInterceptor implements MethodInterceptor { @Inject private Connection connection; @Override public Object invoke(MethodInvocation invocation) throws Throwable { try { var ret = invocation.proceed(); System.out.println("[AOP] Commit"); if (!connection.isClosed()) { connection.commit(); connection.close(); } return ret; } catch (Throwable t) { System.out.println("[AOP] Roll back"); if (!connection.isClosed()) { connection.rollback(); connection.close(); } throw t; } } } 次に、この TransactionInterceptor を @Transactional で注釈されたメソッドに供給する InterceptionService を作成します。 InterceptionService は Jersey 内部の HK2 がオブジェクトを注入するたびに、そのオブジェクトのクラスに定義されたメソッドおよびコンストラクタに対してインターセプターを供給します。4 TransactionInterceptionService public class TransactionInterceptionService implements InterceptionService { @Inject private TransactionInterceptor interceptor; @Override public Filter getDescriptorFilter() { return BuilderHelper.allFilter(); } @Override public List<MethodInterceptor> getMethodInterceptors(Method method) { return method.isAnnotationPresent(Transactional.class) ? List.of(interceptor) : Collections.emptyList(); } @Override public List<ConstructorInterceptor> getConstructorInterceptors(Constructor<?> constructor) { return Collections.emptyList(); } } TransactionInterceptor と TransactionInterceptionService を依存関係に登録します。TransactionInterceptionService は Singleton でなければなりません。一方、この内部に注入される TransactionInterceptor 内の Connection は RequestScoped で、ライフサイクルが一致しません。そのため、 RequestScoped のスコープで生成された Connection を、プロキシを経由して Singleton のスコープからも参照できるように設定します。5 AppConfig.java @ApplicationPath("app") public class AppConfig extends ResourceConfig { public AppConfig() { packages(getClass().getPackage().getName()); register(DatabaseInitializer.class); register(new AbstractBinder() { @Override protected void configure() { bindFactory(ConnectionSupplier.class).to(Connection.class) .proxy(true).proxyForSameScope(false) // 追加 .in(RequestScoped.class); // 追加 bindAsContract(TransactionInterceptor.class); bind(TransactionInterceptionService.class).to(InterceptionService.class).in(Singleton.class); } }); } } 最後に、リソースの呼び出しを @Transactional で注釈します。これでコミットやクローズ処理は不要になったので、コメントアウトしておきます。 Positive.java ... @Transactional // 追加 @PUT public String add(String n) throws SQLException { ... // System.out.println("Commit"); // connection.commit(); // connection.close(); return getMessage(sum); } } ... Web アプリケーションを再起動して、PUT リクエストで値を更新してみましょう。 Terminal(クライアント) curl -X PUT "localhost:8080/app/positive" -d "-3" # Current value is 1. Never be negative! curl -X PUT "localhost:8080/app/positive" -d "-3" # <!doctype html><html lang="en"><head><title>HTTP Status 500 – Internal Server Error... curl localhost:8080/app/positive/ # Current value is 1. Never be negative! サーバー 情報: Starting ProtocolHandler ["http-nio-8080"] Supplied [AOP] Commit Closing... Supplied [AOP] Roll back Closing... ... java.lang.RuntimeException ... Supplied Closing... AOP でコミットとロールバックを実行できていることがわかります。 参考 Chapter 24. Custom Injection and Lifecycle Management Jerseyでリソースメソッドをトランザクション境界にする — 裏紙 本記事では webapp をアプリケーションのルートディレクトリに指定しています。最初の記事 を参照。 ここで context.xml に定義した接続設定は、アプリケーションのエントリポイントとなるクラスのなかでも定義できます。 ↩ 公式ドキュメントの実装例では HK2 の Factory インターフェースが使われていますが、 バージョン 2.26 で Jersey 独自の Supplier インターフェースの導入に伴い廃止されました。その後、 バージョン 2.29 で後方互換性のために Factory のサポートが復活しましたが、いずれは廃止予定のようです。また、 Supplier を登録するためには HK2 の AbstractBinder ではなく、 Jersey 独自の同名クラスを利用する必要があります。 ↩ Connection#setAutoCommit(false) のとき、コミットせずにクローズした場合のふるまいは実装依存です。多くの DBMS ではロールバックされますが、例えば Oracle ではコミットされるようです。なお、記事中では add メソッドの最後で Connection をクローズしていますが、一般には先にコミットされた内容はロールバックしても取り消されないため、クローズしなくても問題はないはずです。 ↩ InterceptionService はあらゆる依存解決で実行されるため、オーバーヘッドを考慮に入れるべきでしょう。 getDescriptorFilter で対象となるオブジェクトを絞り込むこともできますが、現実的には例えばクラスの完全修飾名のような大雑把な条件しか与えられないうえ、この絞り込み結果はキャッシュされません。また、意図しないメソッドやコンストラクタにインターセプターを供給してしまうリスクにも注意が必要です。 ↩ JavaEE屈指の便利機能、CDIを触ってみよう - 技術ブログ | 株式会社クラウディア を参照。 ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

デザインパターン勉強会⑦Builder

はじめに ZOOM勉強会の議事録です。 第7回はBuilderパターンです。 Builderパターン オブジェクトの生成における呼び出すメソッドの順序と具体的な生成メソッドの分離をします。 分離することにより、オブジェクト生成の順序を使い回すことができます。 特に、コンストラクタの引数が多いときに有効です。 ビルダーパターンの役割は以下です。 Builder一つのBuilderに依存しないようにするインタフェース、建築材料を持っているConcreteBuilderインスタンス生成時に使用するメソッドの具体的な実装Director設計図のようにインスタンス生成時の順序を知っている、材料を組み上げる役割Clientビルダーパターンを利用してオブジェクトを生成するコード 今回はRefactoring.GuruのJavaコードを参考にさせていただきました。 リファクタリング前 車を表すクラスと、そのマニュアルを表すクラスをインスタンス化しています。 Public Sub Main() 'スポーツカーとそのマニュアルを生成 Dim sportsCar As New Car(CarType.SPORTS_CAR, 1, New Engine(3.0, 0), Transmission.SEMI_AUTOMATIC, New TripComputer(), New GPSNavigator()) Dim sportsCarManual As New Manual(CarType.SPORTS_CAR, 1, New Engine(3.0, 0), Transmission.SEMI_AUTOMATIC, New TripComputer(), New GPSNavigator()) 'シティーカーとそのマニュアルを生成 Dim cityCar As New Car(CarType.CITY_CAR, 2, New Engine(1.2, 0), Transmission.AUTOMATIC, New TripComputer(), New GPSNavigator()) Dim cityCarManual As New Manual(CarType.CITY_CAR, 2, New Engine(1.2, 0), Transmission.AUTOMATIC, New TripComputer(), New GPSNavigator()) 'SUVとそのマニュアルを生成 Dim suvCar As New Car(CarType.SUV, 4, New Engine(2.5, 0), Transmission.MANUAL, Nothing, New GPSNavigator()) Dim suvCarManual As New Manual(CarType.SUV, 4, New Engine(2.5, 0), Transmission.MANUAL, Nothing, New GPSNavigator()) End Sub 多くのパラメータがあり、車の種類に応じて、コンストラクタの引数が異なり、間違える可能性が高いです。 また、ある車とそのマニュアルでコンストラクタに渡す引数が同じため、共通のパラメータを渡せるようにしたいです。 Builder インスタンスを生成するメソッドを定めたインタフェースです。 コンストラクタに必要なパラメータを受け取るメソッドのみを定義しています。 Builder Public Interface Builder Sub SetCarType(ByVal carType As CarType) Sub SetSeats(ByVal seats As Integer) Sub SetEngine(ByVal engine As Engine) Sub SetTransmission(ByVal transmission As Transmission) Sub SetTripComputer(ByVal tripComputer As TripComputer) Sub SetGPSNavigator(ByVal gpsNavigator As GPSNavigator) End Interface ConcreteBuilder Builderインタフェースを実装した具象クラスです。 それぞれ車用とマニュアル用です。 CarBuilder Public Class CarBuilder : Implements Builder Private type As CarType Private seats As Integer Private engine As Engine Private transmission As Transmission Private tripComputer As TripComputer Private gpsNavigator As GPSNavigator Public Sub SetCarType(carType As CarType) Implements Builder.SetCarType Me.type = carType End Sub Public Sub SetSeats(seats As Integer) Implements Builder.SetSeats Me.seats = seats End Sub Public Sub SetEngine(engine As Engine) Implements Builder.SetEngine Me.engine = engine End Sub Public Sub SetTransmission(transmission As Transmission) Implements Builder.SetTransmission Me.transmission = transmission End Sub Public Sub SetTripComputer(tripComputer As TripComputer) Implements Builder.SetTripComputer Me.tripComputer = tripComputer End Sub Public Sub SetGPSNavigator(gpsNavigator As GPSNavigator) Implements Builder.SetGPSNavigator Me.gpsNavigator = gpsNavigator End Sub '車のインスタンスを生成して返す Public Function GetResult() As Car Return New Car(type, seats, engine, transmission, tripComputer, gpsNavigator) End Function End Class CarManualBuilder Public Class CarManualBuilder : Implements Builder Private type As CarType Private seats As Integer Private engine As Engine Private transmission As Transmission Private tripComputer As TripComputer Private gpsNavigator As GPSNavigator Public Sub SetCarType(carType As CarType) Implements Builder.SetCarType Me.type = carType End Sub Public Sub SetSeats(seats As Integer) Implements Builder.SetSeats Me.seats = seats End Sub Public Sub SetEngine(engine As Engine) Implements Builder.SetEngine Me.engine = engine End Sub Public Sub SetTransmission(transmission As Transmission) Implements Builder.SetTransmission Me.transmission = transmission End Sub Public Sub SetTripComputer(tripComputer As TripComputer) Implements Builder.SetTripComputer Me.tripComputer = tripComputer End Sub Public Sub SetGPSNavigator(gpsNavigator As GPSNavigator) Implements Builder.SetGPSNavigator Me.gpsNavigator = gpsNavigator End Sub 'マニュアルのインスタンスを生成して返す Public Function GetResult() As Manual Return New Manual(type, seats, engine, transmission, tripComputer, gpsNavigator) End Function End Class Builder役を実装しています。 コンストラクタに必要なパラメータをフィールドに持ち、Builder役で定義されていたメソッドで各パラメータをセットします。 GetResult()でフィールドのパラメータを使用して、対象のクラスをインスタンス化して返します。 Director 渡されたBuilder役に、コンストラクタのパラメータをセットしています。 作りたい車に応じて、Builder役に与えるパラメータを変えています。 Director Public Class Director 'スポーツカーのパラメータ設定 Public Sub ConstructSportsCar(ByVal builder As Builder) builder.SetCarType(CarType.SPORTS_CAR) builder.SetSeats(1) builder.SetEngine(New Engine(3.0, 0)) builder.SetTransmission(Transmission.SEMI_AUTOMATIC) builder.SetTripComputer(New TripComputer()) builder.SetGPSNavigator(New GPSNavigator()) End Sub 'シティーカーのパラメータ設定 Public Sub ConstructCityCar(ByVal builder As Builder) builder.SetCarType(CarType.CITY_CAR) builder.SetSeats(2) builder.SetEngine(New Engine(1.2, 0)) builder.SetTransmission(Transmission.AUTOMATIC) builder.SetTripComputer(New TripComputer()) builder.SetGPSNavigator(New GPSNavigator()) End Sub 'SUVのパラメータ設定 Public Sub ConstructSUV(ByVal builder As Builder) builder.SetCarType(CarType.SUV) builder.SetSeats(4) builder.SetEngine(New Engine(2.5, 0)) builder.SetTransmission(Transmission.MANUAL) builder.SetGPSNavigator(New GPSNavigator()) End Sub End Class Client DirectorとBuliderを用意して、DirectorにBuilderを渡しコンストラクタパラメータを設定します。 このように複雑なコンストラクタがなくなり、生成の手順を使い回すことができます。 Client Public Sub Main() Dim director As New Director() 'スポーツカーのインスタンス作成 Dim builder As New CarBuilder() director.ConstructSportsCar(builder) Dim car As Car = builder.GetResult() 'スポーツカーのマニュアルのインスタンス生成 Dim manualBuilder As New CarManualBuilder() director.ConstructSportsCar(manualBuilder) Dim carManual = manualBuilder.GetResult() End Sub 利点 生成処理を使い回すことができる(今回の場合、車とマニュアル) 生成するためのメソッドと生成時の呼び出し順序を分けて管理できる まとめ 今回はBuilderパターンについて学びました。 多数のパラメータを持つコンストラクタが出てきたときや複雑な生成方法があるときに、リファクタリングとしてBuilderパターンを採用するのが良いと思いました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む