こんにちは。LINE ゲームのプラットフォーム開発を担当している Kagaya です。6 日目の記事を担当した 川田さんと同様、今年の 4 月から新卒で入社して主に Java を使ったサーバサイド開発を担当しています。
こちらの記事は LINE Advent Calender 2016 の 11 日目の記事になります。
はじめに
LINE のサービスの多くは Microservices Architecture と呼ばれるような構成になっています。このアーキテクチャそのものについては、過去の LINE Developers Blog でも こちらの記事 で扱ったり、Developer Day にて こちらの講演 を行ったりしています。様々なメリットが提唱されていますが、チーム開発の機能的な側面から言えば、巨大なシステムの各機能を疎結合にすることで、新しくチームにジョインしたメンバーでも開発をスピーディに行う事ができるのがメリットといえるでしょう。
LINE ゲームのプラットフォームも同様に Microservices 化されており、チームの Github Enterprise には多くのリポジトリが存在しています。さて突然ですが、我々のチームに配属されたあなたが新機能を開発するために新しくリポジトリを作りました。最初の一歩としてどのように開発を進めていくことを考えるでしょうか?Microservices の利点のひとつにコードベースの独立性があるとはいえ、まずはテンプレートのようなものからスタートするのが一般的だと思います。プロジェクト内のディレクトリ構成から、横断的に利用しているミドルウェアやチーム内でよく使われるビルドツールの設定など、多かれ少なかれ存在するであろう共通部分をテンプレート化するのはエンジニアにとっては常套手段です(DRY 原則という言葉もあります)。
例えば、我々のチームでは Spring Boot をよく利用しています。Spring が提供している Spring Initializr というサービスを皆さんご存知でしょうか?いくつかの設定項目を入力するだけで、Spring Boot Application の hello world を簡単に行うことができるサービスです。こういったツールのように、少しの設定でテンプレートからスケルトン生成を行うことができると、本質である新機能のコーディングに集中することができます。本記事では、私が所属するチームでも採用を検討している Groovy 製プロジェクト生成ツール Lazybones について紹介したいと思います。
プロジェクト生成ツール
その前に、Java のプロジェクト生成ツールといえば忘れてはいけない Maven Archetype の話を少ししたいと思います。Maven は Java で広く使われているビルドツールで、弊社でも多くのサービスで現役です。その Maven を使ったプロジェクトのイニシャライザ、スケルトン生成に用いることができるプラグインが Maven Archetype です。利用方法について解説したページは多く存在しますのでここでは割愛しますが、mvn archetype:generate
コマンドを入力後、対話形式・非対話形式で version や package 名のパラメタを指定をすることでスケルトンを生成できます。
一方で、Maven と並んで(後継として?)よく利用されているビルドツールに Gradle があります。我々のチームでも新規開発サービスにおいては Gradle への移行を開始しています。Maven Archetype は非常に便利ですが、Maven に特化したツールなので、Gradle を利用したプロジェクトを生成するにはやや相性が悪くなっています。よく云われるように、Gradle 公式では Maven における Maven Archetype に当たる機能は用意されていません。それは、Maven が XML ベースの「設定ファイル」によってビルドを行うのに対し、Gradle が Groovy を利用した「スクリプト」であるという設計思想の違いに依存していると思われますが、それでも一種の「初期化」は必要な工程です。さて、その初期化を行うには、いくつかの選択肢があります。
- 公式の
gradle init
(Gradle 1.9 から導入、前身である setupBuild コマンドは 1.7 から) - 非公式の Gradle プラグインを利用する
- IDE のテンプレートツールを利用する
- 汎用プロジェクト生成ツールを利用する
- Lazybones
この中で Lazybones を選ぶメリットはいくつか挙げられます。
- 特定のビルドツールや IDE に依存しない
- その一方で Groovy 製なので gradle との相性が良い
- 軽量で使いやすい(完成されたテンプレートが zip なので直感的に扱える)
- OSS でありがならも比較的メンテナンスがしっかり行われている
- Groovy が書きやすい
この中でも特定のツールに偏らないというのは個人的には大きいと思っています(Eclipse 全盛の時代から intelliJ が攻勢を強めている Java IDE 界の現在を見ても分かる通りです)。また、Spock という Groovy 製テスティングフレームワークを通じて Groovy の使いやすさを感じていたから、というのも理由の一つです。
Lazybones の使い方
さて、本題の Lazybones の話に入っていきます。Lazybones は Gradle と同様に Groovy 製です(余談になりますが、我々のチームでは Groovy の他にも altJava と呼ばれる言語を積極的に採用しています。先に述べたメインテストフレームワークである Spock は Groovy 製ですし、Scala や Clojure も利用しているプロジェクトもあります)。Lazybones をインストールするには、まず sdkman というパッケージマネージャを導入するのが早いです。
$ curl -s api.sdkman.io | bash
インストールした後は、sdk
コマンドからインストールを行います。
$ sdk install lazybones
以降、テンプレートを利用する場合と作成する場合に分けて説明していきます。
テンプレートを使う
すでに作成されたテンプレートを利用してスケルトンを生成するのは単純で、
$ lazybones create {template URL} {your project directory name} {target directory}
# And then you will asked groupId, artifactId, package, version. DONE!
以上で終了です。簡単ですね。また、Lazybones は bintray をテンプレートのリポジトリとして推奨しており、設定ファイルに repository ID を指定することで省略した形で template を取得することもできます。デフォルトでは作者らの repository が指定されており、java-basic
などシンプルなプロジェクト生成をいきなり試すことができます。(なお、デフォルトの設定ファイルは ~/.lazybones/config.groovy
にあります。)
テンプレートを作る
1. Lazybones テンプレート用のプロジェクトを生成
ここからは実際にテンプレートを作ってみましょう。紛らわしいのですが、テンプレートを作るためのテンプレートが用意されているので、それを利用します。
$ lazybones create lazybones-project advent-calender-template
すると、以下のような構造の java プロジェクトが生成されます。
$ tree advent-calender-template/
advent-calender-template/
├── README.md
├── build.gradle
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── templates
この templates ディレクトリにテンプレートを作成し、advent-calender-template
直下に生成された build.gradle
に基づいて gradle タスクを実行することでテンプレート化することになります。まずは templates ディレクトリに移動します。
$ cd advent-calender-template
$ cd templates
2. 実際のテンプレートの作成
ここからは、ここにディレクトリを一つ作ってそこにどんどんお好きなファイルを入れていくことになります。注意が必要なのは、README.md
と VERSION
が必須である点です。VERSION
ファイルは、テンプレートのバージョンを一行で記載したファイルです。
$ mkdir my-template
$ vi README.md
$ cat << EOF > VERSION
> 1.0.0
> EOF
$
# ... put your files
3. post-install script の作成
Lazybone がやることはシンプルで、テンプレートを展開した後、lazybones.groovy
ファイルに書かれた post-install script を実行するだけです。以下は java のプロジェクトの一例です。
import org.apache.commons.io.filefilter.FileFilterUtils
import org.apache.commons.io.FileUtils
@Grab(group="uk.co.cacoethes", module="groovy-handlebars-engine", version="0.2")
import uk.co.cacoethes.handlebars.HandlebarsTemplateEngine
// a. Use mustache based engine
registerDefaultEngine new HandlebarsTemplateEngine()
// b. set binding
def props = [:]
props.groupId = ask("Define value for 'groupId' [default: com.line.hokutokagaya]: ", "
com.linecorp.games", "groupId")
props.version = ask("Define value for 'version' [default: 1.0.0]: ", "1.0.0", "version
")
props.artifactId = ask("Define value for 'artifactId' [default: sample-project]: ", "s
ample-project", "artifactId")
props.package = ask("Define value for 'package' [default: com.line.hokutokagaya.sample
]: ", "com.linecorp.games.sample", "package")
processTemplates "settings.gradle", props
processTemplates "**/*.java", props
processTemplates "**/*.groovy", props
processTemplates "**/*.xml", props
processTemplates "**/*.yml", props
processTemplates "**/*.properties", props
processTemplates "**/*.txt", props
// c. make new project tree
def packageDirectoryStructure = props.package.replace('.', '/')
def targetDir = projectDir.path
def originalMainDir = new File("${targetDir}/src/main/java")
def originalTestDir = new File("${targetDir}/src/test/groovy")
def destMainDir = new File("${targetDir}/src/main/java/${packageDirectoryStructure}")
def destTestDir = new File("${targetDir}/src/test/groovy/${packageDirectoryStructure}"
)
def moveFiles(File originalDir, File destDir) {
def iterator = FileUtils.iterateFilesAndDirs(originalDir,
FileFilterUtils.trueFileFilter(),
FileFilterUtils.trueFileFilter())
while (iterator.hasNext()) {
def f = iterator.next()
if (f.equals(originalDir)) {
continue
}
if (f.exists()) {
println("moving: " + f.toString())
FileUtils.moveToDirectory(f, destDir, true)
}
}
}
moveFiles(originalMainDir, destMainDir)
moveFiles(originalTestDir, destTestDir)
やっていることは大きく 3 つです。まず、a. では b. で用いるテンプレートエンジンを指定しています。デフォルトだと groovy の SimpleTemplateEngine
を利用しますが、これは gradle のテンプレート処理と同一のため、少し不具合が起こるので差し替えています。b. では実際に processTemplate()
という関数を用いて各ファイルを処理します。配列 props
に実際の値を格納しますが、これは ask()
関数を利用しており、スケルトン生成時にユーザが入力します。例えば、package
に com.linecorp.developers.adventcalender2016.lazybones
が指定された場合、
// src/hoge.java
package {{package}}.controller;
というソースを用意しておくと、
package com.linecorp.developers.adventcalender2016.lazybones.controller
というように置換されます。c. では指定した package に合わせてディレクトリツリーを再構築します。自由度が高い分このような記述も自分でしなければなりません。
4. リモートリポジトリへの publish
前述の通り、Lazybones は公式で bintray へのアップロードを推奨しておりますので、bintray へのアップロードは簡単に行なえます。
lazybones {
repositoryName = "yourAccount/yourRepos"
repositoryUsername = "yourBintrayUsername"
repositoryApiKey = "bintrayApiKey"
}
以上のように build.gradle
に設定を記述して、
$ ./gradlew publishTemplate{your template name with CamelCase}
上記のコマンドを打つだけです。この時、テンプレート名を CamelCase で入力する部分に気をつけてください。例えばテンプレート名が line-advent-calender
なら実行するタスクは publishTemplateLineAdventCalende
になります。
… しかし bintray は基本的に public で、private リポジトリの作成は有料になります。また、社内にファイルサーバ等があってそれを流用したいという方も多いと思います。我々のチームでは library の管理に Sonatype Nexus を使っていますので、Nexus にアップロードするコマンドを作成しました。コードは以下の通りで、これを build.gradle
に直接記述するか、あるいは手続きごと plugin 化することで Nexus へもアップロードできます。
tasks.addRule("publishToNexus<TmplName> - Publishes the named template package to the
configured Nexus repository") {
String taskName ->
def matcher = taskName =~ /publishToNexus([A-Z-]S+)/
if (matcher) {
def camelCaseName = matcher[0][1]
def pkgTask = (Zip) project.tasks.getByName("packageTemplate${camelCaseNam
e}")
if (!pkgTask) {
return
}
tasks.create(taskName).with { t ->
dependsOn pkgTask
def url = "http://<your nexus repository>/${pkgTask.archiveName}"
println "archivePath: ${pkgTask.archivePath}"
println "uploadUrl: ${url}"
doFirst {
if (!pkgTask.archivePath.exists()) {
throw new GradleException("Bad build file: zip archive '${pkgT
ask.archiveName}' does not exist," +
" but should have been creat
ed automatically.")
}
}
doLast {
def conn = new URI(url).toURL().openConnection()
conn.with {
requestMethod = "PUT"
doOutput = true
def fileInputStream = pkgTask.archivePath.newInputStream()
try {
outputStream << fileInputStream
} catch (Throwable ex) {
throw ex
} finally {
fileInputStream.close()
outputStream.close()
}
def status = responseCode
if (!(status in 200..<300)) {
throw new GradleException("Unexpected status ${status} fro
m Bintray")
}
}
}
}
}
}
上記のスクリプトでは、url は決め打ちですし、認証等については省略していますが、そのあたりについても容易にカスタマイズ可能です。gradle スクリプトは結局 Groovy なので、大体のことは実現可能だと思います。ちなみに上記のスクリプトは build.gradle に追記した上で
$ ./gradlew publishToNexus{your template name with CamelCase}
で実行できます。
試してみての感想
Maven から Gradle に移行するにあたって、build ツールや IDE に依存しないツールを探した結果 Lazybones に行き着いたわけですが、非常に自由度の高いツールである一方で、前提条件が殆ど無いので実質機能はパッキング、テンプレートエンジン、ファイルアップロード機能くらいです。かっちり型に決まったことをやるというより、ケースバイケースでスクリプト言語を操ってなんでもできるというのが魅力と言えるでしょう(せっかく自由度が高いのでファイルアップロード先は bintray 以外もスムーズにできるようにしてほしい気もしますが...)。
なお、今回の記事で用いたサンプルプロジェクトを、github にアップロードしています。こちらのリポジトリを御覧ください。
終わりに
さて、本記事ではプロジェクト生成ツール Lazybones について、実際の利用方法を交えて紹介しました。人材の流動が激しい web エンジニア業界では、new comer がいかに素早くチームに馴染むかが重要となります。そのための一つの策として、Lazybones ぜひ使ってみてください!
次回はデータ分析チームに所属する tkengo さんによる「リアルタイム画風変換とその未来」という記事になります。私も学生時代は画像や機械学習に関する研究をしていたので、わくわくしながら待っております。明日もお楽しみに。