21. JSF 集成
Spring Web Flow 提供了一个 JavaServer Faces (JSF) 集成,允许您将 JSF UI 组件模型与 Spring Web 流控制器一起使用。 Web Flow 还提供了一个 Spring Security 标记库,用于 JSF 环境。 有关更多详细信息,请参见使用 Spring Security Facelets 标记库。
Spring Web Flow 3.0 需要 JSF 4.0 或更高版本。
21.1. 配置web.xml
第一步是将请求路由到文件中的 。
在以下示例中,我们将所有以 开头的 URL 映射到 Servlet。
需要配置 Servlet。
An 在 Servlet 中用于将 .
这是 Web 应用程序的 Spring 配置的位置。
以下清单显示了配置详细信息:DispatcherServlet
web.xml
/spring/
init-param
contextConfigLocation
<servlet>
<servlet-name>Spring MVC Dispatcher Servlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/web-application-config.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Spring MVC Dispatcher Servlet</servlet-name>
<url-pattern>/spring/*</url-pattern>
</servlet-mapping>
要使 JSF 正确引导,必须像往常一样配置 JSF,即使当你将 JSF 与 Spring Web Flow 一起使用时,通常根本不需要通过它路由请求。
以下清单显示了配置详细信息:FacesServlet
web.xml
<!-- Just here so the JSF implementation can initialize. *Not* used at runtime. -->
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>jakarta.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<!-- Just here so the JSF implementation can initialize -->
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.faces</url-pattern>
</servlet-mapping>
使用 Facelets 而不是 JSP 通常需要以下元素 :web.xml
!-- Use JSF view templates saved as *.xhtml, for use with Facelets -->
<context-param>
<param-name>jakarta.faces.DEFAULT_SUFFIX</param-name>
<param-value>.xhtml</param-value>
</context-param>
21.2. 配置 Web 流以与 JSF 一起使用
本节介绍如何使用 JSF 配置 Web 流。 支持 Java 和 XML 配置。 以下示例配置适用于 XML 中的 Web 流和 JSF:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:webflow="http://www.springframework.org/schema/webflow-config"
xmlns:faces="http://www.springframework.org/schema/faces"
si:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/webflow-config
https://www.springframework.org/schema/webflow-config/spring-webflow-config.xsd
http://www.springframework.org/schema/faces
https://www.springframework.org/schema/faces/spring-faces.xsd">
<!-- Executes flows: the central entry point into the Spring Web Flow system -->
<webflow:flow-executor id="flowExecutor">
<webflow:flow-execution-listeners>
<webflow:listener ref="facesContextListener"/>
</webflow:flow-execution-listeners>
</webflow:flow-executor>
<!-- The registry of executable flow definitions -->
<webflow:flow-registry id="flowRegistry" flow-builder-services="flowBuilderServices" base-path="/WEB-INF">
<webflow:flow-location-pattern value="**/*-flow.xml" />
</webflow:flow-registry>
<!-- Configures the Spring Web Flow JSF integration -->
<faces:flow-builder-services id="flowBuilderServices" />
<!-- A listener maintain one FacesContext instance per Web Flow request. -->
<bean id="facesContextListener"
class="org.springframework.faces.webflow.FlowFacesContextLifecycleListener" />
</beans>
以下示例在 Java 配置中执行相同的操作:
@Configuration
public class WebFlowConfig extends AbstractFacesFlowConfiguration {
@Bean
public FlowExecutor flowExecutor() {
return getFlowExecutorBuilder(flowRegistry())
.addFlowExecutionListener(new FlowFacesContextLifecycleListener())
.build();
}
@Bean
public FlowDefinitionRegistry flowRegistry() {
return getFlowDefinitionRegistryBuilder()
.setBasePath("/WEB-INF")
.addFlowLocationPattern("**/*-flow.xml").build();
}
}
要点是安装 一个在 Web 流请求期间管理单个 的 ,以及使用自定义命名空间中的元素来配置 JSF 环境的渲染。FlowFacesContextLifecycleListener
FacesContext
flow-builder-services
faces
在 JSF 环境中,您还需要以下与 Spring MVC 相关的配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:faces="http://www.springframework.org/schema/faces"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/faces
https://www.springframework.org/schema/faces/spring-faces.xsd">
<faces:resources />
<bean class="org.springframework.faces.webflow.JsfFlowHandlerAdapter">
<property name="flowExecutor" ref="flowExecutor" />
</bean>
</beans>
自定义名称空间元素将 JSF 资源请求委托给 JSF 资源 API。
这是 Web Flow 通常使用的替代品。
此适配器使用 a 而不是 .resources
JsfFlowHandlerAdapter
FlowHandlerAdapter
JsfAjaxHandler
SpringJavaScriptAjaxHandler
当您使用 Java 配置时,基类会自动注册,因此无需执行任何其他操作。AbstractFacesFlowConfiguration
JsfResourceRequestHandler
21.3. 替换 JSF Managed Bean 工具
将 JSF 与 Spring Web Flow 一起使用时,可以将 JSF 托管 Bean 工具完全替换为 Web 流托管变量和 Spring 托管 Bean 的组合。 它通过定义完善的钩子来初始化和执行域模型,使您可以更好地控制托管对象的生命周期。 此外,由于您可能已经将 Spring 用于您的业务层,因此它减少了必须维护两个不同托管 bean 模型的概念开销。
如果您进行纯 JSF 开发,您可能很快就会发现请求范围对于存储驱动复杂事件驱动视图的对话模型对象来说不够长。
在 JSF 中,通常的选项是开始将内容放入会话范围,这会增加额外的负担,即在进入应用程序的另一个视图或功能区域之前需要清理对象。
真正需要的是一个介于请求和会话范围之间的托管范围。
JSF 提供了 flash 和 view 范围,可以通过 以编程方式访问它们。
Spring Web Flow 提供对 flash、view、flow 和 conversation 范围的访问。
这些范围通过 JSF 变量解析器无缝集成,并且在所有 JSF 应用程序中的工作方式都相同。UIViewRoot.getViewMap()
21.3.1. 使用 Flow Variables
声明和管理模型最简单、最自然的方法是使用流变量。 您可以在流的开头声明这些变量,如下所示:
<var name="searchCriteria" class="com.mycompany.myapp.hotels.search.SearchCriteria"/>
然后,您可以通过 EL 在流的其中一个 JSF 视图模板中引用此变量,如下所示:
<h:inputText id="searchString" value="#{searchCriteria.searchString}"/>
请注意,从模板引用变量时,无需为变量添加其范围前缀(尽管如果需要更具体,可以这样做)。 与标准 JSF Bean 一样,将搜索所有可用范围以查找匹配的变量,因此您可以在流定义中更改变量的范围,而不必修改引用该变量的 EL 表达式。
您还可以定义范围限定为当前视图的视图实例变量,并在转换到另一个视图时自动清理这些变量。 这对于 JSF 非常有用,因为视图通常是为了在转换到另一个视图之前处理多个请求中的多个页面内事件。
要定义视图实例变量,您可以在定义中使用 element,如下所示:var
view-state
<view-state id="enterSearchCriteria">
<var name="searchCriteria" class="com.mycompany.myapp.hotels.search.SearchCriteria"/>
</view-state>
21.3.2. 使用作用域 Spring Bean
尽管定义自动装配的流实例变量提供了很好的模块化和可读性,但在某些情况下,您可能希望使用 Spring 容器的其他功能,例如面向方面的编程 (AOP)。
在这些情况下,你可以在 Spring 中定义一个 bean 并为其提供特定的 Web 流范围,如下所示:ApplicationContext
<bean id="searchCriteria" class="com.mycompany.myapp.hotels.search.SearchCriteria" scope="flow"/>
此方法的主要区别在于,在首次通过 EL 表达式访问 Bean 之前,它不会完全初始化。 这种通过 EL 进行的惰性实例化与通常分配 JSF 管理的 bean 的方式非常相似。
21.3.3. 操作模型
在视图渲染之前初始化模型(例如,通过从数据库加载持久化实体)是很常见的,但 JSF 本身并没有为这种初始化提供任何方便的钩子。 流定义语言通过其 actions 为此提供了一个自然的工具。 Spring Web Flow 为将操作的结果转换为特定于 JSF 的数据结构提供了一些额外的便利。 以下示例显示了如何执行此操作:
<on-render>
<evaluate expression="bookingService.findBookings(currentUser.name)"
result="viewScope.bookings" result-type="dataModel" />
</on-render>
前面的示例采用该方法的结果,并将其包装在自定义 JSF DataModel 中,以便可以在标准 JSF DataTable 组件中使用该列表,如下所示:bookingService.findBookings
<h:dataTable id="bookings" styleClass="summary" value="#{bookings}" var="booking"
rendered="#{bookings.rowCount > 0}">
<h:column>
<f:facet name="header">Name</f:facet>
#{booking.hotel.name}
</h:column>
<h:column>
<f:facet name="header">Confirmation number</f:facet>
#{booking.id}
</h:column>
<h:column>
<f:facet name="header">Action</f:facet>
<h:commandLink id="cancel" value="Cancel" action="cancelBooking" />
</h:column>
</h:dataTable>
21.3.4. 数据模型实现
在上一节所示的示例中,导致使用自定义类型包装 。
自定义提供了额外的便利,例如可序列化以超出请求范围的存储,以及访问 EL 表达式中当前选定的行。
例如,在从操作事件由组件触发的视图中回发时,您可以对所选行的模型实例执行操作,如下所示:result-type="dataModel"
List<Booking>
DataModel
DataModel
DataTable
<transition on="cancelBooking">
<evaluate expression="bookingService.cancelBooking(bookings.selectedRow)" />
</transition>
Spring Web Flow 提供了两种自定义 DataModel 类型:和 。
顾名思义,它们会跟踪一个或多个选定的行。
这是在侦听器的帮助下完成的,侦听器响应 JSF 操作事件,并在数据模型上调用适当的方法来记录当前单击的行。OneSelectionTrackingListDataModel
ManySelectionTrackingListDataModel
SelectionTrackingActionListener
SelectionAware
要了解如何配置此配置,请记住,在启动时会针对别名注册 a。
When 在流定义中使用时,它会导致 被使用。
然后,转换器用 .
要使用 ,您需要注册自己的自定义转换器。FacesConversionService
DataModelConverter
dataModel
result-type="dataModel"
DataModelConverter
List
OneSelectionTrackingListDataModel
ManySelectionTrackingListDataModel
21.4. 使用 Spring Web Flow 处理 JSF 事件
Spring Web Flow 允许您以解耦的方式处理 JSF 操作事件,不需要 JSF API 上的 Java 代码直接依赖。 事实上,这些事件通常可以完全使用流定义语言进行处理,根本不需要任何自定义 Java 操作代码。 这允许更敏捷的开发过程,因为在连接事件(JSF 视图模板和 SWF 流定义)时作的工件可以立即刷新,而无需构建和重新部署整个应用程序。
21.4.1. 处理 JSF 页内操作事件
在 JSF 中,一个简单但常见的情况是需要向一个事件发出信号,该事件以某种方式导致模型纵,然后重新显示相同的视图以反映模型的变化状态。
流定义语言在元素中对此有特殊支持。transition
一个很好的示例是分页列表结果表。
假设您希望能够仅加载和显示大型结果列表的一部分,并允许用户浏览结果。
加载和显示列表的初始定义如下:view-state
<view-state id="reviewHotels">
<on-render>
<evaluate expression="bookingService.findHotels(searchCriteria)"
result="viewScope.hotels" result-type="dataModel" />
</on-render>
</view-state>
您可以构造一个显示当前列表的 JSF DataTable,然后在该表下方放置一个链接,如下所示:hotels
More Results
<h:commandLink id="nextPageLink" value="More Results" action="next"/>
这从其属性发出事件信号。
然后,您可以通过添加到定义来处理事件,如下所示:commandLink
next
action
view-state
<view-state id="reviewHotels">
<on-render>
<evaluate expression="bookingService.findHotels(searchCriteria)"
result="viewScope.hotels" result-type="dataModel" />
</on-render>
<transition on="next">
<evaluate expression="searchCriteria.nextPage()" />
</transition>
</view-state>
在这里,您可以通过增加实例的页面计数来处理事件。
然后,使用更新的标准再次调用该操作,这会导致下一页结果加载到 .
由于元素上没有属性,因此将重新渲染相同的视图,并且模型中的更改会反映在视图中。next
searchCriteria
on-render
DataModel
to
transition
21.4.2. 处理 JSF Action 事件
页面内事件之外的下一个逻辑级别是需要导航到另一个视图的事件,并在此过程中对模型进行一些操作。
使用纯 JSF 实现这一点需要向 JSF 托管 bean 中添加导航规则,并且可能需要添加一些中间 Java 代码(这两项任务都需要重新部署)。使用流定义语言,您可以在一个位置简洁地处理此类情况,其方式类似于处理页面内事件的方式。faces-config.xml
继续我们操作结果分页列表的用例,假设我们希望显示的每一行都包含指向该行实例的详细信息页面的链接。
您可以向包含以下组件的表中添加一列,如下所示:DataTable
commandLink
<h:commandLink id="viewHotelLink" value="View Hotel" action="select"/>
这将引发事件,然后您可以通过将另一个元素添加到现有 , 来处理该事件,如下所示:select
transition
view-state
<view-state id="reviewHotels">
<on-render>
<evaluate expression="bookingService.findHotels(searchCriteria)"
result="viewScope.hotels" result-type="dataModel" />
</on-render>
<transition on="next">
<evaluate expression="searchCriteria.nextPage()" />
</transition>
<transition on="select" to="reviewHotel">
<set name="flowScope.hotel" value="hotels.selectedRow" />
</transition>
</view-state>
在这里,通过将当前选定的 hotel 实例从 into 流范围推送到流范围来处理事件,以便它可以被 .select
DataTable
reviewHotel
view-state
21.4.3. 执行模型验证
JSF 提供了有用的工具,用于在将更改应用于模型之前在字段级别验证输入。 但是,在应用更新后,如果需要在模型级别执行更复杂的验证,则通常必须向托管 Bean 中的 JSF 操作方法添加更多自定义代码。 这种验证通常是域模型本身的责任,但是很难将任何错误消息传播回视图,而不会在域层中引入对 JSF API 的不良依赖。
使用 Web 流,您可以在业务代码中使用 generic 和 low,并且在那里添加的任何消息都可以在渲染时使用。MessageContext
FacesContext
例如,假设您有一个视图,用户在其中输入完成酒店预订所需的详细信息,并且您需要确保 和 日期符合一组给定的业务规则。
您可以从元素调用此类模型级验证,如下所示:Check In
Check Out
transition
<view-state id="enterBookingDetails">
<transition on="proceed" to="reviewBooking">
<evaluate expression="booking.validateEnterBookingDetails(messageContext)" />
</transition>
</view-state>
在这里,通过在预订实例上调用模型级验证方法来处理事件,传递泛型实例,以便可以记录消息。
然后,这些消息可以与组件中的任何其他 JSF 消息一起显示。proceed
MessageContext
h:messages
21.4.4. 在 JSF 中处理 Ajax 事件
JSF 为在服务器端发送 Ajax 请求和执行部分处理和呈现提供了内置支持。
您可以通过 facelets 标记指定用于部分渲染的 ID 列表。<f:ajax>
在 Spring Web Flow 中,您还可以选择使用 render 操作指定用于服务器端部分渲染的 ID,如下所示:
<view-state id="reviewHotels">
<on-render>
<evaluate expression="bookingService.findHotels(searchCriteria)"
result="viewScope.hotels" result-type="dataModel" />
</on-render>
<transition on="next">
<evaluate expression="searchCriteria.nextPage()" />
<render fragments="hotels:searchResultsFragment" />
</transition>
</view-state>
21.5. 在页面上嵌入流
默认情况下,当流程进入视图状态时,它会在呈现视图之前运行客户端重定向。 这种方法称为 “POST-REDIRECT-GET”。 它的优点是将一个视图的表单处理与下一个视图的呈现分开。 因此,浏览器的 Back (返回) 和 Refresh (刷新) 按钮可以无缝工作,而不会引起任何浏览器警告。
通常,从用户的角度来看,客户端重定向是透明的。 但是,在某些情况下,“POST-REDIRECT-GET” 可能不会带来相同的好处。 例如,有时在页面上嵌入流并使用 Ajax 请求驱动流,以仅刷新呈现流的页面区域,这可能很有用。 在这种情况下,不仅没有必要使用客户端重定向,而且在保持页面周围内容完好无损方面也不是理想的行为。
要指示流应在“页面嵌入”模式下执行,您可以传递一个额外的流输入属性,该属性的值为 。以下示例显示了在嵌入式模式下调用子流的顶级容器流:mode
embedded
<subflow-state id="bookHotel" subflow="booking">
<input name="mode" value="'embedded'"/>
</subflow-state>
在“页面嵌入”模式下启动时,子流不会在 Ajax 请求期间发出流执行重定向。
有关嵌入式流的示例,请参阅项目。
您可以在本地签出源代码,像 Maven 项目一样构建它,然后将其导入到 Eclipse 或其他 IDE 中,如下所示:webflow-primefaces-showcase
cd some-directory
git clone https://github.com/spring-projects/spring-webflow-samples.git
cd primefaces-showcase
mvn package
# import into Eclipse
您需要查看的具体示例位于 “Advanced Ajax” 选项卡下,称为 “Top Flow with Embedded Sub-Flow”。
21.6. 在同一状态重定向
默认情况下,只要当前请求不是 Ajax 请求,Web Flow 就会执行客户端重定向,即使它保持相同的视图状态。 这在表单验证失败后非常有用(例如)。 如果用户点击 Refresh (刷新) 或 Back (返回),则不会看到任何浏览器警告。 如果 Web 流不执行重定向,他们会这样做。
这可能会导致特定于 JSF 环境的问题,即特定的 Sun Mojarra 侦听器组件会缓存 ,假设同一实例在整个 JSF 生命周期中都可用。
但是,在 Web Flow 中,渲染阶段会暂时搁置,并执行客户端重定向。FacesContext
Web Flow 的默认行为是可取的,JSF 应用程序不太可能遇到此问题。 这是因为 Ajax 通常在 JSF 组件库中作为默认启用,并且 Web Flow 在 Ajax 请求期间不会重定向。 但是,如果您遇到此问题,则可以在同一视图中禁用客户端重定向,如下所示:
<webflow:flow-executor id="flowExecutor">
<webflow:flow-execution-attributes>
<webflow:redirect-in-same-state value="false"/>
</webflow:flow-execution-attributes>
</webflow:flow-executor>
21.7. 使用 JSF 处理文件上传
大多数 JSF 组件提供程序都包含某种形式的文件上传组件。
通常,在使用这些组件时,JSF 必须完全控制解析多部分请求,并且不能使用 Spring MVC。MultipartResolver
Spring Web Flow 已经使用 PrimeFaces 的文件上传组件进行了测试。 请查看其他提供程序的 JSF 组件库的文档,以了解如何配置文件上传。
通常,您需要在 Servlet 容器中启用多部分支持,
或者通过在 web.xml 中的声明中添加 “multipart-config” 元素,
或者使用编程 servlet 注册DispatcherServlet
jakarta.servlet.MultipartConfigElement
21.8. 使用 Spring Security Facelets 标记库
要使用该库,您需要创建一个文件并在 中注册它。taglib.xml
web.xml
您需要创建一个包含以下内容的文件:/WEB-INF/springsecurity.taglib.xml
<?xml version="1.0"?>
<!DOCTYPE facelet-taglib PUBLIC
"-//Sun Microsystems, Inc.//DTD Facelet Taglib 1.0//EN"
"https://java.sun.com/dtd/facelet-taglib_1_0.dtd">
<facelet-taglib>
<namespace>http://www.springframework.org/security/tags</namespace>
<tag>
<tag-name>authorize</tag-name>
<handler-class>org.springframework.faces.security.FaceletsAuthorizeTagHandler</handler-class>
</tag>
<function>
<function-name>areAllGranted</function-name>
<function-class>org.springframework.faces.security.FaceletsAuthorizeTagUtils</function-class>
<function-signature>boolean areAllGranted(java.lang.String)</function-signature>
</function>
<function>
<function-name>areAnyGranted</function-name>
<function-class>org.springframework.faces.security.FaceletsAuthorizeTagUtils</function-class>
<function-signature>boolean areAnyGranted(java.lang.String)</function-signature>
</function>
<function>
<function-name>areNotGranted</function-name>
<function-class>org.springframework.faces.security.FaceletsAuthorizeTagUtils</function-class>
<function-signature>boolean areNotGranted(java.lang.String)</function-signature>
</function>
<function>
<function-name>isAllowed</function-name>
<function-class>org.springframework.faces.security.FaceletsAuthorizeTagUtils</function-class>
<function-signature>boolean isAllowed(java.lang.String, java.lang.String)</function-signature>
</function>
</facelet-taglib>
接下来,您需要在 中注册 taglib 文件(在前面的清单中),如下所示:web.xml
<context-param>
<param-name>jakarta.faces.FACELETS_LIBRARIES</param-name>
<param-value>/WEB-INF/springsecurity.taglib.xml</param-value>
</context-param>
现在,您已准备好在视图中使用标记库。 您可以使用 authorize 标签有条件地包含嵌套内容,如下所示:
<!DOCTYPE composition PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:sec="http://www.springframework.org/security/tags">
<sec:authorize ifAllGranted="ROLE_FOO, ROLE_BAR">
Lorem ipsum dolor sit amet
</sec:authorize>
<sec:authorize ifNotGranted="ROLE_FOO, ROLE_BAR">
Lorem ipsum dolor sit amet
</sec:authorize>
<sec:authorize ifAnyGranted="ROLE_FOO, ROLE_BAR">
Lorem ipsum dolor sit amet
</sec:authorize>
</ui:composition>
您还可以在任何 JSF 组件的 rendered 或其他属性中使用多个 EL 函数之一,如下所示:
<!DOCTYPE composition PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:sec="http://www.springframework.org/security/tags">
<!-- Rendered only if user has all of the listed roles -->
<h:outputText value="Lorem ipsum dolor sit amet" rendered="#{sec:areAllGranted('ROLE_FOO, ROLE_BAR')}"/>
<!-- Rendered only if user does not have any of the listed roles -->
<h:outputText value="Lorem ipsum dolor sit amet" rendered="#{sec:areNotGranted('ROLE_FOO, ROLE_BAR')}"/>
<!-- Rendered only if user has any of the listed roles -->
<h:outputText value="Lorem ipsum dolor sit amet" rendered="#{sec:areAnyGranted('ROLE_FOO, ROLE_BAR')}"/>
<!-- Rendered only if user has access to given HTTP method/URL as defined in Spring Security configuration -->
<h:outputText value="Lorem ipsum dolor sit amet" rendered="#{sec:isAllowed('/secured/foo', 'POST')}"/>
</ui:composition>