目录
2.2 原生参数&注解注入
在请求处理的过程中,完成了请求映射找到对应Controller方法后,就要完成参数注入了,参数注入包括注解类型的参数、Servlet原生API参数,以及我们自定义类型的POJO参数。本节我们讲解一下注解参数注入和原生参数注入的原理。我们先回顾一下原生参数:
原生参数
实现方式:
1.HttpServletRequest :原生Servlet请求
2. HttpServletResponse :原生Servlet响应
3. HttpSession:原生Session域
4. java.security.Principal :可表示任何实体,通常用来做安全认证和授权
5. Locale :表示地区信息
6. InputStream:字节输入流
7. OutputStream:字节输出流
8. Reader:字符输入流
9. Writer:字符输出流
注解参数
实现方式:
1.@PathVariable:路径传参,如someUrl/{paramId}绑定@Pathvariable paramId
2.@RequestHeader:请求头信息,可获取Map类型的全部请求头信息或String类型的单一信息,如 @RequestHeader(“User-Agent”) String userAgent
3.@CookieValue:Cookie的值信息,可以获取Cookie或String类型的值信息,如@CookieValue(“_ck”) Cookie cookie
4.@RequestParam:请求参数,可以灵活选取参数类型如@RequestParam(“age”) Integer age
5.@RequestBody:请求体参数,通常用来获取前端传递给后端的json字符串,如@RequestBody String jsonValue
原理解析:
前文中说过,所有请求处理的源码都点就是DispatcherServlet类的doDispatch()方法,参数注入的原理我们也从这里开始~如下图所示,首先经过请求映射过程获取到mappedHandler(如蓝框所示),之后为该Handler寻找一个适配器HandlerAdapter(如红框所示):
HandlerAdapter是处理器适配器(接口),它是请求处理部分的关键,其 设计模式是适配器模式(上文已有介绍),在处理器这里应用适配器模式的原因我们可以大胆猜测一下,就是因为处理器为了处理请求,需要调用各类的接口方法,而部分类和接口方法不兼容,因此通过适配器模式协调。
为请求映射获取到的Handler寻找一个HandlerAdapter,在本项目中HandlerAdapter有四个,如下图所示:
这四个适配器见名知意即可知道大概的意思,如第一个适配器是@RequestMapping 方法所使用的,第二个适配器是支持函数式编程的。寻找处理器适配器时依然是按顺序遍历每一个适配器,我们来看一下遍历过程:
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
Iterator var2 = this.handlerAdapters.iterator();
while(var2.hasNext()) {
HandlerAdapter adapter = (HandlerAdapter)var2.next();
//判断adapter适配器是否支持当前handler
if (adapter.supports(handler)) {
return adapter;
}
}
}
throw new ServletException("No adapter for handler [" + handler + "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}
如何判断一个adapter适配器是否支持handler方法?我们查看supports方法源码:
public final boolean supports(Object handler) {
return handler instanceof HandlerMethod && this.supportsInternal((HandlerMethod)handler);
}
判断是否支持的逻辑为当前handler是HandlerMethod类型,如请求映射后我们获得了RequestMappingHandlerMapping,该处理器映射最终得到的handler就会封装成RequestMappingHandler,也就是RequestMappingHandlerAdapter支持的类型。
找到xxxHandlerAdapter后,就会调用适配器的.handle()方法,其内又是多级的方法调用,我们引用雷神的笔记(加一些我的注解)描述这一过程:
适配器处理方法逐级调用:
//DispatcherServlet——doDispatch()——handle():
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
//RequestMappingHandlerAdapter——handleInternal()
//handleInternal()方法内调用——invokeHandlerMethod()
mav = invokeHandlerMethod(request, response, handlerMethod); //其内封装了参数解析器和返回值处理器
//ServletInvocableHandlerMethod ——>invokeForRequest() 执行目标方法
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
//invokeForRequest中首先要获取方法的参数值——>getMethodArgumentValues()
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
我们看一下本项目中的参数解析器argumentResolvers,即参数注入的核心:
每一个argumentResolvers对应一个注解,看到这里大家应该明白了,其实注解参数和Servlet原生API参数的解析原理是一样的,都是通过对应不同的参数解析器来调用其resolve()方法解析的。这里我们以表格形式列出一些常用的参数解析器功能:
参数解析器的实质是一个接口HandlerMethodArgumentResolver,里面包含两个接口方法首先通过supportsParameter判断是否支持这种参数,如果支持的话调用resolveArgument来解析。
回到源码中适配器方法的逐级调用,最终是通过invokeForRequest()真正调用目标方法:
@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
//通过该方法封装参数
Object[] args = this.getMethodArgumentValues(request, mavContainer, providedArgs);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Arguments: " + Arrays.toString(args));
}
return this.doInvoke(args);
}
其中,通过调用getMethodArugumentValues()封装方法的参数,之后就将参数传入目标方法,通过反射doInvoke调用了。我们继续进入方法源码:
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
MethodParameter[] parameters = getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
}
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
//遍历所有参数解析器判断是否支持解析该方法参数
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
//若支持则调用对应参数解析器的解析方法,解析参数值
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
}
catch (Exception ex) {
// Leave stack trace for later, exception may actually be resolved and handled...
if (logger.isDebugEnabled()) {
String exMsg = ex.getMessage();
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
logger.debug(formatArgumentError(parameter, exMsg));
}
}
throw ex;
}
}
return args;
}
上述方法中首先通过==getMethodParameters()==获取到Controller方法的方法参数数组parameters,其内包含每一个参数的索引位置、类型、标注的注解等;然后创建了一个Object数组,遍历parameters进行初始化、使用名称发现器确定参数名,关键步骤是 通过supportsParameter()遍历所有参数解析器,挨个调用supportsParameter()方法,判断是否存在支持解析该方法参数的参数解析器。若存在则找到对应参数解析器并调用其resolveArgument()方法解析参数(并放入缓存)。 supportsParameter()遍历参数解析器的源码:
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = (HandlerMethodArgumentResolver)this.argumentResolverCache.get(parameter);
if (result == null) {
Iterator var3 = this.argumentResolvers.iterator();
while(var3.hasNext()) {
HandlerMethodArgumentResolver resolver = (HandlerMethodArgumentResolver)var3.next();
//确认支持该参数后,会把参数和对应参数解析器放入缓存
if (resolver.supportsParameter(parameter)) {
result = resolver;
this.argumentResolverCache.put(parameter, resolver);
break;
}
}
}
return result;
}
确认支持该方法参数的参数解析器后,调用其解析方法,解析过程比较繁琐,依次解析参数名和参数的值,解析参数值时又用到底层的BeanExpressionResolver等,我们不做深究。只需知道,最终解析是调用确定的HandlerMethodArgumentResolver的resolveArgument方法即可。
需要补充一点的是,当我们给方法中注入复杂参数时(如Model和Map),map、model里面的数据会被放在request的请求域,相当于request.setAttribute。我们再讲解一下SpringBoot中是如何将他们放入到请求域中的呢?其具体实现原理在后文响应处理中讲解,这里做一下简单说明。其实也是通过参数解析器实现的,只不过Model和Map使用的是Model/MapMethodProcessor解析器,他们的resolveArgument()方法比较特殊,我们来看一下:
处理时会返回mavContainer.geModel(),即ModelAndMapContainer,查看该类的getModel()发现最终返回的是BindingAwareModelMap(),该类既是Model也是Map
private final ModelMap defaultModel = new BindingAwareModelMap();
将携带数据的BindingAwareModelMap封装到目标方法中,并doInvoke()执行目标方法。特殊的地方在于,执行完目标方法后,BindingAwareModelMap的值会保存在ModelAndMapContainer(mavContainer)中,并在处理返回结果时将mavContainer传入,具体方法是通过processDispatchResult()
简单总结:复杂参数使用xxxMethodProcessor解析,其resolveArgument()较为特殊,会返回一个mavContainer.getModel()方法,Map/Model方法会封装在Model中,ModelAndViewContainer封装成ModelAndView又层层包装为mergedModel(本质是Map<String,Object>),在响应处理中有一步渲染视图,渲染视图时会将mergedModel中的每一个(k,v)数据放在Request请求域中。
参数解析过程总结:
SpringMVC功能的起点都是doDispatch()方法
请求映射:获取匹配当前请求的Handler处理器(mappedHandler)
寻找匹配的适配器:遍历所有适配器HandlerAdapter(适配器设计模式),并调用其support()方法看是否支持处理当前handler()方法(support源码中判断当前handler是否为HandlerMethod类型)
调用适配器的handle方法:找到支持的适配器HandlerAdapter并调用其ha.handle()方法
方法逐级调用:handle()——>handleInternal()——>invokeHandlerMethod()*
(该方法封装了默认的参数解析器和返回值处理器)——>invokeForRequest()(获取方法参数值后通过反射调用目标方法)
——>getMethodArgumentValues()(参数解析核心方法)
获取目标方法参数数组:getMethodParameters()获取到Controller方法的方法参数数组parameters,包含每一个参数的索引位置、类型、标注的注解
遍历参数解析器判断是否支持解析当前参数:针对每个参数遍历所有参数解析器,挨个调用HandlerMethodArgumentResolver的supportsParameter()方法,判断是否存在支持解析该参数的参数解析器。
调用支持的参数解析器的解析方法:若存在则找到对应参数解析器并调用其resolveArgument()方法解析参数(并放入缓存)
简言之:通过mappedHandler找出匹配的adapter,然后把参数封装成数组后,对每个元素找出匹配的resolver(根据参数加的注解例如@PathVariable找出匹配的)。通过解析器获得参数中的数值,最后再通过doinvoke把参数和参数名绑定为k:v的格式返回。
2.3 请求处理-【源码分析】-Model、Map原理
2.2是说的参数是原生参数和注解参数,有一个细节点没有提到但是个人觉得还是挺重要的。2.2所获取的数据都是model,doDispatch 不仅仅完成对参数的解析后封装到对应参数名的k:v中。还会完成对视图view的解析(仅仅在有view的时候)
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
handle获取model和view的数据,model数据封装为k:v形式,view直接就是值没有为null(view都没解析当然是null)。获取数据后,应该注意一点当视图view为null的时候,mv返回值是null/******************/ 和view没有关系,ha.handle里面不处理view,是通过returnvalue来判断是否为null
。。。边debug边写的,反复试错。。一句话总结:
1.ha.handle是处理view的,根据returnvalue的值,赋值给view,如下代码所示
2.mv的值是取决于mav的,但是mav值由多种因素相关,我只深究了mavContainer.isRequestHandled()是根据view是否为null决定mav的值,其余的今天不想看了。
上代码:
--------------------1-------------------
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
if (returnValue instanceof CharSequence) {
String viewName = returnValue.toString();
mavContainer.setViewName(viewName);
if (isRedirectViewName(viewName)) {
mavContainer.setRedirectModelScenario(true);
}
}
else if (returnValue != null) {
// should not happen
throw new UnsupportedOperationException("Unexpected return type: " +
returnType.getParameterType().getName() + " in method: " + returnType.getMethod());
}
}
-----------------2-------------------------
@Nullable
private ModelAndView getModelAndView(ModelAndViewContainer mavContainer,
ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception {
modelFactory.updateModel(webRequest, mavContainer);
if (mavContainer.isRequestHandled()) {
return null;
}
ModelMap model = mavContainer.getModel();
ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, mavContainer.getStatus());
if (!mavContainer.isViewReference()) {
mav.setView((View) mavContainer.getView());
}
if (model instanceof RedirectAttributes) {
Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
if (request != null) {
RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
}
}
return mav;
}
-------------------
protected Object doInvoke(Object... args) throws Exception {
Method method = getBridgedMethod();
try {
if (KotlinDetector.isSuspendingFunction(method)) {
return CoroutinesUtils.invokeSuspendingFunction(method, getBean(), args);
}
return method.invoke(getBean(), args);
//这里挺有趣的,知道是反射,debug的时候直接就进入自己写的controller中了
}
catch (IllegalArgumentException ex) {
....
}
}
为啥突然提到doInvoke?因为,returnValue的值由它返回。
if (mavContainer.isRequestHandled()) {
return null;
}
mavContainer最终会变成mv,这个不必太过纠结。当view是null,这种情况下不用跳转页面,request handled directly。
当然你要是纠结,我也满足你。mavContainer中显然由view和model的数据,当view数据为null时:(下面的方法返回值不同,也不同,这里返回值是就是参数名参数的键值对,所以走这个,有页面跳转的返回值是forward:/controller中的mapper url)
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
mavContainer.setRequestHandled(true);
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
// Try even with null return value. ResponseBodyAdvice could get involved.
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
最终会走到上面这个地方,当mavContainer.setRequestHandled(true);执行完成后,直接激活request handled directly!!!
你又问怎么走到这?我直接贴源码注释:
iterate over registered HandlerMethodReturnValueHandlers and invoke the one that supports it.
Throws:
IllegalStateException – if no suitable HandlerMethodReturnValueHandler is found
虽然是直接处理,但是也是由对应返回值的handler的——RequestResponseMethodProcessor,找不到support的handler直接就抛出异常了。
下一步就是这个(根据mv是否为null进行一系列操作,包含下面所说的写进域里面):
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
为什么特地谈一下model和map的数据类型。因为,model和map的数据获得后会加入request域中,下面为关键处的源码。
public class InternalResourceView extends AbstractUrlBasedView {
@Override//该方法在AbstractView,AbstractUrlBasedView继承了AbstractView
public void render(@Nullable Map<String, ?> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
...
Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
prepareResponse(request, response);
//看下一个方法实现
renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
}
@Override
protected void renderMergedOutputModel(
Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
// Expose the model object as request attributes.
// 暴露模型作为请求域属性
exposeModelAsRequestAttributes(model, request);//<---重点
// Expose helpers as request attributes, if any.
exposeHelpers(request);
// Determine the path for the request dispatcher.
String dispatcherPath = prepareForRendering(request, response);
// Obtain a RequestDispatcher for the target resource (typically a JSP).
RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
...
}
//该方法在AbstractView,AbstractUrlBasedView继承了AbstractView
protected void exposeModelAsRequestAttributes(Map<String, Object> model,
HttpServletRequest request) throws Exception {
model.forEach((name, value) -> {
if (value != null) {
request.setAttribute(name, value);
}
else {
request.removeAttribute(name);
}
});
}
}
2.4 自定义类型参数处理
首先我们来看一下应用场景,如果我们希望前端提交的信息直接与我们自定义的Bean对象绑定,并自动完成属性注入,SpringBoot能否自动实现呢?如下图所示:
/**
* 姓名: <input name="userName"/> <br/>
* 年龄: <input name="age"/> <br/>
* 生日: <input name="birth"/> <br/>
* 宠物姓名:<input name="pet.name"/><br/>
* 宠物年龄:<input name="pet.age"/>
*/
@Data
public class Person {
private String userName;
private Integer age;
private Date birth;
private Pet pet;
}
@Data
public class Pet {
private String name;
private String age;
}
在正式解析原理之前,我们要明确一点,自定义类型参数封装注入的前期流程和一般参数是一样的,只是采用的参数解析器不同。
原理解析:
自定义类型参数所使用的参数解析器是ServletModelAttributeMethodProcessor,我们依然关注该参数解析器的两个接口方法,首先看一下它的supportsParameter()方法,查看它支持处理哪种类型的参数:
public boolean supportsParameter(MethodParameter parameter) {
//关注最后一个判断是否是简单属性,非简单属性则返回true代表能处理
return parameter.hasParameterAnnotation(ModelAttribute.class) || this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType());
}
该方法的逻辑为,首先判断是否标注了==@ModelAttribute==注解,如果没标注并且注解不是必须的,则判断是否是简单属性,非简单属性则返回true代表能处理。因此,其supportsParameter()表示该参数解析器(实际上包括ModelAttributeMethodProcessor父类下的所有子类)可以处理自定义类型参数。具体简单类型包括哪些,我们也可以查看源码确认:
public static boolean isSimpleValueType(Class<?> type) {
return (Void.class != type && void.class != type &&
(ClassUtils.isPrimitiveOrWrapper(type) ||
Enum.class.isAssignableFrom(type) ||
CharSequence.class.isAssignableFrom(type) ||
Number.class.isAssignableFrom(type) ||
Date.class.isAssignableFrom(type) ||
Temporal.class.isAssignableFrom(type) ||
URI.class == type ||
URL.class == type ||
Locale.class == type ||
Class.class == type));
}
显然我们自定义类型不属于上述简单类型,因此判断可解析。接下来我们关注它的另一个接口方法resolveArgument()是如何进行解析的:
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer");
Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory");
String name = ModelFactory.getNameForParameter(parameter);
ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
if (ann != null) {
mavContainer.setBinding(name, ann.binding());
}
Object attribute = null;
BindingResult bindingResult = null;
if (mavContainer.containsAttribute(name)) {
attribute = mavContainer.getModel().get(name);
}
else {
// Create attribute instance(核心逻辑在这开始!)
//这里创建了一个初始为空的自定义类型的实例(空Person)
try {
attribute = createAttribute(name, parameter, binderFactory, webRequest);
}
catch (BindException ex) {
if (isBindExceptionRequired(parameter)) {
// No BindingResult parameter -> fail with BindException
throw ex;
}
// Otherwise, expose null/empty value and associated BindingResult
if (parameter.getParameterType() == Optional.class) {
attribute = Optional.empty();
}
bindingResult = ex.getBindingResult();
}
}
if (bindingResult == null) {
// Bean property binding and validation;
// skipped in case of binding failure on construction.
//创建数据绑定器,将请求中传入的自定义类型对象参数传入到创建的空attribute中
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
if (!mavContainer.isBindingDisabled(name)) {
bindRequestParameters(binder, webRequest);
}
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
}
// Value type adaptation, also covering java.util.Optional
if (!parameter.getParameterType().isInstance(attribute)) {
attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
}
bindingResult = binder.getBindingResult();
}
// Add resolved attribute and BindingResult at the end of the model
Map<String, Object> bindingResultModel = bindingResult.getModel();
mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel);
return attribute;
}
首先尝试获取@ModelAttribute注解,若存在则将注解内容绑定到mavContainer中。若不存在,则会通过createAttribute创建一个空的自定义类型对象(如创建一个空Person对象)attribute = createAttribute(name, parameter, binderFactory, webRequest)。之后,创建了一个数据绑定器WebDataBinder,它的作用就是将请求参数的值绑定到attribute中,进而绑定到JavaBean内。
我们Debug看一下WebDataBinder的结构如下图所示:
WebDataBinder工作原理:WebDataBinder实际上是通过其转换服务conversionService中的诸多转换器converters将请求数据转换成指定类型的。为什么有这么多转换器呢?这是因为,传输中我们默认使用的是HTTP协议,传输的数据默认是字符串类型,需要通过调用ConversionService里的某一个converter方法将协议中的id之类的数据转成Integer等类型。因此GenericConversionService就是在设置每一个值的时候调用canService()方法,该方法遍历所有converter哪个可以将当前数据类型(如请求携带的字符串类型参数ID,值为“10”)转换到指定的类型(如JavaBean中的Integer类型id,值为10),转换后经过复杂的层层封装和反射工具,最终调用自定义bean对象的set方法为对应属性赋值。
设计模式:策略模式:
我们在ArgumentsResolver、WebDataBinder以及ReturnValueHandler(下一章讲)中都见到了遍历所有底层组件(分别遍历了解析器、转换器、处理器)看谁能执行当前处理(处理参数、处理类型转换、处理返回值),就调用对应类的处理方法。这其实是典型的策略模式。此外,接口的设计也是典型的策略模式,不同应用对象实现同一行为可采用不同方式。其基本概念如下:
策略模式
● 背景:在不同场景下使用不同的方法解决同一问题。
● 概念:属于对象的行为模式。其用意是针对一组算法,将每一个算法封装到具有共同>接口的独立的类中,从而使得它们可以相互替换。简单的说,策略模式定义一系列的算法,把每一个算法封装起来, 并且使它们可相互替换
● 实现:策略模式把对象本身和运算规则区分开来,因此我们整个模式也分为三个部分:
1.环境类(Context):用来操作策略的上下文环境。
2.抽象策略类(Strategy):一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
3.具体策略类(ConcreteStrategy):具体的策略实现。
原理流程图: