Bisakah saya melakukan permintaan sinkron dengan voli?

133

Bayangkan saya berada di Layanan yang sudah memiliki utas latar belakang. Bisakah saya melakukan permintaan menggunakan voli di utas yang sama, sehingga panggilan balik terjadi secara serempak?

Ada 2 alasan untuk ini: - Pertama, saya tidak perlu utas lainnya dan akan sia-sia untuk membuatnya. - Kedua, jika saya menggunakan ServiceIntent, eksekusi utas akan selesai sebelum panggilan balik, dan karenanya saya tidak akan mendapat respons dari Volley. Saya tahu saya dapat membuat Layanan saya sendiri yang memiliki beberapa utas dengan runloop yang dapat saya kendalikan, tetapi akan sangat diinginkan memiliki fungsi ini dalam voli.

Terima kasih!

LocoMike
sumber
5
Pastikan untuk membaca jawaban @ Blundell dan juga jawaban yang sangat tervotasikan (dan sangat berguna).
Jedidja

Jawaban:

183

Sepertinya itu mungkin dengan RequestFuturekelas Volley . Misalnya, untuk membuat permintaan GET HTTP JSON sinkron, Anda dapat melakukan hal berikut:

RequestFuture<JSONObject> future = RequestFuture.newFuture();
JsonObjectRequest request = new JsonObjectRequest(URL, new JSONObject(), future, future);
requestQueue.add(request);

try {
  JSONObject response = future.get(); // this will block
} catch (InterruptedException e) {
  // exception handling
} catch (ExecutionException e) {
  // exception handling
}
Mat
sumber
5
@tasomaniac Diperbarui. Ini menggunakan JsonObjectRequest(String url, JSONObject jsonRequest, Listener<JSONObject> listener, ErrorListener errorlistener)konstruktor. RequestFuture<JSONObject>mengimplementasikan kedua Listener<JSONObject>dan ErrorListenerantarmuka, sehingga dapat digunakan sebagai dua parameter terakhir.
Matt
21
itu memblokir selamanya!
Mohammed Subhi Sheikh Quroush
9
Petunjuk: mungkin memblokir selamanya jika Anda memanggil future.get () SEBELUM Anda menambahkan permintaan ke antrian permintaan.
datayeah
3
itu akan diblokir selamanya karena Anda mungkin memiliki kesalahan koneksi membaca Jawaban Blundell
Mina Gabriel
4
Orang seharusnya mengatakan Anda tidak harus melakukan ini di utas utama. Itu tidak jelas bagi saya. Penyebab jika utas utama diblokir oleh future.get()maka aplikasi akan berhenti atau mengalami batas waktu pasti jika diatur demikian.
r00tandy
125

Catatan @Matthews jawaban benar TETAPI jika Anda berada di utas lainnya dan Anda melakukan panggilan voli ketika Anda tidak memiliki internet, panggilan balik kesalahan Anda akan dipanggil pada utas utama, tetapi utas yang Anda ikuti akan diblokir SELAMANYA. (Oleh karena itu jika utas itu adalah IntentService, Anda tidak akan pernah bisa mengirim pesan lain padanya dan layanan Anda pada dasarnya mati).

Gunakan versi get()yang memiliki batas waktufuture.get(30, TimeUnit.SECONDS) dan tangkap kesalahan untuk keluar dari utas Anda.

Untuk mencocokkan jawaban @Mathews:

        try {
            return future.get(30, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            // exception handling
        } catch (ExecutionException e) {
            // exception handling
        } catch (TimeoutException e) {
            // exception handling
        }

Di bawah ini saya membungkusnya dengan metode & menggunakan permintaan yang berbeda:

   /**
     * Runs a blocking Volley request
     *
     * @param method        get/put/post etc
     * @param url           endpoint
     * @param errorListener handles errors
     * @return the input stream result or exception: NOTE returns null once the onErrorResponse listener has been called
     */
    public InputStream runInputStreamRequest(int method, String url, Response.ErrorListener errorListener) {
        RequestFuture<InputStream> future = RequestFuture.newFuture();
        InputStreamRequest request = new InputStreamRequest(method, url, future, errorListener);
        getQueue().add(request);
        try {
            return future.get(REQUEST_TIMEOUT, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Log.e("Retrieve cards api call interrupted.", e);
            errorListener.onErrorResponse(new VolleyError(e));
        } catch (ExecutionException e) {
            Log.e("Retrieve cards api call failed.", e);
            errorListener.onErrorResponse(new VolleyError(e));
        } catch (TimeoutException e) {
            Log.e("Retrieve cards api call timed out.", e);
            errorListener.onErrorResponse(new VolleyError(e));
        }
        return null;
    }
Blundell
sumber
Itu poin yang cukup penting! Tidak yakin mengapa jawaban ini tidak mendapatkan lebih banyak suara.
Jedidja
1
Yang juga perlu diperhatikan adalah bahwa jika Anda meneruskan ExecutionException ke pendengar yang sama dengan yang Anda masukkan ke dalam permintaan, Anda akan memproses pengecualian dua kali. Pengecualian ini terjadi ketika pengecualian terjadi selama permintaan yang voli akan melewati ke errorListener untuk Anda.
Stimsoni
@ Blundell Saya tidak mengerti balasan Anda. Jika pendengar dieksekusi pada utas UI Anda memiliki utas latar belakang dalam menunggu dan utas UI yang memanggil notifyAll () jadi tidak masalah. Kebuntuan dapat terjadi jika pengiriman dilakukan pada utas yang sama dengan yang Anda blokir di masa depan (). Jadi respons Anda sepertinya tidak masuk akal.
greywolf82
1
@ greywolf82 IntentServiceadalah pelaksana kumpulan thread dari satu utas, oleh karena itu IntentService akan diblokir selamanya saat ia duduk dalam satu lingkaran
Blundell
@ Blundell saya tidak mengerti. Itu duduk sampai pemberitahuan akan dipanggil dan itu akan dipanggil dari utas UI. Sampai Anda memiliki dua utas yang berbeda, saya tidak dapat melihat jalan buntu
greywolf82
9

Mungkin disarankan untuk menggunakan Futures, tetapi jika karena alasan apa pun Anda tidak mau, alih-alih memasak blokir tersinkronisasi Anda sendiri, Anda harus menggunakan a java.util.concurrent.CountDownLatch. Jadi itu akan bekerja seperti ini ..

//I'm running this in an instrumentation test, in real life you'd ofc obtain the context differently...
final Context context = InstrumentationRegistry.getTargetContext();
final RequestQueue queue = Volley.newRequestQueue(context);
final CountDownLatch countDownLatch = new CountDownLatch(1);
final Object[] responseHolder = new Object[1];

final StringRequest stringRequest = new StringRequest(Request.Method.GET, "http://google.com", new Response.Listener<String>() {
    @Override
    public void onResponse(String response) {
        responseHolder[0] = response;
        countDownLatch.countDown();
    }
}, new Response.ErrorListener() {
    @Override
    public void onErrorResponse(VolleyError error) {
        responseHolder[0] = error;
        countDownLatch.countDown();
    }
});
queue.add(stringRequest);
try {
    countDownLatch.await();
} catch (InterruptedException e) {
    throw new RuntimeException(e);
}
if (responseHolder[0] instanceof VolleyError) {
    final VolleyError volleyError = (VolleyError) responseHolder[0];
    //TODO: Handle error...
} else {
    final String response = (String) responseHolder[0];
    //TODO: Handle response...
}

Karena orang-orang tampaknya benar-benar mencoba melakukan ini dan mengalami beberapa masalah, saya memutuskan untuk menyediakan contoh nyata "kehidupan nyata" yang digunakan ini. Ini dia https://github.com/timolehto/SynchronousVolleySample

Sekarang meskipun solusinya bekerja, ia memiliki beberapa keterbatasan. Yang terpenting, Anda tidak dapat menyebutnya di utas UI utama. Volley memang mengeksekusi permintaan di latar belakang, tetapi secara default Volley menggunakan utama Looperaplikasi untuk mengirim respons. Ini menyebabkan jalan buntu karena utas UI utama sedang menunggu respons, tetapi Loopermenunggu untuk onCreateselesai sebelum memproses pengiriman. Jika Anda benar-benar ingin melakukan ini, Anda bisa, alih-alih metode pembantu statis, instantiate Anda sendiri yang RequestQueuemelewatinya, Anda sendiri yang ExecutorDeliveryterkait dengan Handlerpenggunaan Looperyang diikat ke utas berbeda dari utas UI utama.

Timo
sumber
Solusi ini memblokir utas saya selamanya, mengubah Thread.sleep alih-alih countDownLatch dan masalah terpecahkan
snersesyan
Jika Anda dapat memberikan contoh lengkap kode yang gagal dengan cara ini, mungkin kami dapat mencari tahu apa masalahnya. Saya tidak melihat bagaimana tidur bersama dengan kait hitung mundur masuk akal.
Timo
Baiklah @VinojVetha Saya telah memperbarui jawabannya sedikit untuk mengklarifikasi situasi dan memberikan repo GitHub yang dapat dengan mudah Anda tiru dan coba kode dalam tindakan. Jika Anda memiliki lebih banyak masalah, berikan garpu repo sampel yang menunjukkan masalah Anda sebagai referensi.
Timo
Ini adalah solusi yang bagus untuk permintaan sinkron sejauh ini.
bikram
2

Sebagai pengamatan komplementer untuk jawaban @Blundells dan @Mathews, saya tidak yakin ada panggilan yang dikirim ke apa pun kecuali utas utama oleh Volley.

Sumber

Melihat RequestQueueimplementasi tampaknya RequestQueuemenggunakan NetworkDispatcheruntuk mengeksekusi permintaan dan ResponseDeliveryuntuk memberikan hasil (yang ResponseDeliverydisuntikkan ke dalam NetworkDispatcher). Pada ResponseDeliverygilirannya dibuat dengan Handlermenelurkan dari utas utama (suatu tempat sekitar baris 112 dalam RequestQueueimplementasi).

Di suatu tempat sekitar baris 135 di NetworkDispatcher implementasi sepertinya juga hasil yang berhasil disampaikan melalui sama ResponseDeliveryseperti kesalahan. Lagi; a ResponseDeliveryberdasarkan Handlermenelurkan dari utas utama.

Alasan

Untuk kasus penggunaan di mana permintaan harus dibuat dari IntentServiceadil untuk menganggap bahwa utas layanan harus diblokir sampai kami mendapat respons dari Volley (untuk menjamin ruang lingkup runtime hidup untuk menangani hasilnya).

Solusi yang disarankan

Salah satu pendekatan akan menggantikan cara default a RequestQueuedibuat , di mana konstruktor alternatif digunakan sebagai gantinya, menyuntikkan ResponseDeliveryyang memunculkan dari utas saat ini daripada utas utama. Saya belum menyelidiki implikasi dari ini.

dbm
sumber
1
Menerapkan implementasi custom ResponseDelivery rumit oleh kenyataan bahwa finish()metode di Requestkelas dan RequestQueuekelas adalah paket pribadi, selain dari menggunakan hack refleksi saya tidak yakin ada jalan keluarnya. Apa yang saya akhirnya lakukan untuk mencegah sesuatu berjalan pada utas utama (UI) adalah membuat utas alternatif Looper (menggunakan Looper.prepareLooper(); Looper.loop()) dan meneruskan sebuah ExecutorDeliveryinstance ke RequestQueuekonstruktor dengan handler untuk looper itu. Anda memiliki overhead dari looper lain tetapi tetap berada di utas utama
Stephen James Hand
1

Saya menggunakan kunci untuk mencapai efek itu sekarang saya bertanya-tanya apakah itu benar cara saya ada yang mau berkomentar?

// as a field of the class where i wan't to do the synchronous `volley` call   
Object mLock = new Object();


// need to have the error and success listeners notifyin
final boolean[] finished = {false};
            Response.Listener<ArrayList<Integer>> responseListener = new Response.Listener<ArrayList<Integer>>() {
                @Override
                public void onResponse(ArrayList<Integer> response) {
                    synchronized (mLock) {
                        System.out.println();
                        finished[0] = true;
                        mLock.notify();

                    }


                }
            };

            Response.ErrorListener errorListener = new Response.ErrorListener() {
                @Override
                public void onErrorResponse(VolleyError error) {
                    synchronized (mLock) {
                        System.out.println();
                        finished[0] = true;
                        System.out.println();
                        mLock.notify();
                    }
                }
            };

// after adding the Request to the volley queue
synchronized (mLock) {
            try {
                while(!finished[0]) {
                    mLock.wait();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
sebelumnya
sumber
Saya pikir Anda pada dasarnya menerapkan apa yang sudah disediakan Volley ketika Anda menggunakan "futures".
spaaarky21
1
Saya akan merekomendasikan memiliki bagian catch (InterruptedException e)dalam loop sementara. Kalau tidak, utas akan gagal menunggu jika terputus karena alasan tertentu
jayeffkay
@ jayeffkay saya sudah menangkap pengecualian jika ** InterruptedException ** terjadi di loop sementara tangkapan menangkapnya.
sampai
1

Saya ingin menambahkan sesuatu ke jawaban Matius yang diterima. SementaraRequestFuture sepertinya membuat panggilan sinkron dari utas yang Anda buat, itu tidak. Sebaliknya, panggilan dieksekusi di utas latar belakang.

Dari apa yang saya pahami setelah melalui perpustakaan, permintaan dalam RequestQueuedikirim dengan start()metode:

    public void start() {
        ....
        mCacheDispatcher = new CacheDispatcher(...);
        mCacheDispatcher.start();
        ....
           NetworkDispatcher networkDispatcher = new NetworkDispatcher(...);
           networkDispatcher.start();
        ....
    }

Sekarang keduanya CacheDispatcherdanNetworkDispatcher kelas memperpanjang utas. Begitu efektifnya sebuah utas pekerja baru ditelurkan untuk mengeluarkan antrian permintaan dan respons dikembalikan ke keberhasilan dan kesalahan pendengar yang diterapkan secara internal olehRequestFuture .

Meskipun tujuan kedua Anda tercapai tetapi tujuan pertama Anda bukan karena utas baru selalu muncul, tidak peduli dari utas mana Anda menjalankan RequestFuture .

Singkatnya, permintaan sinkron yang sebenarnya tidak dimungkinkan dengan pustaka Volley default. Koreksi saya jika saya salah.

Vignatus
sumber
0

Anda dapat melakukan permintaan sinkronisasi dengan volley tetapi Anda harus memanggil metode di utas yang berbeda atau aplikasi Anda yang berjalan akan diblokir, harus seperti ini:

public String syncCall(){

    String URL = "http://192.168.1.35:8092/rest";
    String response = new String();



    RequestQueue requestQueue = Volley.newRequestQueue(this.getContext());

    RequestFuture<JSONObject> future = RequestFuture.newFuture();
    JsonObjectRequest request = new JsonObjectRequest(Request.Method.GET, URL, new JSONObject(), future, future);
    requestQueue.add(request);

    try {
        response = future.get().toString();
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    } catch (JSONException e) {
        e.printStackTrace();
    }

    return response;


}

setelah itu Anda dapat memanggil metode di utas:

 Thread thread = new Thread(new Runnable() {
                                    @Override
                                    public void run() {

                                        String response = syncCall();

                                    }
                                });
                                thread.start();
Slimani Ibrahim
sumber