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

[Ethereum]web3jを使ってコントラクトを実行する方法~その2~

記事の内容

Web3jを使ってコントラクトを実行する方法は以下の記事で記載しました。
この記事ではコントラクト操作用のJavaクラスを作成し、そのクラスを経由してコントラクトを実行するというものです。

web3jを使ってコントラクトを実行する方法

このやり方ではコントラクトが増えてくるとコントラクトの数だけJavaのクラスが増えてしまい、管理が煩雑になる可能性があります。
そこで、コントラクトのアドレスとファンクション名を指定して実行する方法を記載します。

環境

  • solidity:0.4.24
  • jdk:1.8
  • web3j:4.5.0
  • OS:Windows10

サンプルのコントラクト

コントラクトはOpenzeppelinを使用したERC20トークンを発行するものです。
内容はERC20.solを継承したクラスを作成し、コンストラクタだけ用意したものになります。

MyToken.sol
pragma solidity ^0.4.24;

import "node_modules/openzeppelin-solidity/contracts/token/ERC20/ERC20.sol";

contract MyToken is ERC20 {

    string public name = "OrgToken"; //トークンの名称
    string public symbol = "ORG"; //トークンの単位表記
    uint public decimals = 18; // 小数点の桁数

    address public account = msg.sender;
    uint256 public _totalS = 10000000000000;

    constructor () public {
        super._mint(account, _totalS);
    }

    function balanceOf(address target) public view returns (uint256) {
        return super.balanceOf(target);
    }
}

コントラクト操作用のJavaクラス

続いてかなり雑な作りですがJavaのクラスです。
基本的な考え方はコントラクトのアドレス、ファンクション名、引数、戻り値が分かれば実行出来るよね。という作りです。

MyTokenExec.Java
import java.io.IOException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;

import org.web3j.abi.FunctionEncoder;
import org.web3j.abi.FunctionReturnDecoder;
import org.web3j.abi.TypeReference;
import org.web3j.abi.datatypes.Address;
import org.web3j.abi.datatypes.Bool;
import org.web3j.abi.datatypes.Function;
import org.web3j.abi.datatypes.Type;
import org.web3j.abi.datatypes.Uint;
import org.web3j.abi.datatypes.Utf8String;
import org.web3j.crypto.Credentials;
import org.web3j.crypto.RawTransaction;
import org.web3j.crypto.TransactionEncoder;
import org.web3j.protocol.admin.Admin;
import org.web3j.protocol.core.DefaultBlockParameterName;
import org.web3j.protocol.core.methods.request.Transaction;
import org.web3j.protocol.core.methods.response.EthCall;
import org.web3j.protocol.core.methods.response.EthGetTransactionCount;
import org.web3j.protocol.core.methods.response.EthSendTransaction;
import org.web3j.protocol.http.HttpService;
import org.web3j.utils.Numeric;

public class MyContractExec {

    public static final Admin web3j = Admin.build(new HttpService("http://127.0.0.1:8545"));

    private static final String CONTRACT_ADDRESS = "0x715d4c293482e6f72e1cbb3b22a10fbdae3bd7b0";

    public static final String OWNER_ADDRESS = "0x945cd603a6754cb13c3d61d8fe240990f86f9f8a";
    public static final String TO_ADDRESS = "0x66b4e7be902300f9a15d900822bbd8803be87391";

    public static void main(String args[]) {

        MyContractExec exec = new MyContractExec();

        List<?> result = null;
        try {
            List<Type> inputParam = new ArrayList<>();

            // 引数なし/戻り値uint256
            result = exec.execFunction("totalSupply", inputParam, ResultType.INT);
            System.out.println("Total Supply : " + ((Uint)result.get(0)).getValue());

            // FROM_ADRESS と TO_ADDRESSのBalance確認
            confirmBalance(OWNER_ADDRESS, TO_ADDRESS);

            // 引数あり(address, uint256)/戻り値bool
            inputParam = new ArrayList<>();
            inputParam.add(new Address(TO_ADDRESS));
            inputParam.add(new Uint(BigInteger.valueOf(123456)));
            result = sendSignedTransaction(credentials, "transfer", inputParam, ResultType.BOOL);
            System.out.println( ((Bool)result.get(0)).getValue() );

            confirmBalance(OWNER_ADDRESS, TO_ADDRESS);

        }catch(IOException e) {
            e.printStackTrace();
        }
    }

    public static void confirmBalance(String ... args) throws IOException{

        MyContractExec exec = new MyContractExec();

        List<?> result = null;
        int i = 0;
        for(String address :args) {
            List<Type> inputParam = new ArrayList<>();
            // 引数あり(address)/戻り値uint256
            inputParam.add(new Address(address));
            result = exec.execFunction("balanceOf", inputParam, ResultType.INT);
            System.out.println("Balance of ADDRESS[" + i + "] : " +  ((Uint)result.get(0)).getValue() );
            i++;
        }
    }

    /**
     * Transactionの発生しない(値の更新がない)functionの実行
     * @param functionName
     * @return
     * @throws IOException
     */
    public List<?> execFunction(String functionName, List<Type> args, ResultType type) throws IOException{

        Function function = new Function(
                functionName, args, Arrays.<TypeReference<?>>asList( getTypeReference(type) ));

        String encodedFunction = FunctionEncoder.encode(function);
        EthCall ethCall = web3j.ethCall(
                Transaction.createEthCallTransaction(
                        OWNER_ADDRESS, CONTRACT_ADDRESS, encodedFunction),
                DefaultBlockParameterName.LATEST)
                .send();

        String value = ethCall.getValue();
        return FunctionReturnDecoder.decode(value, function.getOutputParameters());
    }

    /**
     * Transactionの発生する(値の更新がある)functionの実行
     * @param credentials
     * @param functionName
     * @param args
     * @param type
     * @return
     */
    public static List<?> sendSignedTransaction(Credentials credentials, String functionName, List<Type> args, ResultType type){

        Function function = new Function(
                functionName, args, Arrays.<TypeReference<?>>asList( getTypeReference(type) ));

        String encodedFunction = FunctionEncoder.encode(function);

        try {
            // nonce値を取得する
            EthGetTransactionCount ethTransactionCount =
                web3j.ethGetTransactionCount(credentials.getAddress(), DefaultBlockParameterName.PENDING).send();
            BigInteger nonce = ethTransactionCount.getTransactionCount();

            //トランザクション生成
            RawTransaction rawTransaction = RawTransaction.createTransaction(
                    nonce,
                    BigInteger.valueOf(1000),
                    BigInteger.valueOf(4700000),
                    CONTRACT_ADDRESS,
                    encodedFunction);

            //署名
            byte[] signedMessage = TransactionEncoder.signMessage(rawTransaction, credentials);
            String hexValue = Numeric.toHexString(signedMessage);

            //send
            EthSendTransaction ethSendTransaction = web3j.ethSendRawTransaction(hexValue).sendAsync().get();

            //エラー確認
            if(ethSendTransaction.getError() != null){
               System.out.println(ethSendTransaction.getError().getMessage());
            }else {
                String value = ethSendTransaction.getResult();
                return FunctionReturnDecoder.decode(value, function.getOutputParameters());
            }
        }catch(IOException | ExecutionException | InterruptedException e) {
            e.printStackTrace();
        }
        return null;
    }

    public static TypeReference<?> getTypeReference(ResultType type){

        TypeReference<?> typeReference = null;
        switch(type) {
        case ADDRESS:
            typeReference = new TypeReference<Address>() {};
            break;
        case BOOL:
            typeReference = new TypeReference<Bool>() {};
            break;
        case STRING:
            typeReference = new TypeReference<Utf8String>() {};
            break;
        case INT:
            typeReference = new TypeReference<Uint>() {};
            break;
        default:
            break;
        }
        return typeReference;
    }
}

ResultType.java
public enum ResultType {
    ADDRESS,
    BOOL,
    STRING,
    INT
}

mainメソッドでやっていることは単純です。
トークンの発行量の確認、残高を確認し、送金。
最後に残高を再確認する。

これをcontractの作りに着目して言い換えると以下のようになります。
1. 値の更新なしのファンクション実行
2. 値の更新ありのファンクション実行

1. 値の更新なしのファンクション実行

まずは値の更新が発生しないファンクションの実行です。
ERC20.solの中だと「totalSupply」や「balanceOf」などがあります。
コードだと以下の部分になります。

MyTokenExec.java
    public List<?> execFunction(String functionName, List<Type> args, ResultType type) throws IOException{

        Function function = new Function(
                functionName, args, Arrays.<TypeReference<?>>asList( getTypeReference(type) ));

        String encodedFunction = FunctionEncoder.encode(function);
        EthCall ethCall = web3j.ethCall(
                Transaction.createEthCallTransaction(
                        OWNER_ADDRESS, CONTRACT_ADDRESS, encodedFunction),
                DefaultBlockParameterName.LATEST)
                .send();

        String value = ethCall.getValue();
        return FunctionReturnDecoder.decode(value, function.getOutputParameters());
    }

やっていることは
1. Functionのインスタンス作成
2. Functionをバイナリ化
3. Ethereumのネットワーク上に送信
4. バイナリで返ってきた結果を変換

このサンプルコードでは「execFunction」の引数で呼び出し元から戻り値のタイプに何を返して欲しいかを指定しています。
ResultTypeは列挙型でsolidityで設定している型をJavaではどのクラスとして扱うのかを定義しています。
「getTypeReference」メソッドの中で戻り値にあったクラスを返すようにしています。

最後にバイナリデータとして返ってきた値をデコードして呼び出し元に返しています。

2. 値の更新ありのファンクション実行

次は値の更新があるファンクションの呼び出しです。
これは作りがかなり違います。

MyTokenExec.java
    public static List<?> sendSignedTransaction(Credentials credentials, String functionName, List<Type> args, ResultType type){

        Function function = new Function(
                functionName, args, Arrays.<TypeReference<?>>asList( getTypeReference(type) ));

        String encodedFunction = FunctionEncoder.encode(function);

        try {
            // nonce値を取得する
            EthGetTransactionCount ethTransactionCount =
                web3j.ethGetTransactionCount(credentials.getAddress(), DefaultBlockParameterName.PENDING).send();
            BigInteger nonce = ethTransactionCount.getTransactionCount();

            //トランザクション生成
            RawTransaction rawTransaction = RawTransaction.createTransaction(
                    nonce,
                    BigInteger.valueOf(1000),    //GAS
                    BigInteger.valueOf(4700000), //GASLimit
                    CONTRACT_ADDRESS,
                    encodedFunction);

            //署名
            byte[] signedMessage = TransactionEncoder.signMessage(rawTransaction, credentials);
            String hexValue = Numeric.toHexString(signedMessage);

            //send
            EthSendTransaction ethSendTransaction = web3j.ethSendRawTransaction(hexValue).sendAsync().get();

            //エラー確認
            if(ethSendTransaction.getError() != null){
               System.out.println(ethSendTransaction.getError().getMessage());
            }else {
                String value = ethSendTransaction.getResult();
                return FunctionReturnDecoder.decode(value, function.getOutputParameters());
            }
        }catch(IOException | ExecutionException | InterruptedException e) {
            e.printStackTrace();
        }
        return null;
    }

やっていることは
1. Functionインスタンスの作成
2. Functionのバイナリ化
3. ナンス値の取得
4. トランザクションの生成
5. 署名
6. トランザクションの送信
7. 結果の確認

値の更新ありということでトランザクションを生成し、ブロックに取り込まれたタイミングで更新が確定されます。
なので、値の更新がないファンクションとは呼び出し方がかなり違ってきます。

所感

コントラクト毎のクラスを作るのも直感的にコードを組めて個人的には好きなのですが、このやり方だとコントラクト呼び出し、値の取得を行う仕組みを作っておけば、かなり汎用的にコードを組めそうです。

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

Cloud Dataflow for Java 雑多なノウハウ集 - 実装編

Google Cloud Dataflow に触る機会があったのですが、いまいちドキュメントが薄く、また自分が分散処理フレームワーク未経験だった事もあり、いろいろハマったので、得られた知見を書いておきます。

本記事は実装編ということで、Dataflow パイプラインのコードを書くに当たっての知見をまとめます。

なお Cloud Dataflow は Apache Beam の実行環境の1つという位置付けです。以下の内容は特に明記していない限り Apache Beam にも当てはまります。

確認した環境は Apache Beam SDK for Java 2.13.0 です。

想定読者は、Beam 関連のドキュメント、特に Beam Programming Guide を読んだことのある方、です。

Window

FixedWindows や SlidingWindows の期間はキリが良い時刻になる

パイプラインに FixedWindows や SlidingWindows を設定すると、パイプライン実行時に生成される各ウィンドウにはそれぞれ期間が設定されますが、それらは入力データやパイプラインの起動時刻などに依存せず、固定の値になり、しかもキリが良い値になります。どういうことかと言うと、例えば、

input.apply(Window.into(FixedWindows.of(Duration.standardMinutes(5))));

とした場合、各ウィンドウに設定される期間は、毎時 00:00〜05:00, 05:00〜10:00, 10:00〜15:00 などとなります。 02:00〜07:00 や、 01:23〜06:23 などにはなりません。

キリが良い、というと定義が曖昧ですが、Unix epoch が起点になります(Javadoc参照)。

期間はキリが良い値からずらすこともできます。withOffset(Duration) を使います。

input.apply(Window.into(
    FixedWindows.of(Duration.standardMinutes(5))
        .withOffset(Duration.standardMinutes(1))
));
// => 01:00〜06:00, 06:00〜11:00, 16:00〜21:00, ...

Watermark はどんなデータ構造で、何に属しているのか

Beam Programming Guide の 7.4 節Streaming 102: The world beyond batch を読むと、Beam の watermark に関してなんとなくは理解できるのですが、実際にコードを書こうとすると、もう少し具体的な watermark のモデルが知りたくなります。

WatermarkManager の Javadoc が参考になりそうです1。これによると:

  • watermark 自体は単にタイムスタンプ値である
  • PTransform にはそれぞれ入力と出力の watermark がある
  • PCollection にはそれぞれ watermark がある
  • PCollection の watermark はそれを生成した PTransform の出力の watermark と同じである

また、Dataflow の Monitoring UI で transform (Step) を選択すると Watermark の項目があります。

ui_watermark.png

UI 上は分かりづらいですが、transform 毎にこの項目があり、それぞれ異なった値を示すことがあります。ここで表示されているのはおそらく各 transform の入力の watermark です。

withAllowedLateness とは何なのか

Beam Programming Guide を読んでも Window#withAllowedLateness(Duration) の挙動がよくわかりません。

Streaming 102: The world beyond batch の Figure 8 を見るとわかりやすいです。なおこのドキュメントは Beam で Window を扱う上では必読だと思います。

Figure 8 の設定では、2分間隔の FixedWindow を設定しており、 .withAllowedLateness() に「1分」が指定されています。

下の図で、水平の太い白線は、現在時刻(Processing Time) を示しており、太い緑線は watermark を表しています。
赤矢印の箇所に注目してください。[12:00, 12:02) Lateness Horizon という記述が、Event Time での 12:03 の位置にあります。
これは一番左側にある Window [12:00, 12:02) の終端から 1分経過した位置にありますよね。これが、.withAllowedLateness() によって設定された限界点、Lateness Horizon です。同様に、各 Window に対して Lateness Horizon は存在します。

figure8_1.png

Beam の処理が進むと、図の太い白線が上昇していきます。それに伴い、現在いつの event time を処理しているかを表す太い緑線 = watermark が右に進んでいきます。 processing time が 12:07 プラス0.4秒ぐらいに来た時、watermark が 12:03 に達し、[12:00, 12:02) の Window は「閉じ」られます。それ以降その Window に入るはずのデータが来ても、破棄されます(図で言うと丸の9は破棄される)。

Figure 8 の最終的な状態の図を見ると、watermark が各 Lateness Horizon に達した点から水平に白い点線が伸びています。これは各 Window が閉じられたタイミングを表しています。

figure8_3.png

まとめると、
- .withAllowedLateness() の指定は Window の終端からの Duration になる
- .withAllowedLateness() の指定は Event Time ベースである

AfterWatermark トリガーを読み解く

Beam Programming Guide やサンプルコードなどで、よく以下のようなトリガー設定が出てきます。どういう意味なのでしょうか。

input.apply(
    Window.<String>into(FixedWindows.of(Duration.standardMinutes(5)))
        .triggering(
            AfterWatermark.pastEndOfWindow()
                .withLateFirings(AfterProcessingTime
                    .pastFirstElementInPane().plusDelayOf(Duration.standardMinutes(30))))
        .withAllowedLateness(Duration.standardDays(1)));

まず、AfterWatermark.pastEndOfWindow() は決まり文句で、他の組み合わせはありません。watermark が Window の終端まで進んだ時、その時の window の内容 (pane) を出力することを意味します。

他に何も指定しなければ、pane の出力はこの1回きりになりますが、
.withLateFirings() により、それ以降特定の条件を満たす毎に再度その時点での pane が出力されます。

AfterProcessingTime.pastFirstElementInPane() も決まり文句で、他の組み合わせはありません。pane 内に1つでもデータが入ってきたら、即時 pane を出力するという指定になります。

ただし、ここで .plusDelayOf() の指定があります。これはレシーバーのトリガーに対して、pane の出力を指定した時間だけ遅らせる、という指示になります。結果、pane 内にデータが入ってきても、すぐには pane は出力されず、一定時間待った後に、(待っている間に pane に入ってきたデータがあればそれも含めて) pane を出力することになります。

I/O

PubsubIO の読み込みではタイムスタンプを後付けで変更できない

入力データの中にタイムスタンプ情報が含まれるとき、それをウィンドウ処理の event time として扱うために、入力データを parse して PCollection の要素にタイムスタンプを付与する、ということをしたくなります。

PCollection<Record> timestampedRecords =
    input.apply(ParDo.of(new ParseRecords()))
         .apply(WithTimestamps.of((Record rec) -> rec.getTimestamp()));

ところが、入力データを PubsubIO から読み込む場合、後付けでタイムスタンプを付与することができません。PubsubIO から読み込んだデータにはデフォルトでは publish された時点のタイムスタンプが付きます2。それを上記のようなコードでタイムスタンプを付け直そうとすると、通常は元のタイムスタンプより古いタイムスタンプを付与することになり、以下のエラーが発生します。

java.lang.IllegalArgumentException: Cannot output with timestamp 2019-06-20T12:34:00.000Z. Output timestamps must be no earlier than the timestamp of the current input (2019-06-20T12:34:56.000Z) minus the allowed skew (0 milliseconds). See the DoFn#getAllowedTimestmapSkew() Javadoc for details on changing the allowed skew.

WithTimestamps#withAllowedTimestampSkew(Duration) メソッドを使えば、エラーメッセージにもある allowed skew を変更してこの問題に対処することができるようです。ですがこのメソッドは deprecated ですし、使った場合データが late data として扱われるそうで、色々トラブルの元になりそうです。

正当なやり方としては、Cloud Pub/Sub にメッセージを publish するときに、追加の属性としてタイムスタンプを設定する方法が示されています。そして PubsubIO をコンストラクトする時にタイムスタンプの属性名を渡します。

// publish側 (w/Google Cloud Client Library)
Publisher publisher = Publisher.newBuilder(topic).build();
long timestamp = Instant.now().getMillis();
PubsubMessage message = PubsubMessage.newBuilder()
    .setData(bytes)
    .putAttributes("ts", String.valueOf(timestamp)).build();
publisher.publish(message);
// 読み込み側 (Dataflow)
PCollection<String> input =
    p.apply(PubsubIO.readStrings().withTimestampAttribute("ts"));

これだと Pub/Sub に publish する側の処理にも手を入れる必要がありますが、現状は他に方法は無さそうです。

参考:

Runner

DirectRunner では PubsubIO と AfterWatermark Trigger の組み合わせがうまく機能しない

DirectRunner にはいくつか制約があるようで、必ずしも DataflowRunner と同一の挙動になりません。

私が遭遇したのは、 PubsubIO から読み取って GroupByKey のようなウィンドウ処理をし、 AfterWatermark のトリガーで出力するというケースで、GroupByKey 以降の処理がいつまで経っても進まず、パイプラインから何も出力されないという事がありました。 DirectRunner から DataflowRunner に変更することで解決しました。

原因としては DirectRunner では PubsubIO の watermark 処理に問題があるとのことです。

DirectRunner は手軽に動かせるので開発中はついつい頼りがちになりますが、怪しいと思ったらすぐ DataflowRunner で試しましょう。

参考:

その他SDK関連

PipelineOptions#as(Class) の挙動

PipelineOptions#as(Class) で PipelineOptions インスタンスを任意の PipelineOptions のサブインターフェースにキャストできますが、ちょっと振る舞いがわかりにくいです。 as() のレシーバと返り値のオブジェクトはどういう関係にあるのか。

PipelineOptionFactory.create()PipelineOptionFactory.fromArgs(String[]) を呼び出して PipelineOptions インスタンスを生成すると、内部的に key-value の組を保持する Map が作られます。 PipelineOptions#as(Class) を呼び出すと、その Map に対して読み書きするようなプロキシオブジェクトが作られ、返されます。

従って実体の Map は1つなので、 PipelineOptions#as(Class) の呼び出しを繰り返しても、以前にセットしたプロパティは維持されます。例えば、以下のようなコードは妥当です。

PipelineOptions options = PipelineOptionFactory.fromArgs(args).create();
Pipeline p = Pipeline.create(options);
p.getOptions().as(MyOptions).setMyFlag(true);
// 後から p.getOptions().as(MyOptions).getMyFlag() で取得できる

検証コード: https://github.com/yoshizow/dataflow_tasting/blob/master/src/test/java/com/example/PipelineOptionsTest.java

Unit Test

TestPipeline は Pipeline と何が違うのか

ユニットテストの時は TestPipeline を使えとドキュメントに書いてありますが、通常の Pipeline クラスと何が違うのでしょうか。
ざっくりソースコードを眺めて調べました。

まず TestPipeline クラスは Pipeline クラスを継承しており、基本的に Pipeline クラスの代替として使うことができます。

TestPipeline は JUnit の TestRule として実装されており、利用する際は @Rule アノテーションを付けて定義します。

@Rule public TestPipeline p = TestPipeline.create();

@Rule アノテーションを付けない場合、テスト実行時にエラーになります(ドキュメントはこれに従っておらず、内容が古いです)。

@Rule アノテーションにより、TestPipeline は個々のテストケースの開始と終了をフックして処理を行うことができます。これを利用して、テスト終了後にいくつかのチェックを行っています。

  • Pipeline#run() を呼び出し忘れていないか
  • パイプラインに接続されていない PTransform がないかどうか
  • パイプラインに接続されていない PAssert がないかどうか
  • パイプライン中の PAssert が全て実行されたかどうか

正直、そんなに有り難みは感じないですね。将来的にもっと有用なチェックが追加されるかもしれませんが。

なお PAssert は TestPipeline に限らず Pipeline クラスにおいても使えます。

その他

Dataflow でやらない方が良いタスク: 待ち時間のある処理

Dataflow は大量のデータを並列に処理してくれて非常に便利ですが、やらない方が良いタスクもあります。待ちが多いタスクです。

例えば外部のサーバーに対してHTTP通信をするようなケースです。何らかバッチ的な処理をするために他のサーバーに処理を移譲しているようなケースで、1回のHTTPレスポンスが帰ってくるまでに 30分かかる、それを大量に並列に実行する、、というようなシチュエーションを想像してください。

このようなシチュエーションでは、パイプラインの内容によっては、レスポンスを待つ間 Dataflow は何も出来ない可能性があります。Dataflow の料金モデルはインスタンスに対する時間課金なので、何もしない時間は無駄なコストになります。

もし待ちが多い処理を実行する場合は、Dataflow のジョブを分割して Dataflow ワーカーがブロックしないようにすることをお勧めします。


  1. WatermarkManager は DirectRunner の内部実装 

  2. protected method の doc comment に書かれています: https://beam.apache.org/releases/javadoc/2.13.0/org/apache/beam/sdk/io/gcp/pubsub/PubsubClient.html#extractTimestamp-java.lang.String-java.lang.String-java.util.Map- 

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

ラッパークラスの使いどき

概要

ラッパークラスの使いどきです。

finalクラスをモック化したいとき

テストコードを書いていて、finalクラスをモック化したい場面があったのでラッパークラスにすることで対応しました。
元々引数にfinalクラスをとっていたメソッドだったのだけれど、それだとテストできないので引数をラッパークラスで取るようにしました。
最初からテスト駆動型で開発しておけばこういった事態にはならなかったのだろうか。。。難しい。

元のコード

@Override
protected String doInBackground(URL... url) {
    HttpURLConnection con = null;
    URL urls = url[0];

    try {
        con = (HttpURLConnection)urls.openConnection();
        con.setRequestMethod("GET");
        con.connect();

        int resCd = con.getResponseCode();
        if (resCd != HttpURLConnection.HTTP_OK) {
            throw new IOException("HTTP responseCode:" + resCd);
        }

        BufferedInputStream inputStream = new BufferedInputStream(con.getInputStream());
        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
        String line;
        while (true) {
            line = reader.readLine();
            if (line == null) {
                break;
            }
            mBuffer.append(line);
        }
        inputStream.close();
        reader.close();

    } catch (IOException e) {
        e.printStackTrace();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        con.disconnect();
    }
    return mBuffer.toString();
}

修正したコード

@Override
protected String doInBackground(URLWrapper... urlWrapper) {
    HttpURLConnection con = null;
    URLWrapper urls = urlWrapper[0];

    try {
        con = (HttpURLConnection)urls.openConnection();
        con.setRequestMethod("GET");
        con.connect();

        int resCd = con.getResponseCode();
        if (resCd != HttpURLConnection.HTTP_OK) {
            throw new IOException("HTTP responseCode:" + resCd);
        }

        BufferedInputStream inputStream = new BufferedInputStream(con.getInputStream());
        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
        String line;
        while (true) {
            line = reader.readLine();
            if (line == null) {
                break;
            }
            mBuffer.append(line);
        }
        inputStream.close();
        reader.close();

    } catch (IOException e) {
        e.printStackTrace();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        con.disconnect();
    }
    return mBuffer.toString();
}

doInBackgroundはAsyncTaskのメソッドです。
微妙な変化ですが、最初はjava.net.URLを引数にとっていたのですが、これだとURLはfinalクラスなのでモック化できないことに気づき、ラッパークラスに変更。
PowerMockitoを使うことも試したのですが、上手く使えなかったので、今はとりあえずこれで。
これで以下のようなテストコードがかけるようになりました。

@Test
public void test_doInBackground(){

    HttpURLConnection httpURLConnection = null;

    try{

        String rtnXml = "aaaaaaaaaaaa";
        // HttpURLConnectionのmock化
        httpURLConnection = mock(HttpURLConnection.class);
        when(httpURLConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK);
        when(httpURLConnection.getInputStream()).thenReturn(new ByteArrayInputStream(rtnXml.getBytes("utf-8")));

        // URLWrapperのmock化
        URLWrapper urlWrapper = mock(URLWrapper.class);
        when(urlWrapper.openConnection()).thenReturn(httpURLConnection);

        // ConfirmAsyncListenerImplのmock化
        ConfirmAsyncListenerImpl confirmAsyncListener = mock(ConfirmAsyncListenerImpl.class);

        // RestaurantAsync.doInBackground()のテスト
        RestaurantAsync restaurantAsync = new RestaurantAsync(confirmAsyncListener);
        assertThat(restaurantAsync.doInBackground(urlWrapper), is("aaaaaaaaaaaa"));


    } catch (IOException e) {
        e.printStackTrace();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        httpURLConnection.disconnect();
    }

}

java.net.URLはmock化しようとするとコンパイルエラーが出るので、ラッパークラスを使うと対応できることがわかりました。

ちなみにラッパークラスは以下

URLWrapper.java
public class URLWrapper {

    private final URL url;

    public URLWrapper(URL url){
        this.url = url;
    }

    public URLConnection openConnection(){
        URLConnection urlConnection = null;
        try {
            urlConnection =  this.url.openConnection();
        }catch(IOException e){
            e.printStackTrace();
        }
        return urlConnection;
    }
}

引数をfinalクラスで取るのと、それのラッパークラスで取ることの違いやデメリットはあるのかしら。
それはまたわかったら追記します。

まとめ

また気がついたら追記しようと思います。

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

Terasolunaの二重送信防止とSpring Securityのcsrf対策を両立させる方法

はじめに

この組み合わせで問題が起きて解決したメモです。

Terasolunaの二重送信防止ページについては以下をご覧ください

http://terasolunaorg.github.io/guideline/5.5.1.RELEASE/ja/ArchitectureInDetail/WebApplicationDetail/DoubleSubmitProtection.html

環境

Adopt Open JDK Hotspot 11.0.3
Spring boot 2.1.0M4

pom.xml

二重送信防止機能についてはTerasolunaの機能を使いますので、Dependencyに追加します。
terasoluna-gfw-commonはいらない気もしますが、なんとなく入っています。

pom.xml
        <dependency>
            <groupId>org.terasoluna.gfw</groupId>
            <artifactId>terasoluna-gfw-common</artifactId>
            <version>5.5.1.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.terasoluna.gfw</groupId>
            <artifactId>terasoluna-gfw-web</artifactId>
            <version>5.5.1.RELEASE</version>
        </dependency>

InterceptorとRequestDataValueProcessorの設定

Terasolunaの二重送信防止機能はInterceptorでRequestにAttributeを設定して、それをキーにRequestDataValueProcessorでhiddenを作りだすみたいな動きをします。
そのため、Interceptorの設定とhiddenを作ってくれるRequestDataValueProcessorの設定を行います。

ここで注意点があり、Spring Securityを使用するとcsrf対策のためのhiddenを埋め込む設定であるCsrfRequestDataValueProcessorが自動的にBean登録されます。
このProcessorは全体で1つしか登録出来ないため、特に何もしなければcsrf対策か二重送信防止かどちらかしか使うことが出来ません。
そこで、Terasoluna内にあるCompositeRequestDataValueProcessorという複数のRequestDataValueProcessorを使用するためのクラスをBean登録することで両立させることになります。

また、今回は例なので、HandlerInterceptorを全てのURLに設定していますが、必要に応じてexcludePathPatternsで除外条件を作ってあげてください。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.support.RequestDataValueProcessor;
import org.terasoluna.gfw.web.mvc.support.CompositeRequestDataValueProcessor;
import org.terasoluna.gfw.web.token.transaction.TransactionTokenInterceptor;
import org.terasoluna.gfw.web.token.transaction.TransactionTokenRequestDataValueProcessor;

@Configuration
public class WebMcvConfig implements WebMvcConfigurer {

    @Bean
    public TransactionTokenInterceptor transactionTokenInterceptor() {
        return new TransactionTokenInterceptor();
    }

    @Bean
    public RequestDataValueProcessor requestDataValueProcessor() {
        return new CompositeRequestDataValueProcessor(new CsrfRequestDataValueProcessor(), new TransactionTokenRequestDataValueProcessor());
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(transactionTokenInterceptor()).addPathPatterns("/**");
    }
}

Beanの上書きを許容する

Beanはデフォルトだと上書き出来ないように設定されているため、設定で上書き出来るように許容してあげます。
この設定を行わないとBean重複してるとしてエラーとなってしまいます。

ただし、この設定を入れると上書き出来てしまうため、Bean関連の設定がしっかりしていないと思わぬ挙動が発生してしまいます。ご注意ください。

spring:
  main:
    allow-bean-definition-overriding: true

おわりに

なんでSpringに二重送信防止機能がデフォルトで入っていないのでしょうか。

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

【Android】ダークテーマの対応方法

はじめに

Android 10から本格サポートされたダークテーマ(ダークモード)のアプリ側の対応方法について説明します。

res/values/styles.xml

まず初めに、通常のテーマカスタマイズでも利用するstyles.xmlを更新し、ダークモードON/OFF時に切り替えたいカラーを定義しておきます。

<resources>
    <style name="AppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
        <item name="android:colorBackground">@color/colorBackground</item>
        <item name="android:textColor">@color/textColor</item>
        <item name="android:textColorPrimary">@color/textColorPrimary</item>
        <item name="android:textColorSecondary">@color/textColorSecondary</item>
    </style>
</resources>

colors.xmlの作成

システム設定でダークモード有効時に参照されるカラー定義をres/values-night/colors.xmlに、通常時のカラーをres/values/colors.xmlにそれぞれ記載しておきます。

例) res/values-night/colors.xml

<resources>
    <color name="colorPrimary">#212121</color>
    <color name="colorPrimaryDark">#212121</color>
    <color name="colorAccent">#80cbc4</color>
    <color name="colorTransparent">#00000000</color>
    <color name="textColor">#FFFFFF</color>
    <color name="textColorPrimary">#FFFFFF</color>
    <color name="textColorSecondary">#808080</color>
    <color name="colorBackground">#313131</color>
    <color name="colorCardBackground">@color/colorPrimary</color>
    <color name="colorBackgroundBottomAppBar">#353535</color>
</resources>

ここまで対応すれば、システム設定に連動して、コードは一切書かなくても表示が切り替わります。

アプリ内の設定で動的に切り替えたい場合

AppCompatDelegate.setDefaultNightModeを利用します。

常にダークテーマで表示

AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_YES);

常にダークテーマOFF(通常表示)

AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_NO);

システム設定に連動する(特に指定しない場合はこちらがデフォルトです)

AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_FOLLOW_SYSTEM);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む