【TypeScript】Decoratorを覗き見る 2
はじめに
- 【TypeScript】InversifyJSでIoC(DI)体験 1
- 【TypeScript】InversifyJSでIoC(DI)体験 2
- 【TypeScript】Decoratorを覗き見る 1
今回は Decorator Factory から見てみますよ。
Decorator Factory
正直ドキュメントを見ても Class Decorator を返す関数、というだけでどう活用できるものなのか今一つ分かっていなかったのですが、これを使うことで、 Decorator に引数を渡すことができるようになります。
decoratorFactorySample.ts
import { Sample } from "../sample"; export function decoratorFactorySample(name: string, id: number, s: Sample){ return function decoratorSample(constructor: Function){ console.log("generate id:" + id + " name:" + name + " " + constructor); } }
operationSample.ts
import { decoratorFactorySample } from "./decoratorSamples/decoratorFactorySample"; @decoratorFactorySample("john", 1, {call: () => console.log("hello sample"), message: "goodomorning"}) export class OperationSample{ public constructor(){ console.log("constructor death"); } public sayHello(){ console.log("hello world!"); } }
引数の個数や種類に制限は無いようで、上記のように増やしても問題なく実行できました。
逆に引数を 0 にすると。。。
operationSample.ts
import { decoratorFactorySample } from "./decoratorSamples/decoratorFactorySample"; @decoratorFactorySample() export class OperationSample{ public constructor(){ console.log("constructor death"); } public sayHello(){ console.log("hello world!"); } }
おや?これ InversifyJS を利用するコードで見たことあるような。。。
(まだコードを読んでいないので確証はありませんが)
なお当然とも言えますが、この後登場する Method Decorator など Class 以外の Decorator でも使用できます。
Method Decorator
お次は Method Decorator です。
名前の通りメソッドに付与する Decorator ですが、今度は引数が 3 つあります。
methodDecoratorSample.ts
export function methodDecoratorSample(target: any, propertyKey: string, discripter: PropertyDescriptor){ console.log("method decorator start"); console.log(target); console.log(propertyKey); console.log(discripter); console.log("method decorator end"); }
operationSample.ts
import { methodDecoratorSample } from "./decoratorSamples/methodDecoratorSample"; export class OperationSample{ public constructor(){ console.log("constructor death"); } @methodDecoratorSample public sayHello(){ console.log("hello world!"); } }
実行すると下記のようになります。
method decorator start Object { sayHello: sayHello(), … } sayHello Object { value: sayHello(), writable: true, enumerable: true, configurable: true } method decorator end
第一引数はメソッドを定義しているクラス、第二引数は対象のメソッド名、第三引数はメソッドの情報、ということのようです。
出力結果を見ると、 PropertyDescriptor.value は対象のメソッドであるため、 PropertyDescriptor.value.call(target) のようにすれば呼び出すことができます。
が、ドキュメントによると target を ES5 より低く設定している場合は PropertyDescriptor が undefined になるようなので、 ES3 に設定している、またはその恐れがある場合は使わない方が良いかもしれません。
なお Mastering TypeScript 3 では第三引数を optional にしていました(現行バージョンとの違いによるものかもしれませんが)。
なお、 PropertyDescriptor.value を使う以外に関数を実行する方法として、下記があります。
target[propertyKey]();
関数の呼び方
ここまで同じ関数(メソッド)を呼ぶのにいくつかの方法が登場しました。
例えばこんなクラスがあったとして
export class Sample{ private message = "たこやき"; public callMethod(){ console.log("I love " + this.message); this.message = "チーズ"; } public static callStaticMethod(){ console.log("I love " + Sample.prototype.message); Sample.prototype.message = "コーンポタージュ"; } }
これらのメソッドを呼ぶ方法として下記が挙げられます
( 6 、 7 、 8 は 2 、 3 、 4 と同じですが、static メソッドということで追加しました)。
export function doSomething(){ let sample = new Sample(); // 1 sample.callMethod(); // 2 sample.callMethod.call(sample); // 3 sample.callMethod.apply(sample); // 4 sample["callMethod"](); // -- static method -- // 5 Sample.callStaticMethod(); // 6 Sample.callStaticMethod.call(Sample); // 7 Sample.callStaticMethod.apply(Sample); // 8 Sample["callStaticMethod"](); }
1 、5 は C# でもおなじみの方法ですが、他はあまり見慣れない呼び方です。
実行結果は下記の通りです(どれがどの結果かわかるように番号を振っています)。
// 1 I love たこやき // 2 I love チーズ // 3 I love チーズ // 4 I love チーズ // 5 I love undefined // 6 I love コーンポタージュ // 7 I love コーンポタージュ // 8 I love コーンポタージュ
1 ~ 4 までのインスタンスを作って呼んでいるメソッド( prototype メソッド)は、呼び方はともかく想定通りの動きではあります。
5 は2回繰り返すと、他の static メソッド同様「I love コーンポタージュ」となります。
ということは、プロパティ message の定義時に渡している「たこやき」を受け取ることはできず、 かつ static メソッド間では状態が保持されていることがわかります。
なお最後に 1 を追加するともう一度「I love チーズ」と出力されるため、 prototype メソッド側(というか生成したインスタンス)も static メソッドの状態を見ていないことがわかります。
早速それぞれのメソッドを見ていきたいのですが( prototype も気になる)、その前に(見た目上)なじみのある書き方である 1 、5 について。
実は、 JavaScript(TypeScript) におけるクラス、というのは、関数の糖衣構文であるとのこと。
そのため先ほどのクラスは、 ES3 で変換すると下記のようになります( TypeScript Playground で確認) 。
"use strict" var Sample = /** @class */ (function () { function Sample() { this.message = "たこやき"; } Sample.prototype.callMethod = function () { console.log("I love " + this.message); this.message = "チーズ"; }; Sample.callStaticMethod = function () { console.log("I love " + Sample.prototype.message); Sample.prototype.message = "コーンポタージュ"; }; return Sample; }());
インスタンスを作って呼んでいる callMethod はその名の通り prototype を通して、 static メソッドである callStaticMethod はそのまま Sample につながっています。
いつも通り迷走感がすごいですが、今回は prototype を見てみることにしましょう。
Prototype
- クラス · JavaScript Primer #jsprimer
- プロトタイプオブジェクト · JavaScript Primer #jsprimer
- static - JavaScript - MDN
- Eloquent JavaScript
- JavaScript: The Definitive Guide
上記を見ると、まず JavaScript では、関数(メソッド)・プロパティなどの種類にかかわらず、そのほとんどが prototype という Object を継承していると。
先ほどの実験から prototype メソッドと static メソッドで見ているものが違う、ということはわかりました。
ではそれぞれどう違うのか、とりあえずコンソールに出力して比べてみます。
まずは prototype メソッドから。
mainPage.ts
export function doSomething(){ let sample = new Sample(); console.log(sample.callMethod); console.log(sample.callMethod.prototype); }
結果はこちら。
// console.log(sample.callMethod); callMethod() length: 0 name: "" prototype: {…} constructor: function callMethod() < prototype>: Object { … } < prototype>: () apply: function apply() arguments: bind: function bind() call: function call() caller: constructor: function Function() length: 0 name: "" toSource: function toSource() toString: function toString() Symbol(Symbol.hasInstance): function Symbol.hasInstance() < get arguments()>: function arguments() < set arguments()>: function arguments() < get caller()>: function caller() < set caller()>: function caller() < prototype>: Object { … } // console.log(sample.callMethod.prototype); {…} constructor: function callMethod() < prototype>: {…} __defineGetter__: function __defineGetter__() __defineSetter__: function __defineSetter__() __lookupGetter__: function __lookupGetter__() __lookupSetter__: function __lookupSetter__() __proto__: constructor: function Object() hasOwnProperty: function hasOwnProperty() isPrototypeOf: function isPrototypeOf() propertyIsEnumerable: function propertyIsEnumerable() toLocaleString: function toLocaleString() toSource: function toSource() toString: function toString() valueOf: function valueOf() < get __proto__()>: function __proto__() < set __proto__()>: function __proto__()
では static メソッド。
mainPage.ts
export function doSomething(){ console.log(Sample.callStaticMethod); console.log(Sample.callStaticMethod.prototype); }
結果です。
// console.log(Sample.callStaticMethod); callStaticMethod() length: 0 name: "" prototype: {…} constructor: function callStaticMethod() < prototype>: Object { … } < prototype>: () apply: function apply() arguments: bind: function bind() call: function call() caller: constructor: function Function() length: 0 name: "" toSource: function toSource() toString: function toString() Symbol(Symbol.hasInstance): function Symbol.hasInstance() < get arguments()>: function arguments() < set arguments()>: function arguments() < get caller()>: function caller() < set caller()>: function caller() < prototype>: Object { … } // console.log(Sample.callStaticMethod.prototype); {…} constructor: function callStaticMethod() < prototype>: {…} __defineGetter__: function __defineGetter__() __defineSetter__: function __defineSetter__() __lookupGetter__: function __lookupGetter__() __lookupSetter__: function __lookupSetter__() __proto__: constructor: function Object() hasOwnProperty: function hasOwnProperty() isPrototypeOf: function isPrototypeOf() propertyIsEnumerable: function propertyIsEnumerable() toLocaleString: function toLocaleString() toSource: function toSource() toString: function toString() valueOf: function valueOf() < get __proto__()>: function __proto__() < set __proto__()>: function __proto__()
うーん。。。 メソッド名の違いを除くとあまり違っているようには見えませんね。。。
見てるところが違っているのかしら。。。
一旦切ります。
【TypeScript】Decoratorを覗き見る 1
はじめに
ここまで InversifyJS のコードを追いかけてみたわけですが、どうもどのようにしてインスタンスを生成してきているのかがわからない。。
もちろん詳しいところは get の部分を追う必要はあるのですが、一旦その前に関連していそうな Decorator を調べてみることにしました。
Decorator について
雑に調べたところ、下記のような特徴があるようです。
- 定義されたクラスに要素(プロパティや関数など)を動的に追加できる。
- その中身は関数である。
- @関数名 のように Java におけるアノテーションのようにクラスや関数、プロパティに付与して使用する。
- Decorator の関数の引数は付与対象(クラス、関数、プロパティ)によって異なる。
- 実行されるのは Decorator が付与されたクラスが定義された時であり、インスタンス化された時ではない。
- まだ Experimental な機能であり、デフォルトでは有効になっていない。
準備
前述の通り、デフォルトでは有効になっていないため、 tsconfig.json を変更します。
Decorator には直接関係ないところも混ざっていますが、下記のように設定しました。
tsconfig.json
{ "compilerOptions": { "target": "es5", "module": "commonjs", "lib": ["dom", "es6"], ~省略~ "outDir": "dist/js", ~省略~ "strict": true, ~省略~ /* Experimental Options */ "experimentalDecorators": true, "emitDecoratorMetadata": true, } }
で、 HTML からの呼び出し用の関数と Decorator 付与用のクラスを用意します。
import/export はいつも通り webpack を使用します。
mainPage.ts
import { OperationSample } from "./operationSample"; export function doSomething(){ console.log("start"); let o = new OperationSample(); o.sayHello(); console.log("end"); }
operationSample.ts
export class OperationSample{ public constructor(){ console.log("constructor death"); } public sayHello(){ console.log("hello world!"); } }
MainPage.html
< !DOCTYPE html> < html lang="ja"> < head> < meta charset="utf-8"> < title>Decorator Sample< /title> < /head> < body> < button onclick="Page.doSomething()">hello< /button> < script src="../js/main.bundle.js">< /script> < /body> < /html>
これでボタンを押すと下記が出力されます。
start constructor death hello world! end
Class decorator
まず Class に Decorator を付与してみます。
classDecoratorSample.ts
export function classDecoratorSample(constructor: Function){ console.log("classDecoratorSample"); // OperationSample の constructor が実行される. // 戻り値は undefined. let c = constructor(); }
operationSample.ts
import { classDecoratorSample } from './classDecoratorSample'; @classDecoratorSample export class OperationSample{ public constructor(){ console.log("constructor death"); } public sayHello(){ console.log("hello world!"); } }
Class decorator として渡される関数の引数は、Decorator が付与されたクラス(今回は OperationSample )の constructor です。
そのため、上記のコードを実行するとページを読み込んだ時点で OperationSample の constructor が実行され、「constructor death」が出力されます。
なおコメントとして記載しましたが、 C# などとは違い constructor の戻り値はそのクラスのインスタンスではありません。
(undefined となる)
じゃあ初期化できるだけ?かというとそうではありません。
( constructor に限りませんが) TypeScript(JavaScript) の関数は prototype という object を継承しています。
この prototype 、デフォルトでは any 型ですが、アサーションを使って元のクラスの型( OperationSample )に変換することができます。
classDecoratorSample.ts
export function classDecoratorSample(constructor: Function){ ~省略~ // OperationSample として扱うことができる. let c = constructor.prototype as OperationSample; // hello world! と出力される. c.sayHello(); }
ちなみにこのアサーション、「as OperationSample」のクラス名が「as "OperationSample"」と文字列であっても、正しく OperationSample 型に変換してくれたりします。
classDecoratorSample.ts
export function classDecoratorSample(constructor: Function){ ~省略~ // 先ほど同様 OperationSample に変換される. let c = constructor.prototype as "OperationSample"; // hello world! と出力される. c.sayHello(); }
ただし TypeScript 上の型は "OperationSample" であるため、 c.sayHello() の部分はコンパイルエラーになりますが。。。
前回までのコードを見返したり、 get を見ないとわかりませんが、 InversifyJS で依存クラスのインスタンスを取ってくるあたりに関係していそうな気がします。
元クラスで定義されていない関数やプロパティを追加する
またこの prototype に OperationSample で定義されていないプロパティや関数を渡すことで、 OperationSample にプロパティや関数を追加できます。
classDecoratorSample.ts
export function classDecoratorSample(constructor: Function){ console.log("classDecoratorSample"); constructor.prototype.greeting = () => console.log("こんにちは"); }
あとはこのように呼んでやれば OK です。
mainPage.ts
export function doSomething(){ ~省略~ o.greeting(); ~省略~ }
が、当然ながら定義が見つからないとコンパイルエラーになります。
定義ファイルを用意することで、これを防ぐことができます。
下記を参考に src/types というディレクトリ以下に operationSample.d.ts というファイルを作成したところ、コンパイルエラーが解消されました。
operationSample.d.ts
import {OperationSample} from "../ts/operationSample" declare module "../ts/operationSample"{ export interface OperationSample{ greeting(): any; } }
定義の中身
さて気になるのはその中身。
「declare module ~」の部分は、 Ambient Modules と呼ばれる機能で、既存の JavaScript ライブラリなどを使う際に定義を追加・設定するために使われるものであるようです。
まぁコードの見た目的にも、 operationSample.ts というモジュールに対して OperationSample という interface を設定している、という内容に読めます。
んで、その OperationSample という interface についてです。
この interface の名前はクラス名と同じなわけですが、 C# 脳だとどうしても「なぜ同じ名前?エラーにならないの?」と思ってしまいます。
両者の名前が同じである必要があるのは、 TypeScript の interface が open-ended であるため(だと思います)です。
open-ended というのは、このような コードがあったとして、
interface ISample{ greeting(): void; } interface ISample{ doNothing(): void; }
最終的に下記の interface と同じ結果になる、ということです。
interface ISample{ greeting(): void; doNothing(): void; }
で、最初これを interface 間だけの話だと思っていたのですが、これにクラスを加えてみると。。。
class ISample{ } interface ISample{ greeting(): void; } interface ISample{ doNothing(): void; }
ISample というクラスが 2 つの interface を継承した状態となります。
function doSomething(){ let s = new ISample(); s.greeting(); s.doNothing(); }
まぁ実装していないので動かないのですが。。。
ということで、用意した定義ファイルによってクラス OperationSample は interface の OperationSample を継承した状態になるため、関数 greeting() が呼び出し元からも見られるようになった、というわけですね。
なるほどなるほど。面白いですねぇ :)
ということで長くなってきたので一旦切ります。
【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