1、概述
封装一个简易的HttpURLConnection简易框架,内部通过线程池来进行网络请求。同时实现了请求返回数据的缓存和对Cookie的处理。具体代码地址如下:网络框架代码。
2、使用方法
① 导入demo中的httplib这个moudle。
② 配置url,在httplib这个moudle下面的res/xml/url.xml中配置需要调用的url及相关参数,如下所示:
配置url其中Key是查找这个url的关键字;Expires 是get请求回调内容缓存的时间,单位为秒;NetType是请求方法,目前支持GET和POST;Url是请求的url。
③ 需要调用该框架的Activity需要继承BaseActivity这个类。如果不想继承的话,请在onStop()中实现BaseActivity中所实现的内容即在Activity销毁时取消所有请求。
④ 实现RequestCallback.class回调类,其中onSuccess(String content)方法是请求成功的回调,参数是请求成功的数据。onFail(String content)方法是请求失败时回调,参数是失败原因。这两个方法都在主线程中执行,可进行UI操作,但不要做耗时操作。
⑤ 调用getMyHttpClient()方法获得MyHttpClient.class类。通过该类的public void invokeGet(String urlKey, RequestCallback callback) 方法调用http请求,其中urlKey需要传入的是第②步中配置里的Key,callback是第④步中的RequestCallback对象。
public class MainActivity extends BaseActivity {
private TextView textView;
private TextView textView2;
private Button refresh;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = findViewById(R.id.text);
textView2 = findViewById(R.id.text2);
refresh = findViewById(R.id.refresh);
getMyHttpClient().invokeGet("getWeatherInfo", new RequestCallback() {
@Override
public void onSuccess(String content) {
textView.setText(content);
}
@Override
public void onFail(String content) {
textView.setText(content);
}
});
refresh.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
getMyHttpClient().invoke("getWeatherInfo", null, new RequestCallback() {
@Override
public void onSuccess(String content) {
textView2.setText(content);
}
@Override
public void onFail(String errorMessage) {
textView2.setText(errorMessage);
}
});
}
});
}
}
3、框架详细解说
3.1 MyHttpClient
这个类是整个框架的核心类,用到了一些设计模式和面向对象的思想。首先我把它做成了一个单例。整个应用中只可能存在一个该对象,即节省资源,同时也方便管理。
private MyHttpClient(){};
private static class RequestManagerHolder{
private static final MyHttpClient INSTANCE = new MyHttpClient();
}
public static final MyHttpClient getInstance(){
return MyHttpClient.RequestManagerHolder.INSTANCE;
}
public void init(Context context){
if(appContext==null) {
appContext = context.getApplicationContext();
}
if(mHandler ==null){
mHandler = new Handler(context.getMainLooper());
}
if(mCacheManager==null){
mCacheManager = new CacheManager();
}
if(mCookieManager ==null){
mCookieManager = new CookieManager();
}
}
同时该对象也是一个门面类,客户端对这个框架的所有调用都封装在这个类里面,这样做的目的是方便使用者,让使用者尽可能少的需要知道框架内部各种类的信息,就可以完成操作。
// 取消所有请求
public void cancelAllRequest(){
if(requestList!=null&& requestList.size()>0){
for(HttpRequest request:requestList){
request.abort();
}
requestList.clear();
}
}
// get 请求
// @param urlKey url.xml配置中的Key
// @param callback 请求回调函数,在主线程中执行
public void invokeGet(String urlKey, RequestCallback callback){
URLData data=UrlConfigManager.findURL(urlKey,appContext);
data.netType = "GET";
invoke(data, null, callback);
}
// post 请求
// @param urlKey url.xml配置中的Key
// @param params post 请求需要传入的参数
// @param callback 请求回调函数,在主线程中执行
public void invokePost(String urlKey,HashMap params, RequestCallback callback){
URLData data=UrlConfigManager.findURL(urlKey,appContext);
data.netType = "POST";
invoke(data, params, callback);
}
这个类由于需要暴露给用户,内部需要用到几乎所有其他类的功能,所以我也将这个类作为一个中介类,由于目前这个框架相对来说不是很复杂,所以这样设计比较方便,当框架变的更加复杂时,可以将中介类抽取出来单独成类。同时由于该类是单例,所以如果用户只是通过该类操作,一些不需要暴露给用户的类就可以不用设计成单例,只要这个中介类单例持有,那么全局也只会有一份。
private Handler mHandler; // 持有主线程Lopper,用于回调
private Context appContext; // application的 Context
private List requestList = new LinkedList<>(); // 持有的所有请求队列
private ExecutorService executorService; // 用于网络请求的线程池
CacheManager mCacheManager; // 缓存管理
CookieManager mCookieManager; // Cookie管理
该类中持有了一个线程池:
private synchronized ExecutorService executorService()
{
if (executorService == null) {
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS, new SynchronousQueue());
}
return executorService;
}
这个线程的核心线程数是0,并且没有设置最大线程,比较时候处理那些多而小的事物,这点正符合这个网络框架的需求。如果后期需要下载图片,或者上传较大文件,那可能需要另一个线程池来进行处理。
3.2 UrlConfigManager
这个类主要用来管理Url的配置信息,即在前面说的在res/xml/url.xml中配置进去的各个请求信息。
为了不频繁读取url.xml文件,这边做了一个缓存mUrlList,在MyHttpClient初始化时候对其进行赋值,同时在每次通过mUrlList寻找URLData是进行判断,若mUrlList被垃圾回收器回收,则进行重新赋值。
private static Map mUrlList;
public static URLData findURL(String key, Context context){
URLData data = null;
init(context);
data = mUrlList.get(key);
return data;
}
public static void init(Context context){
if(mUrlList ==null || mUrlList.isEmpty()) {
mUrlList = new HashMap<>();
fetchUrlDataFromXml(context);
}
}
UrlConfigManager 中的fetchUrlDataFromXml()是用来从xml数据读取出来如下所示:
private static void fetchUrlDataFromXml(Context context){
XmlResourceParser xrp = context.getResources().getXml(R.xml.url);
try {
int event = xrp.getEventType(); //先获取当前解析器光标在哪
while (event != XmlPullParser.END_DOCUMENT){ //如果还没到文档的结束标志,那么就继续往下处理
switch (event){
case XmlPullParser.START_DOCUMENT:
break;
case XmlPullParser.START_TAG:
//一般都是获取标签的属性值,所以在这里数据你需要的数据
if (xrp.getName().equals(URLData.NODE)){
URLData data = new URLData();
data.key = xrp.getAttributeValue(null,URLData.KEY);
int expires = Integer.parseInt(xrp.getAttributeValue(null,URLData.EXPIRES));
data.expires = expires;
data.netType = xrp.getAttributeValue(null,URLData.NET_TYPE);
data.url = xrp.getAttributeValue(null,URLData.URL);
mUrlList.put(data.key,data);
}
break;
case XmlPullParser.TEXT:
break;
case XmlPullParser.END_TAG:
break;
default:
break;
}
event = xrp.next(); //将当前解析器光标往下一步移
}
} catch (XmlPullParserException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
3.3 HttpRequest
该类继承自Runnable用于根据请求来获取数据的操作。
持有一个Handler的弱引用,用来进行返回结果的回调,持有弱引用是为了防止内存泄漏。
private WeakReference<Handler> mHandler;
if (mHandler != null && !isCancel && mCallback != null) {
mHandler.get().post(new Runnable() {
@Override
public void run() {
if (response.isError()) {
mCallback.onFail(response.getErrorMessage());
} else {
mCallback.onSuccess(response.getResult());
}
}
});
}
该请求会先从缓存中查找是否有符合的结果,如果没有再到网络中获取。
response = null;
if (mUrlData.netType.toUpperCase().equals("GET")) {
CacheManager.CacheBean bean= mCacheManager.getStringFromMemory(mUrlData.url);
if(bean!=null&& mUrlData.expires>0 && System.currentTimeMillis()<=bean.time ){
response = new Response();
response.setError(false);
response.setResult(bean.context);
Log.d(TAG,"调取缓存中数据:"+mUrlData.url);
}
}
if (response == null) {
Log.d(TAG,"从网络拉取数据:"+mUrlData.url);
response = netRequest();
}
网络请求中会设置相关参数,包括Cookies,同时连接上服务器后会存储Cookies。
private Response netRequest() {
Response response = new Response();
HttpURLConnection connection = null;
String strResponse = null;
try {
// 调用URL对象的openConnection方法获取HttpURLConnection的实例
URL url = new URL(mUrlData.url);
connection = (HttpURLConnection) url.openConnection();
// 设置连接超时、读取超时的时间,单位为毫秒(ms)
connection.setConnectTimeout(8000);
connection.setReadTimeout(8000); // 设置Cookie
mCookieManager.setCookies(connection);
// 设置请求方式,GET或POST
connection.setRequestMethod(mUrlData.netType);
if (mParams != null && mUrlData.netType.toUpperCase().equals("POST")) {
DataOutputStream data = new DataOutputStream(connection.getOutputStream()); StringBuilder sb = new StringBuilder(); Iterator> iterable = mParams.entrySet().iterator();
if (iterable.hasNext()) {
Map.Entry e = iterable.next();
sb.append(e.getKey()).append("=").append(e.getValue());
}
while (iterable.hasNext()) {
Map.Entry e = iterable.next();
sb.append("&").append(e.getKey()).append("=").append(e.getValue());
}
data.writeBytes(sb.toString());
}
//连接
connection.connect();
// 存储Cookies
mCookieManager.storeCookies(connection);
//得到响应码
int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
// getInputStream方法获取服务器返回的输入流
InputStream in = connection.getInputStream();
// 使用BufferedReader对象读取返回的数据流
// 按行读取,存储在StringBuider对象response中
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
StringBuilder responseBuilder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
responseBuilder.append(line);
}
strResponse = responseBuilder.toString();
}
} catch (Exception e) {
response.setErrorType(-1);
response.setError(true);
response.setErrorMessage(e.getMessage());
} finally {
if (connection != null) {
// 结束后,关闭连接
connection.disconnect();
}
}
if (strResponse == null) {
response.setErrorType(-1);
response.setError(true);
response.setErrorMessage("网络异常,返回空值");
} else {
response.setError(false);
response.setResult(strResponse);
if(mUrlData.expires>0) {
CacheManager.CacheBean bean = new CacheManager.CacheBean();
bean.time = System.currentTimeMillis()+mUrlData.expires*1000;
bean.context = strResponse;
mCacheManager.setStringToMemory(mUrlData.url,bean);
}
}
return response;
}
3.4 CacheManager
这个类是用来管理有效请求的缓存。目前里面内容不多,主要核心的是一个LruCache,Lru的全称是Least Recently Used ,近期最少使用。其把近期最少使用的数据从缓存中移除,保留使用最频繁的数据。
private LruCache mLruCache ;
public CacheManager(){
// maxMemory 是允许的最大值 ,超过这个最大值,则会回收
long maxMemory = Runtime.getRuntime().maxMemory()/8; // 获取最大的可用内存 一般使用可用内存的1 / 8
mLruCache = new LruCache<>((int)maxMemory);
}
public CacheBean getStringFromMemory(String url){
CacheBean bean = mLruCache.get(url);
return bean;
}
public void setStringToMemory(String url,CacheBean bean){
mLruCache.put(url,bean);
}
3.5 CookieManager
该类用来管理Cookie。用一个Map来存储Cookie,具体实现是ConcurrentHashMap。这是一个线程安全的Map类,相比于Hashtable,ConcurrentHashMap在线程安全的基础上提供了更好的写并发能力,但同时降低了对读一致性的要求。
里面主要有两个方法:
void storeCookies(URLConnection conn),用于 检索并存储由打开的URLConnection另一端的主机返回的cookie。必须使用connect()方法打开连接后调用,不然会抛IOException异常。
public void storeCookies(URLConnection conn) throws IOException {
// 确认这些cookie是从哪边发送来的
String domain = getDomainFromHost(conn.getURL().getHost());
// 为domain存储cookies
Map> domainStore;
synchronized (store) {
if (store.containsKey(domain)) {
domainStore = store.get(domain);
} else {
domainStore = new HashMap<>();
store.put(domain, domainStore);
}
}
// 从URLConnection中获取cookies
String headerName = null;
for (int i = 1; (headerName = conn.getHeaderFieldKey(i)) != null; i++) {
if (headerName.equalsIgnoreCase(SET_COOKIE)) {
Map cookie = new HashMap<>();
StringTokenizer st = new StringTokenizer(conn.getHeaderField(i), COOKIE_VALUE_DELIMITER);
// http协议规范规定字符串中的第一个键值对是cookie键值,因此将它们作为特殊情况处理:
if (st.hasMoreTokens()) {
String token = st.nextToken();
String name = token.substring(0, token.indexOf(NAME_VALUE_SEPARATOR));
String value = token.substring(token.indexOf(NAME_VALUE_SEPARATOR) + 1, token.length());
domainStore.put(name, cookie);
cookie.put(name, value);
}
while (st.hasMoreTokens()) {
String token = st.nextToken();
cookie.put(token.substring(0,token.indexOf(NAME_VALUE_SEPARATOR)).toLowerCase(),
token.substring(token.indexOf(NAME_VALUE_SEPARATOR) + 1, token.length()));
}
}
}
Log.d(TAG,"storeCookies : "+toString());
}
void setCookies(URLConnection conn) 此方法将设置全部未过期的cookie匹配路径或子路径。必须使用connect()方法打开连接前调用,不然会抛IOException异常。
public void setCookies(URLConnection conn) throws IOException {
// 确定检索适当cookie的域和路径 URL url = conn.getURL();
String domain = getDomainFromHost(url.getHost());
String path = url.getPath(); Map> domainStore = store.get(domain);
if (domainStore == null) return;
StringBuffer cookieStringBuffer = new StringBuffer(); IteratorcookieNames = domainStore.keySet().iterator();
while (cookieNames.hasNext()) {
String cookieName = cookieNames.next();
Map cookie = domainStore.get(cookieName);
// 检查cookie以确保路径匹配,并且cookie没有过期,将cookie添加到标题字符串
if (comparePaths( cookie.get(PATH), path) && isNotExpired((String) cookie.get(EXPIRES))) {
cookieStringBuffer.append(cookieName);
cookieStringBuffer.append("=");
cookieStringBuffer.append( cookie.get(cookieName));
if (cookieNames.hasNext())
cookieStringBuffer.append(SET_COOKIE_SEPARATOR);
}
}
try {
Log.d(TAG,"setCookies : "+cookieStringBuffer.toString());
conn.setRequestProperty(COOKIE, cookieStringBuffer.toString());
} catch (java.lang.IllegalStateException ise) {
IOException ioe = new IOException(
"无效的状态! 无法在已连接的URLConnection上设置Cookie。"
+ " 只调用setCookies(java.net.URLConnection)后调用java.net.URLConnection.connect()。");
throw ioe;
}
}
4、总结
总体该框架介绍完毕,该框架主要通过配置url.xml来配置参数。同时用一个线程池来处理请求,内置了缓存和对于Cookie的管理。宿主可以很方便的通过MyHttpClient类来调用这个框架。
网友评论