mmts1007’s diary

プログラミング関連の技術系ブログです。

Lombok でコード量を削減!

前回の記事でも書きましたが、Spring Boot のサンプルではあえて Lombok を使用しませんでした。
理由はまだ Lombok の解説をしていなかったので。

ということで、Lombok の解説をしようと思います。

What's Lombok

公式:Project Lombok

簡単にいうと、Java のお決まりのコードを排除するためのライブラリです。 Getter/Setter とかコンストラクタとかお決まりで書いているコードをアノテーションを付けることで排除できるようになります。(すごい!)

導入

Lombok は依存関係に追加する他に、IDE にも設定する必要があります。

  1. 依存関係の解決 公式にも記載されていますが、 Gradle を使う場合は下記の通り
provided "org.projectlombok:lombok:1.16.6"

ちなみに providedapply plugin: 'war' を指定しないと使えないので、指定していない場合は compile に変更してください。

  1. IDE への追加 Eclipse の場合は公式から jar をダウンロード、ダブルクリックし、Lombok をインストールしたい IDE を選択すれば完了です。

サンプル

サンプルに Spring Boot の時に使用したエンティティをリファクタします。

元ソース

package hello.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "tasks")
public class Task {

    @Id
    @GeneratedValue
    private Integer id;

    @Column(nullable = false)
    private String context;

    public Task() {
    }

    public Task(Integer id, String context) {
        this.id = id;
        this.context = context;
    }

    public Integer getId() {
        return id;
    }

    public String getContext() {
        return context;
    }
}

まずはコンストラクタを削除
今回、引数なしのコンストラクタと、すべてのフィールドを持つコンストラクタが定義されているので、この 2 つを Lombok で生成します。 引数なしのコンストラクタ@NoArgsConstructor、すべてのフィールドを持つコンストラクタ@AllArgsConstructor で生成できます。

package hello.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "tasks")
@NoArgsConstructor
@AllArgsConstructor
public class Task {

    @Id
    @GeneratedValue
    private Integer id;

    @Column(nullable = false)
    private String context;

    public Integer getId() {
        return id;
    }

    public String getContext() {
        return context;
    }
}

同様に Getter/Setter も削除します。
@Getter@Setter アノテーションもあるのですが、データを管理するクラスのためのアノテーション @Data を付与します。
@Data@Getter@Setter アノテーションを含んでいるので、付与されます。

package hello.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Entity
@Table(name = "tasks")
@NoArgsConstructor
@AllArgsConstructor
public class Task {

    @Id
    @GeneratedValue
    private Integer id;

    @Column(nullable = false)
    private String context;
}

コード量がかなり少なくなりました。

f:id:mmts1007:20151012172542p:plain

Outline を見ると、ちゃんとコンストラクタ、Getter/Setter が定義されていますね。

その他のアノテーション

Lombok には他にも様々なアノテーションがあります。

Lombok feature overview

@Data, @Value, @Builder 辺りはすぐに導入できると思います。

@Data

上でも解説しましたが、POJO や Beans に使用するアノテーションです。 また、POJO や Beans に使用するアノテーション @ToString, @EqualsAndHashCode, @Getter, @Setter, @RequiredArgsConstructor も自動で付与されます。

@ToString

デバッグがしやすいように、クラスのフィールド名、フィールドの値を出力するように toString メソッドをオーバーライドします。

@EqualsAndHashCode

オブジェクトに定義されているフィールドの値で equails, hashCode を算出できるように equails, hashCode メソッドをオーバーライドします。
プレーンな Java だと Bean クラスのフィールドの値が一緒でも、equals は false になってしまうので、開発者がいちいち再定義するのは面倒なのでありがたいです。

@Value

イミュータブル(不変の値)を持つクラスを簡単に生成するアノテーションです。
イミュータブルなので値の変更ができないため、フィールドはデフォルトで private, final になります。setter は生成されません。

@Builder

ビルダーパターンを簡単に生成するアノテーションです。

以上です。
Lombok はデファクトスタンダードになると思います。すでにデファクトスタンダードかもしれません。
メリットの方が大きいと思うので、積極的に導入しはいかがでしょうか。

Java Spring Boot その8

7回に渡って Spring Boot について記事を書いてきたので、Spring Boot を触ってみた感想を書いてみようと思います。

良かった点

設定ファイル(XML) が少ない

今までのサンプルを通して、設定ファイルは 1 回も書きませんでした。
Spring というと XML というイメージがあったのですが、全く書かなくてここまで出来るとは思っていませんでした。

実装が揃っている

リポジトリクラスを作成した時に驚いたのですが、よく使う実装を用意してくれているのがすごく有り難いですね。
DRY に繋がると思います。

starter が良い

使いたい機能の starter を設定すれば、必要な依存が自動的に追加される。依存関係の解決がとても楽でした。
Java というとライブラリ依存が激しいので、この機能は開発者にとって楽だと思います。

アノテーションが理解しやすい

こういう意味のアノテーションなんだろうな。というのが名前から推測しやすかったです。

記述量が少ない

実装が揃っている、starterが良い と繋がると思うのですが、
これらのお陰で記述量がかなり削減できていると思います。

組み込み Tomcat が良い

Java の Web アプリケーションというと Tomcat を用意して、EclipseTomcat プラグイン入れて…と色々面倒ですが、
Tomcat が組み込まれていることで、プレーンの Java を実行すればよいのはとても楽でした。
リリース時等も楽なのだろうなと思いながら、実装していました。

Spring Loaded が良い

Spring Boot の利点とは少し違いますが、Spring Loaded が結構効いてくれて、アプリケーションサーバの再起動回数が大幅に削減できました。 再起動自体も組み込み Tomcat のお陰で楽ですし。

気になった点

記述量が多い

上記の記述量が少ないと全く反対のことを書いていますが、Spring Boot として記述量が多いのではなく、Java として。
やっぱり Ruby と比べてると記述量が多いなって感じます。
今回あえて lombok を使わなかったのですが、lombok 使わないとこんなに書かなきゃいけないのかと思いました。
静的型付け言語なので、安心感は大きく得られるのですけどね。

以上です。
次回からは Spring Boot から少し離れようと思います。

Java Spring Boot その7

今回は DB 実装部分のソースを紹介します。

エンティティ

package hello.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "tasks")
public class Task {

    @Id
    @GeneratedValue
    private Integer id;

    @Column(nullable = false)
    private String context;

    public Task() {
    }

    public Task(Integer id, String context) {
        this.id = id;
        this.context = context;
    }

    public Integer getId() {
        return id;
    }

    public String getContext() {
        return context;
    }
}

@Entity

エンティティクラスを表すためのアノテーション

@Table(name = "tasks")

エンティティに紐づくテーブルを指定するアノテーション。デフォルトではエンティティ名がクラス名となります。

@Id

主キーのフィールドを表すアノテーション

@GeneratedValue

主キーが自動採番されることを表す。生成方法を指定することもできる。

@Column(nullable = false)

カラムに対する設定を行うアノテーション
nullable = false と設定し、Not Null であることを設定。

リポジトリ

package hello.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import hello.domain.Task;

public interface TaskRepository extends JpaRepository<Task, Integer> {

}

JpaRepository を継承したインターフェースを作成するだけで、取得、登録、更新、削除ができるようになります。(すごい!)
JpaRepository<Task, Integer> のようにジェネリクスに対象のエンティティ、IDカラムのデータ型を宣言し、終了です。

サービス

package hello.service;

import java.util.List;

import javax.transaction.Transactional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import hello.domain.Task;
import hello.repository.TaskRepository;

@Service
@Transactional
public class TaskService {
    @Autowired
    TaskRepository taskRepository;

    public List<Task> findAll() {
        return taskRepository.findAll();
    }

    public Task findOne(Integer id) {
        return taskRepository.findOne(id);
    }

    public Task create(Task task) {
        return taskRepository.save(task);
    }

    public Task update(Task task) {
        return taskRepository.save(task);
    }

    public void delete(Integer id) {
        taskRepository.delete(id);
    }
}

@Service

サービスクラスを表すアノテーション

@Transactional

トランザクション境界を設定するアノテーション
サービスに設定したため、トランザクション境界はサービスとなります。

@Autowired

DI (依存性注入) を行うためのアノテーション
Autowired アノテーションを記述すると、自動的に DI を解決してくれます。

サービスクラス自体はリポジトリクラスの内容を呼び出す形なので特に説明はいらないと思います。

コントローラ

package hello;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import hello.domain.Task;
import hello.service.TaskService;

@RestController
@RequestMapping("api/tasks")
@EnableAutoConfiguration
@ComponentScan
public class RestSampleController {

    @Autowired
    TaskService taskService;

    @RequestMapping(method = RequestMethod.GET)
    List<Task> getTasks() {
        // GET api/tasks で実行されるメソッド
        return taskService.findAll();
    }

    @RequestMapping(value = "{id}", method = RequestMethod.GET)
    Task getTask(@PathVariable Integer id) {
        // GET api/tasks/{id} で実行されるメソッド
        return taskService.findOne(id);
    }

    @RequestMapping(method = RequestMethod.POST)
    @ResponseStatus(HttpStatus.CREATED)
    Task createTask(@RequestBody Task task) {
        // POST api/tasks で実行されるメソッド
        return taskService.create(task);
    }

    @RequestMapping(value = "{id}", method = RequestMethod.PUT)
    Task updateTask(@PathVariable Integer id, @RequestBody Task task) {
        // PUT api/tasks/{id} で実行されるメソッド
        return taskService.update(task);
    }

    @RequestMapping(value = "{id}", method = RequestMethod.DELETE)
    @ResponseStatus(HttpStatus.NO_CONTENT)
    void deleteTask(@PathVariable Integer id) {
        // DELETE api/tasks で実行されるメソッド
        taskService.delete(id);
    }

    public static void main(String[] args) {
        SpringApplication.run(RestSampleController.class, args);
    }
}

大きな変更はなく、モックデータを削除して、サービスクラスからデータを取得するようにした位です。

@ComponentScan

必要なコンポーネントを自動で読み込んでくれるアノテーションです。
これによりサービス、リポジトリ、エンティティクラスが読み込まれます。

以上で DB 実装部分は終了です。

Java Spring Boot その6

前回のモック部分を DB 接続に変更します。
その前に、今回使う DB、O/R マッパーや概念の説明を先にします。

DB, O/Rマッパーについて

DB は H2、O/R マッパーは Spring Data JPA を使用します。

O/R マッパーというのは DB のデータをオブジェクトにマッピングするためのライブラリです。
DB のデータを Java のクラス、オブジェクトとして扱うためのライブラリだと思ってください。

JPAJava Persistence API の略で、Java の O/R マッパーの仕様です。
Spring Data JPAJPA を拡張したライブラリです。

クラス構成

また、今回は下記の通りクラスを分割し、責務を分散します。

エンティティ

DB の情報を Java オブジェクトに表したもの テーブルがクラス、カラムがフィールドになっている。

リポジトリ

エンティティを登録、検索などといった処理を担うクラス。DB のデータを取得し、エンティティへマッピングする処理等を行う。
DB からのデータ取得方法などはリポジトリクラスに隠蔽させる。

サービス

ビジネスロジックを記述する。リポジトリを使用してデータの取得等もこのクラスが行う。

コントローラ

リクエストをマッピングするクラス
サービスクラスを利用してリクエストに必要な処理を実行する。

MVC のレイヤーと、処理内容を表にすると

  リクエスト 処理 DB
アプリケーション層 コントローラ
ドメイン サービス
ドメイン リポジトリ
ドメイン エンティティ

だと私は考えています。※あくまで私の考えです。

今回はここまで。実際のソースは次の記事で。

Java Spring Boot その5

前回の続きで POST, PUT, DELETE メソッドを作成をします。

ソース

POST, PUT, DELETE を追加したソースは下記の通りです。
また、モックデータの作成方法を変更しました。

package hello;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.IntStream;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("api/tasks")
@EnableAutoConfiguration
public class RestSampleController {

    static class Task {
        public Task() {
        }

        public Task(Integer id, String context) {
            this.id = id;
            this.context = context;
        }

        public Integer id;
        public String context;
    }

    static Map<Integer, Task> tasks = new HashMap<>();

    @RequestMapping(method = RequestMethod.GET)
    List<Task> getTasks() {
        // GET api/tasks で実行されるメソッド
        return new ArrayList<>(tasks.values());
    }

    @RequestMapping(value = "{id}", method = RequestMethod.GET)
    Task getTask(@PathVariable Integer id) {
        // GET api/tasks/{id} で実行されるメソッド
        return tasks.get(id);
    }

    @RequestMapping(method = RequestMethod.POST)
    @ResponseStatus(HttpStatus.CREATED)
    Task createTask(@RequestBody Task task) {
        // POST api/tasks で実行されるメソッド
        int nextIndex = tasks.keySet().stream().max(Comparator.naturalOrder()).get() + 1;
        task.id = nextIndex;
        tasks.put(nextIndex, task);
        return task;
    }

    @RequestMapping(value = "{id}", method = RequestMethod.PUT)
    Task updateTask(@PathVariable Integer id, @RequestBody Task task) {
        // PUT api/tasks/{id} で実行されるメソッド
        Task targetTask = tasks.get(id);
        if (targetTask == null) {
            return null;
        }
        targetTask.context = task.context;
        tasks.put(id, targetTask);
        return targetTask;
    }

    @RequestMapping(value = "{id}", method = RequestMethod.DELETE)
    @ResponseStatus(HttpStatus.NO_CONTENT)
    void deleteTask(@PathVariable Integer id) {
        // DELETE api/tasks で実行されるメソッド
        tasks.remove(id);
    }

    public static void main(String[] args) {
        // モックデータ作成
        IntStream.range(0, 3).forEach(index -> tasks.put(index, new Task(index, String.format("sample%d", index))));
        SpringApplication.run(RestSampleController.class, args);
    }
}

解説

前回解説した通り、@RequestMapping で POST, PUT, DELETE 各メソッドマッピングする Javaメソッドを作成しました。

@RequestMapping

今回 @RequestMapping(value = "{id}", method = RequestMethod.GET) のように value 属性が登場しました。
value は相対パスを指定することができます。なので getTask メソッドは GET "api/tasks/{id}" とマッピングされます。 また value に指定した {id} はメソッドの引数で取得することができます。
取得するためには @PathVariable アノテーションを付与した 変数を定義します。

@ResponseStatus

ResponseStatus アノテーションは名前の通りですが、レスポンスのステータスを指定することができます。
アノテーションに指定できる値は HttpStatus 型の値です。
HttpStatus は enum となっているので、HttpStatus.CREATED など視覚的にわかりやすい形で指定できます。

@RequestBody

メソッドの引数に指定されている RequestBody アノテーションですが、
こちらも名前の通りリクエストボディの値をマッピングするためのアノテーションです。
今回、リクエストボディに設定される JSON を Task 型の値にマッピングするために使用しています。

結果

メソッドを実行した結果をまとめました。

GET api/tasks

$ curl -v http://localhost:8080/api/tasks -X GET
*   Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> GET /api/tasks HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: Apache-Coyote/1.1
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Sat, 10 Oct 2015 23:40:31 GMT
<
* Connection #0 to host localhost left intact
[{"id":0,"context":"sample0"},{"id":1,"context":"sample1"},{"id":2,"context":"sample2"}]

全てのタスクが返却されています。

GET api/tasks/{id}

$ curl -v http://localhost:8080/api/tasks/2 -X GET
*   Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> GET /api/tasks/2 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: Apache-Coyote/1.1
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Sat, 10 Oct 2015 23:41:31 GMT
<
* Connection #0 to host localhost left intact
{"id":2,"context":"sample2"}

id で指定したデータが返却されています。

POST api/tasks

$ curl -v http://localhost:8080/api/tasks -X POST -H "Content-Type: application/json" -d "{\"context\" : \"create\"}"
*   Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> POST /api/tasks HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.43.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 22
>
* upload completely sent off: 22 out of 22 bytes
< HTTP/1.1 201 Created
< Server: Apache-Coyote/1.1
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Sat, 10 Oct 2015 23:42:14 GMT
<
* Connection #0 to host localhost left intact
{"id":3,"context":"create"}

作成したデータが返却されています。
@ResponseStatus(HttpStatus.CREATED) を指定したのでレスポンスステータスが HTTP/1.1 201 Created に変化しています。

PUT api/tasks/{id}

$ curl -v http://localhost:8080/api/tasks/1 -X PUT -H "Content-Type: application/json" -d "{\"context\" : \"update\"}"
*   Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> PUT /api/tasks/1 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.43.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 22
>
* upload completely sent off: 22 out of 22 bytes
< HTTP/1.1 200 OK
< Server: Apache-Coyote/1.1
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Sat, 10 Oct 2015 23:42:40 GMT
<
* Connection #0 to host localhost left intact
{"id":1,"context":"update"}

id で指定した要素のデータが更新されています。

DELETE api/tasks/{id}

$ curl -v http://localhost:8080/api/tasks/1 -X DELETE
*   Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> DELETE /api/tasks/1 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 204 No Content
< Server: Apache-Coyote/1.1
< Date: Sat, 10 Oct 2015 23:42:56 GMT
<
* Connection #0 to host localhost left intact

@ResponseStatus(HttpStatus.NO_CONTENT) を指定したので HTTP/1.1 204 No Content となり レスポンスもありません。

以上で REST API の作成は完了です。
次回はモック部分のデータを DB とやりとりするよう修正したいと思います。

Java Spring Boot その4

今回は Spring Boot を使って REST API を作ろうと思います。

DB を使わず、モックを使用します。
次回以降、モックの実装を DB を使った実装に修正しようと思います。

一覧取得

まずは一覧を取得するソースのみ

package hello;

import java.util.ArrayList;
import java.util.List;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("api/tasks")
@EnableAutoConfiguration
public class RestSampleController {

    static class Task {
        Task(String title, String context) {
            this.title = title;
            this.context = context;
        }

        public String title;
        public String context;
    }

    @RequestMapping(method = RequestMethod.GET)
    List<Task> getTasks() {
        List<Task> tasks = new ArrayList<>();
        tasks.add(new Task("sample1", "sample1"));
        tasks.add(new Task("sample2", "sample2"));
        tasks.add(new Task("sample3", "sample3"));

        return tasks;
    }

    public static void main(String[] args) {
        SpringApplication.run(RestSampleController.class, args);
    }
}

http://localhost:8080/api/tasks にアクセスすると

[
    {
        "title": "sample1",
        "context": "sample1"
    },
    {
        "title": "sample2",
        "context": "sample2"
    },
    {
        "title": "sample3",
        "context": "sample3"
    }
]

上記内容が返ってくると思います。
※上記 JSON は整形しています

解説

@RequestMapping("api/tasks") をコントローラクラスに付与することで、このクラスが "api/tasks" とマッピングされます。

@RequestMapping(method = RequestMethod.GET) を getTasks メソッドに付与することで GET "api/tasks" が getTasks メソッドマッピングされます。

同様に POST, PUT, DELETE をマッピングするメソッドを作成すれば、REST API の作成は完了です。
今回はここまで。

Java Spring Boot その3

今回は Spring Loaded について

Spring Loaded とは

Spring Loaded とは Seasar でいう Hot Deploy を可能にするライブラリです。 アプリケーションを再起動することなく、ソースの変更を反映することができます。 これにより生産性を向上することができます。

使い方

  1. Spring Loaded をダウンロード github.com

  2. アプリケーション実行時に JVM パラメータに Spring Loaded を指定

java -javaagent:<pathTo>/springloaded-{VERSION}.jar -noverify SomeJavaClass

サンプル

今回私が試した Eclipse での実行方法をサンプルとして載せます

  1. Spring Loaded をダウンロード

適当な場所にダウンロード

/path/to/spirng_loaded/springloaded-1.2.4.RELEASE.jar
  1. Eclipse でアプリケーション実行時に JVM パラメータに Spring Loaded を指定

Run as -> Run Configurations...

VM arguments に

-javaagent:/path/to/spirng_loaded/springloaded-1.2.4.RELEASE.jar -noverify

を指定し、実行

これだけで、変更が即時反映されるようになる

補足

本来は Gradle に依存ライブラリ、JVM パラメータを記載し、gradle bootRun で Spring Loaded が動くようにしたかった。 Gradle x Spring Loaded x IntelliJ の記事は見つかるが、Eclipse の記事が見つからず、今の所できていない。 一旦動く状態にしたかったため、上記設定で行っている状態である。 解決次第改めて記事を書こうと思う。