Java
spring
spring-boot
dropwizard

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

More than 3 years have passed since last update.

DropwizardのSpring版とも言えるSpring BootとDropwizardをコードベースで比較してみます。

わかり易く比較したいので、パッケージ構成・命名などはDropwizardに合わせています。

また、Dropwizardと同じように書きたいので、Spring Bootで必須ではないjarも依存させています。

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


maven設定

コードの比較の記事にしたいので、Dropwizardのpom.xmlについては、ここでは書きません。

気になる方は以下の記事も参照ください。

以下は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クラスでの@EnableAutoConfigurationspring-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を使ったときも似たようなクラスになりました。

//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経由で様々なことができました。


テンプレートによる動的ファイル表示の比較(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}}