愛と勇気と缶ビール

ふしぎとぼくらはなにをしたらよいか

JacksonでJSONの一部だけdeserializeさせないための設定

リクエストボディで受け取ったJSON全体はオブジェクトにマッピングして欲しいけどもその一部分だけJSON文字列そのままにしておいて欲しい(= デシリアライズして欲しくない)というケースがある。例えば、JSON-RPCを実装していてparamsの型がmethod依存で変わってしまうような場合。

こうやって

import com.fasterxml.jackson.core.JsonParser;                                                                            
import com.fasterxml.jackson.databind.DeserializationContext;                                                                           
import com.fasterxml.jackson.databind.JsonDeserializer;

public class KeepAsJsonDeserializer extends JsonDeserializer<String> {                                                                                                                                                                                                            
    @Override                                                                                                                            
    public String deserialize(@NotNull JsonParser p, DeserializationContext ctxt) throws IOException {                                   
        return p.getCodec().readTree(p).toString();                                                                                      
    }                                                                                                                                    
}  

こうする

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;

public class JSONRPCRequest {                                                                                                            
    String jsonrpc;                                                                                                                      
    String method;                                                                                                                       
    int id;                                                                                                                              
    @JsonDeserialize(using = KeepAsJsonDeserializer.class)                                                                               
    String params;                                                                                                                       
}     

よかったですね。

Mockitoの新機能を使ったモダンげな使い方

Javaのmock用ライブラリは探せば色々あるが、一番よく使われてそう(自分調べ)ってことでMockitoを使っている。

http://mockito.org/

Mockitoはそのロゴからいっても「モキート」「モッキート」と読むのが正しいのだろうけど、心の中ではいつでも「モック伊藤」「モック伊藤」と呼んでしまう。

それはさておき、Mockitoについて日本語で書かれた記事は「mock()でモックしてverify()でベリファイするんやで〜」くらいで留まっているものが多そうに見受けられたので、新し目の機能を使ってこういう風に書くのもよさげかな、とここに記しておく。

参考: Mockito (Mockito 2.0.33-beta API)

mock()ではなく@Mock, spy()ではなく@Spyをつかう

以下のようにmock()メソッドを使ってmockを作ることも出来るのだが、

MyService service = mock(MyService.class);

Mockitoにはアノテーションでmockを作成する機能があるため、テストクラスの最初にフィールドとして以下のようにして書いておく。

public class MyServiceTest {
    @Mock
    MyService service;
}

こうすると、あるオマジナイを書いておくことでMockitoが各テストケースの前にmockを作成してくれるようになる。(オマジナイについては後述) spyについても同様のことが可能。

answerとかもちゃんと指定できる。

public class MyServiceTest {
    @Mock(answer = Answers.RETURNS_DEEP_STUBS;
    MyService service;
}

mock()やspy()を使っても同様のことが出来るのになぜこちらを選ぶのか。それはテストのコードを初めて見た人が「このクラスのテストではこいつらをmockする」とすぐに理解しやすいと考えるから。もちろん各テストケースの冒頭にmock(),mock()と繰り返し書いてあってもちゃんと読めば分かるが、クラスの先頭にまとめて書いてあった方が「テストケース共通でこいつらはmockされている」というのが分かりやすい。

@InjectMocksを使ってDIしてもらう (Whiteboxは使わない)

テスト対象のクラスが別のクラスのインスタンスに依存している、つまりprivateフィールドに別のクラスのインスタンスをコンポジットする感じになっている、というのはあるあるケースだと思う。

public class MyService {
    @Inject
    private OtherService otherService;
}

依存先クラスの単体テストは別に書くので、こいつもmockして置き換えたいなーと思うのが人情なのだが、これをMockito付属のWhiteboxというユーティリティクラスを使って書くとこんな感じになってしまう。

OtherService otherService = mock(OtherService.class);
Whitebox.setInternalState(service, "otherService", otherService);

フィールド名の文字列による指定がいただけない。これは@InjectMocksで置き換えられる。

public class MyServiceTest {
    @Spy
    @InjectMocks
    MyService service;

    @Mock
    OtherService otherService;
}

こう書くと、MyServiceのフィールドにOtherServiceクラスのmockされたインスタンスがDIされる。 これも先程と同様に、手続き的に行うよりも意図が分かりやすい。

どういった種類のインジェクションが可能なのかはInjectMocks (Mockito 2.0.33-beta API)を参照。

MockitoRuleを使う

以上の@Mock, @Spy, @InjectMocksを動作させるためにはオマジナイというか初期化処理が必要なのだが、それをJUnit4のRuleで行うためのMockitoRuleが用意されているのでそれを使う。

public class MyServiceTest {
    @Rule
    public MockitoRule mockito = MockitoJUnit.rule();

    @Spy
    @InjectMocks
    MyService service;

    @Mock
    OtherService otherService;
}

Matchersを使う

今まで書いたのとは若干毛色が違うが、「テキトーな値」を指定するためのMatcherが色々Mockitoには用意されているのでデータをわざわざ用意したくない場合はそれを使う。

doReturn(1).when(this.service).doSomething(anyString()); // どんなStringが渡ってきても1を返す

any(Class clazz)やisNull()などなど、おおむね想像通りのMatcherが用意されている。

Matchers (Mockito 2.0.33-beta API)

注意点として、Matcherを使った引数と通常の引数を混ぜて指定することは出来ない。 つまり以下のように書いてしまうとMockitoに怒られてエラーになる(ちなみにMockitoのエラーメッセージは結構親切だと思う)

doReturn(1).when(this.service).doSomething2(anyString(), 1);

それだと不便じゃね?と思うかもしれないが、そういう場合はeq()を使って引数をMatcherに統一すればよい。

doReturn(1).when(this.service).doSomething2(anyString(), eq(1));

AdditionalMatchersをインポートすれば、配列に対するMatcherなども使えるようになる。

AdditionalMatchers (Mockito API)

今までのまとめ

以上のことをまとめた上で更にちょっと付け足すと、テストクラスは大体以下の様な書き方になる。

public class MyServiceTest {
    // 初期化のためのおまじない
    @Rule
    public MockitoRule mockito = MockitoJUnit.rule();

    // テスト対象のクラス
    @Spy
    @InjectMocks
    MyService service;

    // テスト対象のクラスが依存している(フィールドに持っている)クラス
    @Mock
    OtherService otherService;

    // テストケース間で共通のデータ
    long userId = 1L;

    @Before
    public void setUp() {
        // テストケース間で共通のモック処理はここでやっておく
        when(this.otherService.foo(anyString()).thenReturn("");
        when(this.otherService.bar(any(MyBean.class)).thenReturn(1);
    }

    @Test
    public void testDoSomething() throws Exception {
        // 各テストケースで固有のモック処理はここでやる
        doReturn(1).when(this.service).doSomething(anyString(), eq(1));

        // assertion
    }
}

好みの問題もあるだろうが、各テストケース内に色んなものをダラダラっと書くよりは「このクラスのテストではこうします」という意図は伝わりやすくなるかと思う。

JavaでiPhoneからアップロードされた画像を(再)回転させる

iPhoneからアップロードした画像をPCで表示すると思ったのと違う向きで表示されるという事象は、よく知られているように、iPhoneが本来横向きの画像にexifのorientationをかました上で縦に表示しているから発生する。

blog.isao.co.jp

これに真面目に対応する場合は以下のようなコードを書くのが正解っぽいが、

mumumu.github.io

多少サボったコードでもいいならThumbnailator (https://github.com/coobird/thumbnailator) を通してしまうのが一番楽かと思う。

Thumbnails.of(inputStream).scale(1).toFile(file);

Vimを使って複数の文字列を一度で置換する

Vimについて書くのとか久しぶりすぎる。

例えば、テキスト中の

  1. FFFF00をFFFFFFに
  2. FFFFFFをFFFF00に

一発で置き換えたい。これを1 => 2の順でやると1の後で全部FFFFFFになってしまうので、アカン。

perlとかでコマンドラインからやってもいいけど、まあVimからやりたいとしよう。

おそらく色々方法はあるのだろうけど、一番楽なのは tpope/vim-abolish · GitHub を入れて

:%Subvert/{FFFF00,FFFFFF}/{FFFFFF,FFFF00}

かな、と思った。

jOOQ (Java Object Oriented Querying) の使い方の紹介

jOOQ (http://www.jooq.org/) についてのメモ。 jOOQは、type safeかつdatabase orientedなクエリビルダーである。 database orientedなので、「アプリケーションからDBのことなんて全然意識したくないねん」的な思想に基いて作られた類のプロダクトとは異なり、思い通りのSQLを生成できるようになっている。

以下gradle前提で。version等はよしなに。

gradle

dependenciesはこんな感じ

compile 'org.jooq:jooq:3.7.1'
compile 'org.jooq:jooq-meta:3.7.1'

テーブル等に相当するクラスをgradle taskから生成するためにbuildscriptのdependenciesにも以下をかいておく

classpath 'org.jooq:jooq-codegen:3.7.1'

んで、以下のようなコードをbuild.gradleに書く。MySQL前提です。細かい部分は自分の環境に合わせてよしなに。

def genXml = { db, writer ->
    new groovy.xml.MarkupBuilder(writer).configuration('xmlns': 'http://www.jooq.org/xsd/jooq-codegen-3.6.0.xsd') {
        jdbc() {
            driver("com.mysql.jdbc.Driver")
            url("jdbc:mysql://localhost:3306/")
            user("root")
            password("")
        }
        generator() {
            database() {
                inputSchema(db)
            }
            generate() {
            }
            target() {
                packageName("com.foo.bar.jooq." + db)
                directory("src/main/java")
            }
        }
    }
}

def generateJooqSources = { db -> 
    def writer = new StringWriter()
    def xml = genXml(db, writer)
    org.jooq.util.GenerationTool.generate(
        javax.xml.bind.JAXB.unmarshal(new StringReader(writer.toString()), org.jooq.util.jaxb.Configuration.class)
    )
}

で、以下のようにデータベース毎にtaskを定義する、と。

task("jooq-generate-example") << {
    generateJooqSources(name)
}

若干DRYではないがここでは気にしない。

初期化

単にクエリを発行するだけなら、ConnectionやDataSourceからDSLContextクラスのオブジェクトを作ればよい。

DSLContext ctx = DSL.using(dataSource, SQLDialect.MYSQL);

クエリの書き方

SQLでやりたいことは大体出来る。たまにめんどくさい書き方をする必要があるが...

以下、userというtableからコード生成して、適宜static importしている前提。

単品SELECT

UserRecord record = ctx.selectFrom(USER)
    .where(USER.ID.equal(userId)
    .fetchOneInto(UserRecord.class); 

カラムを指定して単品SELECT

返り値を制御する関係で、結果として二回カラム名を指定する必要がある

String name = ctx.select(USER.NAME)
    .from(USER)
    .where(USER.ID.equal(userId)
    .fetchOne(USER.NAME); 

複数SELECT

List<UserRecord> userRecords = ctx.selectFrom(USER)
    .where(USER.NAME.equal(name)
    .fetchInto(UserRecord.class);

特定のカラムの値をkeyにしたMapを取得

PerlのDBIでいうところのselectrow_hashref的なことも出来る。1対1のマップ。

Map<Integer, UserRecord> userRecords = ctx.selectFrom(USER)
    .where(USER.NAME.equal(name)
    .fetchMap(USER.ID, UserRecord.class);

特定のカラムの値をkey、keyの値でグルーピングされたレコードのListをvalueにしたMapを取得

PerlのDBIでいうところの…なんだっけ。あったっけか。1対nのマップ。

Map<Integer, List<UserRecord>> userRecords = ctx.selectFrom(USER)
    .where(USER.ID.in(userIds))
    .fetchGroups(USER.STATUS, UserRecord.class);

INSERT

ctx.insertInto(USER, USER.NAME, USER.STATUS)
    .values(name, 0)
    .execute();

batch INSERT

List<UserRecord> userRecords = new ArrayList<UserRecord>();
...
ctx.batchInsert(userRecords)
    .execute();

UPDATE

ctx.update(USER)
    .set(USER.NAME, name)
    .where(USER.ID.equal(userId))
    .execute();

DELETE

ctx.deleteFrom(USER)
    .where(USER.ID.equal(userId))
    .execute();

トランザクション

ctx.transaction(new TransactionalRunnable() {
    @Override
    public void run(Configuration configuration) throws Exception {
        // ここの中
    }
})

where条件の動的な組み立て

SelectQuery<UserRecord> query = ctx.selectFrom(USER).getQuery();

for (User u : users) {
    Condition condition = USER.NAME.equal(userName).and(USER.STATUS.equal(1));
    query.addConditions(Operator.OR, condition);
}

List<UserRecord> userRecords = query.fetchInto(UserRecord.class);

DAO

使ったことはないが生成できるらしい

http://www.jooq.org/doc/2.6/manual/sql-execution/daos/

で、実際どうなん?

たまにかゆい所もあるけど、一通りのことは出来てまあまあ使いやすいです。

ドキュメントが結構充実しているのと、開発者がstackoverflowでこまめにレス付けてる所を見ると、気合の入っていることが分かります。

JDBCによるJavaデータベースプログラミング 第2版

JDBCによるJavaデータベースプログラミング 第2版