ぺんぎんらぼ

お笑いとマンガ好きなしょぼしょぼWeb系エンジニアの日記です。たまに絵を描きます。

お笑いとマンガ好きなしょぼしょぼWeb系エンジニアの日記です

よくあるDIの勘違い③ - Java EEのdependentスコープ

Java EECDIには、dependentというスコープがあります。
このdependentスコープのライフサイクルについて、勘違いしているケースをよく見ます。

どんな勘違い?

このdependentのライフサイクルについて、「Inject先のBeanのスコープに準ずる」というような記載をよく見ます。
この説明を受けて、Inject先がアプリケーションスコープならdependentのBeanもアプリケーションスコープ、Inject先がリクエストスコープならdependentのBeanもリクエストスコープと、ライフサイクルが動的に変化するもの、と勘違いすることが多いです。

勘違いの通りだとすると、Dependentスコープのインスタンスは以下の図のようになります。

実際のライフサイクルは?

dependentのBeanのライフサイクルは、Inject先のBeanのライフサイクルに準ずる、が正しいです。
この説明をしても「やっぱり勘違いしてる時の認識と同じじゃん」と思う人も多いです。別の言い方をすると、dependentのBeanはInject時にインスタンスがnewされる、とも言えます。

dependentのBeanをInjectするコードは、ほかのスコープのBeanをInjectする時と同じです。

@Inject
private DepScope depScope;

このコードは以下のように書き換えたものと同等です。

private DepScope depScope = new DepScope();

このことから、正しいDependentスコープのインスタンスは以下の図のようになります。

DependentスコープのBeanは、Injectのたびに新しいインスタンスが作成されます。つまり、newでインスタンスを作成するコードと同等といえます。

よくあるDIの勘違い② - InjectしたBeanのフィールドに直接アクセスしてはいけない

Spring FrameworkでDIするBeanクラスや、Java EECDI Beanクラスにフィールドを定義して状態を持たせることがあります。

@RequestScope
public class ReqScope {

    public String request = "Request-1";

    public String getRequest() {
        return this.request;
    }

    public String setRequest(String request) {
        this.request = request;
    }

このコードでは、requestという変数名で文字列を保持できるBeanということです。

InjectしたBeanのフィールドへのアクセス方法

先程のクラスをインジェクトして、フィールドにアクセスしてみます。

@RestController
public class FieldTest {
    @Autowired
    private ReqScope reqScope;

    @GetMapping("/request")
    public String request(@RequestParam(value="request")String request) {
        reqScope.request= "Request-2";    ➊
        return reqScope.getRequest();     ➋
    }
}

➊ フィールドに直接アクセスして値を変更します。
➋ Getterメソッド経由でフィールドの値を取得します。

通常のJavaプログラムであれば、このプログラムは期待通りの動作をします。しかし、InjectしたBeanに関しては、このコードは期待通りの動作をしません。
➊でフィールドの値を変更しているにもかかわらず、➋のGetterメソッドで取得できる値は変更前の初期値になります。

フィールドに直接アクセスして値を変更しても、Beanのフィールドに変更結果は反映されません。
以下のようにフィールドの変更にSetterメソッドを使用すると、期待通りの動作になります。

        reqScope.setRequest("Request-2");
        return reqScope.getRequest();

InjectしたBeanのフィールドへの直接アクセスがダメな理由

一部の例外を除き、インジェクトしたBeanのインスタンスはProxyクラスになります。
フィールドに直接アクセスした場合、このProxyクラスのフィールドにアクセスしていて、実装したクラスのフィールドにアクセスしているわけではありません。
実装したクラスのメソッド経由でフィールドにアクセスした場合は、実装したクラスのフィールドへのアクセスになります。

このことから、InjectしたBeanのフィールドに直接アクセスしてはいけないことがわかります。
そもそも、クラスのフィールドは外部から直接アクセスできないスコープ(privateとかprotected)にして、メソッド経由でアクセスしましょう。

今回のケースに該当しないBeanのスコープ

Spring Frameworkprototypeスコープ、Java EEdependentスコープはProxyクラスが生成されず、実装クラスが直接Injectされるため、今回のようなことは起きません。 ただ、特定のBeanスコープに依存するフィールドのアクセス方法を実装することはよくないのです。やはり、Beanスコープにかかわらず、フィールドはメソッド経由でアクセスしましょう。

よくあるDIの勘違い① - 異なるスコープを持つBeanの親子関係

Spring FrameworkのDIや、Java EECDIの対象となるBeanには「スコープ」が定義されてます。
この「スコープ」の定義によって、Beanのライフサイクルが変わることは広く知られていると思います。

例えば、アプリケーションスコープのBeanは、アプリケーション内で1つだけインスタンスが作成され、そのインスタンスが使いまわされます。 リクエストスコープのBeanは、Webのリクエスト単位で1つのインスタンスが作成され、リクエスト内ではそのインスタンスが使いまわされ、別リクエストであれば別のインスタンスが使われます。

異なるスコープを持つBeanのインジェクトの勘違い

以下のようなリクエストスコープのBeanがあったとします。

@RequestScope
public class ReqScope {
   ・
   ・
   ・
}

このリクエストスコープのBeanをアプリケーションスコープのBeanがインジェクトするとします。

@ApplicationScope
public class AppScope {
    @Autowired
    private ReqScope reqScope;
   ・
   ・
   ・
}

アプリケーションスコープのインスタンスは1つなので、アプリケーションスコープのBeanでインジェクトしたリクエストスコープも1つになると思ってる人もいると思います。
なぜなら、このサンプルソースのAppScopeクラスが持つフィールド「reqScope」も1つだから、アプリケーションスコープ1つに対して、リクエストスコープを複数持てないからです。

次の図のようになると思っている人もいると思います。

しかし、これは勘違いなんです。

異なるスコープを持つBeanのインジェクトの実際

では、実際には各スコープのインスタンスはどのようになるのでしょうか。
おそらく、次の図のようなスコープの構成を期待していると思います。そして、実際に期待通りのスコープの構成となります。

アプリケーションスコープのインジェクトフィールドに複数のリクエストスコープのインスタンスが結びついてます。
配列でもない1つのJavaの変数に複数のオブジェクトは格納できません。しかし、実際にリクエストごとに別のインスタンスが結びつきます。この仕組みについては後程解説します。

まずは、アプリケーションスコープ1つに対して、複数のリクエストスコープが結びついているのかを確認します。

それぞれのスコープのメソッドでオブジェクトIDを出力します。

@ApplicationScope
public class AppScope {
    @Autowired
    private ReqScope reqScope;

    public String getMessage(String request) {
        System.out.println("Start Request " + request + " AppScope Object ID : " + ObjectUtils.getIdentityHexString(this));

        String message = reqScope.getMessage(request);

        System.out.println("End Request " + request + " AppScope Object ID : " + ObjectUtils.getIdentityHexString(this));

        return message;
    }
}
@RequestScope
public class ReqScope {
    public String getMessage(String request) {
        System.out.println("Start Request " + request + " ReqScope Object ID : " + ObjectUtils.getIdentityHexString(this));

        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println("End Request " + request + " ReqScope Object ID : " + ObjectUtils.getIdentityHexString(this));

        return request;
    }
}

ほぼ同じタイミングで2つのリクエストを実行してみます。

Start Request A AppScope Object ID : 7aaa1b39
Start Request A ReqScope Object ID : 468bf1a6
Start Request B AppScope Object ID : 7aaa1b39
Start Request B ReqScope Object ID : 475926a2
End Request A ReqScope Object ID : 468bf1a6
End Request A AppScope Object ID : 7aaa1b39
End Request B ReqScope Object ID : 475926a2
End Request B AppScope Object ID : 7aaa1b39

AppScopeのオブジェクトIDは、すべて7aaa1b39と同じことから、アプリケーションスコープのインスタンスは1つであることがわかります。

それに対して、ReqScopeのオブジェクトIDは、リクエストAでは468bf1a6、リクエストBでは475926a2であることから、リクエストスコープのインスタンスは1リクエストにつき1つであることがわかります。

異なるスコープを持つBeanのインジェクトの仕組み

検証により、アプリケーションスコープのBeanにリクエストスコープのBeanをインジェクトしても、正しいスコープで動作していることがわかりました。
Spring FrameworkのDI、Java EECDIともに、広いスコープのBeanに、より狭いスコープのBeanをインジェクトすることができます。

このようなことを可能とする仕組みとして、インジェクトされるインスタンスはプロキシクラスであるためです。
実際のインスタンスの構成は次のようになります。

アプリケーションスコープのフィールドにインジェクトされるインスタンスは、対象クラスのプロキシになります。プロキシクラスは1つであり、1つしかないアプリケーションスコープのフィールドに格納されます。
そして、リクエストスコープのメソッドが呼び出されると、プロキシクラスのメソッドが呼び出され、そこでリクエストごとの振り分け処理が実行され、リクエストに応じたBeanのメソッドが呼び出されます。

MediaWikiのお引越し

中古のQNAPを購入したので、これまでラズパイに立てていたMediaWikiをお引越しします。 wikiページは全30ページ程だったので、愚直にテキストをコピペして移行します。

つまり、手作業で移行します。

ページに埋め込んでいた画像たちは、FTPソフトでローカルにダウンロードしていきます。 /var/www/html/imagesディレクトリ配下に画像が保存されています。 画像ファイルは120px, 800pxなど異なるサイズでそれぞれ保存されており、1つの画像ファイルに付き3~5ファイルほどありました。

pi@raspberrypi:/var/www/html/images $ find ./ -name "*ライフサイクル.png"
./thumb/e/ee/ライフサイクル.png
./thumb/e/ee/ライフサイクル.png/120px-ライフサイクル.png
./thumb/e/ee/ライフサイクル.png/800px-ライフサイクル.png
./e/ee/ライフサイクル.png

ダウンロードした画像ファイルたちは、 新MediaWikiにファイルの一括アップロードプラグインをインストールしておき、 MediaWikiの画面から一番大きなサイズのファイルをアップロードしていくことにしました(サムネなどはアップロードしない)。

Category:Bulk upload - MediaWiki

プラグインはいくつかあるようですが、MsUploadにしました。

手順は公式サイトの通り行っていきます。

Extension:MsUpload - MediaWiki

ダウンロードサイトから圧縮ファイルをダウンロードします。

ファイルげっとです。

mediawikiのextensionsフォルダ配下に解凍します。

次の一行を、LocalSettings.phpの最後に追加してあげます。

wfLoadExtension( 'MsUpload' );

インストール作業は以上で、プラグインは即時反映されます。

戸惑ったのがこのプラグインの使い方です。

「ファイルをアップロード」機能からではなく、ページ編集画面から使用します。 ファイルを埋め込みたいページに限らず、どのページからアップロードしてもOK。 ↓のようにエリアができているので、そこにアップロードしたいファイルをドラッグするだけ。

これであとはポチポチするだけです。

すごく簡単にRaspberry PiのSDカードをバックアップする方法

まえがき

スマホを再起動すると、SDカードに保存していた写真データが消えていました。電源断でいってしまったようです。SDカードのフォーマットで8割復活しました。

ところで、ラズパイのHDDもSDカードです。

2019年にラズパイの3b+モデルを購入し安定的に作動していましたが、これを機にバックアップ機構を作ることにしました。

ラズパイ4モデルでも可能です。

バックアップ方法検討

最も手数が少ない方法1を採用です。

方法 概要 コメント
SDカード(差し込み式のUSBリーダー)にバックアップ rpi-cloneを使う方法です。すべてがCUIで完結し、ラズパイが壊れてもSDカードを差し替えるだけです。
USBメモリにバックアップ ラズパイが壊れた時にUSBからブートするよう設定処理が必要です。ddコマンドなどを使って操作するので慎重に行う必要があります。ラズパイ4だとより簡便になったようです。
WindowsでMiniTool Partition Wizardを使ってバックアップ Windowsのアプリで頑張る方法ですが、一番手間が多いです。バックアップ毎に毎回ラズパイからSDカードを抜く必要があります。

使うもの

上で書いた「SDカード(差し込み式のUSBリーダー)」とは、以下のようなmicroSDカードリーダーとUSBメモリーが一体化した一品です。 Amazonで333円でゲットしました。さすがバッファロです。

これにはSDカードはついていないので、別途購入します。

ラズパイと同じ16GBのSDカードが家にありましたのでそれを使います。

そして前者に後者をドッキングさせます。以下のように差し込むところの意外性にちょっと驚きます。

実施

SD Card Copierでラズパイのバックアップ(クローン)もしくは、rpi-clone + cronで毎夜1時にバックアップする - あとをしNOTE こちらのサイトの通り実行しました。 すべてpiユーザで実行しています。

買ってきたUSBリーダーは後にして、まず挿入前の状態です。

$ lsblk
NAME        MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
mmcblk0     179:0    0 14.9G  0 disk 
├─mmcblk0p1 179:1    0  1.6G  0 part 
├─mmcblk0p2 179:2    0    1K  0 part 
├─mmcblk0p5 179:5    0   32M  0 part 
├─mmcblk0p6 179:6    0  256M  0 part /boot
└─mmcblk0p7 179:7    0   13G  0 part /
$ df -m
ファイルシス   1M-ブロック  使用 使用可 使用% マウント位置
/dev/root            12961  4909   7372   40% /
devtmpfs               404     0    404    0% /dev
tmpfs                  437     0    437    0% /dev/shm
tmpfs                  437    28    409    7% /run
tmpfs                    5     1      5    1% /run/lock
tmpfs                  437     0    437    0% /sys/fs/cgroup
/dev/mmcblk0p6         253    48    205   19% /boot
tmpfs                   88     1     88    1% /run/user/1000

16GB(おそらく実質は15GB)のSDカードなので、大体10GBくらい使っています。

やっていきます。

gitからrpi-cloneモジュールを落としてきて、コマンド実行できるようにsbin配下に置きます。

$ git clone https://github.com/billw2/rpi-clone.git
$ cd rpi-clone
$ sudo cp rpi-clone rpi-clone-setup /usr/local/sbin

ラズパイをシャットダウン(sudo shutdown -h nowコマンド)し、コンセントを引っこ抜き、USBを差し込みます。

USBポートは4口あったのでどこにするか迷います。

また電源を差し込みます。勝手に起動します。

$ lsblk
NAME        MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
sda           8:0    1   15G  0 disk 
mmcblk0     179:0    0 14.9G  0 disk 
├─mmcblk0p1 179:1    0  1.6G  0 part 
├─mmcblk0p2 179:2    0    1K  0 part 
├─mmcblk0p5 179:5    0   32M  0 part 
├─mmcblk0p6 179:6    0  256M  0 part /boot
└─mmcblk0p7 179:7    0   13G  0 part /

sdaパーティションが仲間に加わっています。

さて、あとはrpi-cloneを実行するだけです。

$ sudo rpi-clone sda
Initialize and clone to the destination disk sda? (yes/no):  ←yesを入力
Optional destination ext type file system label (16 chars max):  ←Enter

22分かかりました。

おわりです。

sda配下に色々作られています。

$ lsblk
NAME        MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
sda           8:0    1   15G  0 disk 
├─sda1        8:1    1  1.6G  0 part 
├─sda2        8:2    1    1K  0 part 
├─sda5        8:5    1   32M  0 part 
├─sda6        8:6    1  256M  0 part 
└─sda7        8:7    1 13.1G  0 part 
mmcblk0     179:0    0 14.9G  0 disk 
├─mmcblk0p1 179:1    0  1.6G  0 part 
├─mmcblk0p2 179:2    0    1K  0 part 
├─mmcblk0p5 179:5    0   32M  0 part 
├─mmcblk0p6 179:6    0  256M  0 part /boot
└─mmcblk0p7 179:7    0   13G  0 part /

毎夜3時に実行されるようcronを登録しておきます。

差分バックアップしてくれるようです。

$ sudo crontab -e
00 3 * * * sudo bash rpi-clone sda -q

実行ログは以下のパスにあります。

$ cat /var/log/rpi-clone.log

おわり

これで安心してラズパイを使い続けられるようになりました。 バックアップ処理失敗時に、Slackやgmailに通知するようにしたいですが、またの機会に。

JavaScriptで1から100までの数値配列を生成する(JavaのIntStream.rangeをやりたい)

JavaScriptで1から100までの数値からなる配列を生成しましょう。

Javaの場合はIntStream.range(1, 101)やrangeClosed(1, 100)で一発ですが、JavaScriptの場合は少し面倒です。

完成版は以下になります。

完成版

const range = (start, end) => [...new Array(end-start).keys()].map(n => n + start);
console.log(range(1,101));
// Array [1, 2, 3,(略)98, 99, 100]

考え方

const iterator1 = new Array(101-1).keys();
const iterator2 = [new Array(101-1).keys()];
const iterator3 = [...new Array(101-1).keys()];

console.log(iterator1);
// Object {  }
console.log(iterator2);
// Array [Object {  }]
console.log(iterator3);
// Array [0, 1, 2, 3, (略)97, 98, 99]

iterator1では、Arrayのkey()が配列のインデックス数値からなるイテレーターオブジェクトを生成しています。

Javaイテレーターと使い勝手は異なりますが、JavaScriptではES6から追加されました。

const array1 = ['a', 'b', 'c'];
const iterator = array1.keys();

console.log(iterator.next().value); // 0
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2

iterator2では、イテレーターを [ ] で括ることによってArrayオブジェクトになりました。

さらにiterator3で [...new Array(101-1).keys()]; とすることで、イテレーターの要素をそのままもったArrayオブジェクトに。 ...new Array... は、JavaScriptにおける残余引数構文というもので、以下のように受け取った引数から配列を生成してくれます。

const a = [1,2];
const b = 3;
console.log([...a,b]); // Array [1, 2, 3]

0から99までの配列が得られたので、あとはこれに map() で1から100までの配列にしてあげれば終わりです。

JavaScriptのArray.fill()の注意点

JavaScriptで少し「あれ?」となったことを共有です。 多次元配列の操作中に、思っていたのと違う挙動になってしまいました。

間違った例

コード

const loopArray = new Array(3).fill(new Array(2)); // 初期化

for (let y = 0; y < 3; y++) {
  for (let x = 0; x < 2; x++) {
    loopArray[y][x] = "[y]=" + y + ",[x}=" + x; // 値の設定
  }
}

for (let y = 0; y < 3; y++) {
  for (let x = 0; x < 2; x++) {
    console.log(loopArray[y][x]); // 値の出力
  }
}

出力結果

"[y]=2,[x}=0"
"[y]=2,[x}=1"
"[y]=2,[x}=0"
"[y]=2,[x}=1"
"[y]=2,[x}=0"
"[y]=2,[x}=1"

期待していた出力結果

"[y]=0,[x}=0"
"[y]=0,[x}=1"
"[y]=1,[x}=0"
"[y]=1,[x}=1"
"[y]=2,[x}=0"
"[y]=2,[x}=1"

修正後

期待値のように出力するには、コードを次の★のように直します。

const loopArray = new Array(3); // 初期化★
//const loopArray = new Array(3).fill(new Array(2)); 

for (let y = 0; y < 3; y++) {
  loopArray[y] = new Array(2); // 追加★
  for (let x = 0; x < 2; x++) {
    loopArray[y][x] = "[y]=" + y + ",[x}=" + x; // 値の設定
  }
}

for (let y = 0; y < 3; y++) {
  for (let x = 0; x < 2; x++) {
    console.log(loopArray[y][x]); // 値の出力
  }
}

解説

Array.fill()のコードコメントには、次のように書かれています。

/**
 * Changes all array elements from `start` to `end` index to a static `value` and returns the modified array
 * @param value value to fill array section with
 * @param start index to start filling the array at. If start is negative, it is treated as
 * length+start where length is the length of the array.
 * @param end index to stop filling the array at. If end is negative, it is treated as
 * length+end.
 */
fill(value: T, start?: number, end?: number): this;

一行目の「static value」というのがミソです。 fill()の引数として渡したnew Array(2)は、親の配列要素ごとにインスタンスが生成される訳ではなく、一つのstatic変数として扱われているようです。 配列のようにプリミティブではない値を渡した場合、各配列要素は同じ参照を見るようになってしまうということですね。