Pull to refresh

Необычная интеграция JSF и Spring

Reading time6 min
Views16K

Введение


Хотел бы поделиться своим опытом необычной интеграции этих двух фреймворков. Мне очень бы не хотелось касаться таких исключительно важных вопросов «а зачем вообще нужна 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>


Оборачиваем запросы



До сих пор все достаточно просто и описано практически везде. Сейчас нам предстоить решить следующие задачи:

  1. обеспечить передачу get и post запросов через spring в JSF.
  2. добавить поддержку транзакций (у нас внутри Hibernate с lazy-коллекциями)
  3. создать бины с временем жизни «view»
  4. запретить доступ пользователю к 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]\resources\jsf\accounts\find_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 запросах, борьбу с хаотичными обращениями компонентов к бинам для оптимизации производительности, но это уже тема для отдельной статьи.
Tags:
Hubs:
Total votes 6: ↑5 and ↓1+4
Comments2

Articles