基于Volley和Gson的http网络请求设计(客户端和服务端)

经过对项目中http网络请求的多次修改,也算是一个很好的封装了,这个过程中查找了许多资料,发现网上零零散散的介绍比较多,并没有一个系统的例子讲解,这里给大家分享一下我的设计。


Volley和Gson都是Google为Android平台量身打造的框架,Volley是一个异步访问框架,Gson是对Json数据处理的框架,这里就不介绍了,假设大家已经知道怎么使用这两个框架了(这两个框架的用法都是很简单的)


客户端:Volley、Gson

服务端:SpringMVC


首先介绍数据封装对象ClientResponse<T>,每次我们请求服务端都会返回这么一个对象,它封装了返回的数据。

package com.runsdata.tower.android.message;

import java.io.Serializable;

/**
 * 服务端返回的Json响应
 * 
 * @author leo.lai
 * 
 */
public class ClientResponse<T> implements Serializable {

	private static final long serialVersionUID = 1L;

	/**
	 * 状态码
	 */
	private int statusCode;
	/**
	 * 描述
	 */
	private String message;

	/**
	 * 返回的数据
	 */
	private T data;

	/**
	 * 附带值
	 */
	private Object accessory;


}
它由四个字段组成

  • statusCode:状态码——用来表示服务端返回的请求状态(成功、失败、超时、错误等)
  • message:描述——对响应的描述
  • data:返回的数据——返回的数据,它是一个泛型T类型
  • accessory:附带值——这个值不是必须的,需要传输多个值的时候才用上

下面是一个协议命令字定义类Protocol
public class Protocol {

	// 请求成功状态码
	public final static int TOWER_CODE_STATUS_SUCCESS = 0;
	// 通用的或未知的错误
	public final static int TOWER_CODE_STATUS_UNKNOWN_ERROR = 0x8000;
	// session超时
	public final static int TOWER_CODE_STATUS_SESSION_OVERTIME = 0x8001;
	// 未找到(期望存在的)记录
	public final static int TOWER_CODE_STATUS_DATA_NOT_FOUND = 0x8002;
	// 无数据
	public final static int TOWER_CODE_STATUS_NOTHING = 0x8003;
	// 参数错误
	public final static int TOWER_CODE_STATUS_PARAMETER_ERROR = 0x8004;
	// 数据库操作时发生未知错误
	public final static int TOWER_CMD_REQUEST_DATABASE_UNKNOWN_ERROR = 0x81001;
	// 数据库插入数据时发生错误
	public final static int TOWER_CMD_REQUEST_DATABASE_INSERT_ERROR = 0x8101;
}
这个类可以不用关注,由不同业务定义的不同状态常量,上面ClientResponse中的statusCode就是这里定义的。



--------------------------------------------------------------------客户端--------------------------------------------------------------------------


Volley框架给我们提供了很好的拓展,让我们可以根据自己的想法定义Request请求,这里定义一个GsonRequest
package com.runsdata.tower.android.volley.gson;

import java.io.UnsupportedEncodingException;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;

import android.util.Log;

import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import com.runsdata.tower.android.listener.HttpServiceListener;
import com.runsdata.tower.android.message.ClientResponse;
import com.runsdata.tower.android.message.Protocol;
import com.runsdata.tower.android.utils.SessionUtil;
import com.runsdata.tower.android.volley.AuthFailureError;
import com.runsdata.tower.android.volley.NetworkResponse;
import com.runsdata.tower.android.volley.Request;
import com.runsdata.tower.android.volley.Response;
import com.runsdata.tower.android.volley.Response.ErrorListener;
import com.runsdata.tower.android.volley.Response.Listener;
import com.runsdata.tower.android.volley.VolleyError;
import com.runsdata.tower.android.volley.toolbox.HttpHeaderParser;

/**
 * 对ClientResponse请求进行了封装, 保持了sessionId,同时对请求错误做了统一处理
 */
public class GsonRequest<T> extends Request<ClientResponse<T>> {
	public static final String TAG = "GsonRequest";
	
	//响应监听器
	private final Listener<ClientResponse<T>> mListener;
	
	//请求参数Map
	private final Map<String, String> params = new HashMap<String, String>();
	
	//返回数据的类型
	private Type mType;

	/**
	 * 只处理成功的回调
	 * @param url
	 * @param type
	 * @param listener
	 */
	public GsonRequest(String url, Type type, Listener<ClientResponse<T>> listener) {
		this(url, listener, null);
		this.mType = type;
	}

	/**
	 * 对成功和失败的回调
	 * @param url
	 * @param type
	 * @param listener
	 */
	public GsonRequest(String url, Type type, CompletedListener<T> listener) {
		this(url, listener, listener);
		this.mType = type;
	}

	
	public GsonRequest(String url, Type type, HttpServiceListener<T> listener) {
		this(url, listener, listener);
		this.mType = type;
	}

	private GsonRequest(int method, String url, Listener<ClientResponse<T>> listener, ErrorListener errorListener) {
		super(method, url, errorListener);
		mListener = listener;
	}

	private GsonRequest(String url, Listener<ClientResponse<T>> listener, ErrorListener errorListener) {
		this(Method.POST, url, listener, errorListener);
	}

	@Override
	protected void deliverResponse(ClientResponse<T> response) {
		mListener.onResponse(response);
	}

	/**
	 * 解析返回响应
	 */
	@Override
	protected Response<ClientResponse<T>> parseNetworkResponse(NetworkResponse response) {
		SessionUtil.parserSessionIdFromCookie(response.headers);

		String parsed;
		try {
			parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
		} catch (UnsupportedEncodingException e) {
			parsed = new String(response.data);
		}

		ClientResponse<T> clientResponse = null;
		try {
			clientResponse = new Gson().fromJson(parsed, mType);

		} catch (JsonSyntaxException e) {
			e.printStackTrace();
			return Response.error(new VolleyError("数据解析出错"));
		}

		/**
		 * 错误处理
		 */
		VolleyError error = processError(clientResponse);
		if (error != null) {
			return Response.error(error);
		} else {
			return Response.success(clientResponse, HttpHeaderParser.parseCacheHeaders(response));
		}
	}

	/**
	 * 错误处理
	 * 
	 * @param clientResponse
	 */
	private VolleyError processError(ClientResponse<T> clientResponse) {
		String errorDesc = null;
		
		if(clientResponse == null){
			return new VolleyError("服务器无响应");
		}
		
		if (clientResponse.getStatusCode() == Protocol.TOWER_CODE_STATUS_SUCCESS) {
			// 响应成功
			return null;

		} else {
			int statusCode = clientResponse.getStatusCode();
			

			switch (statusCode) {
			case Protocol.TOWER_CODE_STATUS_UNKNOWN_ERROR:
				errorDesc = "服务端异常";
				break;
			case Protocol.TOWER_CODE_STATUS_SESSION_OVERTIME:
				errorDesc = "登录超时";
				UITools.sendSessionOvertimeBroadcast();
				break;
			case Protocol.TOWER_CODE_STATUS_LOGIN_FAIL_NOT_FOUND:
				errorDesc = "用户不存在";
				break;
			case Protocol.TOWER_CODE_STATUS_LOGIN_FAIL_DENY:
				errorDesc = "用户状态异常不允许登录";
				break;
			case Protocol.TOWER_CODE_STATUS_LOGIN_FAIL_INVALID_PASSWORD:
				errorDesc = "密码错误";
				break;
			default:
				errorDesc = "访问网络发生错误";
				break;
			}
		}

		Log.i(TAG, errorDesc);
		UITools.showTip(errorDesc);
		return new VolleyError(errorDesc);
	}

	/**
	 * 设置请求的Header
	 */
	@Override
	public Map<String, String> getHeaders() throws AuthFailureError {
		Map<String, String> headers = new HashMap<String, String>();
		SessionUtil.addSessionCookie(headers);
		return headers;
	}

	/**
	 * 得到请求参数
	 */
	@Override
	protected Map<String, String> getParams() throws AuthFailureError {
		return params;
	}

	/**
	 * 设置参数
	 * 
	 * @param name
	 * @param value
	 */
	public void setParam(String name, String value) {
		params.put(name, value);
	}

	/**
	 * 设置多个参数
	 * @param paramMap
	 */
	public void setParam(Map<String, String> paramMap) {
		params.putAll(paramMap);
	}

	/**
	 * 去除某个参数
	 * @param key
	 */
	public void removeParam(String key){
		params.remove(key);
	}
}


现在对这个类中几个关键的部分进行解析,从上到下:

//响应监听器
private final Listener<ClientResponse<T>> mListener;

//请求参数Map
private final Map<String, String> params = new HashMap<String, String>();

//返回数据的类型
private Type mType;
mListener是响应成功的监听器,由构造方法传入或在构造方法中设置;

param是存放请求参数的Map;

mType是Gson中的类,用于解析Json数据时,指定解析类型


接着是几种构造方法的重载

/**
 * 只处理成功的回调
 * @param url
 * @param type
 * @param listener
 */
public GsonRequest(String url, Type type, Listener<ClientResponse<T>> listener) {
	this(url, listener, null);
	this.mType = type;
}

/**
 * 对成功和失败的回调
 * @param url
 * @param type
 * @param listener
 */
public GsonRequest(String url, Type type, CompletedListener<T> listener) {
	this(url, listener, listener);
	this.mType = type;
}


public GsonRequest(String url, Type type, HttpServiceListener<T> listener) {
	this(url, listener, listener);
	this.mType = type;
}

private GsonRequest(int method, String url, Listener<ClientResponse<T>> listener, ErrorListener errorListener) {
	super(method, url, errorListener);
	mListener = listener;
}

private GsonRequest(String url, Listener<ClientResponse<T>> listener, ErrorListener errorListener) {
	this(Method.POST, url, listener, errorListener);
}
主要是用到前几个带Type参数的。



Http是无状态的协议,程序需要在Session保持状态,由于我们后台的使用Java实现的,保持session是根据解析请求头中的SessionId来判断身份,

所以我们每次请求都需要带上这个SessionId,至于SessionId如何获取的等到了相应代码再说明。

/**
 * 设置请求的Header
 */
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
	Map<String, String> headers = new HashMap<String, String>();
	SessionUtil.addSessionCookie(headers);
	return headers;
}


这里return的header将会设置成http请求的Header,在这里我们加上之前保存好SessionId的header,看下SessionUtil类
/**
 * Session保持工具类
 * @author leo.lai
 *
 */
public class SessionUtil {
	
	public static final String SET_COOKIE_KEY = "Set-Cookie";
	public static final String COOKIE_KEY = "Cookie";
	public static final String SESSION_COOKIE = "JSESSIONID";
	
    /**
     * 当前保持的sessionId
     */
	public static String SESSIONID;
	
	
	/**
	 * 从Cookie中得到SessionId
	 * @param headers
	 */
	public static void parserSessionIdFromCookie(Map<String, String> headers) {
		
        if (headers.containsKey(SET_COOKIE_KEY) && headers.get(SET_COOKIE_KEY).startsWith(SESSION_COOKIE)) {
                String cookie = headers.get(SET_COOKIE_KEY);
                if (cookie.length() > 0) {
                     String[] splitCookie = cookie.split(";");
                    String[] splitSessionId = splitCookie[0].split("=");
                    SESSIONID = splitSessionId[1];
                }
            }
    }

    /**
     * 给请求带上SessionId
     * @param headers
     */
    public static void addSessionCookie(Map<String, String> headers) {
    	
        if(SESSIONID == null)	return;
    	
        if (SESSIONID.length() > 0) {
            StringBuilder builder = new StringBuilder();
            builder.append(SESSION_COOKIE);
            builder.append("=");
            builder.append(SESSIONID);
            if (headers.containsKey(COOKIE_KEY)) {
                builder.append("; ");
                builder.append(headers.get(COOKIE_KEY));
            }
            headers.put(COOKIE_KEY, builder.toString());
        }
    }
    
    /**
     * 得到设置好sessionId的header
     * @return
     */
    public static Header getSessionHeader(){
    	Header header = new BasicHeader(COOKIE_KEY, "JSESSIONID=" + SESSIONID);
    	return header;
    }
    
    
    /**
     * 得到sessionId
     * @return
     */
    public static String getSessionId(){
    	return "JSESSIONID=" + SESSIONID;
    }
}
这里主要是字符串的解析过程,组装成特定格式的Header


到了最关键的响应解析,当服务端返回响应后,会回调parseNetworkResponse(NetworkResponse response)方法,方法参数中的response中的data就是服务费返回的二进制格式数据

/**
 * 解析返回响应
 */
@Override
protected Response<ClientResponse<T>> parseNetworkResponse(NetworkResponse response) {
	SessionUtil.parserSessionIdFromCookie(response.headers);

	String parsed;
	try {
		parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
	} catch (UnsupportedEncodingException e) {
		parsed = new String(response.data);
	}

	ClientResponse<T> clientResponse = null;
	try {
		clientResponse = new Gson().fromJson(parsed, mType);

	} catch (JsonSyntaxException e) {
		e.printStackTrace();
		return Response.error(new VolleyError("数据解析出错"));
	}

	/**
	 * 错误处理
	 */
	VolleyError error = processError(clientResponse);
	if (error != null) {
		return Response.error(error);
	} else {
		return Response.success(clientResponse, HttpHeaderParser.parseCacheHeaders(response));
	}
}

1.首先从response.headers中解析得到SessionId,这就是我们上面提到的;

2.将response.data生成Json字符串;

3.Gson的解析,clientResponse = new Gson().fromJson(parsed, mType);这里的mType是构造方法中传入的,会根据Type类型把json字符串转换成对应对象.

4.对错误情况进行处理;

5.return后会回调对应的成功或者失败的监听器。


/**
 * 错误处理
 * 
 * @param clientResponse
 */
private VolleyError processError(ClientResponse<T> clientResponse) {
	String errorDesc = null;
	
	if(clientResponse == null){
		return new VolleyError("服务器无响应");
	}
	
	if (clientResponse.getStatusCode() == Protocol.TOWER_CODE_STATUS_SUCCESS) {
		// 响应成功
		return null;

	} else {
		int statusCode = clientResponse.getStatusCode();
		

		switch (statusCode) {
		case Protocol.TOWER_CODE_STATUS_UNKNOWN_ERROR:
			errorDesc = "服务端异常";
			break;
		case Protocol.TOWER_CODE_STATUS_SESSION_OVERTIME:
			errorDesc = "登录超时";
			UITools.sendSessionOvertimeBroadcast();
			break;
		case Protocol.TOWER_CODE_STATUS_LOGIN_FAIL_NOT_FOUND:
			errorDesc = "用户不存在";
			break;
		case Protocol.TOWER_CODE_STATUS_LOGIN_FAIL_DENY:
			errorDesc = "用户状态异常不允许登录";
			break;
		case Protocol.TOWER_CODE_STATUS_LOGIN_FAIL_INVALID_PASSWORD:
			errorDesc = "密码错误";
			break;
		default:
			errorDesc = "访问网络发生错误";
			break;
		}
	}

	Log.i(TAG, errorDesc);
	UITools.showTip(errorDesc);
	return new VolleyError(errorDesc);
}
根据在Protocal中定义的状态对错误状态进行处理。


以上就是GsonRequest的整个定义,接下来我们看怎么使用它发起并处理请求,以登录为例子

GsonRequest<UserDto> loginRequest = new GsonRequest<UserDto>(Configuration.SERVER_ACCESS,
    new TypeToken<ClientResponse<UserDto>>(){}.getType(),
    new CompletedListener<UserDto>() {
        @Override
        public void onResponse(ClientResponse<UserDto> response) {
            processAfterLogin(response);
        }
        @Override
        public void onErrorResponse(VolleyError error) {
//                    Toast.makeText(mContext,error.getMessage(),Toast.LENGTH_SHORT).show();
        }
    }
   );

loginRequest.setParam("username", username);
loginRequest.setParam("password", passwd);
loginRequest.setParam("cmd", Protocol.TOWER_CMD_REQUEST_USER_LOGIN + "");
executeRequest(loginRequest);

在创建GsonRequest的时候指定泛型对象UserDto,

new TypeToken<ClientResponse<UserDto>>(){}.getType(),指定响应的类型,这样在监听器Listener的onRequest方法中的返回值便是我们指定的这个类型,

参数中的“cmd”是请求的命令字,这是我们业务定义好的,服务端根据这个命令字区分不同的方法请求


由于自己的泛型设计能力不好,这里的GsonRequest<UserDto>和new TypeToken<ClientResponse<UserDto>>(){}.getType()有点重复的意思,暂时找不到更好的方法,先凑合着用,如果有更好的方法请告诉我。


以上就是客户端的完整设计


--------------------------------------------------------------------服务端--------------------------------------------------------------------------

服务端的Controller层是基于SpringMVC实现的,换成其他框架思想也是一样的。

服务端也需要开始的时候提到的两个ClientResponse<T>和Protocol

服务端的方法入口是唯一,根据请求的命令字分发request


首先配置拦截器

<!-- 拦截器 -->
<mvc:interceptors>
	<mvc:interceptor>
		<mvc:mapping path="/platform/tower/client/facade.ht" />
		<mvc:mapping path="/platform/tower/client/test.ht" />
		<bean class="com.hotent.mobile.interceptor.MobileAccessInterceptor">
			<!-- 配置放行的CMD -->
			<property name="excludeCMD">
				<list>
					<!-- 用户登录 -->
					<value>0x0101</value>
					<!-- 版本检测 -->	
					<value>0x0201</value>
				</list>
			</property>
		</bean>
	</mvc:interceptor>
</mvc:interceptors>
excludeCMD为放行的命令字(用户登录、版本检查),是不需要检测用户是否在线的。

接下来看看MobileAccessInterceptor.java

/**
 * Session判断拦截器
 * 
 * @author leo.lai
 * 
 */
public class MobileAccessInterceptor extends HandlerInterceptorAdapter {

	private static final Logger logger = Logger
			.getLogger(MobileAccessInterceptor.class);

	private List<Short> excludeCMD;

	@Override
	public boolean preHandle(HttpServletRequest request,
			HttpServletResponse response, Object handler) throws Exception {

		String cmd = request.getParameter("cmd");
		if (cmd == null) {
			ClientResponse clientResponse = new ClientResponse();
			clientResponse.setStatusCode(Protocol.TOWER_CODE_STATUS_UNKNOWN_ERROR);
			clientResponse.setMessage("请求命令字为空");
			writeJson(response,clientResponse);
		}

		// 1.判断命令字是否放行
		if (excludeCMD.contains(Short.valueOf(cmd))) {
			return true;
		}

		HttpSession session = request.getSession();
		// 2.判断Session是否超时
		SysUser sysUser = (SysUser) session.getAttribute("currentUser");
		if (sysUser != null) {
			// 设置后续用到的变量
			ContextUtil.setCurrentUser(sysUser);
			return true;
		} else {

			// 超时处理
			logger.info("session超时,id = " + session.getId());
			ClientResponse clientResponse = new ClientResponse();
			clientResponse.setStatusCode(Protocol.TOWER_CODE_STATUS_SESSION_OVERTIME);
			clientResponse.setMessage("Session超时");
			writeJson(response,clientResponse);
			return false;
		}

	}

	/**
	 * 返回json
	 * @param response
	 * @param clientResponse
	 */
	public void writeJson(HttpServletResponse response,ClientResponse clientResponse) {
		response.setContentType("text/json");
		response.setCharacterEncoding("UTF-8");
		try {
			response.getWriter().write(new Gson().toJson(clientResponse));
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	public List<Short> getExcludeCMD() {
		return excludeCMD;
	}

	public void setExcludeCMD(List<Short> excludeCMD) {
		this.excludeCMD = excludeCMD;
	}
}
先判断命令字是否放行,接着从session中获得user对象判断是否为空(当然可以根据其他超时条件判断),如果超时则直接返回超时的ClientResponse对象;


接下来是程序的入口controller

/**
 * 手机客户端访问入口
 * @author leo.lai
 *
 */
@Controller
@RequestMapping("/platform/tower/client")
public class ClientFacadeController{
	
	private static final Logger logger = Logger.getLogger(ClientFacadeController.class);
	
	@Resource
	private ClientUserService userService;
	
	/**
	 * 请求命令字
	 * @param request
	 * @param session
	 * @param cmd	请求命令字
	 * @return
	 */
	@RequestMapping(value = "facade",produces="application/json;charset=UTF-8")
	@ResponseBody
	public String facade(HttpServletRequest request,HttpSession session,int cmd){
		String result = null;
		
		try{
			result = handleReuqest(request, session, cmd);
		}catch(Exception e){
			e.printStackTrace();
			ClientResponse response = new ClientResponse();
			response.setStatusCode(Protocol.TOWER_CODE_STATUS_UNKNOWN_ERROR);
			response.setMessage(e.getMessage());
			result = new Gson().toJson(response);
		}
		
		logger.info("[========Response content========] " + result);
		return result;
	}
	
	/**
	 * 请求处理
	 * @param request
	 * @param session
	 * @param cmd
	 * @return
	 * @throws Exception 
	 */
	public String handleReuqest(HttpServletRequest request,HttpSession session,int cmd) throws Exception{
		String result = null;
		
		switch (cmd) {
		case Protocol.TOWER_CMD_REQUEST_USER_LOGIN:
			result = userService.login(request, session);
			break;
		case Protocol.TOWER_CMD_REQUEST_SETTING_LATEST_VERSION:
			result = versionControlService.getLatestVersion(request);
			break;
<span style="white-space:pre">		</span>//等等。。。。。
		case Protocol.TOWER_CMD_REQUEST_GET_PROCESSES:
			result = myWorkService.getMyProcessCopyList(request);
			break;
		default:
			break;
		}
		return result;
	}

这里在handleRequest方法包含try_catch代码块,当catch到exception后则返回错误信息的ClientResponse,handlerRequest根据CMD命令字对请求进行分发,在这里根据业务调用对应的Service处理方法,

每个service方法返回的时候都必须构造一个ClientResponse对象,并用Gson( ).toJson方法序列化成json 字符串,最终返回到客户端处理。


以上就是整个Http请求的流程,有什么需要改正的地方敬请提出。







评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值