使用 Retrofit 库的 NullPointerException - Android
NullPointerException using Retrofit library - Android
(在问题末尾滚动查看最终解决方案)
使用 Retrofit Android 库。在通过 POST 成功调用“/login”方法后,我试图将 POST 请求推送到应该 return 3 个字段的 Web 服务器中。信息:
- 终点:http://example.us.es:3456
- 执行登录的服务器端方法:/login
- 服务器端方法的必需参数:"user" 和 "password"
- 服务器管理员允许的 HTTP 方法:POST
无论应用程序客户端输入 "user" 和 "password" 的值是什么,网络服务器都应该发送一个包含三个字段的 JSON对象:
- ok:值为 "true" 或 "false" 的字符串(false 表示凭据无效)。
- msg:如果出现错误(例如,无效凭据或数据库错误),将携带一条字符串消息
- 数据:另一个JSON对象,在此方法中包含一对name/value,id_session(包含会话标识符的字符串)。其他方法包含几个 name/value 对; login 方法不是这种情况。
根据这些信息,我做的第一件事是创建一个 POJO Java 方法,如下所示:
POJO 方法 (LoginInfo_POJO.java)
package com.example.joselopez.prueba1;
import java.util.List;
public class LoginInfo_POJO {
public String _ok;
public String _msg;
public Dataset _dataset;
class Dataset {
String _idsession;
}
}
我接下来要做的是创建一个包含登录方法的接口(我会在成功登录后在此处添加其他方法):
API 接口中的方法 (IApiMethods.java)
package com.example.joselopez.prueba1;
import retrofit.http.POST;
import retrofit.http.Query;
public interface IApiMethods {
// Log-in method
@FormUrlEncoded
@POST("/login")
LoginInfo_POJO logIn(@Query("user") String user,
@Query("password") String password);
}
快到了。现在我创建了一个 class,它从 AsyncTask 扩展,它将在一个单独的线程中执行网络操作。此 class 在 "MainActivity.java" 文件中。
主要 Activity (MainActivity.java)
...
...
private class BackgroundTask_LogIn extends AsyncTask<Void, Void, LoginInfo_POJO> {
RestAdapter restAdapter;
@Override
protected LoginInfo_POJO doInBackground(Void... params) {
IApiMethods methods = restAdapter.create(IApiMethods.class);
LoginInfo_POJO loginInfo = methods.logIn(mUser, mPassword);
return loginInfo;
}
@Override
protected void onPreExecute() {
restAdapter = new RestAdapter.Builder()
.setEndpoint("http://example.us.es:3456")
.build();
}
@Override
protected void onPostExecute(LoginInfo_POJO loginInfo_pojo) {
tv.setText("onPostExecute()");
tv.setText(loginInfo_pojo._ok + "\n\n");
tv.setText(tv.getText() + loginInfo_pojo._msg + "\n\n");
tv.setText(tv.getText() + loginInfo_pojo.data.id_sesion);
}
}
}
MainActivity 布局包含一个 TextView(位于 RelativeLayout 内),其 ID 为 "textView",并在代码中实例化为 "tv",您将在接下来看到。 MainActivity的完整代码是:
主要 Activity (MainActivity.java)
package com.example.joselopez.prueba1;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.TextView;
import retrofit.RestAdapter;
public class MainActivity extends AppCompatActivity {
TextView tv;
String mUser, mPassword;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv = (TextView) findViewById(R.id.textView);
mUser = "test_user";
mPassword = "test_password";
BackgroundTask_LogIn tryLogin = new BackgroundTask_LogIn();
tryLogin.execute();
}
private class BackgroundTask_LogIn extends AsyncTask<Void, Void, LoginInfo_POJO> { ... }
一切正常。但事实并非如此,经过一些调试我发现 class BackgroundTask_LogIn 中的 onPostExecute() 方法在行中停止:
for (LoginInfo_POJO.Dataset dataset : loginInfo_pojo._dataset) {
抛出的错误是:
com.example.joselopez.prueba1 E/AndroidRuntime﹕ FATAL EXCEPTION: main
java.lang.NullPointerException
所以我在这一行设置了一个断点,你猜怎么着?我的 LoginInfo_POJO 实例为其内部变量保存这些值:
- _ok = null
- _msg = null
- _dataset = null
这意味着我的变量不是从服务器响应中填充的,但是连接似乎是成功的,因为 doInBackground 方法完全运行并且正在调用 onPostExecute。
那你怎么看?也许我没有以正确的方式执行 POST 请求?
更新
正如@Gaëtan 所说,我在我的 POJO 中犯了一个大错误 class;那里的局部变量名称必须与结果 JSON 中的名称相等。我说过我期待来自 JSON 的字段 "ok"、"msg"、"data" 和 "id_session",但是我的 LoginInfo_POJO 中的局部变量有命名为“_ok”、“_msg”、“_dataset”、“_idsession”(注意前导下划线)。从 Retrofit 的角度来看这是一个巨大的错误,因此重写 POJO 方法来解决这个问题最终会解决问题。
一些关于如何使用 Retrofit 的信息:
- POJO 中的字段名称必须与 JSON 响应中的第一个匹配。在这里,您的字段被命名为
_ok
、_msg
和 _dataset
,而 JSON 响应包含 ok
、msg
和 data
。您在这里有两个选择:重命名字段以匹配 JSON 响应,或者在每个字段上使用 @SerializedName
注释来给出 JSON 字段的名称。
public class LoginInfo_POJO {
// If you rename the fields
public String ok;
public String msg;
public List<Dataset> data;
// If you use annotation
@SerializedName("ok")
public String _ok;
@SerializedName("msg")
public String _msg;
@SerializedName("data")
public String _dataset;
}
- Retrofit 提供了一种不使用
AsyncTask
的方法。不要为您的 API 方法(此处 LoginInfo_POJO logIn(String, String
)使用 return 类型,而是使用类型 Callback<LoginInfo_POJO>
的最后一个参数。请求将通过改造在后台线程上执行,当请求完成时(或出错时失败)将在主线程上调用回调。
有关详细信息,请参阅 documentation,在 SYNCHRONOUS VS 中。异步 VS。 OBSERVABLE 部分。
在 onPreExecute 中构建适配器时,您可以尝试将 logLevel 选项设置为 RestAdapter.LogLevel.FULL,以获得有关实际情况的更多信息。
来自Square Retrofit API Declaration:
If you need to take a closer look at the requests and responses you can easily add logging levels to the RestAdapter with the LogLevel property.
logLevel 设置为 FULL 的示例:
RestAdapter restAdapter = new RestAdapter.Builder()
.setLogLevel(RestAdapter.LogLevel.FULL)
.setEndpoint("https://api.github.com")
.build();
也就是说,您确实不需要带有 Retrofit 的 AsyncTask。
(在问题末尾滚动查看最终解决方案)
使用 Retrofit Android 库。在通过 POST 成功调用“/login”方法后,我试图将 POST 请求推送到应该 return 3 个字段的 Web 服务器中。信息:
- 终点:http://example.us.es:3456
- 执行登录的服务器端方法:/login
- 服务器端方法的必需参数:"user" 和 "password"
- 服务器管理员允许的 HTTP 方法:POST
无论应用程序客户端输入 "user" 和 "password" 的值是什么,网络服务器都应该发送一个包含三个字段的 JSON对象:
- ok:值为 "true" 或 "false" 的字符串(false 表示凭据无效)。
- msg:如果出现错误(例如,无效凭据或数据库错误),将携带一条字符串消息
- 数据:另一个JSON对象,在此方法中包含一对name/value,id_session(包含会话标识符的字符串)。其他方法包含几个 name/value 对; login 方法不是这种情况。
根据这些信息,我做的第一件事是创建一个 POJO Java 方法,如下所示:
POJO 方法 (LoginInfo_POJO.java)
package com.example.joselopez.prueba1;
import java.util.List;
public class LoginInfo_POJO {
public String _ok;
public String _msg;
public Dataset _dataset;
class Dataset {
String _idsession;
}
}
我接下来要做的是创建一个包含登录方法的接口(我会在成功登录后在此处添加其他方法):
API 接口中的方法 (IApiMethods.java)
package com.example.joselopez.prueba1;
import retrofit.http.POST;
import retrofit.http.Query;
public interface IApiMethods {
// Log-in method
@FormUrlEncoded
@POST("/login")
LoginInfo_POJO logIn(@Query("user") String user,
@Query("password") String password);
}
快到了。现在我创建了一个 class,它从 AsyncTask 扩展,它将在一个单独的线程中执行网络操作。此 class 在 "MainActivity.java" 文件中。
主要 Activity (MainActivity.java)
...
...
private class BackgroundTask_LogIn extends AsyncTask<Void, Void, LoginInfo_POJO> {
RestAdapter restAdapter;
@Override
protected LoginInfo_POJO doInBackground(Void... params) {
IApiMethods methods = restAdapter.create(IApiMethods.class);
LoginInfo_POJO loginInfo = methods.logIn(mUser, mPassword);
return loginInfo;
}
@Override
protected void onPreExecute() {
restAdapter = new RestAdapter.Builder()
.setEndpoint("http://example.us.es:3456")
.build();
}
@Override
protected void onPostExecute(LoginInfo_POJO loginInfo_pojo) {
tv.setText("onPostExecute()");
tv.setText(loginInfo_pojo._ok + "\n\n");
tv.setText(tv.getText() + loginInfo_pojo._msg + "\n\n");
tv.setText(tv.getText() + loginInfo_pojo.data.id_sesion);
}
}
}
MainActivity 布局包含一个 TextView(位于 RelativeLayout 内),其 ID 为 "textView",并在代码中实例化为 "tv",您将在接下来看到。 MainActivity的完整代码是:
主要 Activity (MainActivity.java)
package com.example.joselopez.prueba1;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.TextView;
import retrofit.RestAdapter;
public class MainActivity extends AppCompatActivity {
TextView tv;
String mUser, mPassword;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv = (TextView) findViewById(R.id.textView);
mUser = "test_user";
mPassword = "test_password";
BackgroundTask_LogIn tryLogin = new BackgroundTask_LogIn();
tryLogin.execute();
}
private class BackgroundTask_LogIn extends AsyncTask<Void, Void, LoginInfo_POJO> { ... }
一切正常。但事实并非如此,经过一些调试我发现 class BackgroundTask_LogIn 中的 onPostExecute() 方法在行中停止:
for (LoginInfo_POJO.Dataset dataset : loginInfo_pojo._dataset) {
抛出的错误是:
com.example.joselopez.prueba1 E/AndroidRuntime﹕ FATAL EXCEPTION: main java.lang.NullPointerException
所以我在这一行设置了一个断点,你猜怎么着?我的 LoginInfo_POJO 实例为其内部变量保存这些值:
- _ok = null
- _msg = null
- _dataset = null
这意味着我的变量不是从服务器响应中填充的,但是连接似乎是成功的,因为 doInBackground 方法完全运行并且正在调用 onPostExecute。
那你怎么看?也许我没有以正确的方式执行 POST 请求?
更新
正如@Gaëtan 所说,我在我的 POJO 中犯了一个大错误 class;那里的局部变量名称必须与结果 JSON 中的名称相等。我说过我期待来自 JSON 的字段 "ok"、"msg"、"data" 和 "id_session",但是我的 LoginInfo_POJO 中的局部变量有命名为“_ok”、“_msg”、“_dataset”、“_idsession”(注意前导下划线)。从 Retrofit 的角度来看这是一个巨大的错误,因此重写 POJO 方法来解决这个问题最终会解决问题。
一些关于如何使用 Retrofit 的信息:
- POJO 中的字段名称必须与 JSON 响应中的第一个匹配。在这里,您的字段被命名为
_ok
、_msg
和_dataset
,而 JSON 响应包含ok
、msg
和data
。您在这里有两个选择:重命名字段以匹配 JSON 响应,或者在每个字段上使用@SerializedName
注释来给出 JSON 字段的名称。
public class LoginInfo_POJO {
// If you rename the fields
public String ok;
public String msg;
public List<Dataset> data;
// If you use annotation
@SerializedName("ok")
public String _ok;
@SerializedName("msg")
public String _msg;
@SerializedName("data")
public String _dataset;
}
- Retrofit 提供了一种不使用
AsyncTask
的方法。不要为您的 API 方法(此处LoginInfo_POJO logIn(String, String
)使用 return 类型,而是使用类型Callback<LoginInfo_POJO>
的最后一个参数。请求将通过改造在后台线程上执行,当请求完成时(或出错时失败)将在主线程上调用回调。
有关详细信息,请参阅 documentation,在 SYNCHRONOUS VS 中。异步 VS。 OBSERVABLE 部分。
在 onPreExecute 中构建适配器时,您可以尝试将 logLevel 选项设置为 RestAdapter.LogLevel.FULL,以获得有关实际情况的更多信息。
来自Square Retrofit API Declaration:
If you need to take a closer look at the requests and responses you can easily add logging levels to the RestAdapter with the LogLevel property.
logLevel 设置为 FULL 的示例:
RestAdapter restAdapter = new RestAdapter.Builder()
.setLogLevel(RestAdapter.LogLevel.FULL)
.setEndpoint("https://api.github.com")
.build();
也就是说,您确实不需要带有 Retrofit 的 AsyncTask。