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

в 10:06, , рубрики: java, jsf, Spring MVC, Веб-разработка, метки: , ,

Введение

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

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js