Spring以前
RPC
業務で使うシステムはサーバー間で連携することが多い。2019年現在ではREST apiに対してjsonやprotocolbufferで呼び出す事が当たり前のように行われているが、まだjsonも発見されていない時代はもっと複雑な仕組みが取られていた1。異機種間でやりとりするためのCORBAや、機種に依存しないデータプロトコルのASN.1なども利用されていたが、仕様は複雑でそれぞれをハンドリングするライブラリは有償で売られ、ベンダーからサポートを受けながら使用するようなものだった。
RMI
Javaの世界ではJava同士でやりとりするためのRMIが定義され、比較的に楽にRPCできるようになった2。とはいえhttpでrestをコールすることに比べたらアホみたいな複雑さである。
https://docs.oracle.com/javase/jp/1.3/guide/rmi/getstart.doc.html
J2EE
そのRMIの使い方に一定のルールを設け、EJBを始め様々な指針を提示したのがJ2EEである。実装者がそれぞれに試行錯誤して苦労するような無駄は減ったが、代償として仕様はさらに複雑化した。
Spring
獲得できる機能に対して、設定や規約が悪夢のように複雑なJ2EEのアンチテーゼとして登場したのがSpringである。DIと組み合わせることでソースを綺麗に保つことができた。Clientクラスからサーバー上にあるServer#method()を呼び出すソースは以下だ。
class Client {
private Server server;
public void setServer(Server value) {
server = value;
}
public void action(){
server.method();
}
}
もちろん適切な設定ファイルが必要になる3。が、開発時はローカル(というか同一JVM内)にあるserver.method()を直接呼び出して検証し、設定ファイルを差し替えるだけででserverの中身がテスト用のモックになったり本番用のリモート呼び出し版になったりするのは便利だった。素のRMIなら開発時もローカルにRMIサーバーを上げてrmi://localhost
で接続し、テスト環境では設定ファイルでrmi://serverhost
に変更するだけとか、RMIのオーバーヘッドが嫌ならデバッグフラグを立てておき呼び出し時にif文でローカル実行かRPCかを呼び分けるようにする程度だったから、結構な進化である。RMIのオーバーヘッドはhttpに較べればかなり小さいが、開発時にもいちいち上げなければいけないのは面倒だ。メインサービスのhttpは忘れないが、別途rmiserverだけ起動、とかいう手順が必要だと忘れがちになったり、逆に常に同時に立ち上げるスクリプトを使っていると、httpが生きているのにrmiにつながらない時に開発メンバーが「どうすればいいかわからない」状態になることがある。
SpringはDIコンテナではあるが、そのDIでリモート呼出を簡単にしてくれた事に大きな功績があった。このように実装を切り分ける方法にはDIの前にServiceLocatorがあった。先程のサンプルを書き換えるとこうなる。
class Client {
private Server server = ServiceLocator.find(Service.class);
public void action(){
server.method();
}
}
設定ファイル、または初期化時にServiceLocatorにService.class
に対する実クラスを登録しておく。デフォルトでは指定されたクラスと同じ名前のServiceクラスがあがり、差し替えたいときだけ設定する、というルールでやればいちいち全てのServiceにインターフェースを切ったりする必要もない。デメリットはServiceLocatorに対する依存が発生することだ。細かいことは割愛するがマーチン・ファウラーのブログの日本語訳のリンクを貼っておくので読んでほしい4。
Inversion of Control コンテナと Dependency Injection パターン
ソフトウェア技術の背景
歴史は繰り返す
メインフレームにダム端末を繋いでいた時代から、コンピュータの性能が向上したことでダウンサイジングがブームとなりUNIXワークステーションやPCで処理を行う流れとなり、管理の煩雑さやセキュリティの問題から巨大なサーバーにシンクライアントや仮想デスクトップで接続するという逆行が起きている。データ通信の世界ではシリアル通信から、同時接続で速度を向上させるためパラレルの通信技術が発展した。しかし技術革新でかなり高い周波数で通信できるようになると、今度は複数ある通信経路の同期をとるのが大変になりシリアル通信のSATAに揺り戻しが起きている。
プログラミング言語の世界も同じだ。C言語が世界を席巻していた頃、可読性の低いソースが氾濫していた。インターネットはまだ普及していないし、接続しても毒にも薬にもならない企業ページか、カウンターcgiの設置された個人ページばかり、最新の情報と言えば雑誌だ5。オンラインのコミュティがないのだからオフラインミーティングもない(Niftyなどのパソコン通信ではオフ会もあった、というかオフ会の言葉の発祥がそのへんだと思う)。よく練られたベストプラクティスが無い中、平凡なプログラマが書くコードというのはそれはもう十人十色で、変数名が意味不明に略されていたり、一つのものに対してもいろんな名前がついていたり、読み解くのに一苦労だ。
cからc++へ
ここにC++によるオブジェクト指向の波がやって来る。Cでしか書いたことない人達は「コメントに"//"が使えるC言語」6とか、「困ったらextern C
で書けばいいから」とか言っちゃうし、継承をよくわかってないまま使って単に可読性を悪化させただけだったりした。boostはまだ来ない。オペレーターオーバーロードで演算子の持つ意味が特定のクラスでは異なっていたり、ダイアモンド継承による問題もあった。技術者レベルはCの時代に比べミジンコほどしか上がってないのに、言語の機能と複雑度は爆発的に増えた。
省略名で苦労するなら長い名前でいいじゃん
c/c++で蔓延していたユニークな変数名・略名からの揺り戻しとして「変数名を省略するなブーム」が起きる。過去はみんな640x480のCRT(またはそれ以下の解像度のモノクロ液晶)で仕事をしていたので、変数名が長いとソースの右端が途切れて読みにくかったのである。マウスもなかったし。
Javaが出た1995年ころは、Windows3.1->Windows95,MacはSystem7-漢字Talk7.5くらいで、800x600や1024x7686でマルチウィンドウで任意のフォントサイズで表示ができるようになったこともあり、「変数名を省略しないブーム」が訪れた。これは最近の関数型言語の隆盛でまた揺り返しがきている。日本語->英語名での間違いとかもあり、大事なのはプロジェクトで用語辞書を定義して一貫性をもたせることであって名前を省略しなければいいというものでもない。google spreadsheetのようにフリーでwikiよりも気軽に編集できる辞書スプレッドシートが使えるようになった時代背景も大きい。
インターフェース、セッター、ゲッター...「すいませんこれ手で書くの?」
そして、オブジェクトの状態を隠蔽し振る舞いだけ公開しろということでprivateメンバを作成し、publicなgetter/setterを作成するのである。2019年にこれを手打ちしている猿はいないと思うが、Eclipseは随分前から自動生成をサポートし、JavaではLombokによりアノテーションだけで任意のアクセッサをプリコンパイルしてくれ、モダンな言語はproperty機能を備え(Delphiからあったし、なんならC#はDelphiだが)、Scalaではそもそもメンバ変数もpublicにすることが推奨され出した。これは「変数にしとこうと思ったけど、やっぱ関数にする」みたいな時にインターフェースを変えないまま差し込めるからだ。そもそも振る舞いだけ公開するならアクセッサを用意しては駄目で「Tell, Don't Ask」の原則にしたがって相手のオブジェクトに対してやって欲しい命令をだす方が望ましいのである。
アプリケーションの挙動をどこに明示するか
XML地獄
さて話をSpringに戻そう。ServiceLocatorでは利用側のプログラムが任意のタイミングで取り出すので、実装を差し替えたいクラスだけ設定しておけばよかった。しかしDIコンテナではDIコンテナがインスタンスを生成し、その生成時にインスタンスは別のDIで差し替え可能なインスタンスを持つため、DIで差し替え可能なメンバーを持つクラスすべてをDIコンテナに登録しなくてはならなかった。そのため登録する項目数はかなり多く、spring起動時に依存関係を解決するため起動は遅かった。でもJ2EEのクソ設計に比べれば随分スマートだったので広く受け入れられた。当時は設定ファイルと言えばxmlで7、Springを始めとしたJava界隈はXML地獄の様相を呈していく。
設定より規約
ここは本題と外れるので余談となるが、このような状況で登場したRuby On Rails(以下RoR)は、設定を多数管理するよりも「規約をつくってそれにのっとっていればうまく動くよ」というスタンスで人口に膾炙し、後にRoRのフォロワーを多数輩出した。XML地獄から脱出する大きなムーブメントとなった功績は大きい。ただ「設定より規約」では規約をすべて頭に入れておかないと「これ、どこにも定義されてないんだけどどうやって動いてるの???」となり保守性が悪くなる。少ない規約で運用できるなら非常に有用だと思う。Spring DIに影響されRoRを後追いした日本ローカルなSeasar2では、どちらも劣化コピーだった上にアレがアレだったんで消えた。
アノテーションの是非
SpringはXMLからの脱却に、アノテーションを選択した。確かに設定ファイルは減った。意味不明な設定ファイルの項目があったとき、XMLなら項目名でgrepをかけてどういう処理をしているのか探し出すことができた。アノテーションの場合IDEからコントロールクリックで飛べるが、そこにはアノテーションとしての定義しかなく、実際はDIコンテナがインスタンスを生成する時に、対象となるクラスについているアノテーションを見て挙動を変えるわけで、ベテランじゃないと処理が追えなくなった。
そしてServiceLocatorの事を思い出してほしい。あれにデメリットがあるとされたのは、Locatorに対する依存があるからだ。設定ファイルからアノテーションに移したということは、アノテーションをimportしアノテーションに依存する。ServiceLocatorに対する最大の優位点まで捨ててしまった。後にCDIとしてDI系のアノテーションが標準化されるが、springがやり始めた時は完全にspring依存で替えの効かないシステムになったのである。
アノテーションの例としてこれを以下を見て欲しい。
GET http://host/calc/add?left=1&right=2
に対するサーバーサイドのプログラムである。
きしだのはてなより引用: https://nowokay.hatenablog.com/entry/20131108/1383882109
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.inject.Named;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
@Named
@ApplicationScoped
@Path("/calc")
public class CalcService {
@Inject
CalcLogic logic;
@Path("add")
@GET
public Result add(
@QueryParam("left") int p1,
@QueryParam("right") int p2)
{
int ans = logic.add(p1, p2);
return new Result(p1, p2, ans);
}
}
(筆者による改行変更あり)
ブログオーナーのきしださんは「便利」とおっしゃられているが、これはJavaにおけるWeb開発、DIの知識があるから「すげー少ない労力でかける」のであって、初見の人からしたら「このアノテーションは一体全部でいくつあって、そのうちどれとどれをマスターしておく必要があるのか、@GET
に対しては@POST
や@PUT
があるだろうというのは想像できるが、じゃあ添付ファイルはどうやって取得するの?@Path
はアプリ起動時に自動的にこのクラスが読み込まれてservlet containerのルーティングに登録されるの?とか不安にならないだろうか。これが思い通りに動かない時に、どうやってデバッグするか想像できるだろうか。javax.ws.rs.Path
はJava EEに含まれているアノテーションであるが、実装になにが使われるかはプロジェクトによって異なる。Path登録時にログは表示されるのか、LogLevelはdebugかinfoか、今有効なPath一覧はどこで確認できるか、などの情報を使用している実装に合わせたドキュメントからすぐ探し出すことができるだろうか。
挙動がよくわからなくって、仕様について聞くと事あるごとに「ドキュメントに書いてあるんで」とかいう輩がいるが、「じゃあお前Java EEの仕様書全部覚えててそこに書いてあることは『書いてある』だけで済ませりゃええんか、そんな無駄な事に頭使ってるからコピペのクソコードしか書けずまともな設計できないんちゃうんか」と言いたい。声を大にして言いたい。公式マニュアルというのは機能すべてを網羅して解説する必要があり、リファレンス的に必要な時に参照すれば良くてプロジェクト参画前にすべて目を通して暗記するようなものではない。Getting StartedやFAQが程よく充実していれば幸運だが、重要でない項目までだらだら書いてあると欲しい情報が見つからなくてイライラする(そう、まるでこの記事のように!)。
さて、ここで同じことをするsparkframework8+ServiceLocatorのサンプルを見てほしい。ServiceLocatorはサンプルとしてimport,他に依存するのはSparkframeworkだけである。
import static spark.Spark.*;
import com.example.ServiceLocator;
public class HelloWorld {
public static void main(String[] args) {
get("/calc/add", (req, res) -> {
CalcLogic logic = ServiceLocator.get(CalcLogic.class);
int p1 = intParam(req.params(), "left");
int p2 = intParam(req.params(), "right");
int ans = logic.add(p1, p2);
return new Result(p1, p2, ans).toJson();
}
}
private static int intParam(Map params, String key){
return Integer.valueOf(params.get(key));
}
}
アノテーションはひとつもなく同じことをしている。static import Spark.*
してgetメソッドを使っているあたりは依存関係が明確でないが、IDEでコントロールクリックすれば飛んで処理が追える。膨大なJ2EEやSpringのドキュメントにあらかじめ目を通しておかなくても、実際に書いてあるシンプルなコードから追えるのである。ちなみに上のソースは1行メソッドのためにわざわざintParam()
を定義しているので、直に書けば3行短くなる。それより引数でもらってきたp1とp2を返す意味がわからん(だって呼び出し側が渡してきたパラメータなんだから呼び出し側は持ってるでしょ)ので、intParamは残しつつ無駄を省くとこうなる
import static spark.Spark.*;
import com.example.ServiceLocator;
public class HelloWorld {
public static void main(String[] args) {
get("/calc/add", (req, res) -> {
CalcLogic logic = ServiceLocator.get(CalcLogic.class);
return new Result(logic.add(intParam(req.params(), "left"), intParam(req.params, "right")).toJson());
}
}
private static int intParam(Map params, String key){
return Integer.valueOf(params.get(key));
}
}
おそらくSparkFrameworkの代わりにNinjaFrameworkを使っても似たようなものだろう。学習コストが違いすぎる。大艦巨砲主義の時代は終わった。戦艦大和は沈んだのだ。
Springの終焉
創設者の離脱
いつ終わっていたのか?Springの作者はRederick "Rod" Johnsonであるが、彼がScala言語を開発しているtypesafe(現在はlightbendに社名変更)にジョインした2012年、既に作者から見切りをつけられていたのではないだろうか。おそらく創始者としての責任感は感じていただろうから、実際にジョインした2012よりも前の段階から、すでにSpringは最先端の技術ではなく、より良い方法を追求しようとしていたのだと想像する。まあここはあくまで個人の想像だし、本人はまかりまちがっても「いやあ実はSpringなんてとっくに見切っててさあ」なんて思ってても公言できる立場ではないので、確認する術はない。
SpringBoot
筆者が最後にSpringに触ったのは2016年のSpringBoot+Hibernate+gradleの構成だ。それまで2年ほどscalaをやっていたのでつらいったらなかった。Spring Data JPAというやつでinterfaceにfindByAgeGreaterThanEqual
切ったら実装は自動でやってくれるというやつ、完全に静的型付け言語のメリットをかなぐり捨てにいってて、設計したやつは正気の沙汰とは思えない。唯一gradleだけはsbtよりマシだと思った。
Micronauts
お、マイクロサービス特化のあたらしいフレームワーク?いい線いくのかと思ったけど思想が完全に「Springつらいから軽量Springを作る」になっていてやばい。過去にLinusは「Subversionプロジェクトは無意味、CVSからほとんど進化してないのに多大なリソースをつぎ込んでいる」と批判してgitのベースを作ったが、MicronautsはちょうどSubversionにあたる無意味さに見える。Springのつらさをちょっと軽減してくれるだけで、根本的な問題がなにも解決していないように感じているのは筆者だけだろうか。
Scalaの人はSpringなんていう腐ったフレームワークは使わずにPlayFrameworkを使っているだろうけど9、Kotlinというせっかく「Javaより大分モダンな言語」を使ってるのにSpringやMicronauts使っている話を聞くとつらみが、人生はつらい。
今後
当然「じゃあ何を使うのか」という話になるわけだが、筆者は今の所Kotlin+Javalin(WebF/W※後述)+Exposed(ORM)を使っているものの、ここがゴールだという気は全然しない。まずKotlinってJavaの知識が必要だし、gradleってmavenの知識が必要だし、すごく過渡期の中途半端なプロダクトという感じ。ExposedはマクロのないKotlinではボイラープレートが多すぎてまだまだつらい。kaptでどうにかなるのか?AndroidのおかげでKotlinは今後もシェアを伸ばすだろうけど、じゃあ全部Kotlinでいいかって言われるとうーんどうかな、と思ってしまう。パターンマッチがないのも辛い、kotlin2.0で入って欲しい。あとJVMがでかくて、せっかくalpineでdocker imageつくってもJVMいれた時点でお腹痛くなってくる。busyboxの意味とは。
Javalinについて
sparkjava&spark-kotlin の更新が完全に止まってるので乗り換えた。sparkjavaからforkしていて、sparkjavaよりも依存性が明確である。例えば
import spark.Spark.get
fun main() {
get("/") { "Hello" }
}
の代わりに
import io.javalin.Javalin
fun main(args: Array<String>) {
val app = Javalin.create().start(7000)
app.get("/") { ctx -> ctx.result("Hello World") }
}
このようになり「get()
はどこを呼んでるの?」「誰がいつJettyを起動しているの?」などと悩まなくて良い。この辺JetBrains謹製のKtorは最悪で
install(ContentNegotiation) {
gson {
// Configure Gson here
}
}
routing {
get("/") {
call.respond(MyData("Hello, World!"))
}
}
こんな感じで「え、installってどこから来たの?ContentNegotion以外にどんな選択肢があるの?」っていう感じだし、「gsonとjackson好きな方使ってね」ってなっているのはいいんだけど特にこだわりがない場合どっちを選んで置くのが無難なのかもよくわからないし、そもそもサーバーを作っているのにContentNegotiationでjsonの設定をしないと行けない理由もよくわからない。学習コストが高く機能も膨大でドキュメント漁るのも大変。拡張メソッドを使いまくってるせいでソース追うのも大変。Javaエコシステムから変な影響受けちゃったのかな。
あまりに酷いのでExposedもなんか信用できなくなってきてもっといいORMを探してるんだけど、そもそも勉強する時間が取れるならRustやりたいというのもあり難しい。むーん。
今はGoを試そうとしている。今度ジェネリクスも入るらしいからそれを待ちたい。まともなMaybe/Eitherが使えるようになったら良いかもしれないけど、Goの型システムでいけるのだろうか?無理そうな気がする。
言語機能的にはRustくらい欲しいが、Rustが10年後どれくらい使われているかを考えると、業種にもよるが業務で使うのは勇気がいる。個別に入れ替え可能なようにmicroservicesにしていても、稼働中のシステムで使われている言語の種類は無駄に多くならない方が良いし10、技術者のレベルもバラバラだ。次にアーキ担当になる人が「わしらPHPer11じゃー、人材確保のためPHP12に統一じゃー」とか言い出すと、言語変更に即応できるレベルの人は「やってられるかこんなクソ現場」って砂をかけながら逃げ出し、「PHPしかできません」と言って雇用されて来た人たちがRustのソースみて「ううううううう」となり誰も幸せにならない。
早くAIが人間の仕事を奪って人間は遊んで暮らせるようになり、みんなで幸せに暮らせますように。
-
jsonは2001年頃Douglas Crockford氏によって"発見"された。 https://www.publickey1.jp/blog/17/jsonrfc_8259ecma-404_2nd_editonutf-8.html ↩
-
RMIは当初CORBAに対応しておらずJava同士専用だった。 ↩
-
みんなが大嫌いなxml地獄の事。 ↩
-
本当はServiceLocatorの方が良いと思ってるんだけど言葉を選んでDIの方が良いケースもあるよね、と一応言っておくか的な心情がアリアリとみえる。 ↩
-
イベントやリリースの情報ですら1-2ヶ月遅れで、まとまった有用な情報が本になるのは半年一年遅れ、ネットがないので口コミの伝播速度も遅く、通ってる本屋で偶然出会わなければその情報にアクセスすることもない。 ↩
-
今でこそレガシー代表の冗長かつ無駄の多いフォーマットという扱いだが、前述のASN.1などに比べれば簡単だし、DTDを書くことでルールを厳密に定義し記述時に静的解析してエラーチェックができ、XSLTなどと合わせて「XMLを中心にした技術の波が来るぞ」という空気があった、まあ空気なんで存在感はないんだが。XML-RPCやSOAPなんかはその延長線上じゃないかな?盛大に失敗したSilverlightなんかもその派生である(2010年以後もsilverlightが来ると思ってた人はセンスなさすぎなのでそろそろ引退して欲しい)。jsonみたいに動的に任意で書けるものとは基本的なスタンスが違うのである。え?じゃあ今も使いたいかって?えーーーっっと、jsonバンザーイ!! ↩
-
ポストHadoopとして有名なApache Sparkではなくて軽量web frameworkの方。名前はこっちの方が先に使ってたのだが知名度では月とスッポンになってしまった。もちろんこっちがスッポン。Apacheも名前決める時にJVM界隈で被らないように気を使ってくれたっていいのに ↩
-
それはそれでまた別の苦労があると思うが本記事では触れない ↩
-
言語増えるとノウハウが分散するし人材の確保も大変になる ↩
-
ぺちぱー ↩
-
PHPerが「ぺちぱー」なら、PHPは「ぺちぷ」? ↩