1, Inheritance structure and component initialization of dispatcher Servlet
1.1 dispatcher servlet genealogy
The essence of spring MVC is a Servlet, which inherits from HttpServlet. Spring MVC provides three levels of servlets: HttpServletBean, FrameworkServlet and DispatchServlet, which inherit from each other. HttpServletBean directly inherits from HttpServlet of Java. HttpServletBean is used to set the parameters configured in the Servlet to the corresponding properties. The FrameworkServlet initializes the WebApplicationContext used in spring MVC. The nine components specifically handling requests are initialized in the DispatchServlet (described below). The entire Servlet inheritance structure is as follows:
It can be seen that there are five classes in the inheritance structure of the Servlet. Genericservlet and HttpServlet are in Java (for the analysis of Java Servlet, see JavaWeb -- parsing Servlet ), no need to talk about it. The remaining three classes implement three interfaces directly.
- ApplicationContextAware: in spring, xxxAware means that you can sense XXX. The popular explanation is: if you want to use something of spring in a class, you can tell spring by implementing the xxxAware interface. Spring will send it to you when it sees it, and the only way to accept it is through the only way to implement the interface, set XXX. For example, if a class wants to use the current ApplicationContext, it only needs to implement the ApplicationContextAware interface, and then implement the only method void serApplicationContext(ApplicationContext applicationContext) in the interface. Spring will automatically call this method to pass the ApplicationContext to us, and we just need to accept it.
- EnvironmentAble: environmentAble contains only one method to define getEnvironment, through which you can get the environment variable object. We can use EnvironmentCapable with EnvironmentAware interface
● HttpServletBean
HttpServletBean is a simple extension of HttpServlet abstract class. HttpServletBean overrides the no parameter init method in the parent class, and sets the configuration information in ServletConfig to the subclass object, such as DispatcherServlet.
● FrameworkServlet
The FrameworkServlet is a basic class in the Spring Web framework, which creates a container at initialization time. At the same time, this class overrides methods such as doGet and doPost, and delegates all types of requests to the doService method for processing. doService is an abstract method, which needs to be implemented by subclasses.
● DispatcherServlet
The main responsibility of dispatcher servlet is believed to be clear, that is, to coordinate the work of various components. In addition, the dispatcher servlet has another important thing to do, which is to initialize various components, such as HandlerMapping, HandlerAdapter, etc
1.2 the dispatcher servlet has several attributes of reference type: nine components of spring MVC and their initialization
When spring MVC is working, the key points are all completed by these components.
Common point: the nine components are all interfaces. The advantage is that the interface is the specification, which provides a very strong scalability.
/** File upload parser */ private MultipartResolver multipartResolver; /** Region information resolver, related to internationalization */ private LocaleResolver localeResolver; /** Theme resolver: powerful theme effect change */ private ThemeResolver themeResolver; /** Handler Mapping information */ private List<HandlerMapping> handlerMappings; /** HandlerAdapter Adapters */ private List<HandlerAdapter> handlerAdapters; /** SpringMVC Powerful exception resolution function */ private List<HandlerExceptionResolver> handlerExceptionResolvers; /** RequestToViewNameTranslator */ private RequestToViewNameTranslator viewNameTranslator; /** FlashMap+Manager:SpringMVC The function of redirection carrying data in */ private FlashMapManager flashMapManager; /** view resolver */ private List<ViewResolver> viewResolvers;
1.2.1 initialization details of nine major components
This method is overridden. This method is called as soon as the Spring IOC container is started.
@Override protected void onRefresh(ApplicationContext context) { initStrategies(context); }
Where nine components in dispatcher servlet are initialized: initStrategis() method
protected void initStrategies(ApplicationContext context) { initMultipartResolver(context); initLocaleResolver(context); initThemeResolver(context); initHandlerMappings(context); initHandlerAdapters(context); initHandlerExceptionResolvers(context); initRequestToViewNameTranslator(context); initViewResolvers(context); initFlashMapManager(context); }
Why not write the specific implementation of initStrategies directly to onRefresh? Isn't this initStrategies redundant? In fact, this is mainly because of layering. onRefresh is used to refresh containers and initStrategies is used to initialize some policy components. If you write the code in initStrategies directly into onRefresh, it will have no impact on the operation of the program. However, if you want to add other functions in onRefresh, you will not write a method separately. However, this is not the most important. More importantly, if you need to call the initStrategies method elsewhere (if you need to To modify some policies for hot deployment), but initStrategies are not independent, only onRefresh can be called, so it must be troublesome to add new functions in onRefresh. In addition, writing initStrategies alone can also be overridden by subclasses, and new patterns can be used for initialization.
For example, initialize HandlerMapping:
private void initHandlerMappings(ApplicationContext context) { this.handlerMappings = null; //detectAllHandlerMappings defaults to true //You can modify the default configuration of some properties of the dispatcher servlet in web.xml //Modification of < init param > in < servlet > in project web.xml if (this.detectAllHandlerMappings) { // Find all HandlerMappings in the ApplicationContext, including ancestor contexts. //Use Bean factory to find all the children of HandlerMapping.class Map<String, HandlerMapping> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false); if (!matchingBeans.isEmpty()) { this.handlerMappings = new ArrayList<HandlerMapping>(matchingBeans.values()); // We keep HandlerMappings in sorted order. OrderComparator.sort(this.handlerMappings); } } else { try { //Otherwise, find HandlerMapping with id of handler? Mapping? Bean? Name in IOC container //public static final String HANDLER_MAPPING_BEAN_NAME = "handlerMapping"; HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class); this.handlerMappings = Collections.singletonList(hm); } catch (NoSuchBeanDefinitionException ex) { // Ignore, we'll add a default HandlerMapping later. } } // Ensure we have at least one HandlerMapping, by registering // a default HandlerMapping if no other mappings are found. //If it is always null, the default if (this.handlerMappings == null) { this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class); if (logger.isDebugEnabled()) { logger.debug("No HandlerMappings found in servlet '" + getServletName() + "': using default"); } } }
Initialization is divided into two steps: first, use context.getBean to search the container according to the name or type of registration (in this case, "handleMapping" name or HandleMapping.class type). Therefore, in the configuration file of spring MVC, you only need to configure components of corresponding types, and the container can find them. If not, call getDefaultStrategies method to get the default component by type. Note that the context here specifies the WebApplicationContext created in the FrameworkServlet, not the ServletContext. Here's how getDefaultStrategies get the default components.
getDefaultStrategies() method:
@SuppressWarnings("unchecked") protected <T> List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) { String key = strategyInterface.getName(); String value = defaultStrategies.getProperty(key); if (value != null) { String[] classNames = StringUtils.commaDelimitedListToStringArray(value); List<T> strategies = new ArrayList<T>(classNames.length); for (String className : classNames) { try { Class<?> clazz = ClassUtils.forName(className, DispatcherServlet.class.getClassLoader()); Object strategy = createDefaultStrategy(context, clazz); strategies.add((T) strategy); } catch (ClassNotFoundException ex) { throw new BeanInitializationException( "Could not find DispatcherServlet's default strategy class [" + className + "] for interface [" + key + "]", ex); } catch (LinkageError err) { throw new BeanInitializationException( "Error loading DispatcherServlet's default strategy class [" + className + "] for interface [" + key + "]: problem with class file or dependent class", err); } } return strategies; } else { return new LinkedList<T>(); } }
ClassUtils.forName is the actual method to create in getDefaultStrategies. It needs className as the parameter, so the most important thing is how to get className. If you find the source of className, you can understand the default initialization method. className comes from classNames, classNames comes from value, and value comes from defaultStrategies.getProperty(key). So the key point is in defaultStrategies.
defaultStrategies is a static attribute, which is initialized in the static block:
private static final Properties defaultStrategies; static { // Load default strategy implementations from properties file. // This is currently strictly internal and not meant to be customized // by application developers. try { // rivate static final String DEFAULT_STRATEGIES_PATH = "DispatcherServlet.properties"; ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class); defaultStrategies = PropertiesLoaderUtils.loadProperties(resource); } catch (IOException ex) { throw new IllegalStateException("Could not load 'DispatcherServlet.properties': " + ex.getMessage()); } }
In the same directory of DispatcherServlet.class, you can find DispatcherServlet.properties. In this file, you can find the default values of nine components. The default values of HandlerMapping are:
org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\ org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping
Initialization summary of components:
Some components are found in the container by using the type, and some by using the ID;
Go to the container to find this component. If not, use the default configuration;
2, The doDispatch() method of DispatchServlet resolves - handles requests
Send request
The request processing in Spring MVC is mainly in the DispatchServlet, but the framework servlet of its upper layer also does some work. First, it forwards all kinds of requests to the processRequest method, and then does three things in this method: ① calling the doService template method to process the request specifically, and the doService method is implemented in the DispatchServlet; ② putting the lo of the current request CallContext and ServletRequestAttributes are set to LocaleContextHolder and RequestContextHolder before the request is processed, and are recovered after the request is processed; ③ after the request is processed, a message of ServletRequestHandledEvent type is published.
It can be seen from the analysis that the entry method for processing in DispatcherServlet should be doService, but doService does not directly process it, but instead gives it to do dispatch for specific processing. Before doDispatch processing, doService does some processing: set webApplicationContext,localeResolver, themeResolver, themeSource, FlashMap and FlashMapManager to the property of request for easy use, and then submit the request to doDispatch method for specific processing.
doDispatch source code analysis:
Transfer failureRe uploadcancelprotected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { //The request used in the actual processing. If it is not an upload request, the received request is used directly. Otherwise, it is encapsulated as an upload type request HttpServletRequest processedRequest = request; //Processor chain to process requests HandlerExecutionChain mappedHandler = null; //Whether to upload the requested flag boolean multipartRequestParsed = false; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try { ModelAndView mv = null; Exception dispatchException = null; try { //1. Check whether the file upload request processedRequest = checkMultipart(request); multipartRequestParsed = processedRequest != request; // Determine handler for the current request. //2. Find the type to process according to the current request address //The getHandler method resolves as follows: the method returns the HandlerExecutionChain mappedHandler = getHandler(processedRequest); //3. If no processor (controller) can handle the request, 404 or throw an exception if (mappedHandler == null || mappedHandler.getHandler() == null) { noHandlerFound(processedRequest, response); return; } // Determine handler adapter for the current request. //4. Get the adapter that can execute all methods of this class (annotation method handler adapter) //mappedHandler is known to be of type HandlerExecutionChain. When there is one attribute, Handler is the Handler HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // Process last-modified header, if supported by the handler. String method = request.getMethod(); //Last modified to process GET and HEAD requests boolean isGet = "GET".equals(method); if (isGet || "HEAD".equals(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (logger.isDebugEnabled()) { String requestUri = urlPathHelper.getRequestUri(request); logger.debug("Last-Modified value for [" + requestUri + "] is: " + lastModified); } if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return; } } if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } try { //Method to process (controller) is called //Controller Handler // Actually invoke the handler. //5. The adapter executes the method, takes the return value of the target method after execution as the view name, and saves the settings to ModelAndView //No matter how the target method is written, the final adapter will encapsulate the executed information into ModelAndView after execution mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); } finally { if (asyncManager.isConcurrentHandlingStarted()) { return; } } //If there is no view name, set a default view name applyDefaultViewName(request, mv); mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception ex) { dispatchException = ex; } //Forward to target page //6. After the final execution of the method, the ModelAndView is forwarded to the corresponding page, and the data in the ModelAndView can be retrieved from the request domain processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } catch (Exception ex) { triggerAfterCompletion(processedRequest, response, mappedHandler, ex); } catch (Error err) { triggerAfterCompletionWithError(processedRequest, response, mappedHandler, err); } finally { if (asyncManager.isConcurrentHandlingStarted()) { // Instead of postHandle and afterCompletion mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); return; } // Clean up any resources used by a multipart request. if (multipartRequestParsed) { cleanupMultipart(processedRequest); } } }
Processing flow of the doDispatch() method:
1) All requests come, dispatcher servlet receives the request
2) Call the doDispatch() method for processing
(1) getHandler(): find the target processor class (processor) that can handle the request according to the current request address
Detailed description: find the mapping information of the request in HandlerMapping according to the current request, and obtain the target processor class
(2) getHandlerAdapter(): get the adapter that can execute this processor method according to the current processor class
Details: according to the current processor class, find the HandlerAdapter of the current class
(3) use the annotation method handler adapter just obtained to execute the target method
(4) the target method returns a ModelAndView object
(5) forward the information of ModelAndView to a specific page, and retrieve the model data in ModelAndView in the request domain
In summary, the core code in doDispatch only needs four sentences. Their tasks are: ① find the Handler according to the request; ② find the corresponding HandlerAdapter according to the Handler; ③ handle the Handler with the HandlerAdapter; ④ call the processDispatchResult method to process the results after the above processing (including finding the View and rendering the output to the user). The corresponding code is as follows:
mappedHandler = getHandler(processedRequest); HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
Before going into the details of these four lines of code, three concepts need to be explained:
- Handler: that is, the processor. It is directly to the C in MVC, that is, the Controller layer. It has many specific forms. It can be a class or a method. If you can think of other forms, you can also use it. Its type is Object. All the methods marked with @ RequestMapping in the previous experiment can be regarded as a handler, as long as the request can be actually processed, it can be a handler.
- HandlerMapping: used to find the Handler. In spring MVC, many requests will be processed. Each request needs to be processed by the Handler. Which Handler will be used to process after receiving a request? This is what HandlerMapping does.
- HandlerAdapter: that is, adapter. Because the Handler in spring MVC can be in any form, as long as it can handle the request, but the structure of the processing method needed by the Servlet is stable. How to make the fixed Servlet processing method call the flexible Handler for processing? That's what HandlerAdapter does.
The popular explanation is that Handler is a tool for work, HandlerMapping is a tool found according to the work to be done, and HandlerAdapter is a person who works with tools.
2.1 getHandler() details: how to find which class to handle according to the current request?
The getHandler() method returns the target method execution chain. The source code is as follows:
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { //There are two values in HandlerMapping, one is configuration based HandlerMapping, the other is annotation based HandlerMapping //Loop to determine which handlerMap has value //If it's worth it, get it. No value, continue loop. for (HandlerMapping hm : this.handlerMappings) { if (logger.isTraceEnabled()) { logger.trace( "Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'"); } HandlerExecutionChain handler = hm.getHandler(request); if (handler != null) { return handler; } } return null; }
Handler mapping: processor mapping, which stores the mapping information of which requests each processor can handle
Why does handlerMap have value? When the IOC container starts to create the Controller object, it scans what requests each processor can handle and saves them in the handlerMap attribute of HandlerMapping. Next time the request comes, it's time to see which handler mapping has the request mapping information.
2.2 getHandlerAdapter() details: how to find the adapter of the target processor class and execute the target method with the adapter
AnnotationMethodHandlerAdapter: an adapter that can parse annotation methods. It can be used as long as there are methods in the processor class.
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException { for (HandlerAdapter ha : this.handlerAdapters) { if (logger.isTraceEnabled()) { logger.trace("Testing handler adapter [" + ha + "]"); } if (ha.supports(handler)) { return ha; } } throw new ServletException("No adapter for handler [" + handler + "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler"); }
Why not two other types of handleradapters? The first is HttpRequestHeadlerAdapter, then the supports method is called to decide whether to support handler.
public class HttpRequestHandlerAdapter implements HandlerAdapter { @Override public boolean supports(Object handler) { return (handler instanceof HttpRequestHandler); } .... }
According to the judgment, it is not supported, and then SimpleControllerHandlerAdapter. The judgment steps are the same as above:
public class SimpleControllerHandlerAdapter implements HandlerAdapter { @Override public boolean supports(Object handler) { return (handler instanceof Controller); } ... }
It's still not supported. Finally, it's AnnotationMethodHandlerAdapter. It's supported.
2.3 execution of lock to target method
How to perform the target method through reflection? First, get the method. Method calls the invoke method. How to determine the value of each parameter in the target method is the problem to be solved.
Take the updateBook method written earlier as an example:
public String updateBook( @RequestParam(value="author")String author, Map<String,Object> model, HttpServletRequest request, @ModelAttribute("book")Book book){
(1) mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); analyze the details of the execution target method (only trace the useful details):
(2) In the handle() method: return invokeHandlerMethod(request, response, handler);
(3) invokeHandlerMethod method
protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //Resolver to get method ServletHandlerMethodResolver methodResolver = getMethodResolver(handler); //Method resolver finds the real target method based on the current request address: updateBook Method handlerMethod = methodResolver.resolveHandlerMethod(request); //Create a method actuator ServletHandlerMethodInvoker methodInvoker = new ServletHandlerMethodInvoker(methodResolver); //Package native request and reponse ServletWebRequest webRequest = new ServletWebRequest(request, response); //Create an implicit model ExtendedModelMap implicitModel = new BindingAwareModelMap(); //Real execution of target method: the target method determines the parameter value during the reflection execution, and performs all operations such as modelatibutes in advance in this method Object result = methodInvoker.invokeHandlerMethod(handlerMethod, handler, webRequest, implicitModel); ModelAndView mav = methodInvoker.getModelAndView(handlerMethod, handler.getClass(), result, implicitModel, webRequest); methodInvoker.updateModelAttributes(handler, (mav != null ? mav.getModel() : null), implicitModel, webRequest); return mav; }
(4) In the invokeHandlerMethod method, the object result = methodinvoker.invokeHandlerMethod (handlermethod, handler, webrequest, implicit model) is used to execute the target method;
public final Object invokeHandlerMethod(Method handlerMethod, Object handler, NativeWebRequest webRequest, ExtendedModelMap implicitModel) throws Exception { //① Get the method to execute: public java.lang.String com.test.controller.ModelAttributeTestController.updateBook(java.lang.String,java.util.Map,javax.servlet.http.HttpServletRequest,com.test.bean.Book) Method handlerMethodToInvoke = BridgeMethodResolver.findBridgedMethod(handlerMethod); try { boolean debug = logger.isDebugEnabled(); //Gets the name of the annotation annotating SessionAttribute for (String attrName : this.methodResolver.getActualSessionAttributeNames()) { //Query the value of this name from the space stored in SessionAttribute Object attrValue = this.sessionAttributeStore.retrieveAttribute(webRequest, attrName); if (attrValue != null) { //If the value is not null, it will be put into the implicit model implicitModel.addAttribute(attrName, attrValue); } } //Find all methods annotated with @ ModelAttribute annotation for (Method attributeMethod : this.methodResolver.getModelAttributeMethods()) { Method attributeMethodToInvoke = BridgeMethodResolver.findBridgedMethod(attributeMethod); //First, the value of each parameter is determined when the ModelAttribute method is executed Object[] args = resolveHandlerArguments(attributeMethodToInvoke, handler, webRequest, implicitModel); if (debug) { logger.debug("Invoking model attribute method: " + attributeMethodToInvoke); } //Find the value value of the ModelAttribute annotation String attrName = AnnotationUtils.findAnnotation(attributeMethod, ModelAttribute.class).value(); if (!"".equals(attrName) && implicitModel.containsAttribute(attrName)) { continue; } //Use reflection to make this method accessible ReflectionUtils.makeAccessible(attributeMethodToInvoke); //Execute annotation method of model attribute in advance Object attrValue = attributeMethodToInvoke.invoke(handler, args); //If attrName is not marked, determining a value of attrName will change to return type initial lowercase if ("".equals(attrName)) { //Resolve return value type Class<?> resolvedType = GenericTypeResolver.resolveReturnType(attributeMethodToInvoke, handler.getClass()); //Determine a variable name according to the return value type. For example, the return value is book, and the variable name is book attrName = Conventions.getVariableNameForReturnType(attributeMethodToInvoke, resolvedType, attrValue); } //Put the return value of the pre run ModelAttribute into the implicit model as well if (!implicitModel.containsAttribute(attrName)) { implicitModel.addAttribute(attrName, attrValue); } } //Another function of @ ModelAttribute marked on the method is found. The return value of the method after running can be used according to the key specified by @ ModelAttribute (vlaue = "") marked on the method. If the key is not specified, the return value type is lowercase. //Analyze the parameters of the target method again Object[] args = resolveHandlerArguments(handlerMethodToInvoke, handler, webRequest, implicitModel); if (debug) { logger.debug("Invoking request handler method: " + handlerMethodToInvoke); } ReflectionUtils.makeAccessible(handlerMethodToInvoke); //Real implementation target method return handlerMethodToInvoke.invoke(handler, args); } catch (IllegalStateException ex) { // Internal assertion failed (e.g. invalid signature): // throw exception with full handler method context... throw new HandlerMethodInvocationException(handlerMethodToInvoke, ex); } catch (InvocationTargetException ex) { // User-defined @ModelAttribute/@InitBinder/@RequestMapping method threw an exception... ReflectionUtils.rethrowException(ex.getTargetException()); return null; } }
(5) Object[] args = resolveHandlerArguments(attributeMethodToInvoke, handler, webRequest, implicitModel) in the invokeHandlerMethod method; determine each parameter value used in the method runtime
//First, the value of each parameter is determined when the ModelAttribute method is executed private Object[] resolveHandlerArguments(Method handlerMethod, Object handler, NativeWebRequest webRequest, ExtendedModelMap implicitModel) throws Exception { Class<?>[] paramTypes = handlerMethod.getParameterTypes(); //Create an array as many as the number of parameters to hold the value of each parameter Object[] args = new Object[paramTypes.length]; for (int i = 0; i < args.length; i++) { MethodParameter methodParam = new MethodParameter(handlerMethod, i); methodParam.initParameterNameDiscovery(this.parameterNameDiscoverer); GenericTypeResolver.resolveParameterType(methodParam, handler.getClass()); String paramName = null; String headerName = null; boolean requestBodyFound = false; String cookieName = null; String pathVarName = null; String attrName = null; boolean required = false; String defaultValue = null; boolean validate = false; Object[] validationHints = null; int annotationsFound = 0; //Get annotation for method parameters Annotation[] paramAnns = methodParam.getParameterAnnotations(); //Find all the annotations of the parameter target method, and if there are any, parse and save the annotation information for (Annotation paramAnn : paramAnns) { //If the annotation is RequestParam, assign if (RequestParam.class.isInstance(paramAnn)) { RequestParam requestParam = (RequestParam) paramAnn; paramName = requestParam.value(); required = requestParam.required(); defaultValue = parseDefaultValueAttribute(requestParam.defaultValue()); annotationsFound++; } else if (RequestHeader.class.isInstance(paramAnn)) { RequestHeader requestHeader = (RequestHeader) paramAnn; headerName = requestHeader.value(); required = requestHeader.required(); defaultValue = parseDefaultValueAttribute(requestHeader.defaultValue()); annotationsFound++; } else if (RequestBody.class.isInstance(paramAnn)) { requestBodyFound = true; annotationsFound++; } else if (CookieValue.class.isInstance(paramAnn)) { CookieValue cookieValue = (CookieValue) paramAnn; cookieName = cookieValue.value(); required = cookieValue.required(); defaultValue = parseDefaultValueAttribute(cookieValue.defaultValue()); annotationsFound++; } else if (PathVariable.class.isInstance(paramAnn)) { PathVariable pathVar = (PathVariable) paramAnn; pathVarName = pathVar.value(); annotationsFound++; } else if (ModelAttribute.class.isInstance(paramAnn)) { ModelAttribute attr = (ModelAttribute) paramAnn; attrName = attr.value(); annotationsFound++; } else if (Value.class.isInstance(paramAnn)) { defaultValue = ((Value) paramAnn).value(); } else if (paramAnn.annotationType().getSimpleName().startsWith("Valid")) { validate = true; Object value = AnnotationUtils.getValue(paramAnn); validationHints = (value instanceof Object[] ? (Object[]) value : new Object[] {value}); } } //Only one of the above notes can be marked if (annotationsFound > 1) { throw new IllegalStateException("Handler parameter annotations are exclusive choices - " + "do not specify more than one such annotation on the same parameter: " + handlerMethod); } //No comments found if (annotationsFound == 0) { //To resolve common parameters, enter resolve standard argument Object argValue = resolveCommonArgument(methodParam, webRequest); // WebArgumentResolver.UNRESOLVED: parsing failed //Assignment after successful parsing if (argValue != WebArgumentResolver.UNRESOLVED) { args[i] = argValue; } //Assign default values else if (defaultValue != null) { args[i] = resolveDefaultValue(defaultValue); } else { Class<?> paramType = methodParam.getParameterType(); //Judge whether the parameter type is under Model or Map if (Model.class.isAssignableFrom(paramType) || Map.class.isAssignableFrom(paramType)) { if (!paramType.isAssignableFrom(implicitModel.getClass())) { throw new IllegalStateException("Argument [" + paramType.getSimpleName() + "] is of type " + "Model or Map but is not assignable from the actual model. You may need to switch " + "newer MVC infrastructure classes to use this argument."); } //Assign implicit model to parameter args[i] = implicitModel; } else if (SessionStatus.class.isAssignableFrom(paramType)) { args[i] = this.sessionStatus; } else if (HttpEntity.class.isAssignableFrom(paramType)) { args[i] = resolveHttpEntityRequest(methodParam, webRequest); } else if (Errors.class.isAssignableFrom(paramType)) { throw new IllegalStateException("Errors/BindingResult argument declared " + "without preceding model attribute. Check your handler method signature!"); } else if (BeanUtils.isSimpleProperty(paramType)) { paramName = ""; } else { attrName = ""; } } } if (paramName != null) { args[i] = resolveRequestParam(paramName, required, defaultValue, methodParam, webRequest, handler); } else if (headerName != null) { args[i] = resolveRequestHeader(headerName, required, defaultValue, methodParam, webRequest, handler); } else if (requestBodyFound) { args[i] = resolveRequestBody(methodParam, webRequest, handler); } else if (cookieName != null) { args[i] = resolveCookieValue(cookieName, required, defaultValue, methodParam, webRequest, handler); } else if (pathVarName != null) { args[i] = resolvePathVariable(pathVarName, methodParam, webRequest, handler); } else if (attrName != null) { WebDataBinder binder = resolveModelAttribute(attrName, methodParam, implicitModel, webRequest, handler); boolean assignBindingResult = (args.length > i + 1 && Errors.class.isAssignableFrom(paramTypes[i + 1])); if (binder.getTarget() != null) { doBind(binder, webRequest, validate, validationHints, !assignBindingResult); } args[i] = binder.getTarget(); if (assignBindingResult) { args[i + 1] = binder.getBindingResult(); i++; } implicitModel.putAll(binder.getBindingResult().getModel()); } } return args; }
2.3.1 determine the parameter type and value of the target method
1. No comment:
1) resolve common argument. In this method, resolve standard argument will be called: to determine whether the current parameter is a native API;
protected Object resolveCommonArgument(MethodParameter methodParameter, NativeWebRequest webRequest) throws Exception { // Invoke custom argument resolvers if present... if (this.customArgumentResolvers != null) { for (WebArgumentResolver argumentResolver : this.customArgumentResolvers) { Object value = argumentResolver.resolveArgument(methodParameter, webRequest); if (value != WebArgumentResolver.UNRESOLVED) { return value; } } } // Resolution of standard parameter types... Class<?> paramType = methodParameter.getParameterType(); Object value = resolveStandardArgument(paramType, webRequest); if (value != WebArgumentResolver.UNRESOLVED && !ClassUtils.isAssignableValue(paramType, value)) { throw new IllegalStateException("Standard argument type [" + paramType.getName() + "] resolved to incompatible value of type [" + (value != null ? value.getClass() : null) + "]. Consider declaring the argument type in a less specific fashion."); } return value; }
resolveStandardArgument method:
protected Object resolveStandardArgument(Class<?> parameterType, NativeWebRequest webRequest) throws Exception { HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class); //Judge whether the parameter type belongs to Request if (ServletRequest.class.isAssignableFrom(parameterType) || MultipartRequest.class.isAssignableFrom(parameterType)) { Object nativeRequest = webRequest.getNativeRequest(parameterType); if (nativeRequest == null) { throw new IllegalStateException( "Current request is not of type [" + parameterType.getName() + "]: " + request); } return nativeRequest; } //Judge whether the parameter type is under the response flag else if (ServletResponse.class.isAssignableFrom(parameterType)) { this.responseArgumentUsed = true; Object nativeResponse = webRequest.getNativeResponse(parameterType); if (nativeResponse == null) { throw new IllegalStateException( "Current response is not of type [" + parameterType.getName() + "]: " + response); } return nativeResponse; } else if (HttpSession.class.isAssignableFrom(parameterType)) { return request.getSession(); } else if (Principal.class.isAssignableFrom(parameterType)) { return request.getUserPrincipal(); } else if (Locale.class.equals(parameterType)) { return RequestContextUtils.getLocale(request); } else if (InputStream.class.isAssignableFrom(parameterType)) { return request.getInputStream(); } else if (Reader.class.isAssignableFrom(parameterType)) { return request.getReader(); } else if (OutputStream.class.isAssignableFrom(parameterType)) { this.responseArgumentUsed = true; return response.getOutputStream(); } else if (Writer.class.isAssignableFrom(parameterType)) { this.responseArgumentUsed = true; return response.getWriter(); } return super.resolveStandardArgument(parameterType, webRequest); }
2) judge whether it belongs to Model or Map. If it is directly assigned to this parameter, the implicit Model created before will be directly assigned.
else { Class<?> paramType = methodParam.getParameterType(); if (Model.class.isAssignableFrom(paramType) || Map.class.isAssignableFrom(paramType)) { if (!paramType.isAssignableFrom(implicitModel.getClass())) { throw new IllegalStateException("Argument [" + paramType.getSimpleName() + "] is of type " + "Model or Map but is not assignable from the actual model. You may need to switch " + "newer MVC infrastructure classes to use this argument."); } args[i] = implicitModel; }
3) none, see if it is a simple type; paraName
4) assign value to attrName (i.e. determine custom type parameter - detailed steps follow)
2. Note:
1) save the details of which annotation it is.
for (Annotation paramAnn : paramAnns) { //If the annotation is RequestParam, assign if (RequestParam.class.isInstance(paramAnn)) { RequestParam requestParam = (RequestParam) paramAnn; paramName = requestParam.value(); required = requestParam.required(); defaultValue = parseDefaultValueAttribute(requestParam.defaultValue()); annotationsFound++; } else if (RequestHeader.class.isInstance(paramAnn)) { RequestHeader requestHeader = (RequestHeader) paramAnn; headerName = requestHeader.value(); required = requestHeader.required(); defaultValue = parseDefaultValueAttribute(requestHeader.defaultValue()); annotationsFound++; } else if (RequestBody.class.isInstance(paramAnn)) { requestBodyFound = true; annotationsFound++; } else if (CookieValue.class.isInstance(paramAnn)) { CookieValue cookieValue = (CookieValue) paramAnn; cookieName = cookieValue.value(); required = cookieValue.required(); defaultValue = parseDefaultValueAttribute(cookieValue.defaultValue()); annotationsFound++; } else if (PathVariable.class.isInstance(paramAnn)) { PathVariable pathVar = (PathVariable) paramAnn; pathVarName = pathVar.value(); annotationsFound++; } else if (ModelAttribute.class.isInstance(paramAnn)) { ModelAttribute attr = (ModelAttribute) paramAnn; attrName = attr.value(); annotationsFound++; } else if (Value.class.isInstance(paramAnn)) { defaultValue = ((Value) paramAnn).value(); } else if (paramAnn.annotationType().getSimpleName().startsWith("Valid")) { validate = true; Object value = AnnotationUtils.getValue(paramAnn); validationHints = (value instanceof Object[] ? (Object[]) value : new Object[] {value}); } }
2) link to determine the value
if (paramName != null) { args[i] = resolveRequestParam(paramName, required, defaultValue, methodParam, webRequest, handler); } else if (headerName != null) { args[i] = resolveRequestHeader(headerName, required, defaultValue, methodParam, webRequest, handler); } else if (requestBodyFound) { args[i] = resolveRequestBody(methodParam, webRequest, handler); } else if (cookieName != null) { args[i] = resolveCookieValue(cookieName, required, defaultValue, methodParam, webRequest, handler); } else if (pathVarName != null) { args[i] = resolvePathVariable(pathVarName, methodParam, webRequest, handler); } //Determine the value of the custom parameter and assign each parameter in the request to this object else if (attrName != null) { //Web data binder WebDataBinder binder = resolveModelAttribute(attrName, methodParam, implicitModel, webRequest, handler); boolean assignBindingResult = (args.length > i + 1 && Errors.class.isAssignableFrom(paramTypes[i + 1])); if (binder.getTarget() != null) { doBind(binder, webRequest, validate, validationHints, !assignBindingResult); } args[i] = binder.getTarget(); if (assignBindingResult) { args[i + 1] = binder.getBindingResult(); i++; } implicitModel.putAll(binder.getBindingResult().getModel()); } }
3. To determine the user-defined parameters:
(1) There is no annotation for the custom type parameter:
1) check whether the native API
2) check whether the Model or Map
3) see if there are other types (SessionStatus, HTTPEntity, Errors)
4) whether it is a simple type attribute, such as Integer, String, and basic type. If it is paraName = "
5)attrName="",
(2) User defined type parameters have comments:
1) get the value of ModelAttribute and let attrName save it
If you are a custom type object, you will end up with two effects:
- If the parameter is annotated with the ModelAttribute annotation, the value value of the annotation is assigned to attrName.
- If this parameter is not annotated with the ModelAttribute annotation, the empty string '' will be assigned to attrName.
Source code resolution: the process of assigning a value to a custom parameter. Since attrName is always not null, enter else if and call the resolveModelAttribute method to determine the value of a custom type parameter:
//Determine the value of the custom parameter and assign each parameter in the request to this object else if (attrName != null) { //Web data binder WebDataBinder binder = resolveModelAttribute(attrName, methodParam, implicitModel, webRequest, handler); boolean assignBindingResult = (args.length > i + 1 && Errors.class.isAssignableFrom(paramTypes[i + 1])); if (binder.getTarget() != null) { //Binding values to objects doBind(binder, webRequest, validate, validationHints, !assignBindingResult); } args[i] = binder.getTarget(); if (assignBindingResult) { args[i + 1] = binder.getBindingResult(); i++; } implicitModel.putAll(binder.getBindingResult().getModel()); }
Determine the value of the custom type parameter:
1. If there is a value specified by the key (marked with the model attribute annotation is the vlaue specified by the annotation, and not marked is the lowercase initial of the parameter type) in the implicit model, if there is a value assigned to bindObject.
2. To determine whether the key is a SessionAttributes attribute, get it from the Session. If not, an exception will be thrown.
3. If not, create objects with reflection
private WebDataBinder resolveModelAttribute(String attrName, MethodParameter methodParam, ExtendedModelMap implicitModel, NativeWebRequest webRequest, Object handler) throws Exception { // Bind request parameter onto object... String name = attrName; if ("".equals(name)) { //If attrName is an empty string, use the lowercase initial of the parameter type as the value, for example, Book book11,name=book name = Conventions.getVariableNameForParameter(methodParam); } //Parameter type Class<?> paramType = methodParam.getParameterType(); //Target object Object bindObject; //Determine the value of the target object //1. If there is a value specified by the key (marked with the model attribute annotation is the vlaue specified by the annotation, not marked with the lowercase initial of the parameter type) in the implicit model, if there is a value assigned to bindObject if (implicitModel.containsKey(name)) { bindObject = implicitModel.get(name); } //2. To determine whether the key is a SessionAttribute attribute, get it from the Session else if (this.methodResolver.isSessionAttribute(name, paramType)) { bindObject = this.sessionAttributeStore.retrieveAttribute(webRequest, name); if (bindObject == null) { raiseSessionRequiredException("Session attribute '" + name + "' required - not found in session"); } } //3. If not, create objects with reflection else { bindObject = BeanUtils.instantiateClass(paramType); } WebDataBinder binder = createBinder(webRequest, bindObject, name); initBinder(handler, name, binder, webRequest); return binder; }
To summarize, determine the value of each parameter of the method:
1) Annotation solution: save the annotation information, and finally get the value of the annotation that should be parsed.
2) No comments:
1) see if it is a native API
2) check whether it is a Model or a Map, xxxx
3) none. See if it is a simple type, paramName
4) assign a value to attrName (if the parameter is marked with @ ModelAttribute(""), it is specified, if it is not marked, it is "")
Determine custom type parameters:
1) attrName uses the lowercase initial of the parameter type or the value of the previous @ ModelAttribute("")
2) first check whether the attrName is the key value in the implicit model. If so, get it from the implicit model
3) see if it is the attribute marked by @ SessionAttributes(value =). If it is taken from the session.
4) if not, create an object with reflection.
5) get the object created before, and use the WebDataBinder to bind each data in the request to this object
3, doDispatch() method parsing - render page
It can be concluded from the previous experiments that the view resolution of spring MVC:
1. The return value of the method after execution will be used as the page address reference, forwarded or redirected to the page.
2. The view parser may string page addresses
3.1 source code analysis
1. In the previous source code analysis, the adapter execution method is analyzed. The return value of this method is a ModelAndView type. You can know the return value of any method, and it will eventually be wrapped into a ModelAndView type.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
2. View rendering process: display the data in the domain on the page, which is used to render the model data.
//6. After the final execution of the method, the ModelAndView is forwarded to the corresponding page, and the data in the ModelAndView can be retrieved from the request domain processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
3. call the render(mv,request,reponse) render page in the processDispatchResult method.
View and ViewResolver:
The purpose of the ViewResolver (Interface) is to get the View object based on the View name (the return value of the method).
View interface:
(1) Get the View object. How to get the View object according to the return value (View name) of the method?
protected View resolveViewName(String viewName, Map<String, Object> model, Locale locale, HttpServletRequest request) throws Exception { //One of the nine components of viewResolver. The initialization process of the component is consistent with the above description //Traverse all viewresolvers for (ViewResolver viewResolver : this.viewResolvers) { //The viewResolver View resolver gets a View object based on the return value of the method View view = viewResolver.resolveViewName(viewName, locale); if (view != null) { return view; } } return null; }
resolveViewName implementation details:
@Override public View resolveViewName(String viewName, Locale locale) throws Exception { if (!isCache()) { return createView(viewName, locale); } else { Object cacheKey = getCacheKey(viewName, locale); View view = this.viewAccessCache.get(cacheKey); if (view == null) { synchronized (this.viewCreationCache) { view = this.viewCreationCache.get(cacheKey); if (view == null) { // Ask the subclass to create the View object. //Create a View object based on the return value of the method view = createView(viewName, locale); if (view == null && this.cacheUnresolved) { view = UNRESOLVED_VIEW; } if (view != null) { this.viewAccessCache.put(cacheKey, view); this.viewCreationCache.put(cacheKey, view); if (logger.isTraceEnabled()) { logger.trace("Cached view [" + cacheKey + "]"); } } } } } return (view != UNRESOLVED_VIEW ? view : null); } }
Create a View object and return the View object:
@Override protected View createView(String viewName, Locale locale) throws Exception { // If this resolver is not supposed to handle the given view, // return null to pass on to the next resolver in the chain. if (!canHandle(viewName, locale)) { return null; } // Check for special "redirect:" prefix. //If if (viewName.startsWith(REDIRECT_URL_PREFIX)) { String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length()); RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible()); return applyLifecycleMethods(viewName, view); } // Check for special "forward:" prefix. if (viewName.startsWith(FORWARD_URL_PREFIX)) { String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length()); return new InternalResourceView(forwardUrl); } // Else fall back to superclass implementation: calling loadView. //If there is no prefix, use the parent class to create a View object return super.createView(viewName, locale); }
The flow of View parser getting View object is that all configured View parsers try to get View object by View name (return value); if they can get it, they return it, and if they can't get it, they change to another View parser.
(2) Call the render method of the View object
@Override public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception { if (logger.isTraceEnabled()) { logger.trace("Rendering view with name '" + this.beanName + "' with model " + model + " and static attributes " + this.staticAttributes); } Map<String, Object> mergedModel = createMergedOutputModel(model, request, response); prepareResponse(request, response); //Render all data to output to the page renderMergedOutputModel(mergedModel, request, response); }
InternalResourceView has this method renderMergedOutputModel:
@Override protected void renderMergedOutputModel( Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { // Determine which request handle to expose to the RequestDispatcher. HttpServletRequest requestToExpose = getRequestToExpose(request); // Expose the model object as request attributes. //Expose the data of the implicit model to the request domain exposeModelAsRequestAttributes(model, requestToExpose); // Expose helpers as request attributes, if any. exposeHelpers(requestToExpose); // Determine the path for the request dispatcher. //Forwarding path String dispatcherPath = prepareForRendering(requestToExpose, response); // Obtain a RequestDispatcher for the target resource (typically a JSP). RequestDispatcher rd = getRequestDispatcher(requestToExpose, dispatcherPath); if (rd == null) { throw new ServletException("Could not get RequestDispatcher for [" + getUrl() + "]: Check that the corresponding file exists within your web application archive!"); } // If already included or response already committed, perform include, else forward. if (useInclude(requestToExpose, response)) { response.setContentType(getContentType()); if (logger.isDebugEnabled()) { logger.debug("Including resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'"); } rd.include(requestToExpose, response); } else { // Note: The forwarded resource is supposed to determine the content type itself. if (logger.isDebugEnabled()) { logger.debug("Forwarding to resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'"); } rd.forward(requestToExpose, response); } }
Why does the data in the implicit model end up in the request domain? It is called the exposeModelAsRequestAttributes method in the renderMergedOutputModel method:
protected void exposeModelAsRequestAttributes(Map<String, Object> model, HttpServletRequest request) throws Exception { for (Map.Entry<String, Object> entry : model.entrySet()) { String modelName = entry.getKey(); Object modelValue = entry.getValue(); if (modelValue != null) { request.setAttribute(modelName, modelValue); if (logger.isDebugEnabled()) { logger.debug("Added model object '" + modelName + "' of type [" + modelValue.getClass().getName() + "] to request in view with name '" + getBeanName() + "'"); } } else { request.removeAttribute(modelName); if (logger.isDebugEnabled()) { logger.debug("Removed model object '" + modelName + "' from request in view with name '" + getBeanName() + "'"); } } } }
The view parser is only to get the view object, so the view object can actually forward (put all the model data in the request domain) or redirect to the page. The view object can actually render the view.