ブログ BLOG

ServiceWrapper vs ModelListenerどっちを使うべき?

2025.12.03

ブログ

はじめに

本記事はLiferayへのサーバーサイドのカスタマイズを含みますのでLiferay PaaSおよびLiferay Self-Hosted利用者向けの記事になります。

Liferayではカスタマイズ手法として、サービスのOverrideにServiceWrapperをエンティティ操作のOverrideにはModelListenerとそれぞれカスタマイズ用の拡張ポイントが設けられています。

ただし、ユースケースによってはどちらでも同じ目的を実装できる場合があります。
とはいえ、どちらを使うべきか。でいうと正解は存在し、それを無視することで拡張性が損なわれる事態を招く恐れもあります。

このブログではそれぞれの特徴を説明し、適した利用に繫がれば幸いです。

ServiceWrapperとModelListenerの違い

まず、両者の違いを説明する前にLiferayのアーキテクチャを整理するところから始めます。
Liferayをレイヤードアーキテクチャで整理するとこんな感じです。

ServiceWrapper拡張はこの中のDomain層で拡張するカスタマイズ方法です。
具体的にはServiceやLocalServiceに対応するWrapperのJavaクラスを継承して実装されます。
ユーザーであればUserServiceWrapperやUserLocalServiceWrapperになります。
具体的には以下のようにServiceBuilderで自動生成される既存のServiceWrapperクラスを継承して拡張したいメソッドを実装します。

@Component(service = ServiceWrapper.class)
public class AssetEntryAssetCategoryRelAssetCategoryLocalServiceWrapper
  extends AssetCategoryLocalServiceWrapper {

それに対し、ModelListener拡張はInfraStructure層で拡張するカスタマイズ方法です。
LiferayはMDA(Model Driven Architecture)の考えに基づいて設計されています。
なので、Domain層でもModel(=Entity)用のビジネスロジックとしてServiceおよびLocalServiceが定義され、InfraStructure層ではDBアクセス用にPersistenceが定義されています。
ModelListenr拡張はこのPersistenceでの処理時にフックするものになります。
具体的には以下がModelListenerが実行されるコードの抜粋です。
OSGi経由で取得した対象Modelに対応したModelListenerの配列から得られる各ModelListenerの処理をModelの更新処理の前後に差し込んでいます。

for (ModelListener<T> modelListener : modelListeners) {
	if (isNew) {
		modelListener.onBeforeCreate(model);
	}
	else {
		modelListener.onBeforeUpdate(oldModel, model);
	}
}

model = updateImpl(model);

for (ModelListener<T> modelListener : modelListeners) {
	if (isNew) {
		modelListener.onAfterCreate(model);
	}
	else {
		modelListener.onAfterUpdate(oldModel, model);
	}
}

つまり、ServiceWrapperはビジネスロジックの粒度で拡張でき、Javaの世界で継承(Extends)しているので既存の処理の書き換えも可能ですが、1つのサービスに複数のServiceWrapper拡張を適用できません。
例えば、AssetCategoryLocalServiceWrapperはLiferayの製品内でAssetEntryAssetCategoryRelAssetCategoryLocalServiceWrapperというServiceWrapper拡張を行っているので、独自に拡張できない(訳ではないけどめんどくさい)様になっています。
(つーか、製品内でカスタマイズ用の拡張ポイント塞ぐなよ。せめてinternalパッケージやめれ)

対してModelListenr拡張は同一Modelに対して複数のModelListener拡張が可能ですが、既存の処理を変更することはできず、処理を追加する事しかできません。

上記を整理するとこんな感じです。

拡張レイヤー拡張可能数処理の上書き拡張ポイント
ServiceWrapper拡張DOMAIN層1つのみ可能・Serviceのpublicメソッド
・LocalServiceのpublicメソッド
ModelListener
拡張
INFRASTRUCTURE層無制限不可・追加前 or 後
・更新前 or 後
・削除前 or 後
・所属前 or 後
・脱退前 or 後

ServiceWrapperが向いているユースケース

では、ServiceWrapperでの拡張がどういうユースケースの時に向いているのかを考えていきましょう。

Modelの更新が起こらない場合

これはそもそもModelListenerでは実現できないケースです。

ModelListenerの拡張ポイントは上述の通り、Modelに対して何らかの更新が起こった際です。
なので、Modelに対して更新が起こらないとModelListenerがキックされないので実現できません。

ServiceWrapperでは更新処理以外でもDomain層の処理を行うServiceおよびLocalServiceのpublicメソッドであればget〇〇やsearch〇〇でも拡張可能なので実現可能です。

既存の処理を変更したい場合

ModelListenerでは上述の拡張ポイントにある通り、既存の何らかの更新処理の前後に処理を差し込む事しかできません。

ですので、既存の処理を書き換えたい場合にはModelListenerでは対応できずServiceWrapperで拡張を行うのが良いでしょう。

特定の経路(メソッド)の場合にのみ拡張したい場合

一言で追加・更新・削除の処理といってもLiferayの内部ではいくつかのメソッドが定義されている場合があります。
これらのメソッド毎に呼び出される経路が異なる場合があります。
そうした特定の経路の場合のみ処理を拡張したい場合は、ServiceWrapperで拡張を行うのがよいです。

特定の条件の場合にのみ拡張したい場合

ModelListenerではModelの更新前後のデータでハンドリングは可能ですが、それ以外の情報が渡されないので条件分岐のハンドリングはあまり得意ではありません。

逆にServiceWrapperでは殆どのメソッドでServiceContextというビジネスロジック用の情報が詰め込まれたオブジェクトが引数として渡されます。
ServiceContextの中にはHTTPリクエストも含まれていたりするので、柔軟なハンドリングが可能です。

1回の処理内に対象Modelに複数回の更新が含まれる場合

例えば、ユーザーやWebコンテンツ、ドキュメントなどのワークフロー対応のModelでは更新系操作の中でPersistenceでのDB更新が複数回行われます。

追加時だとDBに対するInsert処理の際はワークフロー用ステータスは未承認の状態です。
この後ワークフローで承認されると(デフォルトのワークフローなしだと即座に)このステータスを書き換えるUpdate処理が起こります。

このUpdateがステータスの更新のUpdateの場合なのか、Modelを更新しようとしているUpdateなのかを判別するのは難しいですし、ModelListenerでupdate時に拡張したい場合はワークフローのステータス変更時にも動作することを覚えておきましょう。

ModelListenerが向いているケース

じゃあ、ModelListenerはどういった場合に向いているんでしょうか。

同一Modelに対して複数のユースケースの拡張が見込まれる場合

上述の通り、ServiceWrapperではビジネスロジックの粒度で拡張できるもののJavaの継承を用いた拡張である以上基本的に1回しか拡張できません。
(多段継承で複数実装は可能ですが、OSGi経由で採用されるのは一つ)
しかし、SRP(Single Responsibility Principle:単一責任の原則)から複数のユースケースに対応する実装を1ServiceWrapperに集約するべきではありません。
その観点に立つとModelListenerの拡張では複数のModelListenerを実装できます。
ですので、こういうシーンではModelListenerを使うべきです。

実際にLiferayのソースコードを見てみるとユーザーModelのModelListenerを拡張したクラスは20個以上存在します。
LDAP連携やAC(Analytics Cloud)連携や監査ログ用など各目的毎の実装がされています。
このように1クラスに複数のユースケースの実装を混合させずに

1Model(Entity)の操作をトリガーとして処理をフックしたい場合

ServiceWrapperでは既存のService/LocalServiceのメソッドをOverrideします。
同一Modelの更新を複数メソッドから行う際に、すべての経路に関わらずに処理を追加したい場合はModelListenerの拡張がよいでしょう。

ServiceWrapperの拡張だと全メソッドに対して拡張が必要になり、ソースコードが冗長になりますが、ModelListenerであれば経路を問わずに対象のModelが更新された時に動作するので実装がシンプルになり、ソースコードからも目的が明確になります。

他システムとのModelレベルのデータ連携

例えばLiferayの標準機能のLDAP連携もそうですが、データ連携は対応するModelのModelListenerを拡張するのが一般的です。
こうする事で連携するデータの粒度に合わせた実装となりますので、コードの影響範囲なども明確になります。

ServiceWrapperでも同様の実装は可能ですが、例えばUserの場合だとLocalServiceではUserだけではなく、マイサイトを表現するためのGroupやContactなどの複数のModelを1メソッド内で更新するので、データ連携の粒度と噛み合わない場合があります。

まとめ

いかがでしたでしょうか。

このようにLiferayのServiceWrapperやModelListenerの特徴を理解すれば、様々なユースケースでどういうカスタマイズを行うべきかが見えてくるかと思います。

Oxygen Designでは今回のServiceWrapperやModelListenerをはじめ、様々なカスタマイズ方法からお客様の要件に合わせてバージョンアップ時の保守性を見据えた適切なご提案と設計、開発をご支援しております。

お困りでしたらぜひお問い合わせください!

まつもとでした!