SpringMVC之自定义参数解析

前面一篇SpringMVC工作原理之参数解析分析了参数解析及转换的过程,先是通过参数解析器解析参数,然后再是转换器转换参数,最终绑定到对应 RequestMapping 方法参数上。但是在有些时候 SpringMVC 提供的参数解析器或参数转换器满足不了我们的需求,这个时候就需要我们自己按照 SpringMVC 提供的接口进行自定义。

一 自定义参数解析器

1 场景分析

在有些开发场景中,SpringMVC 提供的参数解析器满足不了咱们的需求。例如在数据量大的提交环境中,提交数据用到了表单和JSON融合的方式,就是表单某个字段的 value 是JSON字符串。
如果整个提交的数据体是JSON数据还好,导入Jackson架包,用 @RequestBody 修饰参数,最终 SpringMVC 会通过自带的 RequestResponseBodyMethodProcessor 解析器进行解析,使用 Jackson 提供的 MappingJackson2HttpMessageConverter 转换器将JSON数据转换成我们想要的格式。
如果提交的是正常表单数据也好,用 @RequestParam 修饰参数,最终 SpringMVC 会通过自带的 RequestParamMethodArgumentResolver 解析器解析出表单里面的 value,然后找到合适的转换器将数据装换成我们想要的格式。
但是现在是表单里面掺杂了JSON字符串的 value,为了优雅的解决这个问题,就需要我们自定义一个参数解析器。

2 开始代码编写

假设现在是一个发送消息的请求,表单提交的数据前面的 keyvalue 指明了发送人的信息,后面有一个 key 对一个发送消息体,是一个JSON字符串,里面指明了消息的具体情况。

注意:开始下面代码前需要先导入 Jackson 支持 JSON 数据转换的架包。

① 自定义名叫 JSONRequestParam 参数注解
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JSONRequestParam {

    @AliasFor("name")
    String value() default "";

    @AliasFor("value")
    String name() default "";

    boolean required() default true;
}
② 自定义名叫 JSONArgumentResolver 参数解析器,需要实现 HandlerMethodArgumentResolver 接口
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import javax.servlet.http.HttpServletRequest;

public class JSONArgumentResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(JSONRequestParam.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

        //得到 JSONRequestParam 注解信息并将其转换成用来记录注解信息的 JSONRequestParamNamedValueInfo 对象
        JSONRequestParam jsonRequestParam = parameter.getParameterAnnotation(JSONRequestParam.class);
        JSONRequestParamNamedValueInfo namedValueInfo = new JSONRequestParamNamedValueInfo(jsonRequestParam.name(), jsonRequestParam.required());
        if (namedValueInfo.name.isEmpty()) {
            namedValueInfo.name = parameter.getParameterName();
            if (namedValueInfo.name == null) {
                throw new IllegalArgumentException(
                        "Name for argument type [" + parameter.getNestedParameterType().getName() +
                                "] not available, and parameter name information not found in class file either.");
            }
        }

        HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);

        //获得对应的 value 的 JSON 字符串
        String jsonText = servletRequest.getParameter(namedValueInfo.name);

        //得到参数的 Class
        Class clazz = parameter.getParameterType();

        //使用 Jackson 将 JSON 字符串转换成我们想要的对象类
        ObjectMapper mapper = new ObjectMapper();
        Object value = mapper.readValue(jsonText, clazz);

        return value;
    }

    private static class JSONRequestParamNamedValueInfo {

        private String name;

        private boolean required;

        public JSONRequestParamNamedValueInfo(String name, boolean required) {
            this.name = name;
            this.required = required;
        }
    }
}
③ 自定义参数解析器的注入

最终我们需要将自定义的参数解析器注入到 RequestMappingHandlerAdapter 适配器的 customArgumentResolvers 的集合属性中。但是该适配器一般是由 <mvc:annotation-driven /> 标签帮我们注入的,所以在写了该标签的情况下就不要自己手动注入,所以正确的注入姿势如下:

<mvc:annotation-driven>
    <mvc:argument-resolvers>
        <bean class="com.ogemray.springmvc.controller.JSONArgumentResolver"></bean>
    </mvc:argument-resolvers>
</mvc:annotation-driven>

④ 编写参数需要转换的 POJO 类

public class Message {

    //1. 文字  2. 图片 3. 语音 4. 其他
    private int type;

    //文字类容
    private String content;

    //图片信息
    private float picWidth;
    private float picHeight;
    private float picAddress;

    //语音信息
    private float voiceTime;
    private float voiceAddress;
}

⑤ 编写Controller里面对应的方法代码

@ResponseBody
@RequestMapping(value = "hello", method = RequestMethod.POST)
public String testHello(@RequestParam String touid, @JSONRequestParam(value = "msg") Message message) {

    System.out.println("发送人UID : " + touid);
    System.out.println(message);
    return "success";
}

这里为了更直观很多东西做了简化,参数包括两部分,前面是正常键值对标记接收人的uid,后面用自定义 @JSONRequestParam 注解修饰的消息实体。返回值也只是一个字符串,并将该字符串直接写入到 Response 的 body 里面,所以请求头里面的 Accept 字段应该标记接收内容格式为文本格式。

⑥ AJAX 发送请求

<a id="tag" href="${pageContext.request.contextPath}/hello">点击发送消息</a>
<script type="text/javascript">
    $("#tag").click(function () {

        var msgBody = {type : 1, content : "Hello Word"};
        var args = {touid : "893081892", msg : JSON.stringify(msgBody)};

        $.ajax({
            url: this.href,
            type: "POST",
            data: args,
            contentType: "application/x-www-form-urlencoded; charset=utf-8",
            headers: { Accept: "text/html; charset=utf-8" },
            success: function (data, textStatus) {
                console.log(textStatus);
                console.log(data);
            },
            error: function (data, textStatus, errorThrown) {
                console.log(textStatus);
                console.log(data);
                console.log(errorThrown);
            }
        });
        return false;
    });
</script>

注意:这里需要指明发送的数据为表单格式(即 Content-Type = application/x-www-form-urlencoded; charset=utf-8)

二 自定义参数转换器

1 使用场景

在某些时候,表单提交大量数据时就显得很复杂,例如需要提交关于某个人的信息,需要包含名字、年龄、身高......,这时候请求体里面就需要大量的键值对,如果我们将一个人的信息用特殊符号分割写进一个字符串里面,这样一个键值对就可以将整个人的信息都传递出去,服务器按照指定格式解析字符串,这样一来便节省了不必要的流量消耗。这种情况下SpringMVC提供的转换器就不够用了,需要我们自定义转换器。

2 开始代码编写

假设提交的个人信息字符串里面包括人的名字、年龄、身高、和体重,然后用 "&" 字符分割。

① 首先写一个关于记录人信息的 POJO 类
public class Person {
    private String name;
    private Integer age;
    private Float height;
    private Float weight;
}
② 自定义类名为 PersonConverter 转换器,需要实现 Converter 接口
import org.springframework.core.convert.converter.Converter;

public class PersonConverter implements Converter<String, Person> {

    @Override
    public Person convert(String source) {

        if (source != null) {
            String[] array = source.split("&");
            if (array.length >= 4) {
                Person person = new Person();
                person.setName(array[0]);
                person.setAge(Integer.parseInt(array[1]));
                person.setHeight(Float.parseFloat(array[2]));
                person.setWeight(Float.parseFloat(array[3]));
                return person;
            } else  {
                System.out.println("参数不合法 : " + source);
            }
        }
        return null;
    }
}
③ 将自定义的转换器注入到容器

<mvc:annotation-driven /> 标签会自动帮我们创建并注入 ConfigurableWebBindingInitializer 对象,该对象上面记录了验证器(validator 属性)、转换器相关(conversionService 属性)等等,最终将该对象绑定到 RequestMappingHandlerAdapter 适配器上,用作后来的参数验证及转换。
在源码里 <mvc:annotation-driven /> 标签帮我们创建的 conversionServiceFormattingConversionServiceFactoryBean 工厂类bean,所以我们下面自定义也用它。使用FormattingConversionServiceFactoryBean 可以让SpringMVC支持 @NumberFormat@DateTimeFormat 等Spring内部自定义的转换器。

<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <property name="converters">
        <set>
            <bean class="com.ogemray.converter.PersonConverter"></bean>
        </set>
    </property>
</bean>
<mvc:annotation-driven conversion-service="conversionService" />
④ 编写Controller里面对应的方法代码
@ResponseBody
@RequestMapping(value = "hello", method = RequestMethod.POST)
public String testHello(Person person) {
    System.out.println(person);
    return "success";
}

这里也是为了代码直观做了简化,直接返回字符串简单了解提交状态。

⑤ AJAX 发送请求
<a id="tag" href="${pageContext.request.contextPath}/hello">点击发送请求</a>
<script type="text/javascript">
    $("#tag").click(function () {
        $.ajax({
            url: this.href,
            type: "POST",
            data: {person : "Tom&18&175.1&56.8"},
            contentType: "application/x-www-form-urlencoded; charset=utf-8",
            heanders: { Accept: "text/html; charset=utf-8" },
            success: function (data) {
                console.log(data);
            },
            error: function () { }
        });
        return false;
    });
</script>

总结

优秀的框架支持开闭原则,对外扩展开放,内部修改关闭。通过这两个自定义可以学到很多优秀的编程思想。

其他相关文章

SpringMVC入门笔记
SpringMVC工作原理之处理映射[HandlerMapping]
SpringMVC工作原理之适配器[HandlerAdapter]
SpringMVC工作原理之参数解析
SpringMVC之自定义参数解析
SpringMVC工作原理之视图解析及自定义
SpingMVC之<mvc:annotation-driven/>标签

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,265评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,078评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,852评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,408评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,445评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,772评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,921评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,688评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,130评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,467评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,617评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,276评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,882评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,740评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,967评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,315评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,486评论 2 348

推荐阅读更多精彩内容

  • 对于java中的思考的方向,1必须要看前端的页面,对于前端的页面基本的逻辑,如果能理解最好,不理解也要知道几点。 ...
    神尤鲁道夫阅读 804评论 0 0
  • 前提 在日常使用SpringMVC进行开发的时候,有可能遇到前端各种类型的请求参数,这里做一次相对全面的总结。Sp...
    zhrowable阅读 2,017评论 0 15
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,059评论 4 62
  • 门徒 影评 关于这部由尔冬升,陈可辛联手导演并监制的电影,我真的有些不知该如何定位。这部电影说是警匪片...
    行走世间的锁匠阅读 1,541评论 3 2
  • 在城市里穿梭的地铁上,摩肩擦踵的人们神情麻木地低着头,或看着手机,或看着手中的报纸,或打着未完的瞌睡,没有人知道一...
    胆小鬼zxhsdbd阅读 647评论 0 7