在这个系列的前几篇文章中,从最初简单的服务器环境搭建、MySQL数据库的安装、Servlet 的原理及使用、数据库的连接及CURD操作、Android和服务器GET/POST数据交互,到最后JSon格式报文的使用,我们已经将这个过程完整的走完一遍,但是其中用的代码都是片段式的,没有一个清晰的结构,甚至有些代码只是单纯地为了说明用法,还有一些朋友提出说代码中有一些自定义的方法没有说明,所以我们最后来一个总结篇,把之前的代码优化规整一下,顺便把之前的一些问题明确一下。
先从 Android 部分开始吧(注意:这里作为学习的目的,不使用第三方网络通信库,直接使用原生 API)——
之前的文章中说过,在 Android 中进行网络请求使用异步任务类 AsyncTask 比自己手动 new Thread() 要更便捷,这个我们在【一步一个脚印】Tomcat+MySQL为自己的APP打造服务器(3-1)Android 和 Service 的交互之GET方式最后也做过示例。但是如果要在项目中使用,明显不可能每次网络请求都写一个子类来继承 Asynctask,不然还要累死人,所以我们需要写个工具类专门来进行网络请求:
HttpPostTask.java:
/**
* 网络通信异步任务类
*
* @author WangJ
*/
public class HttpPostTask extends AsyncTask {
/** BaseActivity 中基础问题的处理 handler */
private Handler mHandler;
/** 返回信息处理回调接口 */
private ResponseHandler rHandler;
/** 请求类对象 */
private CommonRequest request;
public HttpPostTask(CommonRequest request,
Handler mHandler,
ResponseHandler rHandler) {
this.request = request;
this.mHandler = mHandler;
this.rHandler = rHandler;
}
@Override
protected String doInBackground(String... params) {
StringBuilder resultBuf = new StringBuilder();
try {
URL url = new URL(params[0]);
// 第一步:使用URL打开一个HttpURLConnection连接
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// 第二步:设置HttpURLConnection连接相关属性
connection.setRequestProperty("Content-Type", "application/json;charset=utf-8");
connection.setRequestMethod("POST"); // 设置请求方法,“POST或GET”
connection.setConnectTimeout(8000); // 设置连接建立的超时时间
connection.setReadTimeout(8000); // 设置网络报文收发超时时间
connection.setDoOutput(true);
connection.setDoInput(true);
// 如果是POST方法,需要在第3步获取输入流之前向连接写入POST参数
DataOutputStream out = new DataOutputStream(connection.getOutputStream());
out.writeBytes(request.getJsonStr());
out.flush();
// 第三步:打开连接输入流读取返回报文 -> *注意*在此步骤才真正开始网络请求
int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
// 通过连接的输入流获取下发报文,然后就是Java的流处理
InputStream in = connection.getInputStream();
BufferedReader read = new BufferedReader(new InputStreamReader(in));
String line;
while((line = read.readLine()) != null) {
resultBuf.append(line);
}
return resultBuf.toString();
} else {
// 异常情况,如404/500...
mHandler.obtainMessage(Constant.HANDLER_HTTP_RECEIVE_FAIL,
"[" + responseCode + "]" + connection.getResponseMessage()).sendToTarget();
}
} catch (IOException e) {
// 网络请求过程中发生IO异常
mHandler.obtainMessage(Constant.HANDLER_HTTP_SEND_FAIL,
e.getClass().getName() + " : " + e.getMessage()).sendToTarget();
}
return resultBuf.toString();
}
@Override
protected void onPostExecute(String result) {
if (rHandler != null) {
if (!"".equals(result)) {
/* 交易成功时需要在处理返回结果时手动关闭Loading对话框,可以灵活处理连续请求多个接口时Loading框不断弹出、关闭的情况 */
CommonResponse response = new CommonResponse(result);
// 这里response.getResCode()为多少表示业务完成也是和服务器约定好的
if ("0".equals(response.getResCode())) { // 正确
rHandler.success(response);
} else {
rHandler.fail(response.getResCode(), response.getResMsg());
}
}
}
}
}
上边代码中 HttpURLConnection 的用法之前已经用过几次了,没什么问题。但是会发现其中出现了几个新面孔,下面我们来说说为什么用这几个新面孔:
(1)CommonRequest类
为什么要引入这个类呢?一方面是为了更强健的功能,毕竟我们现在是用 Servlet 来作为服务器处理单元,但是实际上在项目中会使用Spring、Struts、Hibernate等框架,我们可以在请求中加入接口号来区分业务请求,而不仅仅是只上传一个请求参数的Map;另一方面,是为了代码的优化,更符合面向对象的程序设计,网络请求的输入就是一个请求CommonRequest对象,返回就是一个应答CommonResponse对象。下面来看看CommonRequest的代码:
/**
* 基本请求体封装类
* Created by WangJie on 2017-05-03.
*/
public class CommonRequest {
/**
* 请求码,类似于接口号(在本文中用Servlet做服务器时暂时用不到)
*/
private String requestCode;
/**
* 请求参数
* (说明:这里只用一个简单map类封装请求参数,对于请求报文需要上送一个数组的复杂情况需要自己再加一个ArrayList类型的成员变量来实现)
*/
private HashMap requestParam;
public CommonRequest() {
requestCode = "";
requestParam = new HashMap<>();
}
/**
* 设置请求代码,即接口号,在本例中暂时未用到
*/
public void setRequestCode(String requestCode) {
this.requestCode = requestCode;
}
/**
* 为请求报文设置参数
* @param paramKey 参数名
* @param paramValue 参数值
*/
public void addRequestParam(String paramKey, String paramValue) {
requestParam.put(paramKey, paramValue);
}
/**
* 将请求报文体组装成json形式的字符串,以便进行网络发送
* @return 请求报文的json字符串
*/
public String getJsonStr() {
// 由于Android源码自带的JSon功能不够强大(没有直接从Bean转到JSonObject的API),为了不引入第三方资源这里我们只能手动拼装一下啦
JSONObject object = new JSONObject();
JSONObject param = new JSONObject(requestParam);
try {
// 下边2个"requestCode"、"requestParam"是和服务器约定好的请求体字段名称,在本文接下来的服务端代码会说到
object.put("requestCode", requestCode);
object.put("requestParam", param);
} catch (JSONException e) {
LogUtil.logErr("请求报文组装异常:" + e.getMessage());
}
// 打印原始请求报文
LogUtil.logRequest(object.toString());
return object.toString();
}
}
其实就是一个Beans类,只是写了一个获取其JSon类型的方法,在发送请求时可以直接使用commonRequest.getJsonStr()来写入请求了。
(2)CommonResponse类
这个类和 CommonRequest 类的目的其实是一致的,用来封装应答报文,方便网络请求成功后的处理,直接看代码:
/**
* 常规返回报文格式化(如果有数组只能是单层数组,业务逻辑复杂时请服务端优化逻辑,或者分开请求不同的接口)
*
* @author WangJ 2016.06.02
*/
public class CommonResponse {
/**
* 交易状态代码
*/
private String resCode = "";
/**
* 交易失败说明
*/
private String resMsg = "";
/**
* 简单信息
*/
private HashMap propertyMap;
/**
* 列表类信息
*/
private ArrayList> mapList;
/**
* 通用报文返回构造函数
*
* @param responseString Json格式的返回字符串
*/
public CommonResponse(String responseString) {
// 日志输出原始应答报文
LogUtil.logResponse(responseString);
propertyMap = new HashMap<>();
mapList = new ArrayList<>();
try {
JSONObject root = new JSONObject(responseString);
/* 说明:
以下名称"resCode"、"resMsg"、"property"、"list"
和请求体中提到的字段名称一样,都是和服务器程序开发者约定好的字段名字,在本文接下来的服务端代码会说到
*/
resCode = root.getString("resCode");
resMsg = root.optString("resMsg");
JSONObject property = root.optJSONObject("property");
if (property != null) {
parseProperty(property, propertyMap);
}
JSONArray list = root.optJSONArray("list");
if (list != null) {
parseList(list);
}
} catch (JSONException e) {
e.printStackTrace();
}
}
/**
* 简单信息部分的解析到{@link CommonResponse#propertyMap}
*
* @param property 信息部分
* @param targetMap 解析后保存目标
*/
private void parseProperty(JSONObject property, HashMap targetMap) {
Iterator> it = property.keys();
while (it.hasNext()) {
String key = it.next().toString();
Object value = property.opt(key);
targetMap.put(key, value.toString());
}
}
/**
* 解析列表部分信息到{@link CommonResponse#mapList}
*
* @param list 列表信息部分
*/
private void parseList(JSONArray list) {
int i = 0;
while (i < list.length()) {
HashMap map = new HashMap<>();
try {
parseProperty(list.getJSONObject(i++), map);
} catch (JSONException e) {
e.printStackTrace();
}
mapList.add(map);
}
}
public String getResCode() {
return resCode;
}
public String getResMsg() {
return resMsg;
}
public HashMap getPropertyMap() {
return propertyMap;
}
public ArrayList> getDataList() {
return mapList;
}
}
(3)代码中出现了2个Handler
Handler 机制应该都知道,不说了。网络请求过程中会出现各种问题,比如网络不通、报文IO异常、404、500......等等,这是我们需要在UI上报错,最简单的做法就是在 BaseActivity 基类中处理这些状况(待会BaseActivity 中会说明);其实后边那个Handler根本不是Handler,只是一个接口,用于网络交互成功后回调进行业务处理,那看一下这个接口:
public interface ResponseHandler {
/**
* 交易成功的处理
* @param response 格式化报文
*/
void success(CommonResponse response);
/**
* 报文通信正常,但交易内容失败的处理
* @param failCode 返回的交易状态码
* @param failMsg 返回的交易失败说明
*/
void fail(String failCode, String failMsg);
}
(4)异步任务回调方法onPostExecute()中的处理
其实在之前的例子中我们已经知道:异步任务 AsyncTask 的回调 onPostExecute() 可以在UI线程中运行,可以在其中操作UI组件。但是我们这里发现并没有在 onPostExecute() 方法中操作,而是将报文封装成通用请求结果 CommonResponse 交给了 ResponseHandler 这个接口来处理,然后在具体的网络请求中来完成success()、fail()这两个方法。
好了,下来看BaseActivity的代码:
/**
* 基类
*
* Created by WangJie on 2017-03-14.
*/
public class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
protected void sendHttpPostRequest(String url, CommonRequest request, ResponseHandler responseHandler, boolean showLoadingDialog) {
new HttpPostTask(request, mHandler, responseHandler).execute(url);
if(showLoadingDialog) {
LoadingDialogUtil.showLoadingDialog(BaseActivity.this);
}
}
protected Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if(msg.what == Constant.HANDLER_HTTP_SEND_FAIL) {
LogUtil.logErr(msg.obj.toString());
LoadingDialogUtil.cancelLoading();
DialogUtil.showHintDialog(BaseActivity.this, "请求发送失败,请重试", true);
} else if (msg.what == Constant.HANDLER_HTTP_RECEIVE_FAIL) {
LogUtil.logErr(msg.obj.toString());
LoadingDialogUtil.cancelLoading();
DialogUtil.showHintDialog(BaseActivity.this, "请求接受失败,请重试", true);
}
}
};
}
此处只为完成我们的主题,需要创建一个处理网络请求中发生异常时发过来的异常处理Handler;创建一个子类Activity都可以使用的网络请求方法 sendHttpPostRequest(),当然方法设计各人见解不同,此处只做示例。
别的工具类代码就不占地了,有需要下源码看(郑重声明,工具类也是示例,效果不代表个人实力)。下边我们就写一个子类Activity看一下使用效果:
public class MainActivity extends BaseActivity {
private String URL_LOGIN = "http://169.254.170.29:8080/MyWorld_Service/LoginServlet";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final EditText etName = (EditText) findViewById(R.id.et_name);
final EditText etPassword = (EditText) findViewById(R.id.et_password);
Button btnLogin = (Button) findViewById(R.id.btn_login);
btnLogin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
login(etName.getText().toString(), etPassword.getText().toString());
}
});
}
private void login(String name, String password) {
final TextView tvRequest = (TextView) findViewById(R.id.tv_request);
final TextView tvResponse = (TextView) findViewById(R.id.tv_response);
final CommonRequest request = new CommonRequest();
request.addRequestParam("name", name);
request.addRequestParam("password", password);
sendHttpPostRequest(URL_LOGIN, request, new ResponseHandler() {
@Override
public void success(CommonResponse response) {
LoadingDialogUtil.cancelLoading();
tvRequest.setText(request.getJsonStr());
tvResponse.setText(response.getResCode() + "\n" + response.getResMsg());
DialogUtil.showHintDialog(MainActivity.this, "登陆成功啦!", false);
}
@Override
public void fail(String failCode, String failMsg) {
tvRequest.setText(request.getJsonStr());
tvResponse.setText(failCode + "\n" + failMsg);
DialogUtil.showHintDialog(MainActivity.this, true, "登陆失败", failCode + " : " + failMsg, "关闭对话框", new View.OnClickListener() {
@Override
public void onClick(View v) {
LoadingDialogUtil.cancelLoading();
DialogUtil.dismissDialog();
}
});
}
}, true);
}
}
看演示:
嗯,效果还可以看。但是如果你也用之前的 Servlet 来试还是会出问题的,报文可能没法正确解析,接下来我们看看服务端代码变动了哪块。
/**
* Servlet implementation class LoginServlet
*/
@WebServlet(description = "登录", urlPatterns = { "/LoginServlet" })
public class LoginServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
/**
* @see HttpServlet#HttpServlet()
*/
public LoginServlet() {
super();
}
/**
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse
* response)
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
System.out.println("不支持GET方法;");
}
/**
* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse
* response)
*/
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
BufferedReader read = request.getReader();
StringBuilder sb = new StringBuilder();
String line = null;
while ((line = read.readLine()) != null) {
sb.append(line);
}
String req = sb.toString();
System.out.println(req);
// 第一步:获取 客户端 发来的请求,恢复其Json格式——>需要客户端发请求时也封装成Json格式
JSONObject object = JSONObject.fromObject(req);
// requestCode暂时用不上
// 注意下边用到的2个字段名称requestCode、requestParam要和客户端CommonRequest封装时候的名字一致
String requestCode = object.getString("requestCode");
JSONObject requestParam = object.getJSONObject("requestParam");
// 第二步:将Json转化为别的数据结构方便使用或者直接使用(此处直接使用),进行业务处理,生成结果
// 拼接SQL查询语句
String sql = String.format("SELECT * FROM %s WHERE account='%s'",
DBNames.Table_Account,
requestParam.getString("name"));
System.out.println(sql);
// 自定义的结果信息类
CommonResponse res = new CommonResponse();
try {
ResultSet result = DatabaseUtil.query(sql); // 数据库查询操作
// result.getRow();
if (result.next()) {
if (result.getString("password").equals(requestParam.getString("password"))) {
res.setResult("0", "登陆成功");
res.getProperty().put("custId", result.getString("_id"));
} else {
res.setResult("100", "登录失败,登录密码错误");
}
} else {
res.setResult("200", "该登陆账号未注册");
}
} catch (SQLException e) {
res.setResult("300", "数据库查询错误");
e.printStackTrace();
}
// 第三步:将结果封装成Json格式准备返回给客户端,但实际网络传输时还是传输json的字符串
// 和我们之前的String例子一样,只是Json提供了特定的字符串拼接格式
// 因为服务端JSon是用到经典的第三方JSon包,功能强大,不用像Android中那样自己手动转,直接可以从Bean转到JSon格式
String resStr = JSONObject.fromObject(res).toString();
System.out.println(resStr);
response.getWriter().append(resStr).flush();
}
}
我们在代码中也使用了CommonResponse类,和客户端的非常像,只是由于服务端引用JSon包功能的强大,所以没有像客户端CommonRequest那样自己手动拼装JSon,而是直接用json的API转的,这时就需要CommonResponse的成员的名字和客户端拆解时的字段名一致:
public class CommonResponse {
private String resCode;
private String resMsg;
private HashMap property;
private ArrayList> list;
public CommonResponse() {
super();
resCode = "";
resMsg = "";
property = new HashMap();
list = new ArrayList>();
}
public void setResult(String resCode, String resMsg) {
this.resCode = resCode;
this.resMsg = resMsg;
}
public String getResCode() {
return resCode;
}
public void setResCode(String resCode) {
this.resCode = resCode;
}
public String getResMsg() {
return resMsg;
}
public void setResMsg(String resMsg) {
this.resMsg = resMsg;
}
public HashMap getProperty() {
return property;
}
public void addListItem(HashMap map) {
list.add(map);
}
public ArrayList> getList() {
return list;
}
}
可以发现Servlet代码中CommonRequest我并没有像Response一样处理,因为我懒,当然你处理一下更好,此处我只是抛砖引玉做个例子,不必细究(作为服务端的外行,代码优化什么的先放放哈)。
好了,就这么简单,当然在实际开发中可能遇到比较复杂的需求,可能代码要加入更复杂的控制,但是基本的逻辑就是这样的。需要注意的问题有这么几个:
(1)客户端和服务端约定报文字段的名字,不解释,我叫王三儿,你要喊我王麻子我肯定不答应;
(2)客户端和服务端代码中都有JSon的使用,但是看起来不大一样,因为我们使用的Json包不一样。JSon只是一个数据类型,只是一种手段不是目的,所以不用太纠结于这个,找对API就行了。
(3)为啥客户端移动端都要写 CommonRequest、CommonResponse这两个类嗫?因为它俩相当于入口和出口,门当户对嘛!客户端怎么封装的请求,到了服务端就要以同样的方法解开;同理,应答也是如此。
作为巩固,再来一个列表类的报文试试。先建这么一个表:
在Servlet中查询表中所有内容返回给客户端,ProductServlet.java:
@WebServlet("/ProductServlet")
public class ProductServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
/**
* @see HttpServlet#HttpServlet()
*/
public ProductServlet() {
super();
}
/**
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.getWriter().append("Served at: ").append(request.getContextPath());
}
/**
* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
*/
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
BufferedReader read = request.getReader();
StringBuilder sb = new StringBuilder();
String line = null;
while ((line = read.readLine()) != null) {
sb.append(line);
}
String req = sb.toString();
System.out.println(req);
String sql = String.format("SELECT * FROM %s",
DBNames.Table_Product);
System.out.println(sql);
// 自定义的结果信息类
CommonResponse res = new CommonResponse();
try {
ResultSet result = DatabaseUtil.query(sql); // 数据库查询操作
while (result.next()) {
HashMap map = new HashMap<>();
map.put("name", result.getString("name"));
map.put("describe", result.getString("describe"));
map.put("price", String.valueOf(result.getDouble("price")));
res.addListItem(map);
}
res.setResCode("0"); // 这个不能忘了,表示业务结果正确
} catch (SQLException e) {
res.setResult("300", "数据库查询错误");
e.printStackTrace();
}
String resStr = JSONObject.fromObject(res).toString();
response.getWriter().append(resStr).flush();
}
}
在Activity中请求的和报文返回后的处理:
public class ListActivity extends BaseActivity {
private String URL_PRODUCT = "http://169.254.170.29:8080/MyWorld_Service/ProductServlet";
ListView lvProduct;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_list);
lvProduct = (ListView) findViewById(R.id.lv);
getListData();
}
private void getListData() {
CommonRequest request = new CommonRequest();
sendHttpPostRequest(URL_PRODUCT, request, new ResponseHandler() {
@Override
public void success(CommonResponse response) {
LoadingDialogUtil.cancelLoading();
if (response.getDataList().size() > 0) {
ProductAdapter adapter = new ProductAdapter(ListActivity.this, response.getDataList());
lvProduct.setAdapter(adapter);
} else {
DialogUtil.showHintDialog(ListActivity.this, "列表数据为空", true);
}
}
@Override
public void fail(String failCode, String failMsg) {
LoadingDialogUtil.cancelLoading();
}
}, true);
}
static class ProductAdapter extends BaseAdapter {
private Context context;
private ArrayList> list;
public ProductAdapter(Context context, ArrayList> list) {
this.context = context;
this.list = list;
}
@Override
public int getCount() {
return list.size();
}
@Override
public Object getItem(int position) {
return list.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(R.layout.item_product, parent, false);
holder = new ViewHolder();
holder.tvName = (TextView) convertView.findViewById(R.id.tv_name);
holder.tvDescribe = (TextView) convertView.findViewById(R.id.tv_describe);
holder.tvPrice = (TextView) convertView.findViewById(R.id.tv_price);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
HashMap map = list.get(position);
holder.tvName.setText(map.get("name"));
holder.tvDescribe.setText(map.get("describe"));
holder.tvPrice.setText(map.get("price"));
return convertView;
}
private static class ViewHolder {
private TextView tvName;
private TextView tvDescribe;
private TextView tvPrice;
}
}
}
来来来,不多解释,就是取返回结果中的列表数据拿来放到 ListView 中,看效果:
好了,终于完了,应该没什么错吧,欢迎指正,先行谢过!
最后附上本文的代码,仅供参考,点击下载本文示例代码源码。