第8回 Spring Bootを使ってみよう

spring

よく訓練されたアップル信者、都元です。さて、近年のSpring事情を語るのに避けて通れないのがSpring Bootです。

Spring Bootとは

とは言え、正直Spring Bootは何をしてくれるコンポーネントなのか。これを一言で説明するのは実は非常に難しいです。なので、以下に私の思うSpring Bootの大事な特徴を2つ挙げますが、それだけがSpring Bootではない、という認識もしておいてください。

簡単起動

作ったアプリケーションの起動を簡単にできます。具体的には java -jar your-app.jar でOK。面倒くさいクラスパスの指定等不要で、単一のjarファイルで起動可能になります。

これは、スタンドアロンのアプリケーションでも、Webアプリケーションでも同じです。つまり場合によっては java -jar your-app.jar によって、Webサーバが立ち上がることになります。もはやサーバ側にJettyやTomcat等のWebコンテナをセットアップする必要はありません。ただ、これはJettyやTomcat等が要らなくなったわけではなく、1つの実行jarファイルの中に取り込まれただけです。

さて、起動するエントリポイントは分かりやすく、Javaのmainメソッドです。ということは、IDE上でそのまま起動させることも簡単で、IDEのデバッガとの接続も楽々です。

設定の自動化

基本的な設定が自動化されます。XML地獄からは離脱したと思ったら、次に待っているのはアノテーション地獄だった、というのは至極当然な帰結です。

Spring Bootでは、例えば「クラスパス上にorg.springframework.jdbc.datasource.embedded.EmbeddedDatabaseTypeがあったら(つまりspring-jdbc-*.jarが依存に入っていたら)、DataSourceやJdbcTemplateのbean定義を自動的に行う」とか「クラスパス上にorg.thymeleaf.spring4.SpringTemplateEngineクラスがあったら、Thymeleafの基本設定を自動で行う」という動きをします。

かなりの上級黒魔術感がありますね。ただし、この仕組によって、ボイラープレートと呼ばれる大部分のつまらない設定が不要になります。デフォルトの自動設定内容では対応できなかった場合でも、アノテーションを駆使してシンプルな設定内容を維持できる傾向があります。

また、開発を進めるにあたって、ひとまず関連jarファイルを依存に追加するだけで、すぐに新しい機能を使いはじめる体制ができあがるわけです。

サポートするライブラリは、上に挙げた Spring JDBC や Thymeleaf の他に、Spring Security, Spring Data, Solr, 各種Logger, 各種Cache等、様々なものがあります。

Spring Bootによるコマンドラインアプリ

Gradleビルドスクリプトの設定

まず、Spring Bootアプリケーションをビルドするためには、Gradleのビルド設定を少々変更する必要があります。

  1. spring-boot-gradle-pluginを利用するため、ビルドスクリプトの依存ライブラリとして記述を追加する。
  2. spring-bootプラグインの適用を指示する。
  3. アプリケーションの依存ライブラリとして、spring-boot-starterを追加する。
  4. これによりexecuteタスクは不要になるので削除する。

具体的な差分は、GitHub上で確認してみてください。

一点注意頂きたいのは、1番は「Gradleのビルドスクリプトにおける依存ライブラリ」で、3番は「アプリケーションにおける依存ライブラリ」であることです。

アプリケーション本体の記述

続いて、アプリケーション本体を記述します。

@SpringBootApplication
public class SampleApplication implements CommandLineRunner {

    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(SampleApplication.class);
        app.run(args);
    }

    @Autowired
    Foo foo;

    @Override
    public void run(String... args) throws Exception {
        foo.bar();
    }
}

たったこれだけです。メインとなるアプリケーションクラスを1つ作り、@SpringBootApplicationアノテーションを付与、そのクラスのmainにて、自分のクラスを指定してSpringApplicationインスタンスを作って、runするだけです。

これを起動すると、まずこのクラスがあるパッケージ以下をスキャンして@Configuration@Component等のbean自動登録を行います。その上で、必要なDI(ここではFoo)を行いつつ、runメソッドを呼び出してくれるというわけです。

上の例では示しませんでしたが、このクラス内で@Beanによるspring bean定義も可能です。

これで、Spring Bootによるコマンドラインアプリケーションの出来上がりです。

ちなみに本来Springでアプリケーションを構築する場合は、「Spring configurationクラス」と「その他一般クラス」を基本的に分けて管理すべきです。前者は旧来SpringのXML Bean定義ファイルに相当するもので、`@Configuration`が付いたクラス内の`@Bean`が付いたメソッドでSpring bean定義を行うものです。後者は、主にアプリケーションのドメイン及びロジックを記述し、`@Autowired`等を介してDIを受け付けるものです。

コンパクトに例を示すために、上の例も含めて、これらを混ぜて1つのクラスとして示してしまうことが多いのですが、実践的なアプリケーションにおいては、これらのクラスは明確に分離すべきだと思っています。

サンプルアプリを実行してみる

いつものとおり、タグをチェックアウトして実行してみましょう。Spring Bootアプリケーションの起動はbootRunタスクから行います。

$ git clone https://github.com/classmethod-aws/berserker.git
$ cd berserker
$ git checkout refs/tags/part8-1
$ ./gradlew bootRun
:compileJava
:processResources
:classes
:findMainClass
:bootRun

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

2015/07/21 13:00:55.135 [main] INFO  j.c.e.berserker.BerserkerApplication:47 - Starting BerserkerApplication on Daisuke-MBP2015.local with PID 31804 (/Users/daisuke/git/cm-github/berserker/build/classes/main started by daisuke in /Users/daisuke/git/cm-github/berserker)
2015/07/21 13:00:55.138 [main] DEBUG j.c.e.berserker.BerserkerApplication:50 - Running with Spring Boot v1.2.1.RELEASE, Spring v4.1.5.RELEASE
2015/07/21 13:00:55.186 [main] INFO  o.s.c.a.AnnotationConfigApplicationContext:510 - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@11e21d0e: startup date [Tue Jul 21 13:00:55 JST 2015]; root of context hierarchy
2015/07/21 13:00:56.563 [main] INFO  o.s.j.e.a.AnnotationMBeanExporter:431 - Registering beans for JMX exposure on startup
2015/07/21 13:00:57.013 [main] INFO  j.c.e.berserker.BerserkerApplication:-1 - User(username=miyamoto)
2015/07/21 13:00:57.014 [main] INFO  j.c.e.berserker.BerserkerApplication:-1 - User(username=watanabe)
2015/07/21 13:00:57.022 [main] INFO  j.c.e.berserker.BerserkerApplication:56 - Started BerserkerApplication in 2.141 seconds (JVM running for 2.585)
2015/07/21 13:00:57.024 [Thread-1] INFO  o.s.c.a.AnnotationConfigApplicationContext:862 - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@11e21d0e: startup date [Tue Jul 21 13:00:55 JST 2015]; root of context hierarchy
2015/07/21 13:00:57.025 [Thread-1] INFO  o.s.j.e.a.AnnotationMBeanExporter:449 - Unregistering JMX-exposed beans on shutdown

BUILD SUCCESSFUL

Total time: 3.952 secs

ちょっとログの量が多いですが、runメソッドにある通り、全ユーザのリストをログ出力できていますね。

Spring BootによるWebアプリ

さて、そろそろWebアプリを作りたくなって来たと思います。2年前からですって? サーセンww

Gradleビルドスクリプトの設定

アプリケーションの依存にorg.springframework.boot:spring-boot-starter-webを追加するだけです。/diffを見てみましょう。

アプリケーション本体の記述

@SpringBootApplication
@EnableMirageRepositories
@RestController
public class BerserkerApplication {

    private static Logger logger = LoggerFactory.getLogger(BerserkerApplication.class);


    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(BerserkerApplication.class);
        app.run(args);
    }


    @Autowired
    UserRepository userRepos;


    @RequestMapping(value = "/", method = RequestMethod.GET)
    @Transactional
    public ResponseEntity<String> index() {
        logger.debug("index");
        Iterable<User> users = userRepos.findAll();
        String result = StreamSupport.stream(users.spliterator(), false)
            .map(Object::toString)
            .collect(Collectors.joining(","));
        return ResponseEntity.ok(result);
    }
}

まず増えたのは、@RestControllerアノテーションです。これは、このクラスがHTTPリクエストに対するRESTfulなコントローラ(リクエストハンドラメソッドを持つクラス)となる、という宣言です。

次に、このアプリケーションはコマンドラインアプリケーションではありませんので、CommandLineRunnerインターフェイスとrunの実装は削除します。その上で@RequestMappingアノテーションを付与した、リクエストハンドラメソッドを実装します。

@RequestMapping(value = "/", method = RequestMethod.GET)というのは、/に対するGETリクエストは、このメソッドでハンドリングする、という意味です。メソッド名は何でも構いません。

ハンドラメソッドの戻り値としては(色々使えるものはあるのですが)ResponseEntityが一般的です。このクラスは「返したいボディーの値」と「ステータスコード」の組み合わせだとイメージして頂ければ。

で、このメソッドの中身では、とりあえずユーザ一覧を取得し、その文字列表現をカンマ区切りでつないだものを作り出し、それを「返したいボディーの値」として(200 OKのステータスで)返しています。

サンプルアプリを実行してみる

さて今回もまた、タグをチェックアウトして実行してみましょう。先ほどと同様、実行はbootRunタスクから行います。

$ git checkout refs/tags/part8-2
$ ./gradlew bootRun
Starting a new Gradle Daemon for this build (subsequent builds will be faster).
:compileJava
:processResources
:classes
:findMainClass
:bootRun

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

2015/07/21 13:21:12.446 [main] INFO  j.c.e.berserker.BerserkerApplication:47 - Starting BerserkerApplication on Daisuke-MBP2015.local with PID 32269 (/Users/daisuke/git/cm-github/berserker/build/classes/main started by daisuke in /Users/daisuke/git/cm-github/berserker)
2015/07/21 13:21:12.449 [main] DEBUG j.c.e.berserker.BerserkerApplication:50 - Running with Spring Boot v1.2.1.RELEASE, Spring v4.1.5.RELEASE
2015/07/21 13:21:12.494 [main] INFO  o.s.b.c.e.AnnotationConfigEmbeddedWebApplicationContext:510 - Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@78a2da20: startup date [Tue Jul 21 13:21:12 JST 2015]; root of context hierarchy
2015/07/21 13:21:13.376 [main] INFO  o.s.b.f.s.DefaultListableBeanFactory:822 - Overriding bean definition for bean 'beanNameViewResolver': 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]]
2015/07/21 13:21:13.700 [main] INFO  o.h.validator.internal.util.Version:27 - HV000001: Hibernate Validator 5.1.3.Final
2015/07/21 13:21:13.824 [main] INFO  o.s.c.s.PostProcessorRegistrationDelegate$BeanPostProcessorChecker:309 - Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [class org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$$EnhancerBySpringCGLIB$$4a3d9265] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015/07/21 13:21:13.850 [main] INFO  o.s.c.s.PostProcessorRegistrationDelegate$BeanPostProcessorChecker:309 - Bean 'transactionAttributeSource' of type [class org.springframework.transaction.annotation.AnnotationTransactionAttributeSource] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015/07/21 13:21:13.864 [main] INFO  o.s.c.s.PostProcessorRegistrationDelegate$BeanPostProcessorChecker:309 - Bean 'transactionInterceptor' of type [class org.springframework.transaction.interceptor.TransactionInterceptor] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015/07/21 13:21:13.871 [main] INFO  o.s.c.s.PostProcessorRegistrationDelegate$BeanPostProcessorChecker:309 - Bean 'org.springframework.transaction.config.internalTransactionAdvisor' of type [class org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015/07/21 13:21:14.215 [main] INFO  o.s.b.c.e.t.TomcatEmbeddedServletContainer:79 - Tomcat initialized with port(s): 8080 (http)
2015/07/21 13:21:14.426 [main] INFO  o.a.catalina.core.StandardService:180 - Starting service Tomcat
2015/07/21 13:21:14.427 [main] INFO  o.a.catalina.core.StandardEngine:180 - Starting Servlet Engine: Apache Tomcat/8.0.15
2015/07/21 13:21:14.567 [host-startStop-1] INFO  o.a.c.c.C.[Tomcat].[localhost].[/]:180 - Initializing Spring embedded WebApplicationContext
2015/07/21 13:21:14.567 [host-startStop-1] INFO  o.s.web.context.ContextLoader:267 - Root WebApplicationContext: initialization completed in 2076 ms
2015/07/21 13:21:15.192 [host-startStop-1] INFO  o.s.b.c.e.ServletRegistrationBean:165 - Mapping servlet: 'dispatcherServlet' to [/]
2015/07/21 13:21:15.196 [host-startStop-1] INFO  o.s.b.c.e.FilterRegistrationBean:278 - Mapping filter: 'characterEncodingFilter' to: [/*]
2015/07/21 13:21:15.197 [host-startStop-1] INFO  o.s.b.c.e.FilterRegistrationBean:278 - Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2015/07/21 13:21:15.769 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerAdapter:518 - Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@78a2da20: startup date [Tue Jul 21 13:21:12 JST 2015]; root of context hierarchy
2015/07/21 13:21:15.833 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping:220 - Mapped "{[/],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public org.springframework.http.ResponseEntity<java.lang.String> jp.classmethod.example.berserker.BerserkerApplication.index(java.security.Principal)
2015/07/21 13:21:15.835 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping:220 - Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" 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)
2015/07/21 13:21:15.838 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping:220 - Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=1,custom=[]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest)
2015/07/21 13:21:15.866 [main] INFO  o.s.w.s.h.SimpleUrlHandlerMapping:314 - Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2015/07/21 13:21:15.867 [main] INFO  o.s.w.s.h.SimpleUrlHandlerMapping:314 - Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2015/07/21 13:21:15.928 [main] INFO  o.s.w.s.h.SimpleUrlHandlerMapping:314 - Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2015/07/21 13:21:16.076 [main] INFO  o.s.j.e.a.AnnotationMBeanExporter:431 - Registering beans for JMX exposure on startup
2015/07/21 13:21:16.089 [main] INFO  o.a.coyote.http11.Http11NioProtocol:180 - Initializing ProtocolHandler ["http-nio-8080"]
2015/07/21 13:21:16.103 [main] INFO  o.a.coyote.http11.Http11NioProtocol:180 - Starting ProtocolHandler ["http-nio-8080"]
2015/07/21 13:21:16.131 [main] INFO  o.a.tomcat.util.net.NioSelectorPool:180 - Using a shared selector for servlet write/read
2015/07/21 13:21:16.147 [main] INFO  o.s.b.c.e.t.TomcatEmbeddedServletContainer:159 - Tomcat started on port(s): 8080 (http)
2015/07/21 13:21:16.150 [main] INFO  j.c.e.berserker.BerserkerApplication:56 - Started BerserkerApplication in 3.972 seconds (JVM running for 4.448)
2015/07/21 13:21:25.989 [-nio-8080-exec-1] INFO  o.a.c.c.C.[Tomcat].[localhost].[/]:180 - Initializing Spring FrameworkServlet 'dispatcherServlet'
2015/07/21 13:21:25.989 [-nio-8080-exec-1] INFO  o.s.web.servlet.DispatcherServlet:484 - FrameworkServlet 'dispatcherServlet': initialization started
2015/07/21 13:21:26.013 [-nio-8080-exec-1] INFO  o.s.web.servlet.DispatcherServlet:503 - FrameworkServlet 'dispatcherServlet': initialization completed in 24 ms

ちょっとログは多いですが、Apache Tomcat/8.0.15 がポート8080で起動しているようですね。curlコマンドでアクセスしてみると、想定通り、全ユーザの情報を出力してくれています。

$ curl -v http://localhost:8080/
* Hostname was NOT found in DNS cache
*   Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.37.1
> Host: localhost:8080
> Accept: */*
>
< HTTP/1.1 200 OK
* Server Apache-Coyote/1.1 is not blacklisted
< Server: Apache-Coyote/1.1
< Accept-Charset: (略)
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 47
< Date: Tue, 21 Jul 2015 04:26:16 GMT
<
* Connection #0 to host localhost left intact
User(username=miyamoto),User(username=watanabe)

まとめ

いかがでしたでしょうか。これだけ簡単な手順で、Springベースのアプリケーションを実装できるとは、一昔前は思いもよらなかったことです。その分相応の黒魔術を使っているわけですが、この楽々さ加減はやめられませんね。