Spring BootとDropwizardのアプリコード比較


DropwizardのSpring版とも言えるSpring BootとDropwizardをコードベースで比較してみます。
わかり易く比較したいので、パッケージ構成・命名などはDropwizardに合わせています。
また、Dropwizardと同じように書きたいので、Spring Bootで必須ではないjarも依存させています。

ソース中のコメントにDropwizardの場合を書いときます。

# maven設定

コードの比較の記事にしたいので、Dropwizardのpom.xmlについては、ここでは書きません。
気になる方は以下の記事も参照ください。

* [いますぐ採用すべきJavaフレームワークDropWizard(その１)](http://qiita.com/ko2ic/items/cfe5b2f4593b705f9463)
* [いますぐ採用すべきJavaフレームワークDropWizard(その２)](http://qiita.com/ko2ic/items/069c5f75823fd8964bfa)
* [いますぐ採用すべきJavaフレームワークDropWizard(その３)](http://qiita.com/ko2ic/items/8110f5959315d3255ae9)

* [dropwizard-testingが便利でした](http://qiita.com/ko2ic/items/b59447a75952703e809a)


以下はSpring Bootの場合の一部です

```pom.xml
  <!-- Inherit defaults from Spring Boot -->
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.1.2.RELEASE</version>
  </parent>

  <dependencies>
<!-- tomcatからjettyに変更したい場合の記述。liquibaseを依存させたらtomcatで動作しなかったため -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
      <version>${boot.version}</version>
      <exclusions>
        <exclusion>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-jetty</artifactId>
      <version>${boot.version}</version>
    </dependency>

<!-- メトリックスを利用したい場合だけ -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-actuator</artifactId>
      <version>${boot.version}</version>
    </dependency>

<!-- DBを利用したい場合だけ -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
      <version>${boot.version}</version>
    </dependency>

<!-- freemarkerを利用したい場合だけ。これを依存させるとclasspath:/templates/ディレクトリがないとエラーになりました -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-freemarker</artifactId>
      <version>${boot.version}</version>
    </dependency>

<!-- 必須でない -->
    <dependency>
      <groupId>com.google.guava</groupId>
      <artifactId>guava</artifactId>
      <version>17.0</version>
    </dependency>

<!-- liquibaseを利用したい場合だけ -->
    <dependency>
      <groupId>org.liquibase</groupId>
      <artifactId>liquibase-core</artifactId>
      <version>3.2.0</version>
    </dependency>

    <dependency>
      <groupId>com.h2database</groupId>
      <artifactId>h2</artifactId>
      <version>1.3.175</version>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <version>${boot.version}</version>
      <scope>test</scope>
    </dependency>

  </dependencies>

  <build>
    <resources>
      <resource>
       <directory>src/main/resources</directory>
        <includes>
          <include>**/*</include>
        </includes>
      </resource>
    </resources>  
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
・・・
    </plugins>
  </build>

```

# Hello-World（基本）の比較

まずは　RestなHello-Worldで比較します。
ソース中のコメント部分がDropwizardになります。

## Applicationクラス

Dropwizardは、このクラスが色々な機能を追加する役割を持つので、記述量が多くなります。
Spring Bootでは、```SpringApplication```クラスやDIでこのあたりをしています。

```HelloWorldApplication.java
@Configuration
@EnableAutoConfiguration
@ComponentScan
@EnableConfigurationProperties
public class HelloWorldApplication {
//public class HelloWorldApplication extends Application<HelloWorldConfiguration> {

	public static void main(String[] args) throws Exception {
		SpringApplication.run(HelloWorldApplication.class, args);
		//new HelloWorldApplication().run(args);
	}

//	@Override
//	public String getName() {
//		return "hello-world";
//	}
//
//	@Override
//	public void initialize(Bootstrap<HelloWorldConfiguration> bootstrap) {
//	}
//
//	@Override
//	public void run(HelloWorldConfiguration configuration,
//			Environment environment) throws ClassNotFoundException {
//		final Template template = configuration.buildTemplate();
//		environment.jersey().register(new HelloWorldResource(template));
	}
```

## Configurationクラス

```prefix = "hello-world"```は、example.ymlに記述しているキーです。
**Spring Bootではこのprefixをymlに追加しないと起動しませんでした。** 

ちなみに起動のときにexample.ymlを指定しています。

```
$ java -jar target/spike-spring-boot-1.0-SNAPSHOT.jar --spring.config.location=example.yml

$ java -jar target/spike-dropwizard-1.0-SNAPSHOT.jar server example.yml
```

```HelloWorldConfiguration.java
@ConfigurationProperties(prefix = "hello-world", ignoreUnknownFields = false)
@Component
public class HelloWorldConfiguration {
//public class HelloWorldConfiguration extends Configuration {
	@NotEmpty
	private String template;

	@NotEmpty
	private String defaultName = "Stranger";

//	@JsonProperty
	public String getTemplate() {
		return template;
	}

//	@JsonProperty
	public void setTemplate(String template) {
		this.template = template;
	}

//	@JsonProperty
	public String getDefaultName() {
		return defaultName;
	}

//	@JsonProperty
	public void setDefaultName(String defaultName) {
		this.defaultName = defaultName;
	}

//	public Template buildTemplate() {
		return new Template(template, defaultName);
	}
}
```

```example.yml
hello-world:
    template: Hello, %s!
    defaultName: Stranger
#template: Hello, %s!
#defaultName: Stranger
```

## Resourceクラス

POSTでのバリデータエラー(この場合```content```に"hoge"のように4文字以上)の場合は以下の違いがあります。

HTTPステータスコードは

* ```422```　<- Dropwizard
* ```400```　<- Spring Boot

bodyは

* ```{"errors":["content length must be between 0 and 3 (was hoge)"]}```　<- Dropwizard
* ```{"timestamp":1403789235278,"status":400,"error":"Bad Request","exception":"org.springframework.web.bind.MethodArgumentNotValidException","message":"Validation failed for argument at index 0 in method: public void com.github.ko2ic.resources.HelloWorldResource.receiveHello(com.github.ko2ic.core.Saying), with 1 error(s): [Field error in object 'saying' on field 'content': rejected value [hoge]; codes [Length.saying.content,Length.content,Length.java.lang.String,Length]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [saying.content,content]; arguments []; default message [content],3,0]; default message [length must be between 0 and 3]] ","path":"/hello-world"}```　<- Spring Boot


```HelloWorldResource.java
//@Path("/hello-world")
//@Produces(MediaType.APPLICATION_JSON)
@RestController
@RequestMapping(value = "/hello-world")
public class HelloWorldResource {
	private static final Logger LOGGER = LoggerFactory
			.getLogger(HelloWorldResource.class);

//	private final Template template;
//	private final AtomicLong counter;
//
//	public HelloWorldResource(Template template) {
//		this.template = template;
//		this.counter = new AtomicLong();
//	}

	@Autowired
	private HelloWorldConfiguration configuration;
	private final AtomicLong counter = new AtomicLong();

//	@GET
//	@Timed(name = "get-requests")
//	@CacheControl(maxAge = 1, maxAgeUnit = TimeUnit.DAYS)
//	public Saying sayHello(@QueryParam("name") Optional<String> name) {
//		return new Saying(counter.incrementAndGet(), template.render(name));
//	}
	@RequestMapping(method = RequestMethod.GET)
	public Saying sayHello(
			@RequestParam(value = "name", required = false) String name,
			HttpServletResponse response) {
		response.setHeader("Cache-Control", "max-age=86400");
		Template template = configuration.buildTemplate();
		return new Saying(counter.incrementAndGet(), template.render(Optional
				.fromNullable(name)));
	}

//	@POST
	@RequestMapping(method = RequestMethod.POST)
//	public void receiveHello(@Valid Saying saying) {
	public void receiveHello(@RequestBody @Valid Saying saying) {
		LOGGER.info("Received a saying: {}", saying);
	}
}
```

モデルクラス

Spring Bootでは```@JsonProperty```は必要ありません。

```Saying.java
public class Saying {
	private long id;

	@Length(max = 3)
	private String content;

	public Saying() {
	}

	public Saying(long id, String content) {
		this.id = id;
		this.content = content;
	}

//	@JsonProperty
	public long getId() {
		return id;
	}

//	@JsonProperty
	public String getContent() {
		return content;
	}

	@Override
	public String toString() {
		return String.format("id=%d content=%s", getId(), getContent());
	}
```

```Template.java
public class Template {
    private final String content;
    private final String defaultName;

    public Template(String content, String defaultName) {
        this.content = content;
        this.defaultName = defaultName;
    }
    
    public String render(Optional<String> name) {
        return format(content, name.or(defaultName));
    }
}
```

# DBアクセスの比較(Hibernate)

## Applicationクラス

Dropwizardの場合は以下のような設定をする必要がありますが、Spring Bootは特に何も追記する必要はありません。

```HelloWorldApplication.java
//	// hibernateを使うため
//	private final HibernateBundle<HelloWorldConfiguration> hibernateBundle = new HibernateBundle<HelloWorldConfiguration>(
//			Person.class) {
//		@Override
//		public DataSourceFactory getDataSourceFactory(
//				HelloWorldConfiguration configuration) {
//			return configuration.getDataSourceFactory();
//		}
//	};
//
//	@Override
//	public void initialize(Bootstrap<HelloWorldConfiguration> bootstrap) {
//		// liquibaseを使うため
//		bootstrap.addBundle(new MigrationsBundle<HelloWorldConfiguration>() {
//			@Override
//			public DataSourceFactory getDataSourceFactory(
//					HelloWorldConfiguration configuration) {
//				return configuration.getDataSourceFactory();
//			}
//		});
//		bootstrap.addBundle(hibernateBundle);
//	}
//	@Override
//	public void run(HelloWorldConfiguration configuration,
//			Environment environment) throws ClassNotFoundException {
//		final PersonRepository repository = new PersonRepository(
//				hibernateBundle.getSessionFactory());
//		environment.jersey().register(new PeopleResource(repository));

```

## Configurationクラス

Dropwizardの場合は```DataSourceFactory```を使います。
Spring Bootの場合は```DataSourceAutoConfiguration & DataSourceProperties```に自動で設定されるので、設定ファイルに書くだけで設定されます。
（```Applicationクラスでの@EnableAutoConfiguration```で```spring-boot-autoconfigure.jar/META-INF/spring.factories```に記述されたクラスが自動的にロードされるようです。）

```HelloWorldConfiguration.java
	// @Valid
	// @NotNull
	// private DataSourceFactory database = new DataSourceFactory();
	//
	// @JsonProperty("database")
	// public DataSourceFactory getDataSourceFactory() {
	// return database;
	// }
	//
	// @JsonProperty("database")
	// public void setDataSourceFactory(DataSourceFactory dataSourceFactory) {
	// this.database = dataSourceFactory;
	// }
```

Dropwizardの場合は、liquibaseは特にliqibase用に記述しません

```src/main/resources/config/application.yml
spring:
    application:
        name: Hello World!
    datasource:
        driverClassName: org.h2.Driver
        url: jdbc:h2:target/example
        user: sa
        password: sa
    jpa:
        hibernate:
            ddl-auto: false
        database: H2
        show-sql: true
    freemarker:
        cache: false
# デフォルトは、classpath:/templates/
        templateLoaderPath: classpath:/views/  
liquibase:
    change-log: classpath:/migrations.xml
    drop-first: true
    enabled: false  

# dropwizardの場合は、hello-worldで利用したexample.ymlに追記

#database:
#  driverClass: org.h2.Driver
#  user: sa
#  password: sa
#  url: jdbc:h2:target/example
#  properties:
#    charSet: UTF-8
#    hibernate.dialect: org.hibernate.dialect.H2Dialect
#  maxWaitForConnection: 1s
#  validationQuery: "/* MyApplication Health Check */ SELECT 1"
#  minSize: 8
#  maxSize: 32
#  checkConnectionWhileIdle: false

```

## Resourceクラス

意図的にHTTPステータスコード```404```などを返したい場合に特に以下の違いがあります。

* Dropwizardでは```NotFoundException```を投げるだけ
* Spring Bootでは自作例外を作成して、そのクラスを```@ExceptionHandler```でハンドリングする

```PeopleResource.java
//@Path("/people")
//@Produces(MediaType.APPLICATION_JSON)
@RestController
@RequestMapping(value = "/people")
public class PeopleResource {

//	private final PersonRepository repository;
//	public PeopleResource(PersonRepository repository) {
//		this.repository = repository;
//	}

	@Autowired
	private PeopleRepository peopleRepository;

//	@POST
//	@UnitOfWork
	@RequestMapping(method = RequestMethod.POST)
	@Transactional
	//public Person createPerson(Person person) {
	public @ResponseBody
	Person createPerson(@RequestBody Person people) {
		return repository.create(person);
	}

//	@GET
//	@UnitOfWork
	@RequestMapping(method = RequestMethod.GET)
	@Transactional
	public List<Person> listPeople() {
		return repository.findAll();
	}

//	@GET
//	@UnitOfWork
//	@Path("/{personId}")
	@RequestMapping(value = "/{personId}", method = RequestMethod.GET)
	@Transactional
//	public Person getPerson(@PathParam("personId") LongParam personId) {
	public Person getPerson(@PathVariable("personId") Long personId) {
		final Optional<Person> person = repository.findById(personId.get());
		if (!person.isPresent()) {
			//throw new NotFoundException("Not Found Person");
			throw new PersonNotFoundException("Not Found Person");
		}
		return person.get();
	}

	@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "Not Found Person")
	@ExceptionHandler(PersonNotFoundException.class)
	public void notfound() {
		// Nothing to do
	}

	private static class PersonNotFoundException extends RuntimeException {
		public PersonNotFoundException(String message) {
			super(message);
		}
	}
}
```

# Entityクラス

DropwizardもSpring Bootも同じ。
Spring Bootではカラム名を```fullName```のようにキャメルケースにすると動作しませんでした。(Dropwizardは動作した)

```Person.java
@Entity
@Table(name = "people")
@NamedQueries({ @NamedQuery(name = "com.github.ko2ic.core.People.findAll", query = "SELECT p FROM Person p") })
public class Person {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private long id;

	@Column(name = "full_name", nullable = false)
	private String fullName;

	@Column(name = "job_title", nullable = false)
	private String jobTitle;

	public Person() {
	}

	public Person(long id, String fullName, String jobTitle) {
		this.id = id;
		this.fullName = fullName;
		this.jobTitle = jobTitle;
	}
// あとはgetter,setterなので省略
}
```

# Repositoryクラス

Dropwizardでは```AbstractDAO```を利用する。
Spring Bootでは```EntityManager```を扱う。これは[DropwizardでGuiceを使ったとき](http://qiita.com/ko2ic/items/409bfecfd806c2f903e1)も似たようなクラスになりました。

```
//public class PersonRepository extends AbstractDAO<Person> {
//	public PersonRepository(SessionFactory factory) {
//		super(factory);
//	}
//
//	public Optional<Person> findById(Long id) {
//		return Optional.fromNullable(get(id));
//	}
//
//	public Person create(Person person) {
//		return persist(person);
//	}
//
//	public List<Person> findAll() {
//		return list(namedQuery("com.github.ko2ic.core.Person.findAll"));
//	}
//}

@Repository
public class PeopleRepository {

	@PersistenceContext
	private EntityManager entityManager;

	public Optional<Person> findById(Long id) {
		return Optional.fromNullable(entityManager.find(Person.class, id));
	}

	public Person create(Person person) {
		entityManager.persist(person);
		return person;
	}

	@SuppressWarnings("unchecked")
	public List<Person> findAll() {
		return entityManager.createNamedQuery(
				"com.github.ko2ic.core.People.findAll").getResultList();
	}
}
```

## Liquibaseを利用

DropwizardもSpring Bootもchangelogファイルは同じです。

```src/main/resource/migrations.xml
<?xml version="1.0" encoding="UTF-8"?>

<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
         http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">

	<changeSet id="1" author="ko2ic">
		<createTable tableName="people">
			<column name="id" type="bigint" autoIncrement="true">
				<constraints primaryKey="true" nullable="false" />
			</column>
			<column name="full_name" type="varchar(255)">
				<constraints nullable="false" />
			</column>
			<column name="job_title" type="varchar(255)" />
		</createTable>
	</changeSet>	
</databaseChangeLog>
```

Spring Bootでは設定で```enabled: true```にすると起動するたびにテーブルがDropされて、migrateされます。
なので、 **本番環境では注意** が必要です。
Liquibaseの他の機能をSpring Boot経由で利用できるかどうかは不明でした。
直接、Liquibaseを利用する必要があるかもしれません。

``````src/main/resources/config/application.yml
liquibase:
    change-log: classpath:/migrations.xml
    drop-first: true
    enabled: true
```

Dropwizardは以下の記事のようにDropwizard経由で様々なことができました。

* [Dropwizard(Java)でrailsのようにDBマイグレーションをする](http://qiita.com/ko2ic/items/02da56fd64ec39562bec)

# テンプレートによる動的ファイル表示の比較(freemarker)

今回はfreemarkerですが、Spring Bootの場合は、velocity, thymeleafでもほとんど変わりません。

## Applicationクラス

Dropwizardの場合は以下のような設定をする必要がありますが、Spring Bootは特に何も追記する必要はありません。

```HelloWorldApplication.java
//	@Override
//	public void initialize(Bootstrap<HelloWorldConfiguration> bootstrap) {
//		・・・
//		bootstrap.addBundle(new ViewBundle());
//	}
//	@Override
//	public void run(HelloWorldConfiguration configuration,
//			Environment environment) throws ClassNotFoundException {
//		・・・
//		environment.jersey().register(new ViewResource());
//	}
```

## Resourceクラス

Dropwizardの場合はViewを継承したクラスにViewで表示したい値を設定して返します。
Spring Bootの場合は、引数のMapクラスにViewで表示したい値を設定し、戻り値はテンプレートファイルの場所を指定します。

```ViewResource.java
//@Path("/views")
@Controller
public class ViewResource {

	@Value("${spring.freemarker.templateEncoding:UTF-8}")
	private String charset;

//	@GET
//	@Produces("text/html;charset=UTF-8")
//	@Path("/freemarker")
//	public View freemarkerUTF8() {
//		return new View("/views/ftl/utf8.ftl", Charsets.UTF_8) {
//		};
//	}

	@RequestMapping(value = "/views/freemarker")
	public String freemarkerUTF8(Map<String, Object> model) {
		model.put("charset", charset);
		return "ftl/utf8";
	}
}
```

両方とも同じテンプレートファイルです。

```src/main/resouces/views/ftl/utf8.ftl
<html>
<body>
<h1>This is an example of a freemarker</h1>
文字コード：${charset}
</body>
</html>
```

# 静的ファイル表示の比較

## Applicationクラス

Dropwizardの場合は、```AssetsBundle```を登録するだけです。
Spring Bootの場合は、```SpringBootServletInitializer```を継承させて、```configure```メソッドをオーバーライドさせます。

```HelloWorldApplication.java
//public class HelloWorldApplication {
public class HelloWorldApplication extends SpringBootServletInitializer {

//	public void initialize(Bootstrap<HelloWorldConfiguration> bootstrap) {
//		bootstrap.addBundle(new AssetsBundle());
//		・・・
//	}

	@Override
	protected SpringApplicationBuilder configure(
			SpringApplicationBuilder application) {
		return application.sources(HelloWorldApplication.class);
	}
```

ここでの静的ファイルは以下です。

```src/main/resources/static/assets/js/example.js
alert('sample');
```

http://localhost:8080/assets/js/example.js
で表示できることを想定しています。

Dropwizardの場合は、 ```src/main/resources/assets/js/example.js```
Spring Bootの場合は、```src/main/resources/static/assets/js/example.js```
に置きます。

# カスタムヘルスチェックの比較

## Applicationクラス

Dropwizardの場合は以下のような設定をする必要がありますが、Spring Bootは特に何も追記する必要はありません。

```HelloWorldApplication.java
//	@Override
//	public void run(HelloWorldConfiguration configuration,
//			Environment environment) throws ClassNotFoundException {
//		・・・
//		environment.healthChecks().register("template",
//				new TemplateHealthCheck(template));
```

## HealthCheckクラス

DropwizardとSpring Bootでインターフェイスは当然違いますが、似ています。

```TemplateHealthCheck.java_
//public class TemplateHealthCheck extends HealthCheck {
//	private final Template template;
//
//	public TemplateHealthCheck(Template template) {
//		this.template = template;
//	}
//
//	@Override
//	protected Result check() throws Exception {
//		return Result.unhealthy(template.render(Optional.of("error")));
//	}
//}
@Component
public class TemplateHealthCheck implements HealthIndicator {

	@Autowired
	private HelloWorldConfiguration configuration;

	@Override
	public Health health() {
		String data = configuration.buildTemplate()
				.render(Optional.of("error"));
		return Health.down().withDetail("message", data).build();
	}
}
```

```
Drpwizardの場合
$ curl http://localhost:8081/healthcheck
{"deadlocks":{"healthy":true},"hibernate":{"healthy":true},"template":{"healthy":false,"message":"Hello, error!"}}

Spring Bootの場合
$ curl http://localhost:8081/health
{"status":"DOWN","templateHealthCheck":{"status":"DOWN","message":"Hello, error!"},"db":{"status":"UP","database":"H2","hello":1}}
```