Контроллер вводится несколько раз при загрузке одной страницы

#jsf-2 #cdi #postconstruct

#jsf-2 #cdi #постконструкция

Вопрос:

Каждый раз, когда я перехожу на страницу или выполняю действие, мой CustomerController вводится несколько раз. Мой CustomerController ограничен областью запроса, поэтому я понимаю, что каждый запрос будет означать, что создается и вводится новый экземпляр, но то, что я вижу, кажется мне чрезмерным.

Например, после развертывания моего приложения (без ошибок) Я перехожу на index.xhtml страницу. Со index.xhtml страницы я нажимаю на ссылку «Добавить клиентов», которая ведет меня на customer.xhtml страницу. Это приводит к 16 инъекциям CustomerController к моменту customer.xhtml загрузки страницы! Строка

Вызывается CustomerController.init

(и связанный с ним вывод) появляется 16 раз. Поскольку init метод аннотируется @PostConstruct , он вызывается после ввода CustomerController.

Когда я пытаюсь сохранить нового клиента, я получаю 1 500 строк, добавленных в мой журнал из различных println вызовов, которые я ввожу для помощи в отладке. Однако большинство из них являются повторениями, потому что контроллер вводится так много раз.

Почему CustomerController вводится так много раз?

Что я заметил, так это то, что если я импортирую javax.enterprise.context.RequestScoped класс вместо javax.faces.bean.RequestScoped класса, то я получаю поведение одиночной инъекции, которое я ожидал. Я предполагаю, что это означает, что проблема связана с JSF и (что более вероятно) с моим использованием JSF, а не с тем, как я вводил компоненты в целом.

index.xhtml

 <html xmlns:ui="http://java.sun.com/jsf/facelets"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:f="http://java.sun.com/jsf/core">

<ui:composition template="templates/layout.xhtml">
    <ui:define name="content">
        <a href="customer.xhtml">Add Customer</a>
        <a href="customerList.xhtml">Customers</a>
    </ui:define>
</ui:composition>
</html>
 

customer.xhtml

 <html xmlns:ui="http://java.sun.com/jsf/facelets"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:f="http://java.sun.com/jsf/core">
<ui:composition template="templates/layout.xhtml">
    <ui:define name="content">
        <h3>
            <h:outputText value="Customer Details" />
        </h3>
        <br />
        <h:outputText value="* All fields are required" styleClass="errors" />
        <br />

        <h:form method="post" id="customerDetailsForm"
            binding="#{customerController.component}">

            <h:panelGrid columns="2" cellpadding="2" styleClass="frmTblSmall">
                <h:outputText value="ID" styleClass="fieldName" />


                <h:inputText id="username"
                    rendered="#{!customerController.editMode}"
                    value="#{customerController.customer.username}" required="true"
                    immediate="true" />

                <h:outputText rendered="#{customerController.editMode}"
                    value="#{customerController.customer.username}" />


                <h:outputText value="New Password" />
                <h:inputSecret id="password"
                    value="#{customerController.customer.password}" required="true"
                    immediate="true" />

                <h:outputLabel>
                </h:outputLabel>

                <h:outputText value="Status" />
                <h:selectOneRadio id="customerStatus"
                    value="#{customerController.customer.active}"
                    styleClass="textBox" required="true"
                    immediate="true">
                    <f:selectItem itemValue="#{true}" itemLabel="Active" />
                    <f:selectItem itemValue="#{false}" itemLabel="Inactive" />

                </h:selectOneRadio>

            </h:panelGrid>

            <h:panelGrid columns="1" styleClass="btnTblSmall">
                <h:inputHidden id="formMode" value="#{customerController.editMode}">
                </h:inputHidden>

                <h:inputHidden rendered="#{customerController.editMode}"
                    value="#{customerController.hiddenUsername}" id="hiddenUsername"/>


                    <h:commandButton name="saveBtn" value="Save" styleClass="btn"
                        action="#{customerController.save()}">

                    </h:commandButton>
            </h:panelGrid>

        </h:form>
    </ui:define>
</ui:composition>
</html>
 

CustomerController.java

 /**
 *
 */
package my.webapp.web.controller;

import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.annotation.PostConstruct;
import javax.ejb.EJB;
import javax.faces.bean.RequestScoped;
//import javax.enterprise.context.RequestScoped;
import javax.faces.context.FacesContext;
import javax.inject.Inject;
import javax.inject.Named;

import my.model.Customer;
import my.webapp.interfaces.DatabaseException;
import my.webapp.interfaces.ICustomerDao;

/**
 * This is the controller class that will control and handle all request for the
 * customer
 *
 * This bean is a request scoped bean which will be created when the JSF page is
 * loaded and is destroyed when the user navigates to another page.
 *
 * The bean has @Named annotation so that the bean properties can be used in the
 * UI
 *
 */
@Named(value = "customerController")
@RequestScoped
public class CustomerController extends FormRequestController {
    /**
     * This is the bean class for the controller to get the selected data.This
     * bean has a getter and setter method to assign value to the HTML data
     * table that is used to allow the user to set/get an entity object to
     * view/edit/delete
     */
    @Inject
    private HTMLDataTableActionBean htmlDataTableActionBean;

    /**
     * Data Access Object for customer
     */
    @EJB
    private ICustomerDao customerDao;

    @Inject
    private Customer customer;
    private String hiddenUsername;

    /**
     * This method is executed to perform any initialisation. This method is
     * called after dependency injection is done. This method must be invoked
     * before the class is put into service. The customer list is
     * created in post construction, otherwise JSF will retrieve an empty or a
     * completely different list while processing the form submit and thus won't
     * be able to locate the button pressed and won't invoke any action.
     * @throws DatabaseException
     */
    @PostConstruct
    public void init() throws DatabaseException {

        System.out.println("CustomerController.init called");
        System.out.println("editMode:" getEditMode());
        customerDao.setType(Customer.class);
        setEntityObjectList(findAll());
        printParamMap();
        if (null == customer)
        {
            System.out.println("customer is null");
            customer = new Customer();
            setEditMode(false);
        }
        else
        {
            System.out.println("customer.username: " customer.getUsername());
            System.out.println("customer.password: " customer.getPassword());
        }
        System.out.println("CustomerController.init finished");
    }

    /**
     *
     */
    private void printParamMap()
    {
        System.out.println("Print request parameters");
        Map<String, String> requestParameters = FacesContext.getCurrentInstance().getExternalContext().getRequestParameterMap();
        Set<Entry<String, String>> entries = requestParameters.entrySet();
        for(Entry<String, String> entry : entries)
        {
            System.out.println("Key: '" entry.getKey() "'nValue: '" entry.getValue() "'");
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see my.webapp.web.contoller.FormRequestController#processRequest()
     */
    public void processRequest(FormActionToPerform action) throws DatabaseException {

        System.out.println("CustomerController.processRequest with action: " action.toString());
        switch (action) {

        /*
         * The following steps should be performed to display the data table
         * view. fetch the customer list from the database. Always fetch
         * from the database so that new customers are in the list as
         * well
         */
        case SHOW_VIEW_FOR_LIST:
            setEntityObjectList(findAll());
            break;

        /*
         * Get object selected to edit/view/delete
         */
        case SHOW_EDIT_VIEW:
        case SHOW_VIEW_TO_VIEW_SELECTED_OBJECT:
        case SHOW_DELETE_VIEW:
            {
                Integer uniqueId = (Integer)getHtmlDataTableActionBean().getSelectedEntityObject().getUniqueId();
                System.out.println("Selected entity's unique ID: " uniqueId.toString());
                customer = customerDao.read(uniqueId);
                System.out.println("customer.username: " customer.getUsername());
            }
            break;
         default:
             System.out.println("No request processing performed");
             break;
        }
        System.out.println("CustomerController.processRequest finished");
    }

    /*
     * (non-Javadoc)
     *
     * @see
     * my.webapp.web.contoller.FormRequestController#doShowUIView(my.webapp
     * .web.contoller.FormRequestController.FormActionToPerform)
     */
    @Override
    String doShowUIView(FormActionToPerform action) {
        System.out.println("CustomerController.doShowUIView with action: " action.toString());
        String responseURL = HOME;
        switch (action) {
            case SHOW_EDIT_VIEW:
                setEditMode(true);
                setComponent(null);
                responseURL = URL_CUSTOMER;
                break;

            default:
                System.out.println("default condition");
                responseURL = HOME;
        }
        System.out.println("CustomerController.doShowUIView returning URL: " responseURL);
        return responseURL;
    }

    @Override
    public String save() {
        System.out.println("CustomerController.save called");
        System.out.println("editMode:" getEditMode());
        System.out.println("customer.username: " customer.getUsername());
        String URL = URL_CUSTOMER;

        String password = customer.getPassword();
        if (password != null)
        {
            System.out.println("saving new password");
            String pw_hash = password;
            customer.setPassword(pw_hash);
        }
        else
        {
            System.out.println("not saving new password");
        }

        try
        {
            customerDao.update(customer);
            URL = HOME;
        }
        catch (Exception e)
        {
            setErrorMessage(e.toString());
        }

        return URL;
    }

    @Override
    public String delete() {
        return HOME;
    }

    public List<?> findAll() throws DatabaseException {
        List<Customer> dataList = customerDao.findAll();
        Iterator<Customer> customerIterator = dataList.iterator();
        while(customerIterator.hasNext())
        {
            Customer customer = customerIterator.next();
            System.out.println("Found customer - id:" customer.getId() " - username: " customer.getUsername());
        }
        return dataList;

    }

    //Getters and Setters omitted
}
 

FormRequestController.java

 package my.webapp.web.controller;

import java.util.Iterator;
import java.util.List;

import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.model.SelectItem;

import my.webapp.interfaces.DatabaseException;
import my.webapp.web.utilities.NavigationConstants;

/**
 * This is a super class for all controllers and all the controllers should
 * extend this class. FormRequestController class is responsible for providing
 * methods to load UI and methods to handle events produced by HTML components
 * like on click of a button or a link.
 *
 *
 */
public abstract class FormRequestController implements NavigationConstants {

    /**
     * Error message to be displayed in the UI. If required, this is message
     * will be displayed after querying the database for list of any data
     * object. These Strings are designed to be used only by the Form Request
     * Controller. Hence it is defined here
     */
    protected final String NO_SERVICEPROVIDERS_AVAILABLE = "There are no service providers available. Please add a new service provider";

    protected final String NO_CUSTOMERS_AVAILABLE = "There are no customers available. Please add a new customer";

    /**
     * The following is the enumeration type to know the action to be performed
     * to process the request. This enumeration is designed to be used in the
     * process request method. This variable is specifically for form controller
     *
     *
     */
    protected enum FormActionToPerform {
        // To display UI to add a new entity object
        SHOW_ADD_VIEW,
        // To display UI to modify an existing entity object
        SHOW_EDIT_VIEW,
        // To display UI to delete an existing entity object
        SHOW_DELETE_VIEW,
        // To display UI to view the details of the existing object
        SHOW_VIEW_TO_VIEW_SELECTED_OBJECT,
        // To display UI to view the list of the existing objects either in a
        // table or in a list
        SHOW_VIEW_FOR_LIST;

    }

    protected FacesContext context;

    /**
     * This variable holds the list containing all the existing service
     * providers from the database to be represented in the form of a table for
     * the user to view/edit/delete. This variable should have a getter and
     * setter method so that it could be accessed in the UI
     */
    protected List<?> entityObjectList;

    private UIComponent component;

    /**
     * This variable is needed as the UI does not allow the user to edit certain
     * properties. A boolean is required in order to find out if the property
     * can be modified. The FormActionToPerform cannot be used as most of the
     * controllers are request scoped and are also called for adding events to
     * menu
     */
    protected boolean editMode;

    /**
     * This variable is used to hold the form name of the JSF form in which data
     * is handled so that error messages can be set only for that particular
     * form
     */
    protected String componentId = null;

    /*
     * -------------------------------------------------------------------------
     * The following methods starting with "showView" are the methods that would
     * be used to display forms or any details in the UI
     * -------------------------------------------------------------------------
     */

    /**
     * This method will be called from the UI on click of "menu link". This
     * method should load the data from the database (with respect to the
     * selection) in the form of HTML Data Table or List. Each row of data can
     * contain view, edit and delete buttons to allow the user to work on that
     * data
     * @throws DatabaseException
     *
     */
    public String showViewDataTable() throws DatabaseException {
        processRequest(FormActionToPerform.SHOW_VIEW_FOR_LIST);
        return doShowUIView(FormActionToPerform.SHOW_VIEW_FOR_LIST);
    }

    /**
     * This method will be called from the UI on click of "add new" link/button.
     * This method should create a new model object whose properties will be set
     * in the form. This method will then return the String of the XHTML page
     * that contains the form. The server then loads the form.
     *
     * @return String URL - If the data object is created successfully return
     *         form page else return error page
     * @throws DatabaseException
     */
    public String showViewToAdd() throws DatabaseException {
        processRequest(FormActionToPerform.SHOW_ADD_VIEW);
        return doShowUIView(FormActionToPerform.SHOW_ADD_VIEW);
    }

    /**
     * This method will be called from the UI on click of "edit" button, usually
     * from the data table. This method should get the selected object whose
     * properties will be updated in the form.
     *
     * @return String URL - XHTML page that contains the form with values to be
     *         edited. or error URL for a different UI or null to redisplay the
     *         form on failure
     * @throws DatabaseException
     *
     */
    public String showViewToEdit() throws DatabaseException {
        processRequest(FormActionToPerform.SHOW_EDIT_VIEW);
        return doShowUIView(FormActionToPerform.SHOW_EDIT_VIEW);
    }

    /**
     * This method will be called from the UI on click of "delete" button,
     * usually from the HTML data table. This method should get the selected
     * object which will be deleted/or made inactive in the database.
     *
     * @return String URL - If the data object is deleted successfully return
     *         success page else return error page
     * @throws DatabaseException
     *
     */
    public String showViewToDeleteDetails() throws DatabaseException {
        processRequest(FormActionToPerform.SHOW_DELETE_VIEW);
        return doShowUIView(FormActionToPerform.SHOW_DELETE_VIEW);
    }

    /**
     * This method will be called from the UI on click of "view" button, usually
     * from the data table. This method should get the selected object whose
     * properties will be viewed in the form.This method will return XHTML URL
     * as a string that will contain the form with values to be viewed. The
     * server then loads the form.The user will not be allowed to edit in this
     * form.
     *
     * @return String URL - If the data object is deleted successfully return
     *         success page else return error page
     * @throws DatabaseException
     *
     */
    public String showViewToViewDetails() throws DatabaseException {
        processRequest(FormActionToPerform.SHOW_VIEW_TO_VIEW_SELECTED_OBJECT);
        return doShowUIView(FormActionToPerform.SHOW_VIEW_TO_VIEW_SELECTED_OBJECT);
    }

    /**
     * This method should --not--- be called by the UI or by the sub class but
     * by the super class. This method is called usually to display a new form,
     * a form with edit to modify, a UI to view details
     *
     * @return
     */
    abstract String doShowUIView(FormActionToPerform action);

    /**
     * This method is usually called by the super class to process a request.
     * All necessary steps to process a request to create a new object for
     * adding new data, to get the selected object to edit/view/delete
     * @throws DatabaseException
     */
    abstract void processRequest(FormActionToPerform action) throws DatabaseException;

    /**
     * This method is usually called by the UI to bind the list of data in a
     * data table or a form or to bind value for a hidden value. All necessary
     * steps to bind data like setting the value should be done here
     */
    protected void bindData() {

    }

    /*
     * ----------------------------------------------------------------------
     * The following are methods that classes should call to save or update the
     * database through their service classes
     * ----------------------------------------------------------------------
     */

    /**
     * This method is called from the UI on click of the save button in the
     * form. This method should be called when a new data has to be saved/or an
     * existing data was modified and has to be updated in the DB This method
     * should in turn call the respective service method to save form data in
     * the database.
     *
     *
     * @return String - success URL on successful update in the DB An error URL
     *         for a different UI or null to redisplay the form is returned on
     *         failure
     */
    abstract String save();

    /**
     * This method is called from the UI on click of the delete button/ or a
     * confirmation button, usually from the data table. This method should get
     * the selected object whose data should be removed from the database. This
     * method should in turn call the respective service method to remove data.
     *
     * @return An error URL for a different UI or null to redisplay the form is
     *         returned by default. A success URL be returned on successful
     *         operation
     */
    abstract String delete();

    /*
     * ----------------------------------------------------------------------
     * The following methods starting with "find" are the methods the classes
     * should call to get data from the database through their service classes.
     * These method should be called to get list of data objects to be displayed
     * in the data table or to get a specific data object to be edited or
     * deleted. The methods are named starting with "find" rather than get, just
     * to avoid confusions with the bean get methods. All data objects extend
     * class Configuration Data
     * ----------------------------------------------------------------------
     */

    /**
     * This method is called to get a list of all data from the data base. For
     * example, the list will be a list of all customers or service providers.
     * This method will in turn call the findAll method of the services class
     * which will interact with the database
     *
     * @return List<ConfigurationData>
     * @throws DatabaseException
     */

    protected List<?> findAll() throws DatabaseException {
        return null;
    }

    /**
     * This method is called to get a list of all data from the data base. For
     * example, the list will be a list of all customers or service providers
     * but will be of type Select Item. This method is called when the UI
     * requires a single select combo box or a multiple select list box
     *
     *
     * @return List<SelectItem>
     */
    protected List<SelectItem> findAllSelectItems() {
        return null;
    }

    /**
     * This method is responsible to add error message to the current faces
     * context. The error message that is set here applies to the whole view or
     * UI and not specific to any fields. Any field specific validation messages
     * should be handled in the UI.
     *
     * @param message
     */
    public void setErrorMessage(String message) {
        String componentId = null;
        if (null != component) {
            componentId = component.getClientId();
        }

        if (null != componentId) {
            this.getContext().addMessage(componentId,
                    new FacesMessage(FacesMessage.SEVERITY_ERROR, "", message));
        }
    }

    public void resetErrorMessages() {
        Iterator<FacesMessage> errorMessages = this.getContext().getMessages();

        while (errorMessages.hasNext()) {
            errorMessages.next();
            errorMessages.remove();
        }
    }
    //getters and setters omitted
}
 

web.xml

 <?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
  <display-name>MyWebApp</display-name>
  <welcome-file-list>
    <welcome-file>index.html</welcome-file>
    <welcome-file>index.htm</welcome-file>
    <welcome-file>index.jsp</welcome-file>
    <welcome-file>default.html</welcome-file>
    <welcome-file>default.htm</welcome-file>
    <welcome-file>default.jsp</welcome-file>
  </welcome-file-list>
  <servlet>
    <servlet-name>Faces Servlet</servlet-name>
    <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>Faces Servlet</servlet-name>
    <url-pattern>/faces/*</url-pattern>
  </servlet-mapping>
  <context-param>
    <description>State saving method: 'client' or 'server' (=default). See JSF Specification 2.5.2</description>
    <param-name>javax.faces.STATE_SAVING_METHOD</param-name>
    <param-value>client</param-value>
  </context-param>
  <context-param>
    <param-name>javax.servlet.jsp.jstl.fmt.localizationContext</param-name>
    <param-value>resources.application</param-value>
  </context-param>
  <context-param>
    <param-name>javax.faces.FACELETS_LIBRARIES</param-name>
    <param-value>/WEB-INF/balusc.taglib.xml</param-value>
  </context-param>
  <context-param>
    <param-name>javax.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL</param-name>
    <param-value>true</param-value>
  </context-param>
  <filter>
    <filter-name>PrimeFaces FileUpload Filter</filter-name>
    <filter-class>org.primefaces.webapp.filter.FileUploadFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>PrimeFaces FileUpload Filter</filter-name>
    <servlet-name>Faces Servlet</servlet-name>
  </filter-mapping>
  <context-param>
    <param-name>javax.faces.PARTIAL_STATE_SAVING</param-name>
    <param-value>false</param-value>
  </context-param>
  <listener>
    <listener-class>com.sun.faces.config.ConfigureListener</listener-class>
  </listener>
</web-app>
 

faces-config.xml

 <?xml version="1.0" encoding="UTF-8"?>

<faces-config xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd"
    version="2.0">
</faces-config>
 

Ответ №1:

Я думаю, что проблема в javax.faces.bean.RequestScoped аннотации. Этот распознается только JSF, но ваш компонент, похоже, управляется CDI. Без аннотации конкретной области компоненты CDI находятся в зависимой области. Насколько я знаю, если компонент находится в зависимой области видимости, для каждого выражения EL на странице JSF создается новый экземпляр. Есть ли у вас 16 EL-выражений, в которых компонент используется на вашей странице?

Решение должно быть простым: используйте javax.enterprise.context.RequestScoped вместо этого.

Комментарии:

1. У меня была такая же идея (по одному экземпляру на выражение EL), но по моим подсчетам таких выражений не 16. Я думал, что, поскольку я использую javax.faces.bean.RequestScoped аннотацию, она будет управляться JSF. Что мне следует изменить, чтобы компонент управлялся JSF (т. Е. Не могли бы вы уточнить)? Использование javax.enterprise.context.RequestScoped работает, но я хочу знать, почему аннотация JSF не работает и как заставить ее работать.

2. Вам просто нужно заменить @Named на @javax.faces.bean.ManagedBean , чтобы компонент управлялся JSF. В этом случае компонент по-прежнему также является компонентом CDI, но с удалением @Named компонента CDI больше не доступен через EL, а компонент JSF разрешается в выражениях EL (компоненты CDI с тем же именем всегда разрешаются первыми). Но если у вас нет веских причин использовать компоненты JSF, CDI в любом случае может быть лучшим выбором.

3. Этот ответ не является окончательным ответом, но пока он лучший, и переход с JSF ManagedBeans на CDI сработал.