Magnolia Tech

techっぽいことを書くブログです

Scalatra-Jsonがレスポンスを返す仕組み

前回のエントリで、Scalatraがレスポンスを返す仕組みについて紹介しました。Any型で返すのは型安全的にどうなの?という気持ちも有り、いつの日か変えたいという気持ちですが(Scalatra 3.0?)、現状はそんな仕組みになっています。

blog.magnolia.tech

Scalatra-JSON

前回説明した通り、ScalatraではAny型で受けたコードブロックの返り値を、パターンマッチで振り分けます。

NativeJsonSupport traitか、JacksonJsonSupport traitをmix-inすると、下記のrenderPipelineメソッドが有効になります。

github.com

github.com

Json4sがXMLをサポートしている関係で大量のXML関係のコードが含まれていますが、飛ばしながら読み進めていきましょう。

JValueResult traitが持つrenderPipelineメソッドがもっとも優先的に起動されます。対応するcaseが無ければ、orElse super.renderPipelineにより次のrenderPipelineへ処理が委譲されます。ここでのsuperが指す先はJsonOutput traitrenderPipelineメソッドになり、更にそのsuperが指す先はScalatraBase traitrenderPipelineメソッドになります。

JValueResult traitが持つrenderPipelineメソッドの一部を以下に引用します。Json4sのASTであるJValue型であれば、すぐにJsonOutput traitrenderPipelineへ移譲されていることが分かるでしょう。また、詳しくは後述しますが、case a: Any if isJValueResponse && customSerializer.isDefinedAt(a)によりJson ASTへ変換可能なcase classがJValue型として処理されていることが分かるでしょう。

  override protected def renderPipeline: RenderPipeline = renderToJson orElse super.renderPipeline

  private[this] def renderToJson: RenderPipeline = {
    case JNothing =>
    case JNull => response.writer.write("null")
    case a: JValue => super.renderPipeline(a)
    case a: Any if isJValueResponse && customSerializer.isDefinedAt(a) =>
      customSerializer.lift(a) match {
        case Some(jv: JValue) => jv
        case None => super.renderPipeline(a)
      }
    case status: Int => super.renderPipeline(status)

また、status: Intのように一旦JValueResult traitrenderPipelineで処理されるけど、すぐにsuperを呼び出しているパターンが有ることも分かるでしょう。

JsonOutput traitrenderPipelineは以下のようなコードになっていて、JSONPへ対応するコードが混じっていて分かりづらいですが、通常のJSONであればwriteJson(transformResponseBody(jv), writer)というコードでwriterに渡されていることが分かるでしょう。

override protected def renderPipeline = ({

    case JsonResult(jv) => jv

    case jv: JValue if format == "xml" =>
      contentType = formats("xml")
      writeJsonAsXml(transformResponseBody(jv), response.writer)

    case jv: JValue =>
      // JSON is always UTF-8
      response.characterEncoding = Some(Codec.UTF8.name)
      val writer = response.writer

      val jsonpCallback = for {
        paramName <- jsonpCallbackParameterNames
        callback <- params.get(paramName)
      } yield callback

      jsonpCallback match {
        case some :: _ =>
          // JSONP is not JSON, but JavaScript.
          contentType = formats("js")
          // Status must always be 200 on JSONP, since it's loaded in a <script> tag.
          status = 200
          if (rosettaFlashGuard) writer.write("/**/")
          writer.write("%s(%s);".format(some, compact(render(transformResponseBody(jv)))))
        case _ =>
          contentType = formats("json")
          if (jsonVulnerabilityGuard) writer.write(VulnerabilityPrelude)
          writeJson(transformResponseBody(jv), writer)
          ()
      }
  }: RenderPipeline) orElse super.renderPipeline

このように、Scalatraではコードブロックが返す値の型に応じて色々な処理へ分岐していること、新しい処理をカスケードして追加できることが分かってもらえたでしょうか。

case classへの対応

既にコードは出てきましたが、ScalatraではAnyで受けた値を、パターンマッチにより処理を振り分けています。また、Json4sにはcase classへのマッピング機能があります。そのため、Json4sの機能を使って、Json ASTへ変換可能なcase classを受け取った場合は、自動的にJson ASTへ変換して処理したいわけです(わざわざ呼び出し側でJson ASTに変換してから呼び出すのもアレなので)。

Json4sにはCustomSerializerという機能が用意されていて、これによりターゲットとしているオブジェクトがJson ASTに変換可能であるか判定することができます。

これは他のJsonモジュールにはない、Json4s特有の機能です。この機能があるため、Anyで返したオブジェクトがJson ASTに変換可能か、判断することができるようなっています。

Scalatra in Action

Scalatra in Action

Scalatraがレスポンスを返す仕組み

Scalatra-JSONがレスポンスを返す仕組みについて書こうとしたら、その前にそもそもScalatraがレスポンスを返す仕組みを解説しないと分かりづらいな、と思ったので、まとめます。

Scalatraがレスポンスを返す仕組み

Scalatraでは、下記のようにgetpostといったメソッド名に、対応するパスとコードブロックを渡すことでWebアプリケーションとしての振る舞いを定義していきます。

package com.example.app
import org.scalatra._

class HelloWorldApp extends ScalatraFilter {
  get("/") {
    <h1>Hello, {params("name")}</h1>
  }
}

このメソッド名形式での定義は、CoreDslというtraitの中で定義されています。例えばgetメソッドは以下のように定義されていて、transformersがパス名を、Any型を返すコードブロックが実際のWebアプリケーションの挙動を定義するコードブロックを示します。

def get(transformers: RouteTransformer*)(block: => Any): Route

Scalaでは、コードブロックの最後の値が、コードブロック全体の返り値となるので、上記のHelloWorldAppの例で言えば、最後の値である<h1>Hello, {params("name")}</h1>がコードブロック全体の帰り値となります。ScalaではXMLリテラルが有るので、このコードブロックの値はXML型(scala.xml.Elem)になります(型推論される)。

メソッドの引数定義が=> Anyになっているので、getメソッドはAny型として受け取ります。

そして、最終的にScalatraBaseというtraitのrenderPipelineというメソッドで処理されます。詳細は省きますが、renderPipelineはpartial functionとして定義されていて、後から色々なパターン(case)への対応を追加できるようになっています(例えば、JSONのASTを表す型への対応はScalatraBaseには含まれていません)。

renderPipelineメソッドの一部を引用します。

protected def renderPipeline: RenderPipeline = {
    case 404 =>
      doNotFound()
    case ActionResult(status, x: Int, resultHeaders) =>
      response.status = status
      resultHeaders foreach {
        case (name, value) => response.addHeader(name, value)
      }
      response.writer.print(x.toString)
    case status: Int =>
      response.status = status
    case bytes: Array[Byte] =>
      if (contentType != null && contentType.startsWith("text")) response.setCharacterEncoding(FileCharset(bytes).name)
      response.outputStream.write(bytes)

    case x =>
      response.writer.print(x.toString)
  }

パターンマッチは上から順に適合性をチェックしていくので、コードブロックの返り値が404だったらdoNotFoundメソッドが実行され、単なるInt型であればそのままステータスコードとしてのみ使われます。特に適合するものが無ければ、最後に文字列化されてresponsewriterに渡されます。先ほどの例でいけば特にXML型特有のコードはここには無いので、case x =>のパターンにマッチして<h1>Hello, {params("name")}</h1>が文字列として返されます。

ソースコードの全体は以下のリンクから参照してみてください。

github.com

Actions - Scalatra

Surface Precision Mouseを買った

Surface Precision Mouse

Surface Precision Mouse

長年使っていたMagic Mouseが使っている最中に頻繁に接続が切れてしまうようになったので、ずっと気になっていた「Surface Precision Mouse」を購入。

Surface用なのでmacOSで使ってもフル機能が使えるわけでもないけど、大きさといい、重さといい、さすが伝統のMicrosoftマウス、過去に使ったマウスの中では最高の出来映え。デザインもシンプルで、他社の上位機種のようなゲーミング感が無くて良い。

注意事項としては、サイドのボタンが使えないのと、Magic Mouseと比べるとスクロールの方向が逆になることだけど、まぁ元々macOS用ではないので。

Windowsと共用する人にはお勧めだけど、macOSしか使わない人はちょっと価格が高めなので、微妙かも。

Effective DevOpsを読んだ

買ったはいいけど、なかなか読む時間が取れなかった「Effective DevOps」を読み始めた。

一言でまとめれば、以下のツイートで全部かな。

以下、章ごとの感想

1章〜4章

1章から4章までは歴史や概念の説明なので、少しでも最近の開発手法を知っていたり、それなりの経験がある人であれば後から読んでもいい気もするが、2.2.2だけは印象深いトピックが書かれていてぜひ開発組織に属する人であれば読んで欲しい内容(あとで5.2.1でも再度出てくるけど)。

要はミスが起きた時に、個人の責とするか、組織の問題とするか、なのだけど、それを「ヒューマンエラーはトラブルの原因だ」と考える文化と、「ヒューマンエラーをシステムのもっと深いところにある問題の兆候」と考える文化という言い方で説明するところが凄くしっくりくる。

とくに現代的な開発手法やツールの発展を考えると、20年同じ手順を秘伝のタレ的な知見に基づいて続けていく(しかもその根拠は既に失われている)という現場は少なく、個人の責を問うにはあまりに学ぶべきことは多いし、変化も激しい。また、人もどんどん入れ替わっていくし、伝統的な先輩から後輩への伝達とも限らない(ある日突然アウトソーシング先が入札でガラっと変わることだってあり得る)。

そう考えると個人を責めてもしょうがなくて、組織で考えていかないとトータルでは良くなって行かないよねって考え方は実に正しいなって。

5章 devopsに対する誤解とアンチパターン

ある程度の開発経験が有る人であれば、5章の「5.1 devopsに対するよくある誤解」と、「5.2 devopsのアンチパターン」から読み始めると良いかなって思った。特に、5.2は一番最初に読んだ方が良い章。この章に対して納得感が有るか無いかで、この本から得られるものが有るか無いかははっきりすると思う。

6章はⅡ部の導入部なので飛ばして…

7章 コラボレーション

ここは完全に組織で仕事をしていく上での普遍的な話にフォーカスしている。

  • 7.3 個人の違いと経歴、背景
  • 7.4 競争優位を得るためのチャンス
  • 7.5 メンターシップ
  • 7.6 マインドセット入門
  • 7.7 マインドセットと学習する組織
  • 7.8 フィードバックの役割
  • 7.9 評価とランキング
  • 7.10 コミュニケーションと対立の解決スタイル
  • 7.11 共感と信頼
  • 7.12 人材配置と人事管理

これ、完全に組織論、人材論になっているってことがよく分かると思う。個人的には「7.6 マインドセット入門」あたりが興味深かったけど、本当にこの章はdevopsに特化した内容ではなく、どうやってより良い組織を作っていくか?という話が続くので、ひょっとしたら人によってはガッカリしたり、意外な発見が有ったりするのでは、と思う箇所。

だからこそ冒頭のツイートに繋がるんだけど、ひょっとして開発プロセスや、ツールの使い方の本だと思ってこの本を読まないとしたら、本当にそれはもったいないって。開発チームのマネジメントを行う人、特にこれからリーダーになるような人はまず読んだ方が良い。5章と7章だけでも良いので読んで欲しい。

以降はチームでの仕事をどうやってより良くしていくか?という話や、(概念的な意味での)ツール導入、devopsをスケールさせる話とかなので、一気に読むより少しずつ自分の状況に合わせて読み進める方が良いと感じた。

おわりに

全般を通して「これがdevopsだ!」というより、一貫して「良い開発組織であり続けるためには?」という話が続く所が良い本。

ただ、上司にdevopsの導入を進言する時には使いづらいかもね:)

300ページ超は、この手の本としてはそこまで大ボリュームではないけど、それでも全部を一気に読み切ろうとすると消化不良を起こしかねないような「考えさせられる」本であることは間違いないので、あまり慌てずゆっくり(できれば周りの人とディスカッションしたりしながら)読んだ方が良さそう。

Effective DevOps ―4本柱による持続可能な組織文化の育て方

Effective DevOps ―4本柱による持続可能な組織文化の育て方

Effective DevOps: Building a Culture of Collaboration, Affinity, and Tooling at Scale

Effective DevOps: Building a Culture of Collaboration, Affinity, and Tooling at Scale

GraphQLナイトへ参加してきた

たまたま「GraphQL」について調べていたタイミングで開催が決定したのと、最近すっかりpmイベント以外に参加できていないことも有って参加してきた。

connpass.com

GraphQL自体の入門、ScalaRubyでのライブラリなどの紹介など、丁度知りたいレベル感と合っていたので、ぴったりのイベントでした。

あと、今回開催のきっかけが主催の@htomineさんが所属する会社の中でみんながGraphQLについて楽しそうに語っているんだけど、意外と外に出て発表していないからって所も良かった。背中を押す人、環境を作る人ってマジで大事ですね。

確認できた分のスライドへのリンクを張っておきます。そのうち、イベントサイトにもまとめられると思います。

speakerdeck.com

speakerdeck.com

speakerdeck.com

会場提供はfreeeさんでした。

あと、GraphQLのイベントとしてGraphQL Tokyoというミートアップが有るそうです。次回はすぐの7月4日だそうです。

www.meetup.com

GraphQL、複雑化した要求や制約の中で効率よくデータを取得する必要性から生まれたものだそうですが、こうやってベストプラクティスがライブラリや規約の形で世の中に広まっていく感じがいいですね。

イベント最高でした。@htomine++!

Learning GraphQL: Declarative Data Fetching for Modern Web Apps

Learning GraphQL: Declarative Data Fetching for Modern Web Apps

WEB+DB PRESS Vol.104

WEB+DB PRESS Vol.104

  • 作者: 末田卓巳,林田千瑛,陶山嶺,八谷賢,辰己佳祐,竹澤俊季,服部智,藤岡裕吾,牧大輔,西郡卓矢,松木雅幸,穴井宏幸,新日出海,桑原仁雄,小田知央,ひげぽん,池田拓司,はまちや2,竹原,大場光一郎,大場寧子,松館大輝,日高尚美,Vu Xuan Dung,WEB+DB PRESS編集部
  • 出版社/メーカー: 技術評論社
  • 発売日: 2018/04/24
  • メディア: 単行本
  • この商品を含むブログを見る

GraphQL API Design (API-University Series Book 5) (English Edition)

GraphQL API Design (API-University Series Book 5) (English Edition)

java.net.URLConnectionのguessContentTypeFromNameが使うMIME Type設定は実行時に変更することはできない

java.net.URLConnectionのguessContentTypeFromNameは、content.types.user.tableというシステムプロパティで定義された内容で任意のMIME typeを推測できるようになります。

公式ドキュメントにも書かれています。

URLConnection (Java Platform SE 8)

では実際にそれを確かめてみようと、テストコードの中でSystem.setProperty("content.types.user.table", "/path/to/content-types.properties")と指定してもさっぱり有効になりませんでした。

例えば、拡張子.csstext/cssと判定させるためには、以下のような設定ファイルを用意します。

text/css: \
        description=Cascading Style Sheets;\
        file_extensions=.css

以下のようなコードを用意して、設定ファイルを読み込んでみましが、mimeTypeにはnullが入りました。

System.setProperty("content.types.user.table", "/path/to/content-types.properties")
val mimeType = URLConnection.guessContentTypeFromName("test.css")

実行時ではなく、予めsbt -Dcontent.types.user.table=/path/to/content-types.propertiesというふうに実行前に設定されるようにしておくと意図した通りにmimeTypeにはtext/cssが入ります。

java.net.URLConnectionのguessContentTypeFromNameのコードを追いかける

これは何が起きているのでしょうか?

実は最初、Stack OverflowにSystem.setPropertyでセットすれば反映されると書かれていたので、それを鵜呑みにして「正しく動かない!自分の書いたコードがおかしいのか?」と思いましたが、実際にはその回答が誤りでした。

順番に、java.net.URLConnectionguessContentTypeFromNameメソッドの挙動を、openJDK10のコードを例に追いかけていきましょう。

openJDKの該当するソースコードは以下の場所に有ります。

jdk10/master: be620a591379 src/java.base/share/classes/java/net/URLConnection.java

public static String guessContentTypeFromName(String fname) {
        return getFileNameMap().getContentTypeFor(fname);
}

public static FileNameMap getFileNameMap() {
    FileNameMap map = fileNameMap;

    if (map == null) {
        fileNameMap = map = new FileNameMap() {
            private FileNameMap internalMap =
                sun.net.www.MimeTable.loadTable();

            public String getContentTypeFor(String fileName) {
                return internalMap.getContentTypeFor(fileName);
            }
        };
    }

    return map;
}

MIME Typeに関連するデータは、FileNameMapというクラスに格納されていること、値が無ければ(nullならば)されていなければsun.net.www.MimeTable.loadTableを呼び出して、初期化していることが分かります。

なお、getContentTypeForFileNameMapのメソッドで、ほぼ単純なmap構造のデータから該当するMIME typeを取得する機能を提供します。

sun.net.www.MimeTable.loadTableの中身を追いかける

では、実際にMIME typeのテーブルを保持するsun.net.www.MimeTable.loadTableの中身を追いかけてみましょう。

sun.net.www.MimeTable.loadTableは以下のファイルに収録されています。

jdk10/master: be620a591379 src/java.base/share/classes/sun/net/www/MimeTable.java

public static FileNameMap loadTable() {
    MimeTable mt = getDefaultTable();
    return (FileNameMap)mt;
}

public static MimeTable getDefaultTable() {
    return DefaultInstanceHolder.defaultInstance;
}

private static class DefaultInstanceHolder {
    static final MimeTable defaultInstance = getDefaultInstance();

    static MimeTable getDefaultInstance() {
        return java.security.AccessController.doPrivileged(
            new java.security.PrivilegedAction<MimeTable>() {
            public MimeTable run() {
                MimeTable instance = new MimeTable();
                URLConnection.setFileNameMap(instance);
                return instance;
            }
        });
    }
}

loadTableから始まり、DefaultInstanceHolderクラスにdefaultInstanceというstaticなメンバが有ることが分かります。staticなクラスのstaticなメンバなので、クラス自体がロードされた時点で、getDefaultInstanceメソッドが実行され、defaultInstanceにはMIME typeのデータがロードされている、ということなのです。

更に、この時点でなぜかURLConnection.setFileNameMapを呼び出し、ロードしたMIME Typeをセットしています。getFileNameMapメソッドの中でやっているFileNameMap mapがnullか否かを判定するロジックっていらなくない?って思いますね。

実際にロードする部分のコードをもう少し追いかけてみましょう。

MimeTable() {
    load();
}

public synchronized void load() {
    Properties entries = new Properties();
    File file = null;
    InputStream in;

    // First try to load the user-specific table, if it exists
    String userTablePath = System.getProperty("content.types.user.table");
    if (userTablePath != null && (file = new File(userTablePath)).exists()) {
        try {
            in = new FileInputStream(file);
        } catch (FileNotFoundException e) {
            System.err.println("Warning: " + file.getPath()
                                + " mime table not found.");
            return;
        }
    } else {
        in = MimeTable.class.getResourceAsStream("content-types.properties");
        if (in == null)
            throw new InternalError("default mime table not found");
    }

    try (BufferedInputStream bin = new BufferedInputStream(in)) {
        entries.load(bin);
    } catch (IOException e) {
        System.err.println("Warning: " + e.getMessage());
    }
    parse(entries);
}

確かにcontent.types.user.tableというシステムプロパティが設定されていれば、そこからパスを取得するようになっていますね。存在しなければデフォルトのcontent-types.propertiesを取得しています。

ちなみに、このcontent.types.user.tableというシステムプロパティを元に任意のMIME Typeを設定する機能について、テストコードが存在しません。確かにこれではグローバルにシステムプロパティを汚染しないとテストが書けないですね…

おわりに

以上、java.net.URLConnectionguessContentTypeFromNameにおけるcontent.types.user.tableの取扱について、実際のJavaのライブラリのコードを追いかけてみました。

依存関係(sun.net.www.MimeTable.loadTablejava.net.URLConnectionに依存している)がおかしいとか、初回利用時ではなく、クラスロード時に強制的に初期化が行われ、以降は再設定もできないとか、そもそもテストが無いとか、よく考えるとguessContentTypeFromNameというメソッド自体java.net.URLConnectionではなく、独立したMIMEに関するクラスに所属しているべきでは?と、標準のJavaのライブラリでも色々と設計が気になるところが有るんだな、というのが今回の感想でした。

java.net.URLConnection.guessContentTypeFromNameが使うデフォルトのcontent-types.propertiesがJava9から変わった

java.net.URLConnection.guessContentTypeFromNameはファイルの拡張子からMIME Typeを特定するメソッドです。このメソッドはJava8以前ではデフォルトで、$JAVA_HOME/lib/content-types.propertiesを参照していました。

しかし、Java9以降では、$JAVA_HOME/lib/content-types.propertiesは有りません。

この辺りの事情は、Java9で削除された機能に関する解説に書かれています。

Removed APIs, Features, and Options

詳細はこちら。

bugs.java.com

ちなみに本当に削除された訳では無く、jdk/src/java.base/windows/classes/sun/net/www/配下に、ソースと一緒におかれています。

jdk10/master: 4554a9ae19ef jdk/src/java.base/windows/classes/sun/net/www/content-types.properties

なお、システムプロパティのcontent.types.user.tableに設定したパスのファイルに定義を書いておくと、任意のMIME Typeを追加することができる点は変わっていません。

以上、意外なところでJava9の変更点に引っかかって、「ファイルが無い!」と1時間溶かした記録です。モジュール化、難しい。

Java 9 Modularity: Patterns and Practices for Developing Maintainable Applications

Java 9 Modularity: Patterns and Practices for Developing Maintainable Applications

WEB+DB PRESS Vol.101

WEB+DB PRESS Vol.101