Введение
Хотел бы поделиться своим опытом необычной интеграции этих двух фреймворков. Мне очень бы не хотелось касаться таких исключительно важных вопросов «а зачем вообще нужна JSF», оговорюсь, что я не являюсь сторонником этой технологии.
Достаточно длительный срок разрабатывалось приложение-зоопарк на Spring + Hibernate + большое количество PL/SQL файлов и пакетов Oracle. Интерфейс пользователя создавался на ExtJS 4-й и 2-й версии, местами использовался самопальный JavaScript и HTML. В общем нормальный корпоративный франкенштейн. Обстоятельства непреодолимой силы вынудили меня использовать JSF для создания некоторой части интерфейсов, таким образом, JSF должен быть интегрирован в уже существующую систему обработки запросов на базе Spring MVC. Я использовал Primefaces, но полагаю, что все для остальных реализаций применимы те же способы.
Очевидные вещи
Для начала создадим отдельный сервлет в файле web.xml.
<servlet>
<servlet-name>JSF</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>JSF</servlet-name>
<url-pattern>*.xhtml</url-pattern>
</servlet-mapping>
А в файле faces-config.xml добавим поддержку Spring, чтобы в xhtml объектах можно было использовать бины, созданные спринговыми аннотациями.
<application>
<el-resolver>org.springframework.web.jsf.el.SpringBeanFacesELResolver</el-resolver>
</application>
Оборачиваем запросы
До сих пор все достаточно просто и описано практически везде. Сейчас нам предстоить решить следующие задачи:
- обеспечить передачу get и post запросов через spring в JSF.
- добавить поддержку транзакций (у нас внутри Hibernate с lazy-коллекциями)
- создать бины с временем жизни «view»
- запретить доступ пользователю к xhtml файлам
Первые два пункта можно решить одновременно. В нашем приложении все URL по которым будут создаваться интерфейсы на JSF имеют вид "/app/*/jsf/*"
@Controller("")
@RequestMapping("/app/*/jsf")
public class JSFTestController {
@RequestMapping(value = "/*", method = {RequestMethod.GET})
@Transactional(rollbackFor = Exception.class)
public void redirectToJSF(HttpServletRequest request, HttpServletResponse response) throws Exception {
String uri = request.getRequestURI();
// зачем это нужно станет ясно позднее
RequestContextHolder.getRequestAttributes().setAttribute(JSF_REQUEST_URL, uri, RequestAttributes.SCOPE_REQUEST);
String xhtmlPath= getXHTMLPath(uri); // здесь выбираем xhml файл, подробности специфичны для приложения
// здесь много полезного кода, специфичного для приложения
request.getRequestDispatcher(xhtmlPath).forward(request, response);
}
// пригодится потом
public static String getURLFromRequest(HttpServletRequest request) {
return (String) RequestContextHolder.getRequestAttributes().getAttribute(JSF_REQUEST_URL, RequestAttributes.SCOPE_REQUEST);
}
метод getXHTMLPath возpашает путь до xhtml файла, например /resources/jsf/accounts/find_create.xhtml для доступа к файлу, который лежит в [war file]resourcesjsfaccountsfind_create.xhtml.
Этого достаточно для того, чтобы увидеть отрендеренную xhtml страницу, однако PrimeFaces богата POST запросами построения интерфейса, а они явно идут по неправильным адресам. За формирование пути POST запроса (т.е. за значение атрибута action у form) отвечает рендерер формы. Нужно изменить всего одну статическую функцию getActionStr, к несчастью ее сделали статической и придется перекрыть ту функцию, которая ее вызывает:
public class ActCorrectFormRenderer extends FormRenderer {
private static final com.sun.faces.renderkit.Attribute[] ATTRIBUTES = AttributeManager.getAttributes(AttributeManager.Key.FORMFORM);
private boolean writeStateAtEnd;
public ActCorrectFormRenderer() {
WebConfiguration webConfig = WebConfiguration.getInstance();
writeStateAtEnd = webConfig.isOptionEnabled(WebConfiguration.BooleanWebContextInitParameter.WriteStateAtFormEnd);
}
@Override
public void encodeBegin(FacesContext context, UIComponent component)
throws IOException {
rendererParamsNotNull(context, component);
if (!shouldEncode(component)) {
return;
}
ResponseWriter writer = context.getResponseWriter();
assert (writer != null);
String clientId = component.getClientId(context);
// since method and action are rendered here they are not added
// to the pass through attributes in Util class.
writer.write('n');
writer.startElement("form", component);
writer.writeAttribute("id", clientId, "clientId");
writer.writeAttribute("name", clientId, "name");
writer.writeAttribute("method", "post", null);
writer.writeAttribute("action", getActionStr(context), null);
String styleClass =
(String) component.getAttributes().get("styleClass");
if (styleClass != null) {
writer.writeAttribute("class", styleClass, "styleClass");
}
String acceptcharset = (String)
component.getAttributes().get("acceptcharset");
if (acceptcharset != null) {
writer.writeAttribute("accept-charset", acceptcharset,
"acceptcharset");
}
RenderKitUtils.renderPassThruAttributes(context,
writer,
component,
ATTRIBUTES);
writer.writeText("n", component, null);
// this hidden field will be checked in the decode method to
// determine if this form has been submitted.
writer.startElement("input", component);
writer.writeAttribute("type", "hidden", "type");
writer.writeAttribute("name", clientId,
"clientId");
writer.writeAttribute("value", clientId, "value");
writer.endElement("input");
writer.write('n');
// Write out special hhidden field for partial submits
String viewId = context.getViewRoot().getViewId();
String actionURL =
context.getApplication().getViewHandler().getActionURL(context, viewId);
ExternalContext externalContext = context.getExternalContext();
String encodedActionURL = externalContext.encodeActionURL(actionURL);
String encodedPartialActionURL = externalContext.encodePartialActionURL(actionURL);
if (encodedPartialActionURL != null) {
if (!encodedPartialActionURL.equals(encodedActionURL)) {
writer.startElement("input", component);
writer.writeAttribute("type", "hidden", "type");
writer.writeAttribute("name", "javax.faces.encodedURL", null);
writer.writeAttribute("value", encodedPartialActionURL, "value");
writer.endElement("input");
writer.write('n');
}
}
if (!writeStateAtEnd) {
context.getApplication().getViewHandler().writeState(context);
writer.write('n');
}
}
private static String getActionStr(FacesContext context) {
return JSFTestController.getURLFromRequest(HttpUtils.getCurrentRequest());
}
}
Это неудобно, но другого варианта изменить статическую функцию ее нету. Но таким образом удалось изменить action формы и пост запросы будут идти по тому же адресу, что и get запрос, построивший view для страницы. Осталось прописать свой рендерер в файле faces-config:
<render-kit>
...
<renderer>
<component-family>org.primefaces.component</component-family>
<renderer-type>org.primefaces.component.MenuRenderer</renderer-type>
<renderer-class>com.XXXXX.ActCorrectMenuRenderer</renderer-class>
</renderer>
...
</render-kit>
В приложении при обработке GET и POST запросов используются немного разные проверки, поэтому методы разделены, но для простого примера методу redirectToJSF можно поставить аннотацию @RequestMapping(value = "/*", method = {RequestMethod.GET, RequestMethod.POST})
«view» scope
В JSF необходимы бины, которые существуют в рамках view, т.е. порождаются при каждом GET запросе и живут при POST запросах, которые происходят со страницы, порожденной этим GET запросом. Не станем изобретать велосипед, позаимствуем код для класса вот тут: forum.springsource.org/showthread.php?80595-View-scope-with-Spring
Безопасность
Приложение имело возможность работать под несколькими пользователями одновременно с одного браузера, GUID сессии добавлялся в URL, поэтому прямое обращение к xhtml файлу блокируется как не авторизованное (Форма логина была сделана без JSF), а вытащить его(или заставить JSF обработать запрос пользователя напрямую) используя запрос с GUID сессии вида:
localhost:8443/<имя приложения>/app/ea10efcb-4a2e-4eeb-aa71-24310882f7ad/jsf/accounts/find_create.xhtml
не получится, так как никакого файла [xxx.war]/app/ea10efcb-4a2e-4eeb-aa71-24310882f7ad/jsf/accounts/find_create.xhtml
Дополнительно были установлены следующие фильтры:
- "/javax.faces.resource/** " — пустой фильтр(нам не жалко, бери чего хочешь)
- /themes/** — пустой фильтр
- /**/resources/jsf/** — с проверкой авторизации
Заключение
Ну вот мы и рассмотрели основные моменты относительно безболезненного (для программы, для разработчика многие моменты были исключительно болезненны) добавления JSF в сложное приложение. За кадром остались вопросы обработки исключения при POST запросах, борьбу с хаотичными обращениями компонентов к бинам для оптимизации производительности, но это уже тема для отдельной статьи.
Автор: blaze79