user3337410
user3337410

Reputation: 94

Controller injected multiple times during single page load

Each time I navigate to a page or perform an action, my CustomerController is injected multiple times. My CustomerController is Request Scoped so I understand that each request will mean a new instance is created and injected but what I'm seeing seems excessive to me.

For example, after I deploy my application (without errors) I navigate to the index.xhtml page. From the index.xhtml page I click on the 'Add Customers' link which takes me to the customer.xhtml page. This results in 16 CustomerController injections by the time the customer.xhtml page has loaded! The line

CustomerController.init called

(and associated output) appears 16 times. Since the init method is annotated with @PostConstruct it is called after the CustomerController is injected.

When I try to save a new customer I get 1 500 lines added to my log from the various println calls I put in to help debug. Most of them however are repetitions because the controller is injected so many times.

Why is the CustomerController injected so many times?

Something I've noticed is that if I import the javax.enterprise.context.RequestScoped class instead of the javax.faces.bean.RequestScoped class then I get the single injection behaviour I'd expect. I guess this means the issue lies with JSF and (more likely) my use of JSF and not with the way I inject beans in general.

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>

Upvotes: 1

Views: 1909

Answers (1)

Michi
Michi

Reputation: 1595

I think the problem is the javax.faces.bean.RequestScoped annotation. This one is only recognized by JSF but your bean seems to be managed by CDI. Without a specific scope annotation CDI beans are in dependent scope. As far as I know, if a bean is in dependent scope, a new instance is created for every EL expression in a JSF page. Do you have 16 EL expressions where the bean is used in your page?

The solution should be simple: use javax.enterprise.context.RequestScoped instead.

Upvotes: 2

Related Questions