【分享】分享一个自定义的请求包装器

先说下这个请求包装器在我自身项目上的应用:

  1. 参数拦截

    通过定义 ApiImplicitParams 中的过滤模式 fillMode(宽松或者严谨)来达到最终 getParaMap 中值的设值(具体查看以下代码)

    ApiImplicitParams(参考Swagger相关类进行自身项目的调整)

  2. 参数过滤

    暂未实现(自己之前某些项目有单独实现),其实很简单,还是通过注解的方式对每个参数进行定义最后它要过滤成的一个格式,通常用于存在 xss 字段注入的字段用于后台直接转义其值进行保存

  3. 请求体的多次调用支持

    理论上 HttpKit.readData(getRequest()) 仅能获取一次请求体,第二次获取会报错,这个我想大家都懂的,流已经被取走了,你再去掏也掏不出啥。

package plus.jfinal.core.http;

import jodd.io.StreamUtil;
import plus.jfinal.annotations.swagger.ApiImplicitParam;
import plus.jfinal.annotations.swagger.ApiImplicitParams;
import plus.jfinal.enums.ParamFillMode;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;

/**
 * 参数包裹体
 */
public class ParameterHttpServletRequestWrapper extends HttpServletRequestWrapper {

    private HttpServletRequest originalRequest = null;

    private final static String ENCODING = "UTF-8";

    private String contentType = "application/x-www-form-urlencoded;";

    private ApiImplicitParams apiImplicitParams;

    private static final String JSON_CONTENT_TYPE_ = "application/json";

    private Map<String, String[]> parameterMap = null;
    /**
     * 请求体
     */
    private byte[] body;

    public ParameterHttpServletRequestWrapper(HttpServletRequest request, ApiImplicitParams apiImplicitParams) throws IOException {
        super(request);
        this.originalRequest = request;
        this.apiImplicitParams = apiImplicitParams;
        /**
         * 要实现的功能:
         * 1. 对参数接收的限制
         * 2. 对json参数的获取及转换为parametermap
         * 3. 接收到表单请求(含文件时)的处理
         */
        /**
         * 判断上传请求类型
         * null
         * application/x-www-form-urlencoded; charset=UTF-8
         * multipart/form-data; boundary=----WebKitFormBoundarytkozbhbsNZEAQBN4
         * application/json
         */
        contentType = super.getContentType() == null ? "application/x-www-form-urlencoded;" : super.getContentType();
        contentType = contentType.split(";")[0];
        if(JSON_CONTENT_TYPE_.equals(contentType)){
            body = StreamUtil.readBytes(request.getReader(), ENCODING);
        }

    }

    /**
     * 构造参数
     */
    public Map<String, String[]> buildParameterMap(){
        if(parameterMap!=null){
            return this.parameterMap;
        }
        this.parameterMap = new HashMap<>();
        if(apiImplicitParams !=null && ParamFillMode.Strict == apiImplicitParams.fillMode()){
            ApiImplicitParam[] apiImplicitParam = apiImplicitParams.value();
            if(apiImplicitParam.length > 0){
                for(ApiImplicitParam param : apiImplicitParam){
                    /**
                     * 后续在这里考虑对值进行处理
                     */

                    /**
                     * 设置值
                     */
                    if(super.getParameterValues(param.name())!=null){
                        this.parameterMap.put(param.name(),super.getParameterValues(param.name()));
                    }
                }
            }else{
                /* 若列为空就默认变成宽松模式 */
                this.parameterMap.putAll(super.getParameterMap());
            }
        }else{
            this.parameterMap.putAll(super.getParameterMap());
        }
        return this.parameterMap;
    }

    /**
     * 获取所有参数名
     *
     * @return 返回所有参数名
     */
    @Override
    public Enumeration<String> getParameterNames() {
        Vector<String> vector = new Vector<String>(buildParameterMap().keySet());
        return vector.elements();
    }

    /**
     * 获取指定参数名的值,如果有重复的参数名,则返回第一个的值 接收一般变量 ,如text类型
     *
     * @param name 指定参数名
     * @return 指定参数名的值
     */
    @Override
    public String getParameter(String name) {
        String[] results = buildParameterMap().get(name);
        return results[0];
    }


    /**
     * 获取指定参数名的所有值的数组,如:checkbox的所有数据
     * 接收数组变量 ,如checkobx类型
     */
    @Override
    public String[] getParameterValues(String name) {
        return buildParameterMap().get(name);
    }

    @Override
    public Map<String, String[]> getParameterMap() {
        return buildParameterMap();
    }

    public void setParameterMap(Map<String, String[]> parameterMap) {
        this.parameterMap = parameterMap;
    }

    /**
     * 若是 JSON 类型的,就优先读取本地缓存的数据
     * @return
     * @throws IOException
     */
    @Override
    public BufferedReader getReader() throws IOException {
        if(JSON_CONTENT_TYPE_.equals(contentType)){
            return new BufferedReader(new InputStreamReader(getInputStream()));
        }else{
            return super.getReader();
        }
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        if(JSON_CONTENT_TYPE_.equals(contentType)){
            final ByteArrayInputStream bais = new ByteArrayInputStream(body);
            return new ServletInputStream() {
                @Override
                public boolean isFinished() {
                    return true;
                }
                @Override
                public boolean isReady() {
                    return true;
                }
                @Override
                public void setReadListener(ReadListener readListener) {
                }
                @Override
                public int read() throws IOException {
                    return bais.read();
                }
            };
        }else{
            return super.getInputStream();
        }
    }


}


调用方式

1. 定义一个 handler 在其里面对 request 进行包装

request = new ParameterHttpServletRequestWrapper(request,apiImplicitParams);

2. 在你自身工程的 config 中,把你自己的这个 handler 配置进去

以上只是我自己整理的框架中的一个支持,在用的框架支持的另外一个功能就是直接根据swagger注解(有微调,命名一样,里面field有增减)达到生成swagger文档以及基于这个注解上面实现参数验证(包含请求体中的参数!)

以下是示例:

package x.x.x.web.index.controller;

import plus.jfinal.annotations.AccessPass;
import plus.jfinal.annotations.RestController;
import plus.jfinal.annotations.swagger.ApiImplicitParam;
import plus.jfinal.annotations.swagger.ApiImplicitParams;
import plus.jfinal.annotations.swagger.ApiOperation;
import plus.jfinal.core.controller.BaseController;
import plus.jfinal.enums.ParamType;

/* 定义controller控制层,仅定义请求路径若要定义视图格式: @RestController(path="/test",viewPath="/views/test")*/
@RestController("/test")
public class TestController extends BaseController {
    
    @AccessPass //定义访问放行
    @ApiOperation() //定义它是要生成swagger接口文档
    @ApiImplicitParams(value = {
            @ApiImplicitParam(name="b",description="参数B",required = true),
            @ApiImplicitParam(name="d.b",description="参数D.B",required = true) //代表我要验证请求体中 d 下的 b 参数
    }, paramType = ParamType.BODY) //定义参数验证
    public void index(){
        String json = getParams().requestBody().toJSONString(); //自己封装的方法,支持直接把请求体转为JSONObject(基于 fastjson 增强了一些便捷取值的方法),还有其它的支持哦~
        renderJson(json);
    }
}

请求内容

{
"a":1,
"b":"111",
"d":{
	"a":2
}
}

返回

{
"code": 430,
"success": false,
"message": "参数D.B 不能为空"
}


评论区

JFinal

2019-10-10 15:07

jfinal 比较高的版本提供了一个 getRawData() 方法,已经取代了 HttpKit.readData(...)

getRawData() 的优点是可以反复多次调用,不会抛异常

Psbye

2019-10-11 09:18

@JFinal 查看了一下源码,的确提供了!只不过这个是基于controller的基础上,因为我要做一些请求前的拦截及处理(做上文中有提的参数验证或者过滤),该方法就用不上了,因为还没到controller我就要去取它了

JFinal

2019-10-11 11:17

@Psbye 拦截器中可以用的,而且提倡这么用
inv.getController().getRawData()

Psbye

2019-10-11 11:42

@JFinal 我去试试

Psbye

2019-10-12 08:49

@JFinal 测试是通过的,但是在一些特殊情况下就不能用这个方法了,例如某个工具类仅需传递request参数,这时候内部有对body进行获取,就会报空指针异常