¿Por qué JSF llama a los captadores varias veces?

256

Digamos que especifico un componente outputText como este:

<h:outputText value="#{ManagedBean.someProperty}"/>

Si imprimo un mensaje de registro cuando somePropertyse llama al captador y se carga la página, es trivial notar que se llama al captador más de una vez por solicitud (dos o tres veces es lo que sucedió en mi caso):

DEBUG 2010-01-18 23:31:40,104 (ManagedBean.java:13) - Getting some property
DEBUG 2010-01-18 23:31:40,104 (ManagedBean.java:13) - Getting some property

Si el valor de somePropertyes costoso de calcular, esto puede ser un problema potencial.

Busqué en Google un poco y pensé que este es un problema conocido. Una solución alternativa era incluir un cheque y ver si ya se había calculado:

private String someProperty;

public String getSomeProperty() {
    if (this.someProperty == null) {
        this.someProperty = this.calculatePropertyValue();
    }
    return this.someProperty;
}

El principal problema con esto es que obtienes un montón de código repetitivo, sin mencionar las variables privadas que quizás no necesites.

¿Cuáles son las alternativas a este enfoque? ¿Hay alguna manera de lograr esto sin tanto código innecesario? ¿Hay alguna manera de evitar que JSF se comporte de esta manera?

¡Gracias por tu contribución!

Sevas
fuente

Respuestas:

340

Esto se debe a la naturaleza de las expresiones diferidas #{}(tenga en cuenta que las expresiones estándar "heredadas" se ${}comportan exactamente igual cuando se usan Facelets en lugar de JSP). La expresión diferida no se evalúa de inmediato , sino que se crea como un ValueExpressionobjeto y el método getter detrás de la expresión se ejecuta cada vez que se llama el código ValueExpression#getValue().

Esto normalmente se invocará una o dos veces por ciclo de solicitud-respuesta JSF, dependiendo de si el componente es un componente de entrada o salida ( aprende aquí ). Sin embargo, este recuento puede aumentar (mucho) más cuando se usa para iterar componentes JSF (como <h:dataTable>y <ui:repeat>), o aquí y allá en una expresión booleana como el renderedatributo. JSF (específicamente, EL) no almacenará en caché el resultado evaluado de la expresión EL ya que puede devolver diferentes valores en cada llamada (por ejemplo, cuando depende de la fila de tabla de datos iterada actualmente).

Evaluar una expresión EL e invocar un método getter es una operación muy barata, por lo que generalmente no debe preocuparse por esto en absoluto. Sin embargo, la historia cambia cuando se realiza una costosa lógica de DB / negocio en el método getter por alguna razón. ¡Esto se volvería a ejecutar cada vez!

Los métodos Getter en beans de respaldo JSF deben diseñarse de tal manera que solo devuelvan la propiedad ya preparada y nada más, exactamente según la especificación Javabeans . No deberían hacer ninguna lógica de DB / negocio costosa en absoluto. Para eso se @PostConstructdeben usar los métodos de escucha del bean y / o (acción). Se ejecutan solo una vez en algún momento del ciclo de vida JSF basado en solicitudes y eso es exactamente lo que desea.

Aquí hay un resumen de todas las formas correctas de preestablecer / cargar una propiedad.

public class Bean {

    private SomeObject someProperty;

    @PostConstruct
    public void init() {
        // In @PostConstruct (will be invoked immediately after construction and dependency/property injection).
        someProperty = loadSomeProperty();
    }

    public void onload() {
        // Or in GET action method (e.g. <f:viewAction action>).
        someProperty = loadSomeProperty();
    }           

    public void preRender(ComponentSystemEvent event) {
        // Or in some SystemEvent method (e.g. <f:event type="preRenderView">).
        someProperty = loadSomeProperty();
    }           

    public void change(ValueChangeEvent event) {
        // Or in some FacesEvent method (e.g. <h:inputXxx valueChangeListener>).
        someProperty = loadSomeProperty();
    }

    public void ajaxListener(AjaxBehaviorEvent event) {
        // Or in some BehaviorEvent method (e.g. <f:ajax listener>).
        someProperty = loadSomeProperty();
    }

    public void actionListener(ActionEvent event) {
        // Or in some ActionEvent method (e.g. <h:commandXxx actionListener>).
        someProperty = loadSomeProperty();
    }

    public String submit() {
        // Or in POST action method (e.g. <h:commandXxx action>).
        someProperty = loadSomeProperty();
        return "outcome";
    }

    public SomeObject getSomeProperty() {
        // Just keep getter untouched. It isn't intented to do business logic!
        return someProperty;
    }

}

Tenga en cuenta que usted debe no utilizar el bloque constructor o inicialización de frijol para el trabajo, ya que puede ser invocado varias veces si estás utilizando un marco de gestión de frijol que utiliza servidores proxy, como CDI.

Si realmente no hay otras formas para usted, debido a algunos requisitos de diseño restrictivos, entonces debe introducir una carga lenta dentro del método getter. Es decir, si la propiedad es null, entonces cárguela y asígnela a la propiedad, de lo contrario devuélvala.

    public SomeObject getSomeProperty() {
        // If there are really no other ways, introduce lazy loading.
        if (someProperty == null) {
            someProperty = loadSomeProperty();
        }

        return someProperty;
    }

De esta manera, la costosa lógica de DB / negocio no se ejecutará innecesariamente en cada llamada getter.

Ver también:

BalusC
fuente
55
Simplemente no use getters para hacer lógica de negocios. Eso es todo. Reorganice su lógica de código. Apuesto a que ya se solucionó simplemente usando el método de construcción, postconstrucción o acción de manera inteligente.
BalusC
3
-1, estoy en total desacuerdo. El objetivo de la especificación javaBeans es permitir que las propiedades sean más que un simple valor de campo, y las "propiedades derivadas" que se calculan sobre la marcha son perfectamente normales. Preocuparse por las llamadas getter redundantes no es más que una optimización prematura.
Michael Borgwardt
3
Espere si hacen más que devolver datos como usted mismo lo dijo claramente :)
BalusC
44
podría agregar que la inicialización diferida en getters sigue siendo válida en JSF :)
Bozho
2
@Harry: No cambiará el comportamiento. Sin embargo, puede manejar cualquier lógica de negocios en el captador condicionalmente mediante carga diferida y / o comprobando la ID de fase actual mediante FacesContext#getCurrentPhaseId().
BalusC
17

Con JSF 2.0 puede adjuntar un oyente a un evento del sistema

<h:outputText value="#{ManagedBean.someProperty}">
   <f:event type="preRenderView" listener="#{ManagedBean.loadSomeProperty}" />
</h:outputText>

Alternativamente, puede encerrar la página JSF en una f:viewetiqueta

<f:view>
   <f:event type="preRenderView" listener="#{ManagedBean.loadSomeProperty}" />

      .. jsf page here...

<f:view>
César Alforde
fuente
9

He escrito un artículo sobre cómo almacenar en caché el generador de beans JSF con Spring AOP.

Creo un simple MethodInterceptorque intercepta todos los métodos anotados con una anotación especial:

public class CacheAdvice implements MethodInterceptor {

private static Logger logger = LoggerFactory.getLogger(CacheAdvice.class);

@Autowired
private CacheService cacheService;

@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {

    String key = methodInvocation.getThis() + methodInvocation.getMethod().getName();

    String thread = Thread.currentThread().getName();

    Object cachedValue = cacheService.getData(thread , key);

    if (cachedValue == null){
        cachedValue = methodInvocation.proceed();
        cacheService.cacheData(thread , key , cachedValue);
        logger.debug("Cache miss " + thread + " " + key);
    }
    else{
        logger.debug("Cached hit " + thread + " " + key);
    }
    return cachedValue;
}


public CacheService getCacheService() {
    return cacheService;
}
public void setCacheService(CacheService cacheService) {
    this.cacheService = cacheService;
}

}

Este interceptor se usa en un archivo de configuración de resorte:

    <bean id="advisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
    <property name="pointcut">
        <bean class="org.springframework.aop.support.annotation.AnnotationMatchingPointcut">
            <constructor-arg index="0"  name="classAnnotationType" type="java.lang.Class">
                <null/>
            </constructor-arg>
            <constructor-arg index="1" value="com._4dconcept.docAdvance.jsfCache.annotation.Cacheable" name="methodAnnotationType" type="java.lang.Class"/>
        </bean>
    </property>
    <property name="advice">
        <bean class="com._4dconcept.docAdvance.jsfCache.CacheAdvice"/>
    </property>
</bean>

¡Espero que ayude!

Nicolas Labrot
fuente
6

Publicado originalmente en el foro PrimeFaces @ http://forum.primefaces.org/viewtopic.php?f=3&t=29546

Recientemente, he estado obsesionado evaluando el rendimiento de mi aplicación, ajustando consultas JPA, reemplazando consultas SQL dinámicas con consultas con nombre, y esta mañana, reconocí que un método getter era más un HOT SPOT en Java Visual VM que el resto de mi código (o la mayoría de mi código).

Método Getter:

PageNavigationController.getGmapsAutoComplete()

Referenciado por ui: incluir en index.xhtml

A continuación, verá que PageNavigationController.getGmapsAutoComplete () es un HOT SPOT (problema de rendimiento) en Java Visual VM. Si mira más abajo, en la captura de pantalla, verá que getLazyModel (), método de obtención de tabla de datos diferida de PrimeFaces, también es un punto caliente, solo cuando el usuario final está haciendo un montón de cosas / operaciones / tareas de 'tabla de datos diferida' en la aplicación :)

Java Visual VM: mostrando HOT SPOT

Ver el código (original) a continuación.

public Boolean getGmapsAutoComplete() {
    switch (page) {
        case "/orders/pf_Add.xhtml":
        case "/orders/pf_Edit.xhtml":
        case "/orders/pf_EditDriverVehicles.xhtml":
            gmapsAutoComplete = true;
            break;
        default:
            gmapsAutoComplete = false;
            break;
    }
    return gmapsAutoComplete;
}

Referenciado por lo siguiente en index.xhtml:

<h:head>
    <ui:include src="#{pageNavigationController.gmapsAutoComplete ? '/head_gmapsAutoComplete.xhtml' : (pageNavigationController.gmaps ? '/head_gmaps.xhtml' : '/head_default.xhtml')}"/>
</h:head>

Solución: dado que este es un método 'getter', mueva el código y asigne valor a gmapsAutoComplete antes de llamar al método; Ver el código a continuación.

/*
 * 2013-04-06 moved switch {...} to updateGmapsAutoComplete()
 *            because performance = 115ms (hot spot) while
 *            navigating through web app
 */
public Boolean getGmapsAutoComplete() {
    return gmapsAutoComplete;
}

/*
 * ALWAYS call this method after "page = ..."
 */
private void updateGmapsAutoComplete() {
    switch (page) {
        case "/orders/pf_Add.xhtml":
        case "/orders/pf_Edit.xhtml":
        case "/orders/pf_EditDriverVehicles.xhtml":
            gmapsAutoComplete = true;
            break;
        default:
            gmapsAutoComplete = false;
            break;
    }
}

Resultados de la prueba: PageNavigationController.getGmapsAutoComplete () ya no es un HOT SPOT en Java Visual VM (ya ni siquiera aparece)

Compartiendo este tema, ya que muchos de los usuarios expertos han aconsejado a los desarrolladores junior de JSF que NO agreguen código en los métodos 'getter'. :)

Howard
fuente
4

Si está utilizando CDI, puede utilizar los métodos de Productores. Se llamará muchas veces, pero el resultado de la primera llamada se almacena en caché en el alcance del bean y es eficiente para los captadores que están calculando o inicializando objetos pesados. Ver aquí , para más información.

Heidarzadeh
fuente
3

Probablemente podría usar AOP para crear algún tipo de Aspecto que almacenara en caché los resultados de nuestros captadores durante un período de tiempo configurable. Esto evitaría que necesite copiar y pegar código repetitivo en docenas de accesores.

mate b
fuente
¿Es este Spring AOP del que estás hablando? ¿Sabrías dónde puedo encontrar un fragmento de código o dos relacionados con Aspectos? Leer todo el capítulo 6 de la documentación de Spring parece excesivo ya que no estoy usando Spring;)
Sevas
-1

Si el valor de someProperty es costoso de calcular, esto puede ser un problema.

Esto es lo que llamamos una optimización prematura. En el raro caso de que un generador de perfiles le diga que el cálculo de una propiedad es tan extraordinariamente costoso que llamarlo tres veces en lugar de una vez tiene un impacto significativo en el rendimiento, agrega almacenamiento en caché como lo describe. Pero a menos que haga algo realmente estúpido como factorizar números primos o acceder a una base de datos en un captador, su código probablemente tenga una docena de ineficiencias peores en lugares en los que nunca pensó.

Michael Borgwardt
fuente
De ahí la pregunta: si someProperty corresponde a algo costoso de calcular (o como lo pones accediendo a una base de datos o factorizando primos), ¿cuál es la mejor manera de evitar hacer el cálculo varias veces por solicitud y es la solución que enumeré en la pregunta? el mejor. Si no está respondiendo la pregunta, los comentarios son un buen lugar para publicar, ¿no? Además, su publicación parece contradecir su comentario sobre la publicación de BalusC: en los comentarios usted dice que está bien hacer cálculos sobre la marcha, y en su publicación dice que es estúpido. ¿Puedo preguntarte dónde trazas la línea?
Sevas
Es una escala móvil, no un problema en blanco y negro. Algunas cosas claramente no son un problema, por ejemplo, agregar algunos valores, porque toman menos de una millonésima de segundo ( mucho menos, en realidad). Claramente, algunos son un problema, como el acceso a la base de datos o al archivo, porque pueden demorar 10 ms o más, y definitivamente debe conocerlos para evitarlos si es posible, no solo en los captadores. Pero para todo lo demás, la línea es donde el perfilador te dice.
Michael Borgwardt
-1

También recomendaría usar un Marco como Primefaces en lugar de JSF de stock, que abordan dichos problemas ante el equipo de JSF e. g en primefaces puede configurar el envío parcial. De lo contrario, BalusC lo ha explicado bien.

Martin Karari
fuente
-2

Sigue siendo un gran problema en JSF. Por ejemplo, si tiene un método isPermittedToBlaBlapara las comprobaciones de seguridad y, en su opinión, tiene rendered="#{bean.isPermittedToBlaBla}, el método se llamará varias veces.

El control de seguridad puede ser complicado, por ejemplo. Consulta LDAP, etc. Por lo tanto, debe evitar eso con

Boolean isAllowed = null ... if(isAllowed==null){...} return isAllowed?

y debe asegurarse dentro de un bean de sesión esto por solicitud.

Creo que JSF debe implementar aquí algunas extensiones para evitar múltiples llamadas (por ejemplo, la anotación llama a @Phase(RENDER_RESPONSE)este método solo una vez después de la RENDER_RESPONSEfase ...)

Morad
fuente
2
Puede almacenar en caché el resultado en RequestParameterMap
Christophe Roussy el