痛点
@RequestBody接收的参数只能是一个对象,可以通过创建ViewObject对象来接收,但是对于各种不确定参数就需要非常多的ViewObject。 如果不用ViewObject,而是直接用原生Map<String,Object> map 接收,一样需要一个个取出来:
@PostMapping("login")
public Object login(@RequestBody Map<String,Object> map) {
String account = (String) map.get("account");
String pwd = (String) map.get("pwd");
...
}
目标方案
最终想要的效果是即使前端传json,后台先解析出来,再塞到方法参数中,这个过程是隐式完成,最终业务代码就可以非常简洁:
@PostMapping("login")
public Object login(String account,String pwd) {
String account = (String) map.get("account");
String pwd = (String) map.get("pwd");
...
}
个别问题Q&A
Q:如何区分POST的json和form形式的参数 A:先判断前端传的是json(通过contentType或者request.getInputStream),确认json后,解析json,然后赋值给方法的参数。同时,可以使用注解来指定(或排除)某些参数使用(或不使用)json对象赋值。
想要实现以上效果, 需要理解springmvc是哪一步获取请求体数据封装成@RequestBody参数对象, 以及其他表单参数是如何赋值,所以:
第一步:理解springmvc参数接收原理
因为springmvc本身是个庞大的架构体系,可以预知它对参数值的封装这个过程是通过某种设计模式完成的,而不是按代码顺序一步步写下来,这是读spring源码的一个方向。
从整个应用服务层面来看,请求体数据先进入tomcat(或其他),被封装形成基础request请求对象,然后经过filter过滤器,经过interceptor拦截器等,进入统一的DispatcherServlet(spring默认servlet),然后在DispatcherServlet中根据HandlerMapping找到匹配的Handler(也就是Controller中某一个方法),最后request请求对象交由匹配的这个Handler处理。
DispatcherServlet
public class DispatcherServlet extends FrameworkServlet {
--1、servlet的doService方法
@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
try {
doDispatch(request, response);
}
....
}
--2、进入doDispatch方法
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
try {
HttpServletRequest processedRequest = request;
--3、 为当前request匹配对应的Handler
HandlerExecutionChain mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
return;
}
--4、 拿到HandlerAdapter 适配器
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
--5、 先经过多个拦截器
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
--6、 实际调用handle处理(--7、 进入RequestMappingHandlerAdapter.handle()方法)
ModelAndView mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
...
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
Exception dispatchException = ex;
}
catch (Throwable err) {
Exception dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
}
RequestMappingHandlerAdapter
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {
@Override
--7、 RequestMappingHandlerAdapter.handle()方法实际调用handleInternal()如下:
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
checkRequest(request);
...
--8、 实际调用invokeHandlerMethod()
ModelAndView mav = invokeHandlerMethod(request, response, handlerMethod);
...
return mav;
}
--8、 实际调用invokeHandlerMethod()
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ServletWebRequest webRequest = new ServletWebRequest(request, response);
try {
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
--9、 创建ServletInvocableHandlerMethod,并调用invokeAndHandle()方法
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
if (this.returnValueHandlers != null) {
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
invocableMethod.setDataBinderFactory(binderFactory);
invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
modelFactory.initModel(webRequest, mavContainer, invocableMethod);
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
...
--9、 调用ServletInvocableHandlerMethod.invokeAndHandle()方法
invocableMethod.invokeAndHandle(webRequest, mavContainer);
return getModelAndView(mavContainer, modelFactory, webRequest);
}
finally {
webRequest.requestCompleted();
}
}
}
ServletInvocableHandlerMethod
public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
--9、 调用ServletInvocableHandlerMethod.invokeAndHandle()方法
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
--10、 真正的调用Controller方法并获取返回值
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
setResponseStatus(webRequest);
...
try {
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
}
}
小结:
上面三个类的代码,从DispatcherServlet 找到真正的RequestMappingHandlerAdapter 直到构造ServletInvocableHandlerMethod 来最终调用到Controller方法,这个过程都还没涉及到方法入参的封装、方法调用。直到ServletInvocableHandlerMethod.invokeForRequest() 才解析request参数封装进方法参数并调用。
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
return doInvoke(args);
}
从invokeForRequest方法可见,它完成了关键的两步:
- getMethodArgumentValues获取方法的参数
- 用方法参数去调用方法
getMethodArgumentValues 方法如下:
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
MethodParameter[] parameters = getMethodParameters();
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
}
}
return args;
}
getMethodArgumentValues 方法通过RequestResponseBodyMethodProcessor(implements HandlerMethodArgumentResolver) 来解析request中的请求体或者请求参数,并封装成Controller方法的参数返回(比如请求体中的json就会匹配到MappingJackson2HttpMessageConverter 转换器来解析,对于如何解析请求参数这个是转换器内部实现,不在这里展开说明)。
第二步:实现思路
再来看doDispatch方法,如下:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
...
--5、 先经过多个拦截器
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
--6、 实际调用handle处理(--7、 进入RequestMappingHandlerAdapter.handle()方法)
ModelAndView mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
ha.handle() 做的事情包括:
- 通过
getMethodParameters() 拿到所有参数类型,然后通过特定的MappingJackson2HttpMessageConverter 解析封装每一个参数; - 最后返回的参数
Object[] args 去doInvoke(args) 调用Controller方法。
也就是说在doDispatch方法执行拦截器的前拦截applyPreHandle 时还没进入ha.handle() 也就还没从request解析出参数,执行后拦截applyPostHandle 时方法已经调用完毕。
我们的目标是login(String account,String pwd) 也可以接收到@RequestBody 的请求体数据,我们需要修改Controller方法的参数值,所有无法通过过滤器或者拦截器来解析json注入参数!
方案一
使用AOP拦截Controller方法,也就是在spring解析出参数调用doInvoke(args) 的时候拦截,这个时侯,手动解析出json对象(默认为Map对象),通过参数名称和map对象的key值比对,生产参数集合,替换掉doInvoke(args) 中的原来参数。
方案一的实现代码
package com;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.aop.ProxyMethodInvocation;
import org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolverComposite;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
@Aspect
@Component
public class PostMethodAop {
private final HandlerMethodArgumentResolverComposite argumentResolvers;
private final Field proxyMethodInvocationField;
private final MethodParameter methodParameter;
private static void method(@RequestBody(required = false) Object params) {
}
@Autowired
public PostMethodAop(@Qualifier("requestMappingHandlerAdapter") RequestMappingHandlerAdapter requestMappingHandlerAdapter) throws NoSuchFieldException, IllegalAccessException {
Field argumentResolversField = RequestMappingHandlerAdapter.class.getDeclaredField("argumentResolvers");
argumentResolversField.setAccessible(true);
argumentResolvers = (HandlerMethodArgumentResolverComposite) argumentResolversField.get(requestMappingHandlerAdapter);
argumentResolversField.setAccessible(false);
try {
Method method = PostMethodAop.class.getDeclaredMethod("method", Object.class);
methodParameter = new MethodParameter(method, 0);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
Field field = null;
for (Field declaredField : MethodInvocationProceedingJoinPoint.class.getDeclaredFields()) {
if (ProxyMethodInvocation.class.equals(declaredField.getType())) {
field = declaredField;
break;
}
}
proxyMethodInvocationField = field;
proxyMethodInvocationField.setAccessible(true);
}
@Pointcut(value = "@annotation(org.springframework.web.bind.annotation.PostMapping)")
public void postMapping() {
}
@Before("postMapping()")
public void doBefore(JoinPoint jp) throws Throwable {
MethodInvocationProceedingJoinPoint joinPoint = (MethodInvocationProceedingJoinPoint) jp;
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
if (!HttpMethod.POST.matches(request.getMethod())) {
return;
}
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
for (Annotation[] annotations : signature.getMethod().getParameterAnnotations()) {
for (Annotation annotation : annotations) {
if (RequestBody.class.equals(annotation.annotationType())) {
return;
}
}
}
String[] parameterNames = signature.getParameterNames();
if (parameterNames.length == 0) {
return;
}
Map<String, Object> jsonBody = getRequestBody(request);
if (jsonBody == null) {
return;
}
Object[] args = new Object[parameterNames.length];
for (int i = 0; i < parameterNames.length; i++) {
if (jsonBody.containsKey(parameterNames[i])) {
args[i] = jsonBody.get(parameterNames[i]);
} else if (joinPoint.getArgs()[i] != null) {
args[i] = joinPoint.getArgs()[i];
}
}
ProxyMethodInvocation methodInvocation = (ProxyMethodInvocation) proxyMethodInvocationField.get(joinPoint);
methodInvocation.setArguments(args);
}
private Map<String, Object> getRequestBody(HttpServletRequest request) {
if (!argumentResolvers.supportsParameter(methodParameter)) {
return null;
}
Object jsonBody;
try {
jsonBody = argumentResolvers.resolveArgument(methodParameter, null, new ServletWebRequest(request), null);
} catch (Exception e) {
throw new RuntimeException(e);
}
if (jsonBody instanceof Map) {
return (Map<String, Object>) jsonBody;
}
return null;
}
}
|