原生SpringMVC有如下缺陷


  1. 参数的JSON反序列化只支持@RequestBody注解,这意味着不能在controller方法中写多个参数,如下代码是不对的
    public Map test(@RequestBody User user, @RequestBody Role role)
  2. 如下代码,User和Role类都有id字段。假如请求方式是默认的x-www-form-urlencoded格式,那么请求test方法的http请求实体将类似于id=1234&userName=uuuu&roleName=rrrrr,但实际上我们希望传两个id且代表不同的意思。另外,如果User中有School类的成员,School类中有schoolName字段,那么请求参数仅会变成id=1234&userName=uuuu&roleName=rrrrr&&schoolName=ssss。可见这种参数绑定方式有歧义而且没有属性的层级概念
    public class User {
    private int id;
    private String userName;
    ...
    } public class Role {
    private int id;
    private String roleName;
    ...
    } // controller的方法
    public Map test(User user, Role role)
  3. JSON参数绑定不能支持Map参数。如下代码中的userMap将会被SpringMVC初始化成BindingAwareModelMap,而不是前端传过来的JSON对象
    public Map test(@RequestParam Map<String, User> userMap)
  4. 参数校验不支持原始类型、原始类型的包装类。如下代码的校验不会起作用,原因是SpringMVC是对方法参数遍历,分别对每一个参数用适配的参数解析器把request中的parameter转换成参数,在参数解析器中只能知道当前的参数类型和HttpRequest,无法得到test方法的第N个参数带有什么注解
    public Map test(@Valid @Range(min=1, max=10) Integer limit)

对上述缺陷的想法


  1. 可以用增加一个参数解析器去解析@JsonParam注解,请求格式第一层是x-www-form-urlencode,第二层是JSON格式,如下例子(未进行encode)使得请求参数不具有歧义、且有属性有层次
    user={"id": 123, "userName": "abc"}&role={"id": 456, "roleName": "admin"}
  2. SpringMVC默认使用RequestMappingHandlerAdapter去初始化24个参数解析器,把我们拓展的自定义参数解析器排在后面,并且不允许修改顺序。这就导致了MapMethodProcessor比我们的解析器先处理Map参数,返回了ModelMap。因此不能通过增加参数解析器去处理无法传入Map参数的问题。
  3. Spring在初始化一个bean后,在放入BeanFactory前,首先会让BeanPostProcessor对bean进行后处理,同时RequestMappingHandlerAdapter可以通过setArgumentResolvers方法覆盖默认的参数解析器,因此我们可以新建一个后置Bean处理器去处理RequestMappingHandlerAdapter,首先通过getArgumentResolvers拿到参数解析器列表,然后在列表的最前面加上我们自定义的参数解析器,使得我们的参数解析器比MapMethodProcessor要先处理,这样就能解析转换Map参数
  4. JSR-303规范中定义了两个接口,一个是javax.validation.Validator,一个是javax.validation.executable.ExecutableValidator,其中Validator能对一个实体bean进行校验,ExecutableValidator能够对一个方法中的所有带有@Valid注解的参数进行校验,SpringMVC的参数解析器使用的是Validator。为了解决缺陷4,可以使用切面对Controller的方法进行处理,在调用方法之前,先用ExecutableValidator对方法参数进行处理。注意:如果使用Tomcat容器环境,则需要把切面定义在SpringMVC的配置文件中,而不是配置Spring的配置文件中,它们的上下文是不同的,Spring读不到SpringMVC的bean。Spring boot没有这个问题。

实现代码


json参数注解:

package com.baidu.waimai.springext;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; @Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
public @interface JsonParam {
String value(); boolean required() default true; String defaultValue() default "";
}

@JsonParam的参数解析器,参考自org.springframework.web.method.annotation.ModelAttributeMethodProcessor,使用FastJson来做参数转换,做了实体bean的参数校验:

package com.baidu.waimai.springext;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.validation.BindException;
import org.springframework.validation.Errors;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.ServletRequestDataBinder;
import org.springframework.web.bind.WebDataBinder;
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.ServletRequest;
import java.lang.annotation.Annotation;
import java.util.Map; public class JsonArgumentResolver implements HandlerMethodArgumentResolver { @Override
public boolean supportsParameter(MethodParameter parameter) {
JsonParam jsonParam = parameter.getParameterAnnotation(JsonParam.class);
return jsonParam != null;
} protected Object resolveName(String name, MethodParameter methodParameter, NativeWebRequest nativeWebRequest)
throws Exception {
String[] paramValues1 = nativeWebRequest.getParameterValues(name);
if (paramValues1 != null) { return paramValues1.length == 1
? JSON.parseObject(
paramValues1[0], methodParameter.getNestedGenericParameterType(), Feature.OrderedField)
: paramValues1;
}
return null;
} @Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
JsonParam jsonParam = parameter.getParameterAnnotation(JsonParam.class);
Object target = resolveName(jsonParam.value(), parameter, webRequest); WebDataBinder binder = binderFactory.createBinder(webRequest, target, jsonParam.value());
if (binder.getTarget() != null) {
bindRequestParameters(binder, webRequest);
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
} // Add resolved attribute and BindingResult at the end of the model
Map<String, Object> bindingResultModel = binder.getBindingResult().getModel();
mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel); return binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
} protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class);
ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder;
servletBinder.bind(servletRequest);
} protected void validateIfApplicable(WebDataBinder binder, MethodParameter methodParam) {
Annotation[] annotations = methodParam.getParameterAnnotations();
for (Annotation ann : annotations) {
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] { hints });
binder.validate(validationHints);
break;
}
}
} protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter methodParam) {
int i = methodParam.getParameterIndex();
Class<?>[] paramTypes = methodParam.getMethod().getParameterTypes();
boolean hasBindingResult = (paramTypes.length > (i + 1) && Errors.class.isAssignableFrom(paramTypes[i + 1]));
return !hasBindingResult;
}
}

参数校验切面:

package com.baidu.waimai.springext;

import com.baidu.waimai.validator.ParameterBindingResult;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindException;
import org.springframework.validation.Errors;
import org.springframework.validation.beanvalidation.SpringValidatorAdapter; import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import javax.validation.executable.ExecutableValidator;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set; @Aspect
@Component
public class ControllerParameterValidateAspect { @Autowired
private Validator validator; // 需要注入ExtendValidator @Pointcut("execution(public * com.baidu.waimai.controller.*.*(..))")
private void anyControllerMethod() {}//定义一个切入点 @Around("anyControllerMethod()")
public Object validateParameters(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
// 只有MethodSignature才能获取到方法,才能获取参数列表中的注解
if (validator != null && validator instanceof ExecutableValidator &&
joinPoint.getSignature() instanceof MethodSignature) {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); ParameterBindingResult errors = new ParameterBindingResult(
joinPoint.getTarget(), method, args, joinPoint.getTarget().toString()); new ExtendSpringValidatorAdapter(validator)
.validateParameters(errors, joinPoint.getTarget(), method, args); if (errors.hasErrors()) {
throw new BindException(errors);
}
} return joinPoint.proceed(args);
} private static class ExtendSpringValidatorAdapter extends SpringValidatorAdapter {
public ExtendSpringValidatorAdapter(Validator targetValidator) {
super(targetValidator);
} public <T> void validateParameters(Errors errors, T obj, Method method, Object[] params, Class... classes) {
ExecutableValidator validator = (ExecutableValidator)unwrap(Validator.class);
Set<ConstraintViolation<Object>> constraintViolations =
validator.validateParameters(obj, method, params, classes);
processConstraintViolations(constraintViolations, errors);
}
}
}

拓展校验器,让LocalValidatorFactoryBean拥有ExecutableValidator的功能:

package com.baidu.waimai.springext;

import org.springframework.validation.Errors;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import javax.validation.ConstraintViolation;
import javax.validation.executable.ExecutableValidator;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.Set; public class ExtendValidator extends LocalValidatorFactoryBean implements ExecutableValidator { @Override
protected void processConstraintViolations(Set<ConstraintViolation<Object>> violations, Errors errors) {
super.processConstraintViolations(violations, errors);
} @Override
public void validate(Object target, Errors errors, Object... validationHints) {
if (target instanceof Collection) {
validateCollectionHints(((Collection) target), errors, validationHints);
} else {
super.validate(target, errors, validationHints);
}
} @Override
public <T> Set<ConstraintViolation<T>> validateParameters(T t, Method method, Object[] objects, Class<?>... classes) {
if (getValidator() instanceof ExecutableValidator) {
return ((ExecutableValidator) getValidator()).validateParameters(t, method, objects, classes);
}
return Collections.emptySet();
} @Override
public <T> Set<ConstraintViolation<T>> validateReturnValue(T t, Method method, Object o, Class<?>... classes) {
if (getValidator() instanceof ExecutableValidator) {
return ((ExecutableValidator) getValidator())
.validateReturnValue(t, method, o, classes);
}
return Collections.emptySet();
} @Override
public <T> Set<ConstraintViolation<T>> validateConstructorParameters(Constructor<? extends T> constructor, Object[] objects, Class<?>... classes) {
if (getValidator() instanceof ExecutableValidator) {
return ((ExecutableValidator) getValidator())
.validateConstructorParameters(constructor, objects, classes);
}
return Collections.emptySet();
} @Override
public <T> Set<ConstraintViolation<T>> validateConstructorReturnValue(Constructor<? extends T> constructor, T t, Class<?>... classes) {
if (getValidator() instanceof ExecutableValidator) {
return ((ExecutableValidator) getValidator())
.validateConstructorReturnValue(constructor, t, classes);
}
return Collections.emptySet();
} private void validateCollectionHints(Collection collection, Errors errors, Object... validationHints) {
if (collection != null) {
for (Object item : collection) {
validate(item, errors, validationHints);
}
}
}
}

增加一个参数绑定结果:

package com.baidu.waimai.springext;

import org.springframework.validation.AbstractBindingResult;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter; public class ParameterBindingResult extends AbstractBindingResult {
private Object target;
private Method method;
private Object[] args; public ParameterBindingResult(Object target, Method method, Object[] args, String targetName) {
super(targetName);
this.target = target;
this.method = method;
this.args = args;
} @Override
public Object getTarget() {
return target;
} @Override
protected Object getActualFieldValue(String field) {
for (int i = 0; i < method.getParameters().length; i++) {
Parameter parameter = method.getParameters()[i];
if (parameter.getName().equals(field)) {
return args[i];
}
}
return null;
} public Method getMethod() {
return method;
} public Object[] getArgs() {
return args;
}
}

后置bean处理器:

package com.baidu.waimai.springext;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import java.util.LinkedList;
import java.util.List; @Component
public class RequestMappingHandlerAdapterPostProcessor implements BeanPostProcessor { @Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
} @Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof RequestMappingHandlerAdapter) {
RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) bean;
List<HandlerMethodArgumentResolver> argumentResolvers = adapter.getArgumentResolvers();
List<HandlerMethodArgumentResolver> newArgumentResolvers =
processHandlerMethodArgumentResolvers(argumentResolvers);
if (newArgumentResolvers != null) {
adapter.setArgumentResolvers(newArgumentResolvers);
}
}
return bean;
} protected List<HandlerMethodArgumentResolver> processHandlerMethodArgumentResolvers(
List<HandlerMethodArgumentResolver> argumentResolvers) {
List<HandlerMethodArgumentResolver> newArgumentResolvers = new LinkedList<>(argumentResolvers); newArgumentResolvers.add(0, new JsonArgumentResolver()); // 关键,插入在前面,使得带有@JsonParam的参数被优先处理
return newArgumentResolvers;
}
}

异常处理器:

package com.baidu.waimai.springext;

import org.springframework.core.annotation.Order;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.List; @Order(1) // 排序要在SpringMvc默认异常处理器的前面
public class BindExceptionHandler implements HandlerExceptionResolver { @Override
public ModelAndView resolveException(HttpServletRequest req, HttpServletResponse resp, Object o, Exception e) {
if (e instanceof BindException) {
return resolveBindException(e);
// 在Controller的方法加上切面时抛出的异常是UndeclaredThrowableException
// 需要判断里面的是不是BindException
} else if (e instanceof UndeclaredThrowableException) {
Throwable undeclaredThrowable = ((UndeclaredThrowableException) e).getUndeclaredThrowable();
if (undeclaredThrowable instanceof BindException) {
return resolveBindException(undeclaredThrowable);
}
}
return null;
} private ModelAndView resolveBindException(Throwable e) {
BindException be = (BindException) e;
List<ObjectError> errors = be.getBindingResult().getAllErrors();
ObjectError oe = errors.get(0);
ModelAndView mav = new ModelAndView();
BindExceptionJsonView view = new BindExceptionJsonView();
mav.setView(view);
mav.addObject("Exception", e);
mav.addObject("ObjectError", oe);
return mav;
}
}

绑定异常的ModelAndView:

package com.baidu.waimai.springext;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.baidu.waimai.util.ResponseUtil;
import org.springframework.validation.ObjectError;
import org.springframework.web.servlet.view.AbstractView; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.LinkedHashMap;
import java.util.Map; public class BindExceptionJsonView extends AbstractView { @Override
protected void renderMergedOutputModel(
Map<String, Object> context, HttpServletRequest req, HttpServletResponse resp) throws Exception { String OBJECT_ERROR = "ObjectError"; resp.setContentType(ResponseUtil.JSON_CONTENT_TYPE);
PrintWriter out = resp.getWriter();
Map map = new LinkedHashMap();
ObjectError oe = (ObjectError) context.get(OBJECT_ERROR); map.put(ResponseUtil.STATUS, 0);
map.put(ResponseUtil.MSG, "参数校验错误");
map.put(OBJECT_ERROR, oe);
out.print(JSON.toJSONString(map, SerializerFeature.PrettyFormat, SerializerFeature.SortField));
}
}

SpringMVC拓展的更多相关文章

  1. SpringMVC拓展——利用maven构建springMVC项目

    一.构建项目结构 首先需要构建一个符合目录结构的maven项目 file->new->maven project,勾选 create a simple project->next / ...

  2. 解决springmvc+mybatis+mysql中文乱码问题【转】

    这篇文章主要介绍了解决java中springmvc+mybatis+mysql中文乱码问题的相关资料,需要的朋友可以参考下 近日使用ajax请求springmvc后台查询mysql数据库,页面显示中文 ...

  3. Idea SpringMVC+Spring+MyBatis+Maven调整【转】

    Idea SpringMVC+Spring+MyBatis+Maven整合   创建项目 File-New Project 选中左侧的Maven,选中右侧上方的Create from archetyp ...

  4. SpringMVC+Spring+MyBatis+Maven调整【转】

    Idea SpringMVC+Spring+MyBatis+Maven整合   创建项目 File-New Project 选中左侧的Maven,选中右侧上方的Create from archetyp ...

  5. springmvc+spring+mybatis+maven项目集成shiro进行用户权限控制【转】

    项目结构:   1.maven项目的pom中引入shiro所需的jar包依赖关系 ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ...

  6. SpringMVC框架入门配置 IDEA下搭建Maven项目

    初衷:本人初学SpringMVC的时候遇到各种稀奇古怪的问题,网上各种技术论坛上的帖子又参差不齐,难以一步到位达到配置好的效果,这里我将我配置的总结写到这里供大家初学SpringMVC的同僚们共同学习 ...

  7. Hibernate validation 注解 springmvc 验证 分组

    SpringMVC验证框架Validation特殊用法   1. 分组 有的时候,我们对一个实体类需要有多中验证方式,在不同的情况下使用不同验证方式,比如说对于一个实体类来的id来说,保存的时候是不需 ...

  8. SpringMVC注解@initbinder解决类型转换问题

    在使用SpringMVC的时候,经常会遇到表单中的日期字符串和JavaBean的Date类型的转换,而SpringMVC默认不支持这个格式的转换,所以需要手动配置,自定义数据的绑定才能解决这个问题.在 ...

  9. Spring+SpringMVC+MyBatis+easyUI整合基础篇(一)项目简介

    很久之前就打算开始写一下自己的技术博客了,实在抽不出时间所以计划一直搁置了,最近项目进度渐渐缓了下来,不那么忙了,也因此开始筹备自己的博客.说到这次博客的主角,也是无心插柳找到的,来源于两年前自己写的 ...

随机推荐

  1. QT笔记 -- (5) 实现QWidget的paintEvent函数,在widget上画背景图形

    如图,想在界面上画个弧线和直线,于是继承QWidget得到类MainContainer,并实现了paintEvent函数.然后用类MainContainer定义centralWidget. MainC ...

  2. [转载]PyCharm创建.py自动添加文件头注释

    转自:https://blog.csdn.net/qq_36482772/article/details/67218214 创建.py文件时 顺便自动添加作者.时间.文件名信息…… mac系统打开编辑 ...

  3. tinymce原装插件源码分析(三)-code

    code: 用于显示源码.主要包含一个弹框.设置显示内容以及内容的更新. function showDialog() { var win = editor.windowManager.open({ t ...

  4. HDU-4370 '0 or 1' 最短路 要考虑连通性

    题目链接:https://cn.vjudge.net/problem/HDU-4370 题意 给一个矩阵C(nn),要我们找到一个矩阵X(nn),满足以下条件: X_{12}+X_{13}+...X_ ...

  5. Linux 添加挂载硬盘(包含挂载大于2T以上硬盘)

    转自:http://blog.csdn.net/season_hangzhou/article/details/36423223 由于事先规划问题或者业务需求量激增,总会出现机器硬盘空间不够用的情况 ...

  6. mysql 密码的破解

    现在的主流的数据库一般是mysql  ,sql  server ,  oracle. 有的时候我们忘记了数据库密码的时候我们要怎么办,破解别人的数据库的密码的时候我们要怎么搞  忘记密码是一件很头痛的 ...

  7. Android自己定义处理崩溃异常

    用过安卓手机的用户以及安卓开发人员们会时长碰到程序异常退出的情况.普通用户遇到这样的情况,肯定非常恼火,甚至会骂一生垃圾软件,然后卸载掉.那么开发人员们在开发过程中遇到这样的情况给怎么办呢,当然,你不 ...

  8. hadoop-03-安装java

    hadoop-03-安装java 1,su root 2, rpm -qa|grep jdk #查看已经安装的jdk 3,rpm -e --nodeps `rpm -qa|grep jdk ` #删除 ...

  9. 移动端页面弹出对话框效果Demo

    核心思路:设置一个隐藏的(display:none;).背景偏暗的div及其子div作为对话框.当点击某处时,将此div设置为显示. 核心代码例如以下(部分js代码用于动态调整div内容的行高.这部分 ...

  10. 支持并发的httpclient(基于tcp连接池以及netty)

    闲来无事,将曾经自己写的一个库放出来吧. . 有的时候会有这样子的需求: (1)serverA通过HTTP协议来訪问serverB (2)serverA可能会并发的像B发送非常多HTTP请求 类似于上 ...