- 投稿日:2019-02-03T22:03:37+09:00
FirebaseのRealtime Databaseでクリアタイムランキングを作成する(Androidアプリ)
前回の続きです。
https://qiita.com/KIRIN3qiita/items/4573d26187b8c9e53fc6今回はRealtime Databaseの実践編としてゲームクリアタイムのランキングを作成します。
私が昔作成したお釣り支払いゲームのクリアタイムランキング画像です。
自作のしょぼいゲームアプリでも、クリアタイムランキングがあると少しは形になります。(インストール数100ぐらいですが・・・)
至高の支払い(https://play.google.com/store/apps/details?id=jp.kirin3.changegame&hl=ja)
これを応用すればスコアランキングなども作れます。ルール作成
$ Variablesを利用して、ユーザーごとのデータを保存するルールを作成します。
$ Variablesについて
---------------------------
$ 接頭辞を持つキャプチャ変数を宣言することにより、読み取りや書き込みのパスの一部をキャプチャできます。これはワイルドカードとして機能し、ルール宣言の内部で使用するために該当のキーの値を保存します
---------------------------ユーザーごとの名前、クリア時間、クリア日時、ユーザーIDを保存するルールを作成しました。
ルール{ /* Visit https://firebase.google.com/docs/database/security to learn more about security rules. */ "rules": { "info": { ".read": true, ".write": true, "$user_id": { "name": { ".validate": "newData.isString() && newData.val().length < 9" }, "time": { ".validate": "newData.isNumber()" }, "date": { ".validate": "newData.isString()" }, "user_id": { ".validate": "newData.isString()" } } } } }"$user_id"の項目の中に"user_id"があるのは気持ちが悪いですが、同じ階層のデータを引き抜くイメージなのであると便利です。
データ登録
まずデータ保存、取得に利用するクラスを作成。
Userpublic static class User { public int rankingNo; public String name; public Double time; public String date; public String userId; // 空のコンストラクタの宣言が必須 public User() { } public User(String _name, Double _time,String _date, String _userId) { name = _name; time = _time; date = _date; userId = _userId; } public void setRankingNo( int _rankingNo ){ rankingNo = _rankingNo; } public Integer getRankingNo(){ return rankingNo; } public String getName(){ return name; } public Double getTime(){ return time; } public String getDate(){ return date; } public String getUserId(){ return userId; } }データ登録// とりあえずUserIdはUUIDで作成。(その後、毎回変わらないようにプリファランスに保存するなどが必要) String sUserId = UUID.randomUUID().toString(); User user = new User( "UMEHARA",5.3,"2019-10-21 09:00:27" ,sUserId); // インスタンスの取得 FirebaseDatabase database = FirebaseDatabase.getInstance(); // ファイルパスを指定してリファレンスを取得 DatabaseReference ref = database.getReference("info"); // データを登録 ref.child(sUserId).setValue(user);データ取得
取得前に一度だけ終端データを登録しておきます。(なぜ必要かは後で説明)
終端データ登録public void SaveTerminal(){ String userId = TARMINAL; String userName = "ターミナル"; Double time = 99999.9; String date = "1999-01-01 00:00:00"; FirebaseDatabase database = FirebaseDatabase.getInstance(); DatabaseReference ref = database.getReference("info"); User user = new User( userName,time,date,userId ); ref.child(userId).setValue(user); }データ取得// ユーザーデータ配列 public static ArrayList<User> sUsers; // ランキングの最大取得数 final int OUTPUT_RANKING_NUM = 100; FirebaseDatabase database = FirebaseDatabase.getInstance(); // ファイルパスを指定してリファレンスを取得 final DatabaseReference refUser = database.getReference("info"); // クリア時間でソート、最大取得数を設定 refUser.orderByChild("time").limitToFirst(OUTPUT_RANKING_NUM + 1).addChildEventListener(new ChildEventListener() { @Override public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) { User user = dataSnapshot.getValue(User.class); // ランキング順位もオブジェクトに保存 sRankNo++; user.setRankingNo(sRankNo); // 終端データを受け取ったら、アダプターに渡し、リストビューに表示 if( dataSnapshot.getKey().equals(TARMINAL) || sRankNo == OUTPUT_RANKING_NUM + 1 ){ UserAdapter adapter = new UserAdapter(getApplicationContext(), 0, sUsers); sListView.setAdapter(adapter); } // 配列に保存 else { sUsers.add(user); } } @Override public void onChildChanged(DataSnapshot dataSnapshot, String s) { } @Override public void onChildRemoved(DataSnapshot dataSnapshot) { } @Override public void onChildMoved(DataSnapshot dataSnapshot, String s) { } @Override public void onCancelled(DatabaseError databaseError) { } });取得したデータを配列に入れて、アダプターに渡し、ListViewで表示します。
データはクリア時間でソートされ、最大100件の取得に設定しています。
データの取得完了が判断できないので終端データ(必ず最後に取得するデータ)を登録してデータの終わりを判断しています。ルール文で"$user_id"の項目の中に"user_id"があるなど、もっとよい書き方があるかもしれませんが、一応これでも動くということでご参考にして頂けたらと思います。
Realtime Databaseは癖がすごくて使いこなすのは大変ですね。
- 投稿日:2019-02-03T21:52:51+09:00
FirebaseのRealtime Databaseの設定から簡単なデータ入力まで(Androidアプリ)
概要
Firebaseのアカウントがあることが前提です。
Firebase Realtime Databaseの公式解説はこちら。
簡単に説明すると、リアルタイム更新に特化した簡易データベースで、チャットなど雑多なことに使うことが多いようです。Realtime Databaseの始め方
Android Studioの上のメニューにあるTools → Firebaseを選択。
Assistantが表示されるのでRealtime Databaseを選択。
セットアップ方法が書かれたリンクが表示されます。
スタートガイド
これを読まなくても番号付きで手順が書いてあり、
ボタンをクリックしていくだけで、以前は設定できました。
(結果としては今回は苦戦しました・・・)
Realtime Databaseの設定
①Connected your app to Firebase
[Connected to Firebase]をクリックするとGoogleアカウントの選択画面が表示され、認証を許可すると自動的にFirebaseにプロジェクトを作成、リンクしてくれます。
Firebaseにアクセスすると、プロジェクトが見えるはずです。②Add the Realtime Database to your app
[Add the Realtime Database to your app]をクリックすると以下を自動で追加してくれます。
build.gradle(project-level)// Add Firebase Gradle buildscript dependency classpath 'com.google.gms:google-services:4.0.1'app/build.gradle// Add Firebase plugin for Gradle apply plugin: 'com.google.gms.google-services' // build.gradle will include these new dependencies: compile 'com.google.firebase:firebase-database:16.0.1:15.0.0'この状態でビルドすると、なぜかコンパイルが通らない。
implementation 'com.android.support:appcompat-v7:27.1.1'
の赤線のエラー内容を見ると----------------------------------
All com.android.support libraries must use the exact same version specification (mixing versions can lead to runtime crashes). Found versions 27.1.1, 26.1.0. Examples include com.android.support:animated-vector-drawable:27.1.1 and com.android.support:support-media-compat:26.1.0 less... (Ctrl+F1)
There are some combinations of libraries, or tools and libraries, that are incompatible, or can lead to bugs. One such incompatibility is compiling with a version of the Android support libraries that is not the latest version (or in particular, a version lower than your targetSdkVersion). Issue id: GradleCompatible
----------------------------------
implementation 'com.android.support:appcompat-v7:27.1.1'
と
implementation 'com.google.firebase:firebase-database:16.0.1:15.0.0'
の内部で別バージョンの同一ライブラリが見つかったとのこと。firebase-databaseのほうが古いライブラリを含んでいるので最新を探し入れてみても、同じ結果でした。
(参照)Firebase Android Release Note
Realtime Database com.google.firebase:firebase-database:16.0.3仕方がなくappcompatのほうを切り替え。
implementation 'com.android.support:appcompat-v7:27.1.1'
↓
implementation 'com.android.support:appcompat-v7:26.1.0'
compileSdkVersionとtargetSdkVersionを26に変更。そうすると次のエラー
----------------------------------
Could not find firebase-database-15.0.0.jar (com.google.firebase:firebase-database:16.0.1).
----------------------------------
こんなjava Archiveはないとのこと、自動に追加しておいてどういうことかと思いながらも、
スタートガイド
を読むとapp/build.gradleimplementation 'com.google.firebase:firebase-database:16.0.3'と、最新のにしてとのこと。
そうすると
app/build.gradleimplementation 'com.google.firebase:firebase-core:16.0.4'も入れてとWarningが出るので従う。
ここら辺の内部ライブラリバージョンが違う問題はもうすぐ改善されるということをGoogleの開発者ブログに書いてあったような。③Configure Firebase Database Rules
認証についてのリンクとDBの作成ルールのリンクがあります。
Firebase Authentication
データベース ルールを使ってみる認証については、ユーザーの個人情報など機密性の高いものにする場合に必要です。
簡易的なチャットやゲームランキングなどでは使う必要はないと思います。(たぶん)では、ブラウザでFirebaseのコンソールにアクセスして、左タブのDatabaseをクリック。
データベースの作成をクリック。
テストモードで作成。
するとタブがCloud Firestore[ベータ版]になっているので、
Realtime Databaseにしてください。
とにかくDBのルール作成の癖がすごいので、作成が難しくなっています。
リレーショナルDBのような設計はできません。
細かいルールについては以下のリンクを参照してください。
〇Androidについて
・ガイド
スタートガイド
データベースの構造化
Android でのデータの読み取りと書き込み
Android 上でのデータのリストの操作
Android でのオフライン機能の有効化・リファレンス
パッケージサマリー〇セキュリティとルールについて
・ガイド
Firebase Realtime Database ルールについて
データベース ルールを使ってみる
データのセキュリティ保護
ユーザーベースのセキュリティ
非安全性を解決する
データのインデックス作成
REST で Firebase Realtime Database ルールを管理する・リファレンス
Firebase Database Security Rules API
Firebase Security Rules Regular Expressions
Firebase Security Rules for Cloud Storage Reference
とりあえず簡単なルールを作成
ルール{ /* Visit https://firebase.google.com/docs/database/security to learn more about security rules. */ "rules": { "info":{ "user": { ".read": true, ".write": true, "name": { ".validate": "newData.isString()" }, "age": { ".validate": "newData.isNumber()" } } } } }infoの下にuser、その下にname(String型),age(Number型)を作成しただけのルールです。
階層を深くしたのは、パスの指定ルールをわかりやすくするためです。④Write to your database
早速データを書き込んでみます。
Android でのデータの読み取りと書き込み
がわかりやすいです。
データを個別に登録することも可能ですが、かなり面倒な作りになるので、クラス単位での登録、取得のほうが楽です。〇クラスの作成
(注意)そのオブジェクトを定義するクラスに、引数を取らないデフォルト コンストラクタと、代入するプロパティのパブリック ゲッターが存在することが条件です。Userpublic static class User { public String name; public Integer age; // 空のコンストラクタの宣言が必須 public User() { } public User(String _name, Integer _age) { name = _name; age = _age; } public String getName(){ return name; } public Integer getAge(){ return age; } }〇データ登録
registerUser user = new User( "山田太郎",30 ); // インスタンスの取得 FirebaseDatabase database = FirebaseDatabase.getInstance(); // ファイルパスを指定してリファレンスを取得 DatabaseReference refName = database.getReference("info/user"); // データを登録 refName.setValue(user);しっかり登録されてますね。
⑤Read from your database
データの読み取りを行います。
Android でのデータの読み取りと書き込み
よりも
ガイド→管理→データを取得する
のほうがわかりやすいかもしれません。
ドキュメントの例文がとっ散らかっていてわかりづらい・・・簡単に書くとイベントリスナーをセットすることで、全取得や更新取得などができるということです。
とりあえず全取得だけ記載。〇データ取得
reader// インスタンスの取得 FirebaseDatabase database = FirebaseDatabase.getInstance(); // ファイルパスを指定してリファレンスを取得 final DatabaseReference refUser = database.getReference("info"); refUser.addChildEventListener(new ChildEventListener() { @Override public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) { User user = dataSnapshot.getValue(User.class); Log.w( "DEBUG_DATA", "user.name = " + user.name); Log.w( "DEBUG_DATA", "user.age = " + user.age); } @Override public void onChildChanged(DataSnapshot dataSnapshot, String s) { } @Override public void onChildRemoved(DataSnapshot dataSnapshot) { } @Override public void onChildMoved(DataSnapshot dataSnapshot, String s) { } @Override public void onCancelled(DatabaseError databaseError) { } });⑥Optional: Configure ProGuard
プロガードを設定する場合は気を付けてねって内容。
使ってないので割愛・・・When using Firebase Realtime Database in your app along with ProGuard you need to consider how your model objects will be serialized and deserialized after obfuscation. If you use DataSnapshot.getValue(Class) or DatabaseReference.setValue(Object) to read and write data you will need to add rules to the proguard-rules.pro file:
-----
# Add this global rule
-keepattributes Signature# This rule will properly ProGuard all the model classes in
# the package com.yourcompany.models. Modify to fit the structure
# of your app.
-keepclassmembers class com.yourcompany.models.** {
*;
}
-----⑦Preapre for Launch
チェックリストでチェックしてねという内容。
大したことはない。⑧Next Steps
複数のデータベースを利用することもできますよ、という内容。
複数のデータベースでスケールする
メルカリは相当な数のデータベースを結び付けて膨大な量のチャットを実現していると聞いたことがあるような。
本当にサーバーレスな時代ですね。基本編だけで長くなってしまいました。
このままではただ単に簡単なデータを入力、取得しただけになっていて、使い道がなさそうです。
応用編は後程。
- 投稿日:2019-02-03T19:42:48+09:00
SpringBoot+インメモリデータグリッド入門 (データ永続化)
前回やったこと
前回はSpringDataGeodeを使って、インメモリデータグリッドを使ったSpringBootアプリケーションを作成し、データがサーバに同期されたタイミングでイベント処理が行えるように実装を行いました。
・SpringDataGeodeを使用したアプリケーション作成
今回やること
インメモリデータグリッドのキャッシュに登録されたデータをディスクに永続化する
今回はSpringDataGeodeの機能を使い、インメモリキャッシュに登録されたデータをローカルのディスクに永続化し、更にキャッシュ組み込みアプリサーバの起動時に、永続化ファイルをロードしてキャッシュにデータを登録する実装をやります。
調べてみたところ、アノテーション一つ追加してやるだけで実現できそう。
SpringBootApplicationに以下のようにアノテーションを追加。ServerGeodeApplication.javapackage spring.geode.server.geodeServer; import org.apache.geode.cache.GemFireCache; import org.apache.geode.cache.Region; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.gemfire.ReplicatedRegionFactoryBean; import org.springframework.data.gemfire.config.annotation.EnableDiskStore; import org.springframework.data.gemfire.config.annotation.EnableDiskStore.DiskDirectory; import org.springframework.data.gemfire.config.annotation.EnableLocator; import org.springframework.data.gemfire.config.annotation.EnableManager; import org.springframework.data.gemfire.config.annotation.PeerCacheApplication; import org.springframework.data.gemfire.repository.config.EnableGemfireRepositories; import spring.geode.geodeCommon.model.User; import spring.geode.geodeCommon.region.UserRegion; import spring.geode.server.geodeServer.repository.UserRepository; @SpringBootApplication @PeerCacheApplication(name = "SpringGeodeServerApplication", locators = "localhost[40404]") @EnableGemfireRepositories(basePackageClasses = UserRepository.class) // 今回追加したデータ永続化アノテーション @EnableDiskStore( name="SimpleDiskStore", autoCompact=true, diskDirectories = @DiskDirectory(location="/hoge/fuga/SpringGeodeData") ) public class GeodeServerApplication { public static void main(String[] args) { SpringApplication.run(GeodeServerApplication.class, args); } @Configuration @EnableLocator(port = 40404) @EnableManager(start = true) static class LocatorManagerConfiguration { } @Configuration static class CacheInitializer { @Bean Region<Integer, User> userRegion(final GemFireCache cache) { return new UserRegion().createUserRegion(cache); } @Bean public ReplicatedRegionFactoryBean<Integer, User> replicatedRegion(GemFireCache cache) { return new UserRegion().createUserRegionFactory(cache); } } }
@EnableDiskStoreを有効にすることによってdiskDirectoriesで指定した絶対パスの位置に永続化ファイルが吐き出される。と、思いきや何故だかファイルが出力されず。。
アノテーションで解決する方法を模索しましたが、うまくいかず。。仕方がないので、
Beanに直接設定することにしました。。UserRegion.javapackage spring.geode.geodeCommon.region; import java.io.File; import org.apache.geode.cache.GemFireCache; import org.apache.geode.cache.Region; import org.springframework.data.gemfire.ReplicatedRegionFactoryBean; import spring.geode.geodeCommon.listener.UserRegionListener; import spring.geode.geodeCommon.model.User; /** * ユーザを管理する{@link Region}の設定を作成する * */ public class UserRegion { /** * {@link Region}の作成を行う * @param cache * @return */ public Region<Integer, User> createUserRegion(final GemFireCache cache) { return cache.<Integer, User>getRegion("Users"); } /** * {@link Region}に対する設定を行う * @param cache * @return */ public ReplicatedRegionFactoryBean<Integer, User> createUserRegionFactory(GemFireCache cache) { ReplicatedRegionFactoryBean<Integer, User> replicatedRegionFactory = new ReplicatedRegionFactoryBean<>(); UserRegionListener[] listeners = { new UserRegionListener() }; listeners[0] = new UserRegionListener(); replicatedRegionFactory.setCacheListeners(listeners); replicatedRegionFactory.setClose(false); replicatedRegionFactory.setCache(cache); replicatedRegionFactory.setRegionName("Users"); replicatedRegionFactory.setPersistent(true); return replicatedRegionFactory; } /** * {@link Region}に対するファイル永続化設定を行う * @param cache * @param regionFactory * @return */ public ReplicatedRegionFactoryBean<Integer, User> configDiskStore(GemFireCache cache, ReplicatedRegionFactoryBean<Integer, User> regionFactory) { File[] files = { new File("/hoge/fuga/SpringGeode/persistenceFile") }; cache.createDiskStoreFactory()// .setAllowForceCompaction(true)// .setAutoCompact(true)// .setDiskDirs(files)// .create("SimpleDiskStore"); regionFactory.setDiskStoreName("SimpleDiskStore"); return regionFactory; } }
configDiskStoreメソッドで永続化設定を行っています。
Cache内にDiskStoreを作成し、各種永続化方法に関する設定、永続化ファイルパスを設定しています。このメソッドをアプリケーション内で呼び出し、Bean登録してやることでやっと永続化が完了しました。
GeodeServerApplication.javapackage spring.geode.server.geodeServer; import org.apache.geode.cache.GemFireCache; import org.apache.geode.cache.Region; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.gemfire.ReplicatedRegionFactoryBean; import org.springframework.data.gemfire.config.annotation.EnableLocator; import org.springframework.data.gemfire.config.annotation.EnableManager; import org.springframework.data.gemfire.config.annotation.PeerCacheApplication; import org.springframework.data.gemfire.repository.config.EnableGemfireRepositories; import spring.geode.geodeCommon.model.User; import spring.geode.geodeCommon.region.UserRegion; import spring.geode.server.geodeServer.repository.UserRepository; @SpringBootApplication @PeerCacheApplication(name = "SpringGeodeServerApplication", locators = "localhost[40404]") @EnableGemfireRepositories(basePackageClasses = UserRepository.class) public class GeodeServerApplication { public static void main(String[] args) { SpringApplication.run(GeodeServerApplication.class, args); } @Configuration @EnableLocator(port = 40404) @EnableManager(start = true) static class LocatorManagerConfiguration { } @Configuration static class CacheInitializer { @Bean Region<Integer, User> userRegion(final GemFireCache cache) { return new UserRegion().createUserRegion(cache); } @Bean public ReplicatedRegionFactoryBean<Integer, User> replicatedRegion(GemFireCache cache) { UserRegion region = new UserRegion(); return region.configDiskStore(cache, region.createUserRegionFactory(cache)); } } }これで
Regionごとに永続化設定を行うことができ、アプリ起動時にはDiskStoreに設定したファイルからデータをロードしてキャッシュに登録することができるようになりました。アノテーションでサクッと実装したかったのですが、うまくいかなかったのが悔やまれる。。
ReplicatedRegionFactoryBeanを自前でカスタムしているのがいけないのか。。何はともあれ永続化はできたので良しとする。
RDBを永続化対象とした設定もできれば嬉しいけど、そのような機能を
SpringDataGeodeが提供してくれるという情報を得られなかったので、一旦ファイル永続化機能を使用して、永続化をやってみました。
- 投稿日:2019-02-03T17:55:10+09:00
Apache POIを使ってExcelを操作
概要
本記事は、Excelに対して何らかの機械的な操作を行いたい、でもマクロを書くのはめんどくさいという場合の選択肢としてJavaのApache POIを使う際の非常にベーシックな実装方法を紹介します。
システムの開発の現場では、多くの設計書がExcelで書かれていることがあり、一度に大量のテーブル定義書や画面項目定義を確認する際に困るシチュエーション等があるかと思います。
そんな時にVBA(マクロ)以外の方法として、普段慣れ親しんでいるJavaで簡単にツールを作れるのが、「Apache POI」のライブラリとなります。Apache POIとは
Apache POI(アパッチ・ポイまたはピーオーアイ)はApacheソフトウェア財団のプロジェクトで、WordやExcelといったMicrosoft Office形式のファイルを読み書きできる100% Javaライブラリとして提供されている。
https://ja.wikipedia.org/wiki/Apache_POI初期設定
Mavenのプロジェクトを作成しpom.xmlファイルのdependencyに「poi」と「poi-ooxml」を追加します。(下記を参照)
「poi-ooxml」も追加しておくことでOOXML形式のファイルも使用できます。つまり、POIを利用して、拡張子が「xlsx」「docx」といった2007形式のファイルの読み書きが可能となります。pom.xml<dependencies> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>[バージョンを指定]</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>[バージョンを指定]</version> </dependency> </dependencies>最新だと、バージョンは"4.0.1"のようです。(2019/2/1時点)
https://mvnrepository.com/artifact/org.apache.poi/poi/4.0.1
https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml/4.0.1基本実装
Excelファイルの読み取り
ファイルを開く
Workbook workbook = WorkbookFactory.create(new File("ファイルパス"));シートを開く
// シート名がわかっている場合 Sheet sheet = workbook.getSheet("シート名"); // 取得したいシートが何番目かわかっている場合 // シート番号はゼロベース Sheet sheet = workbook.getSheetAt(0); // 全シートを繰り返し処理する場合 Iterator<Sheet> sheets = workbook.sheetIterator(); while(sheets.hasNext()) { Sheet sheet = sheets.next(); } // シート名を取得 String sheetName = inputSheet.getSheetName();セルの値を取得
// 行を取得 // 行番号はゼロベース Row row = sheet.getRow("行番号"); // セルを取得 // 列番号はゼロベース Cell cell = row.getCell("列番号"); // セルの型を取得 int cellType = cell.getCellType(); // 型に応じたgetterで値を取得 // String cell.getStringCellValue(); // Boolean cell.getBooleanCellValue() // Formula cell.getCellFormula(); // Numeric cell.getNumericCellValue(); // etcExcelファイルの作成、出力
ファイルを作成
// Excel2003までのファイルフォーマット Workbook outputWorkbook = new HSSFWorkbook(); // Excel2007におけるOOXML(Office Open XML)形式のファイルフォーマット Workbook outputWorkbook = new XSSFWorkbook();シートを作成
Sheet outputSheet = outputWorkbook.createSheet();セルに値を設定
// 行を作成 // 行番号はゼロベース Row outputRow = outputSheet.createRow("行番号"); // セルを作成 // 列番号はゼロベース Cell outputCell = outputRow.createCell("列番号"); // セルに値を設定 outputCell.setCellValue("設定したい値");ファイルへの出力
// 出力用のストリームを用意 FileOutputStream out = new FileOutputStream("出力ファイルパス"); // ファイルへ出力 outputWorkbook.write(out);参考
今回、個人的に作ったツールは以下となります。
かなり雑な実装ですが、大量のExcelファイルから必要な部分を切り取ってを一つのファイルにマージするという処理を行なっています。
https://github.com/yhayashi30/ExcelMergeTool
- 投稿日:2019-02-03T17:48:26+09:00
シンプルなJavaアプリを実行するdockerイメージを作成
やること
Dockerについて理解するために、非常に簡易なアプリケーションのdockerイメージを作って動かしてみる。
具体的には、"Hello world!" を1秒に1回出すようなアプリケーションを動作させる。
ビルド〜実行まですべてコンテナ上で行うことを目指す。Javaのソースコードは以下のよう。
https://github.com/nannany/very-simple-application環境
Windows10 HOME 上で実行した。
Windows上にDockerの動作環境を作成するにあたっては、Docker Toolboxを使用した。
詳細は以下の記事参照。
https://qiita.com/idani/items/fb7681d79eeb48c05144dockerイメージを作る流れ
Dockerfileを書く→docker buildコマンドを実行
でdockerイメージは作成される。dockerイメージ作成のざっくりとした流れは以下の図のような感じ。
意識すべき登場人物としては、
- 自身のローカル端末
- ビルドコンテキスト
- dockerイメージ
dockerビルドコマンド実行時に、どのパス配下のファイルをビルドコンテキストに追加するかを決める。
このとき、ビルドコンテキストに持っていきたくないファイルは.dockerignoreファイルに記述する。また、Dockerfile内のCOPY命令で、ビルドコンテキスト内の何をイメージに持っていくか決める。
使用するDockerfile
全体としては以下のよう。
FROM ubuntu:disco COPY . . RUN apt-get update && apt-get install -y \ maven \ openjdk-8-jre \ && cd simple \ && mvn package CMD ["java","-jar","simple/target/simple-1.0-SNAPSHOT.jar"]まずはベースイメージを選ぶために、
FROMを記述する。
ここでは、適当にubuntu:discoを選択する。次に、ビルドコンテキストからイメージにファイルをコピーするために、
COPY . .と記述する。その次に、ソースのビルド、Javaの実行に必要なパッケージ(mavenとopenjdk)をインストールし、mavenのjar作成コマンドを実行する。
RUN apt-get update && apt-get install -y \ maven \ openjdk-8-jre \ && cd simple \ && mvn package書き方は下記をまねて、レイヤの数の最小化、apt-get updateとinstallを同時にやることを意識した。
http://docs.docker.jp/engine/articles/dockerfile_best-practice.html最後に、コンテナが起動した後に
java -jar simple/target/simple-1.0-SNAPSHOT.jarが実行されるように、以下のように記述した。CMD ["java","-jar","simple/target/simple-1.0-SNAPSHOT.jar"]ビルド時に実行するdockerコマンド
イメージを作成する際に実行するdockerコマンドは、
docker build -t simple-application -f Dockerfile.cmd .
-t simple-applicationにて、イメージの名称をsimple-applicationにしている。
-f Dockerfile.cmdにて、イメージの作成に際して使用するDockerfileを、上記のコマンドを実行しているパスにあるDockerfile.cmdとしている。(デフォルトは、コマンドを実行しているパスにあるDockerfileが選択される)
最後の.は、コマンドを実行しているパス配下がビルドコンテキストに追加されますよ、ということを意味している。動かす
上記で作成したイメージを、以下のコマンドで動作させてみる。
docker run simple-application-cmd以下のように表示され、うまくいった。
maven入りのイメージ
上では
ubuntu:discoをベースイメージに指定して、RUNでmavenとJavaをイメージにインストールした。
しかし、もともとmavenとJavaが入っているベースイメージが存在しているので、それを使用したDockerfileが以下。(なぜかテストでエラったのでそこはとばした)FROM maven:3-jdk-8 COPY . . RUN cd simple && mvn package -Dmaven.test.skip=true CMD ["java","-jar","simple/target/simple-1.0-SNAPSHOT.jar"]
- 投稿日:2019-02-03T14:33:51+09:00
【Java】遷移元のURLを取得する方法
はじめに
遷移元のURLを取得する方法をメモとして記録しておきます。
今回はログイン機能を作る中で、ログインに成功したら、ログイン前に開いていたページ(ログインボタンを押したページ)に遷移するために利用しました。以下のような流れです。
ログイン前に開いていたページ (jsp)
↓
Login.java (ログイン前に開いていたページのURLを取得し、ログインフォームに遷移)
↓
login.jsp (ログインフォーム)
↓
LoginCheck.java (ログインできるか判定、ログイン前に開いていたページのURLを取り出す)
↓
ログイン前に開いていたページ (ログインに成功したらこのページに戻る)取得する方法
結論としては、getHeaderメソッドを使って、リクエストヘッダーのRefererを取得します。
//リクエストのヘッダー情報の遷移元URLを取得 request.getHeader("REFERER");今回はセッションを使って、取得したURLを保持し、ログインに成功したらそれを取り出して遷移します。
Login.java//リクエストのヘッダー情報の遷移元URLを取得しセッションに格納 session.setAttribute("referer", request.getHeader("REFERER")); request.getRequestDispatcher("/login.jsp").forward(request, response);LoginCheck.java//ログインに成功したら、セッションから遷移元URLを取り出して遷移 String url = (String)session.getAttribute("referer"); request.getRequestDispatcher(response.encodeURL(url).substring(29)).forward(request, response);手こずったところ
LoginCheck.javaからログイン前のページに遷移しようとすると、404のHTTPエラーが出てしまい苦戦しました。
理由としては、refererで取得したURLは、URLの全体であるため不要な部分も指定してフォワードしようとしていたためです。
対処法は単純で、substringを使って必要箇所を切り出せばOKです。例えば
http://localhost:8080/sample/top.jsp「
http://localhost:8080/sample」の部分はいらないのでURLに対してsubstring(29)としてあげれば、/top.jspのみ切り出せます。
- 投稿日:2019-02-03T05:00:29+09:00
SpringBoot+インメモリデータグリッド入門 (イベント処理)
前回やったこと
前回はSpringDataGeodeを使って、インメモリデータグリッドを使ったSpringBootアプリケーションを作成し、SpringDataを使用して永続化したデータが二つのアプリケーションで共有されていることを確認しました。
SpringDataGeodeを使用したアプリケーション作成
今回やること
インメモリデータグリッドを使ったイベント処理
今回はインメモリデータグリッドの仕組みを使ったイベント処理を実装しようと
思います。
- まずは
Regionの変更を検知するリスナーを実装UserRegionListener.javapackage spring.geode.geodeCommon.listener; import java.time.LocalDateTime; import org.apache.geode.cache.CacheListener; import org.apache.geode.cache.EntryEvent; import org.apache.geode.cache.RegionEvent; import spring.geode.geodeCommon.model.User; import spring.geode.geodeCommon.region.UserRegion; /** * {@link UserRegion}の変更を検知するリスナー * */ public class UserRegionListener implements CacheListener<Integer,User> { public void afterCreate(EntryEvent<Integer,User> event) { System.out.println(LocalDateTime.now()); System.out.println("afterCreate!!!!!!!!!" + event.getNewValue()); } public void afterDestroy(EntryEvent<Integer, User> event) { System.out.println("afterDestroy!!!!!!!!!" + event); } public void afterInvalidate(EntryEvent<Integer, User> event) { System.out.println("afterInvalidate!!!!!!!!!" + event); } public void afterRegionDestroy(RegionEvent<Integer, User> event) { System.out.println("afterRegionDestroy!!!!!!!!!" + event); } public void afterRegionCreate(RegionEvent<Integer, User> event) { System.out.println("afterRegionCreate!!!!!!!!!" + event); } public void afterRegionInvalidate(RegionEvent<Integer, User> event) { System.out.println("afterRegionInvalidate!!!!!!!!!" + event); } public void afterUpdate(EntryEvent<Integer, User> event) { System.out.println("afterUpdate!!!!!!!!!" + event); } public void afterRegionClear(RegionEvent<Integer, User> event) { System.out.println("afterRegionClear!!!!!!!!!" + event); } public void afterRegionLive(RegionEvent<Integer, User> event) { System.out.println("afterRegionLive!!!!!!!!!" + event); } public void close() { System.out.println("close!!!!!!!!!"); } }
Regionに対するリスナーを作成するには、CacheListener<K,V>を継承したクラスを作成します。
Key,Valueにはリスナー登録したいRegionのKey,Valueを設定します。今回検証したのは以下の二つ。
・Region作成イベント処理:afterRegionCreateメソッドの処理が実行される
・User新規登録イベント処理:afterCreateメソッドの処理が実行される
- 次に、リスナーを
Regionに登録する実装UserRegion.javapackage spring.geode.geodeCommon.region; import org.apache.geode.cache.GemFireCache; import org.apache.geode.cache.Region; import org.springframework.data.gemfire.ReplicatedRegionFactoryBean; import spring.geode.geodeCommon.listener.UserRegionListener; import spring.geode.geodeCommon.model.User; /** * ユーザを管理する{@link Region}の設定を作成する * */ public class UserRegion { public Region<Integer, User> createUserRegion(final GemFireCache cache) { return cache.<Integer, User>getRegion("Users"); } public ReplicatedRegionFactoryBean<Integer, User> createUserRegionFactory(GemFireCache cache) { ReplicatedRegionFactoryBean<Integer, User> replicatedRegionFactory = new ReplicatedRegionFactoryBean<>(); UserRegionListener[] listeners = {new UserRegionListener()}; listeners[0] = new UserRegionListener(); replicatedRegionFactory.setCacheListeners(listeners); replicatedRegionFactory.setClose(false); replicatedRegionFactory.setCache(cache); replicatedRegionFactory.setRegionName("Users"); replicatedRegionFactory.setPersistent(false); return replicatedRegionFactory; } }
createUserRegionメソッドでUserを管理するRegionを作成して、ApplicationContext内にBean登録する。
createUserRegionFactoryメソッドでRegionへの設定を行います。
上で作ったリスナーをRegionに登録する処理もここで行います。
- 上で実装した
Regionや設定をアプリに設定する実装GeodeClientApplication.javapackage spring.geode.client.geodeClient; import org.apache.geode.cache.GemFireCache; import org.apache.geode.cache.Region; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.gemfire.ReplicatedRegionFactoryBean; import org.springframework.data.gemfire.config.annotation.PeerCacheApplication; import org.springframework.data.gemfire.repository.config.EnableGemfireRepositories; import spring.geode.client.geodeClient.repository.UserRepository; import spring.geode.geodeCommon.model.User; import spring.geode.geodeCommon.region.UserRegion; @SpringBootApplication @PeerCacheApplication(name = "SpringGeodeClientApplication",locators = "localhost[40404]") @EnableGemfireRepositories(basePackageClasses = UserRepository.class) public class GeodeClientApplication { public static void main(String[] args) { SpringApplication.run(GeodeClientApplication.class, args); } @Configuration static class CacheInitializer { @Bean Region<Integer, User> userRegion(final GemFireCache cache) { return new UserRegion().createUserRegion(cache); } @Bean public ReplicatedRegionFactoryBean<Integer, User> replicatedRegion(GemFireCache cache) { return new UserRegion().createUserRegionFactory(cache); } } }GeodeServerApplication.javapackage spring.geode.server.geodeServer; import org.apache.geode.cache.GemFireCache; import org.apache.geode.cache.Region; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.gemfire.ReplicatedRegionFactoryBean; import org.springframework.data.gemfire.config.annotation.EnableEntityDefinedRegions; import org.springframework.data.gemfire.config.annotation.EnableLocator; import org.springframework.data.gemfire.config.annotation.EnableManager; import org.springframework.data.gemfire.config.annotation.PeerCacheApplication; import org.springframework.data.gemfire.repository.config.EnableGemfireRepositories; import spring.geode.geodeCommon.model.User; import spring.geode.geodeCommon.region.UserRegion; import spring.geode.server.geodeServer.repository.UserRepository; @SpringBootApplication @PeerCacheApplication(name = "SpringGeodeServerApplication", locators = "localhost[40404]") @EnableGemfireRepositories(basePackageClasses = UserRepository.class) public class GeodeServerApplication { public static void main(String[] args) { SpringApplication.run(GeodeServerApplication.class, args); } @Configuration @EnableLocator(port = 40404) @EnableManager(start = true) static class LocatorManagerConfiguration { } @Configuration static class CacheInitializer { @Bean Region<Integer, User> userRegion(final GemFireCache cache) { return new UserRegion().createUserRegion(cache); } @Bean public ReplicatedRegionFactoryBean<Integer, User> replicatedRegion(GemFireCache cache) { return new UserRegion().createUserRegionFactory(cache); } } }
CacheInitializerクラス内の処理でRegionやRegionへの設定を反映させています。
これをキャッシュ組み込みサーバとして起動するアプリで実装します。
クラス名にClientやServerと名付けていますが、今回はP2P形式で起動するので、@PeerCacheApplicationアノテーションをクラスに付与しています。
クラス名は気にせず。。以上でイベント処理の実装は完了です。
locatorを起動する処理をGeodeServerApplication.javaに実装しているので、GeodeServerApplication.javaから起動して、動作確認。GeodeServerApplication.java起動時ログ抜粋[info 2019/02/03 02:56:32.334 JST <main> tid=0x1] Initializing region Users [info 2019/02/03 02:56:32.334 JST <main> tid=0x1] Initialization of region Users completed afterRegionCreate!!!!!!!!!RegionEventImpl[region=org.apache.geode.internal.cache.DistributedRegion[path='/Users';scope=DISTRIBUTED_NO_ACK';dataPolicy=REPLICATE; concurrencyChecksEnabled];op=REGION_CREATE;isReinitializing=false;callbackArg=null;originRemote=false;originMember=192.168.11.3(SpringGeodeServerApplication:5899)<ec><v0>:1024;tag=null] [info 2019/02/03 02:56:32.705 JST <main> tid=0x1] Initializing ExecutorService 'applicationTaskExecutor' org.apache.coyote.AbstractProtocol start 情報: Starting ProtocolHandler ["http-nio-9090"] [info 2019/02/03 02:56:32.877 JST <main> tid=0x1] Tomcat started on port(s): 9090 (http) with context path '' [info 2019/02/03 02:56:32.880 JST <main> tid=0x1] Started GeodeServerApplication in 3.66 seconds (JVM running for 5.179)GeodeClientApplication.java起動時ログ抜粋[info 2019/02/03 02:56:49.107 JST <main> tid=0x1] Initializing region Users [info 2019/02/03 02:56:49.113 JST <main> tid=0x1] Region Users requesting initial image from 192.168.11.3(SpringGeodeServerApplication:5899)<ec><v0>:1024 [info 2019/02/03 02:56:49.116 JST <main> tid=0x1] Users is done getting image from 192.168.11.3(SpringGeodeServerApplication:5899)<ec><v0>:1024. isDeltaGII is false [info 2019/02/03 02:56:49.116 JST <main> tid=0x1] Initialization of region Users completed afterRegionCreate!!!!!!!!!RegionEventImpl[region=org.apache.geode.internal.cache.DistributedRegion[path='/Users';scope=DISTRIBUTED_NO_ACK';dataPolicy=REPLICATE; concurrencyChecksEnabled];op=REGION_CREATE;isReinitializing=false;callbackArg=null;originRemote=false;originMember=192.168.11.3(SpringGeodeClientApplication:5901)<v1>:1025;tag=null] [info 2019/02/03 02:56:49.510 JST <main> tid=0x1] Initializing ExecutorService 'applicationTaskExecutor' org.apache.coyote.AbstractProtocol start 情報: Starting ProtocolHandler ["http-nio-9000"] [info 2019/02/03 02:56:49.919 JST <main> tid=0x1] Tomcat started on port(s): 9000 (http) with context path '' [info 2019/02/03 02:56:49.922 JST <main> tid=0x1] Started GeodeClientApplication in 4.245 seconds (JVM running for 5.513)二つのアプリの起動ログで上記のログが確認できたらリージョン作成イベント処理はOKです。
Region Users requesting initial image from 192.168.11.3(SpringGeodeServerApplication:5899)<ec><v0>:1024ちなみに上のログで、
GeodeClientApplicationのUsersというRegionはGeodeServerApplicationで作成したRegionのイメージを使ってRegionを初期化しているみたい。次は新規ユーザデータが
GeodeClientApplcaition側のRegionに登録され、GeodeServerApplcaitionにデータが同期されたことを契機にリスナーの処理が実行されるかの確認。GeodeClientApplicationへのユーザ登録リクエストcurl -X POST -H "Content-Type: application/json" -d '{"name":"Michel", "age":"100"}' localhost:9000/register/userリクエスト送信後の
GeodeServerApplcaitionでのログGeodeServerApplication.javaのログ抜粋[info 2019/02/03 04:48:30.587 JST <pool-3-thread-1> tid=0x5b] Initialization of region _monitoringRegion_192.168.11.3<v1>1025 completed [info 2019/02/03 04:48:30.593 JST <pool-3-thread-1> tid=0x5b] Initializing region _notificationRegion_192.168.11.3<v1>1025 [info 2019/02/03 04:48:30.595 JST <pool-3-thread-1> tid=0x5b] Initialization of region _notificationRegion_192.168.11.3<v1>1025 completed 2019-02-03T04:48:46.176823 afterCreate!!!!!!!!!User(id=-1816523715, name=Michel, age=100)期待値通り、
GeodeServerApplicationでイベント処理メソッドが発火しているようです。おまけ
「
GeodeClientApplicationでデータを永続化した時点」と「GeodeServerApplicationでイベント処理が実行された時点」との差をとって、あるキャッシュサーバでデータが永続化されてから、別のキャッシュサーバでデータ同期イベントが実行されるまでのレイテンシを測ってみました。上の動作確認で行ったことをfor文で10回繰り返し、レイテンシの平均値をとってみたところ結果は
0.002msと非常に高速でした。
試行回数が少ないのであまり参考にはならないかもしれませんが、個人的には満足。次は永続化について実装してみようと思います。
- 投稿日:2019-02-03T01:03:13+09:00
【Java初心者】 多次元配列の初期化について
java Silverの取得に向けて学習をしていたところ、
多次元配列の初期化の箇所で詰まってしまったのでメモとして残しておきます。何が分からなかったか
String[][] array = {{"a","b"},{"c","d","e"},{"f","g","h","i"}};配列宣言時に
[][]と書いているのに{}{}{}と括弧の数が対応していないことで混乱し、
何故この配列のコンパイルが通るのかが理解ができなかった...どう読み取ればよいか
単純な話でした。
String[][] array = {{"array[0][]"},{"array[1][]"},{"array[2][0]"}};3つの{}は1次元目の配列の添え字部分に対応しており、
{}内で記述しているa~iの値は1次元目の配列が参照している2次元目の配列の値であるということでした。public class Main{ public static void main(String[] args) { String array[][] = {{"a","b"},{"c","d","e"},{"f","g","h","i"}}; System.out.println(array[0][0]); //a System.out.println(array[0][1]); //b System.out.println(array[1][0]); //c System.out.println(array[1][1]); //d System.out.println(array[1][2]); //e System.out.println(array[2][0]); //f System.out.println(array[2][1]); //g System.out.println(array[2][2]); //h } }実際に各要素をprintlnメソッドで出力してみると、期待した通りの結果が返ってきました。
ちなみに実際に配列の各要素を出力する際は、
public class Main { public static void main(String[] args) { String array[][] = { { "a", "b" }, { "c", "d", "e" }, { "f", "g", "h", "i" } }; for(String[] tmp : array) { for(String s : tmp) { System.out.println(s); } } } }このようにfor文を用いて配列を回す方法の方が一般的。
3次元目以降についても可読性めちゃ悪いですが、ネストさせて下記のように各要素を出力することが可能です。
public class Main { public static void main(String[] args) { String array[][][] = { { { "a", "b" }, { "c", "d", "e" } }, { { "f", "g", "h", "i" }, { "j", "k", "l", "m" } }, { { "n", "o", "p" }, { "q" } } }; for (String[][] s1 : array) { for (String[] s2 : s1) { for (String s3 : s2) { System.out.println(s3); } } } } }








