原生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. CF 287(div 2) B Amr and Pins

    解题思路:一开始自己想的是找出每一次旋转所得到的圆心轨迹,将想要旋转到的点代入该圆心轨迹的方程,如果相等,则跳出循环,如果不相等,则接着进行下一次旋转.后来看了题解,发现,它的旋转可以是任意角度的,所 ...

  2. ajax返回数据时,如何将javascript值(通常为对象或数组)转为json字符串

    ajax获取值时,返回的数据为空时 alert后出现 [ ]; 用if语句判断时不为空,此时如何判断返回的数据是否为空.可将返回的值转化为json字符串. JSON.stringify() 方法用于将 ...

  3. CDR X6三折促销活动,可入

    继CDR X6双十二限量活动之后,CorelDRAW官方为庆祝2018新年新气象,折扣狂潮,又来一波.上次活动由于时间短,任务急,数量少,使得不少小伙伴抱憾而止,选择默默等待良机.现在,良机来了,即便 ...

  4. vue下assets下的静态资源和static下的静态资源的区别

    区别一(最终位置) assets文件是src下的,所以最后运行是需要进行打包,而static文件不需要打包直接放在最终的文件中了 区别二(引用方式) assets中的文件在vue中的template/ ...

  5. CentOS 7在grub rescue模式中修复系统

    安装完CentOS 7后 修改硬盘分区后,系统重启后,无法正常启动,进入grub rescue模式: 网上大多数centos grub rescue的资料应该是Centos 7之前的,其中提到的命令很 ...

  6. Markdown语法简记

    目录 一.标题 1. 六个级别的标题 2. 主.副两级标题 二.根据标题生成文档结构大纲 三.字体 1. 斜体 2. 粗体 3. 倾斜加粗 4. 行首缩进 5. 删除线 四.引用块 五.代码块 1. ...

  7. CF587F Duff is Mad(AC自动机+树状数组+分块)

    考虑两一个暴力 1 因为询问\([a,b]\)可以拆成\([1,b]\)-\([1,a-1]\)所以把询问离线,然后就是求\([1,x]\)中被\(S_i\)包含的串的数量.考虑当\([1,x-1]- ...

  8. UNIX系统高级编程——第五章-标准I/O库-总结

    基础: 标准I/O库在ANSI C中定义,可移植在不同的系统 文件指针(FILE):标准I/O库操作的不是文件描述符,而是流.FILE文件指针包含的是维护流所需的信息 通过函数fileno获取流的文件 ...

  9. CentOS 7.2 (mini) 里iptables防火墙怎么关闭?

    centos从7开始默认用的是firewalld,这个是基于iptables的,虽然有iptables的核心,但是iptables的服务是没安装的.所以你只要停止firewalld服务即可:sudo ...

  10. 在AutoLyout中动态获得cell的高度 和 autoLyout中的小随笔

    autoLyout中动态获得cell的高度和autoLyout小总结 一.在autoLyout中通过动态的方式来获取cell 的方式呢? 1.       在布局时候要有对于cell中contentV ...