IK.AM

@making's tech note


Spring Sessionを使ったLegacy JavaアプリケーションからSpring BootへのMigration

🗃 {Programming/Java/org/springframework/boot}
🏷 Spring Boot 🏷 Spring Session 🏷 Spring Security 🏷 Java 🏷 Legacy Migration 
🗓 Updated at 2017-07-31T12:57:36Z  🗓 Created at 2017-07-31T09:19:00Z   🌎 English Page

目次

フレームワーク移行にまつわるエトセトラ

JavaフレームワークではSpring Bootが完全に抜け出した現在、過去のアプリケーションをSpring Bootに移行したいと言う話をよく聞きます。

これまでSpring Bootに限らずフレームワーク移行はよく聞いてきたのですが、大体みんな言うのは

既存の機能はそのまま、できるだけ変更せずに移行したい。

はっきり言って、これは移行のアンチパターンです。思考停止の単純移行は、移行しても不幸せが待っています。

移行の目的はなんだったのでしょうか。

  • 新しいフレームワークの新しい機能が使いたい
  • 今のフレームワークが完全に独自フレームワークになってしまったので、今後は標準的な作り方にしたい
  • 今使っているフレームワークがEnd of Lifeなので、乗せ代えたい

がよく聞く理由です。

このような思いがあるにもかかわらず、単純移行をしてしまうと、

  • 移行先フレームワークが持っている機能の劣化版の再開発、及びその機能の利用強制
  • ハックを駆使した既存機能連携
  • 無理矢理な連携に伴う(悪い)副作用

などがしばしば発生します。巷ではStruts -> Spring MVCのソースコードコンバージョンツールなどもあるようですが、機械的な変換はこのような問題を孕みます。

こうなってしまうと、

  • 新フレームワーク(Spring Boot)の本来の使い方と異なる使い方をせざるをえない
  • 新フレームワークが持つ機能が使えない
  • ハックが動かなくなる可能性があり、バージョンアップしづらい

と言う事象を招きえ、せっかく(見せかけの)フレームワーク移行をしたにも関わらず、元々の目的が全く果たされず、 移行後、即、負の遺産と化してしまう可能性があります。

移行の理想形は、本当に必要な機能のみをゼロから再実装です。過去の柵に捉われずに、SPRING INITIALIZRからSpring Bootの雛形プロジェクトを作成し、既存のソースコードはできるだけ見ずに、今のSpringスタックでSpring Bootの作法に従って作ることでメンテナンス・アップデートし続けられる状態にするのが望ましいです。

しかし、残念ながら、ほぼ毎回、

理想はわかるが、現実問題として全てを作り直す工数はない

と言われます。

危険なMigration Pattern

全て作り直す工数がない時に、取りがちなMigration Patternは、フレームワークMix Patternです。

新機能だけSpring Bootで実装するものの、既存のアプリケーションにそのまま放り込んでしまうパターンです。 コードベースは既存のまま、特定のパスのみSpring Bootで新規作成した機能に遷移させるというものです。 一見、やろうと思えばできると思いがちですが、このパターンの大きな欠点は、Spring Bootアプリの作り方が既存の構成に引きづられると言う点です。

例えば、本来は画面はThymeleafで作りたい、jarでパッケージングしたいのに、既存の構成の都合上、Spring Bootでは推奨されてないJSPで画面を作成, warでパッケージングする必要がでてきます。また、既存の認証・認可の機構に引きづられて、Spring Securityが使えない、など様々な副作用が予想されます。

非推奨な使い方は、今後のバージョンアップでなくなる可能性もありますし、二つのフレームワークを統合している箇所もバージョンアップで動作しなくなる可能性もあります。

このパターンはメンテナンスの観点でかなり危険なので、長期的に面倒をみないといけないアプリの場合は、避けるべきでしょう。

Spring Sessionを使った段階的なMigration

ここから本題なのですが、段階的なMigrationにしつつも、できるだけまっさらな状態(Greenfieldと言います)から新機能を開発したいと言う場合に利用できるかもしれないMigration Patternを紹介します。

このPatternのポイントは、新旧二つのアプリはそれぞれ独立したアプリケーションですが、Spring Sessionを使ってセッションを共有し、ログイン状態を引き継いでお互いを画面遷移できると言う点です。

2つのアプリケーションへのルーティングはReverse Proxyで行い、サブドメインまたはコンテキストパスで振り分けます。

image.png

新旧両アプリケーションでSpring Sessionが発行するSESSIONIDクッキーを共有できるようにCookieSerializerを設定します。次の例はサブドメインは無視したドメインでクッキーを共有する設定例です。

    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setDomainNamePattern("^[^.]+\\.(.+\\.[a-z]+)$");
        return serializer;
    }

アプリケーションが使用するDBの共有も避けます(セッションデータベースを除く)。新機能の方は綺麗な状態でDB設計すべきです。移行のためにGlobal Transacationは考えるべきではないので、トランザクションをまたがるような機能分割は避けたほうが良いでしょう。

Spring Boot側の認証はSpring SecurityのPre-Authentication Frameworkを利用して移行します。

旧アプリのログインユーザー情報がHttpSessionのuser属性に設定されている場合のPre-Authentication Frameworkの使い方は次のようになります。

public class LegacyPreAuthenticatedFilter extends AbstractPreAuthenticatedProcessingFilter {
    @Override
    protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
        HttpSession session = request.getSession();
        Object principal = session.getAttribute("user"); // legacy user object
        return principal;
    }

    @Override
    protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
        return "N/A";
    }
}
public class LegacyAuthenticationUserDetailsService implements AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {
    @Override
    public UserDetails loadUserDetails(PreAuthenticatedAuthenticationToken token)
            throws UsernameNotFoundException {
        Object legacyUser = token.getPrincipal();
        NewUser newUser = new NewUser();
        BeanUtils.copyProperties(legacyUser, newUser); // legacy userクラスをcompileで使わないようにReflectionでコピー
        return new NewUserDetails(newUser);
    }
}

旧アプリで生成したログインユーザーを新アプリ側のユーザーにコピーして、Spring SecurityのUserDetailsにラップします。 これで旧アプリの認証の仕組みはしばらく残しつつ、新アプリ側ではSpring Securityの一般的な使い方がある程度できます。 認証部も移行してしまえば、Pre-Authentication部分は不要です。

Greenfieldな状態から、Spring Bootらしいアプリの作り方で新機能を開発し、それに慣れたら既存機能も少しずつSpring Bootで実装し直していけるのが理想です。

Caveat

このパターンも完璧ソリューションではありません。現時点で幾つかの問題点がわかっています。

  1. セッションに乗っているオブジェクトのクラス(jar)は両アプリに含める必要がある
  2. 旧アプリ側でセッション上のオブジェクトを変更した後は明示的にsession.setAttribute("foo", foo)でセットしないといけない
  3. 動的Proxyクラスなど、旧アプリでセッションレプリケーションが想定されていないオブジェクトがセッション上に載っている場合がある

1.が厄介です。基本的には新アプリ側にも旧アプリ及び旧アプリで使用しているライブラリの一部を含めないと、セッション復元時にClassNotFoundExceptionが発生してしまいます。既存アプリもjarに分ける必要が出てくるでしょう。ただし、このjarはruntimeでのみ必要なので、Mavenなら<scope>runtime</scope>を指定しておくことで、間違って古いライブラリを使ってしまうという弊害は防げます。

旧アプリが不要なものまで大量にセッションに格納している場合は、厄介なことになるでしょう。。

2.、3.はこれまでセッションレプリケーションを想定していなければ、このパターンを試すことで初めて顕在化されるかもしれません。移行後に問題のある箇所を洗い出せるかが懸念です。

Spring SessionのJDBCバックエンドならsessionQueryを変えてWHERE句を追加することで新アプリで必要な属性だけ絞れるかもしれない。。

続く

新機能をGreenfieldなSpring Bootで作成しながら移行できるこのパターンは、致命的な問題が見つからなければおすすめできそうです。

もう少し、検証を続けて、このパターンが使えそうであればどこかの勉強会で改めて発表しようと思います。 GitHubに公開できて、SAStruts/S2Strutsあたりで作られているほどほどのサイズのアプリをお持ちの方は検証に使いたいので@makingに連絡ください。


✒️️ Edit  ⏰ History  🗑 Delete