ラクス Advent Calendar 2019の16日目の記事です。
#はじめに
こんにちは。sts-sd2です。
昨日はBlack-Spiderさんの今年一年のアウトプットを振り返るでした!
私ごとですが、業務内でもコーディングする機会が増えてきてアプリの構成やアーキテクチャにフツフツと興味が湧いてきています。
そんな中、社内でも既存アプリの構成がレガシーになりつつあり、新しいものに乗り換えたいよねという声が聞こえてきます。
個人的にはDockerなどをかじってることもあり、マイクロサービスというワードに惹かれています。
なので本日の記事ではマイクロサービス?っぽくアプリを作ってみたので、そのことをネタにしたいと思います。
注)マイクロサービスについてはスズメの涙ほどしか理解していませんのでご了承を。。。
#アプリケーション概要
今回作ったものはアイテム管理サービスです。
できることはアイテムの検索・登録・更新・削除と非常にシンプルなものになっています。
マイクロサービスLikeにしたかったので、フロント(情報表示の機能)、バックエンド(アイテム管理の機能)でアプリ自体を分け、フロント-バックエンド間のやりとりはAPIのみにしています。
フロントは情報をどのように表示するのかに、バックエンドはアイテム管理のロジックに、それぞれ集中することができるので、追加機能の開発もしやすそうです。
ソースはGitに上げてありますので、詳細は省きます。
#RESTfulなアイテム管理APIを作る
バックエンド側はSpring+REST APIで用意しました。
アイテム管理アプリができることは、API経由での検索・登録・更新・削除(CRUD)のみで、画面は用意していません。
(表示したい側がうまく加工すればいいねんと割り切ります。)
RESTfulを謳った以上、RESTの原則に則らなければならないわけですが、、、
過去のラクス Advent Calendar 2017の記事でこの辺りをまとめてくれていますのでここでは割愛します。
MasaKuさんの記事は読んでいただけたでしょうか?
この原則を念頭にいれたうえで、Controllerクラスを確認していきましょう。
@RestController
@RequestMapping("api/items") //①ItemRestControllerを実行するURL
public class ItemRestController {
@Autowired
ItemService itemService;
/**
* 商品一覧取得API
* @return List<item>
*/
@CrossOrigin
@GetMapping //②リクエストの種類に応じてどのメソッドを実行するかをマッピングする
@ResponseStatus(HttpStatus.OK)
List<Item> getItems() {
List<Item> items = itemService.findAll();
return items;
}
/**
* 商品登録API
* @param item
* @return item
*/
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
Item postItem(@RequestBody Item item) { //Itemクラスのデータ構造をJson形式で取得する
return itemService.create(item);
}
/**
* 商品削除API
* @param id
*/
@DeleteMapping(path = "{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
void deleteItem(@PathVariable Integer id) {
itemService.delete(id);
}
/**
* 商品更新API
* @param id
* @param item
* @return item
*/
@PutMapping(path = "{id}")
Item putItem(@PathVariable Integer id, @RequestBody Item item) {
item.setId(id);
return itemService.update(item);
}
}
①でアイテム管理をする機能はapi/items
のURL1つで表現できます。
②でHTTPメソッドごとにControllerのどのメソッドが動くかマッピングしていることで、リクエストがGET・POST・PUT・DELETEできた時の振る舞いが分けられます。
Controllerクラス単体でもAPIの操作がかなりわかりやすくなっているのではないでしょうか?
(マニュアル作れと言われればここ見れば作れそうな気がする)
実際にcurlコマンドでリクエストを送ってみます。
###GET 検索
検索はシンプルに全件検索にしています。
後述のPUTなどのようにIDをパラメータに渡してやれば1件検索も簡単に実現できそうです。
spring-api $ curl http://localhost:8080/api/items
[{"id":1,"name":"Rakus","price":100,"imgPath":"rakus.jpg"},{"id":2,"name":"アドベント","price":200,"imgPath":"advent.jpg"},{"id":3,"name":"カレンダー","price":300,"imgPath":"calendar.jpg"}]
###POST 登録
登録時はJsonの形式でItemの情報を渡します。
このAPIはどんなJsonを受け付つけるのかがわかりやすい(と思ってます。)
spring-api $ curl http://localhost:8080/api/items -i -XPOST -H "Content-Type: application/json" -d "{
> \"name\":\"Merry Christmas \",
> \"price\":\"1000\",
> \"imgPath\":\"christmas.jpg\"
> }"
HTTP/1.1 201
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sat, 14 Dec 2019 07:34:00 GMT
{"id":4,"name":"Merry Christmas ","price":1000,"imgPath":"christmas.jpg"}
spring-api $ curl http://localhost:8080/api/items
[{"id":1,"name":"Rakus","price":100,"imgPath":"rakus.jpg"},{"id":2,"name":"アドベント","price":200,"imgPath":"advent.jpg"},{"id":3,"name":"カレンダー","price":300,"imgPath":"calendar.jpg"},{"id":4,"name":"Merry Christmas ","price":1000,"imgPath":"christmas.jpg"}]
###PUT 更新
URLの中に更新したいアイテムのIDを指定しています。
操作したい情報がURLに含まれているため操作したい対象が明確になっています。
spring-api $ curl http://localhost:8080/api/items/4 -i -XPUT -H "Content-Type: application/json" -d "{
> \"name\":\"Happy New Year\",
> \"price\":\"1000\",
> \"imgPath\":\"new_year.jpg\"
> }"
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sat, 14 Dec 2019 07:40:58 GMT
{"id":4,"name":"Happy New Year","price":1000,"imgPath":"hghg.jpg"}
spring-api $ curl http://localhost:8080/api/items
[{"id":1,"name":"Rakus","price":100,"imgPath":"rakus.jpg"},{"id":2,"name":"アドベント","price":200,"imgPath":"advent.jpg"},{"id":3,"name":"カレンダー","price":300,"imgPath":"calendar.jpg"},{"id":4,"name":"Happy New Year","price":1000,"imgPath":"new_year.jpg"}]spring-api 16:41:18 $
###DELETE 削除
spring-api $ curl http://localhost:8080/api/items/4 -i -XDELETE
HTTP/1.1 204
Date: Sat, 14 Dec 2019 07:42:52 GMT
spring-api $ curl http://localhost:8080/api/items
[{"id":1,"name":"Rakus","price":100,"imgPath":"rakus.jpg"},{"id":2,"name":"アドベント","price":200,"imgPath":"advent.jpg"},{"id":3,"name":"カレンダー","price":300,"imgPath":"calendar.jpg"}]
CRUDの機能を表現することができました。
これだけでもアイテム管理するサービスとしては完成です。
リクエストで求められた情報レスポンスで返してあげたので、どう表示したいかはフロントに任せてしまいましょう。
#APIをクライアントから実行する
APIはできたわけですが、毎回curlのコマンドを手作りするのも辛いところですし、
文字列だけの結果というのも味気ないです。
ということで簡単なフロント側画面を用意します。
フロントができることはAPIを叩くことと、取得したレスポンスを表示することだけです。
担当商材でReactを使っていることもあり、学習含めでReactを採用しました。
「マイクロなサービスを目指して」なのでフロントとバックは明確に分けておきたいので、フロントはNode.jsで動かすことにします。
クライアントから検索APIが叩けるところまで確認しましょう。
Fetch APIを使ってアイテム管理APIを叩いています。
APIから受け取ったJsonはitemsに詰めて画面表示します。
(CSSも何も用意していないので、見た目がブサイクなのはご愛嬌ということで・・・・)
画面側も出来上がり(検索だけ)、Webサービスとしてのスタートがきれました。
reloadItemApi(e) {
fetch('http://localhost:8080/api/items', {
method: 'GET',
}).then(res => {
if(res.ok) {
return res.json();
} else {
console.log('error!');
}
}).then(json => {
const items = json.map(r => {
return {
id: r.id,
name: r.name,
price: r.price
};
});
this.setState({ items: items });
});
}
(略)
render() {
return (
<div>
<h1>アイテム管理</h1>
<ItemList items={ this.state.items } />
</div>
);
}
##余談 CORSの変
慣れないReactも書けたのでいよいよ自作API叩くぞと意気込んでGO。
クライアント側には検索結果が出ず真っ白。ついでに頭の中も真っ白。
APIの結果が帰ってくるのは確認済みなので、表示側だろうとコンソールを開くと
Access to fetch at 'http://localhost:8080/api/items' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
思った通りなんか出てます。
CORS(Cross-Origin Resource Sharing)でブロックされているらしいです。。。。
セキュリティ周りで弾かれているっぽいのですが、お恥ずかしながらコレ初めて目にしました。
Webページの情報を参考に色々調べみましたが、サイトを跨いだアクセスをする際には他サイトからのアクセスを許可してあげる必要があったようです。(ブラウザの仕様みたい)
解決策はとてもシンプルで、Springのクラス・メソッドに@CrossOrigin
のアノテーションを付与してやることで、アクセスに対して許可することができるようになります。
今回は省略していますが、本来はOriginを指定して、アクセス許可する呼び出し元を絞る必要があります。
こちらのページを解決の参考にさせていただきました。
まとめ
React+Spring Bootでマイクロサービス?っぽい構成でアプリケーションを作りました。
概念としてマイクロサービスやREST APIは知っていましたが、いざ作ってみると結構詰まりました。
それはまた別の機会に。。。
マイクロサービスやRESTの設計について体系立てて学んだわけではないのであくまで「っぽい」としています。
アプリとしても基礎の骨組み部分しかできていませんが、機能追加してみたり、フロント・バック・DBをそれぞれDockerコンテナとして繋いでみたり、新しく取り入れることができるものは別サービスとして立ててつなぎ合わせてみたり、いい感じに魔改造できそうで楽しみです。
ではでは引き続き、ラクス Advent Calendar 2019 お楽しみに!!