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

【求人のご案内】Javaアプリ開発エンジニア募集

よく訓練されたアップル信者、都元です。さて、近年の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つのクラスとして示してしまうことが多いのですが、実践的なアプリケーションにおいては、これらのクラスは明確に分離すべきだと思っています。

サンプルアプリ berserker v8.0 を実行してみる

いつものとおり、タグをチェックアウトして実行してみましょう。Spring Bootアプリケーションの起動はbootRunタスクから行います。今までの execute は使えなくなっていますのでご注意ください。

$ git clone https://github.com/classmethod-sandbox/berserker.git
$ cd berserker
$ git checkout 8.0
$ ./gradlew bootRun
:compileJava
:processResources UP-TO-DATE
:classes
:findMainClass
:bootRun

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

2016/04/09 11:00:04.692 [main] INFO  j.c.e.berserker.BerserkerApplication:48 - Starting BerserkerApplication on Daisuke-MBP2015.local with PID 5054 (/Users/daisuke/git/cm-github/berserker/build/classes/main started by daisuke in /Users/daisuke/git/cm-github/berserker)
2016/04/09 11:00:04.695 [main] DEBUG j.c.e.berserker.BerserkerApplication:51 - Running with Spring Boot v1.3.3.RELEASE, Spring v4.2.5.RELEASE
2016/04/09 11:00:04.695 [main] INFO  j.c.e.berserker.BerserkerApplication:666 - No active profile set, falling back to default profiles: default
2016/04/09 11:00:04.749 [main] INFO  o.s.c.a.AnnotationConfigApplicationContext:578 - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@12405818: startup date [Sat Apr 09 11:00:04 JST 2016]; root of context hierarchy
2016/04/09 11:00:06.022 [main] INFO  o.s.j.d.DriverManagerDataSource:133 - Loaded JDBC driver: com.mysql.jdbc.Driver
2016/04/09 11:00:06.620 [main] INFO  o.s.j.e.a.AnnotationMBeanExporter:431 - Registering beans for JMX exposure on startup
2016/04/09 11:00:07.005 [main] INFO  j.c.e.berserker.BerserkerApplication:56 - Create user
2016/04/09 11:00:07.012 [main] INFO  j.c.e.berserker.BerserkerApplication:64 - List user
2016/04/09 11:00:07.017 [main] INFO  j.c.e.berserker.BerserkerApplication:67 -   User(username=miyamoto, password=$2a$10$vxq4n5VB4bsgUlBK9DXV9edhX911Qz/5iYqjLi/6qZPp8Xl7ZACKC)
2016/04/09 11:00:07.021 [main] INFO  j.c.e.berserker.BerserkerApplication:67 -   User(username=watanabe, password=$2a$10$MHPqWJ61alnBlUbvjEGK/uWRvwtYzolWCuFXW8YMJkT54HUB0H9iq)
2016/04/09 11:00:07.021 [main] INFO  j.c.e.berserker.BerserkerApplication:67 -   User(username=yokota, password=$2a$10$nkvNPCb3Y1z/GSearD7s7OBdS9BoLBss3D4enbFQIgNJDvr0Xincm)
2016/04/09 11:00:07.021 [main] INFO  j.c.e.berserker.BerserkerApplication:70 - List user filtered by length
2016/04/09 11:00:07.031 [main] INFO  j.c.e.berserker.BerserkerApplication:75 -   User(username=yokota, password=$2a$10$nkvNPCb3Y1z/GSearD7s7OBdS9BoLBss3D4enbFQIgNJDvr0Xincm)
2016/04/09 11:00:07.031 [main] INFO  j.c.e.berserker.BerserkerApplication:78 - Get user
2016/04/09 11:00:07.035 [main] INFO  j.c.e.berserker.BerserkerApplication:80 -   User(username=miyamoto, password=$2a$10$vxq4n5VB4bsgUlBK9DXV9edhX911Qz/5iYqjLi/6qZPp8Xl7ZACKC)
2016/04/09 11:00:07.035 [main] INFO  j.c.e.berserker.BerserkerApplication:83 - Update user
2016/04/09 11:00:07.040 [main] INFO  j.c.e.berserker.BerserkerApplication:88 - Delete user
2016/04/09 11:00:07.071 [main] INFO  j.c.e.berserker.BerserkerApplication:57 - Started BerserkerApplication in 2.769 seconds (JVM running for 3.577)
2016/04/09 11:00:07.074 [Thread-1] INFO  o.s.c.a.AnnotationConfigApplicationContext:960 - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@12405818: startup date [Sat Apr 09 11:00:04 JST 2016]; root of context hierarchy
2016/04/09 11:00:07.077 [Thread-1] INFO  o.s.j.e.a.AnnotationMBeanExporter:449 - Unregistering JMX-exposed beans on shutdown

BUILD SUCCESSFUL

Total time: 8.536 secs

ちょっとログの量が多いですが、runメソッドにある通りの出力ができていますね。

Spring BootによるWebアプリ

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

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

アプリケーションの依存にorg.springframework.boot:spring-boot-starter-webを追加するだけです。

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

@SpringBootApplication
@RestController
public class BerserkerApplication implements CommandLineRunner {

  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 8.1
$ ./gradlew bootRun
:compileJava
:processResources UP-TO-DATE
:classes
:findMainClass
:bootRun

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

2016/04/09 11:03:41.846 [main] INFO  j.c.e.berserker.BerserkerApplication:48 - Starting BerserkerApplication on Daisuke-MBP2015.local with PID 5098 (/Users/daisuke/git/cm-github/berserker/build/classes/main started by daisuke in /Users/daisuke/git/cm-github/berserker)
2016/04/09 11:03:41.850 [main] DEBUG j.c.e.berserker.BerserkerApplication:51 - Running with Spring Boot v1.3.3.RELEASE, Spring v4.2.5.RELEASE
2016/04/09 11:03:41.850 [main] INFO  j.c.e.berserker.BerserkerApplication:666 - No active profile set, falling back to default profiles: default
2016/04/09 11:03:41.918 [main] INFO  o.s.b.c.e.AnnotationConfigEmbeddedWebApplicationContext:578 - Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@7bc1a03d: startup date [Sat Apr 09 11:03:41 JST 2016]; root of context hierarchy
2016/04/09 11:03:42.548 [ckground-preinit] INFO  o.h.validator.internal.util.Version:30 - HV000001: Hibernate Validator 5.2.4.Final
2016/04/09 11:03:43.057 [main] INFO  o.s.b.f.s.DefaultListableBeanFactory:839 - 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/04/09 11:03:43.486 [main] INFO  o.s.c.s.PostProcessorRegistrationDelegate$BeanPostProcessorChecker:328 - Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [class org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$$EnhancerBySpringCGLIB$$41c5f45] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2016/04/09 11:03:44.163 [main] INFO  o.s.b.c.e.t.TomcatEmbeddedServletContainer:81 - Tomcat initialized with port(s): 8080 (http)
2016/04/09 11:03:44.184 [main] INFO  o.a.catalina.core.StandardService:180 - Starting service Tomcat
2016/04/09 11:03:44.190 [main] INFO  o.a.catalina.core.StandardEngine:180 - Starting Servlet Engine: Apache Tomcat/8.0.32
2016/04/09 11:03:44.459 [host-startStop-1] INFO  o.a.c.c.C.[Tomcat].[localhost].[/]:180 - Initializing Spring embedded WebApplicationContext
2016/04/09 11:03:44.460 [host-startStop-1] INFO  o.s.web.context.ContextLoader:272 - Root WebApplicationContext: initialization completed in 2546 ms
2016/04/09 11:03:44.777 [host-startStop-1] INFO  o.s.b.c.e.ServletRegistrationBean:189 - Mapping servlet: 'dispatcherServlet' to [/]
2016/04/09 11:03:44.784 [host-startStop-1] INFO  o.s.b.c.e.FilterRegistrationBean:258 - Mapping filter: 'characterEncodingFilter' to: [/*]
2016/04/09 11:03:44.785 [host-startStop-1] INFO  o.s.b.c.e.FilterRegistrationBean:258 - Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2016/04/09 11:03:44.785 [host-startStop-1] INFO  o.s.b.c.e.FilterRegistrationBean:258 - Mapping filter: 'httpPutFormContentFilter' to: [/*]
2016/04/09 11:03:44.786 [host-startStop-1] INFO  o.s.b.c.e.FilterRegistrationBean:258 - Mapping filter: 'requestContextFilter' to: [/*]
2016/04/09 11:03:44.912 [main] INFO  o.s.j.d.DriverManagerDataSource:133 - Loaded JDBC driver: com.mysql.jdbc.Driver
2016/04/09 11:03:45.579 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerAdapter:532 - Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@7bc1a03d: startup date [Sat Apr 09 11:03:41 JST 2016]; root of context hierarchy
2016/04/09 11:03:45.695 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping:534 - Mapped "{[/],methods=[GET]}" onto public org.springframework.http.ResponseEntity<java.lang.String> jp.classmethod.example.berserker.BerserkerApplication.index()
2016/04/09 11:03:45.699 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping:534 - 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/04/09 11:03:45.700 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping:534 - Mapped "{[/error],produces=}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2016/04/09 11:03:45.731 [main] INFO  o.s.w.s.h.SimpleUrlHandlerMapping:341 - Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2016/04/09 11:03:45.731 [main] INFO  o.s.w.s.h.SimpleUrlHandlerMapping:341 - Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2016/04/09 11:03:45.794 [main] INFO  o.s.w.s.h.SimpleUrlHandlerMapping:341 - Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2016/04/09 11:03:45.990 [main] INFO  o.s.j.e.a.AnnotationMBeanExporter:431 - Registering beans for JMX exposure on startup
2016/04/09 11:03:46.043 [main] INFO  o.a.coyote.http11.Http11NioProtocol:180 - Initializing ProtocolHandler ["http-nio-8080"]
2016/04/09 11:03:46.060 [main] INFO  o.a.coyote.http11.Http11NioProtocol:180 - Starting ProtocolHandler ["http-nio-8080"]
2016/04/09 11:03:46.087 [main] INFO  o.a.tomcat.util.net.NioSelectorPool:180 - Using a shared selector for servlet write/read
2016/04/09 11:03:46.121 [main] INFO  o.s.b.c.e.t.TomcatEmbeddedServletContainer:162 - Tomcat started on port(s): 8080 (http)
2016/04/09 11:03:46.575 [main] INFO  j.c.e.berserker.BerserkerApplication:73 - Create user
2016/04/09 11:03:46.582 [main] INFO  j.c.e.berserker.BerserkerApplication:81 - List user
2016/04/09 11:03:46.585 [main] INFO  j.c.e.berserker.BerserkerApplication:84 -   User(username=miyamoto, password=$2a$10$vxq4n5VB4bsgUlBK9DXV9edhX911Qz/5iYqjLi/6qZPp8Xl7ZACKC)
2016/04/09 11:03:46.585 [main] INFO  j.c.e.berserker.BerserkerApplication:84 -   User(username=watanabe, password=$2a$10$MHPqWJ61alnBlUbvjEGK/uWRvwtYzolWCuFXW8YMJkT54HUB0H9iq)
2016/04/09 11:03:46.586 [main] INFO  j.c.e.berserker.BerserkerApplication:84 -   User(username=yokota, password=$2a$10$nkvNPCb3Y1z/GSearD7s7OBdS9BoLBss3D4enbFQIgNJDvr0Xincm)
2016/04/09 11:03:46.586 [main] INFO  j.c.e.berserker.BerserkerApplication:87 - List user filtered by length
2016/04/09 11:03:46.594 [main] INFO  j.c.e.berserker.BerserkerApplication:92 -   User(username=yokota, password=$2a$10$nkvNPCb3Y1z/GSearD7s7OBdS9BoLBss3D4enbFQIgNJDvr0Xincm)
2016/04/09 11:03:46.594 [main] INFO  j.c.e.berserker.BerserkerApplication:95 - Get user
2016/04/09 11:03:46.597 [main] INFO  j.c.e.berserker.BerserkerApplication:97 -   User(username=miyamoto, password=$2a$10$vxq4n5VB4bsgUlBK9DXV9edhX911Qz/5iYqjLi/6qZPp8Xl7ZACKC)
2016/04/09 11:03:46.597 [main] INFO  j.c.e.berserker.BerserkerApplication:100 - Update user
2016/04/09 11:03:46.603 [main] INFO  j.c.e.berserker.BerserkerApplication:105 - Delete user
2016/04/09 11:03:46.611 [main] INFO  j.c.e.berserker.BerserkerApplication:57 - Started BerserkerApplication in 5.082 seconds (JVM running for 5.886)
> Building 80% > :bootRun

> Building 80% > :bootRun という状態で止まると思います。(この状態から終了させたいときは、CTRL+Cです。)

ちょっとログは多いですが、Apache Tomcat8系 がポート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, password=$2a$10$vxq4n5VB4bsgUlBK9DXV9edhX911Qz/5iYqjLi/6qZPp8Xl7ZACKC),User(username=watanabe, password=$2a$10$MHPqWJ61alnBlUbvjEGK/uWRvwtYzolWCuFXW8YMJkT54HUB0H9iq)

まとめ

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