Lazybonesによるプロジェクトテンプレート管理(1): Lazybones概要/Hello Lazybones
Lazybonesとは?
Lazybonesはプロジェクトテンプレートからプロジェクトのひな形を自動作成するツールです。
Railsのscaffoldや、Mavenのarchetype:generateに近いメージですが、特定のフレームワークやビルドツールに依存しない汎用的なテンプレート管理ツールになっています。(元々はRatpackにプロジェクト新規作成用のコマンドラインツールが提供されていないことを不満に思って作成したそうです。)
とはいえ、Lazybonesのテンプレート作成機能がGradleベースであることや、post-installスクリプトをGroovyで記述することなどから、Gradleプロジェクトのテンプレート管理ツールとして使われることを想定していると考えてよいのではないかと思います。
Lazybones概要
- テンプレートはzip形式のアーカイブで、基本的にはプロジェクトのひな形となるファイル・ディレクトリをアーカイブに含めます。
- テンプレートはBintrayから配布できます(バージョン管理可能)。
- テンプレートには静的なファイルだけでなく、テンプレートエンジンによって生成する動的なファイルを含めることができます。テンプレートエンジンの処理はpost-installスクリプト(Groovyスクリプト)に記述します。
- post-installスクリプトには対話処理を含めることができます。例えば、自動生成するクラスファイルのパッケージ名などをユーザーに入力させることができます。
- サブテンプレート機能によって、プロジェクトの初期生成後にファイルを追加生成できます。(例えば後からコントローラークラスを追加するなど。)
テンプレート配布方法
デフォルトではBintrayの pledbrook/lazybones-templates リポジトリが検索対象となります。
コンフィグレーションでカスタムリポジトリを追加することができるので、例えば自分のBintrayリポジトリを検索対象に追加することも可能です。
また、Bintrayにアップロードせずに、テンプレートのzipファイルをWebサーバーやファイルサーバーにアップロードしておき、URLを直接指定する方法もあります。(いちいちURLを指定するのが面倒なら、コンフィグレーションでURLに別名をつけることもできます。)
テンプレートの開発方法は後日紹介しようと思いますが、興味のある方はこちらを参照してみてください。静的なファイルのみであれば、テンプレート作成はそれほど難しくないです。
Template developers guide · pledbrook/lazybones Wiki · GitHub
インストール方法
2014/12/28現在の最新バージョンは0.8です。
GVMが利用可能であれば、以下でインストールするのが簡単でよいでしょう。
$ gvm install lazybones
バイナリのzipファイルを展開してもよいです。以下からダウンロードして展開し、bin/以下にパスを通してください。
https://bintray.com/pledbrook/lazybones-templates/lazybones/0.8/view/files
使ってみる
以下のコマンドで公式リポジトリのテンプレート一覧を取得できます。
$ lazybones list Available templates in pledbrook/lazybones-templates aem-multimodule-project afterburnerfx afterburnergfx angular-grails asciidoctor-gradle dropwizard gaelyk gradle-plugin gradle-quickstart groovy-app groovy-lib java-basic lazybones-project nebula-plugin ratpack ratpack-lite spring-boot-actuator
試しにSpring Bootのプロジェクトを生成してみましょう。
テンプレートの詳細情報を確認するには「info」コマンドを利用します。
$ lazybones info spring-boot-actuator Fetching package information for 'spring-boot-actuator' from Bintray Name: spring-boot-actuator Latest: 1.0.1.RELEASE Owner: pledbrook Versions: 0.1, 1.0.1.RELEASE, 0.2
提供されているバージョンが確認できましたので、1.0.1.RELEASEからプロジェクトを生成してみます。
プロジェクトの生成には「create」コマンドを利用します。「help <コマンド名>」で各コマンドの詳細が確認できます。
$ lazybones help create Creates a new project from a template. USAGE: create <template> <version>? <dir> where template = The name of the project template to use. version = (optional) The version of the project template to use. Uses the latest version of the template by default. dir = The name of the directory in which to create the project structure. This can be '.' to mean 'in the current directory.' Option Description ------ ----------- -P Add a substitution variable for file filtering. -h, --help Displays usage. --spaces Sets the number of spaces to use for indent in files. --with-git Creates a git repository in the new project.
プロジェクトを生成したいディレクトリに移動し、以下を実行して「mybootapp」ディレクトリ以下にプロジェクトを新規生成します。
$ lazybones create spring-boot-actuator 1.0.1.RELEASE mybootapp
もしくは、以下のようにしてもかまいません。
$ mkdir mybootapp $ cd mybootapp $ lazybones create spring-boot-actuator 1.0.1.RELEASE .
プロジェクトのファイル一式が生成され、最後にREADME.mdの内容が表示されます。
# Spring Boot Actuator Sample You have just created a simple Spring Boot project in Groovy incorporating the Actuator. This includes everything you need to run the application. In this case, that's a simple JSON endpoint. In this project you get: * A Gradle build file * An application class, `SampleApplication`, implementing a single JSON endpoint * A JUnit test case for `SampleApplication` You can build and run this sample using Gradle (>1.6): ``` $ gradle run ``` If you want to run the application outside of Gradle, then first build the JARs and then use the `java` command: ```
生成されたファイル一式は以下のようになっています。
$ tree . ├── README.md ├── build.gradle └── src ├── main │ ├── groovy │ │ └── sample │ │ └── SampleApplication.groovy │ └── resources │ └── application.properties └── test └── groovy └── sample └── SampleApplicationTests.groovy
README.mdに従って実行してみます。(このテンプレートにはGradle Wrapperが含まれていないので、別途Gradleを導入しておく必要があります。)
$ gradle run :compileJava UP-TO-DATE :compileGroovy :processResources :classes :run . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v1.0.1.RELEASE) 2014-12-28 01:05:53.866 INFO 51749 --- [ main] sample.SampleApplication : Starting SampleApplication on nobusue-MacBookPro.local with PID 51749 (/Users/nobusue/work/mybootapp/build/classes/main started by nobusue) 2014-12-28 01:05:53.924 INFO 51749 --- [ main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@6ae0286d: startup date [Sun Dec 28 01:05:53 JST 2014]; root of context hierarchy ・・・ [org.springframework.boot:type=Endpoint,name=configurationPropertiesReportEndpoint] 2014-12-28 01:05:57.278 INFO 51749 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080/http 2014-12-28 01:05:57.279 INFO 51749 --- [ main] sample.SampleApplication : Started SampleApplication in 4.282 seconds (JVM running for 4.92) > Building 80% > :run
無事Spring Bootアプリケーションが起動しました。
GroovyでApache Sparkアプリケーションを作る #gadvent
このエントリは G*Advent Calendar(Groovy,Grails,Gradle,Spock...) Advent Calendar 2014 - Qiita の12/20担当分です。
Apache Sparkとは?
Hadoopエコシステムにおける次世代の分散処理基盤として注目されています。インメモリ処理とDAGによるタスクスケジューリングを特徴とし、分散処理に必要な耐障害性を備えています。また、RDDという共通のプログラミングモデルの上で機械学習やストリーミング処理が統一的に扱えるため、複雑なビッグデータ処理を実装するのに有利です。
概要をつかむにはこのへんの資料がよいかと思います。
Groovyから使ってみようと思った動機
公式サイト Apache Spark™ - Lightning-Fast Cluster Computing を見ていただくとわかりますが、Spark自体はScalaで開発されており、アプリケーション開発は Scala / Java / Python で行えるようにAPIが提供されています。(機能の実装状況には差異があり、例えばPythonではSpark1.2でやっとStreamingがサポートされたり、と言った状況です。)
せっかくJavaのAPIがあるのでぜひ使いたいところなのですが、Scala/Pythonに比べて以下のようなビハインドがあります。
- ラムダが使えないのでコードが冗長(Java8を使えばマシにはなりますが、ClouderaがJava8をサポートするまで自分は使えません・・)
- 対話型シェル(REPL)がない
JavaのAPIをGroovyから使うことでこれらの課題に対処できないか試行錯誤していますので、まだ道半ばではありますが現在までの状況をまとめておきます。
コードの簡略化
いくらか成果が上がったので、本エントリで記載します。ただし、Sparkの挙動に起因する癖がありますので注意が必要です。
REPL(Groovy Shell)
こちらはSparkの挙動に起因する癖によって壁にぶつかりました。現時点では解決策が見つかっていません。
具体的にはタスク実行時に以下の例外が発生します。
ERROR org.apache.spark.SparkException: Task not serializable at org.apache.spark.util.ClosureCleaner$.ensureSerializable (ClosureCleaner.scala:166)
GroovyでSpark Wordcountを実行してみる
Quick Start - Spark 1.2.0 Documentation にあるWord CountのJavaサンプルをGroovyで書き換えてみました。
プロジェクト全体はnobusue/groovy-spark-sample · GitHubにあげてあります。
Sparkアプリケーション本体
テキストファイルを読み込んで、指定した単語を含む行数をカウントするだけの簡単なサンプルです。適当なテキストファイルを用意して、「sc.textFile("YOUR_TEXT_FILE_PATH")」のところを置き換えてください。
import org.apache.spark.* import org.apache.spark.api.java.* import org.apache.spark.api.java.function.* public class SparkGroovySample { public static void main(String[] args) { def conf = new SparkConf().setMaster("local[2]").setAppName("WordCount") def sc = new JavaSparkContext(conf) def file = sc.textFile("YOUR_TEXT_FILE_PATH").cache() def filterFunc = new Function<String,Boolean>() { public Boolean call(String s) { return s.contains('spark') }} def filterFunc2 = { it.contains('hadoop') } as Function def countsOfSpark = file.filter(filterFunc).count() def countsOfHadoop = file.filter(filterFunc2).count() println "Count of Spark:${countsOfSpark}, Count of Hadoop:${countsOfHadoop}" } }
例えばSparkのREADME.mdに対して上記を実行すると、
Count of Spark:8, Count of Hadoop:10
みたいになるはずです。
フィルタ関数の定義は、SparkのJava APIでは以下のようにFunctionインターフェースを実装する必要があります。
def filterFunc = new Function<String,Boolean>() { public Boolean call(String s) { return s.contains('spark') }}
Groovyの場合はクロージャから変換することで多少楽ができます。
def filterFunc2 = { it.contains('hadoop') } as Function
注意点
コードを眺めているだけでは分かりづらいのですが、sc.textFile()で読み込んだファイルはRDD(JavaRDD)というオブジェクトに格納されています。
RDDに対する操作はワーカーノードで分散処理されるため、filter()などの操作で利用するオブジェクトはすべてシリアライズしてリモートに送信できなければいけません。上記サンプルをGroovyスクリプトではなくGroovyクラスとして実装しているのはこの条件を満たすためです。(実際に試してみると、Groovyスクリプトとして実装した場合にはfilterFunc()がシリアライザブルでないと判断されて例外が発生します。)
おまけ:ログ設定
SparkはLog4jを使っており、デフォルトではSpark自体の動作に関するログがINFOレベルで大量に出力されます。普段は必要ないので、以下のようにして消しておきましょう。
# Set everything to be logged to the console #log4j.rootCategory=INFO, console log4j.rootCategory=WARN, console log4j.appender.console=org.apache.log4j.ConsoleAppender log4j.appender.console.target=System.err log4j.appender.console.layout=org.apache.log4j.PatternLayout log4j.appender.console.layout.ConversionPattern=%d{yy/MM/dd HH:mm:ss} %p %c{1}: %m%n
GroovyでAWS SDK for Javaを使う #gadvent
このエントリは G*Advent Calendar(Groovy,Grails,Gradle,Spock...) Advent Calendar 2014 - Qiita の12/13担当分です。
AWS SDK for Javaとは?
パブリッククラウドサービスであるAmazon Web ServicesにはWebAPIが提供されていますが、生のAPIでは使いにくいため、各言語用からAPIを利用するためのライブラリが提供されています。
AWS SDK for Javaは読んで字のごとくJava用のライブラリです。
AWS SDK for Java | アマゾン ウェブ サービス(AWS 日本語)
Groovyから使うと何が嬉しいの?
http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/index.html を眺めてみていただくとわかりますが、AWS SDK for JavaのAPIはドメイン指向に設計されておりクラス構造が複雑です。
実際に使おうとするといろいろ試行錯誤が必要になるので、Groovyを使ってあらかじめAPIの挙動を調べておくと便利です。(もちろん、そのままGroovyでプロダクションコードを書いてもいいでしょう。)
例) EC2のインスタンス一覧を取得する
簡単な例として、EC2の指定リージョンのインスタンス一覧を取得してみましょう。
いろいろ試行錯誤するにはインタラクティブシェルの方がやりやすいので、
Gradle Groovy Shellプラグインを使って依存ライブラリ込みのREPLを起動する #gadvent - nobusueの日記
で紹介したGradle Groovy Shellプラグインを利用します。
事前準備
AWSのAPIを利用するためには、Access KeyとSecret Keyを取得しておく必要があります。APIからのアクセス専用にIAMアカウントを新しく作成し、EC2のみ権限を与えておくとよいかと思います。
ビルドスクリプト作成
適当なディレクトリで build.gradle 作成します。
apply plugin: 'com.github.tkruse.groovysh' apply plugin: 'java' buildscript { repositories { jcenter() } dependencies { classpath 'com.tkruse.gradle:gradle-groovysh-plugin:1.0.2' } } repositories { jcenter() } dependencies { compile 'com.amazonaws:aws-java-sdk:1.9.10' }
Groovy Shell起動
以下のコマンドでGroovy Shellを起動します。
$ gradle -q shell This is a gradle Application Shell. You can import your application classes and act on them. Groovy Shell (2.3.6, JVM: 1.7.0_72) Type ':help' or ':h' for help. ------------------------------------------------------------------------------- groovy:000>
このGroovy ShellはAWS SDK for Javaのライブラリがダウンロードされ、クラスパスに追加された状態になっています。
EC2インスタンス一覧取得
Groovy Shellで以下を入力します。ここでは例としてOregonリージョン(us-west-2)を指定しています。
groovy:000> import com.amazonaws.services.ec2.* groovy:000> import com.amazonaws.auth.* groovy:000> import com.amazonaws.regions.* groovy:000> credentials = new BasicAWSCredentials("<YOUR_ACCESS_KEY>","<YOUR_SECRET_KEY>") groovy:000> ec2 = new AmazonEC2Client(credentials) groovy:000> ec2.setRegion(Region.getRegion(Regions.US_WEST_2)) groovy:000> result = ec2.describeInstances() groovy:000> result.each{ println "Instance ID: ${it.reservations.instances.instanceId}" } Instance ID: [[i-50aaf05b]]
実際には実行毎に結果(レスポンス)がダンプされますので、見失わないようにしてください。例えば、最後の行の実行結果の後には以下が出力されます。
===> {Reservations: [{ReservationId: r-a8f079a3,OwnerId: 574167580182,Groups: [],GroupNames: [],Instances: [{InstanceId: i-50aaf05b,ImageId: ami-d13845e1,State: {Code: 16,Name: running},PrivateDnsName: ip-10-0-0-200.us-west-2.compute.internal,PublicDnsName: ec2-xx-xx-xx-xx.us-west-2.compute.amazonaws.com,StateTransitionReason: ,KeyName: xxx,AmiLaunchIndex: 0,ProductCodes: [],InstanceType: t2.micro,LaunchTime: Sun Aug 31 09:12:33 JST 2014,Placement: {AvailabilityZone: us-west-2a,GroupName: ,Tenancy: default},Monitoring: {State: disabled},SubnetId: subnet-xxxxxxx,VpcId: vpc-xxxxxx,PrivateIpAddress: 10.0.0.200,PublicIpAddress: xx.xx.xx.xx,Architecture: x86_64,RootDeviceType: ebs,RootDeviceName: /dev/xvda,BlockDeviceMappings: [{DeviceName: /dev/xvda,Ebs: {VolumeId: vol-eb315aea,Status: attached,AttachTime: Sun Aug 31 09:12:36 JST 2014,DeleteOnTermination: true}}],VirtualizationType: hvm,ClientToken: xxxxxxxxxx,Tags: [{Key: Name,Value: jenkins}],SecurityGroups: [{GroupName: xxx,GroupId: sg-xxxxxxxx}, {GroupName: xxx,GroupId: sg-xxxxxxx}],SourceDestCheck: true,Hypervisor: xen,NetworkInterfaces: [{NetworkInterfaceId: eni-34684351,SubnetId: subnet-xxxxxx,VpcId: vpc-xxxxxx,Description: Primary network interface,OwnerId: 574167580182,Status: in-use,MacAddress: 02:7b:60:ba:d1:ee,PrivateIpAddress: 10.0.0.200,PrivateDnsName: ip-10-0-0-200.us-west-2.compute.internal,SourceDestCheck: true,Groups: [{GroupName: xxx,GroupId: sg-xxxxxxx}, {GroupName: xxx,GroupId: sg-xxxxxxx}],Attachment: {AttachmentId: eni-attach-5aafb86d,DeviceIndex: 0,Status: attached,AttachTime: Sun Aug 31 09:12:33 JST 2014,DeleteOnTermination: true},Association: {PublicIp: xx.xx.xx.xx,PublicDnsName: ec2-xx-xx-xx-xx.us-west-2.compute.amazonaws.com,IpOwnerId: amazon},PrivateIpAddresses: [{PrivateIpAddress: 10.0.0.200,PrivateDnsName: ip-10-0-0-200.us-west-2.compute.internal,Primary: true,Association: {PublicIp: xx.xx.xx.xx,PublicDnsName: ec2-xx-xx-xx-xx.us-west-2.compute.amazonaws.com,IpOwnerId: amazon}}]}],EbsOptimized: false,}]}],}
毎回ダンプが出るのはややうっとおしいですが、レスポンスのどこに必要な情報が含まれているか確認するだけなら、これを眺めるだけで問題が解決する場合もあったりします。
Groovy Shellは「Ctrl+D」で終了します。
Groovy Shell以外で実行する場合の注意事項
「ec2 = new AmazonEC2Client(credentials)」のように変数定義を省略しているのはGroovy Shellの制約によるものです。
通常のGroovyスクリプトで実行する場合は「def ec2 = new AmazonEC2Client(credentials)」のようにしてください。
2015/1/29追記
Groovy2.4でgroovyshのinterpreterModeが追加されました。
http://jira.codehaus.org/browse/GROOVY-6623
groovysh起動後に
:set interpreterMode true
を実行すれば、groovysh上でも普通に「def x=3」とかで変数定義できるようになりました。めでたい。
Gradle Groovy Shellプラグインを使って依存ライブラリ込みのREPLを起動する #gadvent
このエントリは G*Advent Calendar(Groovy,Grails,Gradle,Spock...) Advent Calendar 2014 - Qiita の12/8担当分です。
Gradle Groovy Shellプラグインとは?
Groovyには"groovysh"(Groovy Shell)という機能があります。これはGroovyのインタラクティブシェルを起動するもので、いわゆるREPL相当の機能です。
それなりに便利な機能なのですが、残念ながらGroovyの標準ライブラリ以外を読み込むことができず、ライブラリを追加する場合には自力でクラスパスを通す必要があります。
Gradle Groovy Shellプラグインを利用すると、Gradleを利用して依存関係を解決した状態でgroovyshを起動することができます。
Gradle Groovy Shellプラグインの利用方法
Gradleのビルドスクリプトでgradle-groovysh-pluginを追加するだけです。
詳細はこちらを参照いただくとよいかと思います。
tkruse/gradle-groovysh-plugin · GitHub
例) Twitter4Jを使ってみる
ここでは例として Twitter4J - A Java library for the Twitter API を使ってみましょう。
事前に https://apps.twitter.com/ でConsumer KeyやAccess Tokenを取得しておいてください。
適当なディレクトリで build.gradle 作成します。
apply plugin: 'com.github.tkruse.groovysh' apply plugin: 'java' buildscript { repositories { jcenter() } dependencies { classpath 'com.tkruse.gradle:gradle-groovysh-plugin:1.0.2' } } repositories { jcenter() } dependencies { compile 'org.twitter4j:twitter4j-core:4.0+' }
そして、次のコマンドでGroovy Shellを起動します。
$ gradle -q shell This is a gradle Application Shell. You can import your application classes and act on them. Groovy Shell (2.3.6, JVM: 1.7.0_72) Type ':help' or ':h' for help. ------------------------------------------------------------------------------- groovy:000>
"groovy:000>"というプロンプトが表示されたら、おもむろにGroovyコードをタイプしていきましょう。(完全ではありませんが、TABキーによる補完もそれなりに効きます。)
なお、groovyshでは"def"で定義した変数は参照できないので、何もつけずに定義するようにしてください。
groovy:000> import twitter4j.* ===> twitter4j.* groovy:000> import twitter4j.conf.* ===> twitter4j.*, twitter4j.conf.* groovy:000> cb = new ConfigurationBuilder() ===> twitter4j.conf.ConfigurationBuilder@6c01e903 groovy:000> cb.setOAuthConsumerKey("<YOUR_CONSUMER_KEY>") ===> twitter4j.conf.ConfigurationBuilder@6c01e903 groovy:000> cb.setOAuthConsumerSecret("<YOUR_CONSUMER_SECRET>") ===> twitter4j.conf.ConfigurationBuilder@6c01e903 groovy:000> cb.setOAuthAccessToken("<YOUR_OAUTH_TOKEN>") ===> twitter4j.conf.ConfigurationBuilder@6c01e903 groovy:000> cb.setOAuthAccessTokenSecret("<YOUR_OAUTH_SECRET>") ===> twitter4j.conf.ConfigurationBuilder@5a8fddea groovy:000> tf = new TwitterFactory(cb.build()) ===> twitter4j.TwitterFactory@6bffa4b9 groovy:000> twitter = tf.getInstance() ===> TwitterImpl{INCLUDE_MY_RETWEET=PostParameter{name='include_my_retweet', value='true', file=null, fileBody=null}} groovy:000> query = new Query("gradle") ===> Query{query='gradle', lang='null', locale='null', maxId=-1, count=-1, since='null', sinceId=-1, geocode='null', until='null', resultType='null', nextPageQuery='null'} groovy:000> result = twitter.search(query) [Mon Dec 08 02:35:38 JST 2014]Request: [Mon Dec 08 02:35:38 JST 2014]GET https://api.twitter.com/1.1/search/tweets.json?q=gradle&with_twitter_user_id=true&include_entities=true ・・・ groovy:000> result.tweets.each{ println "@${it.user.screenName}: ${it.text}" } @csterwa: RT @danveloper: What if bootstrapping a cloud full of @NetflixOSS was as easy as typing "initCloud"? Now it is. https://t.co/44OVUgfKG4 @IndieGameDevBot: RT @lastpoke: Way to download Utility classes with Gradle http://t.co/suUwF0xSyj #Android #AndroidDev #lastpoke ・・・
Ctrl+Dで終了します。
2015/1/29追記
Groovy2.4でgroovyshのinterpreterModeが追加されました。
http://jira.codehaus.org/browse/GROOVY-6623
groovysh起動後に
:set interpreterMode true
を実行すれば、groovysh上でも普通に「def x=3」とかで変数定義できるようになりました。めでたい。
まとめ
簡単ですが、Gradle Groovy Shellプラグインについて紹介しました。
Groovy Shellプラグインは主にGradleのビルドスクリプト開発を支援するために使われることが多いようですが、それ専用ではなくもう少し汎用的に作られているということがわかります。他の言語処理系におけるREPLに比べるとやや機能不足な感は否めませんが、JavaのREPLが登場するまではこれで凌ぐというのもアリかと思います。
また、Groovyには言語機能としてGrape(@Grab)が用意されているのですが、Grapeはスクリプト実行以外の使い方ではうまく動かない場合もありますので、うまく使い分けるとよいのではないでしょうか。
AntのGroovyタスクを使うときの俺流ベストプラクティス
G* Advent Calendar 2013の12/16担当、@nobusue です。
12/7に続き二度目の登場になりますが、引き続き実務に役立つシリーズでいきたいと思います。といっても、「ビルドはGradleで決まりだよね!」というイマドキの現場ではなく、いまだにAntでがんばっている(ちょっと残念な)現場の方向けです。
最近はMavenやGradleやsbtなんかの新興勢力に押され気味のAnt御大ですが、その安定感からいまだに「Ant以外認めない」という現場も多いと聞きます。(あくまで伝聞ですよ。。。)実際、要件的にはAntで十分というケースも多いでしょう。しかし、やはりAntで無理やりがんばるのはあまり得策ではないケースというのもままあります。例えば、
- テンプレートエンジンを利用して設定ファイルを動的に生成したい(単純なプロパティの置換ではすまない、もしくは日本語を含む文字列を置換したい場合など)
- 環境に応じて動的にビルドのロジックを切り替えたい(ファイルの有無や、プラットフォームの差異によって挙動を変えたい)
- ビルドの中でJavaのクラスを呼びたいが、わざわざカスタムAntタスクを作るのはめんどう
というような場合です。
わたしが今入っている現場では、まさに上記(1)のケースにヒットしてしまいました。(プロパティファイルにUnicodeエスケープした日本語を書いておくと、AntのCopyタスクのfilterでエスケープが解除されてしまい、そのままエスケープなしの状態でプロパティファイルが作られてしまう、、、というややこしい問題です。もう一回native2asciiすればいいんですけど、ダサダサですよね。)
というわけで、今回の内容は「いろいろ大人の事情でGradleに移行できないけど、せめてカスタムタスクぐらいはGroovyで書きたい」という人向けです。たいした内容ではありませんが、意外にまとまった情報がなかったのでまとめておきたいと思います。
AntのGroovyタスクとは?
GroovyのディストリビューションにはAntのカスタムタスクとしてGroovyタスクが内包されています。ですので、Antのクラスパスにgroovy-all-x.y.z.jarを追加し、ビルドスクリプトに以下の定義を追加すればGroovyタスクが利用できるようになります。だまって $ANT_HOME/lib 以下にgroovy-all.jarをぶっこんどくのがおススメです。
<taskdef name="groovy" classname="org.codehaus.groovy.ant.Groovy" classpath="groovy-all-x.y.z.jar" />
あとは、以下のようにビルドスクリプト内でGroovyが使えるようになります。
<groovy><![CDATA[ ant.echo level:'info', message:'Hello groovy task!' ]]></groovy>
詳しくは、 The groovy Ant Task とか Antスクリプト内でGroovyを利用する あたりをご参照ください。
モジュール化してカスタムタスクっぽく使う
Groovyタスクは強力ですが、Ant以外受け付けない方に見つかるとお叱りを受ける可能性もあります。ですので、以下のようにしてGroovyコードの部分を隠蔽してしまいましょう。
呼び出し元:
<project name="main"> <import file="./buildUtil.xml"/> <target name="build"> <antcall target="complexTask"/> </target> </project>
呼び出し先(Groovyタスク):
<project name="util"> <taskdef name="groovy" classname="org.codehaus.groovy.ant.Groovy" classpath="groovy-all-x.y.z.jar" /> <target name="complexTask"> <groovy><![CDATA[ ant.echo level:'info', message:'Groovy makes easier!' ]]></groovy> </target> </project>
GroovyにはAntBuilderという便利な仕組みが内包されており、Groovyタスクの中からAntタスクを呼び出すこともできますので、Antで用意されているファイル操作なんかはそのまま便利に利用しましょう。
パラメータを渡す
前出のようにモジュール化した場合、当然ながら「呼び出し元タスクからパラメータを渡したい」という要望が出てきます。これ、不思議とサンプルコードが見当たらないのですが、やり方は簡単で以下のようにするだけです。
呼び出し元:
<project name="main"> <import file="./buildUtil.xml"/> <target name="build"> <antcall target="complexTask"> <param name="sourcePath" value="./source"/> <param name="distPath" value="./dist"/> </antcall> </target> </project>
呼び出し先(Groovyタスク):
<project name="util"> <taskdef name="groovy" classname="org.codehaus.groovy.ant.Groovy" classpath="groovy-all-x.y.z.jar" /> <target name="complexTask"> <groovy><![CDATA[ def sourcePath = properties['sourcePath'] def distPath = properties['distPath'] ant.echo level:'debug', message:"sourcePath: ${sourcePath}" ant.echo level:'debug', message:"distPath: ${distPath}" ]]></groovy> </target> </project>
要するに、呼び出し側で
ログ出力
汎用性の高いタスクができると、幅広く使われるようになります。そうすると、printデバッグではいずれ限界がきますので、早い段階でメッセージのログレベルをきちんと分けておきましょう。
実は既に小出しにしていましたが、
ant.echo level:'debug', message:"sourcePath: ${sourcePath}"
のように、AntBuilder経由でant.echoをログレベル付きで実行すればよいです。(Taskクラスのloggerを取得しようとしたのですが、どうもうまくいかないのでこの方法にしました。)
ログレベルを分けておくと、開発時はdebugを出力して、運用時はwarning以上のみ出力というような使い分けが可能です。ログレベルはAntのコマンドラインオプションで指定できますので、こちらなどを参考にいろいろためしてみてください。
まとめ
Antに疲れたらGroovyタスクで息抜きしましょう。
次は @nagai_masato さんです!
GroovyでGCログ解析: ちょっと扱いにくいフォーマットを処理するときのパターン
G* Advent Calendar 2013の12/7担当、@nobusue です。
今年は基本に返って、実務に役立つGroovyの利用方法をご紹介しようと思います。(というか実際に業務で使ったのですが。。。)
お題はGCログの解析です。
ここでご紹介するサンプルはOracle JDK7(HotSpot VM)が出力するGCログを想定していますが、仕組みはシンプルなので他のフォーマット(IBM JDK以外)にも多少アレンジすれば適用可能なはずです。
GCログを解析する場合、通常は侍やGCViewerなどのツールを使うことが多いですが、それでは対応が難しい要件もたまにあります。
例えば、「GCログが100本くらい取ってあって、それらを解析して最適なヒープサイズを見積もる」というようなケースです。いちいちGUIツールで一つ一つ開いてられないですよね。。。
そこでGCログをスクリプトで処理してやろうか、と思うのですが、GCログには以下のようなちょっと困った特徴があります。
- 一行で無理矢理情報を階層化しているので、フィールドの分割が単純にいかない
- 設定しているGCのアルゴリズムによってフォーマットがかなり変わる
- CPU負荷が高いとログが壊れてフォーマットが不正になる場合がある
特に3.が厄介で、こいつのせいでまともにパーサーを実装しようとすると例外処理が無駄に複雑になってしまいます。かといって、一行全体をパターンマッチングで処理しようとすると、ものすごく長大な正規表現が必要になり、デバッグに苦しむことになります。(というか実際苦しんだ。)
そこで、現実的なアプローチとして以下の戦略を採用します。
- 一行を区切り文字でおおざっぱに分割する
- 分割した各要素から、正規表現で必要な部分を取り出す
具体的なやり方は以下のようになります。
GCログ出力設定
まずは準備としてGCログを採取します。
JVMの起動時オプションに以下を追加してください。
-Dverbose:gc -Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps
これで、JVM起動時のカレントディレクトリに gc.log が出力されるようになります。(+PrintGCDateStampsはJDK6以降でないと使えないので、JDK5以前の方は+PrintGCTimeStampsに変更してくださいね。)
GCログの具体的な出力は以下のようになります。
マイナーGCの場合:
2013-12-07T17:33:31.668+0900: 19.047: [GC [PSYoungGen: 26476K->6407K(30208K)] 59443K->40152K(74240K), 0.0631133 secs] [Times: user=0.20 sys=0.00, real=0.06 secs]
フルGCの場合:
2013-12-07T17:33:31.731+0900: 19.110: [Full GC [PSYoungGen: 6407K->0K(30208K)] [ParOldGen: 33744K->34259K(65024K)] 40152K->34259K(95232K) [PSPermGen: 40231K->39553K(65536K)], 0.1483206 secs] [Times: user=0.36 sys=0.00, real=0.15 secs]
GCログの1行をおおざっぱに分割する
GCログのフォーマットを眺めてみると、以下のような構造になっています。
2013-12-07T17:33:31.668+0900: 19.047: [GC [PSYoungGen: 26476K->6407K(30208K)] 59443K->40152K(74240K), 0.0631133 secs] [Times: user=0.20 sys=0.00, real=0.06 secs] 2013-12-07T17:33:31.731+0900: 19.110: [Full GC [PSYoungGen: 6407K->0K(30208K)] [ParOldGen: 33744K->34259K(65024K)] 40152K->34259K(95232K) [PSPermGen: 40231K->39553K(65536K)], 0.1483206 secs] [Times: user=0.36 sys=0.00, real=0.15 secs]
「Timesの部分は役に立たないので捨てる」という決断をすれば、1行を"["と"]"、および","で区切れば意味のある固まりに分割できることがわかります。
これをGroovyで実装すると以下のようになります。
def items = line.tokenize("[")*.tokenize("]").flatten()*.tokenize(",").flatten()*.trim()
flatten()が2回発生していますが、元のフォーマットが変態なせいですので我慢してください。
これで、「ヒープ領域ごとのMAXを取得する」という目的にかなり近づくことができました。
パターンマッチングで必要な部分を抜き出す
この前の作業で、
[PSYoungGen: 6407K->0K(30208K)]
みたいな文字列を切り出すことはできました。じゃあ、ここから"(30208K)"の数字部分だけ取り出すにはどうすればよいでしょうか?
良い子の皆さんは既にお分かりだと思いますが、正規表現を使うのが一番手っ取り早くお手軽だと思います。でも、いざ使うとなると、サンプルとか少なくて意外に悩みませんか?
こういう場合、私はいつも次のようにしています。
def s = "[PSYoungGen: 6407K->0K(30208K)]" def max = s.find( /PSYoungGen:.*\((\d+)K\)/ ) {all,m0 -> return m0}
何をしているのか簡単に解説します。
サンプルコード
第一引数としてGCログのファイル名を渡すと、
Young Max: 41472K Old Max : 65024K Total Max: 95744K Perm Max : 65536K Total GC pause time: 0.3325945 sec
みたいな感じで、ヒープの各領域のMAXを表示するサンプルがこちらになります。(フルGCが発生していないとOld/Permは取得できないので"0K"になります。そういう仕様なのでご了承ください。)
def srcFile = new File(args[0]) def youngMax=0, oldMax=0, totalMax=0, permMax=0 def totalPause=0.0 srcFile.eachLine{ line -> if( line =~ /^\d\d\d\d-\d\d-\d\dT/) { def gcinfo = parseGC(line) //println gcinfo try{ ym = gcinfo.ym.toInteger() tm = gcinfo.tm.toInteger() pt = gcinfo.pt.toDouble() } catch(e) { return } youngMax = (ym > youngMax) ? ym : youngMax totalMax = (tm > totalMax) ? tm : totalMax totalPause += pt if( line.contains("[Full GC")) { try{ om = gcinfo.om.toInteger() pm = gcinfo.pm.toInteger() } catch(e) { return } oldMax = (om > oldMax) ? om : oldMax permMax = (pm > permMax) ? pm : permMax } } } println "Young Max: ${youngMax}K" println "Old Max : ${oldMax}K" println "Total Max: ${totalMax}K" println "Perm Max : ${permMax}K" println "Total GC pause time: ${totalPause} sec" def Map parseGC(line) { def items = line.tokenize("[")*.tokenize("]") .flatten()*.tokenize(",").flatten()*.trim() //println items def m = [:] items.each{ switch(it) { case ~/PSYoungGen:.*/: m.ym = it.find( /PSYoungGen:.*\((\d+)K\)/ ) {all,m0 -> return m0} break case ~/ParOldGen:.*/: m.om = it.find( /ParOldGen:.*\((\d+)K\)/ ) {all,m0 -> return m0} break case ~/PSPermGen:.*/: m.pm = it.find( /PSPermGen:.*\((\d+)K\)/ ) {all,m0 -> return m0} break case ~/\d+K->\d+K\(\d+K\)/: m.tm = it.find( /\d+K->\d+K\((\d+)K\)/ ) {all,m0 -> return m0} break case ~/\d*\.\d*\s+secs/: m.pt = it.find( /(\d*\.\d*)\s+secs/ ) {all,m0 -> return m0} } } return m }
以上です。
Groovyの便利さを感じていただくことができましたか?
次は@tyamaさんです!
Grails Database Reverse Engineering Plugin - G* Advent Calendar 2012 -
G* Advent Calender 2012の12/6担当、@nobusue です。
どうも風邪を引いたらしく、体調がやばい事になってます。。。ということで、JGGUG合宿2012の成果まとめで勘弁してください。
Grailsは通常、モデルオブジェクトを作成し、そこからRDB(もしくはKVSなど任意のデータストア)にデータを永続化するという手順で利用します。しかしながら、既存のRDBがあり、そのデータを管理するUIをささっと作りたいというお話は割りとよく聞きます。
そんなときに便利なのが、Database Reverse Engineering Pluginです。
http://grails-plugins.github.com/grails-db-reverse-engineer/
ただ、こいつはちょっと癖があるので、使いこなすにはいくつかコツが必要です。ここでは、私が踏んだ地雷を公表しておきたいと思います。
Pluginのインストール
適当な新規Grailsアプリケーションを作成します。ここでは仮にtestとしました。
Grailsは2.1.1、Database Reverse Engineering Pluginは0.4で動作確認しています。
次に、次のコマンドでPluginをインストールします。
nobusue-MacBookPro:test nobusue$ grails install-plugin db-reverse-engineer Plugin installed.
無事にインストールできました。そう、インストールはね。。。
DBの準備
今回はMySQL5を利用しました。みなさんのお手元では適当なDBを作成してください。(もちろんですがGrailsがサポート対応しているものにしてくださいね!)
データを自分で作るのが面倒だったので、MySQLのサイトで公開されているサンプルデータを利用させていただきました。このデータ、MySQLの記述検定にも使われているそうなので、一度見ておくとよいかもです。
http://dev.mysql.com/doc/index-other.html
今回はDB=worldにworld.sqlを取り込みました。
データソース設定
Grailsのデータソースを設定します。
[grails-app/conf/DataSource.groovy]
dataSource { url = 'jdbc:mysql://localhost/world' driverClassName = 'com.mysql.jdbc.Driver' username = 'root' password = 'password' dialect = org.hibernate.dialect.MySQL5InnoDBDialect }
あと、PluginがJDBCドライバを参照できるように、依存関係を追加してやります。
[grails-app/conf/BuildConfig.groovy]
dependencies { runtime 'mysql:mysql-connector-java:5.1.20' }
最後に、Plugin自体の設定を追加します。
[grails-app/conf/Config.groovy]
grails.plugin.reveng.jdbcDriverJarDep ='mysql:mysql-connector-java:5.1.20'
DB Reverse Engineering Plugin実行
さあ、これで準備完了です。DBサーバーが起動していることを確認して、Pluginを実行してみましょう!
nobusue-MacBookPro:test nobusue$ grails db-reverse-engineer --stacktrace Compiling 10 source files.| Error Error executing script DbReverseEngineer: : Compilation Failed (NOTE: Stack trace has been filtered. Use --verbose to see entire trace.) : Compilation Failed at org.apache.tools.ant.UnknownElement.execute(UnknownElement.java:291) at org.apache.tools.ant.dispatch.DispatchUtils.execute(DispatchUtils.java:106) at DbReverseEngineer$_run_closure1.doCall(DbReverseEngineer:51) Caused by: java.lang.IllegalArgumentException: The includeAntRuntime=false option is not compatible with fork=false ... 3 more Error Error executing script DbReverseEngineer: : Compilation Failed
ということで、残念ながらPluginの実行が失敗します。どうもモデルオブジェクトの自動生成あたりで引っかかっている雰囲気ですが、細かいことを追求するのはやめておいて、ドキュメントにしたがってGrailsとPluginのバージョンを下げてみます。
改めて環境構築
ドキュメントによると、「Grails2.0+Plugin0.4で動かない場合は、いったんGrails1.3+Plugin0.3でモデルオブジェクトを生成し、そのアプリケーションをGrails2.0の環境にマイグレーションしろ」と書いてありました。もっと分かりやすいとこに書いといてくれよ。。。
ということで、Grails1.3.9をインストールし、再びアプリケーションを作成します。
Reverse Engeering PluginがMaven Centralを参照するのですが、Grails1.3のテンプレートでは無効化されているのでリポジトリを追加します。
[grails-app/conf/BuildConfig.groovy]
repositories { mavenLocal() mavenCentral()
Pluginのインストール時はバージョン指定をお忘れなく。
grails install-plugin db-reverse-engineer 0.3
grails-app/conf/Config.groovy および grails-app/conf/DataSource.groovy は、Grails2.0のときと同様に修正します。
改めてDB Reverse Engineering Plugin実行
> grails db-reverse-engineer Running script /Users/nobusue/.grails/1.3.9/projects/test13/plugins/db-reverse-engineer-0.3/scripts/DbReverseEngineer.groovy Environment set to development [copy] Copied 3 empty directories to 1 empty directory under /Users/nobusue/.grails/1.3.9/projects/test13/resources [copy] Copying 1 file to /Users/nobusue/.grails/1.3.9/projects/test13/resources [copy] Copied 3 empty directories to 2 empty directories under /Users/nobusue/.grails/1.3.9/projects/test13/resources [mkdir] Created dir: /Users/nobusue/.grails/1.3.9/projects/test13/plugin-classes [groovyc] Compiling 13 source files to /Users/nobusue/.grails/1.3.9/projects/test13/plugin-classes [mkdir] Created dir: /Users/nobusue/work/grails/test13/target/classes [groovyc] Compiling 7 source files to /Users/nobusue/work/grails/test13/target/classes [mkdir] Created dir: /Users/nobusue/.grails/1.3.9/projects/test13/resources/grails-app/i18n [native2ascii] Converting 13 files from /Users/nobusue/work/grails/test13/grails-app/i18n to /Users/nobusue/.grails/1.3.9/projects/test13/resources/grails-app/i18n [copy] Copying 1 file to /Users/nobusue/work/grails/test13/target/classes [copy] Copied 2 empty directories to 2 empty directories under /Users/nobusue/.grails/1.3.9/projects/test13/resources [echo] Starting database reverse engineering, connecting to 'jdbc:mysql://localhost/world' as 'root' ... [echo] Finished database reverse engineering
おおっ、無事に City.groovy, Country.groovy, CountryLanguage.groovy が生成されました。CountryLanguageは複合キーが定義されているため、hashCode()も自動生成されています。すばらしい。
UI作成
モデルオブジェクトさえ手に入ればこっちのもの。試しにscaffoldして動かしてみます。
grails generate-all xxx.City
すると、scaffoldは問題なく終了しますが、生成されたUIを実行するとエラーが出てしまいます。原因はMySQLのテーブル"CountryCode"がフィールド"String countryCode"にマッピングされており、これを改めてGORMに通すとCOUNTRY_CODEをさしてしまうためでした。
対策としては、モデルオブジェクトにmappingを追加するか、フィールド名をcountrycodeのようにcamel caseでないものに修正する必要があります。今回は後者の対応で対応することで、めでたくUIから操作できるようになりました。
次にCountryですが、mapping追加でviewから操作することはできました。
static mapping = { id name: "code", generator: "assigned" version false surfaceArea column: "surfacearea" indepYear column: "indepyear" lifeExpectancy column: "lifeexpectancy" localName column: "localname" governmentForm column: "governmentform" headOfState column: "headofstate" }
しかし、UI上ではリスト表示の際にID列がないため、editのリンクが機能しません。
同じく、CountryLanguageはテーブル名がcamel caseになるので、こちらもmappingを修正して対応します。
static mapping = { table "countrylanguage" id composite: ["countryCode", "language"] version false countryCode column: "countrycode" isOfficial column: "isOfficial" }
ここで気づいたのが、idはマッピングされているのに、scaffoldで生成したviewのID列には表示されていないということ。何か対策があるんでしょうが、調べ切れていないのでとりあえず宿題ということにさせてください。
あと、元のテーブルにはリレーションが設定されているのですが、モデルオブジェクトの関連には反映されていないですね。Pluginの実行時に何か設定が必要なのでしょうか?
まとめ
ちょっと癖のあるDB Reverse Engineering Pluginですが、ベースとなっているHibernate Toolsのことを理解していればもうちょっと使いこなせるかもしれないです。素直なテーブルなら使えると思いますので、ぜひお試しください。(そしてレポートを。。。)
追記
2012/12/6現在でReverse Engineering Plugin 0.5がリリースされてました。。。こちらも確認せねば。。。
次は @nobeans さんです!