Czy mogę wykonać synchroniczne żądanie z volleyem?

134

Wyobraź sobie, że jestem w usłudze, która ma już wątek w tle. Czy mogę wykonać żądanie przy użyciu woleja w tym samym wątku, aby wywołania zwrotne były synchroniczne?

Są ku temu 2 powody: - Po pierwsze, nie potrzebuję kolejnego wątku, a tworzenie go byłoby marnotrawstwem. - Po drugie, jeśli jestem w ServiceIntent, wykonywanie wątku zakończy się przed wywołaniem zwrotnym i dlatego nie otrzymam odpowiedzi od Volley. Wiem, że mogę stworzyć własną usługę, która ma wątek z pętlą startową, którą mogę kontrolować, ale byłoby pożądane, aby ta funkcja była dostępna w trybie salwy.

Dziękuję Ci!

LocoMike
źródło
5
Koniecznie przeczytaj odpowiedź @ Blundell, a także wysoko ocenioną (i bardzo przydatną) odpowiedź.
Jedidja

Odpowiedzi:

184

Wygląda na to, że jest to możliwe z RequestFutureklasą Volleya. Na przykład, aby utworzyć synchroniczne żądanie JSON HTTP GET, możesz wykonać następujące czynności:

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
}
Matt
źródło
5
@tasomaniac Zaktualizowano. Używa JsonObjectRequest(String url, JSONObject jsonRequest, Listener<JSONObject> listener, ErrorListener errorlistener)konstruktora. RequestFuture<JSONObject>implementuje oba interfejsy Listener<JSONObject>i ErrorListener, więc może być używany jako ostatnie dwa parametry.
Matt
21
to blokuje na zawsze!
Mohammed Subhi Sheikh Quroush
9
Wskazówka: może to blokować na zawsze, jeśli wywołasz future.get () PRZED dodaniem żądania do kolejki żądań.
datayeah
3
Zostanie zablokowany na zawsze, ponieważ możesz mieć błąd połączenia. Przeczytaj odpowiedź Blundell
Mina Gabriel
4
Należy powiedzieć, że nie powinieneś tego robić w głównym wątku. Nie było to dla mnie jasne. Ponieważ jeśli główny wątek jest zablokowany przez, future.get()aplikacja zatrzyma się lub na pewno przekroczy limit czasu, jeśli tak jest ustawiony.
r00tandy
126

Uwaga Odpowiedź @Matthews jest prawidłowa, ALE jeśli jesteś w innym wątku i wykonujesz wolej, gdy nie masz internetu, twoje wywołanie zwrotne błędu zostanie wywołane w głównym wątku, ale wątek, w którym jesteś, zostanie ZAWSZE zablokowany.(Dlatego jeśli ten wątek jest IntentService, nigdy nie będziesz mógł wysłać do niego kolejnej wiadomości, a Twoja usługa będzie w zasadzie martwa).

Użyj wersji, get()która ma limit czasu future.get(30, TimeUnit.SECONDS)i złap błąd, aby wyjść z wątku.

Aby dopasować @Mathews odpowiedź:

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

Poniżej zawinąłem to w metodę i używam innego żądania:

   /**
     * 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
źródło
To dość ważna kwestia! Nie jestem pewien, dlaczego ta odpowiedź nie otrzymała więcej głosów pozytywnych.
Jedidja
1
Warto również zauważyć, że jeśli przekażesz wyjątek ExecutionException do tego samego odbiornika, który przekazałeś do żądania, wyjątek zostanie przetworzony dwukrotnie. Ten wyjątek występuje, gdy wystąpił wyjątek podczas żądania, które salwa przejdzie do modułu ErrorListener.
Stimsoni
@Blundell Nie rozumiem twojej odpowiedzi. Jeśli detektor jest wykonywany w wątku interfejsu użytkownika, masz wątek w tle w oczekiwaniu i wątek interfejsu użytkownika, który wywołuje notifyAll (), więc wszystko jest w porządku. Zakleszczenie może się zdarzyć, jeśli dostawa odbywa się w tym samym wątku, który został zablokowany przez przyszłą metodę get (). Twoja odpowiedź wydaje się więc bez sensu.
greywolf82
1
@ greywolf82 IntentServicejest wykonawcą puli wątków pojedynczego wątku, dlatego IntentService zostanie zablokowany na zawsze, ponieważ znajduje się w pętli
Blundell
@Blundell, nie rozumiem. Pozostaje do momentu wywołania powiadomienia i wywołania z wątku interfejsu użytkownika. Dopóki nie masz dwóch różnych wątków, nie widzę impasu
greywolf82
9

Prawdopodobnie zaleca się korzystanie z kontraktów futures, ale jeśli z jakiegoś powodu nie chcesz, zamiast gotować własne zsynchronizowane blokowanie, powinieneś użyć pliku java.util.concurrent.CountDownLatch. Więc to działałoby w ten sposób ...

//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...
}

Ponieważ ludzie rzeczywiście próbowali to zrobić i wpadli w kłopoty, zdecydowałem, że faktycznie dostarczę próbkę roboczą tego „z prawdziwego życia”. Tutaj jest https://github.com/timolehto/SynchronousVolleySample

Teraz, mimo że rozwiązanie działa, ma pewne ograniczenia. Co najważniejsze, nie możesz tego wywołać w głównym wątku interfejsu użytkownika. Volley wykonuje żądania w tle, ale domyślnie Volley używa głównej Looperczęści aplikacji do wysyłania odpowiedzi. Powoduje to zakleszczenie, ponieważ główny wątek interfejsu użytkownika oczekuje na odpowiedź, ale Looperczeka na onCreatezakończenie przed przetworzeniem dostawy. Jeśli naprawdę chcesz to zrobić, możesz zamiast statycznych metod pomocniczych utworzyć własne wystąpienie, RequestQueueprzekazując je własne, ExecutorDeliverypowiązane z Handlerużyciem a, Looperktóre jest powiązane z innym wątkiem z głównego wątku interfejsu użytkownika.

Timo
źródło
To rozwiązanie blokuje mój wątek na zawsze, zmieniło Thread.sleep zamiast countDownLatch i problem rozwiązany
snersesyan
Jeśli możesz dostarczyć pełną próbkę kodu, który uległ awarii w ten sposób, być może uda nam się ustalić, na czym polega problem. Nie widzę sensu spania w połączeniu z odliczaniem.
Timo,
W porządku @VinojVetha Zaktualizowałem nieco odpowiedź, aby wyjaśnić sytuację i zapewniłem repozytorium GitHub, które możesz łatwo sklonować i wypróbować kod w akcji. Jeśli masz więcej problemów, podaj widełki repozytorium próbek przedstawiające Twój problem jako odniesienie.
Timo,
Jak dotąd jest to świetne rozwiązanie dla żądań synchronicznych.
bikram
2

Jako obserwacja uzupełniająca do odpowiedzi zarówno @Blundells, jak i @Mathews, nie jestem pewien, czy jakikolwiek telefon zostanie dostarczony do niczego innego niż główny wątek przez Volley.

Źródło

Patrząc na RequestQueueimplementację , wydaje się, że RequestQueueużywa a NetworkDispatcherdo wykonania żądania i ResponseDeliverydo dostarczenia wyniku ( ResponseDeliveryjest wstrzykiwany do NetworkDispatcher). ResponseDeliveryJest z kolei utworzone z Handlerikry od głównego wątku (gdzieś w okolicach linii 112 w RequestQueuerealizacji).

Gdzieś w linii 135 w NetworkDispatcherimplementacji wydaje się, że również pomyślne wyniki są dostarczane przez to samo, ResponseDeliveryco wszelkie błędy. Jeszcze raz; a ResponseDeliveryoparty na Handlerspawnie z głównego wątku.

Racjonalne uzasadnienie

W przypadku użycia, w którym żądanie ma być wykonane z poziomu IntentService, można założyć, że wątek usługi powinien blokować się do czasu otrzymania odpowiedzi od Volley (aby zagwarantować żywy zakres środowiska wykonawczego do obsługi wyniku).

Sugerowane rozwiązania

Jednym podejściem byłoby zastąpić domyślny sposób, w jaki RequestQueuejest utworzony , gdy alternatywą konstruktora używany zamiast wstrzykiwania ResponseDelivery, które ikra z obecnym wątku niż nici głównej. Nie zbadałem jednak konsekwencji tego.

dbm
źródło
1
Implementacja niestandardowej implementacji ResponseDelivery jest skomplikowana przez fakt, że finish()metoda w Requestklasie i RequestQueueklasie są pakietami prywatnymi, poza używaniem hackowania refleksji Nie jestem pewien, jak to obejść. To, co ostatecznie zrobiłem, aby zapobiec działaniu wszystkiego w głównym wątku (UI), to skonfigurowanie alternatywnego wątku Looper (przy użyciu Looper.prepareLooper(); Looper.loop()) i przekazanie ExecutorDeliveryinstancji do RequestQueuekonstruktora z obsługą tego looper. Masz narzut innego looper, ale trzymaj się z dala od głównego wątku
Stephen James Hand
2

Chcę coś dodać do zaakceptowanej odpowiedzi Matthew. Chociaż RequestFuturemoże wydawać się, że wykonuje synchroniczne wywołanie z wątku, który utworzyłeś, tak nie jest. Zamiast tego wywołanie jest wykonywane w wątku w tle.

Z tego co rozumiem po przejściu przez bibliotekę, żądania w katalogu RequestQueuewysyłane są w jego start()sposób:

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

Teraz obie CacheDispatcheri NetworkDispatcherklasy rozszerzają wątek. W efekcie nowy wątek roboczy jest tworzony w celu dekolejkowania kolejki żądań, a odpowiedź jest zwracana do detektorów sukcesu i błędów zaimplementowanych wewnętrznie przez RequestFuture.

Chociaż twój drugi cel został osiągnięty, ale twój pierwszy cel nie jest, ponieważ zawsze pojawia się nowy wątek, bez względu na to, z którego wątku wykonujesz RequestFuture.

Krótko mówiąc, prawdziwe żądanie synchroniczne nie jest możliwe z domyślną biblioteką Volley. Popraw mnie, jeśli się mylę.

Vignatus
źródło
1

Używam zamka, aby osiągnąć ten efekt, teraz zastanawiam się, czy to poprawne w moim stylu, ktoś chce komentować?

// 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();
            }
        }
forcewill
źródło
Myślę, że zasadniczo wdrażasz to, co już zapewnia Volley, kiedy używasz „futures”.
spaaarky21
1
Poleciłbym mieć catch (InterruptedException e)wewnętrzną pętlę while. W przeciwnym razie wątek nie będzie czekał, jeśli z jakiegoś powodu zostanie przerwany
jayeffkay Kwietnia
@jayeffkay Już wychwytuję wyjątek, jeśli ** InterruptedException ** wystąpi w pętli while, którą catch obsługuje.
forcewill
0

Możesz wykonać żądanie synchronizacji za pomocą volleya, ale musisz wywołać metodę w innym wątku lub Twoja uruchomiona aplikacja zostanie zablokowana, powinno wyglądać tak:

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;


}

potem możesz wywołać metodę w wątku:

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

                                        String response = syncCall();

                                    }
                                });
                                thread.start();
Slimani Ibrahim
źródło
0

Osiągasz to z kotlin Coroutines

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7"
private suspend fun request(context: Context, link : String) : String{
   return suspendCancellableCoroutine { continuation ->
      val queue = Volley.newRequestQueue(context)
      val stringRequest = StringRequest(Request.Method.GET, link,
         { response ->
            continuation.resumeWith(Result.success(response))
         },
          {
            continuation.cancel(Exception("Volley Error"))
         })

      queue.add(stringRequest)
   }
}

I dzwoń z

CoroutineScope(Dispatchers.IO).launch {
    val response = request(CONTEXT, "https://www.google.com")
    withContext(Dispatchers.Main) {
       Toast.makeText(CONTEXT, response,Toast.LENGTH_SHORT).show()
   }
}
murgupluoglu
źródło