第8回 Spring Bootを使ってみよう
よく訓練されたアップル信者、都元です。さて、近年の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のビルド設定を少々変更する必要があります。
spring-boot-gradle-plugin
を利用するため、ビルドスクリプトの依存ライブラリとして記述を追加する。- spring-bootプラグインの適用を指示する。
- アプリケーションの依存ライブラリとして、
spring-boot-starter
を追加する。 - これにより
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によるコマンドラインアプリケーションの出来上がりです。
コンパクトに例を示すために、上の例も含めて、これらを混ぜて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ベースのアプリケーションを実装できるとは、一昔前は思いもよらなかったことです。その分相応の黒魔術を使っているわけですが、この楽々さ加減はやめられませんね。