【初心者】SpringBootでTodoアプリのREST APIを書いてみる

追記

  • Controllerのコードの一部が切れていたので修正
  • Controllerのアノテーション周りの話が抜けていたので追記

内容

  • Java初心者が夏休み中にSpringの勉強をする
  • Spring Frameworkはハードルが高そうだったので一先ずSpringBootから
  • お題は初心者がよくやるTodoアプリのREST API

詳しく触れないこと

  • gradle周辺
  • Database周辺
    • JPA
    • H2 Database

本編

  • 開発はIntelliJ IDEAを使用
    • ビルドとかアプリケーションの起動はお任せ

雛形作成

  • https://start.spring.io/ で作成
    • Project: Gradle Project
    • Language: Java
    • Spring Boot: 2.1.8
    • Project Metadaba:
      • Group: com.example
      • Artifact: todoapp
      • Options: そのまま
    • Dependencies:
      • Spring Web
      • Spring Data JPA
      • H2 Database
      • Lombok
  • ダウンロードされたzipファイルをunzipしてIntelliJでopenしたところから開始

パッケージ構成

  • com.example.todoapp直下に以下を並べる
    • controllers
    • repositories
    • models
  • servicesとかもあるべきかもしれないけど今回は割愛

コード

models

package com.example.todoapp.models;

import lombok.Data;

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

@Data
@Entity
public class Task {

    @Id
    @GeneratedValue
    private Long id;
    private String summary;
    private Boolean done = false;

    public Task(){}
}
  • アノテーションについて
    • @Data
      • 各フィールドのsetterとgetterを定義してくれる
      • toString()メソッドやhashCode()メソッドを定義してくれる
    • @Entity
      • エンティティであることを示す。
      • このクラスのフィールドを持つテーブルがDBに作られる
    • @Id
      • エンティティの主キーのフィールドを指定する
    • @GeneratedValue
      • 主キーの値の生成方法を指定する
      • strategygeneratorを指定して挙動を変えられそう(試してない)

repositories

  • TaskRepository.java
package com.example.todoapp.repositories;

import com.example.todoapp.models.Task;
import org.springframework.data.jpa.repository.JpaRepository;

public interface TaskRepository extends JpaRepository<Task, Long> {
}
  • JpaRepositoryを継承したinterfaceを定義
    • JpaRepositoryの祖先にいるCrudRepositoryインターフェースがfindByIdなどのインターフェースを定義している
    • 実装はSimpleJpaRepositoryなのかな?
  • JpaRepositoryは対象のエンティティクラスとその主キーの型を指定する
    • ここで指定した主キーの型は、findByIddeleteByIdの引数の型として使われる
    • 今回作成したTaskエンティティの主キーはLong型なのでここではLongを指定

controllers

  • ApiController.java
package com.example.todoapp.controllers;

import com.example.todoapp.models.Task;
import com.example.todoapp.repositories.TaskRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping(value = "/api")
public class ApiController {

    @Autowired
    TaskRepository taskrepo;

    @GetMapping(value = "/todos")
    public List<Task> tasklist(){
        return taskrepo.findAll();
    }

    @GetMapping(value = "todo/{id}")
    public Optional<Task> retrieve(@PathVariable Long id){
        return taskrepo.findById(id);
    }

    @PostMapping(value = "/todo")
    public ResponseEntity<Task> newTask(@RequestBody Task task){
        Task result = taskrepo.save(task);
        return new ResponseEntity<Task> (result, HttpStatus.CREATED);
    }

    @PutMapping(value = "/todo/{id}")
    public Task update(@PathVariable Long id, @RequestBody Task task){
        task.setId(id);
        return taskrepo.save(task);
    }

    @DeleteMapping(value = "/todo/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable Long id){
        taskrepo.deleteById(id);
    }
}
  • アノテーションについて
    • @RestController
      • 各メソッドの戻り値がそのままレスポンスボディになる
      • @Controllerを使う場合、メソッドの戻り値はStringにしてテンプレートに埋め込む形で使うらしい
    • @RequestMapping
      • このコントローラが受け付けるリクエストのパスやメソッドやクエリパラメータを指定できる
    • @Autowired
      • @Componentアノテーションをつけて登録されているBeanとの紐付けを行う
      • @Repository@Serviceをつけたクラスも、内部では@Componentが付加されている
      • ここではTaskRepositoryインターフェースを満たすクラスが自動的にtaskrepoに格納される
    • @GetMapping, @PostMapping, @PutMapping, @DeleteMapping
      • @RequestMappingのメソッドを固定したもの
      • @GetMappingの場合、@RequestMapping(method=RequestMethod.GET)と同等
    • @ResponseStatus

動作確認

新規登録

$ curl http://localhost:8080/api/todo -XPOST -H 'Content-Type: application/json' -d '{"summary": "my first task"}' | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    75    0    47  100    28    170    101 --:--:-- --:--:-- --:--:--   170
{
  "id": 1,
  "summary": "my first task",
  "done": false
}

リスト取得

$ curl http://localhost:8080/api/todos | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    49    0    49    0     0    285      0 --:--:-- --:--:-- --:--:--   286
[
  {
    "id": 1,
    "summary": "my first task",
    "done": false
  }
]

1件取得

$ curl http://localhost:8080/api/todo/1 | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    47    0    47    0     0   2045      0 --:--:-- --:--:-- --:--:--  2136
{
  "id": 1,
  "summary": "my first task",
  "done": false
}

更新

$ curl http://localhost:8080/api/todo/1 -XPUT -H 'Content-Type: application/json' -d '{"summary": "updated my task", "done": true}' | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    92    0    48  100    44   2632   2412 --:--:-- --:--:-- --:--:--  2666
{
  "id": 1,
  "summary": "updated my task",
  "done": true
}

削除(レスポンスボディがないのでStatusCodeで確認)

$ curl http://localhost:8080/api/todo/1 -XDELETE -v
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> DELETE /api/todo/1 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 204
< Date: Wed, 25 Sep 2019 15:29:37 GMT
<
* Connection #0 to host localhost left intact

まとめ

  • 動かしたいもの自体はめっちゃ簡単に動かせた
  • アノテーション種類多すぎて何使えばいいのかわからん
    • とりあえず今回使ったものくらいは最低限おさえておきたい
    • あとは何かするときに都度調べられればよさそう

OracleXE 11gの SJIS化

注釈

この記事について

  • OracleDatabase Express Edition(XE) 11g のDockerコンテナで文字コードを変更するためにやったことを書く(N番煎じ)
  • 11gでは文字コードはデフォルトでUTF8となっており、変更するためにはDBを再構築する必要がある
  • 単体テストとかで使いたいので、起動したときに既に文字コードが変更されている状態にしたい
  • 「え、今の時代に11g…?」
    • 仰るとおりだと思いますが、古いシステムだし皆さんのところにもそういうのありますよね?ね?

前提

  • oracleが公開しているDockerfileを使って作成したイメージをベースイメージとする
    • 記事作成時点での最新コミットは 0972d17

手順

追加で修正する箇所

  • CMDコマンドで実行されている$ORACLE_BASE/$RUN_FILEを実行しないようにする必要がある
    • 起動時に/etc/init.d/oracle-xe configureが実行されないようにするため
  • 以下のことを自前で行う
    • tnsnames.oraの書き換え
    • listener.oraの書き換え
    • /etc/init.d/oracle-xe configureに渡すrspファイルの書き換え
      • SYSとSYSTEM向けのパスワードの部分

上記もろもろに対応したDockerfile

確認

  • コンテナを起動
$ docker run --name oracle --shm-size 1g --rm nkiri/oracle/database:11.2.0.2-sjis
  • 別ターミナルでdocker execでコンテナに入る
$ docker exec -it oracle
  • 以下のコマンドを実行
su -p oracle -c "sqlplus -s / as sysdba" <<EOS
> select * from nls_database_parameters where parameter = 'NLS_CHARACTERSET';
> EOS
  • 結果
PARAMETER
------------------------------
VALUE
--------------------------------------------------------------------------------
NLS_CHARACTERSET
JA16SJISTILDE

まとめ

  • まぁできるんだけど、ビルドに1時間くらいかかる…
  • SYSとSYSTEMのパスワードをビルド時に確定することになるので、公式のやつより若干自由度が下がる

django-rest-frameworkのserializerってどう呼ばれてるんだろう

注釈

目的

  • 前に書いたやつ でserializerの中身は読んだ
  • でもこのserializerってどこでどう呼ばれてるんだろう

環境

  • 前のと同じ
  • Serializerの使い方とかは参考記事を参照

結論

  • mixins.pyで呼ばれてた
    • createupdateのそれぞれで
class CreateModelMixin(object):
    """
    Create a model instance.
    """
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
class UpdateModelMixin(object):
    """
    Update a model instance.
    """
    def update(self, request, *args, **kwargs):
        partial = kwargs.pop('partial', False)
        instance = self.get_object()
        serializer = self.get_serializer(instance, data=request.data, partial=partial)
        serializer.is_valid(raise_exception=True)
        self.perform_update(serializer)
  • get_serializerしてis_validしてperform_hogehogeするっていう流れらしい
    • perform_hogehogeではserializer.save()してる

応用

  • POSTとかPUTでAPIを呼ばれた時の挙動を帰るときはviewのcreateメソッドとかupdateメソッドを上書きしてやればよさげ
  • DBにデータ書き込みはしないけどPOSTでAPIを提供したいときは↓みたいな感じ?
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        # ここでserializer.validated_dataを使って何らかの処理をする
        return Response({'result': 'data'}, status=status.HTTP_200_OK)

参考

django-rest-frameworkのserializerでいろんなメソッドの呼ばれる順番

注釈

この記事の目的

  • django-rest-framework を使っていて、serializerが中でどういう処理をしているのかがわからなくてつらい
  • そうだ、フレームワークの中身を読んでみよう

環境

  • djangorestframework: 3.9.1

見るところ

  • rest_framework/serializers.py
    • class Serializer(BaseSerializer)の辺り
    • とりあえずここ理解すれば他もなんとかなる?

読む

  • renderersとかmixinsとかは後回しでいきなりserializers

to_internal_value(self, data)

 471     def to_internal_value(self, data):
 472         """
 473         Dict of native values <- Dict of primitive datatypes.
 474         """
 475         if not isinstance(data, Mapping):
 476             message = self.error_messages['invalid'].format(
 477                 datatype=type(data).__name__
 478             )
 479             raise ValidationError({
 480                 api_settings.NON_FIELD_ERRORS_KEY: [message]
 481             }, code='invalid')
  • 受け取ったデータ型の検証とか?
 483         ret = OrderedDict()
 484         errors = OrderedDict()
 485         fields = self._writable_fields
  • ret: 最終的な返却値
  • errors: 例外情報
  • fields: to_internal_valueで処理する対象が格納されている。
    • _writable_fieldsは以下の通り。
 367     @cached_property
 368     def _writable_fields(self):
 369         return [
 370             field for field in self.fields.values() if not field.read_only
 371         ]
  • read_onlyプロパティがFalseになっているfieldだけが含まれるらしい。
 487         for field in fields:
 488             validate_method = getattr(self, 'validate_' + field.field_name, None)
 489             primitive_value = field.get_value(data)
 490             try:
 491                 validated_value = field.run_validation(primitive_value)
  • fieldのrun_vaildationを実行
 523     def run_validation(self, data=empty):
 524         """
 525         Validate a simple representation and return the internal value.
 526
 527         The provided data may be `empty` if no representation was included
 528         in the input.
 529
 530         May raise `SkipField` if the field should not be included in the
 531         validated data.
 532         """
 533         (is_empty_value, data) = self.validate_empty_values(data)
 534         if is_empty_value:
 535             return data
 536         value = self.to_internal_value(data)
 537         self.run_validators(value)
 538         return value
  • 渡ってきた値のto_internal_valueを呼び出し。
    • ここの処理はまた今度読む
 492                 if validate_method is not None:
 493                     validated_value = validate_method(validated_value)
  • validate_<field名> という名前でメソッドが宣言されてたらそれを実行する
 494             except ValidationError as exc:
 495                 errors[field.field_name] = exc.detail
 496             except DjangoValidationError as exc:
 497                 errors[field.field_name] = get_error_detail(exc)
 498             except SkipField:
 499                 pass
  • 例外が発生したらerrorsに入れる
 500             else:
 501                 set_value(ret, field.source_attrs, validated_value)
  • validate_<field名>のメソッドを実行した結果をretに入れる
 503         if errors:
 504             raise ValidationError(errors)
  • 何かしらのエラーがあったら例外発火
 506         return ret

to_representation

 508     def to_representation(self, instance):
 509         """
 510         Object instance -> Dict of primitive datatypes.
 511         """
 512         ret = OrderedDict()
 513         fields = self._readable_fields
  • fields: _readable_fieldsで取得
    • _readable_fieldsは以下の処理
    • write_onlyでないフィールドを取得している
 373     @cached_property
 374     def _readable_fields(self):
 375         return [
 376             field for field in self.fields.values()
 377             if not field.write_only
 378         ]
 514
 515         for field in fields:
 516             try:
 517                 attribute = field.get_attribute(instance)
 518             except SkipField:
 519                 continue
  • instanceから指定のフィールドに格納されているデータを取得
 521             # We skip `to_representation` for `None` values so that fields do
 522             # not have to explicitly deal with that case.
 523             #
 524             # For related fields with `use_pk_only_optimization` we need to
 525             # resolve the pk value.
 526             check_for_none = attribute.pk if isinstance(attribute, PKOnlyObject) else attribute
 527             if check_for_none is None:
 528                 ret[field.field_name] = None
 529             else:
 530                 ret[field.field_name] = field.to_representation(attribute)
  • コメントのとおり…
  • to_internal_valueと同様に、fieldのto_representationも順に呼び出される
 531
 532         return ret

まとめ

  • to_internal_value
    • 各fieldに対してto_internal_valueとvalidationを繰り返してる
    • オブジェクトがネストされているときは、深いところからto_internal_valuevalidate_<field名> が実行されることになりそう
  • to_representation
    • fieldto_representationを呼び出してるくらい?
  • field.to_internal_valueとかfield.to_representationはまた今度読む

Cisco 1812j でOSPFを動かすためのコンフィグ

やったこと

Cisco 1812jを用いてマルチエリアOSPFネットワークを構築。
設定を入れて、実際のパケットの流れとか追ってみた。

構成

Endpoint Device1 --- Router1(C1812j) --- Router2(C1812j) --- Endpoint Device2

Cisco 1812jはFastether0とFastether1のインターフェースを持つ。Router間の接続にFastether 0、RouterとEndpoint Deviceの接続にFastether 1を使用した。

コンフィグ

事前準備

IPアドレス設定

Router1

Router1(config)# interface fastether 0
Router1(config-if)# no shutdown
Router1(config-if)# ip address 192.168.0.1 255.255.255.0


Router1(config)# interface fastether 1
Router1(config-if)# no shutdown
Router1(config-if)# ip address 192.168.1.1 255.255.255.0

Router2

Router2(config)# interface fastether 0
Router2(config-if)# no shutdown
Router2(config-if)# ip address 192.168.0.2 255.255.255.0


Router2(config)# interface fastether 1
Router2(config-if)# no shutdown
Router2(config-if)# ip address 192.168.2.1 255.255.255.0
DHCP設定

Router1

Router1(config)# ip dhcp pool network1
Router1(dhcp-config)# network 192.168.1.0 /24
Router(dhcp-config)# default-router 192.168.1.1
Router(dhcp-config)# dns-server 192.168.1.1

Router2

Router2(config)# ip dhcp pool network2
Router2(dhcp-config)# network 192.168.2.0 /24
Router2(dhcp-config)# default-router 192.168.2.1
Router2(dhcp-config)# dns-server 192.168.2.1

本題

OSPF設定

Router1
area 0

Router1(config)# router ospf 1
Router1(config-router)# network 192.168.0.0 0.0.0.255 area 0

area 1

Router1(config)# router ospf 1
Router1(config-router)# network 192.168.1.0 0.0.0.255 area 1

Router2
area 0

Router2(config)# router ospf 1
Router2(config-router)# network 192.168.0.0 0.0.0.255 area 0

area 1

Router2(config)# router ospf 1
Router2(config-router)# network 192.168.2.0 0.0.0.255 area 2

これでコンバージェンスを待つと通信ができるようになる。

仕組みの話は今度調べてまとめる

Ubuntu 14.04でJavaScriptエンジンのSpidermonkeyをビルドして逆アセンブルする

Mozillafirefoxに搭載されているJavaScriptエンジンであるSpidermonkeyをビルドする.
今回は実行したJavaScriptコードを逆アセンブルできるようにする.
公式はこちら

準備

# apt-get install autoconf

ソースの取得

# git clone https://github.com/mozilla/gecko-dev.git

ビルド

# cd gecko-dev/js/src
# autoconf
# mkdir Debug
# cd Debug
# ../configure --enable-debug --disable-optimize
# make

これでdis()とかdissrc()とかできる.

Ubuntu 12.04 LTS で GoogleのJavaScriptエンジンV8をスタンドアロンで動かす

基本的には公式のドキュメントに従って進めるだけ.
今回は実行するJavaScriptを逆アセンブルしたコードを出力させるようにする.

Prepare

$ sudo apt-get install gcc g++ git subversion gcc-multilib g++-multilib

Install depot_tools

$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
$ export PATH="$PATH":`pwd`/depot_tools

Get source code

$ svn co http://v8.googlecode.com/svn/trunk v8

Prerequisite: Installing GYP

$ cd v8
$ make dependencies

Building

$ make -j4 release disassembler=on

Run

$ cd out/xxx.release
$ ./d8 hoge.js -print-code