【TypeScript】InversifyJSでIoC(DI)体験 2
はじめに
*【TypeScript】InversifyJSでIoC(DI)体験 1
続きです。
今回は Container クラスを中心に、もう少し InversifyJS の中身を見ていきたいと思います。
ContainerOptions
前回は使用しませんでしたが、 Container クラスの Constructor では引数としてオプションを指定できます。
内容は Wiki の通りなのですが、せっかくなので試してみますよ。
interfaces/interfaces.ts
~省略~ export interface ContainerOptions { autoBindInjectable?: boolean; defaultScope?: BindingScope; skipBaseClassChecks?: boolean; } ~省略~
autoBindInjectable
autoBindInjectable を true にすると、下記のようにインスタンスを取得できるようになります。
sampleContainer.ts
const sampleContainer = new Container({ autoBindInjectable: true }); sampleContainer.bind< Sample>(TYPES.Sample) .to(DecoratorSample) .inSingletonScope();
mainPage.ts
export function doSomething(){ // sampleContainer.get< Sample>(TYPES.Sample) のように指定する必要がない. let s = sampleContainer.get(DecoratorSample); }
一瞬良いかも、と思ってみたのですが、これだと呼び出す側も実装クラスが誰なのかを知っている必要があるため、どうなんだろう……と思ってしまいました。
もちろん私が気づいていないだけで良い使い方があるかもしれませんが。
defaultScope
defaultScope を指定すると、前回登場した inSingletonScope() などのスコープ設定をデフォルトで設定できます。
sampleContainer.ts
// Default Scope無し. const sampleContainer1 = new Container(); sampleContainer.bind< Sample>(TYPES.Sample) .to(DecoratorSample) .inSingletonScope(); // Default Scopeあり. const sampleContainer2 = new Container({ defaultScope:"Singleton" }); sampleContainer2.bind< Sample>(TYPES.Sample) .to(DecoratorSample);
sampleContainer1 、 sampleContainer2 ともにシングルトンで DecoratorSample のインスタンスが設定されます。
skipBaseClassChecks
一番よく分かっていない機能ですが、拡張クラスを Inject したい場合に便利らしいです。
interface について
ちょっとわき道にそれますが。
Container クラスの Constructor の引数は ContainerOption です。
が、実際には下記のように引数を渡しています。
const sampleContainer = new Container({ defaultScope:"Singleton" });
あれ? ContainerOption (の具象クラス) ではない?
また、先ほど見逃していましたが、 TypeScript の interface は C# とは異なり、変数(プロパティ)を持たせることもできるようです。
interfaces/interfaces.ts
~省略~ export interface ContainerOptions { autoBindInjectable?: boolean; defaultScope?: BindingScope; skipBaseClassChecks?: boolean; } ~省略~
また、具象クラスを定義してインスタンスを作らなくても、 interface が持つプロパティや関数を実装していれば同じものとして扱うことができます。
(この辺りは Go などと(今も仕様が変わっていなければ)共通していますね)
例えばこういうクラスと interface があったとして。
export interface Sample{ message: string; call(): any; } export class IntefaceSample{ public constructor(sample: Sample){ console.log(sample.message); } }
IntefaceSample のコンストラクタに下記を渡すことができます。
let sample = { message: "hello", call: () => {} }; let s = new IntefaceSample(sample);
先ほどの Container のコンストラクタではこの仕組みを利用していたわけですね。
あまり調子に乗って使いすぎるとわけわからん状態になりそうではありますが、こういう柔軟さは良いですね。
Container を追う
さて、 InversifyJS に戻りますよ。
まずは DI Container である(はずの) Container クラスのコンストラクタからたどっていきます。
コンストラクタの最初の処理は先ほどの ContainerOption の設定(引数で渡されていれば)を行っています。
後々 Option の指定によってどのような処理が行われるか、ということにも触れることにはなると思いますが、ここではスキップします。
で、残りの処理はこちら。
container/container.ts
~省略~ public constructor(containerOptions?: interfaces.ContainerOptions) { ~省略~ this.id = id(); this._bindingDictionary = new Lookup< interfaces.Binding< any>>(); this._snapshots = []; this._middleware = null; this.parent = null; this._metadataReader = new MetadataReader(); } ~省略~
id
処理は utils/id.ts にあります。
やっていることは 0 からカウントアップした値を返す、というだけのシンプルなものです。
C# で Equals をオーバーライドする時に出てきた HashCode のようなものでしょうか。
_bindingDictionary
Lookup 、 Binding といういかにも重要そうなクラス、 interface が登場しました。
Lookup は container/lookup.ts 、 Binding は interfaces/interfaces.ts で定義されています。
_metadataReader
planning/metadata_reader.ts で定義されているクラスで、 metadata という名前や Reflect.getMetadata() を呼んでいたりと、 inject するクラスの取得などに関連していると思われます。
( @injectable() を付与するクラスで import が必要だった metadata_reader がコレですね)
空にしているだけであったためスキップした snapshots 、 middleware 、 parent も気になるところですが、さくさく次に進みましょう。
Container.bind
Inject する interface と具象クラスからインスタンスを取得する bind を見てみます。
container/container.ts
~省略~ // Registers a type binding public bind< T>(serviceIdentifier: interfaces.ServiceIdentifier< T>): interfaces.BindingToSyntax< T> { const scope = this.options.defaultScope || BindingScopeEnum.Transient; const binding = new Binding< T>(serviceIdentifier, scope); this._bindingDictionary.add(serviceIdentifier, binding); return new BindingToSyntax< T>(binding); } ~省略~
まず 「const scope = this.options.defaultScope || BindingScopeEnum.Transient;」であれ?と思いましたが、これは this.options.defaultScope が undefined であった場合は BindingScopeEnum.Transient が渡されます。
ということで、何も指定しなかった場合のデフォルトのスコープは Transient になるのですね。
と思っていたら(当然ながら)ドキュメントで触れられていました。
serviceIdentifier
引数の serviceIdentifier ですが、前回使用した string 、 symbol の他 Newable つまりクラス、抽象クラスも扱うことができるようです。
interfaces/interfaces.ts
~省略~ export type ServiceIdentifier< T> = (string | symbol | Newable< T> | Abstract< T>); ~省略~
binding
bindings/binding.ts で定義されているクラスですが、ここでは先ほどの serviceIdentifier とスコープ情報を保持しています。
ここで作られた Binding< Sample>("Sample", BindingScopeEnum.Singleton) が、 container/lookup.ts の Map に格納されます。
container/lookup.ts
~省略~ private _map: Map< interfaces.ServiceIdentifier< any>, T[]>; ~省略~
この値は途中で出てきた _bindingDictionary に格納され、 Container クラスからアクセスされると。
で、この関数の中で最後に返されるのは syntax/binding_to_syntax.ts の BindingToSyntax< T> です。
BindingToSyntax< T>
BindingToSyntax クラスでは Binding< Sample>("Sample", BindingScopeEnum.Singleton) を持っています。
(コンストラクタで渡される)
んで、はじめに戻って今回作成した sampleContainer を見てみると、 to() を呼んでいます。
sampleContainer.ts
sampleContainer.bind< Sample>(TYPES.Sample) .to(DecoratorSample);
syntax/binding_to_syntax.ts
~省略~ public to(constructor: new (...args: any[]) => T): interfaces.BindingInWhenOnSyntax< T> { this._binding.type = BindingTypeEnum.Instance; this._binding.implementationType = constructor; return new BindingInWhenOnSyntax< T>(this._binding); } ~省略~
この引数で DecoratorSample (interface Sample の具象クラス)の情報が _binding に追加されます。
BindingInWhenOnSyntax
syntax/binding_in_when_on_syntax.ts で定義されているクラスです。
in when on 。。。 いえ、良いんですよ。
syntax/binding_in_when_on_syntax.ts
~省略~ public constructor(binding: interfaces.Binding< T>) { this._binding = binding; this._bindingWhenSyntax = new BindingWhenSyntax< T>(this._binding); this._bindingOnSyntax = new BindingOnSyntax< T>(this._binding); this._bindingInSyntax = new BindingInSyntax< T>(binding); } ~省略~
要は in when on でそれぞれインスタンスを作っていると。
Container インスタンスを保持している sampleContainer.ts で以前のように inSingletonScope() を実行した場合、 this._bindingInSyntax が使用されるようですね。
ここまでで、 interface 、具象クラス、スコープ情報が保持されているところが何となく見られてような気がします。
スコープの種類にかかわらず、実際にインスタンスが作られるのは最初に Container にインスタンスを取りに行ったタイミング。
ということで、 get を追うといよいよインスタンスが生成されたり保持されたりするところが見られる、ということ。。。だと思います。多分。
【TypeScript】InversifyJSでIoC(DI)体験 1
はじめに
諸般の事情から TypeScript に漬け込まれている今日この頃。
ふと TypeScript 単体で DI ってできないのかな~と思ってググった時に出てきた、 InversifyJS を試してみることにしました。
TypeScript 単体で~というのは、(今回の InversifyJS でも使われている) Decorator を使うとできそうなので、こちらも引き続き追いかけてはいきたいと思います。
なおタイトルでわざわざ IoC(Inversion of control) を出したのは、 InversifyJS の名前がそこから来てるんだろうな~と思ったためで大した理由はありません。
準備
何はともあれインストールです。
GitHub の手順に従って進めますよ。
npm install --save-dev inversify reflect-metadata
tsconfig.json
{ "compilerOptions": { /* Basic Options */ "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ "lib": ["dom", "es6"], /* Specify library files to be included in the compilation. */ ~省略~ "outDir": "wwwroot/js", /* Redirect output structure to the directory. */ ~省略~ "strict": true, /* Enable all strict type-checking options. */ ~省略~ "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ ~省略~ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ ~省略~ "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ } }
コメントアウトされた部分は省略しています。また変更したところは太字にしています。
あと、ベースにしているプロジェクトは以前使ったものをそのまま再利用している関係もあり、 webpack を使っています。
exports が使えない?
以前は HTML から呼び出す関数を下記のように書いていました。
exports.doSomething = function(){ console.log("hellllloooo"); }
が、 tsconfig.json で「"types": ["reflect-metadata"],」を有効にしようとするとエラーになってしまいました。
どういう関係があるのはよくわかっていないのですが。。。
で、下記のように変更しました。
export function doSomething(){ console.log("hellllloooo"); }
呼び出す側は変わらず Page.doSomething() で OK です。
……いや、これできるならそもそもこれでよかったんやけど感。
ま、まぁいいや。
とりあえず DI ってみる
準備もできたところで早速使ってみましょう。
sample.ts
export interface Sample{ call(): any; }
DI で挿入する interface です。
特に変わったところはないです。
decoratorSample.ts
import { Sample } from "./sample"; import 'reflect-metadata'; import {injectable} from 'inversify'; @injectable() export class DecoratorSample implements Sample{ public call(){ console.log("hello2"); } }
Sample の実装クラスです。
@injectable() なる気になるワードが。
types.ts
const TYPES = { Sample: Symbol.for("Sample") } export { TYPES }
次に登場する Container で使用します。
sampleContainer.ts
import { Container } from "inversify"; import { TYPES } from "./types"; import { Sample } from "./sample"; import { DecoratorSample } from "./decoratorSample"; const sampleContainer = new Container(); sampleContainer.bind< Sample>(TYPES.Sample).to(DecoratorSample); export{ sampleContainer }
ここで Inject される interface と実装クラスの紐づけが行われています。
では Inject されたものを利用してみます。
mainPage.ts
import { Sample } from "./sample"; import { sampleContainer } from "./sampleContainer"; import { TYPES } from "./types"; export function doSomething(){ let s = sampleContainer.get< Sample>(TYPES.Sample); s.call(); }
ASP.NET Core のコンストラクタインジェクションなどより長くなってしまっているのは気になりますが、これでインスタンスを取得できます。
ただしシングルトンではない
このままだとシングルトンではないため、上記とは別に sampleContainer.get でインスタンスを取得すると別インスタンスが渡されてしまいます。
この指定は Container で行います。
sampleContainer.ts
~省略~ const sampleContainer = new Container(); sampleContainer.bind< Sample>(TYPES.Sample) .to(DecoratorSample) .inSingletonScope(); ~省略~
reflect-metadata 大事
注意すべき点は Inject 対象のクラスで reflect-metadata を import することです。
これを忘れると下記のエラーが発生します。
TypeError: Reflect.hasOwnMetadata is not a function
コンパイルエラーは発生せず、見落としがちなので注意が必要です。
InversifyJS とたわむれる 1
というわけで無事 DI できました。
もう少し中身を見てみることにします。
インスタンス生成のタイミング
Inject 対象のクラスインスタンスですが、いつ生成されるのでしょうか。
decoratorSample.ts
~省略~ @injectable() export class DecoratorSample implements Sample{ public constructor(){ console.log("Constructor death"); } public call(){ console.log("hello2"); } }
mainPage.ts
~省略~ export function doSomething(){ console.log("start"); let s = sampleContainer.get< Sample>(TYPES.Sample); s.call(); }
これを実行すると、出力結果は下記のようになります。
start Constructor death hello2
では Inject 対象のクラスが二つ以上ある場合はどうでしょうか。
doSomething.ts
export interface DoSomething{ saySomething():void; }
diDoSomething.ts
import 'reflect-metadata'; import {injectable} from 'inversify'; import { DoSomething } from './doSomething' @injectable() export class DiDoSomething implements DoSomething{ public constructor(){ console.log("DoSomething constructor death"); } public saySomething():void{ console.log("ho"); } }
types.ts
const TYPES = { Sample: Symbol.for("Sample"), DoSomething: Symbol.for("DoSomething") } export { TYPES }
sampleContainer.ts
import { Container } from "inversify"; import { TYPES } from "./types"; import { Sample } from "./sample"; import { DecoratorSample } from "./decoratorSample"; import { DoSomething } from "./decoratorSamples/doSomething"; import { DiDoSomething } from "./decoratorSamples/DiDoSomething"; const sampleContainer = new Container(); sampleContainer.bind< Sample>(TYPES.Sample) .to(DecoratorSample) .inSingletonScope(); sampleContainer.bind< DoSomething>(TYPES.DoSomething) .to(DiDoSomething) .inSingletonScope(); export{ sampleContainer}
mainPage.ts
~省略~ export function doSomething(){ console.log("start"); let s = sampleContainer.get< Sample>(TYPES.Sample); s.call(); }
これを実行すると、出力結果は下記のようになります。
start Constructor death hello2
ということで、下記のことがわかりました。
- ( inSingletonScope の場合)最初に sampleContainer.get が実行されたタイミングで実体化される。
- sampleContainer.get が実行されていないクラスは同じ Container にバインドされていても実体化されない。
TYPES
Container への bind や get で使用されている TYPES 。
types.ts
const TYPES = { Sample: Symbol.for("Sample"), DoSomething: Symbol.for("DoSomething") } export { TYPES }
今回の用途である sampleContainer.get< Sample> の引数の型は string であり、その内容は Inject される interface 名となります。
(実は実装クラス名でも問題なく動作はしますが、あえてそうするメリットはなさそうです)
sampleContainer.get< Sample>("Sample") のように直接 string で指定することはできますが、引数の名前が実際のクラス名( interface 名)と違っていると実行時にエラーが発生するため、一か所にまとめるという発想は理解ができます。
が、それを const Sample: string = "Sample"; のようにせずに Symbol を使っているのはなぜでしょうか。
Symbol
- Symbol - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN
- Symbol - JavaScript | MDN
- JavaScript のデータ型とデータ構造 - JavaScript | MDN
- Symbols in ECMAScript 6
- シンボル型
そもそも Symbol とは?という話になるわけですが、上記を見たところ以下のような特徴があるようです。
- ECMAScript 6 から登場したプリミティブ型
- ユニークかつ不変である
- string と同じくプロパティのキーとして使用できる
特に 2.の特徴から ID として使われることから、今回も string ではなく Symbol が使われているようです。
なお、 InversifyJS の README によると、 Symbol 、 string での指定の他、 class でも指定できるようです。
ただし Symbol 推奨である、ということでした。
なお、 Symbol について注意が必要なのは、下記かなと思っています。
console.log(Symbol.for("Sample") === Symbol.for("Sample")); console.log(Symbol("Sample") === Symbol("Sample"));
結果は下記になります。
true false
Symbol("Sample") はユニークな値を作るのに対し、 Symbol.for("Sample") はグローバルシンボルテーブルと呼ばれる場所に値を作成し、ページをまたいだりしても Symbol.for の引数が同じであれば同じ値を返します。
そのため上記はこのようにしても true となります。
console.log(Symbol.for("Sample") == Symbol.for("Sample"));
InversifyJS については引き続き中身を追っていきたいと思います。
Symbol についても使い方が今一つよくわかってはいないため、ゆるゆると自分でも使用していきたいと思います。
【Java】 volatie に触れてみたい 1
はじめに
前回ちょこっと出てきた volatile 。
CompletableFuture.java
~省略~ volatile Object result; ~省略~
名前すら触れませんでしたが Twitter で流れてきたブログを見て気になっていたこともあり、調べてみることにしました。
volatile とは
とりあえずググってみます。
- Javaの理論と実践: volatile を扱う - IBM Developer 日本語版
- Chapter 8. Classes - The Java® Language Specification
- 3. スレッドの排他制御 (3) - TECHSCORE(テックスコア)
- Javaスレッドメモ - Hishidama's Java thread Memo
- volatileとか使うなと怒られた話 - 谷本 心 in せろ部屋
まとめると下記のような特徴があるらしい、と。
- 変数に付加する(例: 「volatile int count = 0;」)
- マルチスレッド処理の文脈で使用される
- synchronized より軽量に扱うことができる
- synchronized と違ってアトミック性を持たない
- 可視性を持っており、複数のスレッドから同じ変数を変更した場合も、
値を取得するときに(スレッドごとに持っている古い値ではなく)新しい値を取得できる - 取り扱いが難しく、正しく使用できる状況は限られている
とりあえずアトミック性、可視性について見てみます。
アトミック性について
先に挙げた通り、 volatile を変数に付けたとしても、アトミック性は保証されません。
ということは、複数のスレッドから同時に値を変更した場合に、正しい結果にならない場合がある、ということです。
これを 1.通常、2. volatile を付けた場合、3. synchronized を付けた場合、4.変数を AtomicInteger にした場合で試してみます。
まず変数の保持、変更を行うためのクラスです。
3.以外はこのクラスを複数のスレッドから呼び出して最後の値を比較します。
SampleBehaviour.java
package SearchContainedFiles; public interface SampleBehaviour { void add(); void subtract(); int getCount(); }
MultithreadSample.java
package SearchContainedFiles; class MultithreadSample implements SampleBehaviour{ private int count; public void add(){ count++; } public void subtract(){ count--; } public int getCount(){ return count; } }
int の値を +1 / -1 するだけです。
1.通常
App.java
package SearchContainedFiles; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; public class App { public static void main(String[] args) { MultithreadSample s = new MultithreadSample(); CompletableFuturecallSample1 = CompletableFuture.supplyAsync(() -> add(s)); CompletableFuture callSample2 = CompletableFuture.supplyAsync(() -> subtract(s)); var all = CompletableFuture.allOf(callSample2, callSample1); try { all.join(); all.get(); System.out.println("call: " + s.getCount()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } private static int add(SampleBehaviour s){ System.out.println("call1 " + Thread.currentThread().getId()); for(int i = 0; i < 10000; i++){ try{ s.add(); } catch(IllegalArgumentException e){ System.out.println(e.getLocalizedMessage()); } } return 0; } private static int subtract(SampleBehaviour s){ System.out.println("call2 " + Thread.currentThread().getId()); for(int i = 0; i < 10000; i++){ try{ s.subtract(); } catch(IllegalArgumentException e){ System.out.println(e.getLocalizedMessage()); } } return 0; } }
+1 / -1 するメソッドを 10000 回ずつ呼び、最後に値を受け取ります。
期待値は 0 であり、「call: 0」になるはずです。
結果
出力結果は下記の通りです。
call1 13 call2 14 call: -35
Oh......
ややこしいのは、実行されるタイミングによっては正しい結果が出てくることです。
その場合、 s.add() や s.subtract() の前後で Thread.sleep してやると再現しやすい気がします。
アトミック
+1 / -1 を同じ回数実行しても 0 に戻らないのは、 int がアトミックではないためです。
例えば add() を実行するとき、実際には下記の処理が行われることになります。
- count の値を取得する
- count に 1 を追加する
- count の値を上書きする
1.が実行され、 3.が実行されるより前に subtract() が実行されてしまうと、 3.の時点での count の値が 1.と異なってしまうため、ズレが生じてしまう、ということのようです。
これを防ぐ方法は?というのが、後述する AtomicInteger や synchronized です。
2. volatile を付けた場合
とその前に、今回の主役である volatile を試してみます。
MultithreadSample.java
package SearchContainedFiles; class MultithreadSample implements SampleBehaviour{ private volatile int count; public void add(){ count++; } public void subtract(){ count--; } public int getCount(){ return count; } }
変数に volatile をつけただけですが、果たしてどうなるか......
call1 13 call2 14 call: -363
デスヨネー。
volatile はアトミック性は保証しないため、今回の実験結果としては特に変化がありません。
3. synchronized を付けた場合
~省略~ class MultithreadSample implements SampleBehaviour{ private int count; public synchronized void add(){ count++; } public synchronized void subtract(){ count--; } ~省略~
結果は
call1 13 call2 14 call: 0
正しく 0 になりました。
興味深いのは、下記のようにコンソールに出力されるようにしてみると......
~省略~ class MultithreadSample implements SampleBehaviour{ private int count; public synchronized void add(){ System.out.println("add " + count); count++; } public synchronized void subtract(){ System.out.println("subtract " + count); count--; } ~省略~
このような出力結果になります。
subtract 0 subtract -1 subtract -2 subtract -3 subtract -4 ~省略~ subtract -84 subtract -85 subtract -86 subtract -87 subtract -88 add -89 add -88 add -87 add -86 add -85 ~省略~ add 2 add 3 add 4 add 5 add 6 subtract 7 subtract 6 subtract 5 subtract 4 subtract 3 ~省略~
add() 、 subtract() の内一方の処理が実行されるときにロックがかかるため、同時に実行しているつもりでも add() -> subtract() -> add() のような順番では実行されません。
なお、synchronized を呼び出し元である App.java の add() 、 subtract() につけた場合、 add() の処理が全部終了してから subtract() が実行されることになります。
App.java
~省略~ private static synchronized int add(SampleBehaviour s){ System.out.println("call1 " + Thread.currentThread().getId()); for(int i = 0; i < 10000; i++){ try{ s.add(); } catch(IllegalArgumentException e){ System.out.println(e.getLocalizedMessage()); } } return 0; } private static synchronized int subtract(SampleBehaviour s){ System.out.println("call2 " + Thread.currentThread().getId()); for(int i = 0; i < 10000; i++){ try{ s.subtract(); } catch(IllegalArgumentException e){ System.out.println(e.getLocalizedMessage()); } } return 0; } }
このロックの機構、および処理が同時に実行されない辺りが synchronized が重い、と言われる所以でしょうか。
4.変数を AtomicInteger にした場合
実のところ、現状で今回試しているような複数のスレッドから値の変更をしたい場合、 AtomicInteger を使うのが良さそうです。
理由としてはアトミック性を持ちつつも、 synchronized より速いことが挙げられています。
MultithreadSample.java
package SearchContainedFiles; import java.util.concurrent.atomic.AtomicInteger; class MultithreadSample implements SampleBehaviour{ private AtomicInteger count; public MultithreadSample(){ // ぬるぽ注意. count = new AtomicInteger(0); } public void add(){ System.out.println("add " + count); count.incrementAndGet(); } public void subtract(){ System.out.println("subtract " + count); count.decrementAndGet(); } public int getCount(){ return count.intValue(); } }
実行の結果としては正しく 0 になります。
call1 13 call2 14 call: 0
また、先ほどの synchronized と同じように、 add() 、 subtract() がどのような順番で呼ばれているかを見てみると......
add 0 subtract 0 add 1 subtract 0 add 1 subtract 0 add 1 add 1 add 2 add 3 add 4 add 5 add 6 add 7 add 8 subtract 0 add 9 subtract 8 ~省略~
固まっているところもありますが、 synchronized と比較するとバラけているように見えます。
この AtomicInteger の中身を見てみたいところですが、長くなってきたので一旦切ります。
【Java】CompletableFuture で遊ぶ 1
はじめに
以前 Java と C# を使ってローカルファイルを開く、というのをやりました。
C# では(意味があったかどうかはともかくとして) async/await で非同期に処理を行っていましたが、これに近いことを Java で実現するにはどうすれば良いかな~というのが今回のお話です。
CompletableFuture を使う
ググってみたところ、 Java 8 から導入された CompletableFuture を使うのが良さそうだったので、試してみることにしました。
【before】App.java
package SearchContainedFiles; import java.nio.file.Path; import java.nio.file.Paths; import SearchContainedFiles.FileLoaders.FileLoader; public class App { public static void main(String[] args) { if(args == null || args.length < 2){ System.out.println("Need two args"); return; } for(Path file: FileLoader.Search(Paths.get(args[0]), args[1])){ System.out.println(file.toString()); } } }
【after】App.java
package SearchContainedFiles; import java.nio.file.Path; import java.nio.file.Paths; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.function.Supplier; import SearchContainedFiles.FileLoaders.FileLoader; public class App { public static void main(String[] args) { if (args == null || args.length < 2) { System.out.println("Need two args"); return; } Supplier< Path[]> loadedPaths = () -> FileLoader.Search(Paths.get(args[0]), args[1]); CompletableFuture< Path[]> supply = CompletableFuture.supplyAsync(loadedPaths); try { Path[] paths = supply.get(); for(Path file: paths){ System.out.println(file.toString()); } } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }
これでローカルファイルを読み込む処理( FileLoader.Search() )が非同期(別スレッド)で実行されます。
呼び出すメソッドでは( async/await をつけるような)変更が必要なく、呼び出し元の方も単語としては await のような「待つ」といった意味合いのメソッドは見当たりません。
が、ちゃんと非同期の処理が完了するまで待った上で全部の処理が完了します。
どのようにしてこれらの処理が動いているのかを追ってみることにしました。
何をしているのか
まず、 CompletableFuture を含め、 Future を使った非同期処理というのは、 Future パターン と呼ばれるデザインパターンで実行されるとのこと。
Java本格入門 で挙げられている Future パターンの例では、下記 3 つのクラスが登場します。
- ExecutorService
- Future< String>
- Callable< String>
流れとしては下記のような感じだと解釈しています。
(実際の処理は本を見てもらうとして)
では先ほどのサンプルコードが、これと同じような動きになっているのかを見てみたいと思います。
(Completable)Future が出てくるあたり完全に同じでないにしろ、近そうな予感はありますね。
ソースコード
ソースコードは OpenJDKのサイト から入手できます。
今回は下記を ZIP 形式でダウンロードしてきました(ページ左にリンクがあります)。
コードを辿る冒険
今回の処理の中心となるのは下記のクラスです。
これらはいずれも src > java.base > share > classes > java > util > concurrent にあります。
supplyAsync
コードの内容としては、まず CompletableFuture.supplyAsync(Supplier< Path>) からスタートします。
まず引数となる Supplier は、 T (ここでは Path )を返す get() を持つだけの interface です。
で、 supplyAsync は asyncSupplyStage(ASYNC_POOL, supplier) を呼びます。
この ASYNC_POOL は、 ForkJoinPool.getCommonPoolParallelism() が 1 より大きい場合は ForkJoinPool のインスタンスを返します。
また、AccessController.doPrivileged ~ により、 ForkJoinPool に付与されている権限で実行されます。
ForkJoinPool.java
~省略~ common = AccessController.doPrivileged(new PrivilegedAction< >() { public ForkJoinPool run() { return new ForkJoinPool((byte)0); }}); ~省略~
getCommonPoolParallelism() ですが、これが 1 以下になる場合、というのはシングルコアの PC ということなのでしょうか。
ちなみに Surface Pro 6(Core i5-8250U) で実行したところ、 7 が返ってきました。
コア数とイコールでもないのが気になるところですが、今回はスキップすることにします。
ForkJoinPool クラスは AbstractExecutorService を継承しており、 Future パターンで登場した ExecutorService の役割をになっているようです。
asyncSupplyStage
さて asyncSupplyStage に進みます。
ここでは ForkJoinPool.execute に AsyncSupply< U>(ompletableFuture< U> d, Supplier< U> f) を渡しています( U は今回の場合 Path[] )。
ということで、 Callbale< String> にあたるクラスは AsyncSupply< U> ということなのだと思います。
次は ForkJoinPool.java で定義されている AsyncSupply を見てみます。
AsyncSupply
ForkJoinPool.java > AsyncSupply< T>
~省略~ public void run() { CompletableFuture< T> d; Supplier< ? extends T> f; if ((d = dep) != null && (f = fn) != null) { dep = null; fn = null; if (d.result == null) { try { d.completeValue(f.get()); } catch (Throwable ex) { d.completeThrowable(ex); } } d.postComplete(); } } ~省略~
ここで気になる処理は 4 つです。
- CompletableFuture< T>.result
- CompletableFuture< T>.completeValue()
- Supplier< ? extends T>.get()
- CompletableFuture< T>.postComplete()
1.はここまで追ってきた処理には登場しなかったような気がします。
他で代入している値を見ると、複数の処理が行われた時に、先に実行されたタスクでエラーが発生した場合に、処理をスキップして終了する( 4.で )ためのもののようです。
2.は RESULT.compareAndSet(this, null, (t == null) ? NIL : t) ( t は Supplier< ? extends T>.get() )を返しています。
この RESULT が何かというと、このような内容になっています。
( try ~ catch などは省いています)
MethodHandles.Lookup l = MethodHandles.lookup(); VarHandle RESULT = l.findVarHandle(CompletableFuture.class, "result", Object.class);
これにより、 CompletableFuture にある volatile Object result; への参照が取得できます。
VarHandle は 動的に変数への強い型を持った参照を得るためのもので、ここでは CompletableFuture の result に対する参照を取得している、と。
なおここでアクセスチェックが行われるために、 AccessController.doPrivileged ~ が必要となるようです。
2.に戻ると、 compareAndSet によって Supplier< ? extends T>.get() の戻り値を result にセットしているようです。
ここは後でもう少し調べてみることにします。
最後 4.ですが、処理完了後に実行するために登録されている処理を、順に実行して外していく、ということを行っているようです。
が、ここだけに限らないのですが、 CompletableFuture をはじめ同じ型のインスタンスが複数登場して混乱してきているので、後日修正するかもしれません/(^o^)\
compareAndSet について
さて、ちょこちょこと登場している VarHandle.compareAndSet 。
これが何をしているのかちょっとだけ調べてみることにしました。
例えばこのようなクラスがあったとして。
VarHSample2.java
package SearchContainedFiles; class VarHSample2{ String message; void call(){ System.out.println("VarHSample2 " + message); } }
このようにすると、 VarHSample2.java の message の中身が変更されます。
VarHSample.java
package SearchContainedFiles; import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; class VarHSample { void call(){ MethodHandles.Lookup l = MethodHandles.lookup(); VarHandle handle; VarHSample2 sample2 = new VarHSample2(); try{ // 引数は 1. ターゲットとなるクラス 2. 変更したい変数名 3. 2.の型 handle = l.findVarHandle(VarHSample2.class, "message", String.class); // 引数は 1. findVarHandle の第一引数のインスタンス // 2. findVarHandle の第二引数の変数の処理実行前の値 // 3. 2.の値が変数と等しかった場合に代入される値 boolean result = handle.compareAndSet(sample2, null, "hello!"); System.out.println(result); // true } catch(IllegalAccessException e){ System.out.println(e.getMessage()); } catch(NoSuchFieldException e){ System.out.println(e.getMessage()); } sample2.call(); // 「VarHSample2 hello!」と出力される. } }
- findVarHandle で、ターゲットとなる変数は、呼び出し元である VarHSample.java からアクセス可能である必要があります(アクセス不可の場合、実行時にエラー)。
- findVarHandle と compareAndSet の第一引数の型は同じである必要があり、違っていると実行時にエラーがでます。
- compareAndSet の第二引数の値が、 findVarHandle の第二引数で指定する変数と異なる場合、戻り値は false となり、変数の値はそのままになります。
上記を踏まえてもう一度先ほどのコードを見てみると...
MethodHandles.Lookup l = MethodHandles.lookup(); VarHandle RESULT = l.findVarHandle(CompletableFuture.class, "result", Object.class); // T は Supplier< ? extends T>.get() の戻り値. RESULT.compareAndSet(this, null, (t == null) ? NIL : t)
ForkJoinPool.java > AsyncSupply< T> の completeValue 実行時に、 result が null で、かつ Supplier< ? extends T>.get() が null でなかった場合に T (今回は Path[] )が代入される、という動きになっているようです。
ここで一旦切りまして、次は get() で処理の完了を待ち受けてみたいと思います。
ASP.NET Core のアプリを Application Request Routing + IIS で動かしてみる
はじめに
相変わらずコロコロ話題が変わっていますが。。。お察しください。
タイトル通り、 ASP.NET Core で作ったアプリを IIS を使って動かしたい話です。
ASP.NET Core アプリの発行は以前挑戦しましたが、この時は Kestrel で動かしていただけでした。
ASP.NET Core アプリを IIS で実行する方法として、大きく 2 つあります。
- Application Request Routing を使う
- ASP.NET Core Module を使う
今回は Application Request Routing を試してみることにします。
Application Request Routing (以下 ARR)という IIS の Extension を使うことで、 IIS で指定した URL にアクセスがあった場合に、localhost:5000 で動かしている ASP.NET Core アプリにリダイレクトして表示することができるようになります(リバースプロキシ)。
ASP.NET Core に組み込まれている Kestrel は、セキュリティなどの機能が IIS に比べて劣るとのことですが、この方法により外部からのアクセスは IIS で処理し、対象となる URL へのアクセスがあった場合のみ対応すれば良いことになります。
また ARR のインストール( + そのための Web Platform Installer (以下 Web PI )のインストール)は必要ですが、後述の self-contained application として発行していれば実行するサーバーマシンに .NET Core がインストールされていなくても動作させられたり、既存の環境をあまりいじらなくても良い、というのが利点だと思っています。
ASP.NET Core アプリの準備
ASP.NET Core アプリを作る
いつものように Empty テンプレートで ASP.NET Core プロジェクトを作成します。
で、 ASP.NET Core アプリ自体は localhost でのみアクセスできれば良いため、この状態で発行してしまいます。
self-contained application として発行する
self-contained application として発行することで、アプリを動作させるマシン上に .NET Core がインストールされていなくても動作させることが可能になります。
方法は .csproj ファイルに実行環境( Runtime )の情報を追加することと、発行時にそれを指定するだけです。
RunOnIisSample.csproj
< Project Sdk="Microsoft.NET.Sdk.Web"> < PropertyGroup> < TargetFramework>netcoreapp2.2< /TargetFramework> < RuntimeIdentifiers>win-x86< /RuntimeIdentifiers> < /PropertyGroup> < ItemGroup> < PackageReference Include="Microsoft.AspNetCore.App" /> < PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" PrivateAssets="All" /> < /ItemGroup> < /Project>
発行
発行はいくつか方法があると思いますが、今回はコマンドで。
dotnet publish -c Release -r win-x86
これでプロジェクトの bin/Release/netcoreapp2.2/win-x86/publish 以下に、依存する dll ファイルとともに実行ファイルである RunOnIisSample.exe が出力されます。
IIS の準備
IIS Extension のインストール
※ IIS のインストール自体が完了していない場合は先にそちらを完了させてください。
まず ARR をインストールするのに必要な、 Web PI をインストールします。
インストール後、直接 Web PI を開くか、 IIS マネージャーから起動します。
検索で ARR を探し、インストールします( ver.3.0 を選びました)。
インストールが終わったら IIS マネージャーを再起動します(開いている場合)。
インストールした項目が追加されていれば完了です。
IIS にアプリケーションを追加
まず URL 書き換えのベースとなる URL の登録を行います。
C:\inetpub\wwwroot に空のフォルダーを追加します。
※ 本来はアプリケーションのファイルを置くのですが、今回は URL を書き換えるためだけに用意するため、空で問題ありません。
(ただしもう少しちゃんとした方法はあるかも)
IIS マネージャーを起動し、左のツリーで サイト > Default Web Site を開きます
(最新の状態になっていれば、先ほどのフォルダーも表示されているはずです)。
Default Web Site 上で右クリック > アプリケーションの追加 から、アプリケーションを追加します。
サイトの URL + エイリアス (今回は http://localhost:80/samplesite )でアクセスできるようになります。
物理パスは先ほどの空フォルダーを指定します。
これで http://localhost:80/samplesite という URL が使用可能になるわけですが、中身は空であるためアクセスしてもエラーになります。
Proxy の有効化
左のツリーのトップ(今回は DESKTOP-3I579DO~ )をクリックして ARR を開き、右のメニューから Proxy > Server Proxy Settings... を開き、 Enable proxy にチェックを入れて有効化します。
余談ですが、下の Proxy Type で、 Use URL Rewrite to inspect incomming request にチェックを入れて Reverse proxy に URL を設定すると、そのサイトに登録されている全 URL が Reverse proxy で指定した URL に飛ばされます。
便利な場合もあるとは思いますが、今回は個別に設定したいためチェックは入れずに置いておきます(なぜ説明した)。
URL 書き換え
もう一度作成した samplesite をクリックし、 URL書き換え を開きます。
右のメニューから 規則の追加 を起動し、 受信規則と送信規則 > リバースプロキシ を選択します。
受信規則のホストとして、 ASP.NET Core アプリで指定している localhost:5000 を設定します。
この状態で ASP.NET Core アプリを起動し、 http://localhost:80/samplesite にアクセスすると、 http://localhost:5000 を開いた時と同じものが表示されます。
今回 IIS も localhost でアクセスしているせいであまりありがたみは感じられないのですが、これで IIS を外部公開しさえすれば ASP.NET Core アプリを外部から表示することができるようになります。
Static File を読み込む
JavaScript や CSS などの Static File を wwwwroot に置き、 IIS を使ってページを開いたところ、正しく読み込めませんでした。
という話をしたいのですが、 http://localhost:80/samplesite で開いたところ読み込めてしまったため、先に hosts ファイルを使って localhost から masanori.example.jp というドメインに変更してみます。
hosts ファイルによるドメイン変更
C:\Windows\System32\drivers\etc にある hosts ファイルを管理者として開き、下記を追加します。
hosts
~省略~ {マシンのIPアドレス} masanori.example.jp ~省略~
※ ドメイン名はお好みで変更してください。
で、 IIS マネージャーで masanori.example.jp というサイトを作り、先ほどと同じ手順でアプリケーションを追加します。
内容がかぶらなければ先ほどのアプリケーションは放置で問題ありませんが、使わないので消してもらってもよいと思います。
物理パスは同じフォルダーを指定してやれば、 URL の書き換え設定はそのまま有効になっていると思います。
http://masanori.example.jp:8088/samplesite2 のような URL で、 ASP.NET Core のページが表示されれば OK です。
Static Files のパスが合わない
(バグを再現する)準備が整ったところで、下記のファイルを追加していきます。
- RunOnIisSample
HomeController.cs
using Microsoft.AspNetCore.Mvc; namespace RunOnIisSample.Controllers { public class HomeController: Controller { [Route("")] public IActionResult Index() { return View("/Views/Index.cshtml"); } } }
_Layout.cshtml
< !DOCTYPE html> < html lang="ja"> < head> < meta charset="utf-8" /> < title>@ViewData["title"]< /title> < link rel="stylesheet" type="text/css" href="src/css/site.css"/> < /head> < body> @RenderBody() < script src="src/js/site.js">< /script> < /body> < /html>
_ViewStart.cshtml
@{ Layout = "_Layout"; }
Index.cshtml
@{ ViewData["title"] = "Home"; }
site.js
alert("hello");
site.css
body{ background-color: aqua; }
特筆すべきこともないというか、ほとんど空っぽの状態です。
localhost:5000 でページの色が変わること、アラートが表示されることは確認しておきます。
問題を確認
んで、 http://masanori.example.jp:8088/samplesite2 を開くと、 JavaScript も CSS も読み込まれていません。
Firefox の開発者ツールで確認したところ、 JavaScript について下記のようなエラーが出ていました。
“http://masanori.example.jp:8088/src/js/site.js” からのスクリプトが読み込まれました。しかし、この MIME タイプ (“text/html”) は正しい JavaScript の MIME タイプではありません。[詳細] samplesite2 < script> のソース “http://masanori.example.jp:8088/src/js/site.js” の読み込みに失敗しました。
最初 MIME タイプの内容に気を取られてしまい、以前試したように Controller からファイルを返してみる、といったことも試していました。
が、原因は何のことはない、 URL が違うから、ということです orz
「samplesite2」の部分が抜けてしまっているために、 404 エラーが発生していました、と。
なぜ http://localhost:80/samplesite だと問題なく、 http://masanori.example.jp:8088/samplesite2 だとエラーになるのかは不明ですが、とにかく何とかしてみることにします。
IHostingEnvironment.EnvironmentName で切り分け
例えば実行環境が Development だった場合は ~/src/js/site.js を、それ以外は ~/samplesite2/src/js/site.js を開くようにします。
※根本的な解決ではない
HomeController.cs
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; namespace RunOnIisSample.Controllers { private readonly string _additionalPath; public HomeController(IHostingEnvironment env) { _additionalPath = (env.EnvironmentName == "Development") ? "" : "samplesite2/"; } [Route("")] public IActionResult Index() { ViewData["additional_path"] = _additionalPath; return View("/Views/Index.cshtml"); } } }
- コンストラクターで ViewData に値を渡していないのは、そのタイミングで値を入れても View 側で受け取れなかったためです。読み込む前にリセットされるのかもしれません。
_Layout.cshtml
< !DOCTYPE html> < html lang="ja"> < head> < meta charset="utf-8" /> < title>@ViewData["title"]< /title> < link rel="stylesheet" type="text/css" href=@ViewData["additional_path"]"src/css/site.css"/> < /head> < body> @RenderBody() < script src=@ViewData["additional_path"]"src/js/site.js">< /script> < /body> < /html>
一つ一つ設定していくのは面倒なので、一括で処理したいところではあるのですが。。。
次回に続く。。。かもしれない。
参照
IIS
- IIS: C#コードによるARR 3.0 RTMのインストールと設定 - Build Insider
- Reverse Proxy with URL Rewrite v2 and Application Request Routing - Microsoft Docs
ASP.NET Core
- IIS を使用した Windows での ASP.NET Core のホスト - Microsoft Docs
- .NET Core 2.0 – How to publish a self-contained application – Luís Henrique Demetrio
hosts
【Java】【C#】ローカルのファイルを開いて指定の文字列が含まれているか調べてみる
はじめに
諸事情により、指定したディレクトリ以下にあるファイルの中から、特定の文字列が含まれるファイルを調べたくなりました。
このような希望を叶えてくれるツールはきっとたくさんあると思いはするのですが、自分でやってみたかったので作ってみることにしました。
言語は Java で。理由は興味がある以外にはありません。
ただ、 Java でいきなり書き始めると書くのが久しぶりすぎて迷走しそうな気がしたので、まず C# で作ったあとできるだけ同じ動きとなるように Java で作ってみることにします。
あ、 OS はいつも通り Windows 10 です。
C# で書く
まずは C# で書いてみます。
環境は .NET Core ver.2.2.101 で、コンソールアプリとして作ります。
Main メソッドで async/await を使いたいので C# 7.1 にしておきますよ。
SearchContainedFiles.csproj
< Project Sdk="Microsoft.NET.Sdk"> < PropertyGroup> < OutputType>Exe< /OutputType> < TargetFramework>netcoreapp2.2< /TargetFramework> < LangVersion>7.1< /LangVersion> < /PropertyGroup> < /Project>
Program.cs
using System; using FileLoaders; using System.Threading.Tasks; namespace SearchContainedFiles { class Program { static async Task Main(string[] args) { if(args == null || args.Length < 2){ Console.WriteLine("Need two args"); return; } foreach(string file in (await FileLoader.SearchAsync(new Uri(args[0]), args[1]))){ Console.WriteLine("File: " + file); } } } }
FileLoader.cs
using System; using System.IO; using System.Collections.Generic; using System.Threading.Tasks; namespace FileLoaders { public class FileLoader{ public static async Task< List< string>> SearchAsync(Uri rootDirectoryPath, string keyword){ if(Directory.Exists(rootDirectoryPath.AbsolutePath) == false){ return new List< string>(); } List< string> files = new List< string>(); foreach(string file in Directory.GetFiles(rootDirectoryPath.AbsolutePath, "*", SearchOption.AllDirectories)){ if(await CheckKeywordExistAsync(file, keyword)){ files.Add(file); } } return files; } private static async Task< bool> CheckKeywordExistAsync(string filePath, string keyword){ using(StreamReader stream = new StreamReader(filePath)){ while(stream.Peek() >= 0){ if(stream.Peek() < 0){ break; } if((await stream.ReadLineAsync()).Contains(keyword)){ return true; } } } return false; } } }
コマンドライン引数で渡されたパス以下のファイルパスを一括取得 -> 1つずつ開いて確認しています。
Java で書く
次は Java です。
環境は下記の通り。
- OpenJDK ver.12.0.1
- Gradle ver.5.4.1
Gradle は gradle init でプロジェクト作るとかビルドとか実行とかに使ってます。
こちらもコマンドラインアプリで。
Files.lines でファイルを読む(失敗)
ネットの情報などを頼りにまずは書いてみることにします。
App.java
package SearchContainedFiles; import java.nio.file.Path; import java.nio.file.Paths; import SearchContainedFiles.FileLoaders.FileLoader; public class App { public static void main(String[] args) { if(args == null || args.length < 2){ System.out.println("Need two args"); return; } for(Path file: FileLoader.Search(Paths.get(args[0]), args[1])){ System.out.println(file.toString()); } } }
FileLoader.java
package SearchContainedFiles.FileLoaders; import java.io.IOException; import java.nio.file.FileVisitOption; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.util.stream.Stream; public class FileLoader{ public static Path[] Search(Path rootDirectory, String keyword) { if(Files.notExists(rootDirectory, LinkOption.NOFOLLOW_LINKS)){ return new Path[0]; } try(Stream< Path> files = Files.walk(rootDirectory,FileVisitOption.FOLLOW_LINKS) .filter(f -> f.getFileName().toString().matches("..*[.][a-zA-Z]+$"))){ return files.filter(f -> CheckKeywordExistAsync(f, keyword)) .toArray(Path[]::new); } catch(IOException e){ System.out.println(e.getMessage()); } return new Path[0]; } private static boolean CheckKeywordExist(Path filePath, String keyword){ try{ return Files.lines(filePath) .anyMatch(p -> p.contains(keyword)); } catch(IOException e){ System.out.println(e.getMessage()); return false; } } }
- Files.walk は指定のパス以下のフォルダ・ファイルの Path を一括で取得します。ファイルだけに絞りたかったため、正規表現でフィルタリングしてみました(後述しますが、素直にディレクトリかどうかを確認した方が良さそうです)。
- C# の async/await 相当の処理ができないかどうかはいったん後回しにしています。
そしてエラー
これを実行すると、 Files.lines(filePath) の部分で MalformedInputException が発生しました。
Exception in thread "main" java.io.UncheckedIOException: java.nio.charset.MalformedInputException: Input length = 1
開こうとしたファイルの文字コードが違っていると。
BufferedReader でファイルを読む 1(失敗)
FileLoader.java
package SearchContainedFiles.FileLoaders; import java.io.BufferedReader; import java.io.IOException; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.nio.charset.Charset; import java.nio.file.FileVisitOption; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.util.stream.Stream; import java.io.InputStreamReader; ~省略~ private static boolean CheckKeywordExistAsync(Path filePath, String keyword){ try(InputStreamReader streamReader = new InputStreamReader( new FileInputStream(filePath.toString()))){ try(BufferedReader reader = Files.newBufferedReader(filePath, Charset.forName(streamReader.getEncoding()))){ return reader.lines() .anyMatch(p -> p.contains(keyword)); } catch(IOException e){ System.out.println(e.getMessage()); return false; } } catch(FileNotFoundException ex){ System.out.println(ex.getMessage()); return false; } catch(IOException ex){ System.out.println(ex.getMessage()); return false; } } }
何か色々見失いつつあるような気がしますが。。。
エンコードが違っているのが原因であれば、ファイルのエンコーディングを取得してセットしてやれば良いのでは?
と思ったのですが、結果は特に変わりませんでした。
なお、エラーが起きるファイルのエンコーディングは MS932 でした。
ぐぬぬ。。。
getEncoding ではなく直接 Charset.forName("MS932") と指定した場合も結果は変わりませんでした。
なんでや。。。
BufferedReader でファイルを読む 2(成功)
結局 Files.~ を使うのをやめて new BufferedReader としたところ、問題なく読み込めるようになりました。
しかもエンコーディング指定なしで。
FileLoader.java
~省略~ private static boolean CheckKeywordExistAsync(Path filePath, String keyword){ try(BufferedReader reader = new BufferedReader( new InputStreamReader( new FileInputStream(filePath.toString())))){ return reader.lines() .anyMatch(p -> p.contains(keyword)); } catch(IOException ex){ System.out.println(ex.getMessage()); return false; } } ~省略~
問題が解決したのは良いですが、なんとも釈然としない。。。(´・ω・`)
犯人はヤ……俺
ググった限り、別に MS932 のファイルを読むことができない、という話ではなさそうです。
ではなぜ?と思い、ファイルを一つずつ見ていったところ、問題が発生するのは .doc や .zip のような、テキストエディターでは開けない(文字化けしたような文字の羅列として表示される)ファイルでした。
あまり何も考えずにパス指定してそこにあるファイルを読み込む、といったことをしていたために、テキストとして扱えないファイルが混ざってエラーになっていた、と。
では逆にエラーにならずに読み込めたこの三つのクラス、
役割としては、まず画像データなどの raw データをストリームに変換できる FileInputStream がデータを読み込み、 InputStreamReader が文字型入力ストリームに変換 -> BufferedReader で文字列データを読み込む、ということのようです。
raw データから文字型入力ストリームに変換せずに、文字型でないデータを読み込んでしまうとここまで見てきたような問題が発生する、ということのようです。
.doc や .zip ファイルは、一行ずつ読み込めたとして検索できないような気がするのですが、変換なしで文字型入力ストリームとして扱えるかを確認する方法はあるのでしょうか。。。?
(今回調べた限りだと、実際に読み込んでみてエラーになるかを確認するか、拡張子を指定するぐらいしか思いつきませんでした)
というところまでを考えると、確実にテキストファイル(として扱えるファイル)のみを読み込む場合を除き、FileInputStream 、 InputStreamReader 、 BufferedReader を使うのが良いような気はしました。
まぁ今回は外部のライブラリ、フレームワークなどを( Gradle 以外)極力使わないようにしたのですが、それらを含めるともう少し選択肢もありそうですね。
いったん切ります。
参照
Java
- FileInputStream
- InputStreamReader
- BufferedReader
- Files (Java Platform SE 8 )
- Javaファイル関連メモ(Hishidama's Java File Memo)
- Javaファイル関連メモ2(Hishidama's Java Files Memo)
- Java本格入門
Gradle
【ASP.NET Core】【TypeScript】PDF のダウンロード・アップロード 2
はじめに
平成 <--> 令和 をまたいでしまいましたが、続きです。
(令和になりましても引き続きよろしくお願いいたします (..)_ )
FormData を使ってファイル送信する場合の課題として、ファイルの容量制限が、少なくともデフォルトだとかなり厳しいことが挙げられます。
テキストファイル程度なら良いのですが、動画などになってくると数十 MB を超えたりします。
これをそのまま FormData として Body に入れて渡そうとすると BadHttpRequestException が発生します。
info: Microsoft.AspNetCore.Server.Kestrel[17] Connection id "XXXXXXXXXXXXX" bad request data: "Request body too large." Microsoft.AspNetCore.Server.Kestrel.Core.BadHttpRequestException: Request body too large. at Microsoft.AspNetCore.Server.Kestrel.Core.BadHttpRequestException.Throw(RequestRejectionReason reason) at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.Http1MessageBody.ForContentLength.OnReadStarting() at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.MessageBody.TryStart() at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.MessageBody.ConsumeAsync() at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application) at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequestsAsync[TContext](IHttpApplication`1 application)
Microsoft Docs によれば容量が大きいファイルのアップロードには stream を使う、という話があります。
ということで、これを試してみることにします。
Stream を使って PDF をアップロードする(失敗)
まぁ失敗というかなんというか。
サンプルでは Razor が使われていて、 View と Controller (コードビハインド?) で直接データのやり取りができると。
そのため Body に格納して渡すのではなく、 MemoryStream で直接メモリーに対象のファイルデータを読み込んでいるようでした。
まぁ素直に Razor 使えばよいのですが、今回は View 部分を完全に切り離せるようにしたいため、別の方法を探ります。
で、その方法って?
で、その方法なのですが、そもそも C# (Controller) 側が空の状態であっても、 Request の Body に格納して送信した時点で先ほどの Exception が発生するため、 View 側で何とかする必要がありそうです。
結局どうしたか
あれこれ調べてみたのですが、結局のところはファイルを分割して送信 -> C# (Controller) 側でひとまとめにする、という処理が必要になるようでした。
ということでこの方法を試してみます。
ファイルを分割してアップロード
ファイルの分割
ファイルを指定のサイズで分割する方法の一つに、 FileReader の readAsArrayBuffer を使うものがあります。
readAsArrayBuffer でファイルの内容を ArrayBuffer として読み込み、それを指定サイズ(バイト)ごとに分割して送信します。
- FileReader.readAsArrayBuffer() - Web API - MDN
- JavaScriptのStreams APIで細切れのデータを読み書きする - Subterranean Flower Blog
- google chrome - Split an uploaded file into multiple chunks using javascript - Stack Overflow
- JavaScript FileReaderを使って、ファイルをバイナリで読み込む - Qiita
mainPage.ts
exports.uploadFile = async function uploadFile(){ const fileField = document.getElementById("upload_file_form") as HTMLInputElement; if(fileField === null || fileField.files === null || fileField.files.length <= 0){ return; } // 本当は保存先のディレクトリ作成とか必要だと思います. const reader = new FileReader(); let buffer: Uint8Array; // 約 8MB (実際は 7.62MB でした)ずつ分割. const size = 8000000; // 送信は前回と同じく FormData に. let formData: FormData; const file = fileField.files[0]; // 内容が読み込まれたらファイル送信開始. reader.onload = async function(e: ProgressEvent) { // 参考リンクでは e.target.result になっているものがありますが, // 見つからなかったので FileReader から取得. const result = reader.result; if(result === null){ console.error("result is null"); return; } // 読み込んだ内容から Uint8Array を作り、指定サイズごとに分割 -> 送信. buffer = new Uint8Array(result as ArrayBuffer); let failed = false; let fileIndex = 0; for(let i = 0; i < buffer.length; i += size){ failed = false; formData = new FormData(); formData.append('file', new Blob([buffer.subarray(i, i+size)])) await fetch('files/pdf', { method: 'POST', body: formData, headers: { "FileName": file.name, 'FileIndex': fileIndex.toString(), } }) .catch(error => { console.error('Error:', error); failed = true; }); if(failed){ console.log('failed'); return; } fileIndex = fileIndex + 1; // このタイミングでプログレスバーをいじったりすると良さそうです. } // ファイルを送信し終わったらひとまとめにする処理を実行. var endFormData = new FormData(); endFormData.append('fileName', file.name); await fetch('files/pdf/finished', { method: 'POST', body: endFormData, }) .then(response => console.log('ok')) .catch(error => console.error('Error:', error)); } // ファイルの内容を読みこむ. reader.readAsArrayBuffer(file); }
- 最初 PDF で試していたため URL が PDF になっていますが、手元にあるファイルサイズの関係で PDF 以外のファイルも使っていますがお気になさらず。。。
Uint8Array について
new Uint8Array(result as ArrayBuffer) のような形で生成している Uint8Array 。
見た目的には C# の List のように、中の値をコピーして新しい配列を作っているのかな? とも思ったのですが、↓ Uint8Array.prototype.buffer を見ると元の ArrayBuffer への参照を保持しているようにも思えます。
この引数について、ファイルサイズが正しい場合中身が間違っていてもエラーが発生せずに 分割 -> 送信 と処理が進んでしまうため、マージ完了後に初めてファイルが正しくないことに気づく、という落とし穴があるため注意が必要です。
(参考にしたサイトで使われていた ProgressEvent.target.result が見つからなかったのですが、代替となるデータを渡し間違えてえらい苦労をしましたorz )
Header について
fileField.files[0] をそのまま渡していた場合と異なり、ArrayBuffer として分割したデータからはファイル名が取得できません。
そのため何らかの方法で送信する必要があります。
また分割データを順番通り復元するには、送信したデータの番号も送ってやる必要があります。
Body の FormData に追加することもできますが、今回は Header に入れることにしました。
理由は試したときに Body に複数データをセットした場合のサーバー側での受け取り方がわからなかったのと、エンティティヘッダーでは「メッセージボディの内容を記述する」とあったためです。
分割されたデータの受け取り
サーバー側です。
前回と同じく引数を IFormFile にして受け取ることも可能です。
が、 Microsoft Docs も参考にカスタムクラスで受け取ってみることにしました。
UploadFile.cs
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; public class UploadFile{ [FromForm(Name="file")] public IFormFile File{get; set;} }
HomeController.cs
using System.Collections.Generic; using System.Text; using System; using System.IO; using System.Net.Http.Headers; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.FileProviders; using Microsoft.Net.Http.Headers; using System.Net.Http; using System.Linq; namespace Controllers { public class HomeController : Controller { private static readonly string TmpLargeFileDirectory = @"C:/Users/XXX/workspace/DownloadUploadSample/files/tmp/"; [HttpPost] [Route ("/files/pdf")] public async Task< IActionResult> UploadPdfFile (UploadFile uploadFile) { string fileName = Request.Headers["FileIndex"] + "_" + Request.Headers["FileName"]; if(uploadFile?.File == null){ Console.WriteLine("failed"); return BadRequest(); } using(var stream = new FileStream(TmpLargeFileDirectory + fileName, FileMode.Create)){ await uploadFile.File.CopyToAsync(stream); } return Ok(); } } }
ほぼ前回と同じです。
カスタムのクラスを使うことで、複数データを渡す場合も名前指定で受け取ることができます。
今回は端折ってますが、ファイルアップロードの開始時点でファイルの保存先を作る、同時に別のユーザーがアップロードしたときに混ざらないようにするなどの処理が実際には必要になります。
Request.Body
データの受け取りについて、 fetch では Body にセットしているのだから、 Request.Body から受け取れないかな~と思ってみたのですが、データをうまく変換できず、断念しました。
ファイルをひとまとめにする
最後に出力されたファイルを一つにまとめます。
ローカルのファイルを読み込む・書き出す辺りは以前ローカルのファイルを読み込んだときの流用です。
HomeController.cs
~省略~ [HttpPost] [Route ("/files/pdf/finished")] public async Task< bool> FinishUploading(string fileName){ List< byte> readBytes = new List< byte>(); using(PhysicalFileProvider provider = new PhysicalFileProvider(TmpLargeFileDirectory)){ foreach(IFileInfo fileInfo in provider.GetDirectoryContents(string.Empty) .Where(f => f.IsDirectory == false) .OrderBy(f => { string[] fileNames = f.Name.Split('_'); int.TryParse(fileNames[0], out int index); return index; })){ using(Stream reader = fileInfo.CreateReadStream()){ int fileLength = (int)fileInfo.Length; byte[] newReadBytes = new byte[fileLength]; reader.Read(newReadBytes, 0, fileLength); readBytes.AddRange(newReadBytes); } } } using (FileStream stream = new FileStream(TmpLargeFileDirectory + fileName, FileMode.Create)) { await stream.WriteAsync(readBytes.ToArray(), 0, readBytes.Count); } // 本当はこの後分割保存したファイルを削除した方が良いと思います. return true; } ~省略~
おわりに
一応これでサイズが大きなファイルでもアップロードできるようにはなりました。
ただ、ファイルを分割 -> 保存 してもう一度読み込み -> マージして保存 とするのは無駄が大きいように思います。
下記は Rails + nginx ではありますが、最終 Rails ではなく nginx の機能を使って速度を上げています。
これを考えると、もう少し別の方法も考慮した方が良いように思いました。
冒頭でスキップしてしまいましたが、せっかくの ASP.NET Core なので、 Razor を使ってみるのも良いかもしれませんね。
(本当は今回試すつもりでしたが、分割アップロードで躓いて心が折れました。という言い訳)