【一步一個腳印】Tomcat+MySQL為自己的APP打造伺服器(4)完結篇
在這個系列的前幾篇文章中,從最初簡單的伺服器環境搭建、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<String, String, String> { /** 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<String, String> 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<String, String> propertyMap;
/**
* 列表類資訊
*/
private ArrayList<HashMap<String, String>> 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<String, String> 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<String, String> 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<String, String> getPropertyMap() {
return propertyMap;
}
public ArrayList<HashMap<String, String>> 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<String, String> property;
private ArrayList<HashMap<String, String>> list;
public CommonResponse() {
super();
resCode = "";
resMsg = "";
property = new HashMap<String, String>();
list = new ArrayList<HashMap<String, String>>();
}
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<String, String> getProperty() {
return property;
}
public void addListItem(HashMap<String, String> map) {
list.add(map);
}
public ArrayList<HashMap<String, String>> 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<String, String> 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<HashMap<String, String>> list;
public ProductAdapter(Context context, ArrayList<HashMap<String, String>> 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<String, String> 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 中,看效果:
好了,終於完了,應該沒什麼錯吧,歡迎指正,先行謝過!