Spring を使った Web アプリケーションへ Ehcache を適用し JMX でモニタリング
Spring を使った Web アプリケーションへ Ehcache を適用し、JMX でキャッシュ状況を取得できるようにしてみました。
サンプルソースは http://github.com/fits/try_samples/tree/master/blog/20150508/
Spring へ Ehcache を適用
Spring には Cache Abstraction 機能が用意されており、Cache 用のアノテーションをメソッドへ付ければメソッドの戻り値をキャッシュできます。 (デフォルトでは、メソッドの引数がキャッシュキーとなります)
実際のキャッシュ処理には以下のようなライブラリや API が利用でき、今回は Ehcache を使用します。
- ConcurrentMap
- Ehcache
- Guava
- GemFire
- JSR-107 (JCache)
なお、キャッシュ用アノテーションは以下が利用でき、今回は JSR-107 のアノテーションを使用します。
Service クラス
まずはキャッシュを適用するメソッドを実装します。
今回は JSR-107 の @CacheResult
アノテーションを使いました。
src/main/java/sample/service/SampleService.java
package sample.service; import org.springframework.stereotype.Service; import javax.cache.annotation.CacheResult; @Service public class SampleService { @CacheResult(cacheName = "sample") public String sample(String id) { return "sample: " + id + ", " + System.currentTimeMillis(); } }
Controller クラス
次に、キャッシュを適用したメソッドを呼び出す処理を実装します。
今回のようなケースでは、CGLIB によるプロキシを使ってキャッシュ処理を差し込んでおり、内部的なメソッド呼び出しにはキャッシュを適用しないようなのでご注意ください。
src/main/java/sample/controller/SampleController.java
package sample.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import javax.inject.Inject; import sample.service.SampleService; @Controller public class SampleController { @Inject private SampleService sampleService; @RequestMapping("/sample/{id}") @ResponseBody public String sample(@PathVariable("id") String id) { // キャッシュを適用したメソッドの実行 return sampleService.sample(id); } }
設定クラス
設定クラスでは、下記を実施することで Ehcache を適用できます。
@EnableCaching
でキャッシュを有効化EhCacheCacheManager
を Bean 定義
Ehcache の設定ファイル ehcache.xml
をクラスパスから取得するように ClassPathResource
を使っています。
今回は JMX でモニタリングできるように Ehcache の ManagementService
も Bean 定義しています。
src/main/java/sample/config/WebConfig.java
package sample.config; import net.sf.ehcache.management.ManagementService; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.ehcache.EhCacheCacheManager; import org.springframework.cache.ehcache.EhCacheManagerFactoryBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import org.springframework.jmx.support.MBeanServerFactoryBean; import org.springframework.web.servlet.config.annotation.EnableWebMvc; @Configuration @EnableWebMvc @EnableCaching //キャッシュ機能の有効化 public class WebConfig { // CacheManager の定義 @Bean public CacheManager cacheManager() { EhCacheCacheManager manager = new EhCacheCacheManager(); manager.setCacheManager(ehcache().getObject()); return manager; } @Bean public EhCacheManagerFactoryBean ehcache() { EhCacheManagerFactoryBean ehcache = new EhCacheManagerFactoryBean(); ehcache.setConfigLocation(new ClassPathResource("ehcache.xml")); return ehcache; } // JMX 設定 @Bean public MBeanServerFactoryBean mbeanServer() { MBeanServerFactoryBean factory = new MBeanServerFactoryBean(); factory.setLocateExistingServerIfPossible(true); return factory; } // Ehcache 用の JMX 設定 @Bean public ManagementService managementService() { ManagementService service = new ManagementService(ehcache().getObject(), mbeanServer().getObject(), true, true, true, true); service.init(); return service; } }
実行クラス
Spring Boot で実行するための実行クラスです。
src/main/java/sample/Application.java
package sample; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.ComponentScan; @ComponentScan @EnableAutoConfiguration public class Application { public static void main(String... args) { SpringApplication.run(Application.class, args); } }
ビルド定義
Spring の Cache Abstraction 機能は 2つのモジュールに分かれており、Ehcache を使用するには spring-context-support
が必要です。
モジュール | 備考 |
---|---|
spring-context | Cache の基本機能 |
spring-context-support | 各種キャッシュライブラリ用の実装 |
今回使用した spring-boot-starter-web
は spring-context-support
を依存関係に含んでいなかったため、依存定義をする必要がありました。
build.gradle
apply plugin: 'spring-boot' def enc = 'UTF-8' tasks.withType(AbstractCompile)*.options*.encoding = enc buildscript { repositories { jcenter() } dependencies { classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.3.RELEASE' } } repositories { jcenter() } dependencies { compile 'org.springframework.boot:spring-boot-starter-web:1.2.3.RELEASE' // 以下は org.springframework.cache.ehcache のために必要 compile 'org.springframework:spring-context-support:4.1.6.RELEASE' // @Inject アノテーションのために必要 compile 'javax:javaee-api:7.0' // @CacheResult アノテーションのために必要 compile 'javax.cache:cache-api:1.0.0-PFD' compile 'net.sf.ehcache:ehcache:2.10.0' }
キャッシュ設定
Ehcache の設定は以下の通り。 キャッシュの有効期間を 10 秒としています。
src/main/resources/ehcache.xml
<?xml version="1.0" encoding="UTF-8"?> <ehcache> <cache name="sample" maxElementsInMemory="100" eternal="false" timeToLiveSeconds="10" /> </ehcache>
実行
bootRun タスクで Web アプリケーションを起動します。
起動
> gradle bootRun ・・・ :bootRun ・・・ 2015-05-04 18:37:07.807 INFO 6704 --- [ main] sample.Application : Started Application in 5.674 seconds (JVM running for 6.24) 2015-05-04 18:37:13.436 INFO 6704 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring FrameworkServlet 'dispatcherServlet' ・・・
何度か同じ URL へアクセスしてみると、10秒間のキャッシュが効いている事を確認できました。
動作確認
$ curl http://localhost:8080/sample/aaa sample: aaa, 1430732146256 $ curl http://localhost:8080/sample/aaa sample: aaa, 1430732146256 $ curl http://localhost:8080/sample/aaa sample: aaa, 1430732146256 $ curl http://localhost:8080/sample/aaa sample: aaa, 1430732158070
JMX でキャッシュ状況を取得
JMX を使って Ehcache のキャッシュヒット状況などを確認してみます。
Attach API による JMX エージェント (MBean サーバー) の適用
Java の Attach API を使えば、ローカルで実行中の Java プロセスへアタッチして JMX エージェント (MBean サーバー) を動的に適用できます。
そのため、アプリケーションの起動時 (gradle bootRun) に JMX 用の実行時オプションを指定しておく必要はありません。
Attach API を使って JMX エージェントを適用する手順は以下の通りです。
- (1) Java VM の ID (プロセスID) を指定して VM へアタッチ
- (2) (1) を使って JMX 用のサービス URL を取得
- (3) (2) が null の場合に JMX エージェントを適用し (2) を再実施
(2) の戻り値が null の場合、JMX エージェントが未適用という事ですので、management-agent.jar
を loadAgent
し JMX エージェントを適用します。
一度 JMX エージェントを適用しておけば、それ以降はサービス URL を取得できるので、その URL を使って MBean サーバーへ接続します。
Ehcache の JMX
Ehcache の JMX オブジェクト名は以下のようになります。
net.sf.ehcache:type=CacheStatistics,CacheManager=__DEFAULT__,name=<キャッシュ名>
net.sf.ehcache:type=CacheStatistics,*
をオブジェクト名に指定して queryNames
すれば Ehcache に関する全オブジェクト名を取得できます。
そうして取得したオブジェクト名を使って CacheHits
等を getAttribute
すればキャッシュのヒット数などを取得できます。
Groovy で実装
今回は Ehcache の JMX からキャッシュヒット数などを取得する処理を Groovy で実装してみました。
ehcache_jmx1.groovy
import javax.management.ObjectName import javax.management.remote.JMXConnectorFactory import javax.management.remote.JMXServiceURL import com.sun.tools.attach.VirtualMachine // JMX のサービス URL 取得処理 def getServiceUrl = { it.agentProperties.getProperty('com.sun.management.jmxremote.localConnectorAddress') } def pid = args[0] // (1) Java VM へアタッチ def vm = VirtualMachine.attach(pid) // (2) JMX のサービス URL 取得 def url = getServiceUrl(vm) if (url == null) { def javaHome = vm.systemProperties.getProperty('java.home') // (3) JMX エージェントを適用 vm.loadAgent("${javaHome}/lib/management-agent.jar") // JMX のサービス URL を再取得 url = getServiceUrl(vm) } vm.detach() // MBean サーバーへの接続 def con = JMXConnectorFactory.connect(new JMXServiceURL(url)) def server = con.getMBeanServerConnection() def cacheName = new ObjectName('net.sf.ehcache:type=CacheStatistics,*') // EhCache に関する JMX オブジェクト名の取得 server.queryNames(cacheName, null).each { name -> println "# ${name}" // ヒット回数などの属性値を取得 def res = [ 'CacheHits', // キャッシュのヒット回数 'CacheMisses', // キャッシュのミス回数 'InMemoryHits', 'InMemoryMisses' ].collectEntries { attr -> [attr, server.getAttribute(name, attr)] } println res } con.close()
実行例
> groovy ehcache_jmx1.groovy 6704 # net.sf.ehcache:type=CacheStatistics,CacheManager=__DEFAULT__,name=sample [CacheHits:7, CacheMisses:3, InMemoryHits:8, InMemoryMisses:2]
また、JMX のサービス URL は ConnectorAddressLink.importFrom(<プロセスID>)
で取得する事も可能です。
ehcache_jmx2.groovy
・・・ import com.sun.tools.attach.VirtualMachine import sun.management.ConnectorAddressLink // JMX のサービス URL 取得処理 (ConnectorAddressLink 利用版) def getServiceUrl = { ConnectorAddressLink.importFrom(it as int) } def pid = args[0] def url = getServiceUrl(pid) if (url == null) { def vm = VirtualMachine.attach(pid) def javaHome = vm.systemProperties.getProperty('java.home') vm.loadAgent("${javaHome}/lib/management-agent.jar") vm.detach() url = getServiceUrl(pid) } ・・・
Spring MVC で Controller を動的に切り替える - RequestMappingHandlerMapping のサブクラス利用
Spring MVC では、基本的に @RequestMapping
アノテーションで指定した URL パターンに合致する Controller のメソッドを実行し、org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
クラスがその処理を担っています。
そこで、試しに RequestMappingHandlerMapping
のサブクラスを使って、実行対象の Controller (@RequestMapping
のパス違い) を動的に切り替えるようにしてみました。
サンプルソースは http://github.com/fits/try_samples/tree/master/blog/20150507/
はじめに
今回は、以下のように Query string (URL) の debug
パラメータ有無によって、実行する Controller を切り替える処理を RequestMappingHandlerMapping
のサブクラスで実現します。
URL | 実行するメソッド名 |
---|---|
/sample/xxx | SampleController.sample |
/sample/xxx?debug | DebugSampleController.sample |
ただし、Controller を切り替えなくても他にやり様はいくらでもありますので、本件の実用性は低いと思います。
実装
Controller クラス
まずは Controller を 2つ用意します。
src/main/java/sample/controller/SampleController.java
package sample.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; @Controller public class SampleController { @RequestMapping("/sample/{id}") @ResponseBody public String sample(@PathVariable("id") String id) { return "sample: " + id + ", " + System.currentTimeMillis(); } }
2つ目には @RequestMapping
パスの先頭に /debug
を付けました。
src/main/java/sample/controller/DebugSampleController.java
package sample.controller; ・・・ @Controller public class DebugSampleController { @RequestMapping("/debug/sample/{id}") @ResponseBody public String sample(@PathVariable("id") String id) { return "debug-sample: " + id + ", " + new Date(); } }
RequestMappingHandlerMapping サブクラス
次に、本題の RequestMappingHandlerMapping サブクラスを実装します。
とりあえず、下記のように実装すれば別の Controller を呼び出せます。
- (1)
lookupHandlerMethod
メソッドをオーラーライドし lookupPath を変更 - (2)
HttpServletRequest.getServletPath()
の値を (1) に合わせて変更
(1) を実施しただけでは内部的なパスのチェック処理に引っかかるので (2) も合わせて実施する必要があります。
下記では、Query string へ debug
が付いていた場合に Controller を選出するパス (lookupPath) の先頭に /debug
を追加するように実装し、getServletPath() の戻り値を変更するために HttpServletRequestWrapper のサブクラスを使っています。
src/main/java/sample/mapping/SampleRequestMappingHandlerMapping.java
package sample.mapping; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; public class SampleRequestMappingHandlerMapping extends RequestMappingHandlerMapping { // (1) @Override protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception { return super.lookupHandlerMethod( changePath(lookupPath, request), new SampleHttpServletRequest(request) ); } // Controller を選出するパスを書き換える private String changePath(String path, HttpServletRequest request) { if (request.getParameter("debug") != null) { return "/debug" + path; } return path; } class SampleHttpServletRequest extends HttpServletRequestWrapper { public SampleHttpServletRequest(HttpServletRequest req) { super(req); } // (2) @Override public String getServletPath() { return changePath(super.getServletPath(), this); } } }
設定クラス
SampleRequestMappingHandlerMapping
を適用するように Bean 定義を行います。
src/main/java/sample/config/WebConfig.java
package sample.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import sample.mapping.SampleRequestMappingHandlerMapping; @Configuration @EnableWebMvc public class WebConfig { @Bean public RequestMappingHandlerMapping requestMappingHandlerMapping() { return new SampleRequestMappingHandlerMapping(); } }
実行クラス
今回は Spring Boot を使って実行しますので、そのための実行クラスも用意します。
src/main/java/sample/Application.java
package sample; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.ComponentScan; @ComponentScan @EnableAutoConfiguration public class Application { public static void main(String... args) { SpringApplication.run(Application.class, args); } }
ビルド定義
Gradle のビルド定義ファイルは以下の通りです。 (Spring Boot 利用)
build.gradle
apply plugin: 'spring-boot' def enc = 'UTF-8' tasks.withType(AbstractCompile)*.options*.encoding = enc buildscript { repositories { jcenter() } dependencies { classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.3.RELEASE' } } repositories { jcenter() } dependencies { compile 'org.springframework.boot:spring-boot-starter-web:1.2.3.RELEASE' }
実行
bootRun
タスクを実行し、Tomcat 上で Web アプリケーションを起動しておきます。
起動
> gradle bootRun ・・・ :bootRun . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v1.2.3.RELEASE) ・・・ 2015-05-03 16:43:04.306 INFO 4860 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http) 2015-05-03 16:43:04.308 INFO 4860 --- [ main] sample.Application : Started Application in 5.001 seconds (JVM running for 6.5)
/sample/xxx
と /sample/xxx?debug
へアクセスすると、?debug
の有無で実行結果 (実行対象 Controller) が変化する事を確認できました。
動作確認
$ curl http://localhost:8080/sample/abc sample: abc, 1430639107957 $ curl http://localhost:8080/sample/abc?debug debug-sample: abc, Sun May 03 16:45:12 JST 2015
Apache Spark でロジスティック回帰
以前 ※ に R や Julia で試したロジスティック回帰を Apache Spark の MLlib (Machine Learning Library) を使って実施してみました。
サンプルソースは http://github.com/fits/try_samples/tree/master/blog/20150427/
※「 R でロジスティック回帰 - glm, MCMCpack 」、「 Julia でロジスティック回帰-glm 」
はじめに
R の時と同じデータを使いますが、ヘッダー行を削除しています。(「R でロジスティック回帰 - glm, MCMCpack」 参照)
データ data4a.csv
8,1,9.76,C 8,6,10.48,C 8,5,10.83,C ・・・
データ内容は以下の通り。個体 i それぞれにおいて 「 個の観察種子のうち生きていて発芽能力があるものは 個」 となっています。
項目 | 内容 |
---|---|
N | 観察種子数 |
y | 生存種子数 |
x | 植物の体サイズ |
f | 施肥処理 (C: 肥料なし, T: 肥料あり) |
体サイズ x
と肥料による施肥処理 f
が種子の生存する確率(ある個体 i から得られた種子が生存している確率)にどのように影響しているかをロジスティック回帰で解析します。
MLlib によるロジスティック回帰
今回は org.apache.spark.mllib.classification.LogisticRegressionWithLBFGS
を使用します。
LogisticRegressionWithLBFGS について
LogisticRegressionWithLBFGS
で以前と同様のロジスティック回帰を実施するには以下が必要です。
setIntercept
で true を設定
この値が false (デフォルト値) の場合、結果の intercept 値が 0 になります。
なお、今回のように二項分布を使う場合は numClasses の値を変更する必要はありませんが (デフォルト値が 2 のため)、応答変数が 3状態以上の多項分布を使う場合は setNumClasses
で状態数に応じた値を設定します。
LabeledPoint について
LogisticRegressionWithLBFGS
へ与えるデータは LabeledPoint
で用意します。
R や Julia では 応答変数 ~ 説明変数1 + 説明変数2 + ・・・
のように応答変数と説明変数を指定しましたが、LabeledPoint
では下記のようにメンバー変数で表現します。
メンバー変数 | 応答変数・説明変数 |
---|---|
label | 応答変数 |
features | 説明変数 |
値は Double
とする必要がありますので、f 項目のような文字列値は数値化します。
更に、二項分布を使う場合 (numClasses = 2) は応答変数の値が 0 か 1 でなければなりません。
LabeledPoint への変換例
例えば、以下のようなデータを応答変数 y 項目、説明変数 x と f 項目の LabeledPoint
へ変換する場合
変換前のデータ (N = 8, y = 6)
8,6,10.48,C
次のようになります。
変換後のデータイメージ
LabeledPoint(label: 1.0, features: Vector(10.48, 0.0)) LabeledPoint(label: 1.0, features: Vector(10.48, 0.0)) LabeledPoint(label: 1.0, features: Vector(10.48, 0.0)) LabeledPoint(label: 1.0, features: Vector(10.48, 0.0)) LabeledPoint(label: 1.0, features: Vector(10.48, 0.0)) LabeledPoint(label: 1.0, features: Vector(10.48, 0.0)) LabeledPoint(label: 0.0, features: Vector(10.48, 0.0)) LabeledPoint(label: 0.0, features: Vector(10.48, 0.0))
8個(N)の中で 6個(y)生存していたデータのため、
label
(応答変数) の値が 1.0 (生存) のデータ 6個と 0.0 のデータ 2個へ変換します。
ちなみに、f 項目の値が C
の場合は 0.0、T
の場合は 1.0 としています。
実装
実装してみると以下のようになります。
LogisticRegression.scala
import org.apache.spark.SparkContext import org.apache.spark.mllib.classification.LogisticRegressionWithLBFGS import org.apache.spark.mllib.regression.LabeledPoint import org.apache.spark.mllib.linalg.Vectors object LogisticRegression extends App { // f項目の値を数値へ変換 val factor = (s: String) => s match { case "C" => 0 case _ => 1 } val sc = new SparkContext("local", "LogisticRegression") // データの準備 (100行のデータ -> 800個の LabeledPoint) val rdd = sc.textFile(args(0)).map(_.split(",")).flatMap { d => val n = d(0).toInt val x = d(1).toInt // 説明変数の値 val v = Vectors.dense(d(2).toDouble, factor(d(3))) // 応答変数が 1 のデータ x 個と 0 のデータ n - x 個を作成 List.fill(x)( LabeledPoint(1, v) ) ++ List.fill(n -x)( LabeledPoint(0, v) ) } // ロジスティック回帰の実行 val res = new LogisticRegressionWithLBFGS() // .setNumClasses(2) //省略可 .setIntercept(true) .run(rdd) println(res) }
ビルド
以下のような Gradle ビルド定義ファイルを使って実行します。
build.gradle
apply plugin: 'scala' apply plugin: 'application' mainClassName = 'LogisticRegression' repositories { jcenter() } dependencies { compile 'org.scala-lang:scala-library:2.11.6' compile('org.apache.spark:spark-mllib_2.11:1.3.1') { // ログ出力の抑制 exclude module: 'slf4j-log4j12' } // ログ出力の抑制 runtime 'org.slf4j:slf4j-nop:1.7.12' } run { if (project.hasProperty('args')) { args project.args.split(' ') } }
不要な WARN ログ出力を抑制するため以下のファイルも用意しました。
src/main/resources/log4j.properties
log4j.rootLogger=off
実行
実行結果は以下の通りです。
実行結果
> gradle run -Pargs=data4a.csv :clean :compileJava UP-TO-DATE :compileScala :processResources :classes :run (weights=[1.952347703282676,2.021401680901667], intercept=-19.535421113192506) BUILD SUCCESSFUL
以前に実施した R の結果 (Estimate の値) とほとんど同じ値になっています。
R の glm 関数による結果
Coefficients: Estimate Std. Error z value Pr(>|z|) (Intercept) -19.5361 1.4138 -13.82 <2e-16 *** x 1.9524 0.1389 14.06 <2e-16 *** fT 2.0215 0.2313 8.74 <2e-16 ***
Java 8 でグルーピング処理 - List<V> を Map<K, V> へ変換
Java 8 で List<V>
を Map<K, V>
へ変換するようなグルーピング処理をいくつか試してみました。
ソースは http://github.com/fits/try_samples/tree/master/blog/20150420/
はじめに
今回は、下記をリスト化した List<Data>
を id
でグルーピングして Map<String, Data>
へ変換します。
class Data { private String id; private String name; public Data(String id, String name) { this.id = id; this.name = name; } public String getId() { return id; } ・・・ }
Java 8 より前のバージョンでは以下のようにすると思います。
拡張 for 利用
List<Data> dataList = Arrays.asList( new Data("d1", "sample1"), new Data("d2", "sample2"), new Data("d3", "sample3") ); Map<String, Data> res = new HashMap<>(); for (Data d : dataList) { res.put(d.getId(), d); }
また、Java 8 で Map<String, List<Data>>
へ変換するなら Collectors.groupingBy
を使うだけです。
groupingBy で Map<String, List > へ変換
Map<String, List<Data>> res = dataList.stream().collect( Collectors.groupingBy(Data::getId) );
(1) forEach
まずは、拡張 for の変わりに forEach
メソッドを使用する方法です。
Map<String, Data> res = new HashMap<>(); dataList.forEach(d -> res.put(d.getId(), d));
(2) toMap
次は、Collectors.toMap
を使用する方法です。
toMap の 2引数版
Map<String, Data> res = dataList.stream().collect(
Collectors.toMap(Data::getId, d -> d)
);
もしくは
Map<String, Data> res = dataList.stream().collect( Collectors.toMap(Data::getId, UnaryOperator.identity()) );
ここで、2引数版の toMap メソッドには以下のような注意点があります。
- 同一キーを持つオブジェクトを複数含んでいると
IllegalStateException
を throw する
例えば、以下は IllegalStateException となります。
IllegalStateException が発生するコード例
List<Data> dataList2 = Arrays.asList( new Data("d1", "sample1"), new Data("d2", "sample2"), new Data("d3", "sample3"), new Data("d1", "sample1-b") // d1 が重複 ); // IllegalStateException: Duplicate key Data(d1, sample1) が発生 Map<String, Data> res = dataList2.stream().collect( Collectors.toMap(Data::getId, d -> d) );
IllegalStateException エラー内容
Exception in thread "main" java.lang.IllegalStateException: Duplicate key Data(d1, sample1)
IllegalStateException を発生させないようにするには、3引数版の toMap を使います。
toMap の 3引数版
第 3引数で同一キーの値が複数あった場合にどちらを選択するかを指定します。
最初の要素を採用する場合
// 結果 [ d1: Data(d1, sample1), d2: Data(d2, sample2), d3: Data(d3, sample3) ] Map<String, Data> res = dataList2.stream().collect( Collectors.toMap(Data::getId, d -> d, (d1, d2) -> d1) );
最後の要素を採用する場合
// 結果 [ d1: Data(d1, sample1-b), d2: Data(d2, sample2), d3: Data(d3, sample3) ] Map<String, Data> res = dataList2.stream().collect( Collectors.toMap(Data::getId, d -> d, (d1, d2) -> d2) );
(3) groupingBy + collectingAndThen
あまり実用的では無いと思いますが、groupingBy と collectingAndThen を組み合わせる方法も考えられます。
// 結果 [ d1: Data(d1, sample1), d2: Data(d2, sample2), d3: Data(d3, sample3) ] Map<String, Data> res = dataList2.stream().collect( Collectors.groupingBy( Data::getId, Collectors.collectingAndThen( Collectors.toList(), a -> a.get(0) //最初の要素を採用 ) ) );
toList を minBy 等で代用する事も可能です。
Map<String, Data> res = dataList2.stream().collect( Collectors.groupingBy( Data::getId, Collectors.collectingAndThen( Collectors.minBy((a, b) -> 0), a -> a.get() ) ) );
(4) collect の 3引数版
最後に、3引数版の collect を使う方法です。
パラレル実行で使用する第 3引数が必須となっている点が微妙だと思います。 (下記 Map::putAll の箇所を null にすると NullPointerException となります)
Map<String, Data> res = dataList.stream().collect( HashMap::new, (m, d) -> m.put(d.getId(), d), Map::putAll );
Gradle を使った Querydsl SQL のコード生成
前々回 と 前回 に続き、今回は Querydsl SQL のコード生成を Gradle で実施してみました。
ソースは http://github.com/fits/try_samples/tree/master/blog/20150413/
なお、Querydsl 4.0 からパッケージ名等が変更になるようなのでご注意ください。(github の master ブランチでは com.querydsl
となっています)
Gradle を使ったコード生成
Querydsl SQL の場合は Querydsl JPA・MongoDB と違って DB からメタデータを取得してコードを生成します。
querydsl-sql-codegen
モジュールを使用し、アノテーションプロセッサではなく Ant 用のタスクや API を使ってコードを生成します。
(a) Ant 用のタスクを使用
まずは Ant 用のタスククラス com.mysema.query.sql.ant.AntMetaDataExporter
を Gradle から使ってコード生成してみます。
基本的に、DB への接続が発生するため JDBC ドライバーが必要となります。 また、コード生成のログを出力するには slf4j を使います。(slf4j を使わなくても支障はありません)
今回は MySQL を使いました。
build1.gradle
apply plugin: 'java' // DB 接続 URL def qdslDbUrl = 'jdbc:mysql://localhost:3306/sample1?user=root' // コード生成先ディレクトリ def qdslDestDir = 'src/main/qdsl-generated' // 生成するコードの所属パッケージ def qdslDestPackage = 'sample.model' repositories { jcenter() } configurations { querydsl } dependencies { // コード生成用の依存モジュール定義 querydsl 'com.mysema.querydsl:querydsl-sql-codegen:3.6.3' querydsl 'mysql:mysql-connector-java:5.1.35' querydsl 'org.slf4j:slf4j-simple:1.7.12' compile 'com.mysema.querydsl:querydsl-sql:3.6.3' } task generate << { // Ant 用のタスク定義 ant.taskdef( name: 'export', classname: 'com.mysema.query.sql.ant.AntMetaDataExporter', classpath: configurations.querydsl.asPath ) // コード生成の実行 ant.export( jdbcDriverClass: 'com.mysql.jdbc.Driver', dbUrl: qdslDbUrl, targetSourceFolder: qdslDestDir, targetPackage: qdslDestPackage ) } compileJava { dependsOn generate sourceSets.main.java.srcDir qdslDestDir } clean { delete qdslDestDir }
ビルド結果は以下の通りです。
事前に MySQL を起動し、テーブル等を作成しておきます。
ビルド例
> gradle -b build1.gradle build :generate [ant:export] [main] INFO com.mysema.query.sql.codegen.MetaDataExporter - Exported product successfully [ant:export] [main] INFO com.mysema.query.sql.codegen.MetaDataExporter - Exported variation successfully :compileJava :processResources UP-TO-DATE :classes :jar :assemble :compileTestJava UP-TO-DATE :processTestResources UP-TO-DATE :testClasses UP-TO-DATE :test UP-TO-DATE :check UP-TO-DATE :build BUILD SUCCESSFUL
generate タスクの実行時に以下のようなソースコードが生成されました。
- src/main/qdsl-generated/sample/model/QProduct.java
- src/main/qdsl-generated/sample/model/QVariation.java
ちなみに、今回は以下のようなテーブルを用意しています。
DDL 例
CREATE TABLE `product` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `variation` ( `product_id` int(11) NOT NULL, `size` varchar(10) NOT NULL, `color` varchar(10) NOT NULL, PRIMARY KEY (`product_id`,`size`,`color`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
(b) API を使用
次は、コード生成の API (com.mysema.query.sql.codegen.MetaDataExporter
クラス)を Gradle から直接使います。
MetaDataExporter
クラスへ出力パッケージや出力ディレクトリなどを設定した後、export
メソッドへ DatabaseMetaData
を渡せばコードを生成します。
なお、今回は使っていませんが、com.mysema.query.sql.Configuration
で特定のカラムの型 (コード生成の Java の型) を変更するような事も可能です。
build2.gradle
apply plugin: 'java' def qdslDbUrl = 'jdbc:mysql://localhost:3306/sample1?user=root' def qdslDestDir = 'src/main/qdsl-generated' def qdslDestPackage = 'sample.model' buildscript { repositories { jcenter() } dependencies { // コード生成用の依存モジュール定義 classpath 'com.mysema.querydsl:querydsl-sql-codegen:3.6.3' classpath 'mysql:mysql-connector-java:5.1.35' } } repositories { jcenter() } dependencies { compile 'com.mysema.querydsl:querydsl-sql:3.6.3' } task generate << { // DB 接続 def con = new com.mysql.jdbc.Driver().connect(qdslDbUrl, null) def exporter = new com.mysema.query.sql.codegen.MetaDataExporter() exporter.targetFolder = new File(qdslDestDir) exporter.packageName = qdslDestPackage // コード生成の実行 exporter.export(con.metaData) con.close() } compileJava { dependsOn generate sourceSets.main.java.srcDir qdslDestDir } clean { delete qdslDestDir }
ビルド結果は以下の通りです。
ビルド例
> gradle -b build2.gradle build :generate :compileJava :processResources UP-TO-DATE :classes :jar :assemble :compileTestJava UP-TO-DATE :processTestResources UP-TO-DATE :testClasses UP-TO-DATE :test UP-TO-DATE :check UP-TO-DATE :build BUILD SUCCESSFUL
成果物は (a) のケースと同じです。
サンプルアプリケーション
最後に、簡単なサンプルアプリケーションを実装し実行してみます。
ビルド定義
(b) のビルド定義をベースに以下のようなビルド定義を用意しました。
build.gradle
apply plugin: 'application' def enc = 'UTF-8' tasks.withType(AbstractCompile)*.options*.encoding = enc def qdslDbUrl = 'jdbc:mysql://localhost:3306/sample1?user=root' def qdslDestDir = 'src/main/qdsl-generated' def qdslDestPackage = 'sample.model' // 実行クラス mainClassName = 'sample.App' buildscript { repositories { jcenter() } dependencies { classpath 'com.mysema.querydsl:querydsl-sql-codegen:3.6.3' classpath 'mysql:mysql-connector-java:5.1.35' } } repositories { jcenter() } dependencies { compile 'com.mysema.querydsl:querydsl-sql:3.6.3' // 実行用の依存モジュール定義 runtime 'mysql:mysql-connector-java:5.1.35' runtime 'org.slf4j:slf4j-nop:1.7.12' } task generate << { def con = new com.mysql.jdbc.Driver().connect(qdslDbUrl, null) def exporter = new com.mysema.query.sql.codegen.MetaDataExporter() exporter.targetFolder = new File(qdslDestDir) exporter.packageName = qdslDestPackage exporter.export(con.metaData) con.close() } compileJava { dependsOn generate sourceSets.main.java.srcDir qdslDestDir } clean { delete qdslDestDir }
アプリケーションクラス
product・variation テーブルへそれぞれデータを insert して select するだけの単純な処理を実装しました。
insert は set
メソッドを使う方法と columns
・values
メソッドを使う方法を用いてみました。 (他にも Bean を populate
する方法があります)
src/main/sample/App.java
package sample; import com.mysema.query.sql.Configuration; import com.mysema.query.sql.MySQLTemplates; import com.mysema.query.sql.SQLQuery; import com.mysema.query.sql.dml.SQLInsertClause; import sample.model.QProduct; import sample.model.QVariation; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class App { public static void main(String... args) throws SQLException { String dbUrl = "jdbc:mysql://localhost:3306/sample1?user=root"; Configuration conf = new Configuration(new MySQLTemplates()); QProduct p = QProduct.product; QVariation v = QVariation.variation; try (Connection con = DriverManager.getConnection(dbUrl)) { con.setAutoCommit(false); // product テーブルへレコードを insert Integer id = new SQLInsertClause(con, conf, p) .set(p.name, "test" + System.currentTimeMillis()) .executeWithKey(p.id); // variation テーブルへレコードを insert new SQLInsertClause(con, conf, v) .columns(v.productId, v.size, v.color) .values(id, "L", "white") .execute(); // variation テーブルへレコードを insert new SQLInsertClause(con, conf, v) .columns(v.productId, v.size, v.color) .values(id, "S", "blue") .execute(); con.commit(); // product と variation を join して select new SQLQuery(con, conf) .from(p) .join(v).on(v.productId.eq(p.id)) .where(p.name.startsWith("test")) .list(p.id, p.name, v.color, v.size) .forEach(System.out::println); } } }
実行
実行結果は以下の通りです。
実行結果
> gradle run ・・・ :run [1, test1428773665876, white, L] [1, test1428773665876, blue, S] BUILD SUCCESSFUL
Gradle を使った Querydsl MongoDB のコード生成
前回の Querydsl JPA に続き、今回は Querydsl MongoDB のコード生成を Gradle で実施してみました。
ソースは http://github.com/fits/try_samples/tree/master/blog/20150330/
Gradle を使ったコード生成
Querydsl MongoDB の場合は、前回 のビルド定義ファイルから依存モジュールとアノテーションプロセッサクラスを以下のように変えるだけです。 (querydsl-apt はそのまま使います)
- querydsl-jpa を
querydsl-mongodb
へ変更 - javaee-api を
morphia
へ変更 - com.mysema.query.apt.jpa.JPAAnnotationProcessor を
com.mysema.query.apt.morphia.MorphiaAnnotationProcessor
へ変更
なお、エンティティクラスは Morphia のアノテーションを使って定義します。
(a) compileJava タスクでコード生成とコンパイルを実施
compileJava タスクの実行時に Querydsl のコード生成とコンパイルの両方を実施するタイプです。
build1.gradle
apply plugin: 'java' repositories { jcenter() } configurations { apt } dependencies { apt 'com.mysema.querydsl:querydsl-apt:3.6.2' // Querydsl MongoDB 用の依存モジュール compile 'com.mysema.querydsl:querydsl-mongodb:3.6.2' compile 'org.mongodb.morphia:morphia:0.110' } compileJava { classpath += configurations.apt options.compilerArgs += [ '-processor', 'com.mysema.query.apt.morphia.MorphiaAnnotationProcessor' ] } jar { excludes << '**/*.java' }
ビルド結果は以下の通りです。
ビルド例
> gradle -b build1.gradle build :compileJava 注意:Running MorphiaAnnotationProcessor 注意:Serializing Entity types 注意:Generating sample.model.QProduct for [sample.model.Product] 注意:Serializing Embeddable types 注意:Generating sample.model.QVariation for [sample.model.Variation] 注意:Running MorphiaAnnotationProcessor 注意:Running MorphiaAnnotationProcessor :processResources UP-TO-DATE :classes :jar :assemble :compileTestJava UP-TO-DATE :processTestResources UP-TO-DATE :testClasses UP-TO-DATE :test UP-TO-DATE :check UP-TO-DATE :build BUILD SUCCESSFUL
自動生成されるソースの場所やクラス名の先頭に Q
が付くのは前回と同じです。
(b) 別タスクでコード生成
Querydsl のコード生成を別タスク (下記の generate) で実施するタイプです。
Querydsl は IDE で利用すると思いますので、こちらの方が使い易いと思います。
build2.gradle
apply plugin: 'java' def qdslDestDir = 'src/main/qdsl-generated' repositories { jcenter() } configurations { apt } dependencies { apt 'com.mysema.querydsl:querydsl-apt:3.6.2' compile 'com.mysema.querydsl:querydsl-mongodb:3.6.2' compile 'org.mongodb.morphia:morphia:0.110' } task generate(type: JavaCompile) { source = sourceSets.main.java classpath = configurations.compile + configurations.apt destinationDir = new File(qdslDestDir) options.compilerArgs += [ '-proc:only', '-processor', 'com.mysema.query.apt.morphia.MorphiaAnnotationProcessor' ] } compileJava { dependsOn generate sourceSets.main.java.srcDir qdslDestDir } clean { delete qdslDestDir }
ビルド結果は以下の通りです。
ビルド例
> gradle -b build2.gradle build :generate 注意:Running MorphiaAnnotationProcessor 注意:Serializing Entity types 注意:Generating sample.model.QProduct for [sample.model.Product] 注意:Serializing Embeddable types 注意:Generating sample.model.QVariation for [sample.model.Variation] 注意:Running MorphiaAnnotationProcessor 注意:Running MorphiaAnnotationProcessor :compileJava :processResources UP-TO-DATE :classes :jar :assemble :compileTestJava UP-TO-DATE :processTestResources UP-TO-DATE :testClasses UP-TO-DATE :test UP-TO-DATE :check UP-TO-DATE :build BUILD SUCCESSFUL
サンプルアプリケーション
最後に、簡単なサンプルアプリケーションを実装し実行してみます。
ビルド定義
別タスクによるコード生成タイプ (b) のビルド定義をベースに以下のようなビルド定義を用意しました。
build.gradle
apply plugin: 'application' def enc = 'UTF-8' tasks.withType(AbstractCompile)*.options*.encoding = enc def qdslDestDir = 'src/main/qdsl-generated' repositories { jcenter() } configurations { apt } dependencies { apt 'com.mysema.querydsl:querydsl-apt:3.6.2' compile 'com.mysema.querydsl:querydsl-mongodb:3.6.2' compile 'org.mongodb.morphia:morphia:0.110' } task generate(type: JavaCompile) { source = sourceSets.main.java classpath = configurations.compile + configurations.apt destinationDir = new File(qdslDestDir) options.compilerArgs += [ '-proc:only', '-processor', 'com.mysema.query.apt.morphia.MorphiaAnnotationProcessor' ] } compileJava { dependsOn generate sourceSets.main.java.srcDir qdslDestDir } clean { delete qdslDestDir } mainClassName = 'sample.App'
エンティティクラス
Morphia 用のエンティティクラスとして以下を定義しました。
src/main/sample/model/Product.java
package sample.model; import org.mongodb.morphia.annotations.*; import org.bson.types.ObjectId; import java.util.ArrayList; import java.util.List; @Entity public class Product { @Id private ObjectId id; private String name; @Embedded private List<Variation> variationList = new ArrayList<>(); public Product() { } public Product(String name) { setName(name); } public void setId(ObjectId id) { this.id = id; } public ObjectId getId() { return id; } public void setName(String name) { this.name = name; } public String getName() { return name; } public List<Variation> getVariationList() { return variationList; } }
src/main/sample/model/Variation.java
package sample.model; import org.mongodb.morphia.annotations.Embedded; @Embedded public class Variation { private String size; private String color; public Variation() { } public Variation(String size, String color) { setSize(size); setColor(color); } public String getSize() { return size; } public void setSize(String size) { this.size = size; } public String getColor() { return color; } public void setColor(String color) { this.color = color; } }
アプリケーションクラス
Product を保存した後に、Querydsl MongoDB の機能で簡単な検索を行う処理を実装しました。
src/main/sample/App.java
package sample; import com.mongodb.MongoClient; import com.mysema.query.mongodb.morphia.MorphiaQuery; import org.mongodb.morphia.Datastore; import org.mongodb.morphia.Morphia; import sample.model.Product; import sample.model.QProduct; import sample.model.Variation; import java.net.UnknownHostException; public class App { public static void main(String... args) throws UnknownHostException { Morphia morphia = new Morphia(); Datastore ds = morphia.createDatastore(new MongoClient(), "sample"); Product p = new Product("test" + System.currentTimeMillis()); p.getVariationList().add(new Variation("L", "black")); p.getVariationList().add(new Variation("M", "white")); ds.save(p); // Querydsl MongoDB を使った検索処理 QProduct qp = QProduct.product; MorphiaQuery<Product> query = new MorphiaQuery<>(morphia, ds, qp); query.where(qp.name.like("test%")).list().forEach(App::printProduct); } private static void printProduct(Product p) { System.out.println("----------"); System.out.println(p.getId() + ", " + p.getName()); p.getVariationList().forEach(v -> System.out.println(v.getColor() + ", " + v.getSize())); } }
実行
まず、MongoDB を起動しておきます。 今回は開発版の MongoDB 3.1.0 (SSL版) を使用しました。
MongoDB 起動
> mongod --dbpath data ・・・ 2015-03-29T23:50:32.137+0900 I CONTROL [initandlisten] ** NOTE: This is a development version (3.1.0) of MongoDB. ・・・ 2015-03-29T23:50:32.685+0900 I NETWORK [initandlisten] waiting for connections on port 27017
実行結果は以下の通りです。
実行結果
> gradle run ・・・ :run 3 29, 2015 11:52:47 午後 org.mongodb.morphia.logging.MorphiaLoggerFactory choose LoggerFactory 情報: LoggerImplFactory set to org.mongodb.morphia.logging.jdk.JDKLoggerFactory ---------- 551811bfcc87cb6a0dcb43fd, test1427640767733 black, L white, M BUILD SUCCESSFUL
MongoDB shell を使って登録内容を確認してみます。
データ確認
> mongo ・・・ MongoDB shell version: 3.1.0 connecting to: test Welcome to the MongoDB shell. ・・・ > use sample switched to db sample > db.Product.find() { "_id" : ObjectId("551811bfcc87cb6a0dcb43fd"), "className" : "sample.model.Product", "name" : "test1427640767733", "variationList" : [ { "size" : "L", "color" : "black" }, { "size" : "M", "color" : "white" } ] }
Gradle を使った Querydsl JPA のコード生成
今回は Querydsl JPA のコード生成を Gradle で実施してみました。
ソースは http://github.com/fits/try_samples/tree/master/blog/20150322/
はじめに
Querydsl JPA では JPA のエンティティクラスを元に Querydsl JPA 用のコードを自動生成して使います。
この場合のコード生成は querydsl-apt
モジュールに含まれているアノテーションプロセッサ com.querydsl.apt.jpa.JPAAnnotationProcessor
を javac のコンパイルオプションへ指定するだけです。
javac -processor com.mysema.query.apt.jpa.JPAAnnotationProcessor ・・・
Gradle の場合でも同様に上記コンパイルオプションを使用します。
(a) compileJava タスクでコード生成とコンパイルを実施
まずは、compileJava タスクの実行時に Querydsl のコード生成とコンパイルの両方を実施してみます。
下記 (1) のように compileJava の options.compilerArgs
へコンパイルオプション -processor com.mysema.query.apt.jpa.JPAAnnotationProcessor
を追加します。
こうすることで、src/main/java 内の JPA エンティティクラスを元に destinationDir
プロパティ値のディレクトリ (デフォルトでは build/classes/main) へ Querydsl JPA 用のソースファイルを自動生成し、コンパイルを実施します。
デフォルトでは destinationDir
内の全ファイルを JAR ファイルへ格納してしまいますので、下記の (2) では jar タスクの excludes
を使って Querydsl が生成したソースファイルを除外するようにしています。
build.gradle
apply plugin: 'java' repositories { jcenter() } configurations { apt } dependencies { // Querydsl JPA のコード生成モジュール apt 'com.mysema.querydsl:querydsl-apt:3.6.2' compile 'com.mysema.querydsl:querydsl-jpa:3.6.2' compile 'javax:javaee-api:7.0' } compileJava { classpath += configurations.apt // (1) アノテーションプロセッサの利用設定 options.compilerArgs += [ '-processor', 'com.mysema.query.apt.jpa.JPAAnnotationProcessor' ] } jar { // (2) 自動生成したソースファイルを jar ファイルから除外するための設定 excludes << '**/*.java' }
ビルド結果は以下の通りです。
ビルド例
> gradle build :compileJava 注意:Running JPAAnnotationProcessor 注意:Serializing Entity types 注意:Generating sample.model.QProduct for [sample.model.Product] 注意:Running JPAAnnotationProcessor 注意:Running JPAAnnotationProcessor :processResources UP-TO-DATE :classes :jar :assemble :compileTestJava UP-TO-DATE :processTestResources UP-TO-DATE :testClasses UP-TO-DATE :test UP-TO-DATE :check UP-TO-DATE :build BUILD SUCCESSFUL
JPA 用のエンティティクラス src/main/java/sample/model/Product.java を元に build/classes/main/sample/model/QProduct.java ファイルを生成し、ビルドを行っています。
ビルド結果の JAR ファイルは以下のような内容になります。
JAR ファイルの内容例
- sample/model
- Product.class
- QProduct.class
- META-INF
- MANIFEST.MF
(b) 別タスクでコード生成
次は、Querydsl のコード生成を別タスク (下記の generate) で実施するようにしてみます。
javac のコンパイルオプションで -proc:only
とすれば、コンパイルを実施せずアノテーションプロセッサだけを実施するようになりますので、これを利用します。
JavaCompile
を type へ指定したタスクを定義して source・classpath・destinationDir (コード生成の出力先) を設定し、コンパイルオプションへ -proc:only
を追加します。
あとは compileJava タスクのコンパイル対象ソースへ Querydsl コード生成先ディレクトリを追加するだけです。 (タスクの依存設定は必要に応じて実施)
build2.gradle
apply plugin: 'java' // コード生成先 def qdslDestDir = 'src/main/qdsl-generated' repositories { jcenter() } configurations { apt } dependencies { apt 'com.mysema.querydsl:querydsl-apt:3.6.2' compile 'com.mysema.querydsl:querydsl-jpa:3.6.2' compile 'javax:javaee-api:7.0' } // Querydsl JPA コード生成用のタスク定義 task generate(type: JavaCompile) { source = sourceSets.main.java classpath = configurations.compile + configurations.apt // コード生成の出力先を設定 destinationDir = new File(qdslDestDir) options.compilerArgs += [ '-proc:only', // アノテーションプロセッサのみ実施するための設定 (コンパイルしない) '-processor', 'com.mysema.query.apt.jpa.JPAAnnotationProcessor' ] } compileJava { dependsOn generate // コンパイル対象ソースへコード生成先ディレクトリを追加 sourceSets.main.java.srcDir qdslDestDir } clean { delete qdslDestDir }
ビルド結果は以下の通りです。
アノテーションプロセッサを compileJava タスクの前に実行している以外は先程の (a) と同じです。 (当然ながら JAR ファイルの内容も同じです)
ビルド例
> gradle -b build2.gradle build :generate 注意:Running JPAAnnotationProcessor 注意:Serializing Entity types 注意:Generating sample.model.QProduct for [sample.model.Product] 注意:Running JPAAnnotationProcessor 注意:Running JPAAnnotationProcessor :compileJava :processResources UP-TO-DATE :classes :jar :assemble :compileTestJava UP-TO-DATE :processTestResources UP-TO-DATE :testClasses UP-TO-DATE :test UP-TO-DATE :check UP-TO-DATE :build BUILD SUCCESSFUL