Mengapa JSF memanggil getter beberapa kali

256

Katakanlah saya menentukan komponen outputText seperti ini:

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

Jika saya mencetak pesan log ketika pengambil untuk someProperty dipanggil dan memuat halaman, itu sepele untuk melihat bahwa pengambil dipanggil lebih dari sekali per permintaan (dua kali atau tiga kali adalah apa yang terjadi dalam kasus saya):

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

Jika nilai someProperty mahal untuk dihitung, ini berpotensi menjadi masalah.

Saya sedikit Google dan menemukan ini adalah masalah yang diketahui. Salah satu solusinya adalah memasukkan cek dan melihat apakah sudah dihitung:

private String someProperty;

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

Masalah utama dengan ini adalah Anda mendapatkan banyak kode boilerplate, belum lagi variabel pribadi yang mungkin tidak Anda butuhkan.

Apa alternatif dari pendekatan ini? Apakah ada cara untuk mencapai ini tanpa begitu banyak kode yang tidak perlu? Apakah ada cara untuk menghentikan JSF dari berperilaku dengan cara ini?

Terima kasih atas masukan Anda!

Seva
sumber

Jawaban:

340

Ini disebabkan oleh sifat ekspresi yang ditangguhkan #{}(perhatikan bahwa ekspresi standar "lawas" ${}berperilaku sama persis ketika Facelet digunakan sebagai ganti JSP). Ekspresi yang ditangguhkan tidak segera dievaluasi, tetapi dibuat sebagai ValueExpressionobjek dan metode pengambil di belakang ekspresi dieksekusi setiap kali ketika kode panggilan ValueExpression#getValue().

Ini biasanya akan dipanggil satu atau dua kali per siklus permintaan-respons JSF, tergantung pada apakah komponen tersebut merupakan komponen input atau output ( pelajari di sini ). Namun, jumlah ini bisa naik (jauh) lebih tinggi ketika digunakan dalam iterasi komponen JSF (seperti <h:dataTable>dan <ui:repeat>), atau di sana-sini dalam ekspresi boolean seperti renderedatribut. JSF (khususnya, EL) tidak akan menembolok hasil yang dievaluasi dari ekspresi EL sama sekali karena dapat mengembalikan nilai yang berbeda pada setiap panggilan (misalnya, ketika itu tergantung pada baris dataat yang saat ini diulangi).

Mengevaluasi ekspresi EL dan menggunakan metode pengambil adalah operasi yang sangat murah, jadi Anda biasanya tidak perlu khawatir sama sekali. Namun, cerita berubah ketika Anda melakukan logika DB / bisnis mahal dalam metode pengambil karena beberapa alasan. Ini akan dieksekusi kembali setiap kali!

Metode getter dalam JSF backing beans harus dirancang sedemikian rupa sehingga mereka semata-mata mengembalikan properti yang sudah disiapkan dan tidak lebih, sesuai dengan spesifikasi orang Jawa . Mereka seharusnya tidak melakukan logika bisnis / DB yang mahal sama sekali. Untuk itu @PostConstructmetode pendengar kacang dan / atau (tindakan) harus digunakan. Mereka dieksekusi hanya sekali pada satu titik siklus hidup JSF berdasarkan permintaan dan itulah yang Anda inginkan.

Berikut adalah ringkasan dari semua berbeda hak cara untuk preset / memuat properti.

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;
    }

}

Perhatikan bahwa Anda tidak boleh menggunakan konstruktor kacang atau blok inisialisasi untuk pekerjaan itu karena dapat dipanggil beberapa kali jika Anda menggunakan kerangka kerja manajemen kacang yang menggunakan proxy, seperti CDI.

Jika ada untuk Anda benar-benar tidak ada cara lain, karena beberapa persyaratan desain yang membatasi, maka Anda harus memperkenalkan pemuatan malas di dalam metode pengambil. Yaitu jika properti itu null, lalu muat dan tetapkan ke properti, kalau tidak kembalikan.

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

        return someProperty;
    }

Dengan cara ini, DB / logika bisnis yang mahal tidak perlu dieksekusi pada setiap panggilan getter tunggal.

Lihat juga:

BalusC
sumber
5
Hanya saja, jangan menggunakan getter untuk melakukan logika bisnis. Itu saja. Atur ulang logika kode Anda. Taruhan saya itu sudah diperbaiki hanya dengan menggunakan konstruktor, postconstruct atau metode tindakan dengan cara yang cerdas.
BalusC
3
-1, sangat tidak setuju. Seluruh poin dari spesifikasi javaBeans adalah untuk memungkinkan properti menjadi lebih dari sekedar nilai bidang, dan "properti turunan" yang dihitung dengan cepat sangat normal. Khawatir tentang panggilan getter yang berlebihan tidak lain adalah optimasi prematur.
Michael Borgwardt
3
Berharap jika mereka melakukan lebih dari mengembalikan data seperti yang Anda jelaskan sendiri :)
BalusC
4
Anda dapat menambahkan bahwa inisialisasi malas dalam getter masih berlaku di JSF :)
Bozho
2
@ Harry: Itu tidak akan mengubah perilaku. Namun Anda dapat menangani logika bisnis apa pun dalam pengambil dengan syarat dengan pemuatan malas dan / atau dengan memeriksa ID fase saat ini FacesContext#getCurrentPhaseId().
BalusC
17

Dengan JSF 2.0 Anda dapat melampirkan pendengar ke acara sistem

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

Atau Anda dapat melampirkan halaman JSF dalam f:viewtag

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

      .. jsf page here...

<f:view>
César Alforde
sumber
9

Saya telah menulis artikel tentang cara men-cache pembuat getah JSF dengan Spring AOP.

Saya membuat sederhana MethodInterceptoryang memotong semua metode yang dijelaskan dengan penjelasan khusus:

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;
}

}

Pencegat ini digunakan dalam file konfigurasi pegas:

    <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>

Semoga ini bisa membantu!

Nicolas Labrot
sumber
6

Awalnya diposting di forum PrimeFaces @ http://forum.primefaces.org/viewtopic.php?f=3&t=29546

Baru-baru ini, saya terobsesi mengevaluasi kinerja aplikasi saya, menyetel kueri JPA, mengganti kueri SQL dinamis dengan kueri bernama, dan baru pagi ini, saya menyadari bahwa metode pengambil adalah lebih dari HOT SPOT di Java Visual VM daripada sisanya kode saya (atau sebagian besar kode saya).

Metode pengambil:

PageNavigationController.getGmapsAutoComplete()

Dirujuk oleh ui: sertakan di dalam index.xhtml

Di bawah ini, Anda akan melihat bahwa PageNavigationController.getGmapsAutoComplete () adalah HOT SPOT (masalah kinerja) di Java Visual VM. Jika Anda melihat lebih jauh ke bawah, pada tangkapan layar, Anda akan melihat bahwa getLazyModel (), metode pengambil datatable malas PrimeFaces, juga merupakan hot spot, hanya ketika pengguna akhir melakukan banyak jenis barang / operasi / tugas 'malas datatable' dalam aplikasi. :)

Java Visual VM: menampilkan HOT SPOT

Lihat kode (asli) di bawah ini.

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;
}

Dirujuk oleh yang berikut di index.xhtml:

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

Solusi: karena ini adalah metode 'pengambil', pindahkan kode dan berikan nilai ke gmapsAutoComplete sebelum metode dipanggil; lihat kode di bawah ini.

/*
 * 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;
    }
}

Hasil pengujian: PageNavigationController.getGmapsAutoComplete () bukan lagi HOT SPOT di Java Visual VM (bahkan tidak muncul lagi)

Berbagi topik ini, karena banyak pengguna ahli telah menyarankan pengembang JSF junior untuk TIDAK menambahkan kode dalam metode 'pengambil'. :)

Howard
sumber
4

Jika Anda menggunakan CDI, Anda dapat menggunakan metode Produsen. Ini akan dipanggil berkali-kali, tetapi hasil dari panggilan pertama di-cache dalam lingkup bean dan efisien untuk getter yang menghitung atau menginisialisasi objek berat! Lihat di sini , untuk info lebih lanjut.

Heidarzadeh
sumber
3

Anda mungkin dapat menggunakan AOP untuk membuat semacam Aspek yang menyimpan hasil getter kami untuk waktu yang dapat dikonfigurasi. Ini akan mencegah Anda dari perlu untuk menyalin dan menempelkan kode boilerplate di puluhan accessor.

matt b
sumber
Apakah ini AOP Musim Semi yang sedang Anda bicarakan? Apakah Anda tahu di mana saya dapat menemukan satu atau dua cuplikan kode yang berhubungan dengan Aspek? Membaca seluruh bab ke 6 dari dokumentasi Spring sepertinya berlebihan karena saya tidak menggunakan Spring;)
Sevas
-1

Jika nilai someProperty mahal untuk dihitung, ini berpotensi menjadi masalah.

Inilah yang kami sebut optimasi prematur. Dalam kasus yang jarang terjadi, seorang profiler memberi tahu Anda bahwa penghitungan properti sangat mahal sehingga menyebutnya tiga kali daripada sekali memiliki dampak kinerja yang signifikan, Anda menambahkan caching seperti yang Anda gambarkan. Tetapi kecuali jika Anda melakukan sesuatu yang benar-benar bodoh seperti memfaktorkan bilangan prima atau mengakses databse dalam pengambil, kode Anda kemungkinan besar memiliki selusin inefisiensi yang lebih buruk di tempat-tempat yang tidak pernah Anda pikirkan.

Michael Borgwardt
sumber
Oleh karena itu pertanyaan - jika beberapaProperti sesuai dengan sesuatu yang mahal untuk dihitung (atau seperti yang Anda akses mengakses basis data atau anjak piutang), apa cara terbaik untuk menghindari melakukan perhitungan beberapa kali per permintaan dan merupakan solusi yang saya cantumkan dalam pertanyaan yang terbaik. Jika Anda tidak menjawab pertanyaan, komentar adalah tempat yang bagus untuk memposting, bukan? Juga, posting Anda tampaknya bertentangan dengan komentar Anda pada posting BalusC - dalam komentar Anda mengatakan bahwa tidak apa-apa untuk melakukan perhitungan dengan cepat, dan dalam posting Anda Anda mengatakan bahwa itu bodoh. Bisakah saya bertanya di mana Anda menggambar garis?
Sevas
Ini skala geser, bukan masalah hitam-putih. Beberapa hal yang jelas bukan masalah, misalnya menambahkan beberapa nilai, karena mereka mengambil kurang dari sepersejuta detik ( lebih kurang, sebenarnya). Beberapa jelas merupakan masalah, seperti DB atau akses file, karena mereka dapat memakan waktu 10 ms atau lebih lama - dan Anda pasti perlu mengetahuinya sehingga Anda dapat menghindarinya jika memungkinkan, bukan hanya di getter. Tetapi untuk semua yang lain, barisnya adalah tempat profiler memberi tahu Anda.
Michael Borgwardt
-1

Saya juga akan menyarankan menggunakan Kerangka Kerja seperti Primefaces alih-alih JSF saham, mereka membahas masalah seperti itu sebelum tim JSF e. g di primefaces Anda dapat mengatur pengiriman parsial. Kalau tidak, BalusC telah menjelaskannya dengan baik.

Martin Karari
sumber
-2

Ini masih masalah besar di JSF. Misalnya, jika Anda memiliki metode isPermittedToBlaBlauntuk pemeriksaan keamanan dan menurut Anda, Anda punyarendered="#{bean.isPermittedToBlaBla} maka metode tersebut akan dipanggil beberapa kali.

Pemeriksaan keamanan bisa rumit misalnya. Permintaan LDAP dll. Jadi Anda harus menghindari itu dengan

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

dan Anda harus memastikan dalam sesi sesi ini per permintaan.

Saya pikir JSF harus menerapkan beberapa ekstensi di sini untuk menghindari beberapa panggilan (mis., Anasiasi @Phase(RENDER_RESPONSE)calle metode ini hanya sekali setelah RENDER_RESPONSEfase ...)

Morad
sumber
2
Anda bisa menyimpan hasilnya di RequestParameterMap
Christophe