SIerエンジニアが初めてのWebサイト作成〜自治会サイトのマイグレーション〜

記事の内容

SIerエンジニア(34)が初めてのWebサイト作成を行った体験録。
読んで得することはない。

経緯

社長から社長が個人で保守している自治会サイト(≠自治体サイト)のマイグレーションをしたいと飲み会で発言。
サイト自体のマイグレーションは多いに賛成するところであり、私のスキルの棚卸しとしてもチャレンジしやすい案件と思い受けた。

私のスキルセット

  • Javaプログラミング7,8年経験
  • Reactプログラミング3ヶ月経験(SIで経験できている人は少数かと)
  • 業務中、暇なときにKotlin, Haskell, Rustの勉強をしている
  • 過去にウォーターフォール全工程、その後の保守運用を経験済み

私の環境

  • 土日に軽くコーディングできるくらいの時間は取れる
  • 平日夜はコーディングほぼ不可能
  • 2020/4はニート

システム概念図

f:id:metatrading:20200503123515p:plain
システム概念図

実際の構成

サーバ

さくらのVPS
最初はさくらのレンタルサーバを借りたが、ある程度パッケージ化されたものが提供されることを知らず解約・・・。

フロント構成

フロントは、自治会員用の公開サイトと、運営者用の管理者サイトを作成した。

言語

TypeScript

パッケージマネージャ

webpack

フレームワーク

React および React Hooks

ライブラリ
ライブラリ 目的
dotenv 環境変数制御用
react-router ルーティング用
react-table テーブル表示用
material-ui cssフレームワーク
material-io cssフレームワーク
date-picker 日付選択UIライブラリ
marked マークダウン to HTML
外部サービス

firebase authentication
フロントのログイン時に利用。

エディタ

Intellij

バックエンド構成

言語

Kotlin

フロントとの通信方式

REST API

パッケージマネージャ

Gradle

フレームワーク

Spring Boot

ライブラリ
ライブラリ 目的
spring-boot-starter-web RESTエントリポイント作成
spring-boot-starter-data-jpa DBアクセス作成
flyway DBマイグレーション
firebase-admin ログイン後の認証トークン検証
de.dentrassi.crypto:pem-keystore クライアントサイドからのHTTPS通信対応用。Let's Encryptに対応する

外部サービス

firebase authentication

ログイン後、フロントからサーバサイド処理を呼び出された際の認証トークン検証。

情報共有

Stock

 メモ類の管理に使った。
 正直、機能面が弱いのとエディタの書き心地が悪かった。
 マークダウン対応していないのも微妙。
 後発にしては強みがどこなのかわからなかった。
 

Slack

 チャット。個人開発ゆえ、あまり使わなかった。
 使ったことがなかったので使ってみた感。

GitHub

 コード共有。

開発の流れ

超ざっくり要件定義

社長と会話して要件定義を図る。
当初スコープは、現行踏襲+便利機能もろもろ追加(pwaとか備品管理したいとか夢がいっぱい語られた)。

正直、細かい点をどうしたいという希望が出てこなかったので若干の不安を抱いた。
この時点では、自分自身、開発を甘く見ていたのでいろいろ夢を語った。

プロジェクト数

  • フロント(外部公開サイト)
  • 管理サイト
  • サーバサイド
  • 移行用

 現行サイトの記事の移行プロジェクト。
 記事の取り込み、マークダウンへの変換を実施。

難所

ミドルウェア全般

メールサーバ構築 困った度:★★★★★

安請け合いしたメールサーバが地獄だった。
どこかで躓くと死ねる。
反省点は、/var/log/maillogや/var/log/messagesを見てから対応するのが基本である。
コストを考えると、AWSで調達したほうがよかった気がする。
構成は
Webクライアント:Rainloop
メールソフト:postfix, dovecot

hugoインストール 困った度:★★★★★

go製のブログフレームワークであるhugoをインストール。
makeするなどの手順があり、あまり経験がなく難航した。

SSLワイルドカード証明書インストール 困った度:★★★☆☆

Let's Encriptを用いた証明書インストールでマルチドメインかつワイルドカードドメインの登録をした。
苦労したが、慣れの問題だったかな。

やったのは、Apacheへの導入、SpringBootへの導入、メールサーバへの導入などなど。

実装面

raect-table 困った度:★★★★★

Typescriptでコーディングしていたが、react-tableをTypescriptで使うには難易度が高かった。
useSortby, usePaginationなどのpluginを使う場合、型定義ファイルを差し替える必要がある。
この情報を理解するのに手間取った。
正直型定義ファイルを再作成するのは苦痛だった。

CORS 困った度:★★☆☆☆

クライアントからSpring Bootへのリクエストが延々と拒否される。
根本原因と表出したエラーがうまく噛み合っていなくて混乱した。
Webでよく見る事例を行っていれば普通は出来る。

環境別ビルド 困った度:★☆☆☆☆

webpackやSpringBootにおける環境別ビルドの制御。

レスポンシブ 困った度:★★★☆☆

cssって一つの言語レベル。。。

Spring Data JPA 困った度:★★★★★

@OneToOneの際に親側に外部キーを持たせないといけない点を知らず詰まった。

全体的に

いろいろな問題が頻発したが、複合的な問題となっているケースもあり、
過去の障害切り分けの経験が生きた。

楽しかったところ

全体像の設計

Webサイト全体のアーキテクチャから考える経験は無かったので、かなり楽しかった。
どうやって見せるか、どうやって記事を編集するか、など。

react hooks

useContextによるエラー制御やログインユーザの保持。
useStateによる状態管理。
hooksという仕組みの制限が見通しの良さをある程度要求してくれた。

簡易ブログ記述画面

プレビュー付きマークダウンエディタを用意できた。
気持ちいい。
テキストエリアにファイルをドラッグアンドドロップすると、ファイルのアップロードが行われ、
マークダウン用の参照パスがエディタ上に挿入される。

反省点

事前検討のヌルさ

さくらのレンタルサーバの仕様を正しく調査していなかった

さすがにroot権限ないのに借りるのは、ザル過ぎ。

メールサーバ構築

AWSにすればよかったと思う。

Typescriptのジェネリクス

ジェネリクスを活かしたインタフェース定義や共通化をあまり出来ていなかった。

ポエム

フロントエンド実装を伴う開発を初めて一気通貫でやってみたが、
使えるライブラリがとても多くあったし、現在進行形で日々よく出来たライブラリに出会っている。
技術のコモディティ化を身を持って体感した。
より、何を作るのか?が問われる時代が来ている。

まとめ

システム全体のライフサイクル含めて完成したときの気持ちよさはすごい。

Javaの反変・共変

反変・共変

共変とは

型の順序関係を維持する (≤ で順序づけたとき、特殊から一般の順になる) とき、共変である (covariant) という。 wiki

反変とは

型の順序関係を反転させる (≤ で順序づけたとき、一般から特殊の順になる) とき、反変である (contravariant) という。 wiki

不変とは

上記いずれにも該当しないとき、不変である (invariant) という。 wiki

Javaによる例示

の前に、Javaジェネリクス型以外は共変である。
が、ジェネリクスにおいては不変である。

共変(特殊→一般という変換例)

通常の型

f:id:metatrading:20200407114031p:plain

// 通常の型での共変
Object object = new Object();
String mojiretsu = "あああ";
object = mojiretsu; // 共変により代入可能

配列

// 配列
String[] 特殊 = new String[]{""};
Object[] 一般 = new Object[]{1};

一般 = 特殊; // 共変により代入可能

ジェネリクス

List<Object> 一般 = new ArrayList<>();
List<String> 特殊 = new ArrayList<>();

一般 = 特殊; // コンパイルエラー(ジェネリクスは不変のため)

反変(一般→特殊)

通常の型

Object object = new Object();
String mojiretsu = "あああ";
mojiretsu = object; // コンパイルエラー(反変のため)

配列

String[] 特殊 = new String[]{""};
Object[] 一般 = new Object[]{1};

特殊 = 一般;// コンパイルエラー(反変のため)

ジェネリクス

List<Object> 一般 = new ArrayList<>();
List<String> 特殊 = new ArrayList<>();

特殊 = 一般; // コンパイルエラー(ジェネリクスは不変のため)

境界型パラメータ

前提として、
Animalクラス これを継承したDogクラスやCatクラスがあるものとする。

上限境界ワイルドカード

上限境界は、ジェネリクスに共変をもたらすために用いる。
共変つまり、ジェネリクスのリスト(List<? extends Animal>に共変性(特殊→一般)をもたらす。

List<? extends Animal> animalList = new ArrayList<>();
List<Dog> dogList = new ArrayList<>();
animalList = dogList;

animalListに共変性がもたらされているため、変数としてdogListの代入(特殊→一般)が可能になっている。

下限境界ワイルドカード

下限境界は、ジェネリクスに反変をもたらすために用いる。

List<? super Dog> dogList = new ArrayList<>();
List<Animal> animalList = new ArrayList<>();
dogList = animalList;

dogListに反変性がもたらされているため、変数としてdogListにanimalListの代入(一般→特殊)が可能になっている。

上限境界ワイルドカードの使い所

メソッド引数型

特化した要素を持つListや汎化した要素を持つList受け取る。 そして、汎化したクラスで展開できる。

private void kyouhenAgrs(List<? extends Animal> animalList){
    animalList.forEach(e->{
        // Animal型として展開。
    });
}

これはこういった形でAnimalやその特化した要素を持つListで呼び出せる。

kyouhenAgrs(new ArrayList<Dog>());
kyouhenAgrs(new ArrayList<Cat>());
kyouhenAgrs(new ArrayList<Animal>());

メソッド返却型

特化したListを同じ表現で返却できる。

private List<? extends Animal> kyohenReturnDog(){
    return new ArrayList<Dog>();
}
private List<? extends Animal> kyohenReturnCat(){
    return new ArrayList<Cat>();
}
List<Animal> animalList = new ArrayList<>();
List<? extends Animal> dogList = kyohenReturnDog();
List<? extends Animal> catList = kyohenReturnCat();

// 統一的に扱えるため、集約ができる。
animalList.addAll(dogList);
animalList.addAll(catList);

できないこと

パラメータ化された型を引数に取るメソッド呼び出しは不可能。
? extends Animalに相当する型を引数に取るものは呼び出せない。

List<? extends Animal> animalList = new ArrayList<>();
// すべてパラメータ化された型を引数に取る
animalList.add(new Animal()); // コンパイルエラー
animalList.add(new Dog()); // コンパイルエラー
animalList.add(new Cat()); // コンパイルエラー
animalList.add(new Object()); // Objectですらコンパイルエラー

List#addは

boolean add(E e);

であり、
Listは

public interface List<E> extends Collection<E> {
}

であるため、addで受け取るEとは、パラメータ化された型である。

下限境界ワイルドカードの使い所

Animal ->Dogの継承関係では、 List<? super Dog> は、Dogを下限とするListである。
継承で見るとこんな感じでDogより上の関係をすべて内包可能とする。 つまり、Listには、Dog, Animalそして、すべての祖であるObjectが入りうるが、確定できるのはObjectであることだけである。
ということで、取り出し時は、Object型でしか取り出せないということである。
f:id:metatrading:20200407114103p:plain

メソッド引数型

private void callHanpenArgsDog(){
    // List<Dog>, List<Animal>, List<Object>というDogの親以上を
    // 型パラメータに持つListを引数にできる。
    hanpenArgsDog(new ArrayList<Dog>());
    hanpenArgsDog(new ArrayList<Animal>());
    hanpenArgsDog(new ArrayList<Object>());
}

private void hanpenArgsDog(List<? super Dog> dogList) {
    dogList.forEach(e -> {
        // 取りだしはObjectになる。
    });
}

メソッド返却型

上で述べたように<? super T>は、Objectであることしか確定できないため、
取り出し時はすべてObjectになる。
返却型として指定するメリットは無いように思われる。
※後述する「上限付きクラスと下限付き変数型の組み合わせ」の項ではObject以外に確定することができるので、そちらを活用すれば返却型に定義する意味合いが出てくる。

JDK内の利用事例

java.util.Collections#addAll
    public static <T> boolean addAll(Collection<? super T> c, T... elements) {
        boolean result = false;
        for (T element : elements)
            result |= c.add(element);
        return result;
    }

可変長の実装Tを、Tを汎化して保持するコレクションへ追加する。 非常にわかりやすい。Tは、TおよびTの親であればコレクションに格納できるだろう。

Animal, Dogの例でいうと、こんな感じで呼び出せる

Collections.addAll(new ArrayList<Animal>(),new Dog());
Collections.addAll(new ArrayList<Animal>(),new Cat());
Collections.addAll(new ArrayList<Animal>(),new Animal());
java.util.Collections#copy
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        int srcSize = src.size();
        if (srcSize > dest.size())
            throw new IndexOutOfBoundsException("Source does not fit in dest");

        if (srcSize < COPY_THRESHOLD ||
            (src instanceof RandomAccess && dest instanceof RandomAccess)) {
            for (int i=0; i<srcSize; i++)
                dest.set(i, src.get(i));
        } else {
            ListIterator<? super T> di=dest.listIterator();
            ListIterator<? extends T> si=src.listIterator();
            for (int i=0; i<srcSize; i++) {
                di.next();
                di.set(si.next());
            }
        }
    }

国内で探した限りは、非常によく見るPECSの例。
提供する主体をProducerと呼び、消費する主体をCustomerと呼ぶ。
Producerはextends、Customerはsuperとして、PECS。
srcがProducerでTを上限とするListとなり要素を提供する。
destがCustomerでTを下限とするListとなり、要素を消費する。

Collections.copy(new ArrayList<Animal>(), new ArrayList<Dog>());
Collections.copy(new ArrayList<Animal>(), new ArrayList<Animal>());
java.util.List#sort
    default void sort(Comparator<? super E> c) {
        Object[] a = this.toArray();
        Arrays.sort(a, (Comparator) c);
        ListIterator<E> i = this.listIterator();
        for (Object e : a) {
            i.next();
            i.set((E) e);
        }
    }

これもかなりわかりやすい。

List<Animal> animalList = new ArrayList<>();
Comparator<Animal> animalSorter = Comparator.comparing(Animal::no);
animalList.sort(animalSorter);

List<Dog> dogList = new ArrayList<>();
dogList.sort(animalSorter);

List<Animal>でもList<Dog>でも、Comparator<Animal>sortできるようになっているべきだろう。
sortシグネチャComparator<E>だと、List<Dog>sortanimalSorterではできない。

上限付きクラスと下限付き変数型の組み合わせ

最後に載せているリンク集にある記事内で言及されているものを確認したい。

まず、以下2つのクラス群を定義してみる。

f:id:metatrading:20200407113909p:plain
水クラス群

Water->DrinkingWater->Perieは、そのまま水、飲み水、ペリエと具体化していくクラス。

f:id:metatrading:20200408095424p:plain

WaterCupDrinkingWaterCupPerieCupは、それぞれ水を入れられるカップ、飲み水を入れられるカップペリエを入れられるカップとなる。それぞれ型パラメータで上限境界を設定している。
WaterCupには、Water以下が存在することができ、Waterとして取得可能。
extendsパラメータを与えることで、Producerとしての立ち位置としている。

だがextendsのみだと上限境界の例のとおり、何も設定することはできないクラスとなる。
ここからさらに、下限境界指定した変数宣言を取り入れる。

WaterCup<? super Water>には何でも注げる。

WaterCup<? super Water> waterCupLowerWater = new WaterCup<>();
// TをWaterもしくはWaterの親以上にした。Waterなので多態により、Water以下を受け入れる。
waterCupLowerWater.setWater(new Water());
waterCupLowerWater.setWater(new DrinkingWater());
waterCupLowerWater.setWater(new Perie());
// TはWaterもしくはWaterの親以上なので、Waterは確定できる。
waterCupLowerWater.getWater().waterMethod();

下限境界をDrinkingWaterに変えてみる。
WaterCup<? super DrinkingWater>には、DrinkingWaterおよびPerieが注げる。

WaterCup<? super DrinkingWater> waterCupLowerDrinkingWater = new WaterCup<>();
// 下限をDrinkingWaterで与えられているため、値の設定が可能。
// 値自体は、T、もしくはTの子以下を設定可能。つまり、DrikingWater、その子のPerieは設定可能。
//        waterCupLowerDrinkingWater.setWater(new Water()); // compile error
waterCupLowerDrinkingWater.setWater(new DrinkingWater());
waterCupLowerDrinkingWater.setWater(new Perie());
// extends はWaterCupに指定されたwaterとなるため、waterであることが確定できる。
waterCupLowerDrinkingWater.getWater().waterMethod();

DrinkingWaterCup<? super Perie>の例

DrinkingWaterCup<? super Perie> drinkingWaterCup = new DrinkingWaterCup<>();
drinkingWaterCup.setWater(new Water()); // コンパイルエラー
drinkingWaterCup.setWater(new DrinkingWater()); // コンパイルエラー
drinkingWaterCup.setWater(new Perie());
drinkingWaterCup.getWater().drinkingwaterMethod();

PerieCup<? super Perie>の例

PerieCup<? super Perie> perieCup = new PerieCup<>();
perieCup.setWater(new Water()); // コンパイルエラー
perieCup.setWater(new DrinkingWater()); // コンパイルエラー
perieCup.setWater(new Perie());
perieCup.getWater().perieMethod();

こういった組み合わせをまとめるとこうなる。
Xはコンパイルエラー。 f:id:metatrading:20200407164718p:plain

f:id:metatrading:20200408095958p:plain

f:id:metatrading:20200408100019p:plain

extendsの指定がProducerの定義となり、getの返却型(上限境界)を決める。
変数に定義したsuperの指定がConsumerの定義となり、setの受け入れ可能な型(下限境界)を決める。

リンク集

Windows環境における高精度時刻取得について

結論

Windows8未満

 GetSystemTimeAsFileTime

Windows8以降

 以下を比較検討のうえ、採用。
 GetSystemTimePreciseAsFileTime
 GetSystemTimeAsFileTime 


上記に関するMicrosoftのまとめ
docs.microsoft.com


以下は検証の記録。

事前確認

事前知識

分解能

表現粒度を指す    
例:12:11:59.999→分解能1ミリ、12:11:59→分解能1秒

精度

判別可能な最小の断面
例:0.111,0.112,0.112 ... →精度1ミリ、0.110, 0.125,0.140... 精度15ミリ

検証環境

OS:Windows10 Pro

f:id:metatrading:20170617223904p:plain

分解能の確認

分解能の確認コード
// SystimeSample.cpp : コンソール アプリケーションのエントリ ポイントを定義します。
//
#include "stdafx.h"
#include <Windows.h>
#include <stdio.h>
#include <mmsystem.h>
#pragma comment(lib, "winmm.lib")
int main()
{
    DWORD dwTimeAdjustment;
    DWORD dwTimeIncrement;
    BOOL bTimeAdjustmentDisabled;

    GetSystemTimeAdjustment(&dwTimeAdjustment, &dwTimeIncrement, &bTimeAdjustmentDisabled);
    printf("分解能(クロック割り込み間隔):%f ms\n", (double)dwTimeIncrement / 10000.0);
    printf("調整機能:%s\n", (bTimeAdjustmentDisabled ? "True" : "False"));
    printf("加算値(単位:ナノ秒):%f nano \n", GetSystemTimeAdjustment);

    TIMECAPS ptc;
    timeGetDevCaps(&ptc, sizeof(ptc));
    printf("マルチメディアタイマー分解能:%d ms\n", ptc.wPeriodMin);

    LARGE_INTEGER lpFrequency;
    QueryPerformanceFrequency(&lpFrequency);
    printf("高分解能カウンター分解能:%f ms\n", 1000.0 / lpFrequency.QuadPart);
    return 0;
}
結果

分解能(クロック割り込み間隔):15.625000 ms
調整機能:True
加算値(単位:ナノ秒):0.000000 nano
マルチメディアタイマー分解能:1 ms
高分解能カウンター分解能:0.000428 ms


上記はあくまでRTC(リアルタイムクロック・・・ハードウェア クロック)についての情報ぽい。

検証方法

APIを10万回連続で実行し、ミリ秒部分の更新が起きた際に、前回との差を記録する。

Windows previous versions documentation | Microsoft Docsによる計測結果

差分(ms) 出現回数
1 99995
2 1
12 1
15 1
32 2

Windows previous versions documentation | Microsoft Docsによる計測結果

差分(ms) 出現回数
1 99993
2 2
18 1
22 1
29 1
30 1
31 1

Windows previous versions documentation | Microsoft Docsによる計測結果

差分(ms) 出現回数
1 99997
2 2
32 1

GetSystemTimePreciseAsFileTime関数による計測結果

さらに調べると・・・Win8以降、高精度のシステム時刻取得API「GetSystemTimePreciseAsFileTime」が追加されている。

差分(ms) 出現回数
1 99995
4 1
3 1
6 1
2 1
31 1

環境によっては、GetSystemTimePreciseAsFileTimeが遅い場合もあるらしい。

GetSystemTimePreciseAsFileTimeが1ミリ秒の精度を持つのはドキュメントを読むと納得出来るが、
他の関数も1ミリ秒の精度が出ているのはなぜだろうか。


10万回の検証コード

// SystimeSample.cpp : コンソール アプリケーションのエントリ ポイントを定義します。
//
#include "stdafx.h"
#include <Windows.h>
#include <stdio.h>
#include "stdafx.h"
#include <Windows.h>
#include <stdio.h>
#include <unordered_map>
#include <iostream>
const int N = 100001;
const int ignoreN = 0;
int main()
{
	int secondAndMillseconds[N][2];
	std::fill(secondAndMillseconds[0], secondAndMillseconds[N], 0);

	SYSTEMTIME tm, oldTime;
	FILETIME ft;
	GetSystemTimePreciseAsFileTime(&ft);
	//GetSystemTimeAsFileTime(&ft);
	//GetSystemTime(&tm);
	//GetLocalTime(&tm);
	FileTimeToSystemTime(&ft, &tm);

	oldTime = tm;

	for (int i = 0; i < N;) {
		GetSystemTimePreciseAsFileTime(&ft);
		//GetSystemTimeAsFileTime(&ft);
		FileTimeToSystemTime(&ft, &tm);
		//GetSystemTime(&tm);
		//GetLocalTime(&tm);

		if (tm.wMilliseconds != oldTime.wMilliseconds) {
		//	printf("%d:%d:%d.%d\n", tm.wHour, tm.wMinute, tm.wSecond, tm.wMilliseconds);
			secondAndMillseconds[i][0] = tm.wSecond;
			secondAndMillseconds[i][1] = tm.wMilliseconds;

			oldTime = tm;
			i++;
		}
	}
	int oldSecond = 0;
	int oldMillSecond = 0;
	int second = 0;
	int millSecond = 0;
	int diffMillSecond = 0;

	// 差分別の出現回数map
	std::unordered_map<int, int> map;

	int startIndex = ignoreN;
	oldSecond = secondAndMillseconds[startIndex][0];
	oldMillSecond = secondAndMillseconds[startIndex][1];
	for (int i = startIndex + 1; i < N; i++) {
		// 0秒と59秒を比べる状況の考慮
		if (secondAndMillseconds[i][0] == 0 && oldSecond == 59) {
			second = 60;
		}
		else {
			second = secondAndMillseconds[i][0];
		}
		millSecond = secondAndMillseconds[i][1];
		diffMillSecond = (second * 1000 + millSecond) - (oldSecond * 1000 + oldMillSecond);
		if (map[diffMillSecond] == 0) {
			map[diffMillSecond] = 1;
		}
		else {
			map[diffMillSecond] = map[diffMillSecond] + 1;
		}
		oldSecond = secondAndMillseconds[i][0];
		oldMillSecond = secondAndMillseconds[i][1];
	}
	for (auto itr = map.begin(); itr != map.end(); ++itr) {
		std::cout << itr->first           // キーを表示
			<< "," << itr->second << "\n";    // 値を表示
	}

	return 0;
}