Dagger2(android support module)をつかってFragmentにDIする

Dagger2(android support module)をつかってFragmentにDIする方法をまとめていく。

HasSupportFragmentInjectorを実装する

MainActivityにHasSupportFragmentInjectorを継承させ必要な実装をします。

class MainActivity : AppCompatActivity(), HasSupportFragmentInjector {

    @Inject
    lateinit var androidInjector: DispatchingAndroidInjector<Fragment>

    override fun supportFragmentInjector() = androidInjector

    override fun onCreate(savedInstanceState: Bundle?) {
        AndroidInjection.inject(this)
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setContentFragment(R.id.mainLayout)
    }

    private fun setContentFragment(containerViewId: Int) {
        supportFragmentManager.let { manager ->
            manager.findFragmentById(containerViewId)?.let { return }
            ForecastsFragment.newInstance().apply {
                manager?.beginTransaction()?.add(containerViewId, this)?.commit()
            }
        }
    }
}

Fragmenのモジュールをまとめる

Fragmentで管理するモジュールを MainModuleとしてまとめる。

@Module
internal abstract class MainModule {

    @ContributesAndroidInjector
    abstract fun contributeMainFragment(): ForecastsFragment

}

MainActivityのSubcomponentにMainModuleを追加する。

@Module
internal abstract class UiModule {

    @ContributesAndroidInjector(modules = [MainModule::class])
    internal abstract fun contributeMainActivity(): MainActivity
}

今後、MainActivityに機能が追加する場合は、MainModuleに依存するモジュールを追加していく。

Fragmenのライフサイクルに合わせてInject

サンプルのコードではonAttachでInjetctをしている。

class ForecastsFragment : Fragment() {

    @Inject
    lateinit var openWeatherMapRepository: OpenWeatherMapRepository

    private var listView: ListView by Delegates.notNull()

    override fun onAttach(context: Context?) {
        AndroidSupportInjection.inject(this)
        super.onAttach(context)
    }

    override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        val view = inflater?.inflate(R.layout.forcasts_fragment, container, false) ?: return null
        listView = view.findViewById<ListView>(R.id.list_view)
        return view
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        openWeatherMapRepository.findForecastByDaily()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe { forecasts ->
                    listView.adapter = ArrayAdapter<String>(activity, android.R.layout.simple_list_item_1,
                            forecasts.list.map {
                                "%s - %s %s/%s".format(
                                        DateUtils.formatDateTime(activity, it.dt * 1000L, FORMAT_NO_YEAR),
                                        it.weather.get(0).main, it.temp.min, it.temp.max)
                            })
                }
    }

    companion object {
        fun newInstance() = ForecastsFragment()
    }
}
  • Fragmentの理解は公式のドキュメントが分かりやすい。
  • Fragmentを部品と捉えてタブレットとハンドセットで異なるレイアウトでも部品(Fragment)を組み合わせることで柔軟に対応することができるし部品の再利用も容易にできる。

コード

今回のコードはこちらのPRにまとまっていますので参考になれば嬉しいです。

github.com

Dagger2 (android support module)とretrofit2をつかってAPIレスポンスをListViewで表示する

掲題のとおりAndroidのListViewを表示してみる。 APIリクエストは retrofitを使い天気情報を取得できるOpenWeatherMapのAPIを利用する。

github.com

DIにはDaggerを使い、2.11から有効なandroid support moduleを利用する。

github.com

APIをリクエストするServiceクラスをつくる

interface OpenWeatherMapService {

    @GET("/data/2.5/forecast/daily?q=94043&mode=json&units=metric&cnt=7&APPID=XXXXX")
    fun findForecastByDaily(): Observable<Forecasts>
}
  • レスポンスの型はObservable<Forecasts>。型パラメータのForecastsは Parcelableを実装したDTO。
  • APPID=XXXXXはopenweathermapから取得したID

このServiceクラスをRepositoryクラスから呼び出し見通しの良いコードにするためにDIを利用していく。DIについては後述する。

Parcelableを実装したDTO(data class)

data class Forecasts(var cod: Int, var list: List<Forecast>) : Parcelable {

    constructor(src: Parcel) : this(
            cod = src.readInt(),
            list = src.createTypedArrayList(Forecast.CREATOR)
    )

    // -
}

ActivityやFragmentにパラメータを渡すために Parcelableを実装したdata classを用意する。 フィールドにプリミティブ型ではないオブジェクト型を使う場合は次のようにする。

data class Forecast(var dt: Long, var temp: Temp, var weather: List<Weather>) : Parcelable {

    constructor(src: Parcel) : this(
            dt = src.readLong(),
            temp = src.readParcelable(Temp::class.java.classLoader),  //  ← data class `Temp`
            weather = src.createTypedArrayList(Weather.CREATOR) //  ← List型のパラメータに data class `Weather`
    )

    override fun describeContents(): Int {
        return 0
    }

    override fun writeToParcel(dest: Parcel?, flags: Int) {
        dest?.writeLong(dt)
        dest?.writeParcelable(temp, flags) //  ← data class `Temp`
        dest?.writeList(weather) //  ← List型のパラメータに data class `Weather`
    }

    // -
}

MainActivityでDIする

MainActivityでOpenWeatherMapServiceを提供するRepositoryクラスをInjectするまでの過程をまとてめていく。

RepositoryModuleをつくる

class OpenWeatherMapRepository(val openWeatherMapService: OpenWeatherMapService) {
    fun findForecastByDaily() = openWeatherMapService.findForecastByDaily()
}
@Module
internal object RepositoryModule {

    @Provides
    @Singleton
    @JvmStatic
    fun provideOpenWeatherMapRepository(openWeatherMapService: OpenWeatherMapService) =
            OpenWeatherMapRepository(openWeatherMapService)
}

OpenWeatherMapRepositoryを提供するRepositoryModuleをつくった。

DataModuleをつくる

RepositoryModuleをIncludeしたDataModuleをつくる。このモジュールでRetrofitクライアントをビルドする。

@Module(includes = arrayOf(RepositoryModule::class))
internal object DataModule {

    @Provides
    @Singleton
    @JvmStatic
    fun provideMoshi() = Moshi.Builder()
            .add(KotlinJsonAdapterFactory())
            .build()

    @Provides
    @Singleton
    @JvmStatic
    fun provideOkHttp(): OkHttpClient = OkHttpClient.Builder()
            .build()

    @Provides
    @Singleton
    @JvmStatic
    fun provideRetrofit(oktHttpClient: OkHttpClient, moshi: Moshi): Retrofit = Retrofit.Builder()
            .client(oktHttpClient)
            .baseUrl("http://api.openweathermap.org")
            .addConverterFactory(MoshiConverterFactory.create(moshi))
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .build()

    @Provides
    @Singleton
    @JvmStatic
    fun provideOpenWeatherMapService(retrofit: Retrofit) = retrofit.create(OpenWeatherMapService::class.java)
}
  • JSONパーサにはKotlinサポートが入っているMoshiをつかう

@ContributesAndroidInjectorをつかいMainActivityへのInjectを定義する

Dagger 2.11の重要ポイントの1つ。ActivityへのInjectは @ContributesAndroidInjectorをつかいUiModuleをつくる。

@Module
internal abstract class UiModule {

    @ContributesAndroidInjector
    internal abstract fun contributeMainActivity(): MainActivity
}

Activityが増えたときには、ここにActivityへのInjectを追加する。

ApplicationComponentに AndroidInjector<KotlinApplication> を継承させる

Dagger 2.11の重要ポイントの1つ。ApplicationクラスへInjectさせるためにApplicationComponentに AndroidInjector<KotlinApplication> を継承させる。後述するApplicationの親クラスに dagger.android.support.DaggerApplicationを使うためmoduleにAndroidSupportInjectionModule を追加する。

@Singleton
@Component(modules = arrayOf(AndroidSupportInjectionModule::class,
        AppModule::class,
        DataModule::class,
        UiModule::class))
interface ApplicationComponent : AndroidInjector<KotlinApplication> {

    @Component.Builder
    interface Builder {
        @BindsInstance
        fun application(application: KotlinApplication): Builder
        fun build(): ApplicationComponent
    }

    override fun inject(application: KotlinApplication)
}

Applicationクラスに DaggerApplicationを継承させ実装する

HasActivityInjectorを継承する流れを紹介するエントリもあるが DaggerApplicationHasActivityInjectorの実装が含まれているのでこちらをつかう。

class KotlinApplication : DaggerApplication() {

    override fun applicationInjector() = DaggerApplicationComponent.builder()
            .application(this)
            .build()

    override fun onCreate() {
        super.onCreate()
    }
}

MainActivityにOpenWeatherMapRepositoryをInjectする

最後にMainActivityにOpenWeatherMapRepositoryをInjectする。Dagger 2.11の重要ポイントの1つ。InjectするためにはonCreateでAndroidInjection.inject(this)を呼び出す。

class MainActivity : AppCompatActivity() {

    @Inject lateinit var openWeatherMapRepository: OpenWeatherMapRepository

    override fun onCreate(savedInstanceState: Bundle?) {
        AndroidInjection.inject(this)

    // -
}

APIレスポンスをListViewに表示する

class MainActivity : AppCompatActivity() {

    @Inject lateinit var openWeatherMapRepository: OpenWeatherMapRepository

    override fun onCreate(savedInstanceState: Bundle?) {
        AndroidInjection.inject(this)
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        openWeatherMapRepository.findForecastByDaily()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe { forecasts ->
                    findViewById<ListView>(R.id.listview).let { view ->
                        view.adapter = ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,
                                forecasts.list.map {
                                    "%s - %s %s/%s".format(
                                    DateUtils.formatDateTime(this, it.dt * 1000L, FORMAT_NO_YEAR),
                                            it.weather.get(0).main, it.temp.min, it.temp.max)
                                })
                    }
                }
    }
}

ListのItemViewには simple_list_item_1をつかって日にちと最高気温と最低気温を表示している。

まとめ

  • Daggert2(android support module)のDIをまとめた。android support module以前のDI方法だとコピペコードが増える懸念があり登場した経緯を知ってなるほど、と思った。
  • retrofitはシンプルな使い方までに留まっているので引き続き触っていきながら知見をまとめていきたい。

コード

このエントリまでのコードがPull Requestにまとまっていますので参考になれば嬉しいです。(初回のコミットなので不要なlayoutコードなどが散見してます。)

github.com

参考

minikube + helmでローカル環境を構築する

plasmaの動作確認のためにローカル環境を構築する機会がありminikube + helmで構築してみようと始めたのがエントリのモチベーション。

github.com

plasmaはServer Push型のミドルウェアでFRESH!で使われている。ポーリング撲滅を掲げgRPC/SSEを用いて省コネクションでイベントのSubscribeを実現している。
plasmaの動作確認にはplasmaredisのミドルウェアとイベントをSubscribeするアプリケーションが必要(kotlin + SpringBootで書いた)。これらをコンテナ化してローカル環境で確認していきたい。

これまでのローカルのコンテナ実行環境は docker-composeでやっていたけど、Kubernetesの初学も兼ねてminikubeでやってみる。helmを使いコンテナ全体をパッケージングしていく。

minikubeでkubernetes環境を起動する

$  minikube start
$  eval $(minikube docker-env)

必要なDockerイメージをビルドまたはプルした状態がこちら。

$ docker images | grep -v "gcr.io"
REPOSITORY                                             TAG                 IMAGE ID            CREATED             SIZE
soushin/plasmacli                                      latest              49ca95df3dad        2 days ago          942MB
redis                                                  latest              861cc310cd91        4 days ago          107MB
openfresh/plasma                                       latest              7ff567596426        6 weeks ago         16.6MB
java                                                   openjdk-8           d23bdf5b1b1b        12 months ago       643MB

gcr.io/*のイメージはリストから排除しています

helmを使いコンテナ全体をパッケージングをする

$ helm create plasmacli
$ tree ./plasmacli
./plasmacli
├── Chart.yaml
├── charts
├── templates
│   ├── NOTES.txt
│   ├── _helpers.tpl
│   ├── deployment.yaml
│   ├── ingress.yaml
│   └── service.yaml
└── values.yaml

helm create {packageName}で雛形を生成する。


このエントリで修正したyamlは values.yamltemplates/deployment.yamlの2つ。
templates/deployment.yamlにコンテナとコンテナ内の環境変数、Internal/Externalのポートを定義する。値は values.yamlから参照する。

templates/deployment.yamlとvalues.yaml
# `plasmacli`コンテナ定義を抜粋

      containers:
        - name: plasmacli
          image: "{{ .Values.plasmaCli.image.repository }}:{{ .Values.plasmaCli.image.tag }}"
          imagePullPolicy: {{ .Values.plasmaCli.image.pullPolicy }}
          ports:
            - name: plasmacli
              containerPort: {{ .Values.service.internalPort }}
          env:
          - name: PLASMA_CLI_PORT
            value: {{ .Values.service.internalPort | quote }}
          - name: PLASMA_HOST
            value: {{ .Values.plasmaCli.env.plasmaHost | quote }}
          - name: PLASMA_PORT
            value: {{ .Values.plasmaCli.env.plasmaPort | quote }}

# 関連する値を `values.yaml`から抜粋

plasmaCli:
  image:
    repository: soushin/plasmacli
    tag: latest
    pullPolicy: IfNotPresent
  env:
    plasmaHost: localhost
    plasmaPort: 50051

service:
  type: NodePort
  port: 80
  internalPort: 8080

パッケージングしたchartをインストールする

$ helm package plasmacli
$ helm install --name plasmacli local/plasmacli
NAME:   plasmacli
LAST DEPLOYED: Sat Jan 27 11:30:23 2018
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==> v1/Service
NAME       TYPE      CLUSTER-IP    EXTERNAL-IP  PORT(S)       AGE
plasmacli  NodePort  10.98.66.149  <none>       80:31118/TCP  0s

==> v1beta2/Deployment
NAME       DESIRED  CURRENT  UP-TO-DATE  AVAILABLE  AGE
plasmacli  1        0        0           0          0s

podを確認してみると無事起動している。

$ kubectl get pods -n default
NAME                        READY     STATUS    RESTARTS   AGE
plasmacli-957c79484-d4dkh   3/3       Running   0          2d

クラスタの外からアクセスする

クラスタの外からのアクセスにはserviceをNodePort化したのでポート番号が払い出されている。
アクセスURLは次のように確認できる。

$ minikube service plasmacli --url
http://192.168.64.12:31118

このURLのバックエンドにはイベントをSubscribeするアプリケーションが設定されているので次のように /health_check が叩けるようになっている。

$ curl http://192.168.64.12:31118/health_check
true

plasmaの動作確認をしてみよう

イベントをSubscribeするアプリケーションではイベント名:my-eventをSubscribeするようにしている。

redisからchannelへPublishする。

PUBLISH plasma '{"meta": { "type": "my-event"}, "data": "HELLO"}'
PUBLISH plasma '{"meta": { "type": "my-event"}, "data": "My Plasma"}'

イベントをSubscribeするアプリケーションのログの最後にPayloadデータ(HELLO, My Plasmaの文字列)が出力できた。

$ kubectl logs -f  plasmacli-957c79484-d4dkh -c plasmacli

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::             (v2.0.0.M7)

[INFO ][2018-01-27 11:30:26.380] Starting Application.Companion on plasmacli-957c79484-d4dkh with PID 5 (/usr/local/plasma-cli/lib/plasma-cli.jar started by root in /)
[INFO ][2018-01-27 11:30:26.420] No active profile set, falling back to default profiles: default
[INFO ][2018-01-27 11:30:26.582] Refreshing org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext@445b84c0: startup date [Sat Jan 27 11:30:26 GMT+09:00 2018]; root of context hierarchy
[INFO ][2018-01-27 11:30:29.531] Mapped (Accept: [application/json] && /health_check) => {
 (GET && /) -> org.springframework.web.reactive.function.server.RouterFunctionDsl$GET$1@709ba3fb
}
[INFO ][2018-01-27 11:30:29.564] Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.reactive.resource.ResourceWebHandler]
[INFO ][2018-01-27 11:30:29.565] Mapped URL path [/**] onto handler of type [class org.springframework.web.reactive.resource.ResourceWebHandler]
[INFO ][2018-01-27 11:30:29.661] Looking for @ControllerAdvice: org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext@445b84c0: startup date [Sat Jan 27 11:30:26 GMT+09:00 2018]; root of context hierarchy
[INFO ][2018-01-27 11:30:31.598] Registering beans for JMX exposure on startup
[INFO ][2018-01-27 11:30:31.745] Started HttpServer on /0.0.0.0:8080
[INFO ][2018-01-27 11:30:31.761] Netty started on port(s): 8080
[INFO ][2018-01-27 11:30:31.773] Started Application.Companion in 6.292 seconds (JVM running for 7.875)

[INFO ][2018-01-27 11:46:06.378] stream observe: onNext={"HELLO"}
[INFO ][2018-01-27 11:48:48.386] stream observe: onNext={"My Plasma"}

コードはgithubにあります

github.com

コードはgithubに置いてますので合わせて参照ください。

まとめ

  • 簡易的なplasmaの動作確認環境が構築できた。
  • これからは公開するアプリケーションのコンテナパッケージはhelmで公開していこうとモチベーションがあがった。
  • kubectlの各種コマンドはDockerコマンドライクなところが多く、ここらへんはとても飲み込みやすい。
  • 今回は初学ということで用語の解説などを飛ばしまったので触っていきながら用語の理解を深めていく必要を感じた。
  • helmのvaluesで定義した値とdeployment.yamlで参照する値のKeyのマッピングにミスが多かったのでIDEのhelmライブラリがあると便利そう。
  • Serviceの ClusterIPやPod間のアクセスなど気になるところが数多あるので引き続きアウトプットしていく。

SpringBoot 2.0.0でRouterFunctionのエラーハンドリングをWebExceptionHandlerで行う

SpringBoot 2.0.0からサポートされるRouterFunctionのエラーハンドリングをまとめていきたい。

RouterFunctionは従来のアノテーションベースでAPIを作る形ではなくDSLベースでルーティングを定義していく。

@Configuration
class TaskRoutes(private val taskHandler: TaskHandler) {

    @Bean
    fun taskRouter() = router {
        (accept(APPLICATION_JSON) and "/api").nest {
            "/task".nest {
                POST("/", taskHandler::create)
                GET("/{id}", taskHandler::fetchByTaskId)
                PUT("/{id}", taskHandler::updateByTaskId)
                DELETE("/{id}", taskHandler::deleteByTaskId)
                PUT("/{id}/finish", taskHandler::finishByTaskId)
            }
            "/tasks".nest {
                GET("/", taskHandler::fetchAll)
            }
        }
    }
}

TaskHandlerクラスは各エンドポイントを処理するが処理中に発生したエラーを共通的にハンドリングする場合にはどうすればよいか? そんなときは WebExceptionHandlerを実装すれば良い。

@Component
class ApiErrorHandler(private val objectMapper: ObjectMapper) : WebExceptionHandler {

    private val logger = KotlinLogging.logger {}

    override fun handle(exchange: ServerWebExchange, ex: Throwable): Mono<Void> {
        return handle(ex)
                .flatMap {
                    it.writeTo(exchange, HandlerStrategiesResponseContext(HandlerStrategies.withDefaults()))
                }
                .flatMap {
                    Mono.empty<Void>()
                }
    }

    private fun handle(t: Throwable): Mono<ServerResponse> {
        return when (t) {
            is SystemException -> {
                "api error".let {
                    logger.error(t) { t.message ?: it }
                    createResponse(t.status, t.message ?: it)
                }
            }
            is DecodingException -> {
                "invalid request".let {
                    logger.warn(t) { t.message ?: it }
                    createResponse(HttpStatus.BAD_REQUEST, it)
                }
            }
            else -> {
                logger.error(t) { "Unknown Exception: %s".format(t.message ?: "unknown error") }
                createResponse(HttpStatus.INTERNAL_SERVER_ERROR, t.message ?: "internal server error")
            }
        }
    }

    private fun createResponse(httpStatus: HttpStatus, message: String, code: String? = null): Mono<ServerResponse> {
        return Error(objectMapper.writeValueAsString(listOf(ErrorItem(message, code, null)))).let {
            ServerResponse.status(httpStatus).syncBody(it)
        }
    }
}

private class HandlerStrategiesResponseContext(val strategies: HandlerStrategies) : ServerResponse.Context {

    override fun messageWriters(): List<HttpMessageWriter<*>> {
        return this.strategies.messageWriters()
    }

    override fun viewResolvers(): List<ViewResolver> {
        return this.strategies.viewResolvers()
    }
}

[WebExceptionHandler.handle]の引数exに例外クラスが渡ってくるので、メッセージとHTTPレスポンスを組み立てレスポンスBodyにセットしている。

404エラーの場合のエラーハンドリングが正しくできていることを確認。

curl -v -XGET http://localhost:8080/api/task/2
Note: Unnecessary use of -X or --request, GET is already inferred.
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /api/task/2 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.56.0
> Accept: */*
>
< HTTP/1.1 404 Not Found
< transfer-encoding: chunked
< Content-Type: application/json;charset=UTF-8
<
* Connection #0 to host localhost left intact
{"message":"task not found","errorCode":"404","field":null}

まとめ

  • これまでFilterFunctionでエラーハンドリングを行っていたが開発をする過程で間違っていることに気づいた。ただしくWebExceptionHandlerを利用する方法をアウトプットした。合わせて Web on Reactive Stack を参照してほしい。

コード

コードはgithubにあります。

github.com

詳細なコードは次のリンクからどうぞ。 spring5-kotlin-application/ApiErrorHandler.kt at master · soushin/spring5-kotlin-application · GitHub

モダンな負荷テストツールのk6を試してみた

負荷テストツールのk6を試す機会があったのでアウトプットしていく。

k6.io

k6は負荷テストサービスを提供するloadimpact社が開発する負荷テストツールでツール自体はgoでテストケースはES6で書く。

レポート出力はInfluxDB+Grafanaまたはloadimpact Insightsと連携する。連携方法はテスト実行時のコマンドにDB URLやTokenを加えるだけのシンプルな設計。

最近はgRPC通信の採用も増えているが今のところk6ではサポートされていない。ただ負荷テストサービスを提供するloadimpact社なので今後のサポートに期待できる。オープンソースだしgoだし自分で作りたい欲も出てくるのがモダンなk6の良いところ。

今回のエントリは簡易的なAPIサーバに負荷テストを実行するまでの流れをコードを交えてまとめていく。

k6をdockerで動かす

サクッと試せる環境を作りたかったのでAPIサーバとk6のコンテナをdocker-composeで構成管理した。k6はイメージが提供されているので簡単に動かせる。

docker pull loadimpact/k6

テストシナリオを書く

テストシナリオはEC6で書く。馴染みがあるJavascriptでシナリオを書けるのが良い。

テストシナリオの書き方で理解したかったのが次の2点。

上記、githubのリンクにあるとおり難しくなくドキュメントやサンプルも豊富ですぐに理解できた。

また、checkgroupthresholdsはテストシナリオを書く上で重要な要素となる。勿論、その他にも理解すべき要素はある。ドキュメントが豊富なので触っていきながら理解を深めていきたい。

docs.k6.io

k6 runオプションを理解する

テストの実行はk6 runのコマンドを実行する。どれくらいのユーザをアクセスさせるか、どれくらいテストを続けるかのオプションを理解する必要がある。今回使ったオプションは次の通り。

  • vus 同時接続ユーザ数の理解で良さそう。
  • iterations テストの実行回数。
k6 run --vus 5 --iterations 5 ./scripts/localhost.js

上記のコマンドだと5ユーザがアクセスしてテスト回数が5回行われる。iterationsは5ユーザが5回テストを実行するのではなく、全体のテスト回数が5回となる。そのため5ユーザが1回テストを実行するコマンドとなる。

またdurationstageも利用頻度が高いオプションになる。durationはテスト実行期間を指定できる、stageはJmeterのramp upのようなオプションである。

docs.k6.io

テスト結果をグラフに出力してみた

loadimpact Insightsにテスト結果を出力してみた。

docs.k6.io

連携方法もシンプル。loadimpact.comのアカウントをつくりダッシュボードからCLOUDトークンを発行してrunオプションに加えるだけである。連携した結果のイメージは次のとおり。

f:id:n_soushi:20171111221320p:plain

vusの数やテストシナリオの数でアカウントのグレードが異なり有料オプションとなる。継続的に負荷テストを実行するのであれば有料オプションを選択することを検討するのもありだし、InfluxDB+Grafanaの連携方法もある。標準のstdoutからもテスト結果は確認できるので、どのように負荷テストを運用するかでレポート運用方法の選択肢はこちらに委ねられている印象。

docs.k6.io

サクッと動かせるコードあります

githubに動かせるコードを置いたので興味ある人は参照ください。

github.com

grpc-ecosystem/grpc-gatewayのセットアップ方法をまとめてみた

Server Side Kotlinのアウトプットにつかっているレポジトリにgrpc-gatewayを入れてみたので、セットアップ方法をまとめておく。

github.com

grpc-gatewayはHTTP/1.1のAPIの提供する。定義したprotoにoptionを加えることでHTTP/1.1 APIのコードとgrpc-clientのリクエストを仲介するgoコードが生成される。

定義したprotoに次のようなoptionを加える。

service TaskService {
    rpc GetTaskService (GetTaskInbound) returns (TaskOutbound) {
        option (google.api.http) = {
            post: "/grpcgateway/task"
            body: "*"
            };
    }
}

message GetTaskInbound {
    uint32 task_id = 1;
}

このoption定義によるとPOSTメソッドで パスが/grpcgateway/taskのエンドポイントができあがる。リクエストボディには次のようにprotoに定義したGetTaskInboundのmessage定義をJSON形式でセットしてリクエストする。

$ curl -XPOST \
           http://localhost:8081/grpcgateway/task \
           -H 'Content-type: application/json' \
           -d '{"task_id": 1}'

次にprotocでHTTP/1.1 APIのコードとgrpc-clientのリクエストを仲介するgoコードを生成する。

protoc -I/usr/local/include \
    -I./proto \
    -I$$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
    --grpc-gateway_out=logtostderr=true:grpc/gen/ \
    ./proto/soushin/spring5-kotlin-application/task/task.proto

このprotocを実行すると task.pb.gw.goが生成される。生成されたproxyコードを使ってエンドポイントに定義すれば良い。 定義の仕方はgrpc-gatewayのexmapleが分かりやすい。

grpc-gateway/main.go at master · grpc-ecosystem/grpc-gateway · GitHub

コード

動くコードをgithubに置いたので合わせて確認してほしい。

github.com

CA.ktに登壇してServer Side Kotlinについて話してきた。

CA.kt #3で登壇しました。テーマは「FRESH!プロジェクト/Server Side Kotlin 活用事例」。スライドはこちら。

speakerdeck.com

約1年前にFRESH!チームにジョインしてKotlinに出会いServer Side Kotlinと向き合ってきた経験を話してきた。

Server Side Kotlinの導入事例やIdeomatic Kotlin Codeをテーマにしたコード事例を紹介したり、Spring Boot 2.0とgRPCのチャレンジ、DBアクセスには何を使ってるの?、テストは?、Apiドキュメンテーションは?など、Server Side Kotlinを俯瞰して横断的な内容をまとめられと思う。

興味ある方は是非スライドを参照ください👀

そして今回のエントリは、これまでアウトプットしてきたServer Side Kotlin、Spring Boot 2.0、gRPC方面のエントリを総集編的にまとめていきたい。会場で出会えた方にもブログの存在をアピールできたし、「で、そのブログにはどんなエントリあるの?」って少なからず感じてもらえたと思うので応えたい。

Sever Side Kotlin

Kotlinを始めてまず取り掛かったのがテストコード。JavaではJMockitoにお世話になっていたがKotlinで動かないことに絶望を感じたのが記憶にある。JMockitoが使えないならKotlinでどのようにコードの品質を担保するのか!!と差し迫った時期のエントリ。

blog.soushi.me


DelegateとKotlinコルーチンについてまとめたエントリ。ここらへんの話も発表に入れたかった。

blog.soushi.me

blog.soushi.me


純正KotlinのJSONパーサーのKlaxonについてのエントリ。この記事はアクセス数が結構ある。JSONパースって必須と感じる。

blog.soushi.me

Spring Boot 2.0.0

Spring Boot 2.0.0+ Kotlin動きそう? HTTPサーバのInterceptorってどうなってるの? を疑問に思ってまとめたエントリ。

blog.soushi.me


Spring Framework 5から提供が始まったRouter Function DSLのテスト方法についてまとめたエントリ。

blog.soushi.me


Spring Boot 2.0.0 Milestone versionではSpringFox(Swagger)やSpring Rest DocsなどのAPIドキュメンテーションツールのサポートが不完全なので、その時期にどうやって解決したかアウトプットしたエントリ。

blog.soushi.me

gRPC

protobuf typeについてまとめたエントリ。まだ試していない型がたくさんあるしgRPC楽しい。

blog.soushi.me

blog.soushi.me

protoの運用について考えたエントリ。protodep便利。

blog.soushi.me


gRPC Contextについてまとめたエントリ。gRPCはInterceptorを使ってContextに値を入れたり取り出したりすることがある。AOPには必須な知識。

blog.soushi.me


gRPC Client/Serverのテスト方法をまとめたエントリ。

blog.soushi.me

blog.soushi.me

blog.soushi.me


エンドクライアント(iOSやAndroid)のgRPC ClientとgRPC Serverの通信にTLSを有効にするためにはどうすればよいのか?を調べてアウトプットしたエントリ。

blog.soushi.me

blog.soushi.me


TLSについてまとめたけどALPN対応できなくて焦って問題解決できた経緯をまとめたエントリ。

blog.soushi.me

まとめ

  • スライドのテーマに似ているような過去エントリをまとめてみた。
  • まとめた順番とエントリの時期が前後したり、過去の情報で最新ではない可能性もあるので内容は保証できないことをご了承いただきたい🙏
  • こういう機会にエントリをまとめてみるのは頭の整理が進むので良いと感じた。

次回はgrpc-gatewayのアウトプットをしようと計画中。