Как вернуть ответ вызывающей функции? (Android-приложение вызывает Java REST-сервер через JSON)

Я переписываю некоторые части приложения для Android, чтобы вызвать REST-сервер, который я написал на Java, с использованием Spring-Boot Framework.

Подводя итог, скажем, в приложении функция A вызывает функцию B в моем MainController (который должен управлять связью клиент-сервер). Вызовы функции B (с несколькими дополнительными шагами) вызывают мой сервер. Когда функция B выполняет вызов, она использует анонимный внутренний класс, переопределяющий метод, чтобы определить, как обрабатывать ответ. Я изо всех сил пытаюсь получить этот ответ от анонимного внутреннего класса к функции B, чтобы его можно было вернуть функции A. К сожалению, задействована некоторая асинхронность, просто чтобы сделать вещи более интересными.

Моя первая попытка состояла в том, чтобы использовать карту, которая хранит результат вызова с уникальным идентификатором, чтобы сделать его доступным за пределами внутреннего класса. Теоретически работает довольно хорошо, но рушится из-за асинхронности.

Во-вторых, я прямо добавил цикл, который должен ждать, пока результат будет помещен в карту, прежде чем продолжить, чтобы убедиться, что есть что вернуть.

Каким-то образом цикл не выполняется должным образом (я считаю), и когда я использую цикл, вызов моего сервера никогда не завершается (что он и делает, когда цикла нет)

См. следующий код

Клиент: MainController

class MainController
{
    private final AtomicLong counter;
    private final HashMap<Long, Object> callResults;

    public MainController(){
        counter = new AtomicLong();
        callResults  = new HashMap<>();
    }

    public Login getLoginByUsername(String username)
    {
        long callID = counter.incrementAndGet();
        System.out.println("Start call");
        ServerConnection.get("myURL",null, new JsonHttpResponseHandler(){
            @Override
            public void onSuccess(int statusCode, Header[] headers, JSONObject response){
                try {
                    System.out.println("Call success, enter result:");
                    callResults.put(callID, new CallResult(Login.parseFromJson(response)));
                    System.out.println(callResults.get(callID));

                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }
        });

        System.out.println("start waiting");
       while(callResults.get(callID) == null){
            System.out.print(".");
        }
        System.out.println("waiting over, return result:");
        System.out.println(callResults.get(callID));
        //return (Login) callResults.remove(callID).content;
        return null;
    }
}

Клиент: Серверное соединение

import com.loopj.android.http.*;
import cz.msebera.android.httpclient.HttpEntity;

public class ServerConnection {

    private static final String BASE_URL = "http://1.3.3.7:8080/";

    private static AsyncHttpClient client = new AsyncHttpClient();

    public static void get(String url, RequestParams params, JsonHttpResponseHandler responseHandler) {
        client.get(BASE_URL+url, params, responseHandler);
    }
}

Вот что я знаю:

  1. предполагаемый ответ придет в конце концов. Когда я не использую цикл для ожидания и просто возвращаю значение null, консоль будет отображать сообщения «Вызов выполнен успешно, введите результат:» и идентификатор объекта.

  2. При использовании цикла while ближе к концу он никогда не печатает "." - просто ничего не делает. Я знаю, что приложение все еще работает, потому что AndroidStudio постоянно сообщает мне о распределении пространства.

  3. Попытка применить шаблон наблюдателя может полностью выйти за рамки. Мне пришлось бы очень глубоко копаться в клиентском коде, который я бы предпочел не трогать. Честно говоря, с кодом, который мне предоставили, не так-то просто работать.

Я предполагаю, что это проблема: цикл предотвращает выполнение асинхронного REST-вызова, что делает невозможным его завершение.

Мой вопрос: A) Как я могу вернуть Login-Object, который я получил от сервера, в функцию, которая в первую очередь вызвала функцию MainController.getLoginByUsername?

Б) Что происходит с циклом while, который я пытался использовать для ожидания? Почему тело (print(".");) не будет выполнено и, что более важно, почему оно останавливает выполнение/завершение формы REST-вызова?

(также не стесняйтесь оставлять отзывы о моем стиле публикации, я впервые задаю вопрос здесь)

Изменить: добавлен вопрос B, может быть проще найти быстрое решение

Изменить в UPDATE: Извините, что не сделал это достаточно ясным: любое предложение, которое требует от меня адаптации вызывающей функции (например, шаблоны Observer-Patterns, такие как Livedata или Rxjava, или другие действительно хорошие предложения, предоставленные Лео Пелозо) невозможны. Я проверил, по крайней мере, предложение Лео Пелозоса, и базовый код просто не позволяет этого. Я теряю доступ к параметрам и больше не могу использовать возврат функции. Я знаю, что лучше всего было бы переработать базовую систему, но сейчас это просто выходит за рамки...


person Benjamin Batt    schedule 29.04.2019    source источник
comment
доступно много сетевых библиотек, попробуйте использовать одну из них. проверьте developer.android.com/training/volley   -  person karan    schedule 29.04.2019
comment
Спасибо, но, к сожалению, я не вижу, как другая сетевая библиотека решит эту проблему. Проблема больше связана с асинхронными вызовами функций и, следовательно, с многопоточностью. Само сетевое соединение работает нормально   -  person Benjamin Batt    schedule 29.04.2019
comment
Вы можете использовать интерфейс, livedata или rxjava.   -  person Leonardo Velozo    schedule 29.04.2019


Ответы (4)


Спасибо за вашу помощь! Я нашел решение, которое работает для моего случая.

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

public class ServerConnection {

    private static final String BASE_URL = "http://10.203.58.11:8080/";

    private static SyncHttpClient client = new SyncHttpClient();

    public static void get(String url, RequestParams params, JsonHttpResponseHandler responseHandler) {
        client.get(getAbsoluteUrl(url), params, responseHandler);
    }
}

Затем Android не позволит мне выполнять запросы в основном потоке. Это не было проблемой с AsyncHttpClient, так как асинхронная часть обрабатывается в собственном потоке. Поэтому мне пришлось добавить темы к моим запросам. Таким образом, мой MainController теперь выглядит примерно так:

public class MainController
{
    private final AtomicLong counter;
    private final HashMap<Long, CallResult> callResults;

    public MainController(){
        counter = new AtomicLong();
        callResults  = new HashMap<>();
    }

public Login getLoginByUsername(String username)
    {
        long callID = counter.incrementAndGet();
        new Thread(new Runnable() {
            @Override
            public void run() {
                ServerConnection.get("person-login-relations?byUsername="+username,null, new JsonHttpResponseHandler(){
                    @Override
                    public void onSuccess(int statusCode, Header[] headers, JSONObject response){
                        try {
                            callResults.put(callID, new CallResult(Login.parseFromJson(response)));
                            System.out.println(callResults.get(callID));

                        } catch (JSONException e) {
                            e.printStackTrace();
                        }
                    }
                });


            }
        })   .start();

        while(callResults.get(callID) == null){
            //wait
        }
        return (Login) callResults.remove(callID).content;
    }
}

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

По общему признанию, это может быть не идеальное решение, но это наиболее осуществимое, учитывая мой конкретный контекст.

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

person Benjamin Batt    schedule 01.05.2019

Вы использовали GSON от Google, он преобразует объект JSON в объект java, его синтаксис прост, и он вам понравится, я использую его все время. И я использую залп для ответа на HTTP-запрос. Просто используйте GSON, например

GSON gson = new GSON();
LoginObject login = new LoginObject();
login=gson.fromJson(responce,LoginObject.class);

и вуаля все готово.

person raj kavadia    schedule 29.04.2019
comment
Разбор - не моя проблема. У меня нет проблем с созданием действительного объекта входа в систему из JSON-String, который я получаю. Я не хочу показаться грубым, но я не понимаю, как GSON поможет мне получить любой объект из этого внутреннего класса в вызывающую функцию. - person Benjamin Batt; 29.04.2019

Если вы не хотите использовать Livedata или Rxjava, вы можете реорганизовать этот метод, чтобы принимать JsonHttpResponseHandler или пользовательский обратный вызов, это не красиво, но работает:

1.

public void getLoginByUsername(String username, JsonHttpResponseHandler callback)
{
    long callID = counter.incrementAndGet();
    System.out.println("Start call");
    ServerConnection.get("myURL",null, callback);

}

а затем вызывая это так:

getLoginByUsername("myUsername", new JsonHttpResponseHandler(){
        @Override
        public void onSuccess(int statusCode, Header[] headers, JSONObject response){
            try {
                Login myLogin = Login.parseFromJson(response);
                //do stuff

            } catch (JSONException e) {
                e.printStackTrace();
            }
        }
    });

or

2.

class MainController {
    private final AtomicLong counter;
    private final HashMap<Long, Object> callResults;

    interface OnLogin{
        void onSuccessLogin(Login login);
    }

    public MainController() {
        counter = new AtomicLong();
        callResults = new HashMap<>();
    }

    public void getLoginByUsername(String username, OnLogin callback)
    {

        ServerConnection.get("myURL",null, new JsonHttpResponseHandler(){
            @Override
            public void onSuccess(int statusCode, Header[] headers, JSONObject response){
                try {
                    Login loginObject = Login.parseFromJson(response);
                    callback.onSuccessLogin(loginObject);

                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }
        });

    }


}

и вы называете это так:

    MainController controller = new MainController();
    controller.getLoginByUsername("myUsername", new MainController.OnLogin() {
        @Override
        public void onSuccessLogin(Login login) {
            //do whatever here
        }
    });
person Leonardo Velozo    schedule 29.04.2019
comment
Я думаю, это сработает, но я все еще надеюсь на лучшее решение. Это просто пример — в реальном проекте около 50 методов с одной и той же проблемой, которые вызываются из нескольких точек внутри клиента. А клиент действительно плохо спроектирован и написан, поэтому я хочу по возможности не вмешиваться в него. Кроме того, это все еще не объясняет мне, почему цикл while, который я использую для ожидания, каким-то образом останавливает выполнение/завершение REST-вызова в первую очередь - person Benjamin Batt; 29.04.2019
comment
Я только что попробовал предложенное вами решение, и хотя оно работает для этого конкретного сценария, оно ломается сразу после него. Как и в случае с Observer-Solutions, которые вы предложили, это действительно хорошие идеи, которые просто не привязаны к тому неприятному фрагменту кода, с которым мне приходится работать... - person Benjamin Batt; 01.05.2019

Вы можете легко добиться этого, используя CompletableFuture<?>. Он работает аналогично Promise в JavaScript. Если вам требуется совместимость до уровня API 24, проверьте этот ответ здесь.

Таким образом, ваш getLoginByUsername будет выглядеть примерно так:

public CompletableFuture<Login> loginByUsername(String username) {
    CompletableFuture<Login> promise = new CompletableFuture<Login>();

    System.out.println("Start call");
    ServerConnection.get("myURL",null, new JsonHttpResponseHandler(){
        @Override
        public void onSuccess(int statusCode, Header[] headers, JSONObject response){
            try {
                System.out.println("Call success, enter result:");
                promise.complete(Login.parseFromJson(response));

            } catch (JSONException e) {
                promise.completeExceptionally(e);
            }
        }
    });

    return promise;
}

И ваш сайт вызова примерно такой:

Login login = loginByUsername("user").get();
// Or wait a maximum time
Login login = loginByUsername("user").get(30, TimeUnit.SECONDS);

Никогда не вызывайте get() в UIThread. Это заблокирует пользовательский интерфейс и создаст неудобства для пользователя

person JensV    schedule 01.05.2019