きしだのはてな このページをアンテナに追加 RSSフィード

2016-06-12(日) Windows10の強制アップデートは原発や核施設を守るため

Windows10の強制アップデートは原発や核施設を守るため 17:56 Windows10の強制アップデートは原発や核施設を守るためを含むブックマーク

マイクロソフトが強引な手法をとったことの背景には、最近のセキュリティの考え方が、個別のコンピュータを守るためだけではなく社会インフラを守るためでもあると変化してきていることがあるのではないかと思います。


最近、Windows10の強制アップデートについて、あまりにも強引すぎるためにこのままではMSは信頼を失うとかいう発言を目にします。バスの案内表示にWindows10へのアップデートダイアログが表示されたことも話題になりました。

けれども、バス案内表示にWindowsアップデートの画面が出ることと、バス案内表示システムが核施設への攻撃に加担することと、どちらが危険でしょうか。


2014年には韓国の原子力発電所サイバー攻撃が行われています。

世界のセキュリティ・ラボから - 韓国原発に対するサイバー攻撃、情報の身代金を要求:ITpro

韓国の原子力発電所の制御システム自体はインターネットからは隔離されている、という話ですが、2010年のイランでの核施設攻撃ではUSBメモリ経由でWindows脆弱性が狙われ、ウラン濃縮が停止しました。

核施設を狙ったサイバー攻撃『Stuxnet』の全貌|WIRED.jp

韓国への攻撃では、原発の図面がアクセス可能になっていたということですが、もしコンピュータ構成や運用体制がわかれば、イランのときのような間接的な攻撃もやりやすくなります。


金融システムも狙われています。2013年には、韓国で、サイバー攻撃によって銀行システムが止まるということもありました。

韓国のテレビ局と銀行システムが一斉ダウン、サイバーテロか - ITmedia エンタープライズ

システムを停止するだけではなく、金銭を奪うということも行われています。

アジアの銀行を狙った一連のサイバー攻撃 北朝鮮が関与か - ITmedia ニュース


このような攻撃の前段階で多く使わるのは、多数のコンピュータからの同時アクセスで対象コンピュータの動きを不安定にさせるDDoSという手法です。このとき、攻撃に参加するコンピュータは、事前に乗っ取られた、セキュリティ的に弱いコンピュータです。OSをアップデートしていないコンピュータは、攻撃者にとって格好の餌食になります。


コンピュータセキュリティは、個別のコンピュータを守ることから、社会インフラを守ることへと対象が広がっています。アップデートしていないコンピュータが攻撃されて内部の情報が奪われるだけであれば「自分のコンピュータにはたいした情報はないしアップデートしないでもいい」ということは許されるし、もしそこでセキュリティをつかれて大事な情報が奪われても、バカだなぁとその人を笑うだけで終わりました。けど今では、セキュリティに弱いコンピュータを放置することは、社会に脅威を与えることになってきています。


マイクロソフトは、このような脅威に対して早急に、そして強引に対策する必要があると認識しているのではないでしょうか。Windowsアップデートで不評を買うよりも、Windowsが載ったコンピュータによって核施設が攻撃されることのほうが損失が大きいと。


追記 6/13 18:00

誤解されているブコメありますが、原発のシステム自体のアップデートのために強制アップデートをしているという話ではなく、そこに攻撃をかける駒になりうる一般のコンピュータを減らすために強制アップデートをしているという話です。


マイクロソフト自体がどう考えているかということに関しては、こちらにエバンジェリストの方の発言があります。

「Microsoftが変わった」って言われてるけど本当なの?よくわかんないから直接聞いてきた|CodeIQ MAGAZINE


また、Windows7でもサポートがまだありWindows Updateでの対応があるのでは、という点に関しては、Windows 7のサポートの範囲で対応できないセキュリティリスクがありうるという話があります。

「Windows 7なら安心」は危険な考え? FFRIがセキュリティリスク報告書を公開 -INTERNET Watch

KK 2016/06/14 10:03 インフラ守るって、
アップデートして、原発制御で誤作動のがヤバいですよ。
テロの確率より。

m400m400 2016/06/14 21:56 黙って、しかも寝首を掻くような方法で
無理矢理アプデさせてるから文句言われてるんでしょ。

アプデで最悪PCの機能が損なわれる可能性だってあるのに。
最高のセキュリティはPCを破壊することってか?

理由があるなら、そう宣言してからやればいい。
大っぴらにできる理由が本当にあるなら、ね。

そんなお人好しな解釈できないな、俺は。

aoao 2016/06/17 01:02 >誤解されているブコメありますが、原発のシステム自体のアップデートの〜

だったら煽るような見出しにしなければいいじゃない?
他の記事でも煽り見出しで客引きして中身カラなんでしょ?

煽るだけ煽って2件炎上コメでテンパリ弁解ってどんだけ小心者なの?

nowokaynowokay 2016/06/20 02:08 追記はここのコメントがつくより前ですね

2016-06-05(日)

mAgicTVの位置 08:23 mAgicTVの位置を含むブックマーク

ウィンドウをまちがって中途半端な位置に移動してしまうと、エラーが出て起動できなくなってしまうので、レジストリで場所を設定するときのメモ。

HKEY_CURRENT_USER/SOFTWARE/I-O DATA/MontBlanc/mtvTV/RectLeft

2016-04-19(火) 作って理解するWebフレームワーク

[]作って理解するWebフレームワーク 11:21 作って理解するWebフレームワークを含むブックマーク

前回、簡単なDIコンテナを作ってみたので、次はこれを使ってWebフレームワークを作ってみたいと思います。


Webサーバーをつくる

まず、WebフレームワークなのでHTTPサーバーが必要ですね。なので簡単なものを作ります。

とりあえずブラウザからリクエストを受け取ったら200 OKとHTMLを返すだけのサーバーです。

今回は、そこらのブラウザからアクセスできればいいや、ということで、RFCとかの仕様に準拠することは考えません。

public class Server {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSoc = new ServerSocket(8989);
        for (;;) {
            Socket s = serverSoc.accept();
            new Thread(() -> {
                try (InputStream is = s.getInputStream();
                     BufferedReader bur = new BufferedReader(new InputStreamReader(is))) 
                {
                    String firstLine = bur.readLine();
                    for (String line; (line = bur.readLine()) != null && !line.isEmpty(););
                    try (OutputStream os = s.getOutputStream();
                         PrintWriter pw = new PrintWriter(os))
                    {
                        pw.println("HTTP/1.0 200 OK");
                        pw.println("Content-Type: text/html");
                        pw.println();
                        pw.println("<h1>Hello!</h1>");
                        pw.println(LocalDateTime.now());
                    }
                } catch (IOException ex) {
                    System.out.println(ex);
                }
            }).start();
        }
    }
}

First Web Server by kishida · Pull Request #1 · kishida/tinydi


基本的なMVC

こんな感じでコントローラを登録できるMVCフレームワークを作ります。

@Named
@Path("")
public class IndexController {
    @Path("index")
    public String index() {
        return "<h1>Hello</h1>" + LocalDateTime.now();
    }
    
    @Path("message")
    public String mes() {
        return "<h1>Message</h1>Nice to meet you!";
    }
}

まず、コントローラークラスにつけるアノテーション

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Path {
    String value();
}

Contextから登録クラスをひっぱり出せるようにしておきます。

    public static Collection<Map.Entry<String, Class>> registeredClasses() {
        return types.entrySet();
    }

URLパスと処理メソッドを対応づけるための構造体を作ります。

    @AllArgsConstractor
    static class ProcessorMethod {
        String name;
        Method method;
    }

@Pathアノテーションがついたクラスの、@Pathアノテーションがついたメソッドを拾い集めておきます。

        Context.autoRegister();
        Map<String, ProcessorMethod> methods = new HashMap<>();
        Context.registeredClasses().forEach(entry -> {
            Class cls = entry.getValue();
            Path rootAno = (Path) cls.getAnnotation(Path.class);
            if (rootAno == null) {
                return; // continue
            }
            String root = trimSlash(rootAno.value());
            for (Method m : cls.getMethods()) {
                Path pathAno = m.getAnnotation(Path.class);
                if (pathAno == null) {
                    continue;
                }
                String path = root + "/" + pathAno.value();
                methods.put(path, new ProcessorMethod(entry.getKey(), m));
            }
        });

そしたら、HTTPリクエストの1行目「GET /index HTTP/1.1」とかになっているので、メソッド・パス・プロトコルを分離します。

        Pattern pattern = Pattern.compile("([A-Z]+) ([^ ]+) (.+)");
        String first = bur.readLine();
        Matcher mat = pattern.matcher(first);
        mat.find();
        String httpMethod = mat.group(1);
        String path = mat.group(2);
        String protocol = mat.group(3);

とってきたパスから対応するJavaメソッドを取ってきます。

ProcessorMethod method = methods.get(path);

パスと処理を結びつける、ルーティング処理ですね。

実際のフレームワークでは、ここがかなりキモになります。なので、作りこんでいくと、ここが肥大化するはず。


みつからなければNot Foundですね。

    if (method == null) {
        pw.println("HTTP/1.0 404 Not Found");

みつかったら、対応するビーンをとってきて、メソッドを呼び出します。

        Object bean = Context.getBean(method.name);
        Object output = method.method.invoke(bean);

んで、その結果を返します。

        pw.println("HTTP/1.0 200 OK");
        pw.println("Content-Type: text/html");
        pw.println();
        pw.println(output);

これで、なんかMVCフレームワークのような動きになります。

First MVC by kishida · Pull Request #3 · kishida/tinydi


リクエスト情報のインジェクト

そしたら、リクエスト情報を処理時に取れるようにインジェクトできる仕組みを作っておきましょうか。

リクエスト情報を格納する構造体を作っておきます。

@Named
@RequestScoped
@Data
public class RequestInfo {
    private String path;
    private InetAddress localAddress;
    private String userAgent;
}

@Namedをつけたので、コンテナには登録されるはずです。なので、リクエスト処理のときにオブジェクトを取得して、パスとかをつっこんでやります。

    RequestInfo info = (RequestInfo) Context.getBean("requestInfo");
    info.setLocalAddress(s.getLocalAddress());
    info.setPath(path);

ついでに、リクエストヘッダーを分解して、User-Agentを取るようにしておきます。

    Pattern patternHeader = Pattern.compile("([A-Za-z-]+): (.+)");
    for (String line; (line = bur.readLine()) != null && !line.isEmpty();) {
        Matcher matHeader = patternHeader.matcher(line);
        if (matHeader.find()) {
            switch (matHeader.group(1)) {
                case "User-Agent":
                    info.setUserAgent(matHeader.group(2));
                    break;
            }
        }
    }

そうすると、こんな感じでリクエスト情報がとれるようになります。

@Named
@Path("info")
public class RequestInfoController {
    @Inject
    RequestInfo requestInfo;
    
    @Path("index")
    public String index() {
        return String.format("<h1>Info</h1>Host:%s<br/>Path:%s<br/>UserAgent:%s<br/>",
                requestInfo.getLocalAddress(), 
                requestInfo.getPath(), 
                requestInfo.getUserAgent());
    }
}

ちょろちょろっと書いたにしては、なんかすごくまっとうな動きをしててびっくりです。DIコンテナにオブジェクトを管理してもらうと、いろんなことが楽になるんですね。

これが、DIコンテナを中心としていろんな機能のフレームワークが実装される理由だと思います。


Request Info by kishida · Pull Request #7 · kishida/tinydi


セッションIDを振る

さて、ここまでできて足りないのはセッションの仕組みです。セッションの仕組みを実装するには、ブラウザに対してセッションIDを割り当ててやる必要があります。

そのために、クッキーという仕組みが使われますね。レスポンスヘッダーに「Set-Cookie: foo=hoge」とか書いておくと、次のリクエストからブラウザが「Cookie: foo=hoge」とか付けてリクエストしてくれるようになる、なんかブラウザってかわいいなって思うようになる仕組みのことです。


まず現在値を覚えておくためAtomicLongを用意します。マルチスレッド対応でえらい。

        AtomicLong lastSessionId = new AtomicLong();

実際には、HashMapをそのまま使ってたり、各所マルチスレッド対応してないところだらけなのですが、今回はスレッドセーフとか気にせずに行きます。それはそれでがんばれ。


リクエストヘッダーからクッキーを取ってきます。

    case "Cookie":
        Stream.of(value.split(";"))
              .map(exp -> exp.trim().split("="))
              .filter(kv -> kv.length == 2)
              .forEach(kv -> cookies.put(kv[0], kv[1]));

クッキーにセッションIDがあればその値、なければ新しい値を取ってきます。

    String sessionId = cookies.getOrDefault("jsessionid", 
                                            Long.toString(lastSessionId.incrementAndGet()));

実際にはこのように単純に連番を振ってしまうと、セッションIDを横取りしてしまうセッションハイジャックができてしまうので、タイムスタンプなどをまぜつつハッシュ化などの処理をしないといけません。

開発時に再起動したとき、ブラウザに前のIDが残ってたりするのが不便なので、この段階でやってしまっててもよかった。


まあともかく、生成したセッションIDをクッキーとしてレスポンスヘッダーに埋め込みます。

    pw.println("HTTP/1.0 200 OK");
    pw.println("Content-Type: text/html");
    pw.println("Set-Cookie: jsessionid=" + sessionId + "; path=/");

Session ID by kishida · Pull Request #8 · kishida/tinydi


セッション情報をインジェクト

最後に、セッションスコープで値をインジェクトできるようにします。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SessionScoped {
    
}

それから、セッションスコープのビーンを管理するクラスを作ります。名前はSessionから始めると補完がめんどいことになるので不自然な名前に・・・。まあちゃんとフレームワークとして独立させればどうにかなるんだろうけど。

public class BeanSession {
    private final ThreadLocal<String> sessionId = new InheritableThreadLocal<>();
    private final Map<String, Map<String, Object>> beans = new HashMap<>();
    
    public void setSessionId(String id) {
        sessionId.set(id);
        if (!beans.containsKey(id)) {
            beans.put(id, new HashMap<>());
        }
    }
    
    public Map<String, Object> getBeans() {
        return beans.get(sessionId.get());
    }

    public boolean isSessionRegistered(String id) {
        return beans.containsKey(id);
    }
    
}

で、Contextのほうで保持できるようにして。

    static BeanSession beanSession;
    
    public static void setBeanSession(BeanSession beanSession) {
        Context.beanSession = beanSession;
    }

@SessionScopedアノテーションがついていたらそっちを使うようにする。

    if (type.isAnnotationPresent(SessionScoped.class)) {
        scope = beanSession.getBeans();
    }

あとは、スコープの寿命の長さを比較できるようにして。

    private static int scopeRank(Class type) {
        if (type.isAnnotationPresent(RequestScoped.class)) {
            return 0;
        }
        if (type.isAnnotationPresent(SessionScoped.class)) {
            return 5;
        }
        return 10;
    }

寿命の長いオブジェクトに寿命の短いオブジェクトをインジェクトするときはラップするように変更。

            if (scopeRank(type) > scopeRank(field.getType())) {
                bean = scopeWrapper(field.getType(), field.getName());
            } else {
                bean = getBean(field.getName());
            }

ここまでできたら、Webサーバー側と結びつければいいだけです。

クッキーからセッションIDを取ってきて。

    String sessionId = cookies.get("jsessionid");

セッションIDがあったら、セッションとして管理しているかどうか確認して、なければリセット。

    if (sessionId != null) {
        if (!beanSession.isSessionRegistered(sessionId)) {
            sessionId = null;
        }
    }

セッションIDがなければ新しいIDを発行。

    if (sessionId == null) {
        sessionId = Long.toString(lastSessionId.incrementAndGet());
    }

そのセッションIDを登録しておきます。

    beanSession.setSessionId(sessionId);

そうすると、こういうセッションスコープのオブジェクトを用意して。

@Named
@SessionScoped
@Data
public class LoginSession {
    boolean logined;
    LocalDateTime loginTime;
}

こんな感じのコントローラを用意すると、ログインっぽいことができます。

@Named
@Path("login")
public class LoginController {
    
    @Inject
    LoginSession loginSession;
    
    @Path("index")
    public String index() {
        String title = "<h1>Login</h1>";
        if (loginSession.isLogined()) {
            return title + "Login at " + loginSession.getLoginTime();
        } else {
            return title + "Not Login";
        }
    }
    
    @Path("login")
    public String login() {
        loginSession.setLogined(true);
        loginSession.setLoginTime(LocalDateTime.now());
        return "<h1>Login</h1>login";
    }
    @Path("logout")
    public String logout() {
        loginSession.setLogined(false);
        return "<h1>Login</h1>logout";
    }
}

なんか、Webフレームワークみたいになりました!


その他やること

とはいえ、やることはいろいろあります。

  • スコープの処理を整理
  • HTTPに準拠
  • スレッドセーフに
  • 不正なアクセスに対処

その他いろいろ。

で、まあこういうのをいちいち手組みしていては実用的なものはできないので、WebサーバーにはJettyなりなんなり使って、使えるものは使っていったほうがいいですね。

ってやっていくと、まあSpring使いましょうってことになるわけですけど。


わかったこと

ベースとしてDIがあると、フレームワークを作るときに「汚いこと」をやる必要がなくなってよい。


Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)

Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)

2016-04-06(水) 作って理解するDIコンテナ

[]作って理解するDIコンテナ 13:56 作って理解するDIコンテナを含むブックマーク

DIコンテナ使ってるけど、アノテーションってなんなの!って聞かれて、作ってみたらわかるよと答えてみたので、自分でも作ってみました。

よくわかった。


「DIコンテナ使うと何がいいの?」ということも、作ってみるとわかります。あと「DIって何がいいの?」に関しては、「DIはちょっとコードを書くのが楽になるだけで、それだけあっても仕方ない、大事なのはコンテナ」と答えるようにしてますが、コード比率からもそれがよくわかります。


まずはコンテナを作る

とりあえず1ソースの状態で。

こんな感じで、管理する型を登録できるようにします。

        static Map<String, Class> types = new HashMap<>();
        static void register(String name, Class type) {
            types.put(name, type);
        }

それで、名前を指定してオブジェクトを取り出せるようにする、と。

        static Map<String, Object> beans = new HashMap<>();
       
        static Object getBean(String name) {
            return beans.computeIfAbsent(name, key -> {
                Class type = types.get(name);
                Objects.requireNonNull(type, name + " not found.");
                return type.newInstance();
            });
        }

あとはクラスを登録して。

        Context.register("foo", Foo.class);
        Context.register("bar", Bar.class);

オブジェクトを取り出す。

        Bar bar = (Bar) Context.getBean("bar");

この時点では単なるオブジェクトプールですね。

tinydi/Main.java at da602927261402d9c9c1a197ccd12c41c5fcbb33 · kishida/tinydi


DIしてみる

さて、getBeanとかいちいちやるのは面倒なので、関連するオブジェクトは勝手に用意しておくようにしましょう。

ここでは、標準アノテーションの@Injectを使います。


オブジェクトを用意するときに、リフレクションでフィールドをとってきて、@Injectアノテーションがついてたらフィールド名でオブジェクトを取得する、と。

    private static <T> T createObject(Class<T> type)  {
        T object = type.newInstance();
        for (Field field : type.getDeclaredFields()) {
            if (!field.isAnnotationPresent(Inject.class)) {
                continue;
            }
            field.setAccessible(true);
            field.set(object, getBean(field.getName()));
        }
        return object;
    }

それでこんな感じのインジェクションができるようになります。

    @Inject
    Foo foo;

Inject! · kishida/tinydi@8bcecd7


自動登録する

registerとかやるのも面倒なので、アノテーションがついたクラスを自動で登録できるようにします。

    public static void autoRegister() {
        URL res = Context.class.getResource(
                "/" + Context.class.getName().replace('.', '/') + ".class");
        Path classPath = new File(res.toURI()).toPath().resolve("../../..");
        Files.walk(classPath)
             .filter(p -> !Files.isDirectory(p))
             .filter(p -> p.toString().endsWith(".class"))
             .map(p -> classPath.relativize(p))
             .map(p -> p.toString().replace(File.separatorChar, '.'))
             .map(n -> n.substring(0, n.length() - 6))
             .forEach(n -> {
                Class c = Class.forName(n);
                if (c.isAnnotationPresent(Named.class)) {
                    String simpleName = c.getSimpleName();
                    register(simpleName.substring(0, 1).toLowerCase() + simpleName.substring(1), c);
                }
             });
        }
    }

まず自分のクラスのクラスファイルをリソースとしてとってきて、そこからクラスパスのフォルダを取得して、その下の.classという拡張子のファイルを全部とってきて、そこからクラス名を作って読み込んでみて、@Namedがついてれば登録する、と、なんという素敵な処理。

これで、@Namedがついたクラスが自動的に登録されます。


まあ、これでDIというものができるようになったのですが、これ、全然素敵ではないですよね。

これ以降、どれだけ機能拡張していっても、DIという機能はコンストラクタインジェクションとかバリエーションが増えるだけでほとんど変わらないです。つまり、DIだけできても何もうれしくないということです。

Auto register! · kishida/tinydi@4449c53


AOPしてみる

ちょっと嬉しさを出すためにAOPしてみましょう。とはいえ汎用のAOPフレームワークにするのは大変なので、ここではアノテーションがついたメソッドが呼び出されるときに時刻とメソッド名を出力する、ということをやってみます。


それをどうやって実装するか、なのですが、登録したクラスを継承したクラスを作って、ログを出すメソッドをオーバーライドしてログ出力処理をはさむ、という方針です。

class Foo{
  @InvokeLog
  void hoge(){
    some();
  }
}

というクラスが登録されたときに

class Foo$$ extends Foo{
  void hoge() {
    log();
    super.hoge();
  }
}

というクラスのオブジェクトを作って返すようにするわけです。


ここで裏側でクラスを作らないといけません。

標準のJavaでは、Proxyという仕組みがあって求める動作が実現可能ですが、インタフェースとして登録する必要があります。

古いDIコンテナがインタフェースを必要としていたのは、そういった実装上の制約によるものです。そして実装上の制約にもかかわらず、そんなのめんどうという声に対して「インタフェースに対して実装を行うのだ!」みたいに設計方針をかかげて正当化していたわけですね。


しかしぼくたちはもう目が覚めてしまっているので、そういうたわごとにはだまされませんよ。

とはいえ、じゃあどうやって実現するかですが、バイトコード操作という闇の技術が開発されて、実行時にバイトコードを生成することができるcglibやjavassistといったライブラリが出てきました。このバイトコード操作技術が、今のJavaの「Ease of Development」を支えているといっても過言ではないと思います。


ここではJavassistを使います。

クラス名に「$$」をつけた新たなクラスを作って、元のクラスを継承させます。

            CtClass cls = pool.makeClass(type.getName() + "$$");
            cls.setSuperclass(orgCls);

で、printlnしつつオーバーライド元を呼び出すメソッドを作って追加するわけです。

                newMethod.setBody(
                                  "{"
                                + "  System.out.println(java.time.LocalDateTime.now() + "
                                          + "\":" + method.getName() + " invoked.\"); "
                                + "  return super." + method.getName() + "($$);"
                                + "}");
                cls.addMethod(newMethod);

このように、フレームワーク内でリフレクションやバイトコード操作を行ってJavaの標準的な仕組みではできないことを実現することで、言語仕様の制約をはずれた処理を、アプリケーションとしては言語仕様内で書けるようにする、というのがアノテーションやDIコンテナの利用価値だと思います。

AOP! · kishida/tinydi@18f200d


スコープを定義する

最後に、スコープを実装してみます。

スコープとしては、アプリケーションスコープとリクエストスコープが実装できれば、中間的なスコープは応用可能なので、今回はその2つのスコープを実装します。

とりあえず、何にもついてなければアプリケーションスコープということにします。また、今回は特にWebアプリケーションを作ってるわけでもないので、スレッドごとのスコープをリクエストスコープということにしておきます。


オブジェクトの保持に関しては、ThreadLocalでオブジェクト格納場所を用意すればいいです。

    static ThreadLocal<Map<String, Object>> requestBeans = new InheritableThreadLocal<>();

あとは、RequestScopedがついていればそちらにオブジェクトを登録するようにすれば大丈夫。

        if (type.isAnnotationPresent(RequestScoped.class)) {
            scope = requestBeans.get();
            if (scope == null) {
                scope = new HashMap<>();
                requestBeans.set(scope);
            }
        }

と、単にオブジェクトをスコープ単位で保持するだけならこれだけでいいのですが、DIを考えるとこれでは問題があります。

例えば、こんな感じで、RequestScopedなクラスを登録します。通常ならLombok使うところですが、ここではわかりやすさのためにsetter/getterをちゃんと書いてます。

@Named
@RequestScoped
public class Now {
    private LocalDateTime time;

    public LocalDateTime getTime() {
        return time;
    }

    public void setTime(LocalDateTime time) {
        this.time = time;
    }
    
}

そして、アプリケーションスコープのクラスにインジェクトします。

@Named
public class Bar {
    
    @Inject
    Now now;
 
    void longProcess() {
        now.setTime(LocalDateTime.now());
        System.out.println("start:" + now.getTime());
        try {
            Thread.sleep(5000);
        } catch (InterruptedException ex) {
        }
        System.out.println("end  :" + now.getTime());
        
    }
}

2秒置いて別スレッドでlongProcessメソッドを呼び出してみます。

    public static void main(String[] args) {
        Context.autoRegister();
        Bar bar = (Bar) Context.getBean("bar");
        
        ExecutorService es = Executors.newFixedThreadPool(2);
        for (int i = 0; i < 2; ++i) {
            es.execute(() -> {
                bar.longProcess();
            });
            Thread.sleep(2000);
        }
        es.shutdown();
    }

これを単純に実装している状態だと、一度nowフィールドに値を設定するとその後は値が更新されないので、最初のnowオブジェクトが使い続けられてしまいます。それでは、と2スレッド目で値を更新すると、nowフィールドはThreadLocalなどではない普通のフィールドなので、最初のスレッドで見るnowフィールドも置き換わってしまいます。

さて、どうしましょう?


ということで、ここでは裏側でこんな感じのクラスを作ってThreadLocalに保持したオブジェクトに委譲すればいけそう。

class Now$$_ extends Now{
  ThreadLocal<Now> obj;
  Now getObj(){
     if (obj.get() == null) {
       obj.set(Context.getBean("now"));
     }
     return obj.get();
  }

  public LocalDateTime getTime() {
    return getObj().getTime();
  }

  public void setTime(LocalDateTime time) {
    getObj().setTime(time);
  }
}

そこでまずオブジェクト保持用のクラスを作ります

public class LocalCache<T> {
    private ThreadLocal<T> local = new InheritableThreadLocal<>();
    private String name;

    public LocalCache(String name) {
        this.name = name;
    }
    
    public T get() {
        T obj = local.get();
        if (obj == null) {
            obj = (T) Context.getBean(name);
            local.set(obj);
        }
        return obj;
    }
}

あとは寿命の長いオブジェクトがインジェクトされるときに$$_をつけたクラスを作って

                cls = pool.makeClass(type.getName() + "$$_");
                cls.setSuperclass(orgCls);

ローカルオブジェクトを保持するフィールドを用意して

                CtClass tl = pool.get(LocalCache.class.getName());
                CtField org = new CtField(tl, "org", cls);
                cls.addField(org, "new " + LocalCache.class.getName() + "(\"" + name + "\");");

各メソッドに対応する委譲メソッドを作ればおっけー。

                    override.setBody(
                              "{"
                            + "  return ((" + type.getName() + ")org.get())." + method.getName() + "($$);"
                            + "}");
                    cls.addMethod(override);

こうすることで、寿命の長いオブジェクトに寿命の短いオブジェクトをインジェクトすることができるようになります。

ただし、メソッドをラップしているだけなので、フィールドを直接アクセスしてしまうとダメなわけです。


ここでがんばったことは、ThreadLocalをいかにうまく扱うかということです。つまり、DIコンテナでのスコープというのは、ThreadLocalを隠蔽する仕組みということですね。

RequestScope! · kishida/tinydi@6061739


まとめ

実装してみると、DIコンテナというのは、Javaのリフレクションやバイトコード操作、ThreadLocalといった、あまり美しくない部分を覆い隠してきれいなコードでアプリケーションを構築するための仕組みということがわかります。

このように、Javaの標準の言語仕様や仕組みでは対応できない部分を代わりに行ってくれるという仕組みであるために、Java以外の言語を使っている人に抽象的な言葉でDIコンテナのメリットを説明しても伝わらないわけですね。


もちろん、ただ便利な仕組みを作りこんでいけば便利になるかというとそうではなくて、やはりそこへの抽象的な意味付けとそこから導かれる「あるべき論」というのは大切で、Springはそのあたりがしっかりしているために多く使われてきたし、ここまで発展したのだと思います。


ということで、みんなオレオレDIコンテナとオレオレ方法論作ればいいと思うよ!

Javaフレームワーク開発入門

Javaフレームワーク開発入門

megascusmegascus 2016/04/09 23:06 @Inject等は標準ではありますがJava EEでないと使用できないので
http://mvnrepository.com/artifact/javax/javaee-api/7.0
が必要とか書いたほうがよいと思いました。(今更)

nowokaynowokay 2016/04/09 23:23 まあ、全部のコードだしてるわけじゃないから、githubのリンク先のpomみてもらえばわかるんでいいんじゃないかなーと。

2016-04-02(土) マニアへの説明、普通の人への説明、買う人への説明

マニアへの説明、普通の人への説明、買う人への説明 16:57 マニアへの説明、普通の人への説明、買う人への説明を含むブックマーク

アウディR8の新型が出たという朝日新聞の記事がなんかかわいかった。


車好き向けの専門サイトだと「V型10気筒エンジンをミッドシップに搭載。ギアボックスは新開発の7速Sトロニックが採用された」「軽量ボディ構造、アウディスペースフレームにより実現できた」「このフレームはアルミとCFRP(炭素繊維複合材料)により、重量はわずか200kg」と、なんかよくわからん用語と数字の羅列で説明されてます。

【アウディ R8 新型】先代比15%の軽量化、フレームはわずか200kg | レスポンス(Response.jp)


それが朝日新聞だと、こう。

「車体を頑丈にしたり軽くしたりする改良で、加速性能を向上」

なんかかわいい。

アウディ、R8を全面改良 3.2秒で時速100キロに:朝日新聞デジタル


面白いのは、その結果どうなるかという、0→100km/h 3.2秒っていうのが、マニア向けの記事には載ってなくて、そしてそれを違和感なく満足して読んじゃったところ。

いいんです、メカの説明だけで!仕様やら製法の説明だけで!


ちなみに、実際にお金を出して乗る人向けの、公式サイトだと、こう。

「過酷なルマン24時間レースにおいてもドライバーの疲労を最小限に抑えて、最長走行距離を樹立」「高級セダンを思わせる、街中での滑らかな走り」「最高の安らぎをもたらす居住空間と機能性」

Audi R8 Coupé > Audi Japan


やっぱ、お金もっている年上の方々には、快適性と高級感大事ですよねぇ〜

トラックバック - http://d.hatena.ne.jp/nowokay/20160402