- 投稿日:2019-12-19T23:34:48+09:00
java エラー対策編
今回はjavaで出るエラーについて書いて見ます。
コンパイルエラー
ます初心者にありがちなコンパイルエラーと対処方法を紹介します。
- セミコロン「;」が不足
- ダブルクオーテーション「”」が不足
- 全角空白文字
- ファイル名とクラス名が異なる
- 閉じ括弧「}」
などあります。
セミコロン「;」が不足
Javaの記述ルールは、1つの命令ごとにセミコロン「;」をつける決まりです。
セミコロン「;」が不足public class HelloWorld { public static void main(String[] args) { System.out.println("Hello World!") // ← セミコロン(「;」)が足りない } }ダブルクオーテーション「”」が不足
文字列の終わりのダブルクオーテーション「”」が不足していると、こんなエラーが起きます。
ダブルクオーテーション「”」が不足public class HelloWorld { public static void main(String[] args) { System.out.println("Hello World!); ← World!の後に「"」がない } }全角空白文字
本来「半角空白」文字を入れるべきところに、「全角空白」文字を入れてしまったことから発生してます。
半角空白文字public □ class HelloWorld { // ← publicの後に全角空白(□) public static void main(String[] args) { System.out.println("Hello World!") } }ファイル名とクラス名が異なる
例をあげると
ソースファイルに記述されているクラス名が「HelloWorld」であるのに対し
ファイル名を「Hello.java」としたために発生します。Javaでは、一つのファイルに一つのクラスを記述し
ファイル名は「クラス名.java」とする決まりです。閉じ括弧「}」
数十行のプログラムで、if(){}やfor(){}のような段落のとじ括弧が不足していると
非常にわかりづらいです。閉じ括弧「}」public static void main(String[] args) { System.out.println("Hello World!") // ← 「}」がない }構文エラー
少し難しくなって、配列やif構文で出てくるエラー内容を紹介します。
主な内容はこちら
- java.lang.NullPointerException
- java.lang.ArrayIndexOutOfBoundsException
- java.lang.NumberFormatException
- java.lang.ClassCastException
- java.lang.ArithmeticException
- java.lang.NoClassDefFoundError
- java.lang.OutOfMemoryError
java.lang.NullPointerException
このエラーはnullのオブジェクトに対してアクセスしようとしたときに発生する。
プログラミング時に比較的発生しやすいエラーで、エンジニアの間では「ヌルポ」と呼ばれています。対策
- インスタンスの生成や値の設定が正しく行われているかをチェックする。
- オブジェクトがnullになってしまうタイミングをデバッグで確認する。
- プログラムでオブジェクトがnullかどうかのチェックを行い、エラーの発生をできる限り回避する。
java.lang.ArrayIndexOutOfBoundsException
配列のインデックスとして存在しない要素を指定した場合に発生するエラーで
先ほどのエラーの例でも紹介した。配列のインデックスとして1~9のインデックスしかないところ
10のインデックスに値を設定しようとする場合などに発生する。対策
* 正しい配列のインデックスを指定できているかチェックする。
* 配列の設定を繰り返し処理で行っている場合、繰り返し回数などに問題がないかチェックする。java.lang.NumberFormatException
このエラーは文字列の値を数値に変換(キャスト)しようとしたが
文字列に数値として正しくない値が設定されている場合に発生する。対策
- 文字列にアルファベットや記号など、数値以外の文字が設定されていないかチェックする。
- 文字列に空白やnullが設定されていないかチェックする。
- 文字列には数値が設定されているが、変換しようとしている数値型で扱える値の範囲をオーバーしていないかチェックする。
java.lang.ClassCastException
このエラーはあるクラスを変換できないクラスにキャストしようとした時に発生する。
対策
- 配列などに設定した値のクラスと、値を取り出す時にキャストしようとしている型が一致しているかをチェックする。
- 継承関係のある2つのクラスで、サブクラスにスーパークラスをキャストしようとしていないかをチェックする。
java.lang.ArithmeticException
このエラーは数値の計算において、不正な算術処理が行われた場合に発生する。例えば数学的にあり得ない「ゼロ除算(10÷0 など)」が発生するとこのエラーが発生する。
対策
- ゼロ除算が行われてないかをチェックする。
- 数値型変数で計算している場合、割る数の変数にゼロが設定されていないかチェックする
java.lang.NoClassDefFoundError
メソッドを呼び出す時や、インスタンスを生成しようとしている時に
Javaがクラス定義を読み込むことができないエラーです。対策
- 環境変数CLASSPATHが誤っている可能性があるのでチェックする。
- ファイル名やパッケージ名に誤りがないかチェックする。
- プログラムに必要な外部ファイル(jar)が読み込まれているかチェックする。
java.lang.OutOfMemoryError
Javaは実行時にスタック領域とヒープ領域という2つのメモリー領域を使用するが
このエラーはプログラムの実行に必要なヒープ領域のメモリーサイズが不足した時に発生する。ヒープ領域とはクラスやメソッドの定義など、オブジェクトのインスタンスが割り当てられるメモリー領域のことだ。
対策
- プログラム内で無限ループになっていしまっている箇所がないかチェックする。
- 一度に大量のデータを処理しようとしている箇所がないかチェックする。
- 不必要にスコープ(公開範囲)が広いメソッドや変数を多用していないかチェックする。
- 投稿日:2019-12-19T23:06:01+09:00
Spring Boot のコントローラーを MockMvc でユニットテストするサンプルコード
今回の環境
- Java 11 (OpenJDK 11.0.2)
- Spring Boot 2.2.2
- Thymeleaf 3.0.11
- JUnit 5.5.2
- Hamcrest 2.1
テスト対象のコントローラーと Thymeleaf テンプレート
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.servlet.ModelAndView; @Controller public class FooBarController { @GetMapping("/") public ModelAndView topPage(ModelAndView mav) { mav.setViewName("index"); mav.addObject("userName", "Alice"); return mav; } }<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>My App</title> </head> <body> <p id="user-name" th:text="${userName}"></p> </body> </html>コントローラーをテストするクラス
import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.HttpHeaders; import org.springframework.test.web.servlet.MockMvc; import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; @SpringBootTest @AutoConfigureMockMvc public class FooBarControllerTest { @Autowired private MockMvc mockMvc; // ユーザーエージェント文字列 private static final String USER_AGENT = "Mozilla/5.0 (iPhone; CPU iPhone OS 13_0 like Mac OS X) " + "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0 " + "Mobile/15E148 Safari/604.1"; @Test public void testTopPage() throws Exception { this.mockMvc.perform( // GET メソッドでアクセス get("/") // リクエストヘッダを指定 .header(HttpHeaders.USER_AGENT, USER_AGENT)) // HTTP ステータスコードをテスト .andExpect(status().is2xxSuccessful()) // ビュー名をテスト .andExpect(view().name("index")) // モデルの属性をテスト .andExpect(model().attribute("userName", "Alice")) // Content Type をテスト .andExpect(content().contentType("text/html;charset=UTF-8")) // ページに指定したテキストが含まれるかテスト .andExpect(content().string(containsString("Alice"))); } }参考資料
- 投稿日:2019-12-19T22:34:04+09:00
Spring-Batchのテストでハマった
Spring-Batchのテストでハマった
ハマった内容
- あるバッチにパラメーターを与えて実行する
- 1回目は成功するのだが、2回目以降は実行できない
ハマりの原因
- Spring-Batchにはジョブの実行結果を保存しておく機能がある
- ジョブ成功後に前回と同じパラメーターを与えて実行することはできない
Going by the concept of Job Instances and Job Executions in Spring Batch, you can't start a COMPLETED job instance again though you can launch same instance of job again & again till its not COMPLETE ( and few more job statuses ). Job instance uniqueness is achieved by jobId & job parameters
- 別のエラーメッセージが表示されるためにこの話までたどり着くのに時間がかかった
なんだこのクソゴミ機能は…素晴らしい機能ですね
- 投稿日:2019-12-19T20:50:03+09:00
テスタブルなコード
概要
この記事は、以下に投稿したソースをリファクタリングするという記事です。
テスト困難なコードを書く
https://qiita.com/oda-kazuki/items/bac33094e82b0f51da41テスタブルなコードの結論
以下のようにコーディングしましょう。
- クラスはどんどん分割する
- 最終的にひとつのことだけに専念させるようにする
- 特にグローバルなリソースへのアクセスは一箇所にまとめる
- メンバ変数の初期化やSingletonの利用はDependency Injectionで行う
- 外部ライブラリの生成箇所は場合によってはラップするファクトリクラスを作る
- 読みやすさを重視 (処理を簡単に理解できるようにするため)
- 早期リターンを心がける
- 変数のスコープは最小限に
- メソッド名や変数名にはこだわる
- Mapはなるべくは利用しない
これらをやることで、テストは非常に容易になります。それでは、前回のコードをリファクタリングしていきます。
凝集度を高める
前回書いた通り、LoginServiceは複数の役割を担っていました。
まずは、その中の最もな害悪である「WebAPIの直接呼び出し」をやめます。そちらを外だししてしまいましょう。モデルの作成
ただその前に、Mapで表現してたアカウント情報やユーザー情報のクラスを作成します。
本当は中にも色々と処理をいれたいところですが、今回のだと特に使わなそうなのでDTOっぽい感じになります。@Data public class AuthToken { private String token; } @Data public class AuthResult { private String accountId; } @Data public class Account { // 今回のでは使わないけど、一応こういうのがあるという意味で入れときます private String accountId; } @Data public class User { // 今回のでは使わないけど、一応こういうのがあるという意味で入れときます private String name; }ファクトリメソッドの作成
また、今回は
JsonMapperというライブラリを利用してる(という設定)なのですが、こいつがジェネリクスを指定する必要があるので、ちょっと使いづらいです。なので、それ用のファクトリメソッドも作成します。単純に対象のJsonMapperを作成するだけの簡単なクラスです。public class JsonMapperFactory { public JsonMapper<Account> createAccountMapper() { return new JsonMapper<>(); } public JsonMapper<AuthToken> createAuthMapper() { return new JsonMapper<>(); } public JsonMapper<AuthResult> createAuthResultMapper() { return new JsonMapper<>(); } public JsonMapper<User> createUserMapper() { return new JsonMapper<>(); } }WebAPIを閉じ込める
では、まず
HttpClientというものを作成することにします。その中にWebAPIを直接呼び出す部分を閉じ込めます。public abstract class HttpClient { protected final Client client; protected final JsonMapperFactory mapperFactory; public HttpClient(Client client, JsonMapperFactory factory) { this.client = client; this.mapperFactory = factory; } }では、次は実際にWebAPIを呼び出す
AuthClientとUserClientを作成します。public class AuthClient extends HttpClient { public AuthClient(Client client, JsonMapperFactory factory) { super(client, factory); } public AuthResult authorize(String accessToken) throws HttpException { JsonMapper<AuthToken> tokenMapper = factory.createAuthMapper(); AuthToken token = new AuthToken(accessToken); Json json = client.post("/auth" , tokenMapper.serialize(token)).body(); JsonMapper<AuthResult> resultMapper = factory.createAuthResultMapper(); return resultMapper.deserialize(json); } public Account fetchAccount(String accountId) throws HttpException { Json json = client.get("/accounts/" + accountId).body(); JsonMapper<Account> accountMapper = factory.createAccountMapper(); return accountMapper.deserialize(json); } } public class UserClient extends HttpClient { public UserClient(Client client, JsonMapperFactory factory) { super(client, factory); } public User fetchUser(Account account) throws HttpException { JsonMapper<Account> accountMapper = factory.createAccountMapper(); Json json = client.post("/users" , accountMapper.serialize(account)).body(); JsonMapper<User> userMapper = factory.createUserMapper(); return userMapper.deserialize(json); } public void logout(User user) throws HttpException { JsonMapper<User> userMapper = factory.createUserMapper(); client.post("/logout", userMapper.serialize(user)); } }Cacheクラスの修正
次に、キャッシュも修正します。
最終的にはシングルトンとして利用はするのですが、一旦はSingletonパターンでの記述をなくします。public class Cache { private User loggedInUser; @Getter @Setter private String accessToken; @Getter private Account loggedInAccount; public Cache() { this.reset(); } public boolean isLoggedIn() { return loggedInUser != null; } public String cacheToken(String accessToken) { if (accessToken != null) { this.accessToken = accessToken; } return this.accessToken; } public void cacheAll(String accessToken, Account account, User user) { this.accessToken = accessToken; this.loggedInAccount = account; this.loggedInUser = user; } public void reset() { this.accessToken = null; this.loggedInAccount = null; this.loggedInUser = null; } }こう見ると、もともとすごい無駄なことしてるなぁとも思いますが、リファクタリングなのでそのままとしましょう。
Dependency Injection
さて、これでリファクタリングの準備もできました。
ただその前に、テスタブルなコードを書く切り札であるDI(Dependency Injection)について軽く紹介します。詳しく知りたい方は、以下を見るほうが良いと思います。
猿でも分かる! Dependency Injection: 依存性の注入
https://qiita.com/hshimo/items/1136087e1c6e5c5b0d9fこちらは、日本語では「依存性の注入」と言います。その名の通り、外部から依存するファイルを入れてあげるというデザインパターンです。
一番簡単なDIは、コンストラクタでメンバ変数を渡すというものです。
たとえば上記の「AuthClient」や「UserClient」のやり方がそれになります。再掲public class UserClient extends HttpClient { public UserClient(Client client, JsonMapperFactory factory) { super(client, factory); } // ︙ }それの何がいいのか
テストする際、依存してるファイルを簡単にMock化できます。
基本的に、クラスはメンバ変数に依存しています。それがデータであれば単なる「情報」として扱えばいいだけですが、メソッドに依存しているとテストの際にMock化する必要があるケースがでてきます。
※ 特にそれが外部リソースやグローバルなリソースに依存していると、テストがしづらいです。Dependency Injectionパターンを用いることで、テストする際にそういった依存している部分をモックに容易に置き換えることができるようになります。
Google Guice
Javaの場合、Google Guice や Springなど、強力なDIライブラリが多数存在しています。
今回はGoogle Guiceを使うということにしましょう。LoginServiceの修正
さて、いよいよ今回テストしたかったLoginServiceをなおしていきます。
その前に、LogoutServiceという新たなサービスがあるものとします。
(もう面倒なので、インタフェースだけ書きます)public interface LogoutService { /** ログインに成功したらキャッシュを全削除してtrueを返す。ログアウトに失敗したらfalseを返す */ boolean logout(); }ということで、LoginServiceです。
public class LoginService { @Inject // Guiceでの依存性の注入 private Cache cache; @Inject private AuthClient authClient; @Inject private UserClient userClient; @Inject private LogoutService logoutService; public User login(String authToken) { if(cache.isLoggedIn()) { if(!logoutService.logout()) { return null; } } String token = cache.cacheToken(authToken); return this.executeLogin(token, 0); } private User executeLogin(String token, int retryCount) { if (retryCount >= 3) { return null; } try { AuthResult auth = authClient.aurhotize(token); Account account = cache.getAccount(); if (account == null) { account = authClient.fetchAccount(auth.getAccountId()); } User user = userClient.fetchUser(account); if (user == null) { return null; } cache.cacheAll(token, account, user); return user; } catch (HttpException e) { if (e.getReason() == HttpException.Reason.TIMEOUT) { return this.executeLogin(authToken, ++retryCount); } return null; } } }だいぶシンプルになりました。
なお、このクラスを使う際には、Guiceでは以下のようにしてインジェクトします。Injector injector = Guice.createInjector(() -> { bind(Cache.class).toProvider(() -> { return new Cache(); }).asEagerSingleton(); bind(AuthClient.class).toProvider(() -> { return new AuthClient(HttpClientFactory.createClient("https://account.example.com"), new JsonMapperFactory()); }).asEagerSingleton(); bind(UserClient.class).toProvider(() -> { return new UserClient(HttpClientFactory.createClient("https://example.com"), new JsonMapperFactory()); }).asEagerSingleton(); bind(LogoutService.class).to(LogoutServiceImpl.class); }); // インスタンス生成 LoginService service = injector.getInstance(LoginService.class);Google Guice 使い方メモ
https://qiita.com/opengl-8080/items/6fb69cd2493e149cac3a実際のテスト
では、実際にテストの例を書いてみます。
メンバ変数はインジェクトできるわけですので、テストでもMockをインジェクトしてあげることで簡単にテストできるようになります。class LoginServiceTest { @Mock Cache cache; @Mock AuthClient authClient; @Mock UserClient userClient; @Mock LogoutService logoutService; Injector injector; @Before public void setup() { injector = Guice.createInjector(() -> { bind(Cache.class).toInstance(this.cache); bind(AuthClient.class).toInstance(this.authClient); bind(UserClient.class).toInstance(this.userClient); bind(LogoutService.class).toInstance(this.logoutService); }); } @Test public void 正常にログインできる() { Account account = new Account("accountId"); User expectedUser = new User("name001"); LoginService test = injector.getInstance(LoginService.class); when(cache.isLoggedIn()).thenReturn(false); when(cache.cacheToken("token01")).thenReturn("token01"); when(authClient.authorize("token01")).thenReturn(new AuthResult("account01")); when(cache.getAccount()).thenReturn(null); when(authClient.fetchAccount("account01")).thenReturn(account); when(userClient.fetchUser(account)).thenReturn(expectedUser); User actualUser = test.login("token01"); Assert.that(actualUser, is(expectedUser)); // 呼ばれた / 呼ばれていない の確認 verify(logoutService, never()).logout(); verify(cache, times(1)).cacheAll("token01", acount, expectedUser); } }こんな感じです。最初のやつと比べても、断然テストはしやすくなってるんじゃないかとは思います。
その他にもクラスが増えたのでテストの総数はあまり変わらないかもしれませんが、ひとつひとつのクラスのテストの作りやすさは最初より断然やりやすくなってるはずです。もっとテストしやすくするために
今回は割愛したのですが、実際にはこの
executeLoginは更に簡単にすることができるでしょう。
たとえば、 アクセストークンを取得した際ってだいたいは最終的にユーザー情報などを取得するためになると思うので、以下の処理をするサービスクラスを外だしし、アクセストークンを渡して、アカウントとユーザー情報を取得するサービスクラスを作ると LoginServiceはリトライとキャッシュ管理のみに専念させることができるようになるでしょう。AuthResult auth = authClient.aurhotize(token); // ここでキャッシュ読んでるから、ここをどうやってLoginServiceに残しつつこの全体の処理を外だしするかは要検討だけど Account account = cache.getAccount(); if (account == null) { account = authClient.fetchAccount(auth.getAccountId()); } User user = userClient.fetchUser(account); if (user == null) { return null; }「クラスを分割しすぎると何があるかわからなくなるんじゃないか」と心配する人もいるかもしれませんが、名前やパッケージ構成などを気にしていたら全然問題ありません。実際、ぼく自身クラス数が1000を超えるシステムを作ったことがありますが、どのパッケージに何を入れておくかさえちゃんとしていたらわからなくなるということもありませんでした。
ところで、JsonMapperFactoryは何だったのか
これも割愛をしていますが、このJsonMapperFactoryを作ることで、今度は
UserClientとAuthClientのテストをするのが容易になります。なぜなら、その際のテストではClientとJsonMapperFactoryをMock化することができるからです。このClientとJsonMapperは外部ライブラリ(という設定)なので、それらをテストする必要はありません。なので、容易にMock化できるととてもテストがしやすくなります。
このように、外部ライブラリの生成をラップ化するのはテスタブルなコードを書く第二の切り札です。
全部そうする必要はないですが、今回のJsonMapperのようにジェネリクスなものであれば、ファクトリクラスを作るのはとても有用です。最後に
もうひとつ重要なこととして、コードの読みやすさがあります。
テスト困難なコードを書く でも書きましたが、正しくテストするためには正しく処理を理解できることがとても大事です。
そのため、最初に書いた結論の部分はまもっていくようにするのが良いかと思います。
- 投稿日:2019-12-19T17:19:51+09:00
テスト困難なコードを書く
概要
今回、ユニットテストしづらいコードの書き方を紹介したいと思います。
コードの言語はJavaです(他の言語は読み替えて下さい)。この記事に書いた内容をリファクタリングする記事も書きました。
https://qiita.com/oda-kazuki/items/b66fe3d4efec822497e6結論
- 可読性を悪くする
- 結合度を高くする
- 循環的複雑度を10以上にする
- 凝集度を低くする
凝集度・結合度・循環的複雑度については以下でもまとめてます
https://qiita.com/oda-kazuki/items/a16b43dc624429de7db3テスト困難なコードサンプル
今回、以下のような処理を想定します。
- ログイン処理
- 入力:アクセストークン(指定しない場合、キャッシュがあればそれを利用する)
- 出力:ログインしたユーザー情報 (問題があった場合はnullを返却する)
- 処理:以下の処理を最大3回行う (スマホでのログインを想定し、通信でタイムアウトした場合時間を置いてリトライする)
- ログアウトしてない場合、自動でログアウト処理をしてからログイン処理を続行
- ログアウト処理では、キャッシュを全部削除する
- アカウントIDがキャッシュにない場合、アクセストークンをサーバーに渡して
アカウントIDを取得する(アカウントIDがキャッシュされている場合この処理はスルー)- 取得したアカウントIDからアカウント情報を取得
- アカウント情報を用いてユーザー情報を取得して返す
- その際のログイン情報とアクセストークン、はキャッシュに保持する(ログイン中かどうかをキャッシュによって判別)
また、記載を簡素化するため、以下のインタフェースの外部ライブラリを利用してるものとします。
public class HttpClientFactory { /** HTTP通信を行うためのクライアントを返す */ static Client createClient(String domain); } public interface Client { /** WebAPIでのGet */ HttpResult get(String path) throws HttpException; /** WebAPIでのPost */ HttpResult post(String path, Json json) throws HttpException; } public class HttpResult { /** JSONのResponseBodyを取得する */ public Json body(); } public class JsonMapper<T> { /** オブジェクトをJSONにシリアライズする */ public Json serialize(T obj); /** JSONをオブジェクトにデシリアライズする */ public T deserialize(Json json); }そんなわけでテストしづらいコード
まずはキャッシュを作成しましょう。ここはデザインパターンを使ったSingletonパターンで華麗にやっちゃいましょうか。デザインパターン使えるぼくかっこいい。
Cache.javapublic class Cache { private static Cache instance = new Cahce(); // こだわりポイント1: Mapの有効活用 private Map<String,Object> user; private String token; private String accountId; private Cache() { } public static Cache getInstance() { return instance; } public void setUser(Map<String,Object> u) { this.user = u; } public Map<String, Object> getUser() { return this.user; } public void setToken(String a) { this.token = a; } public String getToken() { return this.token; } public String setAccountId(String accountId) { this.accountId = accountId; } public String getAccountId() { return this.accountId; } }次に処理を書きます。
LoginService.javapublic class LoginService { // こだわりポイント2: クラス内部で初期化する private JsonMapper<Map<String,Object>> mapper = new JsonMapper<>(); // こだわりポイント1: 無駄にメンバ変数を使う private HttpException ex = null; private Map<String,Object> user = null; private Map<String,Object> body = null; private String accessToken = null; private Json json = null; public Map<String,Object> login(String accessToken) { // こだわりポイント1: メンバ変数を使いまわす this.accessToken = accessToken; this.account = null; // こだわりポイント1: ローカル変数を使いまわす Map<String,Object> user = null; this.ex = null; boolean result = true; this.json = null; this.body = null; if(this.accessToken != null) { getCache().setToken(accessToken); } else { this.accessToken = getCache().getToken(); } // こだわりポイント3: ネストいっぱい for (int i = 0; i < 3; i++) { if (getCache().getUser() != null) { result = this.logout(); if (result) { try { // こだわりポイント1: ここだけみてもいつ bodyに値が入ったかわからなくしてる this.getAccountId(); if (this.body != null) { this.getAccount(); this.account = this.body; if (this.body != null) { getCache().setAccountId(this.body.get("accountId")); this.getUser(); } } } catch (HttpException e) { // こだわりポイント1: 可読性悪いからこんなコメントいれるハメになってる // exは、getAccount, getAccountId, getUserで格納してる if (this.ex != null) { if(this.ex.getReason() == HttpException.Reason.TIMEOUT) { Thread.sleep(1); continue; } else { // その他のエラーがおきたので中断 break; } } } if(this.body != null) { // ユーザーがとれてるから処理をやめる user = this.body; break; } } else { // ログアウト失敗したから中断する this.body = null; break; } } try { // こだわりポイント1: DRY?なにそれ? this.getAccountId(); if (this.body != null) { this.getAccount(); if (this.body != null) { getCache().setAccountId(this.body.get("accountId")); this.getUser(); } } } catch (HttpException e) { // exは、getAccount, getAccountId, getUserで格納してる if (this.ex != null) { if(this.ex.getReason() == HttpException.Reason.TIMEOUT) { Thread.sleep(1); continue; } else { // その他のエラーがおきたので中断 break; } } } if(this.body != null) { // ユーザーがとれてるから処理をやめる user = this.body; } break; } if(user != null) { this.user = user; // こだわりポイント4: ログインだけじゃなくキャッシュの管理もするよ this.getCache().setUser(this.user); } return this.user; } // こだわりポイント4: 本来の役割とは違うメソッド private Cache getCache() { // こだわりポイント2: シングルトンを直接参照する return Cache.getInstance(); } // こだわりポイント1: getterっぽい名前のgetterじゃないメソッド // こだわりポイント4: 本来の役割とは違うメソッド private void getAccountId() throws HttpException { try { this.ex = null; if(getCache().getAccountId() != null) { // こだわりポイント1: メンバ変数をいろんな方法で使い回す this.body = new HashMap<>(); this.body.put("accountId", getCache().getAccountId()); } else { if(this.accessToken != null) { // こだわりポイント1: メンバ変数をいろんな方法で使い回す this.body = new HashMap<>(); this.body.set("token" , this.accessToken); // こだわりポイント2: staticメソッドの利用 & WebAPIを直接呼び出す this.json = Http.createClient("https://account.example.com").post("/auth", this.mapper.serialize(body)).body(); this.body = this.mapper.deserialize(this.json); } } } catch (HttpException e) { this.body = null; this.ex = e; throw e; } } // こだわりポイント1: getterっぽい名前のgetterじゃないメソッド // こだわりポイント4: 本来の役割とは違うメソッド private void getAccount() throws HttpException { try { this.ex = null; // こだわりポイント2: staticメソッドの利用 & WebAPIを直接呼び出す this.json = Http.createClient("https://account.example.com").get("/accounts/" + this.body.get("accountId")).body(); this.body = this.mapper.deserialize(this.json); } catch (HttpException e) { this.body = null; this.ex = e; throw e; } } // こだわりポイント1: getterっぽい名前のgetterじゃないメソッド // こだわりポイント4: 本来の役割とは違うメソッド private void getUser() throws HttpException { try { this.ex = null; // こだわりポイント2: staticメソッドの利用 & WebAPIを直接呼び出す this.json = Http.createClient("https://example.com").post("/users", this.mapper.serialize(this.body)).body(); this.body = this.mapper.deserialize(this.json); } catch (HttpException e) { this.body = null; this.ex = e; throw e; } } // こだわりポイント4: LoginServiceだけどログアウトもできちゃうよ public boolean logout() { this.ex = null; if (this.getCache().getUser() != null) { this.user = this.getCache().getUser(); try { // こだわりポイント2: staticメソッドの利用 & WebAPIを直接呼び出す Json json = Http.createClient("https://example.com").post("/logout", this.mapper.serialize(this.user)).body(); } catch (HttpException e) { this.ex = e; } } if (this.ex != null) { this.user = null; return false; } else { this.user = null; // こだわりポイント4: ログアウトだけじゃなくキャッシュの削除もするよ Cache.getInstance().setUser(null); Cache.getInstance().setAccountId(null); Cache.getInstance().setToken(null); return true; } } }我ながらなかなか頭がクラクラするものをかけました。これをテストしろって言われたらちょっと泣いちゃう。
今から書くこだわりポイントをもっと徹底すればさらなる高みへと登ることができそうですが、高みに登りすぎてだれもついてこれなくなるのは嫌なので今回はこれくらいにしておきます。こだわりポイント
それでは、ぼくがこだわった「テストしづらいポイント」を説明していきます。
こだわり1.可読性にこだわりました
ユニットテストを作る目的は、大前提として「仕様通りであること」を確認するというのにあります。なので、あくまでも「コード内でどんな処理が記述されているか知らない」体で期待する結果を確認していく必要があります。そうしないと、単に「書かれたコードを確認してるだけで本当に必要なテストをしていない」というテストのアンチパターンに陥ります。
とはいえ、ユニットテストを通すためにはある程度そのメソッド内でどういう処理がされているのかも知っている必要があります。そのテストの対象ではない、クラスの外側にある処理はモック化しながら作成をしていくためです。
なので、可読性を悪くするだけでもだいぶテストは作りづらくなります。今回は以下のやり方で可読性を下げました。
- ローカル変数を最初に初期化して使い回す
- それが何をあらわしているのかよくわからない変数がある
- getXXとか書いてるのに値をgetterではないメソッドがある
- 不要なメンバ変数を作成し、かつ複数の用途で使い回す
- DRY原則は無視する
- ひとつの処理のまとまりを、プライベートメソッド内だけで完結させない
- 何でも入れられるMapを活用する
最後のだけちょっと補足すると、たとえば
getAccount()はgetAccountId()を呼び出していることが前提となっていて、これだけを実行しても失敗します。なので、処理をちゃんと理解しようとする場合はあちこちコードを飛び飛びで確認していかなければならないため、可読性の悪さに貢献しています。こだわり2. 依存性にこだわりました
この LoginService に書かれてあるメソッドは、全てlogin()メソッドとえぐく結合させました。
これはテストのしづらさにそのまま直結してくれます。密な結合にするため、以下のことを行いました。
- シングルトンをそのまま使う(外部結合および共有結合にできる)
- WebAPIのリクエストを行う(同上)
- Staticメソッドを使う(インタフェースじゃないためMock化が困難)
- メンバ変数を自分自身で初期化する(同じくMock化が困難)
これらを活用することで、テストをする際に「PowerMock」などを使って無理やりMock化させたりしないとテスト困難にすることができます。
もし、無理やりMock化することができない言語の場合、諦めてWebAPIの接続など許容するしかないですね。ただその場合、Webサーバー側のデータに依存するので状態によってはテストに失敗するようになります。しびれますね。
本当は制御結合なども盛り込みたかったのですが、面倒だったのでやめました。こだわり3. ネストにこだわりました(循環的複雑度が高い)
無駄にif文などを使いまくった結果、循環的複雑度は20くらいになってます(ちゃんとはかってないけど)。
これは、少なくとも20パターンのテストをする必要があることを表していますし、そのルートをちゃんと通すために、Mockにするのも針に糸を通すような感じでやらないといけないでしょう。プライベートメソッドがあるのも困難さに拍車をかけています。
作成時に発狂する姿が目に浮かびますね。こだわり4. 神クラスを目指しました(凝集度が低い)
このLoginServiceは、ログインに必要な全ての処理を自分自身が持っており、凝集度がとても低い仕上がりとなっております。今はまだ行数が少ないので神クラスと名乗るにはおこがましいですが、その見込はあるでしょう。
このLoginServiceは、少なくとも以下の役割を担ってます。
- 4つのWebAPIのリクエストを発行する
- 取得した情報をキャッシュに保存する
- ログインのフロー(ビジネスロジック)の記述
- ログアウトのフロー(ビジネスロジック)の記述
これらがいっしょくたになっていることで、ただログインのフローをテストしたいだけなのにWebAPIのリクエストやら、ログアウトのロジックやらキャッシュのことやらいろんな雑音を考慮しなければならなくなります。
更に結合度の高さも相まってテストを記述する際のSAN値を削ることができるでしょう。というわけで
次回はこのコードをもうちょっとテストしやすい形に作り変えようかと思います。
- 投稿日:2019-12-19T17:10:53+09:00
androidアプリのdebugビルド署名をリリース署名に変更すると、色々便利だ
はじめに
androidアプリのdebugビルド署名、普通はリリースビルド署名と違いますね。
まあリリースビルド署名のキーストアは重要なので、利用制限したほうがいいんですが、debugビルド署名をリリースビルド署名に変更すると、開発が色々楽になります。
リリース版にdebug版を上書きインストールしたりできますからね。
やりかた
signingConfigs { //DEBUG版をリリースビルド署名したいときに、これを有効にする debug { storeFile=file(project.properties.storeFile) storePassword=project.properties.storePassword keyAlias=project.properties.keyAlias keyPassword=project.properties.keyPassword } }こんなんを追加するのみ。
効果
- リリースアプリにandroidStuidoから上書きインストールできる。これがあると本番データ使って色々できる。バージョンアップ時のデータ移行のdebugとかね。
- google認証とか、サーバー側にdebugのキーストアを登録せず、debug版で認証できるようになる。
- デバイスにリリース版がインストールされているとき、debugインストールに失敗しない。いちいちアンインストールを求められない。これ、マジでうざい。初期インストールになっちゃうと、色々消えてしまうし。
まとめ
いや、まじで便利。もっと早くやればよかったと後悔している今日この頃
いや、変えられるのは前から知ってたんですが、必要に駆られてやってみたら、もう・・・・。セキュリティ的に問題がないのであれば、オススメです。
まあパッケージ名同じ場合のみですがね。
- 投稿日:2019-12-19T16:32:03+09:00
SpringBoot + JODConverter で OnlineConverter を使ってみた
概要
jodConverter を OnlineConverter で利用した際のメモ。
SpringBoot を利用していたため、JODConverter Spring Boot Starter を利用しての実装となっている。環境
- SpringBoot:2.1.3.RELEASE
- JODConverter:4.1.0
- JODConverter Spring Boot Starter
OnlineConverter呼び出し側の実装
- 基本的にはLocalConverterを利用する時と変わらないが、application.yml に OnlineConverterサーバーのURLを指定する
- リクエスト時のURLは
【ymlに設定したURL】/lool/convert-to/【変換先の拡張子】という形で送信されるapplication.ymljodconverter: online: enabled: true url: 【OnlineConverterサーバーのURL】OnlineConverterサーバーの実装
build.gradle の dependencies に以下の設定を追記する
compile 'org.jodconverter:jodconverter-spring-boot-starter:4.1.0'application.yml に LibreOffice のインストール先を設定する
application.ymljodconverter: enabled: true officeHome: 【LibreOffice インストール先】
- リクエストを受け取るための Controller を実装する
- MultipartFile で変換元のファイルの受け取りを行う
- outputExtension で変換後の拡張子を指定する
@RestController public class SampleController { @Autowired private DocumentConverter documentConverter; @RequestMapping(path = "/api/jodconverter/lool/convert-to/{outputExtension}", method = RequestMethod.POST) public Resource convert( @RequestParam("data") MultipartFile file, @PathVariable("outputExtension") String outputExtension ) throws IOException, OfficeException { // 変換前ファイル情報取得 String inputExtension = FilenameUtils.getExtension(file.getOriginalFilename()); DocumentFormatRegistry registry = this.documentConverter.getFormatRegistry(); DocumentFormat inputFormat = registry.getFormatByExtension(inputExtension); if (inputFormat == null) { inputFormat = registry.getFormatByExtension("xlsx"); } // 変換後拡張子設定 DocumentFormat outputFormat = registry.getFormatByExtension(outputExtension); // 変換処理 try ( InputStream is = file.getInputStream(); ByteArrayOutputStream os = new ByteArrayOutputStream(); ) { this.documentConverter.convert(is).as(inputFormat).to(os).as(outputFormat).execute(); return new ByteArrayResource(os.toByteArray()); } catch (Exception e) { e.printStackTrace(); } return null; } }まとめ
OnlineConverter は今まで使う機会がなかったため、使い方の勉強になった。
ただ、LocalConverter のほうが OnlineConverter 利用時のサーバーの実装がない分簡単に導入できるので、
よほどの理由がない限り使わないかな。。。
- 投稿日:2019-12-19T13:08:56+09:00
[Heroku] SpringBootにNginxのビルドパックを実装してみた。
[Heroku] SpringBootにNginxのビルドパックを実装してみた。
HerokuはDynoのポートをHerokuルータにバインドして、WebプロセスがHTTPリクエストを受け付けています。そのため、Webサーバーの手前に、Reverse Proxyサーバーを立てることができません。
しかしながら、NGiNXのようなソフトウェアをDynoと組み込むことにより、アプリケーションサーバーへ到達する前に、リクエストをさばくことができます。
今回はHerokuのレポジトリから参照できるこちらのビルドパックを使用することにします。
Github にあるドキュメントですと、
1. /tmp/nginx.socketのUNIXドメインソケットに接続する。
2. /tmp/app-initializedにtouchにてファイル作成をするとあります。しかしながら、JavaではUNIXドメインソケットに接続がサポートされていないため、不特定のポート番号を$APP_PORTへアサインし、WebサーバーをNginxへBindしてあげる必要がある
1 - WebサーバーをNginxへBindする。
1.1 APP_PORTをアサイン
heroku config:set APP_PORT=1234を実行1.2 NGINXのコンフィグファイルを変更
nginx.conf.erb のファイルにある以下を変更。
変更前:
upstream app_server { server unix:/tmp/nginx.socket fail_timeout=0; }変更後
upstream app_server { server localhost:<%= ENV["APP_PORT"] %> fail_timeout=0; }2 - Webサーバー起動後にファイルを作成
public static void main(String[] args) throws IOException { SpringApplication.run(DemoApplication.class, args); FileUtils.touch(new File("/tmp/app-initialized")); }3. Buildpackを追加します。
$ heroku buildpacks:add ご自身のgithubレポジトリURL4. Procfileの記述を以下のようにし、デプロイします。
web: bin/start-nginx java -Dserver.port=$APP_PORT $JAVA_OPTS -jar target/*.jar
-Dserver.port=$PORTとするとWebサーバーがHerokuルーターにバインドしてしまうので、Dserver.port=$APP_PORTとすることでNGINXとWebサーバーがバインドしてくれます。Nginx側で$PORTをバインドしてくれています。5. Nginxを実装してみる
server { listen <%= ENV["PORT"] %>; server_name _; keepalive_timeout 5; location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_redirect off; proxy_pass http://app_server; } location /google/ { return 301 https://www.google.com/; } }無事にリダイレクトできました。
- 投稿日:2019-12-19T13:06:08+09:00
【JAVA】interfaceとabstractの違いって???
ざっくり違いを解説
interface ・・・クラスじゃない(重要)
abstract ・・・クラス等の修飾子
interfaceについて
①変数
変数の宣言および実装ができる。
ただし、暗黙的に「public static final」が付く。interface noTsukaikata { int Num = 6; //finalが付くので初期化できない。→実質的に定数扱いになる。 }②メソッド
抽象化メソッドのみ宣言できる。
interface noTsukaikata { void method(); //抽象化メソッドの宣言(interface内のメソッドはabstract句が付与される) // abstract void method();に自動変換される。 }実は、default句かstatic句を使うことで実装もできる。
interface deJisou{ //default句を使うことで、interface内にメソッドを実装 default void method1(){ //more codes here } //static句を使うことで、interface内にメソッドを実装 static void method2(){ //more codes here } //default句かstatic句がないとコンパイルエラー void method3() { //more codes here } }③継承方法
interfaceはimplementsのみ継承可能です。
※interfaceで未実装のメソッドは全てオーバーライドしなければならない。interface Super{ int Num; default void method1(){ //more codes here. } static void method2(){ //more codes here. } void method3(); } class Sub implements Super{ //変数はfinalなので書く必要ない。 //defaultメソッドは実装済みのため、Subクラスで実装しなくてよい。必要に応じてオーバーライド可能。 //staticメソッドは継承対象外のため、Subクラスに実装できない。 //method3を実装 @Override protected void method3(){//公開範囲(スコープ)が狭くならない限り、アクセス修飾子を変化させてもよい。 //more codes here. } }上の継承によるSubクラスの中身は、以下の通り。
class Sub implements Super{ int Num = 6; default void method1(){ //more codes here. } //staticメソッドは継承対象外のためいない。 protected void method3(){ //more codes here. } }abstractについて
①変数
通常クラスと全く同じ扱い方。
abstract class Super { int Num1; //宣言できる int Num2 = 6; //初期化できる abstract int Num3 = 10; //コンパイルエラー(変数を抽象化できない) }②メソッド
抽象化メソッドも宣言できる。
interface Super { abstract void method1(); //抽象メソッドを宣言できる。 void method2() {} //abstract句が無ければ実装もできる。 }③継承方法
abstractはextendsのみ継承可能です。
※abstractメソッドは全てオーバーライドしなければならない。abstract class Super { int Num1; int Num2 = 6; abstract void method1(); void method2() {} } class Sub extends Super{ @Override void method1() {} }上の継承によるSubクラスの中身は、以下の通り。
class Sub extends Super{ int Num1; int Num2 = 6; void method1() {} void method2() {} }結局何が違ったのか???
interface abstract 変数 自動的に「public static final」 通常クラスと全く同じ メソッド 基本的に抽象メソッドのみ宣言できる。ただし、「default」や「static」句を利用し、実装できる。 抽象メソッドも宣言できる。 継承方法 implementsにより継承。サブクラスでは抽象メソッドをすべて実装する。 extendsにより継承。サブクラスでは抽象メソッドをすべて実装する。 補足
implementsとextendsの違いは???
implements・・・interfaceを継承する。
extends・・・クラスを継承する。結論、
interface ・・・クラスじゃない
abstract ・・・クラス等の修飾子
- 投稿日:2019-12-19T13:04:57+09:00
SpringBoot1.5 + Gradle4.4 + Java8 + Docker 環境を Java11 に対応させる
この記事は、Java Advent Calendar 2019 15日目の記事です。
はじめに
Java8の無償サポートが2019年1月に切れてから一年ほど放置してましたが、
今回時間ができたので8以降初のLTSであるJava11への更新を行いました
その時のメモですと書き始めようとしましたが、OpenJDK8は2023年6月までサポートされるらしいので、
OracleJDKからOpenJDKに変えるだけでよかったのでは......
でもやってしまったので記録しときますJDKのアップデート
AdoptOpenJDK11をインストール
以下のコマンドを入れると最新のJDKがインストールされる
brew cask install adoptopenjdkしかし、現状ではJDK13が入ってしまうので、LTS版であるJDK11を入れるにはバージョンを指定する必要がある
ということで公開されているバージョンを検索$ brew search adoptopenjdk ==> Casks adoptopenjdk adoptopenjdk11-openj9-jre adoptopenjdk12-openj9 adoptopenjdk13-jre adoptopenjdk8 adoptopenjdk8-openj9-large adoptopenjdk10 adoptopenjdk11-openj9-jre-large adoptopenjdk12-openj9-jre adoptopenjdk13-openj9 adoptopenjdk8-jre adoptopenjdk9 adoptopenjdk11 adoptopenjdk11-openj9-large adoptopenjdk12-openj9-jre-large adoptopenjdk13-openj9-jre adoptopenjdk8-openj9 adoptopenjdk11-jre adoptopenjdk12 adoptopenjdk12-openj9-large adoptopenjdk13-openj9-jre-large adoptopenjdk8-openj9-jre adoptopenjdk11-openj9 adoptopenjdk12-jre adoptopenjdk13 adoptopenjdk13-openj9-large adoptopenjdk8-openj9-jre-large今回入れたいのはJDK11なので、以下のコマンドでバージョンを指定してインストール
brew cask install adoptopenjdk11すでに通っているPATHをJDK11に変更
export JAVA_HOME=`/usr/libexec/java_home -v 11`バージョンを確認
$ java -version openjdk version "11.0.5" 2019-10-15 OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.5+10) OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.5+10, mixed mode)IntelliJのProjectSDKにJDK11を指定
Command + ; でProjectStructureを開き、ProjectSDKとProjectLunguageLebelにJDK11を指定
Gradleをアップデート
Gradle4.4はJDK11に対応していないため、アップデートする必要がある
5.0以降のバージョンは対応しているようだが、折角なので現在の最新である6.0.1にしてみるgradle-wrapper.propertiesの更新
gradle/wrapper/gradle-wrapper.properties に記載されているバージョンを更新すればプロジェクトに適用されるGradleのバージョンが変更される
今回は以下の 4.4 の部分を 6.0.1 に変更するだけdistributionUrl=https\://services.gradle.org/distributions/gradle-4.4-bin.zipgradlewを更新
それだけだとgradlewに変更が伝播しないので、gradlewを生成し直す
./gradlew wrappergradle.buildを更新
各種フレームワーク・ライブラリをJDK11に対応したものに変更していく
基本的には全てgradle.buildに記載されているので、それを修正していけばよいJavaのバージョン指定を変更
各指定の意味は公式に記載されているので、必要な物を指定する
sourceCompatibility = 11 targetCompatibility = 11Gradle6系の記載方法に変更
jarの指定方法が変更になっているので更新
詳細は公式を参照のことjar { baseName = 'example' version = '1.0.0-SNAPSHOT' }上記だったものを以下に変更
bootJar { archiveBaseName = "example" archiveVersion = "1.0.0-SNAPSHOT" }SpiringBootのバージョンを変更
Java11を利用するにはSpringBoot2.1.x以上が必要
折角なので現在の最新である2.2.2にしてみる
基本的には公式のマイグレーションガイドに従えばいいbuildscript { ext { springBootVersion = '1.5.13.RELEASE' } repositories { mavenCentral() } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") } }SpringBootが自動で依存関係管理プラグインを適用しなくなったので、プラグインも追加する
apply plugin: 'io.spring.dependency-management'メインクラスの指定方法が変わっているので、それも変更
springBoot { mainClassName = "jp.co.example.Application" }ライブラリのバージョンを更新
MavenRepositoryを見ながらライブラリのバージョンを更新していく
SpringBoot周りのライブラリ
SpringBoot周りのライブラリはバージョンを変数で指定しておくと楽
ただし、一部SpringBootのバージョンと異なる場合があるので注意が必要dependencies { compile("org.springframework.boot:spring-boot-starter-web:${springBootVersion}") compile("org.springframework.boot:spring-boot-starter-jdbc:${springBootVersion}") compile("org.springframework.boot:spring-boot-starter-security:${springBootVersion}") compile("org.springframework.boot:spring-boot-starter-actuator:${springBootVersion}") testCompile("org.springframework.boot:spring-boot-starter-test:${springBootVersion}") testCompile("org.springframework.security:spring-security-test:5.2.1.RELEASE") }lombok
依存関係の指定方法が変わっているため、普通にバージョンだけ指定して更新するとビルド時にlombokで自動生成されるメソッドが見つからないとエラーが出る
公式を参考にして、以下の指定を追加するcompileOnly('org.projectlombok:lombok:1.18.10') annotationProcessor('org.projectlombok:lombok:1.18.10')また、@Valueを使っていると、Jacksonでのデシリアライズがうまく機能しないエラーが発生する
project直下に lombok.config というファイルを作って以下を記載することで回避できる
その他の設定については公式のマイグレーションガイドに記載があるlombok.addJavaxGeneratedAnnotation = false lombok.addLombokGeneratedAnnotation = true lombok.noArgsConstructor.extraPrivate = false lombok.anyConstructor.addConstructorProperties = true config.stopBubbling = truejjwt
一部のアルゴリズムを利用する際にJava11で廃止されたAPIに依存しているため、
Java8時点で利用していてJava11に移行してきた場合、依存関係を追加する必要がある
公式を参考に以下を追加するcompile('javax.xml.bind:jaxb-api:2.3.0') compile('com.sun.xml.bind:jaxb-core:2.3.0') compile('com.sun.xml.bind:jaxb-impl:2.3.0')その他
よしなに更新する
dependencies 以外で指定しているライブラリについても忘れず更新すること
jacoco の更新を忘れて時間を無駄にしましたJava11で廃止になったAPIに対応する
この記事を参考に廃止になったAPIを修正していく
SpringBoot2.0で廃止になったクラスを修正する
ビルドしてみるとSpringBoot周りで廃止されたクラスが複数出てくるので、それを修正していく
公式のマイグレーションガイドを参考にする今回はエラーメッセージを制御していた org.springframework.boot.autoconfigure.web 周りのクラスだけが対象だった
以下のメソッドを@RequestMapping(value = "/error") public ErrorResponse error(HttpServletRequest request, HttpServletResponse response) { ServletRequestAttributes attributes = new ServletRequestAttributes(request); Throwable error = errorAttributes.getError(attributes); return convertException(error); }こうする
@RequestMapping(value = "/error") public ErrorResponse error(WebRequest request) { Throwable error = errorAttributes.getError(request); return convertException(error); }テストしてみる
ここまでやるととりあえずビルドが通るようになるので、テストを回してデグレが起きていないか確認する
今回は特に問題なさそう
Warnの解消は後回しにして、とりあえずJDK11対応済みのアプリをDockerに固めてデプロイすることを目指すDockerfileを変更する
Dockerfileはベースイメージの変更だけでよい
どのイメージを選択すればいいかはこの記事が参考になる
今回はおすすめの通り、adoptopenjdk/openjdk11:alpine-slim を選択するFROM adoptopenjdk/openjdk11:alpine-slimその他
CircleCIの設定を変更する
CI/CDツールを利用している場合はそちらの設定も忘れず変更すること
テスト環境構築時のベースイメージの指定等、忘れがちSpringBootActuator
SpringBootActuatorで自動生成される全てのエンドポイントに/actuatorのprefixが追加されているので注意が必要
特に /health を利用しているとデプロイがうまくいかなくなる
その他の変更点はこの記事が参考になるさいごに
以上の修正でとりあえずデプロイできる状態にはなっているはずです
何か問題が起きれば適宜追記していきます
「こっちの書き方の方が正しい」「これだとバグる可能性がある」等あれば是非教えてください参考
mac上にhomebrewでAdoptOpenJDK11をインストール
Gradle wrapper のバージョン更新についてのメモ
Spring Boot 1.5.10 → Spring Boot 2.0.0 にしたときの覚書
Spring Boot 2.0 Migration Guide
Java 11 リリース後のオススメ Docker イメージを考える
Spring Boot 2.0のActuator、とりあえず動かすために知っておきたい変更点3つ
Javaエンジニアが Java 11 リリースに向けて備えておくべきこと
ObjectMapper can't deserialize without default constructor after upgrade to Spring Boot 2
- 投稿日:2019-12-19T13:04:57+09:00
SpringBoot1.5 + Gradle4.4 + Java8 + Docker 環境を JDK11 に対応させる
はじめに
Java8の無償サポートが2019年1月に切れてから一年ほど放置してましたが、
今回時間ができたので8以降初のLTSであるJava11への更新を行いました
その時のメモですと書き始めようとしましたが、OpenJDK8は2023年6月までサポートされるらしいので、
OracleJDKからOpenJDKに変えるだけでよかったのでは......
でもやってしまったので記録しときますJDKのアップデート
AdoptOpenJDK11をインストール
以下のコマンドを入れると最新のJDKがインストールされる
brew cask install adoptopenjdkしかし、現状ではJDK13が入ってしまうので、LTS版であるJDK11を入れるにはバージョンを指定する必要がある
ということで公開されているバージョンを検索$ brew search adoptopenjdk ==> Casks adoptopenjdk adoptopenjdk11-openj9-jre adoptopenjdk12-openj9 adoptopenjdk13-jre adoptopenjdk8 adoptopenjdk8-openj9-large adoptopenjdk10 adoptopenjdk11-openj9-jre-large adoptopenjdk12-openj9-jre adoptopenjdk13-openj9 adoptopenjdk8-jre adoptopenjdk9 adoptopenjdk11 adoptopenjdk11-openj9-large adoptopenjdk12-openj9-jre-large adoptopenjdk13-openj9-jre adoptopenjdk8-openj9 adoptopenjdk11-jre adoptopenjdk12 adoptopenjdk12-openj9-large adoptopenjdk13-openj9-jre-large adoptopenjdk8-openj9-jre adoptopenjdk11-openj9 adoptopenjdk12-jre adoptopenjdk13 adoptopenjdk13-openj9-large adoptopenjdk8-openj9-jre-large今回入れたいのはJDK11なので、以下のコマンドでバージョンを指定してインストール
brew cask install adoptopenjdk11すでに通っているPATHをJDK11に変更
export JAVA_HOME=`/usr/libexec/java_home -v 11`バージョンを確認
$ java -version openjdk version "11.0.5" 2019-10-15 OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.5+10) OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.5+10, mixed mode)IntelliJのProjectSDKにJDK11を指定
Command + ; でProjectStructureを開き、ProjectSDKとProjectLunguageLebelにJDK11を指定
Gradleをアップデート
Gradle4.4はJDK11に対応していないため、アップデートする必要がある
5.0以降のバージョンは対応しているようだが、折角なので現在の最新である6.0.1にしてみるgradle-wrapper.propertiesの更新
gradle/wrapper/gradle-wrapper.properties に記載されているバージョンを更新すればプロジェクトに適用されるGradleのバージョンが変更される
今回は以下の 4.4 の部分を 6.0.1 に変更するだけdistributionUrl=https\://services.gradle.org/distributions/gradle-4.4-bin.zipgradlewを更新
それだけだとgradlewに変更が伝播しないので、gradlewを生成し直す
./gradlew wrappergradle.buildを更新
各種フレームワーク・ライブラリをJDK11に対応したものに変更していく
基本的には全てgradle.buildに記載されているので、それを修正していけばよいJavaのバージョン指定を変更
各指定の意味は公式に記載されているので、必要な物を指定する
sourceCompatibility = 11 targetCompatibility = 11Gradle6系の記載方法に変更
jarの指定方法が変更になっているので更新
詳細は公式を参照のことjar { baseName = 'example' version = '1.0.0-SNAPSHOT' }上記だったものを以下に変更
bootJar { archiveBaseName = "example" archiveVersion = "1.0.0-SNAPSHOT" }SpiringBootのバージョンを変更
Java11を利用するにはSpringBoot2.1.x以上が必要
折角なので現在の最新である2.2.2にしてみる
基本的には公式のマイグレーションガイドに従えばいいbuildscript { ext { springBootVersion = '1.5.13.RELEASE' } repositories { mavenCentral() } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") } }SpringBootが自動で依存関係管理プラグインを適用しなくなったので、プラグインも追加する
apply plugin: 'io.spring.dependency-management'メインクラスの指定方法が変わっているので、それも変更
springBoot { mainClassName = "jp.co.example.Application" }ライブラリのバージョンを更新
MavenRepositoryを見ながらライブラリのバージョンを更新していく
SpringBoot周りのライブラリ
SpringBoot周りのライブラリはバージョンを変数で指定しておくと楽
ただし、一部SpringBootのバージョンと異なる場合があるので注意が必要dependencies { compile("org.springframework.boot:spring-boot-starter-web:${springBootVersion}") compile("org.springframework.boot:spring-boot-starter-jdbc:${springBootVersion}") compile("org.springframework.boot:spring-boot-starter-security:${springBootVersion}") compile("org.springframework.boot:spring-boot-starter-actuator:${springBootVersion}") testCompile("org.springframework.boot:spring-boot-starter-test:${springBootVersion}") testCompile("org.springframework.security:spring-security-test:5.2.1.RELEASE") }lombok
lombokを利用している場合注意が必要
依存関係の指定方法が変わっているため、普通にバージョンだけ指定して更新するとビルド時にlombokで自動生成されるメソッドが見つからないとエラーが出る
公式を参考にして、以下の指定を追加するcompileOnly('org.projectlombok:lombok:1.18.10') annotationProcessor('org.projectlombok:lombok:1.18.10')その他
よしなに更新する
dependencies 以外で指定しているライブラリについても忘れず更新すること
jacoco の更新を忘れて時間を無駄にしました廃止になったクラスを修正する
ビルドしてみるとSpringBoot周りで廃止されたクラスが複数出てくるので、それを修正していく
公式のマイグレーションガイドを参考にする今回はエラーメッセージを制御していた org.springframework.boot.autoconfigure.web 周りのクラスだけが対象だった
以下のメソッドを@RequestMapping(value = "/error") public ErrorResponse error(HttpServletRequest request, HttpServletResponse response) { ServletRequestAttributes attributes = new ServletRequestAttributes(request); Throwable error = errorAttributes.getError(attributes); return convertException(error); }こうする
@RequestMapping(value = "/error") public ErrorResponse error(WebRequest request) { Throwable error = errorAttributes.getError(request); return convertException(error); }テストしてみる
ここまでやるととりあえずビルドが通るようになるので、テストを回してデグレが起きていないか確認する
今回は特に問題なさそう
Warnの解消は後回しにして、とりあえずJDK11対応済みのアプリをDockerに固めてデプロイすることを目指すDockerfileを変更する
Dockerfileはベースイメージの変更だけでよい
どのイメージを選択すればいいかはこの記事が参考になる
今回はおすすめの通り、adoptopenjdk/openjdk11:alpine-slim を選択するFROM adoptopenjdk/openjdk11:alpine-slimその他
CircleCIの設定を変更する
CI/CDツールを利用している場合はそちらの設定も忘れず変更すること
テスト環境構築時のベースイメージの指定等、忘れがちSpringBootActuator
SpringBootActuatorで自動生成される全てのエンドポイントに/actuatorのprefixが追加されているので注意が必要
特に /health を利用しているとデプロイがうまくいかなくなる
その他の変更点はこの記事が参考になるさいごに
以上の修正でとりあえずデプロイできる状態にはなっているはずです
「こっちの書き方の方が正しい」「これだとバグる可能性がある」等あれば是非教えてください参考
mac上にhomebrewでAdoptOpenJDK11をインストール
Gradle wrapper のバージョン更新についてのメモ
Spring Boot 1.5.10 → Spring Boot 2.0.0 にしたときの覚書
Spring Boot 2.0 Migration Guide
- 投稿日:2019-12-19T12:43:29+09:00
Javaで日付処理 (LocalDate: 初期化編)
日付の取り扱い
日付の取り扱いに関しては Java 8 から新しいAPIが導入されました。
今まで標準のAPIで Date, Calendar を使用していた日付操作が LocalDate, LocalDateTime 等に変わってきています。日付の初期化
今回は LocalDate の簡単な使い方と使用時の注意点、実はこんな便利な使い方があったというものを紹介していきます。
後半には取り扱い方の注意点を紹介していますので最後まで見てみてください。現在日で初期化
現在日を取得するために Java8 以前では、以下のようなコードを書いていました。
Date currentDate1 = new Date(); Calendar currentDate2 = Calendar.getInstance();しかしこのコードでは日付だけでなく時間も含まれているため日付のみ使用する場合には注意が必要でした。
例えばある日付の間にある情報を取得したい場合にこの時間が影響してしまい、意図しないデータがとれてきてしまうこともあります。
意図しない動きをしないように時間部分をカットする必要があります。final LocalDate currentDate = LocalDate.now(); System.out.println(currentDate); // 2020-01-15LocalDate を使用し現在日を設定する場合は now メソッドを使用します。
LocalDate の場合、年月日がセットされ時間以降は取り扱われません。日付を指定して初期化
日付を指定して初期化するために Java8 以前では、以下のようなコードを書いていました。
Calendar calendar = Calendar.getInstance(); calendar.set(Calendar.YEAR, 2019); calendar.set(Calendar.MONTH, 11); calendar.set(Calendar.DAY_OF_MONTH, 25);この書き方の場合月は0はじまりのため、11をセットすると12月になります。
またこれも時間がついてくるため、時間のカットが必要になります。LocalDate targetDate = LocalDate.of(2019, 12, 25) System.out.println(targetDate); // 2019-12-25日付を指定して初期化を行う場合 of メソッドを使用します。
Date や Calendar に比べると日付の設定が直感的になり楽になりました。文字から日付を初期化
文字から日付を取得するために Java8 以前では、以下のようなコードを書いていました。
DateFormat formatter = new SimpleDateFormat("yyyy/MM/dd"); Date date = formatter.parse("2019/12/25");こちらは見たまま直感的に文字列から日付へ変更できています。
しかし実は SimpleDateFormat はスレッドセーフではありません。
スレッドプログラミングやWebアプリケーションを作る際に、誤った使い方をすると別のユーザーの日付が書き換わるといった現象が発生してしまいます。DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd"); LocalDate targetDate = LocalDate.parse("2019/12/25", formatter); System.out.println(targetDate); // 2019-12-25日付の文字列から LocalDate 型のインスタンスを生成します。
DateTimeFormatter スレッドセーフのため安心です。LocalDate targetDate = DateTimeFormatter.ofPattern("yyyy/MM/dd") .parse("2019/12/25", LocalDate::from); System.out.println(targetDate); // 2019-12-25このようなコードも書けます。
上記のコードは一見すると良さそうに見えますが問題があります。[問題1] 正確な日付の取り扱い
先ほどのコードを真似て以下のコードを書いてみました。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd") .parse("2020/02/30", LocalDate::from); System.out.println(targetDate); // 2020-02-292020年2月は29日までしかなく、30日を指定してもエラーにならず勝手に29日に変換されました。
一見便利にみえますが、正確な日付チェックや変換を行いたい場合には向いていません。
そこで以下のように書き換えます。DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd") .withResolverStyle(ResolverStyle.STRICT) .parse("2020/02/30", LocalDate::from); System.out.println(targetDate); // 例外発生ResolverStyle.STRICT を設定することで日付を厳密に解決するため、不正な日付の場合は例外が発生するようになります。
[問題2] 日付フォーマット
先程のプログラムを少し変えて日付文字列のスラッシュがないバージョンを作りました。
LocalDate targetDate = DateTimeFormatter.ofPattern("yyyyMMdd") .withResolverStyle(ResolverStyle.STRICT) .parse("20191225", LocalDate::from); System.out.println(targetDate); // ???こちらのコードは正しいように見えますが出力時に例外が発生します。
DateTimeFormatter (Java Platform SE 8)
上記のドキュメントを見ると以下のように書かれています。
Symbol Meaning Presentation Examples g era text AD; Anno Domini; A u year year 2004; 04 y year-of-era year 2004; 04 フォーマットのシンボル y には year-of-era と表記されています。
era は暦を表しており先程の STRICT で厳密に日付を取り扱ったことで暦を判定できずエラーとなっています。LocalDate targetDate = DateTimeFormatter.ofPattern("uuuuMMdd") .withResolverStyle(ResolverStyle.STRICT) .parse("20191225", LocalDate::from); System.out.println(targetDate); // 2019-12-25シンボルを y から u へ変更したところ、日付の変換が成功しました。
LocalDate targetDate = DateTimeFormatter.ofPattern("GGGGyy年MM月dd日") .withLocale(Locale.JAPAN) .withChronology(JapaneseChronology.INSTANCE) .withResolverStyle(ResolverStyle.STRICT) .parse("平成30年12月25日", LocalDate::from); System.out.println(targetDate); // 2018-12-25ちなみに和暦を取り扱う場合は上記のように書きます。
※ 和暦の取り扱いは厳密には難しいためここでは簡易的な変換のみ紹介ですまとめ
Java 8 から日付の取り扱い方がかなり簡単になりました。
しかし使用時には色々な注意点があることもわかりました。
ネットで調べれば使い方はすぐに出てきますが、本当にそのやり方が正しいかはきちんとした知識がないと判断できません。
今回紹介した内容も含めてきちんと公式のドキュメントを見ることも一緒におすすめします。
- 投稿日:2019-12-19T11:49:40+09:00
コードを複雑化させないために意識したいパターン集
はじめに
はじめはシンプルだったコードも積み重なる機能追加や変更、バグ修正等などによって、徐々にコードが複雑化し、修正コストの増加や品質低下に繋がります。
これはある程度の規模を持つプロダクトでは至って自然なことであり、そうならないようにするためには意識して設計しなければいけません。ここではコードを複雑化させないために普段意識している手法やパターンを紹介します。(DDDやClean Architecture成分多め)
また本記事で登場するコードはJavaですが、Javaを知らなくてもある程度は理解できるかなと思います。あと、割とまとまりもなく幅広い範囲でダラダラと書いてしまい長いです...
以下項目ごとのアンカーリンクとなっておりますので興味がある項目のみどうぞ。
- getter/setterがよくない理由
- デメテルの法則と尋ねるな命じよ(Tell, Don't Ask!)
- 関心の分離 : Separation of Concerns (SoC)
- 依存関係とその方向
- 良いDRYと悪いDRY
- 名前重要
getter/setterがよくない理由
ビジネスロジックが絡むところでのgetter
ここで言うところの
getterとは外部から参照できるインスタンス変数のことを指します。「ビジネスロジックが絡むところで」と限定しているのはフレームワークの制約などで一概には対応できない場合があるためです。getterを使っているクラスpublic class Employee { private String name; // 外部から参照できないのでOK private LocalDate contractDate; public int salary; // publicなインスタンス変数は外部から参照できるので禁止 ... // getterで外部からインスタンス変数を参照できるので禁止 public LocalDate getContractDate() { return this.contractDate; } }この例の
getContractDate「従業員の契約日を取得する」という処理には業務的な意味を何も持っていません。
従業員の契約日を取得して何がしたいのかが分からないからです。ここで実現したことは、契約日を使った従業員の判別なのか? 契約日による従業員情報の変更なのか?? 等など、これだけでは意味のないものになってしまっています。これは取得したデータの用途は利用側に丸投げしていることと同義であり、設計の先送りです。では、「設計の先送り」が発生しているクラスを利用する側を見てみます。
「設計の先送り」が発生しているクラスを利用する側int salary; if (employee.getContractDate().isBefore(LocalDate.now())) { // 契約済み従業員の給料を取得する (当日を含んでないですが細かいことはあれで...) salary = employee.salary; }上記のように
employeeを使いたいクラスにビジネスロジックが漏れ出してしまいます。今後も同じ用にemployeeを使いたいクラスが出てくると、同じようなコピペコードが至るところに量産されてしまいます。
それを防ぐためにも以下のようにデータを持つクラスに直接ビジネスロジックを書きます。getterを禁止したクラスpublic class Employee { private LocalDate contractDate; private int salary; ... public int calculateSalary() { if (this.contractDate.isBefore(LocalDate.now())) { return this.salary; } return 0; // この返し方はイマイチですが、例ということで... } }利用側のクラスint salary = employee.calculateSalary();
getterをやめて、データを保持しているクラスにビジネスロジックを書くようにすることで、コピペコードの量産を防ぎ、該当クラスを見ただけでビジネスルールを知ることができるようになります。ビジネスロジックが絡むところでのsetter
ここで言うところの
setterも同様に外部から値を変更できるインスタンス変数のことを指します。setterを使っているクラスpublic class Employee { public LocalDate contractDate; // publicなインスタンス変数は外部から代入できるので禁止 private int salary; ... // setterで外部からインスタンス変数を代入できるので禁止 public void setSalary(int salary) { this.salary = salary; } }こちらでも
setSalary「従業員の給与を変更する」という業務的な意味を持たないロジックになっています。例えば「業績が良かったので上乗せする」や「入力を間違えたので訂正する」など業務上のフローやルールを表現したメソッドにするべきです。また
setterによって値が変更されるおそれがあるとということは状態の変更が発生するということなので、安全ではなくなります。特に以下のように処理が深くなるに従って、簡単に人が認識できる状態を超えてくるので、可読性が落ちます。methodA(Employee employee) └ methodB(Employee employee) └ methodC(Employee employee) └ methodD(Employee employee) <- employeeの状態をここで書き換えられたらもう無理!追いきれない! └ methodE(Employee employee)完全コンストラクタとイミュータブルオブジェクトでsetterを避ける
完全コンストラクタ : Complete Constructor
完全コンストラクタとは「コンストラクタで全てのインスタンス変数の値を確定させる」ということです。
特にビジネスルールを扱うクラスではコンストラクタで以下の状態を作り出すことを保証します。
- すべてのインスタンス変数の値が決まる
- ビジネスルールとして矛盾がない
- インスタンス変数の値は範囲やビジネスルールに基づく計算可能な有効値である
簡単な例として「従業員は必ず契約しており(契約日があり)、また入社予定として3ヶ月先までの契約ができる」というビジネスルールを表現するクラスを定義します。
public class Employee { private LocalDate contractDate; public Employee(LocalDate contractDate) { if (contractDate == null) { throw new IllegalArgumentException("契約日は必須です"); } LocalDate currentDate = LocalDate.now(); if (contractDate.isAfter(currentDate.plusMonths(3))) { throw new IllegalArgumentException("3ヶ月先を超える契約日は不正です"); } this.contractDate = contractDate; } }このように従業員の契約日は必ず必要、かつ3ヶ月先を超える日付は制限されるようなコンストラクタの設計をすることで、このインスタンスを使う際は強制的にビジネスルール上の制約が守られた、安全なインスタンスであることが保証されます。
また、どのような値が来ても安全で予測可能なインスタンスになり、利用側は余計なことを気にすることなく使用できます。一度インスタンスを生成した後で、状態を変更したり、一部のフィールドを徐々に組み立てていくなどの操作は、安全性や可読性を下げます。インスタンス生成は常にアトミック操作で完結するべきです。
コンストラクタの代わりのstaticファクトリメソッド
コンストラクタに関連して、staticファクトリメソッドを紹介します。
ビジネスルール的にオーバーロードしたコンストラクタを複数作りたいときがあると思います。以下の例のコメントに書いてあるように「○○の場合はこのコンストラクタ」「△△の場合はこのコンストラクタ」という感じにです。public class SearchDateTime { private LocalDate date; private LocalTime time; // 当日以外の場合はこのコンストラクタを使ってほしい... public SearchDateTime(LocalDate date, LocalTime time) { this.date = date; this.time = time; } // 当日の場合のみコンストラクタを使ってほしい... public SearchDateTime() { this.date = LocalDate.now(); this.time = LocalTime.now(); // 関係ないけどコンストラクタチェーンしよう! // this(LocalDate.now(), LocalTime.now()); } }上記の用にコンストラクタが複数あった場合、コメントには書いてありますが、利用側は引数の違いだけでどちらを使えばよいのか、わかりにくいものになっています。
これを解決するのが表題の「コンストラクタの代わりのstaticファクトリメソッド」です。
以下のように利用用途ごとに命名した staticなファクトリメソッドを用意します。public class SearchDateTime { private LocalDate date; private LocalTime time; public static SearchDateTime of(LocalDate date, LocalTime time) { return new SearchDateTime(date, time); } public static SearchDateTime ofToday() { return new SearchDateTime(LocalDate.now(), LocalTime.now()); } // コンストラクタは private に可視性を下げて、外部には公開しない private SearchDateTime(LocalDate date, LocalTime time) { this.date = date; this.time = time; } }当初のコンストラクタは
privateに可視性を下げて、外部から直接利用できないように制限します。
staticなファクトリメソッドの利点は 命名できることにあります。名前を明示的にすることで、曖昧なものにせず利用側に意図を伝えることができます。
また他のユースケース例として、管理者ロールと通常ロールで操作させるオブジェクトを制限させたりなど、誤ってはいけない操作を命名と引数の型によって明示させたりするなども有効だと思います。イミュータブルオブジェクト
一度決まった値から変更されないことを保証することで、安全性を高めることができます。
インスタンス変数にfinal修飾子を定義することで、コンストラクタでの初期化後に値が変更されることがなくなります。public class Employee { // すべてのインスタンス変数にfinal修飾子をつける private final String name; private final LocalDate contractDate; private final int salary; public Employee(String name, LocalDate contractDate, int salary) { this.name = name; this.contractDate = contractDate; this.salary = salary; } public void addSalary(int salary) { this.salary += salary // コンパイルエラー!! } }setterをやめて、完全コンストラクタかつイミュータブルなオブジェクトにするのも、一番の目的は状態変更を気にしないでもよくするためです。
人が一時的に覚えておける情報量は少ないので、なるべく気にすることを減らすことが大事です。これで値を不変にしたイミュータブルオブジェクトができましたが、値の変更を行いたい場合はどうするかというと、新たなインスタンスを生成して返します。あくまでも自分自身の値を変更しません。
public class Employee { private final String name; private final LocalDate contractDate; private final int salary; ... // 再契約して契約日と給与を変更する public Employee contractRenewal(int salary) { return new Employee(this.name, LocalDate.now(), salary); } } // 利用側 Employee employee = new Employee(...); Employee extendedEmployee = employee.contractRenewal(200000); // 別インスタンスとして扱います。デメテルの法則と尋ねるな命じよ(Tell, Don't Ask!)
「デメテルの法則」と「尋ねるな命じよ」は両方とも情報隠蔽に関連する設計原則です。
はじめにこの設計原則が守られていない例と守られている例のイメージを見てみます。設計原則が守れてない例
- Serviceが問い合わせしている
- Serviceが保有しているオブジェクト以外 (ModelB, ModelC) に参照している
設計原則が守れている例
- 命じている
- ServiceはModelBとModelCを参照していない
この設計原則を守るときれいなV字になることがわかります。
次にコードで見てみます。
簡単な例として「クーポン料金を持つ商品の販売価格を求める」という場合のロジックを表しています。デメテルの法則と尋ねるな命じよが守られてない利用側ロジックProduct product = new Product(something); int sellingPrice; if (product.price.couponExpiration.isAfore(LocalDate.now())) { // クーポンの有効期限が有効なら商品のクーポン価格を返す (こちらも当日を含んでないですが細かいことはあれで...) sellingPrice = product.price.couponValue; } else { // クーポンの有効期限が無効なら製品価格を返す sellingPrice = product.price.value; }上記では、クーポンの有効期限を判定するため
product.price.couponExpirationでif文による問い合わせが発生しています。さらにproductが保有するpriceにもアクセスしています。このコードに対して、「デメテルの法則」と「尋ねるな命じよ」を適用して改良されたコードは以下のようになります。
デメテルの法則と尋ねるな命じよが守られた利用側ロジックProduct product = new Product(something); int sellingPrice = product.sellingPrice();上記の「デメテルの法則と尋ねるな命じよが守られた利用側ロジック」では、1行の命令で完結しています。利用側は必要最小限の知識しか与えられておらず、ただ「販売価格を求める」という命令を呼び出すだけで済みます。
この「デメテルの法則と尋ねるな命じよ」が組み込まれた
Productクラスは以下のような形になります。public class Product { private Price price; ... int sellingPrice() { return price.sellingPrice(); } } class Price { private int value; private int couponValue; private LocalDate couponExpiration; ... int sellingPrice() { if expirationDate.isAfore(LocalDate.now()) { return couponValue; } return value; } }元々、利用側にあった分岐等の判断処理は
Priceクラスが担当し、Productクラス内でコンポジションされています。計算や判定などのビジネスロジックはデータを持つクラスが担当するということです。
さらにPriceクラスはパッケージプライベートで定義され、sellingPriceメソッドも同様にパッケージプライベートです。つまり利用側(別パッケージと前提)からはこのクラスやメソッドが見えず、直接実行することが許可されておりません。こうすることで強制的に知識の露出を減らし依存度を下げ疎結合にすることができます。関心の分離 : Separation of Concerns (SoC)
関心の分離といえば、MVCやレイヤードアーキテクチャなどで語られることが多いと思いますが、どのような粒度にせよ意識したい原則です。
例えば「従業員」を扱うクラスがあるとします。
この「従業員」クラスにおいては、仮に業務エラーが発生した場合、「どう通知するか」については、関心事ではありません。
APIであればJSONのような形式にメッセージを詰めるべきでしょうし、WEBサイトのように画面を表示するものであれば、ユーザが認識しやすいように修飾して出力すべきでしょう。
この「従業員」クラスにおいては、「業務エラーが発生したこと」と「業務エラーの内容(エラーメッセージ)」までが知っているべき関心事になります。それをどのように出力して、どのように修飾するかは、別のクラス(例えばPresentation層)の関心事です。
上記、関心の分離を意識せずに「従業員」クラスに利用側の画面の処理が書かれていると、画面の出力方式に変更があった場合、本来関係ないはずの「従業員」クラスにも修正が発生し、影響範囲が広がります。SOLID原則の一つである、単一責任の原則にも反することになります。
https://qiita.com/UWControl/items/98671f53120ae47ff93aこのように役割に応じて関心を分離し、どこまでの責任を持つかの境界線を決め、線引することで、外部との依存度が少なくなり、疎結合で安全なアプリケーションを設計することが可能です。知識の露出を減らすことにも繋がります。
依存関係とその方向
TL;DR
ちょっと長いので。
- 循環依存が生じないようにする
- より安定したコンポーネントに依存するようにする
- 依存先が少ないほど安定する、または自身が依存されてるほど安定する
- 不安定なコンポーネントが依存されていると変更影響が大きくなるので、 安定したコンポーネントからは依存してはいけない
- それらを解決するために、依存方向を逆転させる
コンポーネントの関連を扱う原則
「コンポーネント」とはモジュールと言ったり、jarやgemの単位だったりするものを指します。
ただコンポーネントに限らず、あらゆるオブジェクトにも適用すべき内容になります。そのためここではクラスを基準に説明します。非循環依存関係の原則 : Acyclic Dependencies Principle (ADP)
コンポーネントの依存グラフに循環依存があってはならない
という原則があります。
「コンポーネント」に限らず、あらゆる構造において、循環依存が生じないように設計すべきです。
依存は必ず内側に向かって、循環させずに一方向のみに依存するようにします。内側から外側を知ることはなく、外側が持つ知識やルールを内側に持ち込まないようにすることが大事です。安定依存の原則 : Stable Dependencies Principle (SDP)
安定度の高い方向に依存する
クラスで考えると、依存しているクラスが少ないほど安定します。また自身のクラスが依存されてるほど安定しています。
特に後者(自身のクラスが依存されてるほど安定)の理由に関しては、変更の影響が大きく変更しづらいため、安定せざるを得ないというためです。
(言語よりますが、importやrequire、includeの数は一定の基準になると思います。)そのため、変更を想定したクラスは、変更しづらいクラスからは依存されてはいけません。
安定度・抽象度等価の原則 : Stable Abstractions Principle (SAP)
コンポーネントの抽象度は、その安定度と同程度でなければいけない
安定度の高いコンポーネントは抽象度も高くあるべきで、安定度の高さが拡張の妨げになってはいけない、という原則です。
逆に言うと、安定度の低いコンポーネントはより具体的であるべき、ということになります。安定度が低いことによってその内部の具体的なコードが変更しやすくなるためだからです。「安定依存の原則(SDP)」では、安定度の高い方向に依存すべきで、「安定度・抽象度等価の原則(SAP)」では、安定度は抽象度に比例すべきということなので、つまりは 抽象度が高くなる方向に依存すべき ということになります。
依存関係逆転の原則 : Dependency Inversion Principle (DIP)
「安定依存の原則(SDP)」や「安定度・抽象度等価の原則(SAP)」を実現するために依存関係の方向を変更する必要があります。その方法としてインターフェースを導入することで、依存関係の方向を逆転させ、これを実現します。
DIP適用前を見てみましょう。
上記の「変更前」では「安定したクラス」が「修正が多い不安定なクラス」に依存しているため、後者のクラスに変更があった場合、前者のクラスにその影響を与えてします。
具体的には以下のようの例です。
- 「修正が多い不安定なクラス」が外部に公開しているメソッドの戻り値を変更すると「安定したクラス」も修正しなければならない
- 「修正が多い不安定なクラス」がAPIコールしている場合、別のAPIに交換すると「安定したクラス」も修正しなければならない
このような問題に対して、依存関係を逆転させて問題を解消した状態がこちらです。
インターフェースを用いて抽象に依存させたことによって、「修正が多い不安定なクラス」は直接依存されなくなっています。「修正が多い不安定なクラス」をどんなに修正しても、インターフェースを継承している限り「安定したクラス」の修正は不要です。
ここで重要なことは、「安定依存の原則(SDP)」や「安定度・抽象度等価の原則(SAP)」にも書いてありますが、依存される抽象クラスは極めて安定していなければいけません。なので、この部分の設計が重要になります。
依存関係逆転の原則(DIP)を実際に適用する場合について
個人的にはこの依存関係逆転の原則(DIP)を何でもかんでも適用する必要は無いかなと考えています。
特に現状シンプルな構成だった場合、1階層抽象的なオブジェクトが入ることでかえってわかりづらさを生む場合があるからです。実際の具体例として、依存方向を逆転させる場面としては「このクラスを依存したくないな」と思ってしまうような、以下の場合が多いかなと思います。
- 頻繁な仕様変更、またはバクが多く安定していないクラスがあり、その影響を受けている別のクラスがある
- フレームワークが提供する機能など外部ライブラリを使用している
- 頻繁に仕様が変わったり、または外部など管理外にあるAPIアクセス
これはプロジェクトとして「どこまでフレームワークにロックされてもよいか」など事前にある程度方向性を決めておくべきでしょう。
良いDRYと悪いDRY
一般的にソースコードにおける「重複は悪」とされています。「Don't Repeat Yourself」DRY原則と言われるやつです。
例えば、画面の構成が同じような複数のユースケースがあるとします。作った当初は同じ処理をDRYにするために共通化するとします。
しかし、その時点では問題なかったコードですが、それぞれのユースケースの要求が異なるため、共通ロジックに利用側それぞれの個別の意図を持った場合分けのif分岐が入り、数年後には判断分岐だらけの肥大化した共通だったロジックに変わることがあります。これを避けるためには、ビジネスが関心を持つ領域は何なのかという、プロダクトが扱う問題領域(ドメイン)を明確にする必要があります。
その上で 偶然に重複しているのか、本当に重複しているのかを慎重に見極める必要があります。前者の場合は偶然に重複しているだけなので、DRYにする必要性はありません。これはプロダクトが大規模になればなるほど発生しやすい問題です。特にビジネスルールを扱うクラスに
commonやbaseなどという名前が入っていたりするのは要注意です。名前重要
なんだかんだでプログラミングする上で最重要事項です。
名前に関する内容はさんざん他の記事でも語られているので、概要に留めますが、以下の書籍は大変参考になるかと思います。
- リーダブルコード : 基本中の基本を分かりやすく体系立てて言語化してくれています
- CODE COMPLETE : 命名について10項目以上を割いて、これほど深堀りした書籍は他にないです(多分)
その上で「ビジネスルールを扱う部分」の命名について、まとめます。
- パッケージ名/モジュール名がドメインが扱う問題領域の名前と一致している
- クラス名が問題領域で扱う関心事と一致している
- メソッド名が実現したい振る舞い、知りたい知識になっている
- 汎用的、曖昧、幅広い意味を持つ名前になっていない
DDDでは「戦略的設計」と「戦術的設計」というのがあります。
- 戦略的設計 : コアドメイン、境界づけられたコンテキスト、コンテキストマップ
- 戦術的設計 : 実際にビジネスルールを実現するための設計パターンやアーキテクチャデザイン
重要なのは「戦略的設計が定まっていないと適切な名前が決められない」ということです。
DRYの項目でも記載しましたので再掲になりますが、自分たちのビジネスが関心を持つ領域は何なのか、更にその領域内で分割できるものはあるのか、などプロダクトが扱う問題領域(ドメイン)を明確にして、それに従ったパッケージ名やクラス名を決めていくという作業が良い命名を行うためには必要です。最後に
これまで述べてきた手法やパターンは適用した1回で終わりにせず、しっくりこなければ何度でも見返すことが大事です。
その時は良いコードを書いたつもりでも後日に見返すとイマイチなコードになってたりするというのは、あるあるだと思います。ただ、このような設計を意識してきたコードでは以前に比べ修正コストが格段に下がり、変更が容易になっているはずです。常に改善を実施し、小さなリファクタリングを繰り返すことが大事です。
- 投稿日:2019-12-19T08:09:35+09:00
Javaで作る自動販売機のサンプル(クラス分け)
前回書いた自動販売機:https://qiita.com/TakumiKondo/items/e93aa60031e2fe09d969
をクラス分けしました。Items.javapackage vm; import java.util.HashMap; import java.util.Map; public class Items { private Map<String, Integer> items; // 商品リスト private Map<String, Integer> availablePurchases; // 購入可能商品 // コンストラクタ(商品リストの初期化) Items() { this.items = new HashMap<String, Integer>(); items.put("コーラ", 100); items.put("オレンジジュース", 120); items.put("水", 80); } // 最低購入金額の取得 int minPrice() { int minPrice = 0; int loopCount = 0; for (String itemKey : items.keySet()) { minPrice = validMinPrice(loopCount, minPrice, itemPrice(itemKey)); loopCount++; } return minPrice; } // 最低購入金額の判定 int validMinPrice(int loopCount, int minPrice, int price) { if (loopCount == 0 || minPrice > price) { minPrice = price; } return minPrice; } // 商品金額 private int itemPrice(String itemKey) { return items.get(itemKey); } // 購入可能商品金額 private int availablePurchasePrice(String itemKey) { return availablePurchases.get(itemKey); } // 購入可能商品リストの取得 void createAvailablePurchases(int deposit) { availablePurchases = new HashMap<String, Integer>(); for (String itemKey : items.keySet()) { addAvailablePurchase(deposit, itemKey); } } // 購入可能商品リストの追加 private void addAvailablePurchase(int deposit, String itemKey) { if (deposit >= items.get(itemKey)) { availablePurchases.put(itemKey, items.get(itemKey)); } } // 購入可能商品の表示 void showAvailablePurchase() { for (String itemKey : availablePurchases.keySet()) { System.out.println(itemKey + ":" + items.get(itemKey) + "円"); } } // 購入可能商品であるかの判定 boolean isAvailablePurchase(String itemName, Deposit deposit) { return availablePurchases.containsKey(itemName); } // 販売価格 int saleAmount(String itemName) { return availablePurchasePrice(itemName); } }Deposit.javapackage vm; public class Deposit { private int deposit; // 入金額 // コンストラクタ(初期値0円) Deposit() { deposit = 0; } // 入金 void deposit(int amount) { deposit = deposit + amount; } // 現在入金額 int amount() { return this.deposit; } // 課金 void charge(int amount) { int charge = this.deposit - amount; this.deposit = charge; } }VendingMachine.javapackage vm; public class VendingMachine { private Items items; // 商品 private Deposit deposit; // 入金額 // コンストラクタ VendingMachine() { items = new Items(); deposit = new Deposit(); } // 入金 void deposit(int amount) { deposit.deposit(amount); } // 最低価格商品以上の入金があるか判定 boolean hasMinDeposit() { if (deposit.amount() < items.minPrice()) { return false; } return true; } // 購入可能商品を表示 void showAvailablePurchases() { items.createAvailablePurchases(deposit.amount()); items.showAvailablePurchase(); } // 購入可能商品であるか判定 boolean isAvailablePurchases(String itemName) { return items.isAvailablePurchase(itemName, deposit); } // 購入商品を表示 void selectItem(String itemName) { System.out.println(itemName + "です!"); } // 課金 void charge(String itemName) { int saleAmount = items.saleAmount(itemName); deposit.charge(saleAmount); System.out.println("おつりは、" + deposit.amount() + "円です。"); } }Main.javapackage vm; import java.util.Scanner; /** * 自動販売機(Level6) 自動販売機クラスの作成 */ public class Main { // メインロジック public static void main(String[] args) { // ①商品の初期化 VendingMachine vendingMachine = new VendingMachine(); // 購入最低金額の場合、追加入金 Scanner scanner = new Scanner(System.in); do { // ②入金 System.out.println("お金を入れて下さい。"); int amount = scanner.nextInt(); vendingMachine.deposit(amount); // ③金額チェック(最低購入金額) } while (!vendingMachine.hasMinDeposit()); // ④購入可能商品を表示 System.out.println(""); System.out.println("購入可能な商品です。"); vendingMachine.showAvailablePurchases(); // ⑤商品を選択 String itemName; do { System.out.println(""); System.out.println("商品名を入力して下さい。"); itemName = scanner.next(); } while (!vendingMachine.isAvailablePurchases(itemName)); vendingMachine.selectItem(itemName); scanner.close(); // ⑥課金機能 vendingMachine.charge(itemName); } }
- 投稿日:2019-12-19T04:36:18+09:00
Rotate Array
Rotate Array
説明
整数型の配列と整数N(回転数)の二つの引数が渡されたとき、配列の要素をN回(マイナスは左へ、プラスは右へ)
回転させた配列にしよう。例
以下の配列が渡されたとしよう。
もし、回転数Nが−1の時は、全ての要素が左側へ一つずつずれる。
また、もし回転数Nが2の場合は、全ての要素が右側へ一つずつ二回ずれる。
Try It Yourself (Brute Force)
この問題でまず私が最初に思い浮かんだ、力任せアルゴリズムは回転後に
必ず二つの左側、右側の部分配列が現れるので、その二つの配列を作る。
(例のN=2のケースでは、left_sub_array = {9 , 40}で、right_sub_array = {1, 10, 20, 0, 59, 86, 32, 11})
そして、左側と右側の部分配列に保存してある回転後の要素を元の配列に順番に更新していく。
もし、Nがマイナスの時は左回転を右回転のアルゴリズムに適用できる様にする。実行時間(Runtime Complexity) O(n)
空間計算量(Memory Complexity) O(n)
実装
TEST
OUTPUT
見直し
要素の順序を入れ替えていくには、全ての要素の位置を並び替える必要があるため、実行時間O(n)が限界である。
ただ、空間計算量はO(1)にすることができる。つまりは渡されたデータ構造のみを使う。最適化: Memory Complexity O(n) -> O(1)
- 元の配列の要素を反転します。
- 0からN-1までの要素を反転します。
- NからLength-1までの要素を反転します
例
先の例と同じ配列が渡されたとする。また、回転数はN=2とする。
1. まず、元の配列の要素全てを反転させる。
2. それから、index 0 から N-1(2 - 1 = 1)までの要素を反転させる。
3. 最後にN(2)から配列の末尾までの要素を反転させる。
これで、空間計算量が定数O(1)で最適化できる。実装
OUTPUT (TESTのコードは同じ)
- 投稿日:2019-12-19T02:47:13+09:00
AspectJが書き換えたコードを無理やり覗いてみた
この記事は、富士通システムズウェブテクノロジー Advent Calendarの19日目の記事です。
(お約束)本記事の掲載内容は私自身の見解であり、所属する組織を代表するものではありません。はじめに
この記事は、JavaでAOPを実現するOSSであるAspectJが、どんなコードを生成しているのかを目視確認してみよう!というものです。
AOP自体は昔から使われている技術ですが、個人的にはあまりプロジェクトで使われている印象がありませんでした。が、最近になって自分の周りでAOPを使っているプロジェクトをちらほら見かけるようになり、今後使う人が増えるかも?と思い記事を書いてみました。
AOPは使ってみると便利なのですが、正直裏で何が起こっているのかよくわからない部分があります。
こういうところの理解を深めるために、本記事ではAOPでウィービングした際のコードを覗いてみます。前提知識
AOPってなんぞや!
みたいな話は既にいろんな記事でまとめられているので割愛します。
以下の記事はすごいまとまってるのでおすすめ。時代が来るかも?アスペクト指向プログラミング, AspectJ言語とは何か
まずは、本記事を読むにあたって必要な用語だけ簡単に説明します。
ウィービング
簡単に言うと、コンパイルして生成したクラスファイルを後から書き換えて、任意の処理を埋め込むこと。
例えば、全くログが実装されてないプログラムに、元のソースを書き換えずにデバッグログを埋め込むような芸当ができます。
そう、AOPならね。ちなみに、ウィービングには以下の2種類があります。今回は静的ウィービングを使います。
- 静的ウィービング:Javaのクラスファイルに直接処理を織り込む方法
- 動的ウィービング:JVMにクラスがロードされた際、動的に処理を織り込む方法
ポイントカット
ウィービングを実施する際、処理を埋め込むメソッドなどをポイントカットとして定義します。
ポイントカットには様々な種類があり、それぞれがプログラム実行上の様々なタイミングを表しています。
代表的なもの:
- execution:特定のメソッドが実行されたとき
- call:特定のメソッドを呼び出したとき
- handle:特定の例外がハンドルされたとき
アドバイス
ポイントカットで特定したメソッドのどこに処理を埋め込むかを定義するのがアドバイスです。
こちらも埋め込む場所に応じていくつかの種類が用意されています。
代表的なもの:
- before:前処理
- after:後処理
- after returning:後処理(正常時のみ)
- after throwing:後処理(異常時のみ)
ウィービング結果を覗いてみる
用語説明はこれくらいにして、ウィービングによって処理がどう書き換えられるのかを見てみます。
今回はアドバイスはbefore, afterとし、よく使うポイントカット3種でウィービングしました。使ったもの
- Eclipse
- AdoptOpenJDK 11.0.5.10 hotspot
- aspectj-1.9.5:AspectJのライブラリやコンパイラなど
- jd-gui-windows-1.6.5:Javaのデコンパイラ
サンプルコード
ウィービング対象のプログラム
AspectPointcutSampleMain.javapublic class AspectjPointcutSampleMain { public static void main(String[] args) { new AspectjPointcutSampleMain().doProcess(); } private void doProcess() { Sample sample = new Sample(); sample.outputMessage(); sample.getString(); try { sample.throwException(); } catch (Exception e) { e.printStackTrace(); } } public static class Sample { private String sampleStr; public void outputMessage() { System.out.println("this is sample."); } public String getString() { return this.sampleStr; } public void throwException() throws Exception { throw new Exception(); } } }
ウィービングの定義
SampleAspect.java@Aspect public class SampleAspect { @Before("execution(public * *..*.Sample.*(..))") public void beforeExecutionPointcut() throws Throwable { System.out.println("execution before process."); } @After("execution(public * *..*.Sample.*(..))") public void afterExecutionPointcut() throws Throwable { System.out.println("execution after process."); } }SampleAspect.javaにウィービングのための定義を記述しました。
各メソッドのアノテーションでアドバイス、ポイントカットを指定します。
今回はSampleクラスのpublicメソッド全てに処理を埋め込むよう定義しています。ウィービング結果
execution
ウィービング定義
@Before("execution(public * *..*.Sample.*(..))") public void beforeExecutionPointcut() throws Throwable { System.out.println("execution before process."); } @After("execution(public * *..*.Sample.*(..))") public void afterExecutionPointcut() throws Throwable { System.out.println("execution after process."); }
デコンパイル結果
public void outputMessage() { try { SampleAspect.aspectOf().beforeExecutionPointcut(); System.out.println("this is sample."); } catch (Throwable throwable) { SampleAspect.aspectOf().afterExecutionPointcut(); throw throwable; } SampleAspect.aspectOf().afterExecutionPointcut(); }長かったので抜粋しました。
元のソースと見比べると、対象に指定したメソッド内に、以下の処理が新たに追加されていることがわかります。
- 前処理でのbeforeExecutionPointcutメソッド呼び出し
- 後処理でのafterExecutionPointcutメソッド呼び出し
- 処理全体がtry-catchで括られている
- 追加されたcatch句内でのafterExecutionPointcutメソッド呼び出し
call
ウィービング定義
@Before("call(public * *..*.Sample.*(..))") public void beforeCallPointcut() throws Throwable { System.out.println("call before process."); } @After("call(public * *..*.Sample.*(..))") public void afterCallPointcut() throws Throwable { System.out.println("call after process."); }
デコンパイル結果
private void doProcess() { Sample sample = new Sample(); try { SampleAspect.aspectOf().beforeCallPointcut(); sample.outputMessage(); } catch (Throwable throwable) { SampleAspect.aspectOf().afterCallPointcut(); throw throwable; } SampleAspect.aspectOf().afterCallPointcut(); ~略~ }デコンパイル結果を見ると、executionでは対象のメソッド内に前処理・後処理が埋め込まれていたのに対し、メソッドの呼び出し元に処理が埋め込まれていることがわかります。
callは「特定のメソッドを呼び出したとき」を表すポイントカットなので、メソッドの呼び出し元がウィービングの対象となります。handler
ウィービング定義
@Before("handler(Exception)") public void beforeHandlerPointcut() throws Throwable { System.out.println("handler before process."); }
デコンパイル結果
private void doProcess() { Sample sample = new Sample(); sample.outputMessage(); sample.getString(); try { sample.throwException(); } catch (Exception exception) { SampleAspect.aspectOf().beforeHandlerPointcut(); Exception e = exception; e.printStackTrace(); } }handlerの場合はcatch句の中に処理が埋め込まれるようです。
おわりに
簡単ではありますがデコンパイル結果を掲載してみました。
今回、改めてAOPについて簡単に調べなおしたのですが、思った以上にいろんなことができるので改めて関心しました。
本記事には掲載しきれていない機能も多々ありますので、興味があれば調べてみてもらえると面白いかもです。
- 投稿日:2019-12-19T00:25:09+09:00
Java側からManagedBeanを取得する[JSF]
目的
JSFを使っているが、ManagedBeanをJava側から取得したい。
? 「普通にインスタンス生成すりゃええんじゃ?」
じゃなくて ManagedBeanとして取得したいんじゃ!
というときの解決法
解決
とりあえず以下にBeanの例を書きます。
中身はそれっぽく適当に書いています。HogeBean.java/* ↑省略 */ @Named("hoge") @ManagedBean @ViewScope class HogeBean implements Serializable { private static final long serialVersionUID = 1L; private String fuga; @PostConstruct public void init() { fuga = "アイス"; } public String getFuga() { return fuga; } }で、取得する方法の例はこちら。
ELContext elContext = FacesContext.getCurrentInstance().getELContext(); HogeBean bean = (HogeBean) elContext.getELResolver().getValue(elContext, null, "hoge");例
都合により、ManagedBeanを経由してManagedBeanを取得してテキスト出力すると仮定
BeanBean.java/* ↑省略 */ @Named("beeen") @ManagedBean @ViewScope class BeanBean implements Serializable { private static final long serialVersionUID = 1L; public String getHogeBean() { ELContext elContext = FacesContext.getCurrentInstance().getELContext(); HogeBean bean = (HogeBean) elContext.getELResolver().getValue(elContext, null, "hoge"); } }view.xhtml<!-- 省略 --> <h:outputText value="#{beeen.hogeBean.fuga}"/>出力結果: アイス
おわりに
Beanの振り分けを行うBean(用途として適切かは別として...)とかには使えそうですが、
多用するようになると設計を見直したほうがいいですね。
















