20190203のJavaに関する記事は8件です。

FirebaseのRealtime Databaseでクリアタイムランキングを作成する(Androidアプリ)

前回の続きです。
https://qiita.com/KIRIN3qiita/items/4573d26187b8c9e53fc6

今回はRealtime Databaseの実践編としてゲームクリアタイムのランキングを作成します。
qiita.png

私が昔作成したお釣り支払いゲームのクリアタイムランキング画像です。

自作のしょぼいゲームアプリでも、クリアタイムランキングがあると少しは形になります。(インストール数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"があるのは気持ちが悪いですが、同じ階層のデータを引き抜くイメージなのであると便利です。

データ登録

まずデータ保存、取得に利用するクラスを作成。

User
public 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は癖がすごくて使いこなすのは大変ですね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

FirebaseのRealtime Databaseの設定から簡単なデータ入力まで(Androidアプリ)

概要

Firebaseのアカウントがあることが前提です。
Firebase Realtime Databaseの公式解説はこちら
簡単に説明すると、リアルタイム更新に特化した簡易データベースで、チャットなど雑多なことに使うことが多いようです。

Realtime Databaseの始め方

Android Studioの上のメニューにあるTools → Firebaseを選択。
Assistantが表示されるのでRealtime Databaseを選択。
1.png

セットアップ方法が書かれたリンクが表示されます。
スタートガイド
これを読まなくても番号付きで手順が書いてあり、
ボタンをクリックしていくだけで、以前は設定できました。
(結果としては今回は苦戦しました・・・)
2.png

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.gradle
implementation 'com.google.firebase:firebase-database:16.0.3'

と、最新のにしてとのこと。

そうすると

app/build.gradle
implementation 'com.google.firebase:firebase-core:16.0.4'

も入れてとWarningが出るので従う。
ここら辺の内部ライブラリバージョンが違う問題はもうすぐ改善されるということをGoogleの開発者ブログに書いてあったような。

③Configure Firebase Database Rules

認証についてのリンクとDBの作成ルールのリンクがあります。
Firebase Authentication
データベース ルールを使ってみる

認証については、ユーザーの個人情報など機密性の高いものにする場合に必要です。
簡易的なチャットやゲームランキングなどでは使う必要はないと思います。(たぶん)

では、ブラウザでFirebaseのコンソールにアクセスして、左タブのDatabaseをクリック。
データベースの作成をクリック。
テストモードで作成。

3.png
するとタブがCloud Firestore[ベータ版]になっているので、
Realtime Databaseにしてください。
4.png

とにかく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 でのデータの読み取りと書き込み
がわかりやすいです。
データを個別に登録することも可能ですが、かなり面倒な作りになるので、クラス単位での登録、取得のほうが楽です。

〇クラスの作成
(注意)そのオブジェクトを定義するクラスに、引数を取らないデフォルト コンストラクタと、代入するプロパティのパブリック ゲッターが存在することが条件です。

User
public 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;
    }
}

〇データ登録

register
User user = new User( "山田太郎",30 );
// インスタンスの取得
FirebaseDatabase database = FirebaseDatabase.getInstance();

// ファイルパスを指定してリファレンスを取得
DatabaseReference refName = database.getReference("info/user");
// データを登録
refName.setValue(user);

Firebaseのデータを見てみると
5.png

しっかり登録されてますね。

⑤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

複数のデータベースを利用することもできますよ、という内容。
複数のデータベースでスケールする
メルカリは相当な数のデータベースを結び付けて膨大な量のチャットを実現していると聞いたことがあるような。
本当にサーバーレスな時代ですね。

基本編だけで長くなってしまいました。
このままではただ単に簡単なデータを入力、取得しただけになっていて、使い道がなさそうです。
応用編は後程。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SpringBoot+インメモリデータグリッド入門 (データ永続化)

前回やったこと

前回はSpringDataGeodeを使って、インメモリデータグリッドを使ったSpringBootアプリケーションを作成し、データがサーバに同期されたタイミングでイベント処理が行えるように実装を行いました。

・SpringDataGeodeを使用したアプリケーション作成

・SpringDataGeodeを使用したイベント処理実装

今回やること

インメモリデータグリッドのキャッシュに登録されたデータをディスクに永続化する

今回はSpringDataGeodeの機能を使い、インメモリキャッシュに登録されたデータをローカルのディスクに永続化し、更にキャッシュ組み込みアプリサーバの起動時に、永続化ファイルをロードしてキャッシュにデータを登録する実装をやります。

調べてみたところ、アノテーション一つ追加してやるだけで実現できそう。

  • SpringBootApplicationに以下のようにアノテーションを追加。
ServerGeodeApplication.java
package 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.java
package 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.java
package 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が提供してくれるという情報を得られなかったので、一旦ファイル永続化機能を使用して、永続化をやってみました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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();
// etc

Excelファイルの作成、出力

ファイルを作成

// 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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

シンプルな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/fb7681d79eeb48c05144

dockerイメージを作る流れ

Dockerfileを書く→docker buildコマンドを実行
でdockerイメージは作成される。

dockerイメージ作成のざっくりとした流れは以下の図のような感じ。

意識すべき登場人物としては、

  • 自身のローカル端末
  • ビルドコンテキスト
  • dockerイメージ

dockerビルドコマンド実行時に、どのパス配下のファイルをビルドコンテキストに追加するかを決める。
このとき、ビルドコンテキストに持っていきたくないファイルは.dockerignoreファイルに記述する。

また、Dockerfile内のCOPY命令で、ビルドコンテキスト内の何をイメージに持っていくか決める。

ビルドコンテキスト.jpg

使用する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

以下のように表示され、うまくいった。

hello.gif

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"]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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のみ切り出せます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SpringBoot+インメモリデータグリッド入門 (イベント処理)

前回やったこと

前回はSpringDataGeodeを使って、インメモリデータグリッドを使ったSpringBootアプリケーションを作成し、SpringDataを使用して永続化したデータが二つのアプリケーションで共有されていることを確認しました。

SpringDataGeodeを使用したアプリケーション作成

今回やること

インメモリデータグリッドを使ったイベント処理

今回はインメモリデータグリッドの仕組みを使ったイベント処理を実装しようと
思います。

以下の図のような実装をします。
Untitled Diagram.png

  • まずはRegionの変更を検知するリスナーを実装
UserRegionListener.java
package 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 にはリスナー登録したいRegionKey,Valueを設定します。

今回検証したのは以下の二つ。
・ Region作成イベント処理: afterRegionCreateメソッドの処理が実行される
・User新規登録イベント処理: afterCreateメソッドの処理が実行される

  • 次に、リスナーをRegionに登録する実装
UserRegion.java
package 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.java
package 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.java
package 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クラス内の処理でRegionRegionへの設定を反映させています。
これをキャッシュ組み込みサーバとして起動するアプリで実装します。
クラス名にClientServerと名付けていますが、今回は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

ちなみに上のログで、GeodeClientApplicationUsersというRegionGeodeServerApplicationで作成した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と非常に高速でした。
試行回数が少ないのであまり参考にはならないかもしれませんが、個人的には満足。

次は永続化について実装してみようと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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);
                }
            }
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む