CLOVER🍀

That was when it all began.

Spring Boot CLI+Groovy+Grapeを使って、簡単にSpring Bootアプリケーションを書く

sdkman(gvmの頃からですが)を使って、Spring Bootをインストールすることができます。

$ sdk install springboot

ここでインストールされるspringコマンド(Spring Boot CLIって言ったらいいんでしょうか?)で、簡単にSpring Bootアプリケーションが書けるようなのですが、最近になってやっと試してみたのでメモ。

Spring Boot CLI

インストール

sdkman、もしくはzip、tar.gzを展開してインストールします。

Installing the Spring Boot CLI

今回は、sdkmanを使用しました。「springboot」をインストールします。

$ sdk install springboot

Downloading: springboot 1.3.2.RELEASE

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
100 9098k  100 9098k    0     0   749k      0  0:00:12  0:00:12 --:--:-- 1028k

Installing: springboot 1.3.2.RELEASE
Done installing!

Do you want springboot 1.3.2.RELEASE to be set as default? (Y/n): y 

Setting springboot 1.3.2.RELEASE as default.

確認。

$ spring version
Spring CLI v1.3.2.RELEASE

これで、インストールは完了です。

簡単なSpring Bootアプリケーションを書いてみる

ドキュメントに習って、サンプルアプリケーションを動かしてみます。

Running applications using the CLI

アプリケーションは、Groovyで記述します。

書いたスクリプトはこちら。
hello-mvc.groovy

@RestController
class WebApplication {
  @RequestMapping('helloworld')
  def hello() {
    'Hello World'
  }
}

実行。

$ spring run hello-mvc.groovy

初回は依存関係の解決に時間がかかったりしますが、恐ろしいことにこれだけでSpring MVCのアプリケーションが組み込みTomcatと共に起動します…。

Resolving dependencies................................

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.3.2.RELEASE)

2016-02-14 22:37:51.380  INFO 124992 --- [       runner-0] o.s.boot.SpringApplication               : Starting application on node with PID 124992 (/path/to/.m2/repository/org/springframework/boot/spring-boot/1.3.2.RELEASE/spring-boot-1.3.2.RELEASE.jar started by xxxxx in //path/to)
2016-02-14 22:37:51.473  INFO 124992 --- [       runner-0] o.s.boot.SpringApplication               : No active profile set, falling back to default profiles: default
2016-02-14 22:37:52.733  INFO 124992 --- [       runner-0] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@695ddd49: startup date [Sun Feb 14 22:37:52 JST 2016]; root of context hierarchy
2016-02-14 22:37:55.156  INFO 124992 --- [       runner-0] o.s.b.f.s.DefaultListableBeanFactory     : Overriding bean definition for bean 'beanNameViewResolver' with a different definition: replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]]
2016-02-14 22:37:56.740  INFO 124992 --- [       runner-0] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
2016-02-14 22:37:56.817  INFO 124992 --- [       runner-0] o.apache.catalina.core.StandardService   : Starting service Tomcat
2016-02-14 22:37:56.819  INFO 124992 --- [       runner-0] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.0.30
2016-02-14 22:37:57.136  INFO 124992 --- [ost-startStop-1] org.apache.catalina.loader.WebappLoader  : Unknown loader org.springframework.boot.cli.compiler.ExtendedGroovyClassLoader$DefaultScopeParentClassLoader@6c5060ab class org.springframework.boot.cli.compiler.ExtendedGroovyClassLoader$DefaultScopeParentClassLoader
2016-02-14 22:37:57.255  INFO 124992 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2016-02-14 22:37:57.255  INFO 124992 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 4523 ms
2016-02-14 22:37:58.359  INFO 124992 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean        : Mapping servlet: 'dispatcherServlet' to [/]
2016-02-14 22:37:58.369  INFO 124992 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'characterEncodingFilter' to: [/*]
2016-02-14 22:37:58.502  INFO 124992 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2016-02-14 22:37:58.503  INFO 124992 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2016-02-14 22:37:58.503  INFO 124992 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'requestContextFilter' to: [/*]
2016-02-14 22:37:58.781  INFO 124992 --- [ost-startStop-1] o.a.c.util.SessionIdGeneratorBase        : Creation of SecureRandom instance for session ID generation using [SHA1PRNG] took [212] milliseconds.
2016-02-14 22:37:59.380  INFO 124992 --- [       runner-0] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@695ddd49: startup date [Sun Feb 14 22:37:52 JST 2016]; root of context hierarchy
2016-02-14 22:37:59.490  INFO 124992 --- [       runner-0] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/helloworld]}" onto public java.lang.Object WebApplication.hello()
2016-02-14 22:37:59.494  INFO 124992 --- [       runner-0] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2016-02-14 22:37:59.495  INFO 124992 --- [       runner-0] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2016-02-14 22:37:59.543  INFO 124992 --- [       runner-0] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2016-02-14 22:37:59.543  INFO 124992 --- [       runner-0] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2016-02-14 22:37:59.616  INFO 124992 --- [       runner-0] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2016-02-14 22:38:00.901  INFO 124992 --- [       runner-0] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2016-02-14 22:38:01.298  INFO 124992 --- [       runner-0] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2016-02-14 22:38:01.325  INFO 124992 --- [       runner-0] o.s.boot.SpringApplication               : Started application in 11.18 seconds (JVM running for 50.941)

確認。

$ curl http://localhost:8080/helloworld
Hello World

コマンドライン引数を渡す場合は、「--」の後ろに入力するのだとか。例えば、組み込みTomcatのリッスンポートを変える場合はこちら。

$ spring run hello-mvc.groovy -- --server.port=9000

「--」の後に、「--server.port=9000」が入ります。

JavaVMにオプションを渡す場合は、JAVA_OPTSを使って設定するそうです。

$ JAVA_OPTS=-Xmx1024m spring run hello-mvc.groovy

プロキシ設定が必要な場合なども、これを使えばいいかな?

デフォルトでimportされているもの

先ほどのスクリプトで、いきなり@RestControllerや@RequestMappingを使用しましたが、@Component、@RestController、@RequestMappingはデフォルトでimportされているそうです。

Default import statements

Grapeに依存関係を推測させる

こちらにも記載がありますが、特定のクラスを使うことで、Grapeにヒントを与えていくつかの自動importが有効になるようです。

Deduced “grab” dependencies

このあたりの仕掛けは、以下のパッケージを見ればよさそうです。

https://github.com/spring-projects/spring-boot/tree/v1.3.2.RELEASE/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/autoconfigure

例えば、Spring MVCの場合は@Controller、@RestController、@EnableWebMvcのいずれかを見付けると、Spring MVC関連のものをimportに加えます。

https://github.com/spring-projects/spring-boot/blob/v1.3.2.RELEASE/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/autoconfigure/SpringMvcCompilerAutoConfiguration.java#L37-L38

この他、JdbcTemplateや@EnableTransactionManagementなどにも反応するようなので、必要に応じて見ていくとよいでしょう。

また、@Grabアノテーションに以下のようにartifactIdを書くだけでも、Spring Bootのデフォルトの依存関係の中から推測してくれるようです。

@Grab('freemarker')

Deduced “grab” coordinates

まあ、将来この動きは変わるかもよ、みたいなことが書かれていますが…。

その他

「spring test」でテスト、「spring jar [JARファイル名] [Groovyスクリプト]」でJARファイルが作れたり、「spring init」、「spring shell」などが使えるようですが、今回は割愛。

SpringのCache機能を使ったサンプル

というわけで、ここまでに紹介してきた機能を使ったサンプルを書いてみたいと思います。

Spring Cacheを使って、先ほどのSpring MVCのサンプルにCacheを入れてみます。追加するCacheはJCache(実装はHazelcast)とします。

で、書いたスクリプトはこちら。
hello-mvc-cache.groovy

import java.util.concurrent.TimeUnit

@Grab('cache-api')
@Grab('hazelcast')
import javax.cache.Caching
import javax.cache.configuration.MutableConfiguration
import org.springframework.cache.jcache.JCacheCacheManager

@EnableCaching
@Configuration
class Config {
  @Bean
  public CacheManager cacheManager() {
    def cacheManager = Caching.getCachingProvider().getCacheManager()
    cacheManager.createCache("simpleCache", new MutableConfiguration())

    new JCacheCacheManager(cacheManager)
  }
}

@RestController
@CacheConfig(cacheNames = 'simpleCache')
class WebApplication {

  @RequestMapping('helloworld')
  @Cacheable
  def hello(@RequestParam message) {
    TimeUnit.SECONDS.sleep(5L)
    "Hello $message".toString()
  }
}

CacheManagerの作成を追加したのと、RestControllerにはスリープを入れています。

で、Configurationの方ですが、@EnableCachingアノテーションがあるので以下の部分が動作します。

https://github.com/spring-projects/spring-boot/blob/v1.3.2.RELEASE/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/autoconfigure/CachingCompilerAutoConfiguration.java

これにより、いくつかのorg.springframework.cache関連のパッケージがimportされます。

JCacheとHazelcastは、artifactIdだけで引き込んでいます。

@Grab('cache-api')
@Grab('hazelcast')

では、起動。

$ spring run hello-mvc-cache.groovy

起動途中で、Hazelcastのログが現れるようになります。

2016-02-14 23:31:50.539  INFO 126713 --- [       runner-0] com.hazelcast.config.XmlConfigLocator    : Loading 'hazelcast-default.xml' from classpath.
2016-02-14 23:31:51.344  INFO 126713 --- [       runner-0] c.h.instance.DefaultAddressPicker        : [LOCAL] [dev] [3.5.4] Prefer IPv4 stack is true.
2016-02-14 23:31:51.357  INFO 126713 --- [       runner-0] c.h.instance.DefaultAddressPicker        : [LOCAL] [dev] [3.5.4] Picked Address[172.17.0.1]:5701, using socket ServerSocket[addr=/0:0:0:0:0:0:0:0,localport=5701], bind any local is true
2016-02-14 23:31:51.691  INFO 126713 --- [       runner-0] com.hazelcast.spi.OperationService       : [172.17.0.1]:5701 [dev] [3.5.4] Backpressure is disabled
2016-02-14 23:31:51.748  INFO 126713 --- [       runner-0] c.h.s.i.o.c.ClassicOperationExecutor     : [172.17.0.1]:5701 [dev] [3.5.4] Starting with 4 generic operation threads and 8 partition operation threads.
2016-02-14 23:31:52.451  INFO 126713 --- [       runner-0] com.hazelcast.system                     : [172.17.0.1]:5701 [dev] [3.5.4] Hazelcast 3.5.4 (20151125 - 56676b2) starting at Address[172.17.0.1]:5701
2016-02-14 23:31:52.451  INFO 126713 --- [       runner-0] com.hazelcast.system                     : [172.17.0.1]:5701 [dev] [3.5.4] Copyright (c) 2008-2015, Hazelcast, Inc. All Rights Reserved.
2016-02-14 23:31:52.460  INFO 126713 --- [       runner-0] com.hazelcast.instance.Node              : [172.17.0.1]:5701 [dev] [3.5.4] Creating MulticastJoiner
2016-02-14 23:31:52.465  INFO 126713 --- [       runner-0] com.hazelcast.core.LifecycleService      : [172.17.0.1]:5701 [dev] [3.5.4] Address[172.17.0.1]:5701 is STARTING
2016-02-14 23:31:54.797  INFO 126713 --- [       runner-0] c.h.cluster.impl.MulticastJoiner         : [172.17.0.1]:5701 [dev] [3.5.4] 


Members [1] {
	Member [172.17.0.1]:5701 this
}

2016-02-14 23:31:54.858  INFO 126713 --- [       runner-0] com.hazelcast.core.LifecycleService      : [172.17.0.1]:5701 [dev] [3.5.4] Address[172.17.0.1]:5701 is STARTED

動作確認。

1回目は低速ですが、2回目以降は高速になります。

## 1回目
$ time curl http://localhost:8080/helloworld?message=Groovy
Hello Groovy
real	0m5.451s
user	0m0.009s
sys	0m0.000s

## 2回目
$ time curl http://localhost:8080/helloworld?message=Groovy
Hello Groovy
real	0m0.065s
user	0m0.006s
sys	0m0.006s

まとめ

Spring Boot CLIを使ったGroovyスクリプトのサンプルと、簡単な説明を記載してみました。

なかなか簡単に使えて、便利そうですね!