需求
假如用户没有传递cooperatorId,那么默认等于uin。
常规做法是在interceptor或者filter或者action中判断如果没有cooperatorId参数,则设置到request的parameterMap中:
String cooperatorId = request.getParameter("cooperatorId");
if(cooperatorId == null){
request.getParameterMap().put("cooperatorId",request.getParameter("uin")) ;
}
问题
public interface ServletRequest {
...
/** Returns a java.util.Map of the parameters of this request.
* Request parameters
* are extra information sent with the request. For HTTP servlets,
* parameters are contained in the query string or posted form data.
*
* @return an immutable java.util.Map containing parameter names as
* keys and parameter values as map values. The keys in the parameter
* map are of type String. The values in the parameter map are of type
* String array.
*
*/
public Map getParameterMap();
...
}
但是HttpServletRequest.getParameterMap()返回的是immutable的java.util.Map,所以不能直接修改parameterMap。只能另辟蹊径了。
解决方案
虽然request本身的属性是不可修改的,但是方法确是可以重载的,假如我们定义一个HttpServerRequest的子类,覆盖他的getParameter()和getParameterMap()方法,让其好像有cooperatorId这请求参数一般,也可以达到我们的目的。J2EE提供了一个HttpServletRequestWrapper可以帮我们很方便实现这个子类:
/**
*
* Provides a convenient implementation of the HttpServletRequest interface that
* can be subclassed by developers wishing to adapt the request to a Servlet.
* This class implements the Wrapper or Decorator pattern. Methods default to
* calling through to the wrapped request object.
*
*
* @see javax.servlet.http.HttpServletRequest
* @since v 2.3
*
*/
public class HttpServletRequestWrapper extends ServletRequestWrapper implements HttpServletRequest {
...
}
代码实现:
package me.arganzheng.study;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
/**
* <pre>
* Api的 HttpServletRequest,主要是完成如下功能:
* 背景:现在我们所有的API都是要求传递cooperatorId,而且cooperatorId的定位是卖家QQ号。但是现在逐渐有针对买家的三方应用出现。
* 这些应用是以买家为纬度的,不需要传递cooperatorId。但是我们现在的鉴权体系包括后台的IDL都是要求传递cooperatorId,这里提出了这么一个逻辑:
* 如果没有传递cooperatorId,那么默认就是授权者,即uin。这样cooperatorId只有在和uin不同的情况下才需要传递。
* 通过这样,给买家用户造成一个不需要传递cooperatorId的假象。而且这个逻辑也是简化参数传递不错的逻辑。
*
* @author arganzheng
* @date 2012-9-10
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public class ApiHttpServletRequest extends HttpServletRequestWrapper {
private static final String UIN = "uin";
private static final String COOPERATOR_ID = "cooperatorId";
public ApiHttpServletRequest(HttpServletRequest request){
super(request);
}
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
if(value == null && COOPERATOR_ID.equals(name)){
value = super.getParameter(UIN);
}
return value;
}
@Override
public Map getParameterMap() {
Map map = super.getParameterMap();
if(!map.containsKey(COOPERATOR_ID) && map.containsKey(UIN)){
map = new HashMap(map);
map.put(COOPERATOR_ID, map.get(UIN));
}
return map;
}
@Override
public Enumeration getParameterNames() {
if(super.getParameter(COOPERATOR_ID) == null && super.getParameter(UIN) != null){
return Collections.enumeration(getParameterMap().keySet());
}else{
return super.getParameterNames();
}
}
@Override
public String[] getParameterValues(String name) {
String[] values = super.getParameterValues(name);
if(values == null && COOPERATOR_ID.equals(name)){
values = super.getParameterValues(UIN);
}
return values;
}
}
package me.arganzheng.study.filter;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import me.arganzheng.study.ApiHttpServletRequest;
/**
* 如果没有传递cooperatorId,那么默认就是授权者,即uin。这样cooperatorId只有在和uin不同的情况下才需要传递。
*
* @author arganzheng
* @date 2012-9-10
*/
public class ApiResetServletRequestFilter implements Filter {
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain filterChain) throws IOException, ServletException {
filterChain.doFilter(new ApiHttpServletRequest((HttpServletRequest) request), response);
}
@Override
public void init(FilterConfig arg0) throws ServletException {
}
}
然后在web.xml中配置一个filter:
<filter>
<filter-name>ApiResetServletRequestFilter</filter-name>
<filter-class>
me.arganzheng.study.filter.ApiResetServletRequestFilter
</filter-class>
</filter>
<filter-mapping>
<filter-name>ApiResetServletRequestFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
补充:关于unmodifiableMap
其实Java的unmodifiableMap,其实现原理就是利用Java的final变量的immutable特性+函数重载。final特性保证了变量引用的immutable,而将写接口重载使得内容不可更改了。具体代码如下:
public class Collections {
// Suppresses default constructor, ensuring non-instantiability.
private Collections() {
}
/**
* Returns an unmodifiable view of the specified map. This method
* allows modules to provide users with "read -only" access to internal
* maps. Query operations on the returned map "read through"
* to the specified map, and attempts to modify the returned
* map, whether direct or via its collection views, result in an
* <tt>UnsupportedOperationException</tt> .<p>
*
* The returned map will be serializable if the specified map
* is serializable.
*
* @param m the map for which an unmodifiable view is to be returned.
* @return an unmodifiable view of the specified map.
*/
public static <K,V> Map<K,V> unmodifiableMap(Map<? extends K, ? extends V> m) {
return new UnmodifiableMap<K,V>(m);
}
/**
* @serial include
*/
private static class UnmodifiableMap <K,V> implements Map<K,V>, Serializable {
// use serialVersionUID from JDK 1.2.2 for interoperability
private static final long serialVersionUID = -1034234728574286014L;
private final Map<? extends K, ? extends V> m;
UnmodifiableMap(Map<? extends K, ? extends V> m) {
if (m==null )
throw new NullPointerException();
this.m = m;
}
public int size() { return m .size();}
public boolean isEmpty() {return m.isEmpty();}
public boolean containsKey(Object key) {return m.containsKey(key);}
public boolean containsValue(Object val) {return m.containsValue(val);}
public V get(Object key) { return m.get(key);}
public V put(K key, V value) {
throw new UnsupportedOperationException();
}
public V remove(Object key) {
throw new UnsupportedOperationException();
}
public void putAll(Map<? extends K, ? extends V> m) {
throw new UnsupportedOperationException();
}
public void clear() {
throw new UnsupportedOperationException();
}
...
}
}
也可以通过反射改变unmodifiedMap的特性。
具体例子:参见 http://vangjee.wordpress.com/2009/02/25/how-to-modify-request-headers-in-a-j2ee-web-application/
不过貌似只能通过Cache访问了。
补记:另一个实战例子 2015-07-24
最近在做一个新项目,出于对用户隐私的考虑,需要对所有提交的参数进行加密,不让用户知道我们到底提交了什么,特别是统计参数(语言、国际、版本、uid等)。原来浏览器那边也有类似的需求,于是打算直接用他们那一套加密机制。参数就约定为_p=加密后的JSON串。比如
_p=3lg62PMuu5QhfQAn8HxmjUQndEdunnPa2JjrnPHT2IEjqNVsDvin2z0UhUypPXGvif4hXibzuCbjJu6RUX9%252Bs7rq8VslqGZAr4XdsKs6XnWjnG8DXKtad3TyQlDTux9MQgESqZXlWGjvxzK0NHb5I995HzBvMmmqtZzbl0O4%252FwouMT4xZuNxelACNceoQkno6IEF%252BUIFclwDyRiNE6QqVNupOR5cYdqE75u1kEpMRePZivOlrX%252FeFBXf%252FhWC0xVHQdbFAuJGSgUr%252BzxcdC05%252BX74nufbkL8GJzNJktS47tWVJHiMArwk2ziXIcvDNDfdfJF%252F02Cq8Lt4in1oBFPR0mTZIKYariNNlv1I%252BuueZMUzjg1YFvDTvalFmkyeChWWNfAoLrQNf9vfHKRro8de7AknPTdh0UgE5kfauFq%252FAzi6b1zOIRCih9gZcCHXw477YdDw8gtrYQX0X2ACBDut%252BHliMF7CCO4STi9cdS85HCKl9L5yzZXTqBNrwlWiGr8Y6QSITvxFj5jne1sokTOkR%252FAZbOjqUnoQAhGV5G%252Bt32R3SkM4tZkO6rLMJ4j40dw7XY%252FCWfKX9JGG2Pd6ZJHwyfxCPFPXOOcdfSrgstTnFu4mbrIEXFhl9YwI%252Fd04IA%252F3%252FdFWx7sAc2vnDOfCpzQbQvPWdjQLv%252BlwVS6kn%252BAUzYME%253D
解密之后就变成了:
_p={"pt":"dl","uid":"1234556","cf":"gp","co":"us","la":"en","of":"gp","pr":"","sv":"a_22","av":"1.1.0.1002","packageNames":["com.example.android.apis","com.dianxinos.powermanager","com.android.gesture.builder","com.hexin.plat.android","com.google.android.apps.plus","com.google.android.inputmethod.pinyin"]}
然后将JSON串反序列化成Java POJO对象,进行相应的操作,实现起来大概是这个样子:
@RequestMapping(value = “/getAppCategory”)@ResponseBody
public RestResponse<Map<String, String>> getAppCategory(HttpServletRequest request){
String params = request.getParameter("_p");
Assert.notEmpty(params);
String reqString = params;
if (ConfigurationTool.decodeParameter()) { // if need decode
reqString = ParameterUtil.decodeParameter(params);
}
GetAppCategoryRequest getAppCategoryRequest = objectMapper.readValue(reqString, GetAppCategoryRequest.class);
// do string
...
}
虽然,我们把加密过程封装了,但是有如下三个问题:
- 每个方法都要做解密,反序列化过程,麻烦
- 接收参数只能是通用的HttpServletRequest,无法自动使用Spring MVC的参数注入
- 不方便测试,即使这里用配置参数决定要不要加密,提交一个JSON串仍然没有传统的k-v方式简单友好
于是想一下能不能将这一切透明化,让服务端以为客户端提交的还是传统的k-v方式呢?一个简单的想法就是通过在filter或者interceptor中将_p参数提取出来,然后反序列化,再将参数一个个重新塞入HttpServletRequest中。
如上讨论,传统的HttpServletRequest是不可修改的,于是写了这么一个类:
package me.arganzheng.study.sever.launcher.common;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import org.apache.commons.lang.StringUtils;
import org.codehaus.jackson.annotate.JsonProperty;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion;
import org.codehaus.jackson.type.TypeReference;
import me.arganzheng.study.server.launcher.request.BaseRequest;
import me.arganzheng.study.server.launcher.util.ConfigurationTool;
import me.arganzheng.study.server.launcher.util.ParameterUtil;
public class ApiHttpServletRequest extends HttpServletRequestWrapper {
private static ObjectMapper objectMapper = new ObjectMapper();
private static Map<String, String> baseReqKeyMap = new HashMap<String, String>();
static {
objectMapper.setSerializationConfig(objectMapper.getSerializationConfig().withSerializationInclusion(
Inclusion.NON_NULL));
Field[] fileds = BaseRequest.class.getDeclaredFields();
for (Field f : fileds) {
String fname = f.getName();
JsonProperty jsonPropertyAnnotation = f.getAnnotation(JsonProperty.class);
if (jsonPropertyAnnotation != null) {
String shortName = jsonPropertyAnnotation.value();
baseReqKeyMap.put(shortName, fname);
}
}
}
// modified parameters map
private final Map<String, String[]> parameters = new TreeMap<String, String[]>();
public ApiHttpServletRequest(HttpServletRequest request) {
super(request);
// 请求参数是特殊的_p=加密的JSON字符串
String params = request.getParameter("_p");
if (StringUtils.isBlank(params)) {
parameters.putAll(super.getParameterMap());
return;
}
// for API requests
String reqString = params;
if (ConfigurationTool.decodeParameter()) {
reqString = ParameterUtil.decodeParameter(params);
}
// parse json and put it to request parameters map.
try {
Map<String, Object> req = objectMapper.readValue(reqString, new TypeReference<Map<String, Object>>() {
});
if (req != null) {
addMapEntries(req);
}
} catch (IOException e) {
e.printStackTrace();
}
// put the original param value back
parameters.put("_p", new String[] { reqString });
}
private void addMapEntries(Map<String, Object> req) {
if (req != null && !req.isEmpty()) {
for (Map.Entry<String, Object> prop : req.entrySet()) {
Object value = prop.getValue();
if (value != null) {
if (value instanceof Collection<?>) {
Collection<?> a = (Collection<?>) value;
List<String> v = new ArrayList<String>();
for (Object e : a) {
v.add(e.toString());
}
parameters.put(getKey(prop.getKey()), v.toArray(new String[0]));
} else {
String svalue = value.toString();
parameters.put(getKey(prop.getKey()), new String[] { svalue });
}
}
}
}
}
private String getKey(String key) {
String v = baseReqKeyMap.get(key);
if (v == null) {
return key;
}
return v;
}
@Override
public String getParameter(String name) {
String[] strings = getParameterMap().get(name);
if (strings != null) {
return strings[0];
}
return super.getParameter(name);
}
@Override
public Map<String, String[]> getParameterMap() {
// Return an unmodifiable collection because we need to uphold the interface contract.
return Collections.unmodifiableMap(parameters);
}
@Override
public Enumeration<String> getParameterNames() {
return Collections.enumeration(getParameterMap().keySet());
}
@Override
public String[] getParameterValues(final String name) {
return getParameterMap().get(name);
}
}
然后写个filter偷梁换柱一下:
package me.arganzheng.study.server.launcher.filter;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import me.arganzheng.study.server.launcher.common.ApiHttpServletRequest;
public class ApiResetServletRequestFilter implements Filter {
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException,
ServletException {
filterChain.doFilter(new ApiHttpServletRequest((HttpServletRequest) request), response);
}
@Override
public void init(FilterConfig arg0) throws ServletException {
}
}
然后在web.xml中注册一下:
<filter>
<filter-name>ApiResetServletRequestFilter</filter-name>
<filter-class>me.arganzheng.study.server.launcher.filter.ApiResetServletRequestFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>ApiResetServletRequestFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Done!现在我们可以这样子接受请求参数了:
@RequestMapping(value = "/getAppCategory")
@ResponseBody
public RestResponse<Map<String, String>> getAppCategory(GetAppCategoryRequest getAppCategoryRequest) {
// do string
...
}
GetAppCategoryRequest定义如下:
package me.arganzheng.study.server.launcher.request;
import java.util.List;
public class GetAppCategoryRequest extends BaseRequest {
private List<String> packageNames;
public List<String> getPackageNames() {
return packageNames;
}
public void setPackageNames(List<String> packageNames) {
this.packageNames = packageNames;
}
}
其中BaseRequest定义如下:
public class BaseRequest {
// API版本
private String api;
// 产品,固定值 dl
@JsonProperty("pt")
private String product = "*";
// App语言标识
@JsonProperty("la")
private String language = "*";
// 当前渠道号
@JsonProperty("cf")
private String currentFrom = "*";
// 原始渠道
@JsonProperty("of")
private String oldFrom = "*";
// App版本
@JsonProperty("av")
private String appVersion = "*";
@JsonProperty("sv")
private String sdkVersion = "*";
// 系统国家标识 (两位,大写字母) http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
@JsonProperty("co")
private String country = "*";
// 运营商
@JsonProperty("pr")
private String provider = "*";
// ...
}
并且可以使用_p=加密的JSON串提交,也可以用打散的key-value方式提交请求了,方便测试。