目的
Spring Integrationを学ぶために、Spring Projectが提供しているサンプルを理解して、実装し直す + アレンジしてみた。本記事は、その備忘録として。なので色々間違いがあるとは思いますが、その点はご指摘いただければ幸いです。
サンプルは以下のGitに登録してあるもの。
spring-integration-sample
今回はbasicの_Enricher_について。
JMS Gatewayの続きで、JMSの_aggregator_でもやろうかなと思いましたが、JMSとかのAdapter系をやる前に基本的なコンポーネントをやっておこうと思ったところ、このサンプルプログラムは単にenricherだけでなく、普通のgatewayタグあったりと基本そうだったのでこちらを先にやることに。
ちなみにSpring Integrationのバージョンは4.3.10.RELEASEである。
概要
enricherの概念は簡単で、途中でメッセージヘッダやペイロードに対して何か情報を付け足すというもの。
サンプルアプリはペイロードに対する情報を付加しているのみであるため、せっかくなので私はヘッダに情報を付与するものも実装してみた。
アプリの流れは以下の通り。
AP起動 -> コンソールで1~3のいずれかを入力してEnterを押下(qの場合はAP停止) -> 1~4によって、それぞれ異なる情報を付与した結果をコンソールに出力する。1 ~ 4はそれぞれ以下のようになっている。
1.PayloadであるUserオブジェクトに値を入れて表示する。
2.1のPayloadがUser.usernameになったもの
3.PayloadのMapに項目を追加して表示する。
4.Haederに属性を追加して表示する。
実装
全体像
全体のフローは以下のような感じ。
- 1(2,3もServiceActivator用に定義してあるChannel名が異なるだけであとは同じ)
RequestChannel(gateway) -> findUserEnricherChannel -> ServiceActivator ->  findUserServiceChannel -> ReplyChannel(gateway) -> 標準出力(Mainクラスで値を出力)
まずMainクラスにてgatewayタグで定義してあるservice interfaceをgetBeanしておく。そして、コンソールからの入力値に応じて呼び出すメソッドを変更する。呼び出されたメソッドに対する紐づけはgatewayタグにて行っており、<int:method>タグの値で指定したchannel名に応じて<int:enricher>タグ→<int:service-activator>へと処理を委譲していく流れである。
- 4(こちらはgatewayを使わない。というかgatewayはdefaultだとpayloadをreplyしてしまうので上手く実装できず。。。)
MessageChannel ->  HeaderEnricher -> PollableChannel ->  標準出力(Mainクラスで値を出力)
HeaderEnricherの方は、gatewayタグで実装する方法がわからなかったため、Mainクラスで送信用のChannelと受信用のChannelをそれぞれgetBeanして、sendとreceiveして値を取り出す一番基本的な実装としている。
bean定義ファイル
サンプルプログラムの構成は3部構成(gateway, enricher, service-activator)になっているが、  bean定義が長いとQiita上、辛いので今回は_service-activator_はアノテーションで実装した。
1はその_service-activator_用のコンポーネントスキャン。2は_gateway_で処理をルーティングする際に各_enricher_に送信するためのチャネルのbean定義である。3は_gateway_の設定で、service-interfaceで_interface_を定義し、子要素にてメソッド毎に送るチャネルを変えていることを表す定義をしている。
- spring-integration-context.xml
    <!-- 1 -->
    <context:component-scan base-package="org.ek.sample"/>
    
    <!-- 2 -->
    <int:channel id="findUserEnricherChannel"/>
    <int:channel id="findUserByUsernameEnricherChannel"/>
    <int:channel id="findUserWithMapEnricherChannel"/>
    <int:channel id="tryHeaderEnricherChannel"/>
    <int:channel id="requestChannel"/>
    <int:channel id="replyChannel"/>
         
	<!-- 3 -->
	<int:gateway id="gateway"
		default-request-timeout="5000"
		default-reply-timeout="5000"
		default-request-channel="requestChannel"
		default-reply-channel="replyChannel"
		service-interface="org.ek.sample.service.UserService">
		<int:method name="findUser"                  request-channel="findUserEnricherChannel"/>
        <int:method name="findUserByUsername"        request-channel="findUserByUsernameEnricherChannel"/>
        <int:method name="findUserWithUsernameInMap" request-channel="findUserWithMapEnricherChannel"/>
    </int:gateway>
4では、request-channel属性のチャネルを入力とするメソッド(service-activator)に処理を委ねるという意味である。また、gatewayタグのservice-interface属性で指定したメソッドの中で、ここから委譲するメソッドは引数がorg.ek.sample.domain.Userというbeanファイルであり、特に他の属性の設定がないため_serviceActivator_はそれに合わせてメソッドを定義する必要がある。次に、子要素の<int:property>で表しているのはreturnするtypeがorg.ek.sample.domain.Userであるが、そのうちemailとpassword属性だけ_enricher_の内容を採用するということを表している。つまり、この設定において、_enricher_で他の項目をどれだけ操作してもそれはreplyされる値に反映されない。
- spring-integration-context.xml -続き
    <int:channel id="findUserServiceChannel"/>
    <int:channel id="findUserByUsernameServiceChannel"/>
    <!-- 4 -->
    <int:enricher id="findUserEnricher"
                  input-channel="findUserEnricherChannel"
                  request-channel="findUserServiceChannel">
        <int:property name="email" expression="payload.email"/>
        <int:property name="password" expression="payload.password"/>
    </int:enricher>
    <!-- 5 -->
    <int:enricher id="findUserByUsernameEnricher"
                  input-channel="findUserByUsernameEnricherChannel"
                  request-channel="findUserByUsernameServiceChannel"
                  request-payload-expression="payload.username">
        <int:property name="email" expression="payload.email"/>
        <int:property name="password" expression="payload.password"/>
    </int:enricher>
    <!-- 6 -->
    <int:enricher id="findUserWithMapEnricher"
                  input-channel="findUserWithMapEnricherChannel"
                  request-channel="findUserByUsernameServiceChannel"
                  request-payload-expression="payload.username">
        <int:property name="user" expression="payload"/>
    </int:enricher>
    
    <int:channel id="tryHeaderEnricherPollarChannel" >
    	<int:queue capacity="10"/>
    </int:channel>
    
    <!-- 7 -->
    <int:header-enricher id="tryHeaderEnricher" 
    					input-channel="tryHeaderEnricherChannel" 
    					output-channel="tryHeaderEnricherPollarChannel">    			
    					<int:header name="headerTest" value="test" />		
    					<int:header name="addedHeader" ref="stringConv" method="upperCase" />
    </int:header-enricher>
    <bean id="stringConv" class="org.ek.sample.domain.StringConverter"/>
5は4とほぼ同じだが、1つだけ異なるのは<request-payload-expression>を設定しているということ。この_enricher_が指定する_serviceActivator_のメソッドは引数がStringであるため、ここでは_payload_のusernameという項目を_serviceActivator_に引数として連携するという意味である。
また、6は5と一見一緒に思えるが、6ではservice-interfaceにて、引数が_Map_のメソッドを呼び出したときのルートであり、ここでは5と異なり、_Map_の_key_でusernameという項目がある場合、それを_serviceActivator_のメソッド(5と同じ)引数に連携するという意味である。
7は_headerEnricher_であり、単純なinput_channelから渡ってきた_MessageHeaders_に2つの属性を付与するというものであり、2個目はbeanファイルの参照(ref)を使って表現した。参照しているクラスはただただ引数を大文字にして返却するという簡単なクラスを作成した。
service-interface
これはgatewayのタグで定義したインターフェースである。
public interface UserService {
	
	User findUser(User user);
	
	User findUserByUsername(User user);
	
	Map<String, Object> findUserWithUsernameInMap(Map<String, Object> userdata);
}
serviceActivator
実際にEnricherの役割のクラスは_serviceActivator_として定義する。xmlのところで述べた通り、1つ目のメソッドは引数の型がorg.ek.sample.domain.Userであり、2つ目のメソッドは引数の型がStringとなっている。
@MessageEndpoint
@EnableIntegration
public class UserServiceEnricher {
	
	private static final Logger LOGGER = Logger.getLogger(UserServiceEnricher.class);
	
	@ServiceActivator(inputChannel="findUserServiceChannel")
	public User findUser(User user) {
		LOGGER.info(String.format("Calling method 'findUser' with parameter %s", user));
		return new User(user.getUsername() + "-test",//usernameで付け足している文字列は反映されない
				   "secret",
				   user.getUsername() + "@springintegration.org");
	}
	@ServiceActivator(inputChannel="findUserByUsernameServiceChannel")
	public User findUserByUsername(String username) {
		LOGGER.info(String.format("Calling method 'findUserByUsername' with parameter: %s", username));
		return new User(username, "secret", username + "@springintegration.org");
	}
Mainクラス
大事なところ以外は省略するが、以下の通り。
- Main
	User user = new User();
			if ("1".equals(input)) {
				final User fullUser = service.findUser(user);
				printUserInformation(fullUser);
			} else if ("2".equals(input)) {
				final User fullUser = service.findUserByUsername(user);
				printUserInformation(fullUser);
			} else if ("3".equals(input)) {
                // 3
				final Map<String, Object> userData = new HashMap<String, Object>();
				userData.put("username", "foo_map");
				userData.put("username2", "bar_map");
				final Map<String, Object> enrichedUserData = service.findUserWithUsernameInMap(userData);
				printUserFullInformation(enrichedUserData);
			} else if("4".equals(input)){
              // 4
				MessageChannel mc = context.getBean("tryHeaderEnricherChannel", MessageChannel.class);
				PollableChannel pc = context.getBean("tryHeaderEnricherPollarChannel", PollableChannel.class);
				mc.send(new GenericMessage<String>("foo.bar"));
				printHeaderInfo(pc.receive().getHeaders());				
				
			} else{			
				LOGGER.info("\n\n    Please enter '1', '2', or '3' <enter>:\n\n");
			}	
            
            
            
            // omitted
            
            
                // 5
            	private static void printUserFullInformation(Map<String, Object> userdataMap) {
		for(Map.Entry<String, Object> entry : userdataMap.entrySet()) {
			Object val = entry.getValue();
			if(val instanceof User) {
				User user = (User) entry.getValue();
				LOGGER.info(String.format("\n\n    User found - Key of Map: '%s',  Username: '%s',  Email: '%s', Password: '%s'.\n\n",
						entry.getKey(), user.getUsername(), user.getEmail(), user.getPassword()));
			}else {
				LOGGER.info(String.format("\n\n    User found - Key of Map: '%s',  Username: '%s',  Email: '%s', Password: '%s'.\n\n",
						entry.getKey(), val, null, null));
			}
		}
	}
    
    // 6
    private static void printHeaderInfo(MessageHeaders mh) {
		LOGGER.info("\n\n    " + "headerTest :" + mh.get("headerTest") + ", addedHeader :" + mh.get("addedHeader"));
	}
多少変更しているのが3の部分で、引数のMapにusernameとusername2というkey-valueをputして引数とした。また、戻り値も全て_map.entrySet()_で全て表示するようにした(5)が、以下の結果が示す通り引数に指定したmapの要素 + _enricher_で作成した1要素となるようである。なお、key名はxmlの<int:property>で定義しているuserであり、xmlではvalue属性にpayloadを指定していたため、UserオブジェクトがそのままMapのvalueとなる。
0:10:42.631 INFO  [main][org.ek.sample.Main] 
    User found - Key of Map: 'username2',  Username: 'bar_map',  Email: 'null', Password: 'null'.
00:10:42.632 INFO  [main][org.ek.sample.Main] 
    User found - Key of Map: 'user',  Username: 'foo_map',  Email: 'secret', Password: 'foo_map@springintegration.org'.
00:10:42.633 INFO  [main][org.ek.sample.Main] 
    User found - Key of Map: 'username',  Username: 'foo_map',  Email: 'null', Password: 'null'.
4は_headerEnricher_であり、お作法通りにMessageHeaderを取り出すと、xmlで指定した通りに、headerに属性が追加される。なんでheaderだけこんな実装にしているかというと、前述の通り、gatewayのデフォルトはpayloadを返すようになっているため、payload用のenricher同様にheaderの値を取得することができなかった。ただ、それだけだと使い勝手悪そうだし、gatewayタグでなんとかheaderもenricherできるように設定できるんだろうなと思ったものの、時間のあるときに調べることにする。
以下は6の表示結果である。
00:16:42.883 INFO  [main][org.ek.sample.Main] 
    headerTest :test, addedHeader :FOO.BAR
今回はadapterじゃないgatewayのタグをはじめて使ったので、なんとなくintegrationの基礎がわかってきた気がした。ところで効果的にspring integrationを学ぶためにはどういう順序でサンプルを実装すればいんだろうか...
今回実装したコードは以下。
サンプルをいじったもの