【一步一个脚印】Tomcat+MySQL为自己的APP打造服务器(4)完结篇

版权声明:^_^ 尊重原创,共享知识,转载请注明"衷水木"http://blog.csdn.net/a_running_wolf https://blog.csdn.net/a_running_wolf/article/details/70828280

        在这个系列的前几篇文章中,从最初简单的服务器环境搭建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 中,看效果:

        列表示例

        好了,终于完了,应该没什么错吧,欢迎指正,先行谢过!

        最后附上本文的代码,仅供参考,点击下载本文示例代码源码

猜你喜欢

转载自blog.csdn.net/a_running_wolf/article/details/70828280