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
第一步是将请求路由到DispatcherServlet
在web.xml
文件。
在以下示例中,我们将映射所有以/spring/
添加到 servlet 中。
需要配置 Servlet。
一init-param
用于传递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 流和 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 请求的持续时间内以及使用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
自定义名称空间元素将 JSF 资源请求委托给 JSF 资源 API。
这JsfFlowHandlerAdapter
是FlowHandlerAdapter
通常与 Web Flow 一起使用。
此适配器使用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 范围,可以通过UIViewRoot.getViewMap()
.
Spring Web Flow 提供对 flash、view、flow 和 conversation 范围的访问。
这些范围通过 JSF 变量解析器无缝集成,并且在所有 JSF 应用程序中的工作方式都相同。
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 非常有用,因为视图通常是为了在转换到另一个视图之前处理多个请求中的多个页面内事件。
要定义视图实例变量,可以使用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 本身并没有为这种初始化提供任何方便的钩子。 流定义语言通过其 actions 为此提供了一个自然的工具。 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
,您可以对所选行的 Model 实例执行作,如下所示:
<transition on="cancelBooking">
<evaluate expression="bookingService.cancelBooking(bookings.selectedRow)" />
</transition>
Spring Web Flow 提供了两种自定义 DataModel 类型:OneSelectionTrackingListDataModel
和ManySelectionTrackingListDataModel
.
顾名思义,它们会跟踪一个或多个选定的行。
这是在SelectionTrackingActionListener
listener 的 API 调用,它响应 JSF作事件并在SelectionAware
data models 来记录当前单击的行。
要了解它是如何配置的,请记住,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,该 API 的 API 中的hotels
list 中,然后放置一个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 实现此目的需要向faces-config.xml
以及 JSF 托管 bean 中的一些中间 Java 代码(这两项任务都需要重新部署)。使用流定义语言,您可以在一个位置简洁地处理此类情况,其方式类似于处理页面内事件的方式。
继续我们作结果分页列表的用例,假设我们希望显示的每一行DataTable
以包含指向该 ROW 实例的详细信息页面的链接。
您可以向包含以下内容的表中添加一列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
事件的处理方法是将当前选定的 hotel 实例从DataTable
到 flow 范围内,以便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
事件是通过在 booking 实例上调用模型级验证方法来处理的,并传递泛型MessageContext
实例,以便可以记录消息。
然后,这些消息可以与任何其他 JSF 消息一起显示在h:messages
元件。
21.4.4. 在 JSF 中处理 Ajax 事件
JSF 为在服务器端发送 Ajax 请求和执行部分处理和呈现提供了内置支持。
您可以通过<f:ajax>
facelets 标记。
在 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 请求驱动流,以仅刷新呈现流的页面区域,这可能很有用。 在这种情况下,不仅没有必要使用客户端重定向,而且在保持页面周围内容完好无损方面也不是理想的行为。
要指示流应在 “page embedded” 模式下执行,您可以传递一个名为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
您需要查看的具体示例位于 “Advanced Ajax” 选项卡下,称为 “Top Flow with Embedded Sub-Flow”。
21.6. 在同一状态重定向
默认情况下,只要当前请求不是 Ajax 请求,Web Flow 就会执行客户端重定向,即使它保持相同的视图状态。 这在表单验证失败后非常有用(例如)。 如果用户点击 Refresh (刷新) 或 Back (返回),则不会看到任何浏览器警告。 如果 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 组件的 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>