Spring Bootが楽しいので、社内勉強会で「Spring Bootを触ってみた」というタイトルでLTしてきました。
内容が薄くなってしまったので、もっと使い込んだら、もう少し長い時間でやりたいなぁと思っています。
Spring Bootが楽しいので、社内勉強会で「Spring Bootを触ってみた」というタイトルでLTしてきました。
内容が薄くなってしまったので、もっと使い込んだら、もう少し長い時間でやりたいなぁと思っています。
Spring Boot を勉強し始めて、いろいろ楽しいので、いったんここにまとめてみます。
ここで書いている内容は、下記のリポジトリで試している内容になります。(今後もいろいろ試すので、リポジトリの内容はどんどん変わっていくかもしれません…)
またSpring projectでも、たくさんのサンプルが公開されていてとても参考になります。
Spring Bootを使うと、さまざまなコンポーネントを組み合わせて、よい感じのアプリケーション構成に仕上げてくれます。ちょっとうまく言い表せないのですが、、いろいろなコンポーネントを組み合わせて、結果的にフルスタックのフレームワークのような機能を提供してくれている感じです。組み合わせ自体はSpring Bootが解決してくれるので、そこで迷うことはありません。
サンプルとしてRESTなアプリケーションを書いてみましたが、APIを簡単に作れるのは当然で、それに対する試験だったり、DBのマイグレーションなどといった仕組みも簡単に試すことが出来ました。
@RestController
アノテーションを付与したクラスが、RESTのAPIとして公開されます。
Jersey でもそうですが、アノテーションだけで済むので、とっても簡単です。
package com.example.api; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import com.example.domain.Customer; import com.example.service.CustomerService; @RestController @RequestMapping("api/customers") public class CustomerRestController { @Autowired private CustomerService customerService; @GetMapping public List<Customer> getCustomers() { return customerService.findAll(); } @GetMapping(path = "{id}") public Customer getCustomer(@PathVariable Integer id) { return customerService.findOne(id); } @GetMapping(path = "/search") public List<Customer> getCustomersByName(@RequestParam String name) { return customerService.findByName(name); } @PostMapping @ResponseStatus(HttpStatus.CREATED) public Customer craeteCustomer(@RequestBody Customer customer) { return customerService.create(customer); } @PostMapping(path = "{id}") public Customer upateCustomer(@PathVariable Integer id, @RequestBody Customer customer) { customer.setId(id); return customerService.update(customer); } @DeleteMapping(path = "{id}") @ResponseStatus(HttpStatus.NO_CONTENT) public void deleteCustomer(@PathVariable Integer id) { customerService.delete(id); } }
簡単なCRUDならば、JPAを使うのがよいかと思います。Spring DATA JPAは、JPAをとても使いやすくしてくれています。
基本的にはインタフェースを定義するだけです。EntityManager
を触ることはありません。
JpaRepository
をextendsするだけで、CRUDのメソッドを提供してくれます。
package com.example.repository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import com.example.domain.Customer; @Repository public interface CustomerRepository extends JpaRepository<Customer, Integer> { }
他にもメソッド名にあわせて自動的にSQLを作成してくれたり、アノテーションでクエリを指定できたりと便利です。
また、単純なデータアクセスのAPIならば、@RestControler
使わずに、@Repository
を直接APIとして公開することができます。
複雑なクエリになる場合は、JPAだと使いこなせる自信が無いので、MyBatisを使おうと考えています。MyBatisもSpring Bootと連携できます。
DBのマイグレーション方法としてFlywayを使った方法が提供されています。
アプリケーションの起動時に、Flywayが実行されるイメージです。クラスパス上のdb/migration
配下 に配置したSQLが実行されます。
SpringFoxを使うと、SpringのアノテーションからSwagger JSONを生成してくれます。
SpringFoxを依存関係に追加して、下記のようなクラスを定義しておくだけです。
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.service.ApiInfo; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; @Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket document() { return new Docket(DocumentationType.SWAGGER_2) .select() .paths(paths()) .build() .apiInfo(apiInfo()); } @SuppressWarnings("unchecked") private Predicate<String> paths() { return Predicates.or(Predicates.containsPattern("/api/*")); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("Example API") .version("1.0") .build(); } }
さらにSwagger UIと組み合わせて、APIのドキュメントがアプリケーション上で確認できます。
Swagger UIの良いところは、そこから直接APIを叩いて確認ができるところです。いちいちcurl
コマンドで確認するなどといった必要が無くなります。
テスト方法もいろいろ提供されています。
RESTのAPIに対する結合テストは、下記のような感じです。TestRestTemplate
クラスが、RESTのテストを行うのに便利です。
package com.example; import static org.assertj.core.api.Assertions.assertThat; import java.util.Arrays; import java.util.List; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.context.junit4.SpringRunner; import com.example.domain.Customer; import com.example.repository.CustomerRepository; @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "spring.datasource.url=jdbc:log4jdbc:h2:mem:test;DB_CLOSE_ON_EXIT=FALSE" }) public class SpringBootRestJpaApplicationTests { @Autowired private TestRestTemplate restTemplate; @Autowired private CustomerRepository customerRepository; private Customer customer1; private Customer customer2; private Customer customer3; @Before public void setup() { customerRepository.deleteAll(); customer1 = new Customer(); customer1.setName("Taro"); customer1.setAddress("Tokyo"); customer2 = new Customer(); customer2.setName("花子"); customer2.setAddress("千葉"); customer3 = new Customer(); customer3.setName("Taku"); customerRepository.save(Arrays.asList(customer1, customer2, customer3)); } @Test public void getCustomers() { ResponseEntity<List<Customer>> response = restTemplate.exchange( "/api/customers", HttpMethod.GET, null, new ParameterizedTypeReference<List<Customer>>() { }); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody()).containsOnly(customer1, customer2, customer3); } @Test public void getCustomer() { ResponseEntity<Customer> response = restTemplate.getForEntity( "/api/customers/{id}", Customer.class, customer1.getId()); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody()).isEqualTo(customer1); } @Test public void getCustomersByName() { ResponseEntity<List<Customer>> response = restTemplate.exchange( "/api/customers/search?name=a", HttpMethod.GET, null, new ParameterizedTypeReference<List<Customer>>() { }); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody()).containsOnly(customer1, customer3); } @Test public void createCustomer() { Customer newCustomer = new Customer(); newCustomer.setName("Tayler"); newCustomer.setAddress("New York"); ResponseEntity<Customer> response = restTemplate.postForEntity( "/api/customers", newCustomer, Customer.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); Customer result = response.getBody(); assertThat(result.getId()).isEqualTo(customer3.getId() + 1); // 最後に追加したもの+1 assertThat(result.getName()).isEqualTo("Tayler"); assertThat(result.getAddress()).isEqualTo("New York"); } @Test public void upateCustomer() { customer1.setName("New Name"); ResponseEntity<Customer> response = restTemplate.postForEntity( "/api/customers/{id}", customer1, Customer.class, customer1.getId()); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody()).isEqualTo(customer1); } @Test public void deleteCustomer() { { ResponseEntity<Void> response = restTemplate.exchange( "/api/customers/{id}", HttpMethod.DELETE, null, Void.class, customer1.getId()); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); } { ResponseEntity<Customer> response = restTemplate.getForEntity( "/api/customers/{id}", Customer.class, customer1.getId()); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody()).isNull(); } } }
テストのときだけDBの接続先を切り替えたり、空いているポートを探してサーバ立ち上げたりと、テストに必要な機能はいろいろ揃っています。
また、そもそもDIの仕組み自体が、テストがしやすい(オブジェクトをモックに差し替えたりとかが楽)です。
ざっくりとした説明になってしまいましたが、、ちょっとでも楽しさが伝わればと思います。
Spring Bootの勉強には、下記の本がとても役に立ちました。この本を最初に読んだおかげで、イメージがだいぶ掴めました。
はじめてのSpring Boot―スプリング・フレームワークで簡単Javaアプリ開発 (I・O BOOKS)
また、サンプルコードもたくさんWeb上にあるので、今のところ嵌るといったこともなく勉強できています。
いろいろなコンポーネントがあるので、これからがさらに楽しみです。
下記の構成で Spring boot + Swagger のサンプルを作ってみたところ、
下記のような警告が出ました。
Class 'springfox.documentation.swagger.web.ClassOrApiAnnotationResourceGrouping' is marked deprecated
警告がひとつでもあると嫌(出ていることを許容してしまうと、他の重要な警告を見逃しかねない)なので、下記にしたがって、Eclipseの設定(SpringのValidationでBeans Validatorところ)を変えて警告が出ないようにしました。
そもそも出てしまっている理由がいまいち理解できていない(どういった形でこのクラスが参照されているのか)のですが、いったんこれで様子みることにします。
GoogleグループのRedmine Users (japanese)で、チケットの説明欄を非表示にするにはといった質問があったので、View customize pluginで対応してみました。
非表示にするだけならば、CSSで設定できる場合も多いですが、今回対象となる要素を指定するためには、xxを持った親要素といった指定が必要で、現状のCSSだとできない(CSS4でhasがあって、それが入って各ブラウザに実装されれば…)ので、JavaScriptで実施してみました。
チケット画面を対象とします。
/issues
Type:JavaScript
として下記を設定します。
$(function() { $('#issue_description_and_toolbar').parent().hide(); $('div.issue div.description').hide() .next('hr').hide(); });
説明欄を消すことができました。
@onozaty 色々とご相談ですみません。特定のカスタムフィールドを、説明欄の下に移動させるにはどうすればいいですか。JQueryのmanipulationを使えば出来そうな気がするのですが、上手くいってません。よろしくお願いします。
— 松谷 秀久 (@mattani) November 15, 2016
といった質問をいただいたので、View customizeで試してみました。
該当の要素を取得して、それを説明欄の下に移動するイメージです。
なお、カスタムフィールドの表示は、div
がネストして1行に2項目を表示しているような形になっているので、それを再現して配置します。
チケット画面を対象とします。
/issues
Type:JavaScript
として下記を設定します。
cf_2のところは、移動したいカスタムフィールドに変更してください。
$(function() { // 対象のカスタムフィールドの要素を取得 var customField = $('.cf_2.attribute'); // 説明の後に移動 $('.description') .after( $('<div class="splitcontent">') .append( $('<div class="splitcontentleft">').append(customField))); })
リスト2となっているカスタムフィールドが移動しました。
なお、今回は一番末尾の項目を移動したので、表示的に違和感ない形となっていますが、途中の項目を移動すると、そこだけ歯抜けになるので、きれいに見せたい場合には、詰めるような処理も必要になってきます…
Tomcat8をCentOS7にインストールするためのRoleを書きました。
最初はAnsible Galaxyにあがっているものを試してみようと思いましたが、やっていることが理解できない部分も多かったので、勉強もかねて自分で書いてみました。
Tomcat8をインストールして、サービスとして登録するところまでの、シンプルなものになっています。
すぐに確認できるようVagrantfile
を置いています。
cloneしてvagrant up
とすると、bento/centos-7.2
のboxを立ち上げて、Ansible
を実行してTomcatのインストールを確認できます。
Tomcat8をインストールして、manager
を見ようとしたところ、403 Access Denied となったので、画面に表示されていた通りtomcat-user.xml
にmanager-gui
というロールでユーザを追加します。Tomcat6の時は、manager
だったのが、Tomcat7以降で変わったようですね。
<role rolename="manager-gui"/> <user username="tomcat" password="s3cret" roles="manager-gui"/>
これだけだと、ローカル以外からアクセスできませんでした。
よくよく調べたところ、デフォルトだとローカル以外からアクセスできないように制限されているようです。(ちゃんとエラーページにも書いてありました)
By default the Manager is only accessible from a browser running on the same machine as Tomcat. If you wish to modify this restriction, you'll need to edit the Manager's context.xml file.
webapps/manager/META-INF/context.xml
には、下記のようにアクセスを制限するような記載がありました。
<Context antiResourceLocking="false" privileged="true" > <Valve className="org.apache.catalina.valves.RemoteAddrValve" allow="127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1" /> </Context>
allow
のところに正規表現で許可するアドレスを書いてあげると、問題なくアクセスできるようになりました。