Java
spring-boot
spring-webflow

時代遅れかもしれないSpring Web Flow入門 3

More than 3 years have passed since last update.

乗りかかった船なので。。。


検索フローの実装


フロー定義

まずは単純な画面遷移のみを行うフローを定義します。

flow-search.xmlは「/src/main/resources/templates/search」直下に配置します。


flow-search.xml

<?xml version="1.0" encoding="UTF-8"?>

<flow
xmlns="http://www.springframework.org/schema/webflow"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/webflow
http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd"
>
<!-- 検索トップ画面 -->
<view-state id="top" view="/search/top">
<transition on="search" to="result"/>
</view-state>

<!-- 検索トップ画面(検索結果表示状態) テンプレートは同じ-->
<view-state id="result" view="/search/top">
<transition on="search" to="result"/>
<transition on="detail" to="detail"/>
</view-state>

<!-- 詳細画面 -->
<view-state id="detail" view="/search/detail">
<transition on="back" to="top"/>
</view-state>
</flow>



設定


WebConfig.java

package flowapp;

@Configuration
public class WebFlowConfig extends AbstractFlowConfiguration {

@Autowired
SpringTemplateEngine templateEngine;

/**
* WebFlow を利用している場合、enfoce=trueだ文字化けする。
* @return
*/

@Bean
public CharacterEncodingFilter characterEncodingFilter() {

CharacterEncodingFilter filter = new CharacterEncodingFilter();
filter.setEncoding("UTF-8");
filter.setForceEncoding(false);
return filter;
}

/**
* フロー定義を配置するルートフォルダを定義。
* テンプレートと同じ階層にするため、Spring bootデフォルトの「templates」フォルダを指定
* @return
*/

@Bean
public FlowDefinitionRegistry flowRegistry() {
return getFlowDefinitionRegistryBuilder()
.setBasePath("classpath:templates")
.addFlowLocationPattern("/**/flow-*.xml")
.setFlowBuilderServices(flowBuilderServices()).build();
}

@Bean
public FlowBuilderServices flowBuilderServices() {
return getFlowBuilderServicesBuilder()
.setViewFactoryCreator(viewFactoryCreator())
.setValidator(validator())
.setDevelopmentMode(true)
.build();
}

@Bean
public Validator validator(){
return new LocalValidatorFactoryBean();
}

@Bean
public ViewFactoryCreator viewFactoryCreator() {
MvcViewFactoryCreator bean = new MvcViewFactoryCreator();
bean.setViewResolvers(Arrays.asList(viewResolver()));
bean.setUseSpringBeanBinding(true);
return bean;
}

@Bean
public FlowExecutor flowExecutor() {
return getFlowExecutorBuilder(flowRegistry())
.build();
}

@Bean
public FlowHandlerAdapter flowHandler() {
FlowHandlerAdapter bean = new FlowHandlerAdapter();
bean.setFlowExecutor(flowExecutor());
bean.setSaveOutputToFlashScopeOnRedirect(true);
return bean;
}

@Bean
public FlowHandlerMapping flowHandlerMapping() {
FlowHandlerMapping bean = new FlowHandlerMapping();
bean.setFlowRegistry(flowRegistry());
bean.setOrder(0);
return bean;
}

@Bean
public AjaxThymeleafViewResolver viewResolver() {
AjaxThymeleafViewResolver viewResolver = new AjaxThymeleafViewResolver();
viewResolver.setViewClass(FlowAjaxThymeleafView.class);
viewResolver.setTemplateEngine(templateEngine);
viewResolver.setCharacterEncoding("UTF-8");
return viewResolver;
}
}



フロー定義のテスト

AbstractXmlFlowExecutionTestsはスーパークラスとなるため、Spockで使うためにTestRuleクラスにしてみました。(多分、何かバグはある。)


FlowExecutionTestsRule.java

package flowapp.test;

public class FlowExecutionTestsRule extends AbstractXmlFlowExecutionTests
implements TestRule {

private String flowDefPath;
private Map<String, Object> registoredBeans = new HashMap<>();

public FlowExecutionTestsRule(String flowDefPath) {
super();
this.flowDefPath = flowDefPath;
}

public void registorBean(String beanName, Object bean) {
registoredBeans.put(beanName, bean);
}

@Override
public Statement apply(Statement base, Description description) {
return new Statement() {

@Override
public void evaluate() throws Throwable {
base.evaluate();
}
};
}

@Override
protected FlowDefinitionResource getResource(
FlowDefinitionResourceFactory resourceFactory) {
return resourceFactory.createFileResource(flowDefPath);
}

@Override
protected void configureFlowBuilderContext(
MockFlowBuilderContext builderContext) {

registoredBeans.entrySet().forEach(e ->
builderContext.registerBean(e.getKey(), e.getValue())
);
}

}


テストクラス。


SearchFlowSpec

package flowapp;

class SearchFlowSpec extends Specification{

@Rule
public FlowExecutionTestsRule flowExecutionTestsRule = new FlowExecutionTestsRule("src/main/resources/templates/search/flow-search.xml");

def "フローの初期化"() {
when:
MockExternalContext context = new MockExternalContext();
flowExecutionTestsRule.startFlow(context);

then:
flowExecutionTestsRule.assertCurrentStateEquals("top");
}

def "画面遷移のテスト"(){
setup:
MockExternalContext context = new MockExternalContext();

when:
flowExecutionTestsRule.setCurrentState(currentState)
context.setEventId(eventId)
flowExecutionTestsRule.resumeFlow(context)

then:
flowExecutionTestsRule.assertCurrentStateEquals(resultState);

where:
currentState|eventId |resultState
"top" |"search"|"result"
"result" |"search"|"result"
"result" |"detail"|"detail"
"detail" |"back" |"top"
}

}


この段階では、まだ、画面との結合はできていません。


画面の作成

モック画面を作成して、遷移の動作確認をします。

src/main/resources/search直下にファイルを作成します。


top.html

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:tiles="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org" lang="jp">
<head>
<meta charset="UTF-8" />
<title>検索画面</title>
</head>
<body>
<!-- ステートIDを表示 -->
<span th:text="${flowRequestContext.currentState.id}"></span>
<form action="#" th:action="${flowExecutionUrl}" method="POST">
<table>
<tr>
<td>ISBN</td>
<td><input type="text" name="isbn" /></td>
</tr>
<tr>
<td>タイトル</td>
<td><input type="text" name="title" /></td>
</tr>
</table>
<input type="submit" name="_eventId_search" value="検索" />
<table>
<tr>
<th>ISBN</th>
<th>タイトル</th>
<th>詳細</th>
</tr>
<tr>
<td>1234567890</td>
<td>タイトル1</td>
<td><a href="#" th:href="@{${flowExecutionUrl}(_eventId='detail')}">詳細</a></td>
</tr>
<tr> <td>1234567891</td>
<td>タイトル2</td>
<td><a href="#" th:href="@{${flowExecutionUrl}(_eventId='detail')}">詳細</a></td></tr>
</table>
</form>
</body>
</html>


detail.html

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:tiles="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org" lang="en">
<head>
<script type="text/javascript" th:src="@{/resources/dojo/dojo.js}"></script>
<script type="text/javascript" th:src="@{/resources/spring/Spring.js}"></script>
<script type="text/javascript"
th:src="@{/resources/spring/Spring-Dojo.js}"></script>
<meta charset="UTF-8" />
<title>詳細</title>
</head>
<body>
<form action="" th:action="${flowExecutionUrl}" method="POST">
<table>
<tr>
<th>ISBN</th>
<td>1234567891</td>
</tr>
<tr>
<th>タイトル</th>
<td>タイトル2</td></tr>
</table>
<input type="submit" name="_eventId_back" value="戻る" />
</form>
</body>
</html>


イベントID

ステートの遷移に利用されるeventIdは、submit時のname属性のほか、queryStringでも渡すことができます。


動作確認

gradle bootRun,もしくは、IDE上からApplicationクラスを実行してください。

下記の通りに遷移すると正常に遷移できます。

top→result→detail→top

しかし、下記のようなフロー定義に記載がない遷移はできません。

top→detail

本来なら、クライアント側でも操作できないように制御するべきですが、まずは、サーバ側でしっかりチェックされているので安心です。


実行キー

実行はqueryStringに渡される実行キー(execution=exsx)によって管理されます。xは自動でインクリメントされていきます。メニュー画面からもう一度入りなおした場合、新たに実行キーが割り振られ、過去の実行中のデータとは別のコンテキストで実行できます。複数画面でも、相互に干渉せずに操作が行えます。(デフォルトでは5つまでの実行状態を保持します。5つを超えると上書きされます。)

原則として、フローが開始されればフローの終了まで実行することを想定しているようです。参照系の画面などは、WebFlowではなく、通常のコントローラの方が都合がよいかもしれません。