AWS Lambda で NoClassDefFoundError

やろうとしたこと

AWS Lambda に jar をアップロードして実行したかった。

問題

jarを作成してAWSのコンソールにアップロードして実行すると、ローカルで作成した自作のモジュールのクラスが NoClassDefFoundError

環境

IntelliJ IDEA 2018.2.6 (Community Edition)
macOS 10.14.2
gradle-4.8-all
kotlin 1.2.71

やったこと

AWS ドキュメント - .zip デプロイパッケージの作成 (Java)

takeda-san.hatenablog.com

を参考に、以下のようなタスクを定義して ./gradlew :app:build

task buildZip(type: Zip) {
    from compileJava
    from compileKotlin
    from processResources
    into('lib') {
        from configurations.runtime
        from configurations.compileClasspath
    }
}
build.dependsOn buildZip

これでうまくいくはずだけど、AWSコンソールから実行すると NoClassDefFoundError と言われる。

jarをunzipしてlibを覗いてみると…

ローカルの自作モジュールがjarになってない!

原因

ローカルの自作モジュールに java-library プラグインを入れていたこと。

plugins {
    ...
    id 'java-library' // ←これ
}

とりあえずこれを消したらちゃんとjarを作ってくれてうまく実行できました。





AWS Lambda実践ガイド (impress top gear)

AWS Lambda実践ガイド (impress top gear)

ndk-buildにパラメータを渡す

コマンドラインから外部引数を渡したい場合は、 ndk-build -e で指定することでパラメータを渡すことができる。

$ ndk-build -e HOGE=HOGE MAGE=MAGE

これで Android.mk から $(HOGE) とか $(MAGE) とかで値を利用できる。

 

 

AndroidNDKネイティブプログラミング第2版

AndroidNDKネイティブプログラミング第2版

dependenciesの@aarの正体

build.gradleでしばしば見かけるこの記法

dependencies {
   implementation "jp.co.mst.android:awsome:1.0.0@aar"
}

この @aar という記法は アーティファクトオンリー記法 といい、以下に説明があります。

第51章 依存関係の管理

アーティファクトオンリー記法は、指定した拡張子のファイルのみをダウンロードする、というモジュール依存関係を作成します。ディスクリプタがあっても無視されます。

ライブラリの依存関係を無視する

例えば、 RxAndroidbuild.gradleを覗いてみますと

dependencies {
    api 'io.reactivex.rxjava2:rxjava:2.2.0'
    ...
}

とあるように、RxAndroidはRxJavaに依存しています。

なので、アプリからRxAndroidをgradleで取り込むと勝手にRxJavaがついて来るのですが、RxAndroidを取り込むときに以下のように @aar をつけると、RxAndroidのaarだけ持ってきて、RxJavaは取り込まれなくなります。

dependencies {
   implementation 'io.reactivex.rxjava2:rxandroid:2.1.0@aar'
}

何が嬉しいの?

あんまり積極的にお目にかかる機会もないとは思いますが、この記法のミソは

ディスクリプタがあっても無視されます。

というところでしょうか。

ディスクリプタというのは、mavenリポジトリ上で依存関係を管理するファイルで、RxAndroidの場合は rxandroid-2.1.0.pom がそれに当たります。

たとえば、何らかのとてつもない事情があってmavenリポジトリ上のディスクリプタが壊れてしまい、ライブラリの依存関係の解決ができなくなってしまったが、自分には治す権限がないけどaarだけ取り急ぎ使いたい!といったものすごく込み入った事情がある場合なんかには重宝されるかもしれません。

あるいは、ライセンス上取り込んじゃまずいライブラリの依存関係だけが何故か記述されてしまっているような罠に掛かりそうになったときなどにも使えるかもしれません。

実際には、多分 aarをローカルから読み込む - Learn to Live and Live to Learn などの記事にあるように、ローカルのパスからディスクリプタを使わないでaarを指定して取り込むときに使うことが大多数なのかなという気はしますが、社内リポジトリMaven Central Repository などで公開されているライブラリを @aar 付きで取り込んでるコードをどこかで見かけることがありましたら、そこには並々ならぬ事情があるのかもしれません。

アプリの依存関係とライブラリの依存関係がかぶってるときは@aarつけて取り込んだほうがいいの?

依存関係がかぶっていてライブラリのバージョンの競合が発生するような場合は、バージョンの競合を解決するための戦略が別途ありますので、@aar をつけることが最適な解決策になるケースは限られているでしょう(デフォルトではより新しいバージョンが採用されるようになってます)。

ちなみに、今回例に出したRxAndroidは、RxAndroidのバージョンによらず常に最新のRxJavaをアプリ側で取り込むことを推奨しています。

Gradle徹底入門 次世代ビルドツールによる自動化基盤の構築

Gradle徹底入門 次世代ビルドツールによる自動化基盤の構築

再生時間をLongから mm:ss 形式に変換する

やりたいこと

Android の MediaPlayer などで Long で取れる duration を、2:50 のように 「分:秒」の形式の文字列にする。

joda-timeを使えば簡単

LocalTime を使うのもよいが、API Level 26以上を要求されるので、 joda-time-android を使う。

// build.gradle
dependencies {
    implementation 'net.danlew:android.joda:x.x.x.x'
}
val format = DateTimeFormat.forPattern("mm:ss")
val str = format.print(duration)

雨が降りそうになったらGoogle Homeに教えてもらえるようにした

やったこと

雨が降りそうになったらGoogle Homeが「雨が降りそうです」と教えてくれるようにした。

環境

ハードウェア

ミドルウェア

  • raspbian 9.4
  • node.js 9.11.1
  • google-home-notifier 1.2.0

WebAPI

環境づくり

Raspberry Pi のセットアップ

以下の記事を参考にOSのインストールからsshでの接続まで。

MacユーザがRaspberry Pi2をセットアップする-1 | scribble warehouse

node.jsはこちらの記事を参考に9.11.1を入れた。
第三回 Raspberry Pi 3に最新のNode.jsをインストールする

$ apt-get update
$ apt-get install -y nodejs npm
$ npm cache clean
$ npm install n -g
$ n 9.11.1

Google Homeを能動的に喋らせる

Google Homeを喋らせるにはgoogle-home-notifierを使うのが楽とのことで、以下の記事を参考にセットアップ。
Google Home開発入門 / google-home-notifier解説

$ npm init
$ npm install  google-home-notifier

dns_sd.h がありません的なエラーが出たので以下を実行。

$ sudo apt-get install libavahi-compat-libdnssd-dev

天気の取得

天気取得APIはいくつかあるが、以下の要件を満たせていて手軽に叩けたので今回は YOLP(地図):気象情報API - Yahoo!デベロッパーネットワーク を使う。
API自体シンプルでcurlサンプルもあったのが嬉しい。

  • 緯度・経度で指定できる
  • 現在の天気が取れる
  • 少しあとの天気予報が取れる

nodeの環境変数

APIの呼び出しにYahoo!のAppIDが必要だったので、以下の記事を参考に dotenv をインストール。
Nodeプロジェクトで環境依存の設定の管理方法

$ npm install dotenv --save

.envは忘れず.gitignoreへ…。
せっかくなのでgitで晒すのは憚られる以下の情報も.envに書いた。

コード

Github

GitHub - ergooo/GoogleHomeWeatherNotifier: 雨が振りそうになったらGoogleHomeに喋ってもらう

javascript初心者なのでいろいろ有り得ないところがあるかもしれませんが…。

GoogleHomeを喋らせる

// GoogleHome.js
module.exports = class GoogleHome {
  constructor(ipAddress) {
    this._ipAddress = ipAddress
  }
  tell(message) {
    const googlehome = require('google-home-notifier')
    const language = 'ja';

    googlehome.device('Google-Home', language); 
    googlehome.ip(this._ipAddress);
    googlehome.notify(message, function(res) {
      console.log(res);
    });
  }
}

ほぼサンプルそのまま。IPアドレスとメッセージを受け取って喋らせる。

天気取得

// WeatherApi.js
module.exports = class WeatherApi {
  constructor(appId) {
    this._appId = appId
  }

  /**
  * @param {WeatherApi~RequestCallback} callback
  */
  request(latitude, longitude, callback) {
    const request = require('request');
    const url = this._buildUrl(latitude, longitude, this._appId)
    console.log('url: ' + url)
    const options = {
      url: url,
      method: 'GET'
    }

    request(options, function (error, response, body) {
      if (!error && response.statusCode == 200) {
        const json = JSON.parse(body);
        callback(error, json.Feature[0].Property.WeatherList.Weather)
      } else {
        callback(error, null)
      }
    })
  }

  _buildUrl(latitude, longitude, appId) {
    return 'https://map.yahooapis.jp/weather/V1/place?' + 
      'output=json' + 
      '&coordinates=' + longitude + ',' + latitude +
      '&appid=' + appId
  }
}

javascriptでHTTPするのは require('http') する方法と requeire('request') する方法があるようだったが、後者のほうがナウくて楽なようだったので後者で。 JSONのパーズは何も考えなくてもやってくれるので非常に手早く書ける。

天気をチェックして喋らせる

// WeatherCheckerMain.js
module.exports = class WeatherCheckerMain {
  static check() {
    require('dotenv').config()
    const GoogleHome = require('./GoogleHome')
    const googleHome = new GoogleHome(process.env.NODE_GOOGLE_HOME_IP_ADDRESS)

    const WeatherApi = require('./WeatherApi')
    const weatherApi = new WeatherApi(process.env.NODE_YAHOO_APP_ID)
    weatherApi.request(process.env.NODE_LATITUDE, process.env.NODE_LONGITUDE, function(error, weathers) {
      if(!error) {
        console.log('request ok.')
        const current = weathers[0]
        const next = weathers[1]
        if(current.Rainfall == 0 && next.Rainfall > 0) {
          googleHome.tell("雨が振りそうです。")
        }
      } else {
        console.log(error)
      }
    })
  }
}

定期実行させやすそうだったのでメインの処理もクラスにしてみた。 Yahoo!APIでは、現在とその後10分おきの降水予報が取れたので、現在降水0で10分後降水0以上なら通知するようにした。

定期実行

node.jsのいろいろなモジュール14 – node-cronでcron的にプログラムを実行する | Developers.IO こちらの記事を参考にほぼコピペで。タイムゾーン部分だけエラーになったので直した。

$ npm install cron time
// cron.js
const cronJob = require('cron').CronJob;
const cronTime = "00 00,10,20,30,40,50 00,01,09-23 * * *";
const job = new cronJob({
  cronTime: cronTime
   , onTick: function() {
    console.log('onTick!');
    let Main = require('./WeatherCheckerMain')
    Main.check()
  }
   , onComplete: function() {
    console.log('onComplete!')
  }
  , start: true
  , timeZone: "Asia/Tokyo"
})
 
job.start();

実行

pm2 でcron.jsを実行するようにした。

$ npm install pm2 -g
$ pm2 start cron.js

感想

なんだかんだ1ヶ月ぐらいかかるかと思ったけどほぼコピペでトラブルなく行けたので1日でできてしまった。 nodeは手軽でサクッとできるというとても良い印象が得られた。 まだ雨が降ってないので本当にちゃんと動くかわからないけど、とりあえず満足。

java libraryのテストからresourcesが見えない

環境

Android Studio 2.3.1

やりたいこと

Android StudioJava Libraryのモジュールを作成し、テストコードからリソースファイルを読み込みたい。

うまくいかない

リソースの読み込みはgetResouce()すれば取れるはずなのだが、main/resources/にファイルを配置してもどういうわけか結果はnullになる。
build/resourcesに対象のリソースファイルが生成されてるので、ビルドはうまくいっている。

URL url = getClass().getClassLoader().getResource("trades.json");
System.out.println(url);

結果

null

解決

build.gradleに以下を追加

sourceSets {
    test {
        output.resourcesDir = output.classesDir
    }
}

参考

stackoverflow.com

qiita.com

Kotlin + Pure Java + JUnit で "Empty test suite."

AndroidStudioで開発中のモジュールを切り出してPure Javaなモジュールにした時に、JUnitがクラスを見つけてくれなくなってしまってかなりハマりました。

エラーメッセージ

Class not found: "jp.example.HogeTest"Empty test suite.

解決

build.gradleに以下を記述

apply plugin: 'kotlin'