Можно ли сделать синхронный запрос с залпом?

133

Представьте, что я нахожусь в службе, у которой уже есть фоновый поток. Могу ли я сделать запрос, используя залп в том же потоке, чтобы обратные вызовы происходили синхронно?

Для этого есть 2 причины: - Во-первых, мне не нужен еще один поток, и создание его было бы пустой тратой. - Во-вторых, если я нахожусь в ServiceIntent, выполнение потока завершится до обратного вызова, и поэтому у меня не будет ответа от Volley. Я знаю, что могу создать свою собственную Службу, у которой есть поток с циклом выполнения, который я могу контролировать, но было бы желательно иметь эту функциональность в режиме залпа.

Спасибо!

LocoMike
источник
5
Обязательно прочитайте ответ @Blundell, а также высоко оцененный (и очень полезный) ответ.
Jedidja

Ответы:

183

Похоже, это возможно с RequestFutureклассом Волли . Например, чтобы создать синхронный запрос JSON HTTP GET, вы можете сделать следующее:

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
источник
5
@tasomaniac Обновлено. При этом используется JsonObjectRequest(String url, JSONObject jsonRequest, Listener<JSONObject> listener, ErrorListener errorlistener)конструктор. RequestFuture<JSONObject>реализует как Listener<JSONObject>и ErrorListenerинтерфейсы, поэтому он может быть использован в качестве последних двух параметров.
Мэтт
21
это блокировка навсегда!
Мохаммед Субхи Шейх Куруш
9
Подсказка: это может быть заблокировано навсегда, если вы вызовете future.get () ДО того, как добавить запрос в очередь запросов.
дата
3
он будет заблокирован навсегда, потому что у вас может быть ошибка соединения, прочитайте Ответ Бланделла
Мина Габриэль
4
Надо сказать, что вы не должны делать это в главном потоке. Мне это было непонятно. Причина, если основной поток заблокирован, future.get()тогда приложение остановится или запустится с тайм-аутом, если установлено так.
r00tandy
125

Обратите внимание, что ответ @Matthews правильный, НО если вы находитесь в другом потоке и выполняете залп, когда у вас нет Интернета, ваш обратный вызов ошибки будет вызываться в основном потоке, но поток, в котором вы находитесь, будет заблокирован НАВСЕГДА. (Поэтому, если этот поток является IntentService, вы никогда не сможете отправить ему другое сообщение, и ваш сервис будет в основном мертвым).

Используйте версию get()с тайм-аутом future.get(30, TimeUnit.SECONDS)и перехватите ошибку, чтобы выйти из потока.

Чтобы соответствовать ответу @Mathews:

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

Ниже я завернул его в метод и использовал другой запрос:

   /**
     * 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;
    }
Бландел
источник
Это довольно важный момент! Не уверен, почему этот ответ не получил больше голосов.
Jedidja
1
Также стоит отметить, что если вы передадите исключение ExecutionException тому же слушателю, который вы передали в запрос, вы будете обрабатывать исключение дважды. Это исключение возникает, когда во время запроса возникла исключительная ситуация, залп которой будет передан для errorListener для вас.
Стимсони
@Blundell Я не понимаю вашего ответа. Если слушатель выполняется в потоке пользовательского интерфейса, у вас есть ожидающий фоновый поток и поток пользовательского интерфейса, который вызывает notifyAll (), так что все в порядке. Может произойти взаимоблокировка, если доставка выполняется в том же потоке, который вы заблокировали в будущем get (). Так что ваш ответ кажется бессмысленным.
greywolf82
1
@ greywolf82 IntentService- исполнитель пула потоков для одного потока, поэтому IntentService будет заблокирован до тех пор, пока он находится в цикле,
Бланделл
@ Бланделл, я не понимаю. Он находится до тех пор, пока не будет вызвано уведомление, и оно будет вызвано из потока пользовательского интерфейса. Пока у вас не будет двух разных
веток,
9

Вероятно, рекомендуется использовать Futures, но если по какой-либо причине вы этого не хотите, вместо того, чтобы создавать свою собственную синхронизированную блокировку, вы должны использовать файл java.util.concurrent.CountDownLatch. Так что это будет работать так

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

Поскольку люди, казалось, действительно пытались это сделать и столкнулись с некоторыми проблемами, я решил, что на самом деле предоставлю "реальный" рабочий образец этого в использовании. Вот это https://github.com/timolehto/SynchronousVolleySample

Теперь, хотя решение работает, у него есть некоторые ограничения. Самое главное, вы не можете вызвать его в основном потоке пользовательского интерфейса. Volley действительно выполняет запросы в фоновом режиме, но по умолчанию Volley использует основную Looperчасть приложения для отправки ответов. Это вызывает взаимоблокировку, поскольку основной поток пользовательского интерфейса ожидает ответа, но Looperожидает onCreateзавершения перед обработкой доставки. Если вы действительно действительно хотите это сделать, вы можете вместо статических вспомогательных методов создать экземпляр своего собственного, RequestQueueпередавая его своим собственным ExecutorDeliveryпривязанным к Handlerusing a, Looperкоторый привязан к другому потоку из основного потока пользовательского интерфейса.

Timo
источник
Это решение навсегда блокирует мой поток, изменено Thread.sleep вместо countDownLatch, и проблема решена
snersesyan
Если вы можете предоставить полный образец кода, который не работает таким образом, возможно, мы сможем выяснить, в чем проблема. Я не понимаю, какой смысл спать в сочетании с защелками обратного отсчета.
Timo
Хорошо, @VinojVetha. Я немного обновил ответ, чтобы прояснить ситуацию, и предоставил репозиторий GitHub, который вы можете легко клонировать и опробовать код в действии. Если у вас есть другие проблемы, предоставьте для справки вилку образца репозитория, демонстрирующего вашу проблему.
Timo
Пока это отличное решение для синхронного запроса.
bikram
2

В качестве дополнительного наблюдения к ответам @Blundells и @Mathews я не уверен, что Volley доставит какой-либо вызов кому- либо, кроме основного потока.

Источник

Взглянув на RequestQueueреализацию, кажется, что RequestQueueон использует a NetworkDispatcherдля выполнения запроса и a ResponseDeliveryдля доставки результата ( ResponseDeliveryвводится в NetworkDispatcher). В ResponseDeliveryсвою очередь, создается с помощью Handlerпорождения из основного потока (где-то около строки 112 в RequestQueueреализации).

Где-то около строки 135 в NetworkDispatcherреализации кажется, что успешные результаты доставляются так же, ResponseDeliveryкак и любые ошибки. Очередной раз; a ResponseDeliveryоснован на Handlerпорождении из основного потока.

обоснование

Для случая использования, когда запрос должен быть сделан из IntentService, справедливо предположить, что поток службы должен блокироваться до тех пор, пока мы не получим ответ от Volley (чтобы гарантировать живую область выполнения для обработки результата).

Предлагаемые решения

Один из подходов будут переопределить путь по умолчанию RequestQueueсоздается , когда альтернативный конструктор используется вместо инжекции , ResponseDeliveryкоторый размножается от текущей нити , а не в главном потоке. Однако я не исследовал последствий этого.

дБмВт
источник
1
Реализация пользовательской реализации ResponseDelivery усложняется тем фактом, что finish()метод в Requestклассе и RequestQueueклассе являются частными пакетами, кроме использования метода взлома отражений. Я не уверен, что есть способ обойти это. То, что я в конечном итоге сделал, чтобы предотвратить что-либо, работающее в основном потоке (UI), это настройка альтернативного потока Looper (использование Looper.prepareLooper(); Looper.loop()) и передача ExecutorDeliveryэкземпляра RequestQueueконструктору с обработчиком для этого петлителя. У вас есть накладные расходы другого петлителя, но вы не
Стивен Джеймс Хэнд
1

Я использую блокировку для достижения этого эффекта, теперь мне интересно, правильно ли это, по моему мнению, кто-то хочет прокомментировать?

// 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
источник
Я думаю, что вы, по сути, реализуете то, что Volley уже предоставляет, когда вы используете «фьючерсы».
spaaarky21
1
Я бы рекомендовал иметь catch (InterruptedException e)внутри цикла while. В противном случае поток не сможет ждать, если по какой-то причине
прерван
@jayeffkay, я уже улавливаю исключение, если ** InterruptedException ** возникает в цикле while, который обрабатывает catch.
Forcewill
1

Я хочу добавить кое-что к принятому ответу Мэтью. Хотя RequestFutureможет показаться, что вы делаете синхронный вызов из созданного вами потока, это не так. Вместо этого вызов выполняется в фоновом потоке.

Из того, что я понимаю после прохождения библиотеки, запросы в RequestQueueотправляются в ее start()методе:

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

Теперь как CacheDispatcherи NetworkDispatcherклассы расширения потока. Таким образом, эффективно создается новый рабочий поток для удаления очереди запросов, а ответ возвращается в прослушиватели успеха и ошибок, реализованные внутри RequestFuture.

Хотя ваша вторая цель достигнута, но первая цель - нет, поскольку всегда создается новый поток, независимо от того, из какого потока вы выполняете RequestFuture.

Короче говоря, истинный синхронный запрос невозможен с библиотекой Volley по умолчанию. Поправь меня, если я ошибаюсь.

Vignatus
источник
0

Вы можете выполнить запрос синхронизации с залпом, но вы должны вызвать метод в другом потоке, иначе ваше работающее приложение заблокируется, это должно быть так:

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;


}

после этого вы можете вызвать метод в потоке:

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

                                        String response = syncCall();

                                    }
                                });
                                thread.start();
Слимани Ибрагим
источник