- 投稿日:2020-09-09T18:34:29+09:00
【Elasticsearch×Java】Javaで取得されたクエリの実行結果が想定と異なったので調査→対応した内容まとめ
JavaでElasticsearchを用いた検索を行った際、想定通りの結果が得られなかったので、調査→対応を行いました。
その内容をまとめておきます。
※対応内容と結論だけ知りたい方は、対応内容、結論のみお読みくださいませ!環境
- OS: Windows10
- Java: 11(Amazon Corretto 11)
- IDE: Eclipse 2020-03
- Elasticsearch: 7.4.2
- Kibana: 7.4.2
調査
ソース
- インデックス定義
- Javaソースコード
- Kibanaでの実行クエリ(Javaソースコードと同等の内容) はそれぞれ以下の通り。
インデックス定義では、以下の通り、mappingsの定義に加え、analyzerの設定も行っています。
フィールド名のポジショニングは以下の通り。
フィールド名 データタイプ ポジショニング itemId integer 商品ID(主キーに当たる) itemName text 商品名 itemNameKana text 商品名(カタカナ) itemNameHira text 商品名(ひらがな) インデックス定義{ "settings": { "analysis": { "filter": { "my_ngram": { "type": "ngram", "min_gram": 1, "max_gram": 2 } }, "analyzer": { "my_kuromoji_analyzer": { "type": "custom", "tokenizer": "kuromoji_tokenizer", "char_filter": [ "icu_normalizer", "kuromoji_iteration_mark" ], "filter": [ "kuromoji_stemmer", "my_ngram" ] } } } }, "mappings": { "properties": { "itemId": { "type": "integer" }, "itemName": { "type": "text", "analyzer": "my_kuromoji_analyzer" }, "itemNameKana": { "type": "text", "analyzer": "my_kuromoji_analyzer" }, "itemNameHira": { "type": "text", "analyzer": "my_kuromoji_analyzer" } } } }Java側の処理としては、入力されたされた検索ワードが商品名、商品名(カタカナ)、商品名(ひらがな)のフィールドのいずれかにマッチするものがないか検索し、スコアの高い順にソートする内容となっています。
Javaソースコード/** * 商品検索 * * @param keyword 検索ワード * @param index インデックス名 * @param limit 件数 * @param client Elasticsearch接続クライアント * @return 検索結果 * @throws IOException */ public SearchResponse search(String keyword, String index, int limit, RestHighLevelClient client) throws IOException{ // 検索条件の初期化 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder(); boolQueryBuilder.should(QueryBuilders.matchQuery("itemName", keyword)) .should(QueryBuilders.matchQuery("itemNameKana", keyword)) .should(QueryBuilders.matchQuery("itemNameHira", keyword)); searchSourceBuilder.query(boolQueryBuilder); // ソート順の設定(スコアでソート) searchSourceBuilder.sort(new FieldSortBuilder("_score").order(SortOrder.DESC)); // 返却件数を設定 searchBuilder.size(limit); SearchRequest request = new SearchRequest(index).source(searchSourceBuilder); return client.search(request, RequestOptions.DEFAULT); }最後にKibanaでの実行クエリ。
「検索ワード」を入力となっている箇所に検索したいワードを入力して検索します。
また、size(Javaのソースコードでいうところのlimit)は5を指定。Kibanaでの実行クエリPOST item_list/_search { "from": 0, "size": 5, "sort": { "_score": { "order": "desc" } }, "query": { "bool": { "should": [ { "match": { "itemName": "検索ワードを入力" } }, { "match": { "itemNameKana": "検索ワードを入力" } }, { "match": { "itemNameHira": "検索ワードを入力" } } ] } } }候補要因
候補要因として考えられたのは以下の通り。
- 1. Javaで発行されたクエリの内容が間違っている
- 2. analyzerの設定がおかしい
- 3. 暗黙的に設定された内容を修正する必要がある
これらを順に調査していくことにしました。
調査実施
1. Javaで発行されたクエリの内容が間違っている
まず、Javaで発行されたクエリの内容が間違っているのでは?という観点から。
確認方法としては、Javaソースコードの
SearchRequest request = new SearchRequest(index).source(searchBuilder);
の1行うしろの文にブレークポイントをはり、searchSourceBuilderの中身を見るというもの。内容は以下の通りとなっていました。
searchBuilderの中身{"size"5,"query":{"bool":{"should":[{"match":{"itemName":{"query":"検索ワードを入力","operator":"OR","prefix_length":0,"max_expansions":50,"fuzzy_transpositions":true,"lenient":false,"zero_terms_query":"NONE","auto_generate_synonyms_phrase_query":true,"boost":1.0}}},{"match":{"itemNameKana":{"query":"検索ワードを入力","operator":"OR","prefix_length":0,"max_expansions":50,"fuzzy_transpositions":true,"lenient":false,"zero_terms_query":"NONE","auto_generate_synonyms_phrase_query":true,"boost":1.0}}},{"match":{"itemNameHira":{"query":"検索ワードを入力","operator":"OR","prefix_length":0,"max_expansions":50,"fuzzy_transpositions":true,"lenient":false,"zero_terms_query":"NONE","auto_generate_synonyms_phrase_query":true,"boost":1.0}}}],"adjust_pure_negative":true,"boost":1.0}},"sort":[{"_score":{"order":"desc"}}]}パッと見、問題なさそう。
この内容をKibanaで実行すると、「Kibanaでの実行クエリ」をたたいた場合と全く同じ結果になりました。
実行したクエリは以下の通り。searchSourceBuilderの内容で実行したクエリPOST item_list/_search {"size"5,"query":{"bool":{"should":[{"match":{"itemName":{"query":"検索ワードを入力","operator":"OR","prefix_length":0,"max_expansions":50,"fuzzy_transpositions":true,"lenient":false,"zero_terms_query":"NONE","auto_generate_synonyms_phrase_query":true,"boost":1.0}}},{"match":{"itemNameKana":{"query":"検索ワードを入力","operator":"OR","prefix_length":0,"max_expansions":50,"fuzzy_transpositions":true,"lenient":false,"zero_terms_query":"NONE","auto_generate_synonyms_phrase_query":true,"boost":1.0}}},{"match":{"itemNameHira":{"query":"検索ワードを入力","operator":"OR","prefix_length":0,"max_expansions":50,"fuzzy_transpositions":true,"lenient":false,"zero_terms_query":"NONE","auto_generate_synonyms_phrase_query":true,"boost":1.0}}}],"adjust_pure_negative":true,"boost":1.0}},"sort":[{"_score":{"order":"desc"}}]}この結果より、「Javaで発行されたクエリの内容が間違っている」は原因ではないといえます。
また、Javaでの取得結果(Elasticsearch→Javaに返却された結果:client.search(request, RequestOptions.DEFAULT)の戻り値)に対し、この段階でスコア(_score)を確認したところ、Kibanaでの実行結果と異なることがわかりました。(調査の中で明らかになった今回重要ポイントなので太字にしておきます)2. analyzerの設定がおかしい
続いて、analyzerの設定がおかしいのでは?という観点ですが、analyzerの設定がおかしければKibanaでクエリを実行した場合におかしな結果が出るはず。
今回それはなかったので、「analyzerの設定がおかしい」も原因ではないことがわかりました。3. 暗黙的に設定された内容を修正する必要がある
残るはこれ。
searchSourceBuilderの内容に問題がなかったので、可能性として高いのが、
- request(SearchRequest)
- client.search(request, RequestOptions.DEFAULT)(SearchResponse)
のいずれか。
デバッグモードで、request(SearchRequest)の中身を確認したところ、
request(SearchRequest)の中身SearchRequest{searchType=QUERY_THEN_FETCH, indices=[item_list], indicesOptions=IndicesOptions[ignore_unavailable=false, allow_no_indices=true, expand_wildcards_open=true, expand_wildcards_closed=false, allow_aliases_to_multiple_indices=true, forbid_closed_indices=true, ignore_aliases=false, ignore_throttled=true], types=[], routing='null', preference='null', requestCache=null, scroll=null, maxConcurrentShardRequests=0, batchedReduceSize=512, preFilterShardSize=128, allowPartialSearchResults=null, localClusterAlias=null, getOrCreateAbsoluteStartMillis=-1, ccsMinimizeRoundtrips=true, source={"size":10,"query":{"bool":{"should":[{"match":{"itemName":{"query":"検索ワードを入力","operator":"OR","prefix_length":0,"max_expansions":50,"fuzzy_transpositions":true,"lenient":false,"zero_terms_query":"NONE","auto_generate_synonyms_phrase_query":true,"boost":1.0}}},{"match":{"itemNameKana":{"query":"検索ワードを入力","operator":"OR","prefix_length":0,"max_expansions":50,"fuzzy_transpositions":true,"lenient":false,"zero_terms_query":"NONE","auto_generate_synonyms_phrase_query":true,"boost":1.0}}},{"match":{"itemNameHira":{"query":"検索ワードを入力","operator":"OR","prefix_length":0,"max_expansions":50,"fuzzy_transpositions":true,"lenient":false,"zero_terms_query":"NONE","auto_generate_synonyms_phrase_query":true,"boost":1.0}}}],"adjust_pure_negative":true,"boost":1.0}},"sort":[{"_score":{"order":"desc"}}]}}となっていました。
source以降はsearchSourceBuilderの中身確認で確認したので、除外。となると、大項目的に見れば、
- searchType
- indicesOptions
ですね。
searchTypeについては、
- Search Type(Elasticsearch Reference [6.8] ) | elastic
- Search API(Elasticsearch Reference [7.9])| elastic
- Enum SearchType(elasticsearch 7.4.2) | org.elasticsearch
に記載があります。
indicesOptionsについては、
と、あとはちょっと違う気がしないでもないですが、
- Indices module(Elasticsearch Reference [7.9]) | elastic
- index_options(Elasticsearch Reference [7.9]) | elastic
でしょうか。
このうち、スコア(score)に関して明記があったのがsearchTypeのほう。
(indicesOptionsについてはザっと見渡した限りではスコア(score)に関する内容は見当たりませんでした。)上記であげたページ、Search Type(Elasticsearch Reference [6.8] ) | elasticの「Dfs, Query Then Fetch」の箇所に、
・・・more accurate scoring.
とあるので、より正確なスコアを出してくれるとみられるこちらを設定してみることに。
(「対応内容」に続く)対応内容
調査結果より、SearchTypeの内容を修正する必要があるらしいことがわかったので対応していきます。
SearchRequestのsearchTypeを「Dfs, Query Then Fetch」を設定します(「追記」と記載のある箇所)。Javaソースコード(変更後)/** * 商品検索 * * @param keyword キーワード * @param index インデックス名 * @param limit 件数 * @param client Elasticsearch接続クライアント * @return 検索結果 * @throws IOException */ public SearchResponse search(String keyword, String index, int limit, RestHighLevelClient client) throws IOException{ // 検索条件の初期化 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder(); boolQueryBuilder.should(QueryBuilders.matchQuery("itemName", keyword)) .should(QueryBuilders.matchQuery("itemNameKana", keyword)) .should(QueryBuilders.matchQuery("itemNameHira", keyword)); searchSourceBuilder.query(boolQueryBuilder); // ソート順の設定(スコアでソート) searchSourceBuilder.sort(new FieldSortBuilder("_score").order(SortOrder.DESC)); // 返却件数を設定 searchSourceBuilder.size(limit); SearchRequest request = new SearchRequest(index).source(searchSourceBuilder); // 検索タイプをDfs, Query Then Fetchに設定 // 追記 request.searchType(SearchType.DFS_QUERY_THEN_FETCH); // 追記 return client.search(request, RequestOptions.DEFAULT); }そして、動かしてみたところ、見事想定通りの結果(Kibanaでの実行クエリと同じ)に!
無事問題解決です!!結論
SearchRequestのsearchTypeを「Dfs, Query Then Fetch」としてあげることが今回の正解でした。
最初想定通りの結果が得られなくて焦りましたが、解決できてよかったです。参考
本文中に出てきませんが、参考にさせていただいた記事一覧です。
- 投稿日:2020-09-09T18:00:27+09:00
アリババクラウドのLOG Java Producerの使い方
この記事では、Alibaba CloudのLOG Java Producerという、Log Serviceにデータを送信するのに役立つ、使いやすく設定性の高いJavaライブラリの使い方を紹介しています。
本ブログは英語版からの翻訳です。オリジナルはこちらからご確認いただけます。一部機械翻訳を使用しております。翻訳の間違いがありましたら、ご指摘いただけると幸いです。
背景
Alibaba Cloud LOG Java Producerは、ビッグデータや高同時実行シナリオで動作するJavaアプリケーション向けに設計された高性能な書き込みLogHubライブラリです。APIやSDKを使用する場合と比較して、Alibaba Cloud LOG Java Producer(プロデューサー)を使用することで、高性能、コンピューティングとI/Oロジックの分離、制御可能なリソース使用量など、多くのメリットがあります。Producerの機能や仕組みを理解するには、記事「Alibaba Cloud LOG Java Producer - ログをクラウドに移行するための強力なツール」を参照してください。本記事では、Producerの使い方を中心に解説します。
利用方法の手順
以下の図のように、Producerを使用するためには、3つのステップを実行することができます。
Producerの作成
Producerを作成する際には、以下のオブジェクトが関与します。
プロジェクトのコンフィグ
ProjectConfig オブジェクトには、対象プロジェクトのサービス・エンドポイント情報と、呼び出し元の身元を示すアクセス・クレデンシャルが格納されます。
サービス エンドポイント
最終的なアクセスアドレスは、プロジェクト名とサービスエンドポイントで構成されます。プロジェクトのエンドポイントの決定方法の詳細は、「サービスエンドポイント」を参照してください。
アクセス・クレデンシャル
Producerの AccessKey または Security Token Service (STS) トークンを設定することができます。STS トークンを使用する場合は、定期的に新しい ProjectConfig オブジェクトを作成して ProjectConfig に入れる必要があります。
ProjectConfigs
異なるプロジェクトにデータを書き込む必要がある場合は、複数の ProjectConfig オブジェクトを作成して ProjectConfigs に置くことができます。ProjectConfigsは、マップを介して異なるプロジェクトの構成を維持します。マップのキーはプロジェクト名、値はプロジェクトのクライアントです。
ProducerConfig
ProducerConfig は、送信ポリシーを設定するために使用します。異なるビジネスシナリオに応じて異なる値を指定することができます。これらのパラメータの説明を次の表に示します。
詳しくはTimeoutExceptionとAttemptsを参照してください。
LogProducer
LogProducerはProducerの実装クラスであり、producerConfigパラメータのみを受け付けるようになっています。producerConfigを用意したら、以下のようにProducerのインスタンスを作成します。
Producer producer = new LogProducer(producerConfig);Producerのインスタンスを作成すると、一連のスレッドが作成されます。これはかなりの量のリソースを消費します。1 つのアプリケーションに対して 1 つの Producer インスタンスのみを使用することをお勧めします。Producerインスタンスのスレッドは以下のようにリストアップされています。
さらに、LogProducer が提供するメソッドはすべてスレッドセーフです。これらのメソッドはマルチスレッド環境で安全に実行できます。
データの送信
Producerのインスタンスを作成した後、それが提供するメソッドを使用してデータを送信することができます。
パラメータの説明
Producerは、複数のデータ送信方法を提供します。これらの方法のパラメータは以下の通りです。
異なるデータをビッグバッチにマージするには、データが同じプロジェクト、ログストア、トピック、ソース、および shardHash プロパティを持つ必要があります。データ マージ機能を正しく動作させ、メモリ リソースを節約するために、これら 5 つのプロパティの値の範囲を制御することをお勧めします。トピックなどのフィールドの値があまりにも多くの異なる値を持つ場合は、トピックを直接使用するのではなく、これらの値を logItem に追加することをお勧めします。
データ送信結果の取得
Producerは非同期にデータを送信します。データ送信結果は、Producerが返すフューチャーオブジェクトやコールバックから取得する必要があります。
Future
sendメソッドはListenableFutureを返します。ListenableFutureでは、I/Oスレッドをブロックしてget()メソッドを呼び出してデータ送信結果を取得するだけでなく、コールバックを登録することができます。コールバックはFutureの設定が完了した後に呼び出されます。以下のスニペットはListenableFutureの使い方を示しています。この未来のためにFutureCallbackを登録し、実行のためにアプリケーションが提供するEXECUTOR_SERVICEスレッドプールにコールバックを送る必要があります。完全なサンプルコードについては、SampleProducerWithFuture.javaを参照してください。
ListenableFuture<Result> f = producer.send("project", "logStore", logItem); Futures.addCallback(f, new FutureCallback<Result>() { @Override public void onSuccess(@Nullable Result result) { } @Override public void onFailure(Throwable t) { } }, EXECUTOR_SERVICE);コールバック
将来的には、sendメソッドを呼び出した際にコールバックを登録して、データ送信結果を取得することもできます。コードスニペットは以下の通りです。完全なサンプルコードは、SampleProducerWithCallback.javaを参照してください。
producer.send( "project", "logStore", logItem, new Callback() { @Override public void onCompletion(Result result) { } });コールバックはProducerの内部スレッドによって実装されます。バッチで占有されているスペースは、対応するコールバックが実行された後にのみ解放されます。Producerをブロックして全体のスループットを低下させないようにするために、コールバックで時間を消費する操作を実行しないようにしてください。プロデューサバッチの送信を再試行するためにsendメソッドを呼び出すことも推奨されません。retries パラメータの値を増やすか、ListenableFuture オブジェクトのコールバックでバッチの送信をリトライすることができます。
Futureとコールバックの比較
データ送信結果を取得するには、Futureとコールバックのどちらを選択すればいいのでしょうか?結果を取得した後の処理ロジックが比較的単純で、ProducerのI/Oスレッドをブロックしない場合は、直接コールバックを使用します。それ以外の場合は、ListenableFutureを使用して、その後の処理ロジックを別のスレッド(プール)で実行することをお勧めします。
Producerをシャットダウン
送信するデータが少なくなった場合や、現在の処理を終了する場合は、Producerをシャットダウンする必要があります。そうすることで、Producerのキャッシュされたデータを完全に処理することができます。
安全なシャットダウン
ほとんどの場合、安全にシャットダウンすることをお勧めします。close()メソッドを呼び出すことで、Producerを安全にシャットダウンすることができます。このメソッドは、Producerのキャッシュされたデータがすべて処理され、すべてのスレッドが終了し、登録されたコールバックが実行され、すべての先物が設定されたときにのみ返されます。
全てのデータが処理されるまで待たなければならないが、Producerをシャットダウンした後は、キャッシュされたバッチはすぐに処理され、失敗してもリトライされることはありません。そのため、コールバックがブロックされていない限り、通常はcloseメソッドですぐに戻ることができます。
限定的なシャットダウン
コールバックが実行されたときにブロックされる可能性が高く、closeメソッドを短く返したい場合は、制限付きシャットダウンモードを使用します。
close(long timeoutMs)
メソッドを使って制限付きシャットダウンを実装することができます。指定した timeoutMs が経過しても完全にシャットダウンされない場合、IllegalStateException 例外が発生します。この場合、キャッシュされたデータが処理されたか、登録されたコールバックが実行されたかに関わらず、Producerはシャットダウンされます。サンプルアプリケーション
Producerの学習をより簡単にするために、Alibaba Cloud LOG Java Producerサンプルアプリケーションを用意しました。サンプルでは、Producerの作成からシャットダウンまでを網羅しています。
アリババクラウドは日本に2つのデータセンターを有し、世界で60を超えるアベラビリティーゾーンを有するアジア太平洋地域No.1(2019ガートナー)のクラウドインフラ事業者です。
アリババクラウドの詳細は、こちらからご覧ください。
アリババクラウドジャパン公式ページ
- 投稿日:2020-09-09T17:05:23+09:00
Array練習
package Array_pra; import java.util.ArrayList; import java.util.Iterator; public class Animals { public static void main(String[] args) { //まずはインスタンスをつくる ArrayList<String> Animal=new ArrayList<String>(); //値をaddメソッドで追加 Animal.add("ひつじ"); Animal.add("ねこ"); Animal.add("いぬ"); Animal.add("きつね"); Animal.add("りす"); //Animalの要素数を出力 System.out.println(Animal.size()); //Animalの配列内が空っぽか否か System.out.println(Animal.isEmpty()); //Animalのどこに"ひつじ"が入ってるか System.out.println(Animal.indexOf("ひつじ")); //Animalの中に入っている要素をひとつづつ取り出す Iterator <String> itAnimal=Animal.iterator(); while(itAnimal.hasNext()){ String aaa=itAnimal.next(); System.out.println(aaa); } } }実行結果
5
false
0
ひつじ
ねこ
いぬ
きつね
りす
- 投稿日:2020-09-09T15:40:38+09:00
列挙の練習
列挙の練習
◎rekkyo練習クラス
package rekkyo_renshu; public class Film { private String Name; // 予約名 private FilmType filmType; // 映画のなまえ // 列挙型の宣言。これ以外を指定してインスタンスは作れない enum FilmType{ COMEDY,LOVE,HORROR } public Film(String name,FilmType ft) { //受け取ったnameをそのまま使う(switch文では特に使用しないので) //ここに代入 String Name=name; //switch文で、各列挙型の変数入りインスタンスが生成された時の処理 switch(ft) { case COMEDY: filmType=FilmType.COMEDY; System.out.println("予約映画名[AustinPowers] 予約者名["+Name+"]"); break; case LOVE: filmType=FilmType.LOVE; System.out.println("予約映画名[AboutTime] 予約者名["+Name+"]"); break; case HORROR: filmType=FilmType.HORROR; System.out.println("予約映画名[Shining] 予約者名["+Name+"]"); break; } } }◎Mainクラス
package rekkyo_renshu; import rekkyo_renshu.Film.FilmType; public class Main { public static void main(String[] args) { Film film1=new Film("ヤマダタカユキ",FilmType.COMEDY); } }実行結果
予約映画名[AustinPowers] 予約者名[ヤマダタカユキ]
- 投稿日:2020-09-09T15:22:20+09:00
Javaライブラリ - Alibaba CloudのLOG Java Producerでログサービスへのデータ送信を支援
本記事では、ログサービスへのデータ送信を支援する、使いやすく設定性の高いJavaライブラリ「Alibaba CloudのLOG Java Producer」を紹介します。
本ブログは英語版からの翻訳です。オリジナルはこちらからご確認いただけます。一部機械翻訳を使用しております。翻訳の間違いがありましたら、ご指摘いただけると幸いです。
背景
ログは至る所にあります。世の中の変化を記録するキャリアとして、ログはマーケティング、研究開発、運用、セキュリティ、BI、監査など多くの分野で広く利用されています。
アリババログサービスは、ログデータのオールインワンサービスプラットフォームです。その中核コンポーネントであるLogHubは、高スループット、低遅延、自動スケーリングなどの優れた機能により、ビッグデータ処理、特にリアルタイムデータ処理のインフラとなっています。Flink、Spark、Stormなどのビッグデータコンピューティングエンジン上で動作するジョブは、データ処理結果や中間結果をリアルタイムでLogHubに書き込みます。LogHubからのデータを利用して、下流のシステムは、クエリ分析、アラームの監視、機械学習、反復計算など多くのサービスを提供することができます。LogHubのビッグデータ処理アーキテクチャは次の図のようになっています。
システムが正常に動作するようにするためには、便利で効率の良いデータ書き込み方法を利用する必要があります。APIやSDKを直接利用するだけでは、ビッグデータシナリオにおけるデータ書き込み能力の要件を満たすには不十分です。そこで開発されたのが「Alibaba Cloud LOG Java Producer」です。
特徴
Alibaba Cloud LOG Java Producerは、使いやすく、高度に設定可能なJavaクラスライブラリです。以下の機能を備えています。
1、スレッドセーフ:Alibaba Cloud LOG Java Producer (以下「Producer」)によって公開されるすべてのメソッドはスレッドセーフです。
2、非同期送信:Producer の SEND メソッドへの呼び出しは、通常、すぐに返され、データの送信やサーバからの応答の受信を待つことはありません。Producerには、送信するデータを一括でキャッシュするための内部キャッシュ機構(LogAcccumulator)があり、データを一括で送信することでスループットを向上させています。
3、自動リトライ: Producerは、再試行可能な例外に対して、自動で設定可能なリトライ機構(RetryQueue)を提供します。RetryQueue の最大リトライ時間とバックオフ期間を設定することができます。
4、トレーサビリティ:コールバックやフューチャーを使用して、対象のデータが正常に送信されたかどうかや、データを送信するために行われた試行を知ることができます。この機能を使用することで、問題をトレースし、問題解決のための判断を下すことができます。
5、コンテキストリストア: 同じProducerで生成されたログは同じコンテキストにあり、あるログの前後の関連ログをサーバー側で確認することができます。
6、シャットダウン:closeメソッドが結果を返すと、Producerがキャッシュしたデータが全て処理され、それに応じた通知を受け取ることができます。メリット
Producerを使用してLogHubにデータを書き込むことは、APIやSDKを使用した場合と比較して以下のようなメリットがあります。
ハイパフォーマンス
大量のデータと限られたリソースでは、所望のスループットを実現するために、マルチスレッド、キャッシュポリシー、バッチ処理、障害発生時のリトライなどの複雑なロジックを実装する必要があります。Producerは、アプリケーションのパフォーマンスを向上させ、アプリケーション開発プロセスを簡素化するために、上記のロジックを実装しています。
非同期かつノンブロッキングのタスク実行
十分なキャッシュメモリがあれば、ProducerはLogHubに送信するデータをキャッシュします。sendメソッドを呼び出すと、指定されたデータは処理をブロックすることなく即座に送信されます。これにより、演算とI/Oロジックの分離を実現しています。後日、返された未来のオブジェクトや登録されたコールバックからデータ送信結果を取得することができます。
制御可能なリソースの利用
送信するデータをキャッシュするためにProducerが使用するメモリのサイズは、データ送信タスクに使用するスレッド数と同様にパラメータで制御することができます。これにより、Producerが無制限にリソースを消費することを回避することができます。また、実情に応じてリソース消費量と書き込みスループットのバランスをとることができます。
まとめ
要約すると、Producerは、複雑な基礎となる詳細を自動的に処理し、シンプルなインターフェースを公開することで、多くの利点を提供します。また、上位レイヤサービスの正常な動作に影響を与えず、データアクセスの敷居を大幅に下げることができます。
メカニズムの説明
Producer のパフォーマンスをよりよく理解するために、このセクションでは、データ書き込みロジック、コアコンポーネントの実装、グレースフルシャットダウンを含む Producer がどのように動作するかを説明します。Producer の全体的なアーキテクチャは以下の図に示されています。
データ書き込み
Producerのデータ書き込みロジック:
1、
producer.send()
メソッドを呼び出して指定したログストアにデータを送信した後、LogAccumulator内のProducerバッチにデータがロードされます。通常、sendメソッドはすぐに結果を返します。しかし、Producer インスタンスに目的のデータを格納する十分なスペースがない場合、以下の条件のいずれかが満たされるまで send メソッドはブロックされます。------1、以前にキャッシュされたデータがバッチハンドラで処理され、そのデータで占有されていたメモリが解放されます。その結果、Producerは対象のデータを格納するための十分なスペースを持つことになります。
------2、指定されたブロッキング時間を超えると例外が発生します。2、Producer.send() を呼び出すと、対象バッチのログ数が maxBatchCount を超えてしまったり、対象バッチに対象データを格納するための十分なスペースがない場合があります。この場合、Producerは最初にターゲットバッチを IOThreadPool に送信してから、ターゲットデータを格納するための新しいバッチを作成します。スレッドをブロックしないようにするために、IOThreadPool は無制限のブロッキングキューを使用します。Producer インスタンスにキャッシュできるログの数は限られているので、キューの長さが無限に伸びることはありません。
3、Mover は LogAccumulator の各 Producer バッチをトラバースし、最大キャッシュ時間を超えたバッチを expiredBatches に送ります。また、期限切れになっていないバッチの最も早い期限切れ時間(t)も記録します。
4、その後、LogAccumulator から IOThreadPool に期限切れバッチを送信します。
5、その後、Mover は RetryQueue から送信条件に合致したProducerバッチを取得します。条件を満たすバッチがない場合は、t の期間待機します。
6、そして、期限切れのバッチを RetryQueue から IOThreadPool に送信する。ステップ 6 が終了すると、Mover はステップ 3 から 6 を繰り返します。
7、IOThreadPool のワーカースレッドは、ブロックされたキューからターゲットのログストアにバッチを送ります。
8、バッチがログストアに送られた後、それは成功キューに行きます。
9、送信に失敗し、以下の条件のいずれかを満たす場合は、失敗キューに行きます。
-------1、失敗したバッチは再試行できません。
-------2、RetryQueueが閉じられます。
-------3、指定されたリトライ回数に達し、失敗キューのバッチ数が送信するバッチの総数の1/2を超えません。10、そうでなければ、ワーカースレッドは失敗したバッチの次回の送信時刻を計算してRetryQueueに送信します。
11、SuccessBatchHandler スレッドは、成功キューからバッチを取り出し、このバッチに登録されているすべてのコールバックを実行します。
12、FailureBatchHandler スレッドは、失敗キューからバッチを取り出し、このバッチに登録されているすべてのコールバックを実行します。コアコンポーネント
Producer のコアコンポーネントには、LogAccumulator、RetryQueue、Mover、IOThreadPool、SendProducerBatchTask、BatchHandler があります。
LogAccumulator
スループットを向上させるために、より大きなバッチにデータを蓄積し、バッチでデータを送信するのが一般的です。ここで説明するLogAccumulatorの主な役割は、データをバッチにマージすることです。異なるデータを大きなバッチにマージするには、データが同じプロジェクト、ログストア、トピック、ソース、およびshardHashプロパティを持っている必要があります。LogAccumulator は、これらのプロパティに基づいて、これらのデータを内部マップの異なる位置にキャッシュします。マップのキーは、上記の5つのプロパティの5つの要素であり、値はProducerBatchです。スレッドの安全性と高い並行性を確保するために、ConcurrentMapが使用されます。
LogAccumulatorのもう一つの機能は、キャッシュされたデータの合計サイズを制御することです。この制御ロジックを実装するためにSemaphoreを使用しています。SemaphoreはAbstractQueuedSynchronizerベース(AQSベース)の高性能同期ツールです。Semaphoreは、まずスピニングによる共有リソースの取得を試み、コンテキストスイッチのオーバーヘッドを削減します。
RetryQueue
RetryQueue は、送信に失敗して再試行を待っているバッチを保存するために使用されます。これらの各バッチは、バッチを送信する時間を示すフィールドを持っています。期限切れのバッチを効率的に引き出すために、プロデューサーはこれらのバッチを保存するためのDelayQueueを持っています。DelayQueue は時間ベースの優先度の高いキューで、最も早い期限切れのバッチが最初に処理されます。このキューはスレッドセーフです。
Mover
Mover は独立したスレッドです。LogAccumulator と RetryQueue から期限切れのバッチを定期的に IOThreadPool に送信します。Mover はアイドル状態でも CPU リソースを占有します。CPU リソースの無駄遣いを避けるために、Mover は、LogAccumulator および RetryQueue から送信される適格なバッチが見つからない間、RetryQueue からの期限切れバッチを待ちます。この期間は、構成した最大キャッシュ時間 lingerMs です。
IOThreadPool
IOThreadPool内のワーカースレッドは、ログストアにデータを送信します。IOThreadPool のサイズは ioThreadCount パラメータで指定でき、デフォルト値はプロセッサ数の 2 倍です。
SendProducerBatchTask
SendProducerBatchTaskは、バッチ送信ロジックでカプセル化されています。I/O スレッドのブロックを避けるために、SendProducerBatchTask は、ターゲットのバッチが正常に送信されたかどうかにかかわらず、コールバック実行のために別のキューにターゲットのバッチを送信します。さらに、失敗したバッチがリトライ条件を満たした場合、現在のI/Oスレッドではすぐに再送されません。すぐに再送された場合、通常は再び失敗します。その代わりに、SendProducerBatchTask は、指数的なバックオフポリシーに従って RetryQueue に送ります。
BatchHandler
プロデューサーは、送信に成功したバッチと失敗したバッチを処理するために、SuccessBatchHandler と FailureBatchHandler を起動します。ハンドラがコールバックの実行やバッチの未来の設定を完了した後、新しいデータを使用するために、このバッチが占有しているメモリを解放します。別々の処理は、正常に送信されたバッチと失敗したバッチが分離されていることを確実にします。これは Producer のスムーズな操作を保証します。
GracefulShutdown
GracefulShutdownを実装するには、以下の要件を満たす必要があります。
1、close メソッドが結果をあなたに返すとき、プロデューサーのすべてのスレッドが終了している必要があります。また、キャッシュされたデータが適切に処理されていること、自分で登録したコールバックがすべて実行されていること、自分に返す先物がすべて設定されていることが必要になります。
2、また、closeメソッドの最大待ち時間を設定できるようにしておく必要があります。メソッドは、スレッドが終了したかどうか、キャッシュされたデータが処理されたかどうかに関わらず、この期間を超えた後、直ちに結果をあなたに返さなければなりません。
3、closeメソッドは、マルチスレッド環境でも複数回呼び出すことができ、正常に動作します。
4、コールバックでcloseメソッドを呼び出すことは安全であり、アプリケーションにデッドロックを起こすことはありません。前述の要件を満たすために、プロデューサーのクローズロジックは以下のように設計されています。
1、LogAccumulatorを閉じる。LogAccumulator にデータを書き続けると例外が発生します。
2、RetryQueue を閉じます。RetryQueue にバッチを送り続けると、例外がスローされます。
3、Mover を閉じて、完全に終了するのを待ちます。クローズシグナルを検出した後、Mover は送信条件を満たしているかどうかに関わらず、LogAccumulator と RetryQueue から残っているすべてのバッチを IOThreadPool に送信します。データ損失を避けるために、Mover は、他のスレッドが書き込みをしなくなるまで、常に LogAccumulator と RetryQueue からバッチを引っ張ってきます。
4、IOThreadPool を閉じて、送信されたすべてのタスクが完了するのを待ちます。RetryQueue が既に閉じられている場合、失敗したバッチは直接失敗キューに送られます。
5、SuccessBatchHandlerを閉じて、完全に終了するのを待ちます。コールバックでcloseメソッドが呼び出された場合、待機処理はスキップされます。Closeシグナルを検出した後、SuccessBatchHandlerは成功キューからすべてのバッチを取り出し、1つずつ処理します。
6、FailureBatchHandlerを閉じて、完全に終了するのを待つ。コールバックでcloseメソッドが呼び出された場合、待機処理はスキップされます。Close シグナルを検出した後、FailureBatchHandler は失敗キューからすべてのバッチを取り出し、1 つずつ処理します。このように、データの流れの方向を基準にしてキューやスレッドを1つずつ閉じていくことで、優雅なシャットダウンと安全な終了を実現しています。
まとめ
Alibaba Cloud LOG Java Producerは、以前のバージョンのProducerの包括的なアップグレードです。ネットワーク例外が発生した場合のCPU使用率の高さや、Producerを終了する際のデータ損失の少なさなど、以前のバージョンでの多くの問題点を解決しています。さらに、フォールトトレランス機構が強化されました。Producerは、操作ミスをした後でも、適切なリソース使用量、高いスループット、厳密な隔離を確保することができます。
アリババクラウドは日本に2つのデータセンターを有し、世界で60を超えるアベラビリティーゾーンを有するアジア太平洋地域No.1(2019ガートナー)のクラウドインフラ事業者です。
アリババクラウドの詳細は、こちらからご覧ください。
アリババクラウドジャパン公式ページ
- 投稿日:2020-09-09T15:18:11+09:00
*Android*【HTTP通信_2】FlaskとHTTP通信をする(WebAPIを叩く[GET/POST])
はじめに
前回の記事では,httpbinをWebAPIとして使用し,HttpURLConnectionの使い方について解説しました.今回は,FlaskをWebAPIとして使用し,AndroidとFlask間でHTTP通信(GET/POST)を行ってみます.HttpURLConnectionやcurlコマンドの詳しい解説は,前回の記事を参考にしてください.
前提
*Android Studio 4.0.1
*targetSdkVersion 28
*Google Nexus 5xcurlコマンド, Python
*Ubuntu 20.04 LTS (WSL 2)
*Python 3.7.3GETメソッド
GETメソッドを使ったHTTP通信は簡単に実現できます.Android側は前回の記事を参考にしてください.最後に示すサンプルコードにはAndroid側のコードも記述します.ここではFlask側に関して記述します.
app.pyfrom flask import Flask, jsonify app = Flask(__name__) @app.route('/api/get', methods=['GET']) def get(): # GETリクエストに対するレスポンスを作成 response = {'result': 'success', 'status': 200} # JSONオブジェクトとして返す return jsonify(response)curlコマンドを使ってリクエストを送ってみます.
curl http://127.0.0.1:5000/api/get以下のようなレスポンスが出力されるはずです.
{ "result": "success", "status": 200 }POSTメソッド
POSTメソッドを使ったHTTP通信について説明します.
flaskでJSON形式のデータを読み込む
JSON形式のデータを読み込む方法としては3つあります.今回は,POSTリクエストで受け取ったデータを読み込むことになります.
app.pyfrom flask import Flask, request, jsonify import json app = Flask(__name__) @app.route('/api/post', methods=['POST']) def post(): # 方法1 # POSTで受け取ったデータをrequest.dataを取り出すとbytes型であることがわかる print(type(request.data)) # 出力してもbytes型であることがわかる print(request.data) # bytes型なので文字列に直すためにdecodeする print(request.data.decode('utf-8')) # JSON形式で書かれている文字列をloadsメソッドによってディクショナリ型に変換できる data = json.loads(request.data.decode('utf-8')) print(data) # 方法2 # loadsメソッドでは,文字列以外にbytes型,bytearray型を入れることができる data = json.loads(request.data) print(data) # 方法3 # request.jsonメソッドを使うことで,POSTで受け取ったデータをディクショナリ型として扱うことができる # この書き方が1番早いので推奨する print(request.json) # 方法3を採用し,戻り値としてJSONオブジェクトを返す data = request.json return jsonify(data) if __name__ == '__main__': app.run(host='0.0.0.0', debug=True)app.pyを実行して,試しにcurlを使ってPOSTリクエストを送ってみる.新しいターミナルを開き,以下のコマンドを実行する.bodyは,JSON形式で文字列を記述する.
curl -X POST -H 'Content-Type:application/json' -d `{"name": "foge", "value": "fogefoge"}` http://127.0.0.1:5000/api/postcurlコマンドを実行すると,app.pyを実行したターミナルに以下のように出力される.下3行の出力が同じであることが確認できる.
<class 'bytes'> b'{"name": "foge", "value": "fogefoge"}' {"name": "foge", "value": "fogefoge"} {'name': 'foge', 'value': 'fogefoge'} {'name': 'foge', 'value': 'fogefoge'} {'name': 'foge', 'value': 'fogefoge'}AndroidでのJsonのフォーマット
Androidから送信したJSON形式で書かれた文字列のデータをPythonで読み込むには,Androidの方でJSONの形式に則ってボディを記述する必要がある.具体的には以下のようにする.
String postData = "{\"name\": \"foge\", \"value\": \"fogefoge\"}";このように,文字列の中に二重引用符を記述する必要があるためエスケープシーケンスを使って記述する.また,文字列の中に変数のデータを埋め込みたい場合は以下のように記述する.
String name = "foge"; int value = 100; String postData = String.format("{\"name\": \"%s\", \"value\": %d}", name, value);しかし,このように記述するのは非常に面倒なため,一般的には以下のように記述する.
// 連想配列を作成する HashMap<String, Object> jsonMap = new HashMap<>(); jsonMap.put("name", "foge"); jsonMap.put("value", 100); // 連想配列をJSONObjectに変換 JSONObject responseJsonObject = new JSONObject(jsonMap); // JSONObjectを文字列に変換 String postData = responseJsonObject.toString();サンプルコード
では,以下にサンプルコードを示す.
app.pyfrom flask import Flask, request, jsonify app = Flask(__name__) # jsonifyの結果に日本語が含まれる場合,以下の1行を記述することで文字化けを回避できる app.config['JSON_AS_ASCII'] = False @app.route('/api/get', methods=['GET']) def get(): response = {"result": "success", "status": 200} return jsonify(response) @app.route('/api/post', methods=['POST']) def post(): data = request.json name = data['name'] value = data['value'] array = data['array'] print(f'data: {data}') print(f'data["name"]: {name}') print(f'data["value"]: {value}') print(f'data["array"]: {array}') print(f'data["array[0]"]: {array[0]}') print(f'data["array[1]"]: {array[1]}') print(f'data["array[2]"]: {array[2]}') return jsonify(data) if __name__ == '__main__': app.run(host='0.0.0.0', debug=True)Androidのサンプルコードを記述する前にcurlで一度HTTP通信ができるかどうか確認を行ってください.
curl http://127.0.0.1:5000/api/getcurl -X POST -H 'Content-Type:application/json' -d '{"name": "foge", "value": 100, "array":["おはよう", "こんにちは", "こんばんは"]}' http://127.0.0.1:5000/api/post以下にAndroidのサンプルコードを示します.
AndroidManifest.xml<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.samplehttpconnection"> <uses-permission android:name="android.permission.INTERNET"/> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>activity_main.xml<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="100dp" android:gravity="center" android:text="" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@+id/button2" /> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="100dp" android:text="GET" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/textView2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="100dp" android:gravity="center" android:text="" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/textView" /> <Button android:id="@+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="50dp" android:text="POST" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/button" /> </androidx.constraintlayout.widget.ConstraintLayout>MainActivity.javapackage com.example.samplehttpconnection; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.os.Handler; import android.view.View; import android.widget.Button; import android.widget.TextView; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.Locale; public class MainActivity extends AppCompatActivity { private Handler handler = new Handler(); private Button button; private Button button2; private TextView textView; private TextView textView2; // IPアドレスは各自変更してください.Pythonのプログラムを実行しているPCのIPアドレスを記述 private String urlGetText = "http://192.168.0.10:5000/api/get"; private String urlPostText = "http://192.168.0.10:5000/api/post"; private String getResultText = ""; private String postResultText = ""; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); button = findViewById(R.id.button); button2 = findViewById(R.id.button2); textView = findViewById(R.id.textView); textView2 = findViewById(R.id.textView2); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Thread thread = new Thread(new Runnable() { @Override public void run() { String response = ""; try { response = getAPI(); JSONObject rootJSON = new JSONObject(response); getResultText = rootJSON.toString(); } catch (JSONException e) { e.printStackTrace(); } handler.post(new Runnable() { @Override public void run() { textView.setText(getResultText); } }); } }); thread.start(); } }); button2.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Thread thread = new Thread(new Runnable() { @Override public void run() { String response = ""; try { response = postAPI(); JSONObject rootJSON = new JSONObject(response); postResultText = rootJSON.toString(); } catch (JSONException e) { e.printStackTrace(); } handler.post(new Runnable() { @Override public void run() { textView2.setText(postResultText); } }); } }); thread.start(); } }); } public String getAPI(){ HttpURLConnection urlConnection = null; InputStream inputStream = null; String result = ""; String str = ""; try { URL url = new URL(urlGetText); urlConnection = (HttpURLConnection) url.openConnection(); urlConnection.setConnectTimeout(10000); urlConnection.setReadTimeout(10000); urlConnection.addRequestProperty("User-Agent", "Android"); urlConnection.addRequestProperty("Accept-Language", Locale.getDefault().toString()); urlConnection.setRequestMethod("GET"); urlConnection.setDoInput(true); urlConnection.setDoOutput(false); urlConnection.connect(); int statusCode = urlConnection.getResponseCode(); if (statusCode == 200){ inputStream = urlConnection.getInputStream(); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "utf-8")); result = bufferedReader.readLine(); while (result != null){ str += result; result = bufferedReader.readLine(); } bufferedReader.close(); } } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return str; } public String postAPI(){ HttpURLConnection urlConnection = null; InputStream inputStream = null; OutputStream outputStream = null; String result = ""; String str = ""; try { URL url = new URL(urlPostText); urlConnection = (HttpURLConnection) url.openConnection(); urlConnection.setConnectTimeout(10000); urlConnection.setReadTimeout(10000); urlConnection.addRequestProperty("User-Agent", "Android"); urlConnection.addRequestProperty("Accept-Language", Locale.getDefault().toString()); urlConnection.addRequestProperty("Content-Type", "application/json; charset=UTF-8"); urlConnection.setRequestMethod("POST"); urlConnection.setDoInput(true); urlConnection.setDoOutput(true); urlConnection.connect(); outputStream = urlConnection.getOutputStream(); HashMap<String, Object> jsonMap = new HashMap<>(); jsonMap.put("name", "foge"); jsonMap.put("value", 100); ArrayList<String> array = new ArrayList<>(); array.add("おはよう"); array.add("こんにちは"); array.add("こんばんは"); jsonMap.put("array", array); JSONObject responseJsonObject = new JSONObject(jsonMap); String jsonText = responseJsonObject.toString(); BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream, "utf-8")); bufferedWriter.write(jsonText); bufferedWriter.flush(); bufferedWriter.close(); int statusCode = urlConnection.getResponseCode(); if (statusCode == 200){ inputStream = urlConnection.getInputStream(); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); result = bufferedReader.readLine(); while (result != null){ str += result; result = bufferedReader.readLine(); } bufferedReader.close(); } urlConnection.disconnect(); } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return str; } }参照してもらいたいページ
- 投稿日:2020-09-09T14:30:39+09:00
Decoratorパターン
Derocatorパターンとは
オブジェクトに飾り付けをほどこしていく。
Componentの役
機能を追加するときの核になる役。
package decorator; public abstract class Display { public abstract int getColumns(); public abstract int getRows(); public abstract String getRowText(int row); public final void show() { for (int i = 0; i < getRows(); i++) { System.out.println(getRowText(i)); } } }ConcreteComponentの役
Component役のインターフェースを実装する役。
package decorator; /** * 1行の文字列を表示するクラス */ public class StringDisplay extends Display{ private String string; public StringDisplay(String string) { this.string = string; } @Override public int getColumns() { return string.getBytes().length; } @Override public int getRows() { return 1; } @Override public String getRowText(int row) { return row == 0 ? string : null; } }Decorator(装飾者)の役
Component役と同じインターフェースを持つ。
また、Decorator役が飾る対象となるComponent役を持つ。
この役は自分が飾る対象を知っている。package decorator; /** * 飾り枠を表すクラス */ public abstract class Border extends Display { // この飾り枠がくるんでいる「中身」をさす protected Display display; protected Border(Display display) { this.display = display; } }ConcreteDecorator(具体的な装飾者)の役
具体的なDecoratorの役。
package decorator; /** * 文字列の左右に決まった文字で飾りをつけるクラス * */ public class SideBorder extends Border { private char borderChar; protected SideBorder(Display display, char ch) { super(display); this.borderChar = ch; } @Override public int getColumns() { // 文字数は両側に飾り文字分を加えたもの return 1 + display.getColumns() + 1; } @Override public int getRows() { return display.getRows(); } @Override public String getRowText(int row) { return borderChar + display.getRowText(row) + borderChar; } }package decorator; /** * 上下左右に飾りをつけるクラス * */ public class FullBorder extends Border{ protected FullBorder(Display display) { super(display); } @Override public int getColumns() { // 文字数は両側に飾り文字分を加えたもの return 1 + display.getColumns() + 1; } @Override public int getRows() { // 文字数は上下に飾り文字分を加えたもの return 1 + display.getRows() + 1; } @Override public String getRowText(int row) { if (row == 0) { // 枠の上側 return "+" + makeLine('-', display.getColumns()) + '+'; } else if (row == display.getRows() + 1) { // 枠の下側 return "+" + makeLine('-', display.getColumns()) + '+'; } else { return "|" + display.getRowText(row - 1) + "|"; } } /** * 指定した文字を連続して作成する * */ private String makeLine(char ch, int count) { StringBuffer buf = new StringBuffer(); for (int i = 0; i < count; i++) { buf.append(ch); } return buf.toString(); } }呼び出し元
package decorator; public class Main { public static void main(String[] args) { Display d1 = new StringDisplay("Hello world"); Display d2 = new SideBorder(d1, '#'); Display d3 = new FullBorder(d2); d1.show(); d2.show(); d3.show(); } }実行結果
包めば包むほど機能が追加されていく。
その際、包まれる側を変更することなく、機能の追加を行うことができる。https://github.com/aki0207/decorator
こちらを参考にさせていただきました。
増補改訂版Java言語で学ぶデザインパターン入門
- 投稿日:2020-09-09T12:09:27+09:00
Compositeパターン
Compositeパターンとは
容器と中身を同一視し、再帰的な構造を作る。
Leaf(葉)の役
「中身」を表す役。
この役の中には他のものを入れることはできない。package composite; public class File extends Entry{ private String name; private int size; public File(String name, int size) { this.name = name; this.size = size; } @Override public String getName() { return name; } @Override public int getSize() { return size; } @Override protected void printList(String prefix) { System.out.println(prefix + "/" + this); } }Composite(複合体)の役
「容器」を表す役。
Leaf役やComposite役を入れることができる。package composite; import java.util.ArrayList; import java.util.Iterator; public class Directory extends Entry{ private String name; private ArrayList<Entry> directory = new ArrayList<>(); public Directory(String name) { this.name = name; } @Override public String getName() { return name; } @Override public int getSize() { int size = 0; Iterator<Entry> it = directory.iterator(); while (it.hasNext()) { Entry entry = (Entry) it.next(); size += entry.getSize(); } return size; } public Entry add(Entry entry) { directory.add(entry); return this; } @Override protected void printList(String prefix) { System.out.println(prefix + "/" + this); Iterator<Entry> it = directory.iterator(); while (it.hasNext()) { Entry entry = (Entry) it.next(); entry.printList(prefix + "/" + name); } } }Componentの役
Leaf役とComposite役を同一視するための役。
Component役はLeaf役とComposite役に共通のスーパークラスとして実現する。package composite; public abstract class Entry { public abstract String getName(); public abstract int getSize(); protected abstract void printList(String prefix); public Entry add(Entry entry) throws FileTreatmentException { throw new FileTreatmentException(); } public void printList() { printList(""); } @Override public String toString() { return getName() + "(" + getSize() + ")"; } }Client(依頼者)の役
Compositeパターンの利用者。
package composite; public class Main { public static void main(String[] args) { try { System.out.println("Making root entryies..."); Directory rootDir = new Directory("root"); Directory binDir = new Directory("bin"); Directory tmpDir = new Directory("tmp"); Directory usrDir = new Directory("usr"); rootDir.add(binDir); rootDir.add(tmpDir); rootDir.add(usrDir); binDir.add(new File("vi", 10000)); binDir.add(new File("latex", 20000)); rootDir.printList(); System.out.println(); System.out.println("Making user entryies..."); Directory yuki = new Directory("yuki"); Directory hanako = new Directory("hanako"); Directory tomura = new Directory("tomura"); usrDir.add(yuki); usrDir.add(hanako); usrDir.add(tomura); yuki.add(new File("diary.html", 100)); yuki.add(new File("Composite.java", 200)); hanako.add(new File("memo.tex", 300)); tomura.add(new File("game.doc", 400)); tomura.add(new File("junk.mail", 500)); rootDir.printList(); } catch (FileTreatmentException e) { e.printStackTrace(); } } }実行結果
https://github.com/aki0207/composite
こちらを参考にさせていただきました。
増補改訂版Java言語で学ぶデザインパターン入門
- 投稿日:2020-09-09T11:25:07+09:00
Windowsでjarファイルをダブルクリックで開く
Windowsでjarファイルを開く
配布用にjarファイルを作り、いざ配布をしたところMacではダブルクリックで簡単に開けるもののWindowsでは開けない場合がある。私がダブルクリックで動くようにした方法をここにまとめる。
javaのインストール
javaがないと始まらないので無料のjava8を指示にしたがってインストールする。
https://java.com/ja/download/レジストリエディタの編集
ここが大事。虫眼鏡のところでregeditと検索。開いたら、HKEY_CURRENT_USER/Software/Classes/Applications/javaw.exe/shell/open/command
のように移動していく。「(既定)」をダブルクリックし「"C:¥...exe" "%1"」となっているものを「"C:¥...exe" -jar "%1"」と書き換える。HKEY_CLASSES_ROOT/Applications/javaw.exe/shell/open/commandでも良い。まとめ
最初はjarファイルを右クリックでプログラムから開く→別のプログラムを選択→常にこのアプリを...にチェック→Java(TM) Platform SE binaryで実行。
以降はダブルクリックで動かせるでしょう。
- 投稿日:2020-09-09T08:00:59+09:00
ゴリゴリのSIerのSEが初めての個人開発で公開まで頑張ってみた過程
ゴリゴリのSIerのSEが個人開発でWebサービスを作ってみた
の続き、みたいな。前回「Webサービスを作ってみた」話を書きました。
書いたやつを見て思ったんですが、ほとんどWebサービスの紹介でSIerの要素があんまり入ってない。
前回の意図としては、Webサービスそのものをアピールするというより「SIerはBPに投げるだけでプログラミングなんてしないよ」なんていう声をたまに聞くので、SIerの経験しかないSEがプログラミングやってWebサービスっぽいものを作るとこんな感じになりますよ、というひとつの実験結果をお見せしたかったのです。それが成功か失敗かは置いといて。
ただ、「こんなの作りました」しか書いてない気がするので、大まかにどんな過程で開発を進めていったかを書きますので、何かの参考になれば…と思います。
ほんと大まかです。
モダンではありません。
読み物的な感じです。
「ポエム」タグです。1.動機づけ
今回は「作りたいもの」が自分の開発のきっかけになりました。
なぞるように一から体系的に学ぶのもいいですが、「作りたいもの」に必要な機能、その機能に必要な技術は何か?って考えながら進めていくと結構時間を忘れてどんどん形になっていきました。
とはいえ「作りたいもの」はそんな簡単に出てこない気もするので、いろんな人に「何か欲しいもの」をひたすら聞くのも良いと思ってます。
それを「作りたいもの」にすればもうあとは手が勝手に動くと思います。2.開発環境
そう思い立って、では何から始めるか…
本番環境をイメージしました。
私はモダンな経験はしていません。
「まぁ最初の個人開発だしサーバーは1台でいいか~、で、Web/APサーバーに Apache Tomcat、DBはMySQLでもいいけど今回は触ったことのないMariaDBの方で!」
みたいな。
あとは言語。
言語は前に書いた通り、未経験のVueと経験のあるJavaでバランスよく学んでいこうという思いでフロントはVue、バックエンドはJava(Spring Boot)にしました。
そしてエディタ。
私はC#とJavaの経験があり、業務で使った経験があるのはVisual StudioとEclipseです。
Visual Studioのライセンスは持ってません。
Eclipseは経験上、重くて起動時にたまにコケてたのが気になります。
よく目にするVS Codeが軽くてJavaにも使いやすそうなのでVS Codeにしました。
ソースのバージョン管理は特にしていません。
コンテナ的なものもありません。3.Javaで開発スタート
まず、ログイン画面から作り始めました。
最初にログイン画面って…あんまりおもしろくないですね。
Spring Bootの機能を利用して実装しました。次にSIerの基本、マスタメンテナンス画面。
SIerが開発を学ぶためにまず最初に作らされるのがマスメン画面というイメージ。
経験があるのでとりえず実装完了。
このときはまだ見た目に何の装飾もしていません。
ヨメに「こんなのできたんだけど」とログイン画面とマスメン画面を見せてみました。
反応は「こんなのに何時間かけてんの?」みたいな反応でした。
機能がどうのこうのというより、見た目のしょぼさのインパクトが強かったみたいです。
これは私のモチベーションが下がったぁぁ
…ということでこの時点になってVueを導入しました、私は、Vue未経験者です。(素のJavaScriptはそれなりに扱ってます)
何かライブラリになってて、ファイルをどこかに置いて参照する感じ?って思ってました。
学習用ならそんな感じでもいいかもしれませんが、Node.js、Vue CLIをインストールし、Vue CLIからプロジェクトを作成すると想像と違うものが出てきました。
単体で動かす感じ…
な私のVueレベル。
※あとで公式を見たら「初心者が vue-cliで始めることは推奨しません」って書いてある…
とりあえず見た目を何とかしたいので業務で扱ったことのあるBootstrapでも入れればいいのかなと思いましたが、調べるとElementというコンポーネントライブララリがVueとセットになってる記事をよくみかけたので、Elementを採用。
Vueの説明の最初らへんで「リアクティブ」って言葉をよく見かけましたが、その言葉に最初はピンときませんでした。
今は「動的に変わる」ってことだと思ってます。4.開発の流れ
業務では仕様書を書いてレビューしてもらって直してまたレビュー…の繰り返しですが、今回はそんな細かくやってません。
SIerとして「要件定義→外部設計→内部設計→実装→テスト色々→リリース」まで一通り経験してるので、頭の中では色々やってたかもしれません。
今回の流れとしては、機能ごとに
・画面イメージのメモ
・画面遷移図のメモ
・DBのテーブル設計のメモ
を作ったくらいです。
しかもそれらは今現在手元にありません。
紛失です。
業務の引継ぎ時は「仕様書はありません。ソース見てください」って伝える必要があります。
画面イメージや画面遷移が変わってもだいたい機能内で閉じてたので影響は少なかったのですが、テーブルの項目が変わると他の機能への影響が大きかったり色々面倒臭かったので、そっちはもう少しちゃんと考えてたほうがよかったな…と今は思ってます。5.いきなりAPI
さて、Node.jsとTomcatという二つの実行環境ができました。
今さらですがここらへんでバックエンドをREST API化するイメージが固まりました。
開発時はJavaのプロジェクトにVueのプロジェクトを突っ込んで、Node.jsとTomcat両方立ち上げてました。
本番環境ではビルドしたVueのファイルがwarファイルに含まれるようにして、全部Tomcatに乗せました。
※APIのURLの先頭を/api/… にしないとVue側とURLがかぶってしまう問題が発生したのは後のこと…6.Vue + Java(Spring Boot)で開発スタート
ログイン画面とマスメン画面をVue + Spring Boot(REST API)で作り直し。
ここら辺は探すと色々やり方が出てくるのでそこまで詰まることはなかったです。バリデーションには最初Elementを使ってましたが、サーバーサイドのエラーを扱おうとしたとき上手く使えなかったので、VeeValidateに切り替えました。
VeeValidateでなんとかやってます。実装の流れはだいたいルーティーン化していました。
画面作成(Vue)→テーブル作成(DB)→リポジトリクラス作成(Java)→サービスクラス作成(Java)→コントローラークラス作成(Java)→REST APIアクセス部分作成(Vue)→画面調整(Vue)
これを機能ごとに繰り返す感じです。ちなみに私のSIerとしての経験的に、ちょっとデザイナーと仕事をしたことがあるくらいで私にはデザインの知見がほとんどありません。なのでデザインに関しては何とも言えない仕上がりになっています。
7.その他各機能の作成
各機能の作成は経験と世にあふれてるサービスを参考にしました。
排他制御は細かくやりました…
あと、世にあふれてるサービスを参考にしなくても、チャットなんかは実装方法を教えてくれてるサイトがたくさんありました。
ただシンプルなチャットが多いので、メンバーがオンラインかとか削除機能とかアイコンの表示とかは自力で考えました。チャットのメッセージを削除する機能をあとから実装しようとしたら、テーブルの構造上消せないということが判明しました。
その時点でチャットに関するテーブルの構造を結構見直したので、影響範囲が大きかったです。
メッセージの削除はあとでいいかな~と思ってましたが、テーブルの構造はある程度そこも考慮に入れるべきだったな~と思ったりもしました。ちょくちょくヨメに見せてましたが、けっこう辛辣なことを言ってくれました。
特にデザインと操作性ですね。
そこまで言う?くらい。
まあユーザーにとって大事なのはそこなんですよね。
ヨメの身も蓋もない指摘でダメージを受けつつ良くなった部分もあるので感謝です。8.迫りくるネガティブ
開発中、ときどきあいつはやってきます。
「こんなサービス誰が使うのか…?」という蠢き。
それは大きな影となり、視界を闇で包みます。
レビュー「クソサービスですね」
レビュー「使えない」
レビュー「バグ多すぎ」
頭の中をマイナスの評価が駆け巡ります。
そして手を止めて、すべてを捨て去りたくなります。
そうやって途中で止めて放置状態になった作りかけのアプリは至る所で眠っているでしょう。こういうときは本来ユーザー目線でやりがいを取り戻すべきなのかもしれませんが、私はプログラミングの楽しさに逃げました。
で、気持ちが落ち着いてきたらまたユーザー目線に戻ります。
それの繰り返しでなんとかモチベーションを保っていました。9.ちょっと苦労したユーザー間で同期するタスク管理
チャットみたいにWebSocketでユーザー同士の画面を同期させる機能をタスク管理にもつけてみようかな~と思い、Vue.Draggable + WebSocket で実現しようと思ったのですが、一つ微妙に、いやかなり気になる現象が発生しました。
Vue.Draggable の奇妙な動きを対症療法でなんとかする
動きが微妙なのです。
結局丸2日くらい悩んで対症療法で何とかしました。10.公開準備
サーバーはAWS EC2 を利用しました。
最初は無料枠のインスタンスを利用していましたが、メモリが少なすぎてパフォーマンスに影響が出たので、有料でも少しスペックのいいものにしました。
勉強代、勉強代…
ここにDBとWeb/APサーバーをセットアップして、ビルドしたモジュールを乗せるだけ。一通り機能を作り終えたら、公開に必要なドメイン取得、HTTPS化にとりかかりました。
何か大変なんだろうな~と思っていましたが、AWSでドメイン取得、HTTPS化はあっさり完結しました。
.comドメインは年1,000円くらいかかるみたいですが…
無料より手軽さを優先しました。準備が整ったら、あとは告知するのみ。
今回は自分がアカウントを持ってるSNSで告知しました。
このボタンを押せば告知…って思うとなかなかの指プルプル状態でした。11.公開後
さほど変わりはありません。
辛辣なコメントをいただき沈むこともありますが…
利用してもらうのはなかなか難しいと思ってます。
今後、どうすべきかあまり決まっていません。
公開してからが一番大事な気がしますが。12.最後に
開発するぞ!と決めてから公開するまで大まかにこんな感じです。
技術的な要素はほとんどありませんが、ポエムということで。
やってて思ったのは、「あ~これ絶対に解決しなさそう!」って思っても長くても一週間以内にはなんとかなるということです。
意外に解決します。
ベタですがお風呂とかトイレの中で思いついたり。やってて「こんなサービス誰か使うのか…?」という疑問は常に付きまといます。
実際誰も使わない気もします。
でもやっぱり一回公開まで経験することは大事だと思います。
ダメならまた作ればいいだけですし。
私は心臓が剛毛なメンタル強者ではないですが、へこみつつも何とか前に進む気はあります。
なのでこれまで一度も公開用のアプリを作ったことの無い方も、一度作ってみてはどうでしょうか。
- 投稿日:2020-09-09T00:56:08+09:00
始めて学ぶjava #3 式と演算子
式とは?
public class Main { public static void main(String[] args) { int a; int b; a = 20; b =a+5; System.out.println(a); System.out.println(b); } }実行結果
20 256行目のようなものを式と呼ぶ。
「a」、「b」、「5」をオペランド、「+」、「*」を演算子と呼ぶ。
複雑な式であっても同じく、すべての式はこの二つで成り立っている。リテラル
オペラントの中でも「5」や「hello,world」などソースコードに記述してあることをリテラルと呼ぶ。
リテラルは(int)などのデータ型を持っている。エスケープシーケンス
¥記号とそれに続く一文字で記述する記述方法で特殊な一文字を表す。
表記 意味 ¥” 二重引用符記号 ¥ ’ 引用符記号 ¥¥ 円記号 ¥n 改行 演算子
演算子 機能 + 足し算 ー 引き算 * 掛け算 / 割り算 % 割り算の余り(余剰) + 文字列の連結 = 右辺を左辺に代入 += 左辺と右辺を加算して左辺に代入(算術演算子の通りある) ++ 値を一つ増やす ーー 値を一つ減らす
- 投稿日:2020-09-09T00:48:03+09:00
[ev3×Java] 表示、音、LEDの制御
この記事はJavaでev3を操作してみたい人のための記事です。
今回はインテリジェントブロックを使ってテキストを表示したり、音を出したり、LEDを制御していきたいと思います。目次
0 . 用意するもの
1 . 表示
2 . 音
3 . LED
最後に0.用意するもの
◯ ev3(インテリジェントブロック)
◯ パソコン(VSCode)
◯ bluetooth
◯ microSD
◯ API Documentation(これをみながら進めていくのがオススメです。)1.表示
1-0 . 表示の基礎プログラム ①
◯3秒間文字を表示するプログラムです。
display00.Javaimport lejos.utility.Delay; public class Display00 { public static void main(String[] args) { System.out.println("Hello World!!"); Delay.msDelay(3000); } }
1-1 . 表示の基礎プログラム ②
◯テキストを表示する場所やフォントを変更することができるプログラムです。
display01.javaimport lejos.hardware.lcd.Font; import lejos.hardware.lcd.GraphicsLCD; import lejos.hardware.ev3.LocalEV3; import lejos.utility.Delay; public class Display01 { public static void main(String[] args) { GraphicsLCD g = LocalEV3.get().getGraphicsLCD(); final int SW = g.getWidth(); final int SH = g.getHeight(); g.drawString("Hello World", 5, 0, 0); Delay.msDelay(3000); g.setFont(Font.getSmallFont()); g.drawString("Programming is so fantastic!!", 2, 20, 0); Delay.msDelay(3000); g.drawString("Good BYE", SW/2, SH/2, 0); Delay.msDelay(1000); } }
Point:drawStringメソッド
void drawString(java.lang.String str,int x,int y,boolean inverted)
Point:Font
Point:インターフェースGraphicsLCD
1-2 . 表示の基礎プログラム ③
◯図形などを表示するプログラムです。
display02.javaimport lejos.hardware.lcd.GraphicsLCD; import lejos.hardware.ev3.LocalEV3; import lejos.utility.Delay; public class Display02 { public static void main(String[] args) { GraphicsLCD g = LocalEV3.get().getGraphicsLCD(); final int SW = g.getWidth(); final int SH = g.getHeight(); g.drawChar('A', 30, 30, 0); g.drawRect(0,0,100,100); g.fillRect(SW/2,SH/2,50,50); Delay.msDelay(3000); } }Point:getGraphicsLCD
Get graphics access to the LCD
Returns:the graphics LCDPoint:drawRectメソッド
void drawRect(int x,int y,int width,int height)
2.音
2-0 . 音の基礎プログラム①
◯様々な音を出すプログラムです。
Sound00.javaimport lejos.hardware.Sound; public class Sound00 { public static void main(String[] args) { Sound.beep(); Sound.buzz(); Sound.systemSound(true,2); } }
Point:Soundクラスのメソッド
2-1 . 音の基礎プログラム②
◯周波数を調整して音を出すプログラムです。
Sound01.javaimport lejos.hardware.Sound; public class Sound01 { public static void main(String[] args) { Sound.playTone(262, 500);//ド Sound.playTone(294, 500);//レ Sound.playTone(330, 500);//ミ Sound.playTone(349, 500);//ファ Sound.playTone(392, 500);//ソ } }Point:音階の周波数
3.LED
3-0 . LEDの基礎プログラム
◯様々なパターンで色を表示することができます。
led00.javaimport lejos.hardware.Button; import lejos.utility.Delay; public class LED00 { public static void main(String[] args) { for (int i = 0;i < 10;i++) { System.out.println(i); Button.LEDPattern(i); Delay.msDelay(3000); } } }最後に
読んで頂きありがとうございました!!
次回はインテリジェントブロックボタンについて書いていきたいと思います!より良い記事にしていきたいので
◯こうした方がわかりやすい
◯ここがわかりにくい
◯ここが間違っている
◯ここをもっと説明して欲しい
などの御意見、御指摘のほどよろしくお願い致します。
- 投稿日:2020-09-09T00:11:10+09:00
SpringBootによるWEBアプリをWARにしてTomcatサーバーにデプロイ
前提
- AmazonLinux2(EC2)
- JDK1.8.0
- MySQL 8.0
- 構成管理はGradle 6.0
- こちらでEC2、RDSによるアプリケーションサーバー環境を構築済み。なお、当初EC2インスタンスをmicroにしていたが、メモリ不足でビルドが失敗したのでmediumにした。
- こちらでTomcatとJDKをインストール済み。今回はApacheは使わずとりあえずTomcatのみで動かす。
- プロジェクトはGitHubからcloneする。
Springを実行するクラスにwarデプロイ用記述を追加
- プロジェクトを作成したとき、自動でSpringを実行するクラスが作成されていると思う(@SpringBootApplicationアノテーションがついているクラス)。
- ここにwarとしてデプロイ可能にするための記述をする。Spring公式を参照した。
@SpringBootApplication public class Application extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application.sources(Application.class); } public static void main(String[] args) throws Exception { SpringApplication.run(Application.class, args); } }Gradleの設定とビルド
Gradleの設定
- war作成用の設定をする。
archiveName
はwarの名前となる。- SpringBoot組み込みTomcatが、EC2上のものと競合しないよう設定
- ほかに、自分の場合はSeleniedによる画面テストコードもプロジェクトに含めていたので、これを除外する設定をtest内に記述した。
plugins { id 'org.springframework.boot' version '2.2.4.RELEASE' id 'io.spring.dependency-management' version '1.0.9.RELEASE' id 'java' id 'war' } // ~省略~ war { enabled = true archiveName = 'sample.war' } dependencies { // ~省略~ providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat' } test { // ~省略~ exclude 'com/example/demo/ViewTest.class' }EC2上でビルド
- ホームディレクトリにWEBアプリのプロジェクトを
git clone
しておく。- プロジェクト内のapplication.propertiesにRDSの設定を反映させておく。
プロジェクトのapplication.propertiesがあるディレクリにて$ sudo vim application.propertiesapplication.propertiesspring.datasource.url=jdbc:mysql://RDSのエンドポイント:3306/データベース名?serverTimezone=JST spring.datasource.username=****** spring.datasource.password=*********** spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.jpa.open-in-view=true #~省略~
- gradlew buildコマンド実行。buildが成功すると、
project名/build/libs
内にsample.war
が生成される。- まだ分かっていないところがあるが、
project名-0.0.1-SNAPSHOT.war
というwarも生成されていた(sample.warと中身は同じ?)。プロジェクトのディレクトリにて$ ./gradlew build生成されたwarをTomcatへ配備
tomcat/webapps
配下へsample.war
をコピーする。※自分の場合は/opt/tomcat-9/webapps
に配備。$ cp sample.war /opt/tomcat-9/webapps/
- Tomcat再起動
$ sudo systemctl restart tomcat↓のようにアクセスするとアプリが動く。
http://EC2のエンドポイント:8080/sample/作成アプリに応じたリクエスト先