Spring Boot を勉強し始めて、いろいろ楽しいので、いったんここにまとめてみます。
ここで書いている内容は、下記のリポジトリで試している内容になります。(今後もいろいろ試すので、リポジトリの内容はどんどん変わっていくかもしれません…)
またSpring projectでも、たくさんのサンプルが公開されていてとても参考になります。
Spring Bootを使うと、さまざまなコンポーネントを組み合わせて、よい感じのアプリケーション構成に仕上げてくれます。ちょっとうまく言い表せないのですが、、いろいろなコンポーネントを組み合わせて、結果的にフルスタックのフレームワークのような機能を提供してくれている感じです。組み合わせ自体はSpring Bootが解決してくれるので、そこで迷うことはありません。
サンプルとしてRESTなアプリケーションを書いてみましたが、APIを簡単に作れるのは当然で、それに対する試験だったり、DBのマイグレーションなどといった仕組みも簡単に試すことが出来ました。
APIの作成
@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のマイグレーション
DBのマイグレーション方法としてFlywayを使った方法が提供されています。
アプリケーションの起動時に、Flywayが実行されるイメージです。クラスパス上のdb/migration
配下 に配置したSQLが実行されます。
APIのドキュメント生成
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);
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の勉強には、下記の本がとても役に立ちました。この本を最初に読んだおかげで、イメージがだいぶ掴めました。
また、サンプルコードもたくさんWeb上にあるので、今のところ嵌るといったこともなく勉強できています。
いろいろなコンポーネントがあるので、これからがさらに楽しみです。