1クール続けるブログ

とりあえず1クール続けるソフトウェアエンジニアの備忘録

dokkuのソースコードでシェルスクリプトを勉強する

5月からの仕事がアプリケーション側でなくインフラ側になったので、急遽シェルスクリプトの勉強。powershellはちょいちょい扱ってはいたけども、Invoke-RestMethod(Linuxでのcurlコマンド)でjson取ってきて加工するみたいなことしかやってこなかったため知識不足甚だしい。
dokkuというbash200行ちょいでdockerベースのPaaS環境を構築できるプロジェクトがあり、せっかくなのでシェルスクリプトを学びつつPaaS構築のことも勉強できたら良いなと思い、dokkuのbootstrap.shのコードを見つつ勉強していきます。

bashスクリプトの先頭

#!/usr/bin/env bash
set -eo pipefail; [[ $TRACE ]] && set -x

シェルスクリプトの一行目に必ず記述する#!で始まる行はshebangと言われる。
ここで#!/bin/bashと指定すれば、絶対パスでのbash指定となります。

上記のように、#!/usr/bin/env bashと指定すれば、$PATH 上の bash が使われます。$PATHはコマンド検索パスを格納している環境変数printenv PATHで確認することができる。
メリットは、例えば $HOME/.opt 配下に最新の bash をインストールするなどした場合、$PATH にさえ入っていればそっちが使われるというのがあります。ちなみに$PATHに指定するときには、export PATH="$PATH:/opt/local/bin"とする。

set -eを宣言しておくとエラーが起きた行で中断するので、予想外のエラーを無視してスクリプトが処理を続行するのを防げます。ただ、パイプの中で起こったエラーは検知できず、右端のコマンドがエラーとなった時のみ有効です。set -o pipefailを指定することで、パイプ内のコマンドのエラーでも中断します。その2つを組み合わせてエラーを無視せず処理を中断するようにしたのが、set -eo pipefail

&&の前にあるコマンドを実行し、もし正常に終了した場合(戻り値が0)に&&の後ろにあるコマンドを実行する。set -xによってシェルのtraceが有効になる。実行したコマンドとその引数がトレース情報として標準エラー出力へ出力される。
[[ $TRACE ]]は調べたが分からなかった。

変数宣言と関数の書き方

main() {
  export DOKKU_DISTRO DOKKU_DISTRO_VERSION
  # shellcheck disable=SC1091
  DOKKU_DISTRO=$(. /etc/os-release && echo "$ID")
  # shellcheck disable=SC1091
  DOKKU_DISTRO_VERSION=$(. /etc/os-release && echo "$VERSION_ID")

  export DEBIAN_FRONTEND=noninteractive
  export DOKKU_REPO=${DOKKU_REPO:-"https://github.com/dokku/dokku.git"}

  ensure-environment
  install-requirements
  install-dokku
}

main "$@"

関数の書き方はfunction main() { ... }だが、functionは省略可能であり、通常省略されるため上記のソースのようになる。関数を呼び出すときはmain "$@"のように関数名を宣言すれば良い。引数として値を渡す場合には、関数名と並べて書いていく。今回の場合は、このスクリプトが呼び出された時の全ての引数をそのまま渡している($@は全ての引数を意味する)。
export 変数名環境変数の宣言となる。定数に関しては、Java命名規則と同じで定数+アンダースコアになる。 export DEBIAN_FRONTEND=noninteractiveのように環境変数の宣言と代入を同時に行うことができる。

変数の代入は変数=値の形となる。注意点としては、DOKKU_DISTRO = $(. /etc/os-release && echo "$ID")のように=の前後に空白を入れてしまうとうまく機能しません。

${変数値:-デフォルト値という書き方で変数を初期化することができる。export DOKKU_REPO=${DOKKU_REPO:-"https://github.com/dokku/dokku.git"}$DOKKU_REPOが存在していなければ、"https://github.com/dokku/dokku.git"で初期化する。

変数スコープとリダイレクト、終了ステータス

ensure-environment() {
  local FREE_MEMORY
  echo "Preparing to install $DOKKU_TAG from $DOKKU_REPO..."

  hostname -f > /dev/null 2>&1 || {
    echo "This installation script requires that you have a hostname set for the instance. Please set a hostname for 127.0.0.1 in your /etc/hosts"
    exit 1
  }

# 後半の処理は次のセクション

}

local FREE_MEMORYのようにlocalキーワードを変数宣言の前につけることによって、変数のスコープを関数内に狭めることができる。

echo は改行つきで値を表示するための命令。""(ダブルクォーテーション)の中では$変数または${変数}が展開されるが、''(シングルクォーテーション)の中では展開されない。

hostname -fでは、ホスト名とドメイン名からなるFQDNを表示する。
>command > /dev/null 2>&1これは標準エラー出力の結果を標準出力にマージして、/dev/nullに捨てることを意味する。つまり、標準出力された物も標準エラー出力された物も捨てるということ。詳しくは右記を参照。( 5分で一通り理解できる!Linuxのリダイレクト 使い方と種類まとめ

&&は左辺のコマンドが成功していたら右辺のコマンドも実行するものだが、||に関しては、左辺のコマンドが失敗していたら右辺のコマンドが実行される。このスクリプトの場合は、hostnameが設定されておらず、hostname -fのコマンドが評価され失敗したときに、「127.0.0.1ループバックアドレス)にhostnameを設定してください」とメッセージを出力し終了ステータス1をセットしてスクリプトを終了している。

コマンド終了時には「終了ステータス (exit-status)」と呼ばれるコマンドの成否を表す数値が特殊変数 $? に自動で設定される。一般的には、コマンド成功時には「0」、失敗時には「1」が設定される。exit コマンドに指定したパラメータ (0 もしくは 1~255 の正の整数値のみ可) が、そのシェルの終了ステータスとなる( bashの&&と||: みズとおかズ )。

パイプとawk、if文

ensure-environment() {
  
# 前半の処理は上のセクション

  FREE_MEMORY=$(grep MemTotal /proc/meminfo | awk '{print $2}')
  if [[ "$FREE_MEMORY" -lt 1003600 ]]; then
    echo "For dokku to build containers, it is strongly suggested that you have 1024 megabytes or more of free memory"
    echo "If necessary, please consult this document to setup swap: http://dokku.viewdocs.io/dokku/advanced-installation/#vms-with-less-than-1gb-of-memory"
  fi
}

grep 正規表現 ファイル名でファイル内の文字を検索し該当する行を抽出できる。/proc/meminfoは、カーネルが内部的に管理している枠組みでのメモリ情報を持っている。MemTotalカーネルが認識している全物理メモリを表す。
|(パイプ)はコマンドの入出力をコマンドへ引き渡す処理で、今回の場合はgrepで抽出した行MemTotal: 8069288 kBawkコマンドへ引き渡している。
awk 'パターン {アクション}' ファイル名で、テキストファイルを1行ずつ読み、パターンに合致した行に対して、アクションで指定された内容を実行する。パターンが指定されていない場合には、全ての行に対して処理を行う。テキストの各行を空白文字で区切って“フィールド”として処理するので、今回は1番目のフィールドがMemTotal:となり$1で表現できる。2番目のフィールドが8069288となり、これをprint(出力)している。

if文は、testを使用するもの、[(シングルブラケット)を使用するもの、[[(ダブルブラケット)を使用するものに分けられる。testを使用するのは直感的でないし他言語からくると若干困惑する。[(シングルブラケット)は、変数展開するときにダブルクォーテーションが必要であっったり、数値比較を行う場合には丸括弧で囲まなくてはいけないなどハマりポイントが多いため、[[(ダブルブラケット)を使用するべき。 test と [ と [[ コマンドの違い - 拡張 POSIX シェルスクリプト Advent Calendar 2013 - ダメ出し Blog

-lt比較演算子はless thanを意味しており、今回ではメモリが1003600kBより小さかった場合にメッセージを出力している。
ifの終了はfiキーワードを使用する。またthenキーワードは1行消費してしまい見にくいので条件文に;(セミコロン)をつけて続けて書いている。

case文

install-requirements() {
  echo "--> Ensuring we have the proper dependencies"

  case "$DOKKU_DISTRO" in
    debian|ubuntu)
      apt-get update -qq > /dev/null
      ;;
  esac
}

case文はcase 値 in 値1) 処理 ;; 値2) 処理 ;; esacと記述していく。この場合は、$DOKKU_DISTROdebianもしくはubuntuだった場合に、apt-get updateでパッケージ情報を更新している。-qqオプションを使用することによってエラー以外は表示しない。
パッケージをinstallする際に参照するインデックスファイルが存在しない可能性があるから?単純にパッケージリストを更新したいから?

dokkuのソースコードを追ってみる

install-dokku() {
  if [[ -n $DOKKU_BRANCH ]]; then
    install-dokku-from-source "origin/$DOKKU_BRANCH"
  elif [[ -n $DOKKU_TAG ]]; then
    local DOKKU_SEMVER="${DOKKU_TAG//v}"
    major=$(echo "$DOKKU_SEMVER" | awk '{split($0,a,"."); print a[1]}')
    minor=$(echo "$DOKKU_SEMVER" | awk '{split($0,a,"."); print a[2]}')
    patch=$(echo "$DOKKU_SEMVER" | awk '{split($0,a,"."); print a[3]}')

    use_plugin=false
    # 0.4.0 implemented a `plugin` plugin
    if [[ "$major" -eq "0" ]] && [[ "$minor" -ge "4" ]] && [[ "$patch" -ge "0" ]]; then
      use_plugin=true
    elif [[ "$major" -ge "1" ]]; then
      use_plugin=true
    fi

-n 文字列で文字列の長さが0より大きければ真となる。
sourceからdokkuのupgradeをする際に、DOKKU_BRANCHをmasterに設定することになっている( Dokku - The smallest PaaS implementation you've ever seen )。 設定されていた場合には、install-dokku-from-source関数が実行される。

通常インストールされる際には、以下のようなコマンドでインストールされる。
sudo DOKKU_TAG=v0.12.5 bash bootstrap.sh
この際にDOKKU_TAGに"v0.12.5"という値が入り条件が真となる。
${変数名//置換前文字列/置換後文字列}で置換前文字列に一致した全ての文字列が、置換される。今回の場合は"v"が排除される形となり、DOKKU_SEMVERには"0.12.5"という値が入る。
awkのsplit関数はsplit(分割する文字列,格納先の配列,分割文字列の指定)というような使い方をする。配列は(0, 12, 5)となりmajorには"0"、minorには"12"、patchには"5"の値が入る。

# install-dokku()の続き

    # 0.3.13 was the first version with a debian package
    if [[ "$major" -eq "0" ]] && [[ "$minor" -eq "3" ]] && [[ "$patch" -ge "13" ]]; then
      install-dokku-from-package "$DOKKU_SEMVER"
      echo "--> Running post-install dependency installation"
      dokku plugins-install-dependencies
    elif [[ "$use_plugin" == "true" ]]; then
      install-dokku-from-package "$DOKKU_SEMVER"
      echo "--> Running post-install dependency installation"
      sudo -E dokku plugin:install-dependencies --core
    else
      install-dokku-from-source "$DOKKU_TAG"
    fi
  else
    install-dokku-from-package
    echo "--> Running post-install dependency installation"
    sudo -E dokku plugin:install-dependencies --core
  fi
}

Effective Java 読書メモ 2章

英語版のEffective Java 第3版が出ていたので、日本語版が出るまでの繋ぎに。 もし読み終わっても出る気配が無かったら英語版買ってなんとか解読するしか。

コンストラクタの代わりにstaticファクトリーメソッドを活用する

コンストラクタが複数で、渡す引数によって動きが異なる場合には実装すべきと思う。メソッドの戻り値の型の任意のサブタイプで返せるが、そのサブタイプのコンストラクタはpublicもしくはprotectedでなくてはならない。

pros

  • コンストラクタと違い名前を持てること
  • メソッドが呼び出される毎に新たなオブジェクトを生成する必要なし
  • メソッドの戻り値の型の任意のサブタイプで返せる
  • パラメータ化された方のインスタンス生成の面倒さを低減できる

cons

  • public/protedctedのコンストラクタを持たないサブタイプは作れない
  • 他のstaticメソッドと区別しにくい
// 利用する側
char[] array = new char[] { 'H', 'A', 'T', 'E', 'N', 'A' };
String str1 = new String(array)  // コンストラクタの場合
String str2 = String.valueOf(array)  // staticメソッドの場合

// 利用される側(jdk8)
public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
}
public static String valueOf(char data[]) {
        return new String(data);
}

数多くのコンストラクタパラメータに直面した時にはビルダーを検討する

個人的には利用側がスマートになって素敵だと思った。パラメータが多くなる場合は、まずクラス設計を見直すべきだと思うが、仕方なくパラメータを増やさざるを得ない場合には良いのでは。

// 利用側のコード
public class Main
{
  // arguments are passed using the text field below this editor
  public static void main(String[] args)
  {
    Contact contact = new Contact.Builder("Satoshi", "Matsuda", LocalDateTime.now()).address("東京都千代田区××町3−24").age(27).build();
  }
}

// 利用される側のコード
class Contact
{
  private final String firstName;
  private final String lastName;
  private final String address;
  private final int age;
  private final LocalDateTime contactDate;
  
  static class Builder {
    
    // 必須パラメータ
    private final String firstName;
    private final String lastName;
    private final LocalDateTime contactDate;
    
    // オプションパラメータ
    private String address = ""; // String.emptyってJavaには無いんだね・・・
    private int age = 0;  
    
    public Builder(String firstName,  String lastName, LocalDateTime contactDate){
      this.firstName = firstName;
      this.lastName = lastName;
      this.contactDate = contactDate;
    }
    
    public Builder address(String val){
        address = val;
        return this;
    }
    
    public Builder age(int val){
        age = val;
        return this;
    }
    
    public Contact build(){
        return new Contact(this); // クラス内部ならprivateでも呼べる
    }
  }
  
  private Contact(Builder builder){
      firstName = builder.firstName;
      lastName = builder.lastName;
      address = builder.address;
      age = builder.age;
      contactDate = builder.contactDate;
  }
}

privateのコンストラクタかenum 型でシングルトン特性を強制する

コンストラクタのアクセス修飾子をprivateにすると初期化の時に1回だけ呼ばれる

singleton実装①
public finalによるシングルトン。単純で分かりやすい。

public class Main {
    public static void main(String[] args) throws Exception {
        Singleton obj1 = Singleton.INSTANCE;
    }
}

class Singleton {
    public static final Singleton INSTANCE = new Singleton();
    private Singleton(){ /* process */ }
}

singleton実装②
staticファクトリーメソッドによるシングルトン。結城先生のデザインパターン本にも載っている方法がこれ。

public class Main {
    public static void main(String[] args) throws Exception {
        Singleton obj1 = Singleton.getInstance(); // この時点でSingletonクラスの初期化が行われ、staticフィールドも初期化される
        Singleton obj2 = Singleton.getInstance();
        // Singleton obj3 = Singleton(); エラーが発生する
        
        System.out.println(obj1 == obj2); // output : true
    }
}

class Singleton {
    private static final Singleton SINGLETON = new Singleton();
    private Singleton(){ /* process */ }
    
    public static Singleton getInstance(){
        return SINGLETON;
    }
}

singleton実装③
上記の2つの方法だと、シングルトンのクラスをシリアライズするときには、"implements Serializable"を追加するだけではダメとのこと。全てのインスタンスフィールドをtransientと宣言し、readResolveメソッドを実装する必要がある。そうしない場合に、シリアライズされたインスタンスをディシリアライズするたびに、新たなインスタンスが生成されてしまうため、シングルトンを保証できない。transient修飾子は、シリアライズの対象から外すもの。
また、AccessibleObject.setAccesibleメソッドを利用してリフレクションにより、privateのコンストラクタを呼び出すことができてしまうため、シングルトンを保証できない。
Enumを使うとシリアライズするためにアレコレしなくてよくてスマート。

public class Main {
    public static void main(String[] args) throws Exception {
        Singleton.INSTANCE.saySingleton();  // singleton!!
    }
}

enum Singleton {
    INSTANCE;
    public void saySingleton(){
        System.out.println("singleton!!");
    }
}

privateのコンストラクタでインスタンス化不可能を強制する

コンパイラはデフォルトでpublicで引数なしのコンストラクタを提供しているが、明示的にprivateのコンストラクタを用意することでインスタンス化できないようにすることが可能。

不必要なオブジェクトの生成を避ける

staticファクトリーメソッドとコンストラクタの両方を提供している不変クラスの場合は、staticファクトリーメソッドを使用した方が、不必要なオブジェクトの生成を防げる。可変オブジェクトに関しても、メンバのインスタンス生成が一度でいい場合には、static初期化子を使用することを検討すること。
オートボクシング(auto boxing: プリミティブ型→ラッパー型)はパフォーマンスを悪くするので、注意すること。意図した上で変換するのであれば、明示的に行うこと。

廃れたオブジェクト参照を取り除く

クラスが独自にメモリを管理している場合には、例えば下記のようなEffecive JavaにあるようなStackクラスなどに関しては、メモリリークに注意する必要がある。

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack () {        
        this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push (Object e) {
        ensureCapacity();
        elements[size++] = e; // 参照先をセット
    }

    public Object pop () {
        if (size == 0) {
            throw new EmptyStackException();
        }
        return elements[size--]; 
  // 参照値を渡してスタックには残って無いように見えるが、実際はまだ参照値を持っている
  // 参照値を持っているが、もう参照されることは無い(=廃れた参照)
    }

    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}

以下のように明示的にnull参照を突っ込むことによって、Garbage Collectionが動いてくれる。

public Object pop () {
    if (size == 0) {
        throw new EmptyStackException();
    }

    Object result = elements[size--];
    elements[size] = null; // ここ!!
    return result;
}

クラスがメモリを独自管理している場合以外にも、キャッシュやリスナー・コールバックでメモリリークが起きる可能性は高くなる。ヒープの使用量とかデバッグしているときに意識できるといいよね的な感じだろうか。
HashMapを使用している場合には、WeakHashMap(WeakHashMap (Java Platform SE 8))で代替すると、使われることがなくなったキーが自動的に削除されてガベージコレクションの対象となります。

ファイナライザを避ける

C++プログラマJavaに移ってきたときに誤解しがちなデストラクタ=ファイナライザに対して「そんなことないぞ!!」と。try-finallyを使用して(DBとかファイルはtry-resourcesでしたっけ?)、明示的に終了メソッドを組み込めとのこと。安定してファイナライザは動作をしてくれない。

黒べこ本(kotlin)でSpring Boot入門メモ③ (DI・バリデーション)

DIとは

DIはDependency Injectionのことで日本語では依存性の注入と言われる。Springのドキュメントでは基本的には、類似の意味を持つIoC(Inversion of Control)という言葉が使われている。DIは、オブジェクトが依存関係を定義する処理となる。以下に挙げる方法でのみ、オブジェクトが依存する他のオブジェクトの定義を行う。

  • コンストラクタ引数 ex) val regex = Regex("Kotlin")
  • ファクトリメソッドの引数 ex) val mutableList = mutableListOf(1, 2, 3)
  • 返り値のオブジェクトインスタンスに設定されるプロパティ ex) person.age = 25

DIコンテナ

DIコンテナは、Beanを作成するときにそれらの依存関係を注入する。BeanとはSpring IoC コンテナによって管理されるオブジェクトのことです。コンテナによって管理されるオブジェクトとしては以下のものが例としてあげられる。

  • ViewResolverのBean (Controllerの戻り値の文字列を解決してViewを戻す処理を行う)
  • ValidatorのBean
  • DataSourceのBean
  • トランザクションマネージャのBean
  • 型変換のBean
  • Controller・Service・Repositoryなどの業務ロジックに関係したBean

DIコンテナは、ConfigurationにしたがってBeanを作成し、IDを割り振って管理する。インタフェースorg.springframework.context.ApplicationContextがSpring IoCコンテナを表している。

Beanの定義

Configurationの設定の仕方は以下の3種類ある。

<bean id="..." class="...">
        <!-- collaborators and configuration for this bean go here -->
</bean>
  • JavaConfig
@Configuration
public class AppConfig {

    @Bean
    public MyService myService() {
        return new MyServiceImpl();
    }
}

インスタンスをBeanとしたクラスに@Componentもしくは@Componentをベースにしたアノテーションを付与する(@Controller / @Service / @Repository / @Configuration)
Java.configに@ComponentScan(basePackages = {"hoge.service.impl"})を付与する。ここで指定したベースパッケージ配下のアノテーションをチェックする。
@Autowiredをインスタンス変数に付与することでコンテナ内から、インジェクションする対象となる。

@Controller
@RequestMapping("houses")
class HouseController {

    @Autowired
    private lateinit var shs: HouseFactoryImpl

@Autowiredの使い方

@Autowiredを使用したDependency Injectionの種類は以下の3種類有ります。記述が簡易なフィールド・インジェクションが使用されることが多いと思われます。ただフィールド・インジェクションは、Spring FrameworkのDIコンテナに依存してしまう点と設計の妥当性(依存関係が多くなり過ぎていないか、循環依存になっていないか)が分かりづらいということが有り、必須の依存関係でコンストラクタ・インジェクション、オプションの依存関係をセッター・インジェクションを使用する方がアプリが大規模になる場合に有効であると言えます。

  • コンストラクタ・インジェクション
class Foo @Autowired constructor(dependency: MyDependency, service: MyService) {
    //...
}

ただし、Spring4.3以降でコンストラクタが単一の場合は@Autowiredが省略できるため、以下のようになります

class Foo(dependency: MyDependency, service: MyService) {
    //...
}
  • フィールド・インジェクション
    lateinitはプロパティを遅れて初期化することをコンパイラに知らせるものです。 バッキングフィールドを持つプロパティは初期化が必要となるはずですが、lateinitを付けることでプロパティ宣言時の初期化を避けることが出来ます。
class Foo {
   @Autowired
   private lateinit var dependency: MyDependency
   @Autowired
   private lateinit var service: MyService
}
  • セッターインジェクション
    kotlinでは無し? → 調査中

バリデーション

Formのクラスを作成し、そのクラスのフィールドにアノテーションを付与することで、単項目チェックを行うことができる。Formのクラスは、HTMLなどから受け取る送信formに対応するもの。アノテーションは、Bean Validationの実装であるhibernate validatorを利用する。Bean Validationは値検証のためのJavaの仕様の一つで、hibernate validatorはそのリファレンス実装となっている。 相関チェックは、Validatorインターフェースを実装したチェック用のクラスを作成し、Controllerにインジェクションします。

単項目チェック

class TaskCreateForm {

    @NotBlank
    @Size(max = 20)
    var content: String? = null
}

仕様頻度が高そうなアノテーションは以下

  • @Length(min=, max=) //文字列長の検査
  • @Max(value=) //数値の最大値の検査
  • @Min(value=) //数値の最小値の検査
  • @NotNull //値がNullでないか
  • @NotEmpty //文字列がNullまたは空文字で無いか
  • @Pattern(regex="regexp", flag=) //正規表現に一致するか否か

以下のようにメソッドの引数としてとったFormにアノテーション@Validatedをつけ、そのすぐ次の引数にBindingResultを取るようにすると、バリデーションが施され、その結果がBindingResultオブジェクトとして表現される。BindingResultオブジェクトのhaserror()メソッドを使用することで検査の結果を得ることが出来ます。

class TaskController(private val taskRepository: JdbcTaskRepository) {

    @PostMapping("")
    fun create(@Validated form: TaskCreateForm,
               bindingResult: BindingResult): String {
        if(bindingResult.hasErrors())
            return "tasks/new"

        val content = requireNotNull(form.content)
        taskRepository.create(content)
        return "redirect:/tasks"
    }

}

次は例外処理周りか永続化について書きます。


参考記事

SpringでField InjectionよりConstructor Injectionが推奨される理由 - abcdefg.....

[随時更新]SpringBoot(with Thymeleaf)チートシート - Qiita

Validator (Java Platform SE 8)

黒べこ本(kotlin)でSpring Boot入門メモ② (Controller・ざっくりThymeleaf)

黒べこ本では、"Hello World"でREST APIのController・"ToDoアプリ"でMVCのControllerを書くことになっています。 Controllerに着目して書いていきます。また、Viewを返す後者で使用されるテンプレートエンジンであるThymeleafも少しだけ知っておこうと思います。

Spring Boot での Controller

そもそもMVCってなんだっけ

ウェブアプリの設計においてポピュラーな選択肢であるModel View Controller (MVC) のこと。アプリケーションロジックを3つに分割することで再利用性や柔軟性を高めている。

例えば、簡単なショッピングリストアプリを作成すると考えてみる。今週買う必要のある商品の名前・数量・価格のみを表示してくれる。

f:id:jrywm121:20180313004328p:plain

Model

Modelはリストの商品データと既に存在するリストを実装する。

View

Viewはリストをユーザに対してどのように見せるかを定義する。モデルから表示するデータを受け取る。

Controller

アプリからの入力を受け取って、それに応じてModelやViewを更新するロジック。 例えば、ショッピングリストアプリには入力フォームや追加・削除ボタンが有ります。これらのアクションにより、Modelのアップデートが必要となります。入力情報は一度、Controllerに送られます。その後、Modelの操作が行われ、更新されたデータがViewへ送られます。 また、Viewを更新し別のデータ形式(アルファベットを昇順→降順 / 価格順に並び替える)にすることもあるでしょう。この場合は、modelへのupdateを必要とせず、直接Controllerがハンドルします。

※ 内容書いてから気づきましたが、MDNを参照したので若干Webフロントエンドよりの解釈となっています。

実際のJavaアプリーションだと以下のような構成になると思われます。 これから始めるSpringのwebアプリケーションの7スライド目を引用 f:id:jrywm121:20180313012920j:plain

Controller関係のアノテーション

Javaが標準で提供しているWEBアプリ(ex: Servlet, JDBC)はプログラムや設定ファイルの記述が面倒。確かに新人研修の時にやったJava EEServletJSPxmlに記述したり、冗長な記述でDispatchしていたような記憶がかすかに有ります。

SpringはJava標準APIをラップして簡単にしてくれる。Configurationの方法はアノテーションXMLJava Configが有りますが、Controller / Service / Dao はアノテーションという手段を取るのが見通し良くデメリットも小さいらしい。 @Controllerコンポーネントと@RestControllerコンポーネントでは、アノテーションを使用してリクエスマッピング、リクエスト入力、例外処理などを表現できる(引用元)。

黒べこ本ではToDoリストのアプリで以下のようにControllerを記述しました(一部分のみです)。

@Controller
@RequestMapping("tasks")
class TaskController(private val taskRepository: InMemoryTaskRepository) {

    @PostMapping("")
    fun create(@Validated form: TaskCreateForm,
               bindingResult: BindingResult): String {
        if(bindingResult.hasErrors())
            return "tasks/new"

        val content = requireNotNull(form.content)
        taskRepository.create(content)
        return "redirect:/tasks"
    }

    @GetMapping("{id}/edit")
    fun edit(@PathVariable("id") id: Long,
             form: TaskUpdateForm): String {
        val task = taskRepository.findById(id) ?: throw NotFoundException()
        form.content = task.content
        form.done = task.done
        return "tasks/edit"
    }
@Controller

このアノテーションは、@Componentと同じようにコンテナで管理するように指定するもの。Beanとして扱われ、@AutowiredアノテーションがControllerの使用者側に付けられることによってInjectionされる。

@RequestMapping

RequestをController内のメソッドにマッピングするのに使用されます。URL、HTTPメソッド、要求パラメータ、ヘッダー、およびメディアタイプによって一致するさまざまな属性があります。これをクラスレベルで使用して共有マッピングを表現したり、メソッドレベルで特定のエンドポイントマッピングに絞り込むことができます。

@GetMapping @PostMapping @PutMapping @DeleteMapping @PatchMapping

@RequestMappingとHTTPメソッドの組み合わせ。()内で指定するURI正規表現でも書ける。引数を取らない場合は()内にパスの文字列のみを記述すれば良いが、引数を2つ以上取る場合には、パラメータとなる変数名を記述しなくてはいけない。
path: エンドポイント / consumes: Content-Type / produces: Accept / headers: request header / params: request parameter

@PostMapping(path = "/pets", consumes = "application/json", produces = "application/json;charset=UTF-8", headers = "myHeader=myValue",  params = "myParam=myValue")
@PathVariable

pathの中で{ }で定義した変数には、このアノテーションを使用することでアクセス出来ます。URI変数はString変数でない場合には、自動的に適切な型に変換されるか、 もし出来ない場合にはTypeMismatchExceptionが発生します。int, long, Dateはデフォルトでサポートされています。

@Validated

このアノテーションが付与された引数は、標準のBean検査が行われ、もし検査にひっかかってしまった場合にはデフォルトでは400(Bad Request)を返すようにしている。BindingResult型の引数を取ることで、ローカルで処理をすることも可能。

redirect:(Path)

指定されたパスにリダイレクトする。

ハンドルに関するアノテーション
  • @RequestParam : リクエストパラメータの値を取る
  • @RequestHeader : リクエストヘッダーの値を取る
  • @CookieValue : Cookieの値を取る
  • @ModelAttribute : モデル(インスタンス)の値を取る。既にModelメソッドで追加されているモデルから もしくは @SessionAttributes経由のHTTPセッションから もしくは デフォルトのコンストラクタの呼び出しから 等々
  • @SessionAttribute : 既存のセッション属性にアクセスする
  • @RequestAttribute : 作成された既存のリクエスト属性にアクセス
  • @RequestBody : リクエストボディにアクセス
  • @ResponseBody : 関数の戻り値をシリアライズします。@ResponseBody + @Controller = @RestController

戻り値がString型だけど・・・

ビューの論理名からビューの物理名を解決する。 "tasks/new" → "/resources/templates/tasks/new.html" Thymeleaf用のViewResolverクラスがあり、それが名前解決をしてくれる。

Viewに相当するThymeleaf

Thymeleafとは

ThymeleafはJavaのテンプレートエンジンライブラリ。XML/XHTML/HTML5で書かれたテンプレートを変換して、アプリケーションのデータやテキストを表示することができる。JSPの代替技術として近年注目されている

Thymeleafの基本

黒べこ本の中で書くコードは以下となります。

<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>タスク一覧</title>
    <link rel="stylesheet" href="webjars/bootstrap/4.0.0-2/css/bootstrap.min.css">
</head>
<body>
<a th:href="@{tasks/new}">作成</a>
<p th:if="${tasks.isEmpty()}">タスクがありません</p>
<ul th:unless="${tasks.isEmpty()}">
    <li th:each="task: ${tasks}">
        <a th:href="@{tasks/{id}/edit(id=${task.id})}">
            <span th:unless="${task.done}" th:text="${task.content}"></span>
            <s th:if="${task.done}" th:text="${task.content}"></s>
        </a>
    </li>
</ul>
</body>
</html>
@{...} : リンク式

/で始めると、レスポンスされるhtmlにはコンテキストパスが補完されます。コンテキストパスは絶対パスで記述する際に必要となるもので、Applicationサーバの資源の位置を表す。相対パスで指定する場合は環境によって上手く通らないケースが出てくるため危険。

${...} : 変数式

コンテキストのModelにaddされているインスタンスを指定して展開することが出来ます。事前にController側で以下のコードのように、してあげる必要があります。

    @GetMapping("")
    fun index(model: Model): String {
        val tasks = taskRepository.findAll()
        // 直下の部分。第一引数に取っている名前でView側は参照する。
        model.addAttribute("tasks", tasks)
        return "tasks/index"
    }
th:if=${condition} th:unless=${condition}

conditionが正であれば、if記述のブロックはActiveとなる。 conditionが負であれば、unless記述のブロックはActiveとなる。

th:text

サーバーで実行した時は、th:text属性を指定したタグに挟まれた部分が置き換えられます。htmlで直書きしている内容は上書かれるので、基本的にはモックとして扱われる際のデフォルト値

@{path1/{変数}/path2(変数=代入値}

パスに変数を埋め込む

黒べこ本では先ほど挙げたものの他にこのようなコードも書きます

<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>タスク作成</title>
</head>
<body>
<form th:method="post" th:action="@{./}" th:object="${taskCreateForm}">
    <div>
        <label>
            内容:<input type="text" th:field="*{content}">
        </label>
        <p th:if="${#fields.hasErrors('content')}" th:errors="*{content}">エラーメッセージ</p>
    </div>
    <div>
        <input type="submit" value="作成" />
    </div>
</form>
</body>
</html>
*{...} : 選択変数式

上記のコードでは、*{content}は本来${taskCreateForm.content}と記述しなくてはいけないのですが、なんども同じような記述が生まれてしまう状況が起きやすいです。そのため簡略化して書けるよう選択変数式があります。親要素でth:object="${taskCreateForm}"と宣言されていれば、*{プロパティ名}で子要素はコンテキストにアクセスすることが可能です。

#fields

#を先頭に付けたものはユーティリティメソッドと呼ばれるもので他にはdateやcalender、sessionに保管されている値を参照するものなどがある。この#fieldsは、Validate後の各プロパティで発生したエラーメッセージを参照している。${#fields.errors('【プロパティ名】')}でアクセスできる。結果はリストになっているので、ループで回せば各エラーメッセージを取得できる。

次回はDIとValidateについてメモしていこうと思います。


参考にさせていただきましたサイト様

MVC architecture - App Center | MDN

これから始めるSpringのwebアプリケーション

Springを何となく使ってる人が抑えるべきポイント

Web on Servlet Stack

Tutorial: Using Thymeleaf (ja)

必要最小限のサンプルでThymeleafを完全マスター - Java EE 事始め!

Spring Boot で Thymeleaf 使い方メモ - Qiita

黒べこ本(kotlin)でSpring Boot入門メモ① (Spring Boot概要・ざっくりGradle)

春が来たのでSpring Bootに入門しました。

今まではC#を書いていたのですが、次の仕事からJavaを使うことになりそうだったので、アクロ本結城浩さんのJava言語で学ぶデザインパターン入門で勉強しています。デザインパターン本の方は、言語仕様が古かったりするんですが、Java8以降ならこう書けるなというものを見つけていく作業が楽しかったりします。

その流れでSpringも勉強しようと思ったのですが、どうせならJavaと一緒にkotlinも勉強したいと思っていたので、いっそのことkotlinでSpringに入門するのもありかもと。ちょうどそのころに、書店でKotlin Webアプリケーション 新しいサーバサイドプログラミングを見つけて購入し、kotlinでSpring入門することとなりました。 ちなみにサーバサイドkotlinに興味を持ったのは、engineer meeting podcastのvol.104で既にkotlinで書かれたサービスがいくつか動いていることを知ったからです。

Spring Bootとは

Spring Bootを理解するためにSpringは避けて通れない

SpringSpring Frameworkをベースとしたプロジェクトの集合体

From configuration to security, web apps to big data – whatever the infrastructure needs of your application may be, there is a Spring Project to help you build it. Start small and use just what you need – Spring is modular by design.(公式サイトより)

Configurationからセキュリティ、ウェブアプリ、ビッグデータまでアプリケーションのinfrastructureに必要なものはSpringのプロジェクトで用意することができる。Springはモジュールごとに分かれているため、使いたい機能だけを選んで小さく始めることができる。

SpringのベースとなっているSpring Framework

Spring FrameworkはDIコンテナを主に特徴にあげられる様々なレイヤに対するフレームワークコンポーネントの集まり

Core support for dependency injection, transaction management, web applications, data access, messaging, testing and more.(公式サイトより)

Spring Bootの立ち位置

Spring Bootは、Spring Frameworkの持つ設定する項目が多すぎて本来の開発に集中できないという問題点を解決するため、Spring FrameworkにCoCを導入したもの。CoC = Convention over Configurationのこと。「設定より規約」ということで規約どおりなら無駄な設定ファイルを省略できるようにすることで無駄な設定を少なくする。 では、Springにおける設定ファイルとはなにか → Bean(コンテナ内のインスタンス)の設定。

Beanは大きく分けて2種類有る。

Spring Bootでは前者があらかじめ定義済みとなっている。 まとめると以下のような感じ。

Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can "just run". (公式サイトより)

"動かせる"ProductionレベルのSpringをベースとしたアプリケーションを簡単に作ることが出来る。

Spring Initializrとは

Spring Initializrは、Spring Bootの雛形を作ってくれるそう。ビルドツールや言語、バージョンによって変えなければいけない設定などで苦労することがないため、よりスピーディーに開発出来る。
ページ下部の"switch to the full version"を選択することで、パッケージングやJavaのバージョンの選択を行うことが出来る。また、依存ライブラリを一覧から選択することも可能となる。依存ライブラリの中で、赤文字になっているものは、選択しているSpring Bootのバージョンをサポートするバージョンのライブラリが無いこと示している。

入力項目

  • Group : パッケージ名(通例ドメイン名を逆にしたものから始まる)
  • Artifact : プロジェクト名
  • Dependencies : 開発に必要なコンポーネントを選ぶ

Gradleとは

Gradleはビルドツール

ビルドツールは以下のことをやってくれる。Java製アプリを Eclipse から実行したことしかない新人に「ビルドツールとは?」を説明してみる…そして CI へより引用

  • ビルドを構成するタスクを定義し,それを実行する.例えば以下の様なタスクを行う.
    - ソースファイルのコンパイル
    - テストや静的解析の実行,およびその結果のレポート出力
    - リリース用アプリケーションイメージの構築
    
  • ビルドを構成するタスク間の依存関係を管理し,そのタスクのインプットとなる上流タスクの結果やソースコードなどの変更有無からタスク- の実行有無を判断する.
  • ライブラリの依存関係を管理する.(これを行うことに特化した「パッケージマネージャー」というカテゴリに属するツールもあります.有名なのは JavaScript の npm や,RubyRubyGems など.)
  • タスクのインプットとなるファイルを監視し,変更があった場合に自動的にタスクを実行する. (Grunt における watch や,Gradle における continuous build など)

Angular開発におけるwebpack + npm的存在ではないでしょうか。

Gradleの特徴とは

GroovyベースのDSLを用いたビルドスクリプトJavaのビルドツールとしては他にAntやMavenなどが有名ですが、それらはxmlベースのため大きく違う点と言えます。 Groovyは、Javaプラットフォーム上で動作する動的プログラミング言語です。DSLDomain Specific Language(ドメイン固有言語)の略で、メタプログラミングで使われます。 メタプログラミングはロジックを直接コーディングするのではなく,あるパターンをもったロジックを生成する高位ロジックを定義する方法(Wikipediaより)のようです。

Gradle has been counted in the top 20 open-source projects and is trusted by millions of developers to build software for billions of people

LinkedInやNETFLIXなどの大きな企業からも信頼を得ているようです。

Gradleの設定ファイルの構成

Gradleの設定ファイルはbuild.gradleです。 黒べこ本の手順で作成されたbuild.Gradleは以下です。コメントでそれぞれに注釈を付けました。

// build.gradle自体が依存するライブラリの設定を行なっています
buildscript {
    // ext ブロックは変数を定義するためのブロック
    ext {
        kotlinVersion = '1.2.20'
        springBootVersion = '2.0.0.RELEASE'
    }
    // repositories ブロックは依存性を解決する際に、Mavenリポジトリとしてアクセスする先を指定する
    repositories {
        mavenCentral()
    }
    // dependencies ブロックは依存ライブラリを指定するブロック
    dependencies {
        // groupId : artifactId : version
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
        classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}")
    }
}

// ビルド設定で利用するプラグインの宣言
apply plugin: 'kotlin'
apply plugin: 'kotlin-spring'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

// プロジェクト自体のバージョンや依存するJavaのバージョン
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

// Production Codeのコンパイル
compileKotlin {
    // kotlinOptionsで追加のコンパイルオプションを指定する
    kotlinOptions {
        freeCompilerArgs = ["-Xjsr305=strict"]
        jvmTarget = "1.8"
    }
}
// Test codeのコンパイル
compileTestKotlin {
    kotlinOptions {
        // freeCompilerArgs:追加のコンパイル引数リスト
        // JSR-305 annotation によるNullチェックをErrorとして出力
        freeCompilerArgs = ["-Xjsr305=strict"]
        jvmTarget = "1.8"
    }
}

repositories {
    mavenCentral()
}


dependencies {
    compile('org.springframework.boot:spring-boot-starter-thymeleaf')
    compile('org.springframework.boot:spring-boot-starter-web')
    compile('com.fasterxml.jackson.module:jackson-module-kotlin')
    compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    compile("org.jetbrains.kotlin:kotlin-reflect")
    compile('org.springframework.boot:spring-boot-starter-jdbc')
    compile('org.webjars:bootstrap:4.0.0-2')
    testCompile('org.springframework.boot:spring-boot-starter-test')
    testCompile('org.assertj:assertj-core:3.8.0')
    // runtime:プロダクトのクラスを実行するときに必要になる依存関係
    runtime('com.h2database:h2')
}

次回はSpringの簡単な動きとThymeleafについて整理したいと思います。


参考にさせていただきましたサイト様

Spring Bootの本当の理解ポイント #jjug

第1回 DSLとは?:今そこにある“DSL”|gihyo.jp … 技術評論社

これから始めるSpringのwebアプリケーション

Gradle初心者によるGradle事始め - Qiita

Gradle を始めてみよう - Qiita

Kotlin 1.1.50 is out | Kotlin Blog