21. JSF 集成
Spring Web Flow 提供了 JavaServer Faces (JSF) 集成,允许您将 JSF UI 组件模型与 Spring Web Flow 控制器一起使用。 Web Flow 还提供了一个用于 JSF 环境的 Spring Security 标签库。 有关更多详细信息,请参阅使用 Spring Security Facelets 标签库。
Spring Web Flow 3.0 需要 JSF 4.0 或更高版本。
21.1. 配置web.xml
第一步是将请求路由到DispatcherServlet
在web.xml
文件。
在以下示例中,我们映射所有以/spring/
到 servlet。
需要配置servlet。
一init-param
用于在 servlet 中传递contextConfigLocation
.
这是 Web 应用程序的 Spring 配置的位置。
以下列表显示了配置详细信息:
<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 正确引导,请使用FacesServlet
必须在web.xml
就像通常一样,即使当您将 JSF 与 Spring Web Flow 一起使用时,您通常根本不需要通过它路由请求。
以下列表显示了配置详细信息:
<!-- 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 Flow 和 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();
}
}
要点是安装一个FlowFacesContextLifecycleListener
管理单个FacesContext
在 Web 流请求的持续时间和使用flow-builder-services
元素中的faces
自定义命名空间来配置 JSF 环境的渲染。
在 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>
这resources
custom namespace 元素将 JSF 资源请求委托给 JSF 资源 API。
这JsfFlowHandlerAdapter
是FlowHandlerAdapter
通常与 Web Flow 一起使用。
此适配器使用JsfAjaxHandler
而不是SpringJavaScriptAjaxHandler
.
当您使用 Java 配置时,AbstractFacesFlowConfiguration
基类自动注册JsfResourceRequestHandler
,所以没有什么可做的了。
21.3. 替换 JSF 托管 Bean 设施
当您将 JSF 与 Spring Web Flow 一起使用时,您可以将 JSF 托管 Bean 工具完全替换为 Web Flow 托管变量和 Spring 托管 Bean 的组合。 它使您可以更好地控制托管对象的生命周期,并具有用于初始化和执行域模型的明确定义的钩子。 此外,由于您可能已经将 Spring 用于业务层,因此它减少了必须维护两个不同托管 Bean 模型的概念开销。
如果您进行纯 JSF 开发,您可能很快就会发现请求范围的寿命不够长,无法存储驱动复杂事件驱动视图的对话模型对象。
在 JSF 中,通常的选择是开始将事物放入会话作用域,在进入应用程序的另一个视图或功能区域之前需要清理对象的额外负担。
真正需要的是一个介于请求和会话范围之间的托管范围。
JSF 提供了闪存和视图范围,可以通过UIViewRoot.getViewMap()
.
Spring Web Flow 提供对 flash、view、flow 和对话范围的访问。
这些作用域通过 JSF 变量解析器无缝集成,并且在所有 JSF 应用程序中的工作方式相同。
21.3.1. 使用流变量
声明和管理模型的最简单、最自然的方法是使用流变量。 您可以在流开始时声明这些变量,如下所示:
<var name="searchCriteria" class="com.mycompany.myapp.hotels.search.SearchCriteria"/>
然后,您可以通过 EL 在流的 JSF 视图模板之一中引用此变量,如下所示:
<h:inputText id="searchString" value="#{searchCriteria.searchString}"/>
请注意,从模板引用变量时,不需要为变量添加其作用域前缀(但如果您需要更具体,可以这样做)。 与标准 JSF Bean 一样,所有可用的作用域都会搜索匹配的变量,因此您可以在流定义中更改变量的作用域,而无需修改引用它的 EL 表达式。
您还可以定义范围限定为当前视图并在转换到另一个视图时自动清理的视图实例变量。 这对于 JSF 非常有用,因为视图通常被构造为在转换到另一个视图之前处理跨多个请求的多个页面内事件。
要定义视图实例变量,您可以使用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 中定义一个 beanApplicationContext
并为其指定特定的 Web 流范围,如下所示:
<bean id="searchCriteria" class="com.mycompany.myapp.hotels.search.SearchCriteria" scope="flow"/>
这种方法的主要区别在于,在首次通过 EL 表达式访问 Bean 之前,它不会完全初始化。 这种通过 EL 的延迟实例化与通常分配 JSF 管理的 Bean 的方式非常相似。
21.3.3.作模型
在视图渲染之前初始化模型(例如通过从数据库加载持久实体)的需求是很常见的,但 JSF 本身没有为这种初始化提供任何方便的钩子。 流定义语言通过其作为此提供了自然设施。 Spring Web Flow 为将作的结果转换为特定于 JSF 的数据结构提供了一些额外的便利。 以下示例显示了如何执行此作:
<on-render>
<evaluate expression="bookingService.findBookings(currentUser.name)"
result="viewScope.bookings" result-type="dataModel" />
</on-render>
前面的示例采用bookingService.findBookings
方法并将其包装在自定义 JSF DataModel 中,以便该列表可以在标准 JSF DataTable 组件中使用,如下所示:
<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. 数据模型实现
在上一节所示的示例中,result-type="dataModel"
结果List<Booking>
使用自定义DataModel
类型。
自定义DataModel
提供了额外的便利,例如可序列化用于超出请求范围的存储以及访问 EL 表达式中当前选择的行。
例如,在从视图回发时,作事件是由DataTable
,您可以对选定行的模型实例执行作,如下所示:
<transition on="cancelBooking">
<evaluate expression="bookingService.cancelBooking(bookings.selectedRow)" />
</transition>
Spring Web Flow 提供了两种自定义 DataModel 类型:OneSelectionTrackingListDataModel
和ManySelectionTrackingListDataModel
.
顾名思义,它们跟踪一行或多行选定行。
这是在SelectionTrackingActionListener
listener,它响应 JSF作事件并在SelectionAware
数据模型来记录当前单击的行。
要了解这是如何配置的,请记住FacesConversionService
注册一个DataModelConverter
针对别名dataModel
启动时。
什么时候result-type="dataModel"
在流定义中使用时,它会导致DataModelConverter
待使用。
然后转换器将给定的List
实例为OneSelectionTrackingListDataModel
.
要使用ManySelectionTrackingListDataModel
,您需要注册自己的自定义转换器。
21.4. 使用 Spring Web Flow 处理 JSF 事件
Spring Web Flow 允许您以解耦的方式处理 JSF作事件,不需要在 Java 代码中直接依赖 JSF API。 事实上,这些事件通常可以完全在流定义语言中处理,而根本不需要任何自定义的 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,该 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作事件
页面内事件之外的下一个逻辑级别是需要导航到另一个视图的事件,并在此过程中对模型进行一些作。
使用纯 JSF 实现这一点需要将导航规则添加到faces-config.xml
以及 JSF 托管 Bean 中可能的一些中间 Java 代码(这两个任务都需要重新部署)。使用流定义语言,您可以在一个位置以类似于处理页内事件的方式简洁地处理此类情况。
继续作结果分页列表的用例,假设我们希望显示的每一行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>
在这里,select
事件是通过从DataTable
进入流范围,以便可以被reviewHotel
view-state
.
21.4.3. 执行模型验证
JSF 提供了有用的工具,用于在将更改应用于模型之前在字段级别验证输入。 但是,当您需要在应用更新后在模型级别执行更复杂的验证时,通常必须向托管 Bean 中的 JSF作方法添加更多自定义代码。 这种验证通常是域模型本身的责任,但是很难将任何错误消息传播回视图,而不引入对域层中 JSF API 的不良依赖。
使用 Web Flow,您可以使用通用和低级MessageContext
,并且添加的任何消息都可以用于FacesContext
在渲染时。
例如,假设您有一个视图,用户在其中输入必要的详细信息以完成酒店预订,并且您需要确保Check In
和Check Out
日期遵循一组给定的业务规则。
您可以从transition
元素,如下所示:
<view-state id="enterBookingDetails">
<transition on="proceed" to="reviewBooking">
<evaluate expression="booking.validateEnterBookingDetails(messageContext)" />
</transition>
</view-state>
在这里,proceed
事件是通过在预订实例上调用模型级验证方法来处理的,并将泛型MessageContext
实例,以便可以记录消息。
然后,这些消息可以与h:messages
元件。
21.4.4. 在 JSF 中处理 Ajax 事件
JSF 为发送 Ajax 请求以及在服务器端执行部分处理和渲染提供了内置支持。
您可以通过<f:ajax>
facelets 标签。
在 Spring Web Flow 中,您还可以选择指定要使用渲染作在服务器端进行部分渲染的 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”。 它的优点是将一个视图的表单处理与下一个视图的呈现分开。 因此,浏览器的“后退”和“刷新”按钮可以无缝工作,而不会引起任何浏览器警告。
通常,从用户的角度来看,客户端重定向是透明的。 但是,在某些情况下,“POST-REDIRECT-GET”可能不会带来相同的好处。 例如,有时在页面上嵌入流并使用 Ajax 请求驱动它,以仅刷新呈现流的页面区域可能很有用。 在这种情况下,不仅没有必要使用客户端重定向,而且在保持页面周围内容完整方面也不是所需的行为。
要指示流应在“页面嵌入”模式下执行,您可以传递一个名为mode
值为embedded
.以下示例显示了在嵌入式模式下调用子流的顶级容器流:
<subflow-state id="bookHotel" subflow="booking">
<input name="mode" value="'embedded'"/>
</subflow-state>
在“页面嵌入”模式下启动时,子流不会在 Ajax 请求期间发出流执行重定向。
有关嵌入式流的示例,请参阅webflow-primefaces-showcase
项目。
您可以在本地签出源代码,像构建 Maven 项目一样构建它,然后将其导入 Eclipse 或其他 IDE,如下所示:
cd some-directory
git clone https://github.com/spring-projects/spring-webflow-samples.git
cd primefaces-showcase
mvn package
# import into Eclipse
您需要查看的具体示例位于“高级 Ajax”选项卡下,称为“具有嵌入式子流的顶流”。
21.6. 在同一状态下重定向
默认情况下,Web Flow 会执行客户端重定向,即使它保持相同的视图状态,只要当前请求不是 Ajax 请求。 这在表单验证失败后非常有用(例如)。 如果用户点击“刷新”或“返回”,他们不会看到任何浏览器警告。 如果 Web 流没有执行重定向,他们就会这样做。
这可能会导致特定于 JSF 环境的问题,其中特定的 Sun Mojarra 监听器组件缓存FacesContext
,假设同一实例在整个 JSF 生命周期中可用。
但是,在 Web Flow 中,呈现阶段被暂时搁置,并执行客户端重定向。
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 容器中启用多部分支持,要么通过将“multipart-config”元素添加到DispatcherServlet
声明,web.xml,或通过使用jakarta.servlet.MultipartConfigElement
在编程 Servlet 注册中
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>
接下来,您需要在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 组件的渲染属性或其他属性中使用多个 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>