SpringMVC请求源码分析

文章目录

    • 一、SpringMVC简介
      • 1. 概念
      • 2. 从Servlet到SprigMVC
      • 3. SpringMVC的XML实现
      • 4. SpringMVC的请求流程
    • 二、SpringMVC源码分析
      • 1. SpringMVC启动流程验证
      • 2. 细节补充

一、SpringMVC简介

1. 概念

官网介绍

Spring Web MVC is the original web framework built on the Servlet API and has been included in the Spring Framework from the very beginning. The formal name, “Spring Web MVC,” comes from the name of its source module (spring-webmvc), but it is more commonly known as “Spring MVC”.

Spring Web MVC是基于Servlet API构建的原始Web框架,从一开始就已包含在Spring框架中。正式名称“ Spring Web MVC”来自其源模块的名称(spring-webmvc),但它通常被称为“ Spring MVC”。

2. 从Servlet到SprigMVC

最典型的MVC就是JSP + servlet + javabean的模式。

在这里插入图片描述
传统的Servlet模式有这些弊端:

  • xml下配置servlet的映射非常麻烦 开发效率低
  • 必须要继承父类、重写方法侵入性强
  • 如果想在一个Servlet中处理同一业务模块的的功能分发给不同方法进行处理非常麻烦
  • 参数解析麻烦:单个参数(转换类型)—>pojo对象 Json文本—>pojo对象
  • 数据响应麻烦:pojo对象—>json … Content-type
  • 跳转页面麻烦, 对path的控制、 如果使用其他模板也很麻烦 、设置编码麻烦…等等…

所以SpringMVC 就是在Servlet的基础上进行了封装,帮我把这些麻烦事都给我们做了

3. SpringMVC的XML实现

首先我们需要配置web.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <!--spring 基于web应用的启动-->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <!--全局参数:spring配置文件-->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring-core.xml</param-value>
    </context-param>


    <!--配置前端控制器  、核心调度器
    加载spring容器
    -->
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring-mvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
        <async-supported>true</async-supported>
    </servlet>
    <!--/ 除了jsp所有请求都会被匹配-->
    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

</web-app>

DispatcherServlet 是 Spring MVC 框架中的核心组件,用于处理 HTTP 请求并将其分派到合适的处理器(Controller)进行处理。

  1. Servlet容器初始化: 当应用启动时,Servlet 容器(如Tomcat)会加载和初始化 DispatcherServlet。在初始化过程中,DispatcherServlet 会创建并初始化自己的 ApplicationContext,该上下文包含了整个 Spring 应用的配置信息。
  2. WebApplicationContext初始化: DispatcherServlet 内部使用了一个 WebApplicationContext,该上下文是 ApplicationContext 的子接口,专门用于 Web 应用。WebApplicationContext 的初始化会根据配置文件(如 web.xml 或者注解配置)加载相应的 Bean 定义。
  3. HandlerMapping的注册: 在 DispatcherServlet 初始化时,会注册一个或多个 HandlerMapping。HandlerMapping 的作用是根据请求的 URL 映射到具体的处理器(Controller)。
  4. HandlerAdapter的注册: HandlerAdapter 负责执行具体的处理器方法,根据请求的参数和返回值进行适当的处理。在 DispatcherServlet 初始化时,会注册多个 HandlerAdapter,每个适配器负责处理不同类型的处理器。
  5. ViewResolver的注册: ViewResolver 负责将处理器方法的逻辑视图名解析为实际的视图对象。DispatcherServlet 初始化时,会注册一个或多个 ViewResolver。
  6. 请求分发过程: 当有 HTTP 请求到达时,DispatcherServlet 会根据请求的 URL 通过注册的 HandlerMapping 找到相应的处理器。然后,使用合适的 HandlerAdapter 执行处理器方法,并得到处理结果。
  7. 视图解析和渲染: 处理器方法执行完毕后,DispatcherServlet 会将处理结果交给注册的 ViewResolver 进行解析,得到实际的视图对象。最后,使用该视图对象进行渲染,并将渲染结果返回给客户端。
  8. 拦截器的应用: DispatcherServlet 还支持拦截器的应用,拦截器可以在请求处理前、处理后或渲染视图前进行一些额外的逻辑处理。

然后创建控制器

@RestController
public class TulingController {

	@Autowired
	private TulingServiceImpl tulingServiceImpl;

	@RequestMapping(value = {"/angle"})
	public String testTuling(HttpServletRequest httpServletRequest) {
		System.out.println("URL:"+httpServletRequest.getRequestURL());
		System.out.println("URI:"+httpServletRequest.getRequestURI());
		System.out.println("contextPath:"+httpServletRequest.getContextPath());
		System.out.println("serlvetPath:"+httpServletRequest.getServletPath());
		ServletContext servletContext = httpServletRequest.getServletContext();
		tulingServiceImpl.sayHello();
		return "smlz";
	}

	@RequestMapping(value = {"/tuling"})
	public String testAngle(HttpServletRequest httpServletRequest, HttpServletResponse response) {

		ServletContext servletContext = httpServletRequest.getServletContext();
		return "smlz";
	}

	@RequestMapping("/returnJson")
	public Object returnJson() {
		Map<String,String> retMap = new HashMap<>();
		retMap.put("name","张三");
		return retMap;
	}

	@RequestMapping("/testQuestPram")
	public String testRequestParam(@RequestParam("${name}") String name) {
		System.out.println("name="+name);
		return name;
	}

	public TulingController() {
		System.out.println("TulingController 执行构造方法");
	}


	@RequestMapping("/initbinder/user")
	public User getFormatData(User user) {
		System.out.println("user:"+user.toString());
		return user;
	}

	/**
	 * 作用于单个controller
	 * WebDataBinder 的作用
	 * @param webDataBinder
	 */
	@InitBinder
	public void initWebBinderDataFormatter(WebDataBinder webDataBinder) {
		//作用一:加入类型转化器
		DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		CustomDateEditor dateEditor = new CustomDateEditor(df, true);

		webDataBinder.registerCustomEditor(Date.class,dateEditor);
	}
}

创建xml配置文件

<!--spring.xml-->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop" xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd">

    <!--扫描所有除了controller包的其他包-->
    <context:component-scan base-package="com.tuling.xml">
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>
</beans>


<!--springmvc.xml-->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!--扫描包-->
    <context:component-scan base-package="com.tuling.xml"></context:component-scan>

    <mvc:annotation-driven></mvc:annotation-driven>

    <!--默认视图解析器 -  配上前缀和后缀  简化 逻辑视图名称-->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" name="viewResolver">
        <property name="prefix" value="/WEB-INF/jsp/"></property>
        <property name="suffix" value=".jsp"></property>
    </bean>


   <!-- <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>


    <bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping" >
        <property name="mappings">
            <props>
                <prop key="/simpleController">simpleController</prop>
            </props>
        </property>
    </bean>


    <bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>
-->

    <!--<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
        <property name="messageConverters">
            <list>
                &lt;!&ndash; json转换器 属于HandlerAdapter,单独配置是没用的&ndash;&gt;
                <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
                    <property name="supportedMediaTypes">
                        <list>
                            <value>text/html;charset=UTF-8</value>
                        </list>
                    </property>
                </bean>
            </list>
        </property>
    </bean>-->
</beans>

4. SpringMVC的请求流程

Spring MVC 是围绕前端控制器模式设计的,其中:中央 Servlet DispatcherServlet 为请求处理流程提供统一调度,实际工作则交给可配置组件执行。这个模型是灵活的且开放的,我们可以通过自己去定制这些组件从而进行定制自己的工作流。

在这里插入图片描述

二、SpringMVC源码分析

1. SpringMVC启动流程验证

在这里插入图片描述
结合上面给的案例,我们进行调试,首先浏览器输入请求地址,此时方法就会进入DispatcherServlet类的doDispatch方法。这个方法的重要性和Spring中的Spring框架中的refresh方法是一样重要的,几乎所有的工作都是在这个方法中完成的。

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
		HttpServletRequest processedRequest = request;
		HandlerExecutionChain mappedHandler = null;
		boolean multipartRequestParsed = false;

		WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

		try {
			ModelAndView mv = null;
			Exception dispatchException = null;

			try {
				processedRequest = checkMultipart(request);
				multipartRequestParsed = (processedRequest != request);

				// 进行映射
				mappedHandler = getHandler(processedRequest);
				if (mappedHandler == null) {
					noHandlerFound(processedRequest, response);
					return;
				}

				// 找到最合适的HandlerAdapter
				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

				// Process last-modified header, if supported by the handler.  HTTP缓存相关
				String method = request.getMethod();
				boolean isGet = HttpMethod.GET.matches(method);
				if (isGet || HttpMethod.HEAD.matches(method)) {
					long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
					if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
						return;
					}
				}
				// 前置拦截器
				if (!mappedHandler.applyPreHandle(processedRequest, response)) {
					// 返回false就不进行后续处理了
					return;
				}

				// Actually invoke the handler.
				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

				if (asyncManager.isConcurrentHandlingStarted()) {
					return;
				}
				// 如果mv有  视图没有,给你设置默认视图
				applyDefaultViewName(processedRequest, mv);
				//后置拦截器
				mappedHandler.applyPostHandle(processedRequest, response, mv);
			}
			catch (Exception ex) {
				dispatchException = ex;
			}
			catch (Throwable err) {
				// As of 4.3, we're processing Errors thrown from handler methods as well,
				// making them available for @ExceptionHandler methods and other scenarios.
				dispatchException = new NestedServletException("Handler dispatch failed", err);
			}
			// 渲染视图
			processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
		}
		catch (Exception ex) {
			triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
		}
		catch (Throwable err) {
			triggerAfterCompletion(processedRequest, response, mappedHandler,
					new NestedServletException("Handler processing failed", err));
		}
		finally {
			if (asyncManager.isConcurrentHandlingStarted()) {
				// Instead of postHandle and afterCompletion
				if (mappedHandler != null) {
					mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
				}
			}
			else {
				// Clean up any resources used by a multipart request.
				if (multipartRequestParsed) {
					cleanupMultipart(processedRequest);
				}
			}
		}
	}

我们先看主流程,其它细节后面再讲。它执行到源码中的mappedHandler = getHandler(processedRequest);这句代码,我们进入getHandler方法。

	@Nullable
	protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		if (this.handlerMappings != null) {
			/** 拿到所有handlerMappings (容器启动阶段初始化:拿到所有实现了HandlerMapping的Bean)
			 * @see DispatcherServlet#initHandlerMappings
			 * 测试发现: 不同的HandlerMapping可以有相同path, 谁先解析到就用哪个
			 * */
			for (HandlerMapping mapping : this.handlerMappings) {
				HandlerExecutionChain handler = mapping.getHandler(request);
				if (handler != null) {
					return handler;
				}
			}
		}
		return null;
	}

这个方法首先会使用一个for循环,遍历所有的handlerMapping。他会调用每个HandlerMappinggetHandler方法进行映射,如果某个HandlerMapping映射成功了就会直接返回(可以使用@Order注解配置优先级),不会再接着映射了,返回一个处理器映射器链(为什么会是链,因为可能会有拦截器需要执行),如果没有配置HandlerMapping,就会直接返回null。

在Spring MVC中,HandlerMapping的作用是将HTTP请求的URL映射到相应的处理器(Controller)。它负责根据请求的信息,如URL和请求方法,找到合适的处理器,以便进行进一步的请求处理。不同的HandlerMapping实现支持不同的映射策略,其中RequestMappingHandlerMapping通过注解配置实现了常用的URL映射。这一过程使得开发者能够通过定义Controller类和相应的映射关系,实现对不同URL请求的处理逻辑。

继续回到doDispatch方法,下面就会执行HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());去找到合适的HandlerAdapter。我们进入该方法。

protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
		if (this.handlerAdapters != null) {
			for (HandlerAdapter adapter : this.handlerAdapters) {
				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");
	}  

首先它会遍历所有的HandlerAdapter,然后调用supports方法判断当前的HandlerAdapter是否支持。它会根据前面handlerMapping返回的Handler的具体类去调用不同的supports的实现方法,来判断。具体的作用就是根据不同的Handler去选择最合适的HandlerAdapter。继续回到doDispatch方法。然后调用下面代码。

HandlerAdapter 是 Spring MVC 中的一个关键组件,它的主要作用是负责执行处理器(Controller)方法,并处理方法的参数、返回值等。通过适配不同类型的处理器方法,HandlerAdapter 实现了框架与各种处理器之间的解耦,使得不同类型的处理器方法能够被统一地执行和处理,从而实现了灵活的方法调用和适配。

if (!mappedHandler.applyPreHandle(processedRequest, response)) {
    // 返回false就不进行后续处理了
	return;
}

上面代码就是处理前置拦截器的,首先我们先回顾一下SpringMVC的拦截器的使用方法。

Spring MVC 拦截器是一种强大的机制,允许在请求处理的不同阶段执行额外的逻辑。拦截器通常用于日志记录、权限验证、国际化等需求。下面是一个详细的案例,演示如何在 Spring MVC 中使用拦截器。

  • 创建拦截器类
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

public class MyInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        System.out.println("Interceptor: preHandle method is called");
        // 返回true表示继续执行后续拦截器和处理器,返回false则中断执行
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
            ModelAndView modelAndView) throws Exception {
        System.out.println("Interceptor: postHandle method is called");
        // 在处理器方法执行完毕后,在渲染视图前执行
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        System.out.println("Interceptor: afterCompletion method is called");
        // 在渲染视图后执行,可以用于资源清理等操作
    }
}

  • 步骤二:配置拦截器
<mvc:interceptors>
    <mvc:interceptor>
        <mvc:mapping path="/**" />
        <bean class="com.example.interceptor.MyInterceptor" />
    </mvc:interceptor>
</mvc:interceptors>

  • 创建Controller类
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class MyController {

    @RequestMapping("/hello")
    @ResponseBody
    public String helloWorld() {
        return "Hello, Spring MVC!";
    }
}
  • 启动应用并访问 /hello 路径,观察控制台输出

当访问 /hello 路径时,拦截器的 preHandle 方法会被调用。如果 preHandle 返回 true,则执行相应的处理器方法(Controller中的 helloWorld 方法)。执行完处理器方法后,拦截器的 postHandle 方法被调用。最后,在渲染视图后,拦截器的 afterCompletion 方法被调用。

继续回到源码:

if (!mappedHandler.applyPreHandle(processedRequest, response)) {
    // 返回false就不进行后续处理了
	return;
}

这个就是调用了applyPreHandle方法,我们进入该方法。

boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
		for (int i = 0; i < this.interceptorList.size(); i++) {
			HandlerInterceptor interceptor = this.interceptorList.get(i);
			if (!interceptor.preHandle(request, response, this.handler)) {
				triggerAfterCompletion(request, response, null);
				return false;
			}
			this.interceptorIndex = i;
		}
		return true;
	}

上面代码就是调用了前置拦截器preHandler方法。继续doDispatch方法。

mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

上面代码就执行了最合适的HandlerAdapter的handle方法。我们进入该方法:

	@Override
	@Nullable
	public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {

		return handleInternal(request, response, (HandlerMethod) handler);
	}

@Override
	protected ModelAndView handleInternal(HttpServletRequest request,
			HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

		ModelAndView mav;
		// 检查当前请求的method是否为支持的method(默认Null,可通过继承AbstractController设置supportedMethods)
		// 检查当前请求是否必须session  (默认false,可通过继承AbstractController设置requireSession)
		checkRequest(request);

		/**
		 * 判断当前是否需要支持在同一个session中只能线性地处理请求
		 * 因为锁是通过 synchronized 是 JVM 进程级,所以在分布式环境下,
		 * 无法达到同步相同 Session 的功能。默认情况下,synchronizeOnSession 为 false
		 */
		if (this.synchronizeOnSession) {
			// 获取当前请求的session对象
			HttpSession session = request.getSession(false);
			if (session != null) {
				// 为当前session生成一个唯一的可以用于锁定的key
				Object mutex = WebUtils.getSessionMutex(session);
				synchronized (mutex) {
					// 对HandlerMethod进行参数等的适配处理,并调用目标handler
					mav = invokeHandlerMethod(request, response, handlerMethod);
				}
			}
			else {
				// 如果当前不存在session,则直接对HandlerMethod进行适配
				mav = invokeHandlerMethod(request, response, handlerMethod);
			}
		}
		else {
			// *如果当前不需要对session进行同步处理,则直接对HandlerMethod进行适配
			mav = invokeHandlerMethod(request, response, handlerMethod);
		}


		//判断当前请求头中是否包含Cache-Control请求头,如果不包含,则对当前response进行处理
		if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
			// 如果当前SessionAttribute中存在配置的attributes,则为其设置过期时间。
			// 这里SessionAttribute主要是通过@SessionAttribute注解生成的
			if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
				applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
			}
			else {
				// 如果当前不存在SessionAttributes,则判断当前是否存在Cache-Control设置,
				// 如果存在,则按照该设置进行response处理,如果不存在,则设置response中的
				// Cache的过期时间为-1,即立即失效
				prepareResponse(response);
			}
		}

		return mav;
	}

然后执行invokeHandlerMethod方法。

@Nullable
	protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
			HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
		// 把我们的请求req resp包装成 ServletWebRequest
		ServletWebRequest webRequest = new ServletWebRequest(request, response);
		try {
			// 获取容器中全局配置的InitBinder和当前HandlerMethod所对应的Controller中
			// 配置的InitBinder,用于进行参数的绑定
			WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);

			// 获取容器中全局配置的ModelAttribute和当前HandlerMethod所对应的Controller 中配置的ModelAttribute,
			// 这些配置的方法将会在目标方法调用之前进行调用
			ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);

			// 封装handlerMethod,会在调用前解析参数、调用后对返回值进行处理
			ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
			if (this.argumentResolvers != null) {
				// 让invocableMethod拥有参数解析能力
				invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
			}
			if (this.returnValueHandlers != null) {
				// 让invocableMethod拥有返回值处理能力
				invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
			}
			// 让invocableMethod拥有InitBinder解析能力
			invocableMethod.setDataBinderFactory(binderFactory);
			// 设置ParameterNameDiscoverer,该对象将按照一定的规则获取当前参数的名称
			invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
			// ModelAndView处理容器
			ModelAndViewContainer mavContainer = new ModelAndViewContainer();
			// 将request的Attribute复制一份到ModelMap
			mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
			// *调用我们标注了@ModelAttribute的方法,主要是为我们的目标方法预加载
			modelFactory.initModel(webRequest, mavContainer, invocableMethod);
			// 重定向的时候,忽略model中的数据 默认false
			mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);

			// 获取当前的AsyncWebRequest,这里AsyncWebRequest的主要作用是用于判断目标
			// handler的返回值是否为WebAsyncTask或DeferredResult,如果是这两种中的一种,
			// 则说明当前请求的处理应该是异步的。所谓的异步,指的是当前请求会将Controller中
			// 封装的业务逻辑放到一个线程池中进行调用,待该调用有返回结果之后再返回到response中。
			// 这种处理的优点在于用于请求分发的线程能够解放出来,从而处理更多的请求,提高吞吐。
			// 只有待目标任务完成之后才会回来将该异步任务的结果返回。
			AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
			asyncWebRequest.setTimeout(this.asyncRequestTimeout);
			// 封装异步任务的线程池、request、interceptors到WebAsyncManager中
			WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
			asyncManager.setTaskExecutor(this.taskExecutor);
			asyncManager.setAsyncWebRequest(asyncWebRequest);
			asyncManager.registerCallableInterceptors(this.callableInterceptors);
			asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);

			// 这里就是用于判断当前请求是否有异步任务结果的,如果存在,则对异步任务结果进行封装
			if (asyncManager.hasConcurrentResult()) {
				Object result = asyncManager.getConcurrentResult();
				mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
				asyncManager.clearConcurrentResult();
				LogFormatUtils.traceDebug(logger, traceOn -> {
					String formatted = LogFormatUtils.formatValue(result, !traceOn);
					return "Resume with async result [" + formatted + "]";
				});
				invocableMethod = invocableMethod.wrapConcurrentResult(result);
			}
			// *对请求参数进行处理,调用目标HandlerMethod,并且将返回值封装为一个ModelAndView对象
			invocableMethod.invokeAndHandle(webRequest, mavContainer);
			if (asyncManager.isConcurrentHandlingStarted()) {
				return null;
			}

			// 对封装的ModelAndView进行处理,主要是判断当前请求是否进行了重定向,如果进行了重定向,
			// 还会判断是否需要将FlashAttributes封装到新的请求中
			return getModelAndView(mavContainer, modelFactory, webRequest);
		}
		finally {
			webRequest.requestCompleted();
		}
	}

上面代码主要就是执行了很多的初始化的操作,然后执行invocableMethod.invokeAndHandle(webRequest, mavContainer);方法。

public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
			Object... providedArgs) throws Exception {

		/*真正的调用我们的目标对象 很重要 很重要*/
		Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
		// 设置相关的返回状态
		setResponseStatus(webRequest);
		// 如果请求处理完成,则设置requestHandled属性
		if (returnValue == null) {
			if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {
				disableContentCachingIfNecessary(webRequest);
				mavContainer.setRequestHandled(true);
				return;
			}
		}
		// 如果请求失败,但是有错误原因,那么也会设置requestHandled属性
		else if (StringUtils.hasText(getResponseStatusReason())) {
			mavContainer.setRequestHandled(true);
			return;
		}

		mavContainer.setRequestHandled(false);
		Assert.state(this.returnValueHandlers != null, "No return value handlers");
		try {
			// 遍历当前容器中所有ReturnValueHandler,判断哪种handler支持当前返回值的处理,
			// 如果支持,则使用该handler处理该返回值
			this.returnValueHandlers.handleReturnValue(
					returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
		}
		catch (Exception ex) {
			if (logger.isTraceEnabled()) {
				logger.trace(formatErrorForReturnValue(returnValue), ex);
			}
			throw ex;
		}
	}

然后执行invokeForRequest执行我们真正的Controller中方法的逻辑。

@Nullable
	public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
			Object... providedArgs) throws Exception {
		//*获取我们目标方法入参的值
		Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
		if (logger.isTraceEnabled()) {
			logger.trace("Arguments: " + Arrays.toString(args));
		}
		//真的的调用我们的目标方法
		return doInvoke(args);
	}

这里同样用的是反射,这里就是解析我们的参数,然后调用目标方法。继续回到invokeAndHandle方法,拿到了执行的返回值后,我们会对返回值结果进行解析。

this.returnValueHandlers.handleReturnValue(
					returnValue, getReturnValueType(returnValue), mavContainer, webRequest);

调用了handleReturnValue进行返回值解析

@Override
	public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
			ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {

		HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
		if (handler == null) {
			throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
		}
		handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
	}

@Nullable
	private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {
		boolean isAsyncValue = isAsyncReturnValue(value, returnType);
		for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
			if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
				continue;
			}
			if (handler.supportsReturnType(returnType)) {
				return handler;
			}
		}
		return null;
	}

HandlerMethodReturnValueHandler方法会根据你的返回值类型是String或者ModelAndView来选择合适的返回值解析器,然后在handleReturnValue方法中调用返回值解析器的handleReturnValue方法对返回值进行解析。回到handleInternal返回上面代码的处理结果最后会被封装为一个ModelAndView对象,返回到handleInternal方法中(如果返回值是Json这个mva对象就是null,就没有后续处理了)。继续回到doDispatch方法。继续执行下面代码:

applyDefaultViewName(processedRequest, mv);

这个就是给你的ModelAndView对象设置一个默认视图,如果我们没有手动在Controller方法中设置视图的话。

ModelAndView 是 Spring MVC 中的一个对象,用于封装控制器方法的处理结果和模型数据。它包含了视图名(View Name)和一个模型对象(Model),其中模型对象是一个 Map 类型,存储了控制器方法产生的数据。通过 ModelAndView,控制器方法可以同时指定要展示的视图和传递给视图的模型数据,实现了控制器与视图之间的松耦合,并提供了一种方便的方式来组织和传递数据以渲染视图。

继续执行下面代码:

mappedHandler.applyPostHandle(processedRequest, response, mv);

这句代码就是执行后置拦截器了。实际就是执行postHandler方法。

void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
			throws Exception {

		for (int i = this.interceptorList.size() - 1; i >= 0; i--) {
			HandlerInterceptor interceptor = this.interceptorList.get(i);
			interceptor.postHandle(request, response, this.handler, mv);
		}
	}

继续执行下面代码:

processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

上面代码就是真正的开始渲染视图了,我们看看它底层是怎么做的。

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
			@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
			@Nullable Exception exception) throws Exception {

		boolean errorView = false;

		// 异常视图(处理抛出异常的视图)
		if (exception != null) {
			if (exception instanceof ModelAndViewDefiningException) {
				logger.debug("ModelAndViewDefiningException encountered", exception);
				mv = ((ModelAndViewDefiningException) exception).getModelAndView();
			}
			else {
				Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
				mv = processHandlerException(request, response, handler, exception);
				errorView = (mv != null);
			}
		}

		// Did the handler return a view to render?
		if (mv != null && !mv.wasCleared()) {
			// 解析、渲染视图
			render(mv, request, response);
			if (errorView) {
				WebUtils.clearErrorRequestAttributes(request);
			}
		}
		else {
			if (logger.isTraceEnabled()) {
				logger.trace("No view rendering, null ModelAndView returned.");
			}
		}

		if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
			// Concurrent handling started during a forward
			return;
		}

		if (mappedHandler != null) {
			// Exception (if any) is already handled..   拦截器:AfterCompletion
			mappedHandler.triggerAfterCompletion(request, response, null);
		}
	}

当没有出现异常的时候会调用,就调用render方法去解析、渲染视图。

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
		// Determine locale for request and apply it to the response.
		Locale locale =
				(this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
		response.setLocale(locale);

		View view;
		String viewName = mv.getViewName();
		if (viewName != null) {
			// 调用resolveViewName方法去解析视图名称
			view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
			if (view == null) {
				throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
						"' in servlet with name '" + getServletName() + "'");
			}
		}
		else {
			// No need to lookup: the ModelAndView object contains the actual View object.
			view = mv.getView();
			if (view == null) {
				throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
						"View object in servlet with name '" + getServletName() + "'");
			}
		}

		// Delegate to the View object for rendering.
		if (logger.isTraceEnabled()) {
			logger.trace("Rendering view [" + view + "] ");
		}
		try {
			if (mv.getStatus() != null) {
				response.setStatus(mv.getStatus().value());
			}
			view.render(mv.getModelInternal(), request, response);
		}
		catch (Exception ex) {
			if (logger.isDebugEnabled()) {
				logger.debug("Error rendering view [" + view + "]", ex);
			}
			throw ex;
		}
	}

上面代码首先会解析视图的名称(我们知道ModelAndView中的视图名称可能只是一个具体的jsp的文件名,但实际要解析成有前缀和后缀的详细文件名称),调用的代码是view = resolveViewName(viewName, mv.getModelInternal(), locale, request);

@Nullable
	protected View resolveViewName(String viewName, @Nullable Map<String, Object> model,
			Locale locale, HttpServletRequest request) throws Exception {

		if (this.viewResolvers != null) {
			for (ViewResolver viewResolver : this.viewResolvers) {
				View view = viewResolver.resolveViewName(viewName, locale);
				if (view != null) {
					return view;
				}
			}
		}
		return null;
	}

上面代码同样是拿到一个视图解析器就直接开始解析文件名,然后返回解析结果。继续回到render方法,以切准备工作做完之后就开始真正解析和渲染视图了。

view.render(mv.getModelInternal(), request, response);

进入该方法:

@Override
public void render(@Nullable Map<String, ?> model, HttpServletRequest request,
		HttpServletResponse response) throws Exception {

       if (logger.isDebugEnabled()) {
			logger.debug("View " + formatViewName() +
					", model " + (model != null ? model : Collections.emptyMap()) +
					(this.staticAttributes.isEmpty() ? "" : ", static attributes " + this.staticAttributes));
		}

		Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
		prepareResponse(request, response);
		// 这里
		renderMergedOutputModel(mergedModel, getRequestToExpose(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. 将model设置到request的attribute中.
		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).
		// 通过request拿到RequestDispatcher request.getRequestDispacther("/test.jsp")
		RequestDispatcher rd = getRequestDispatcher(request, 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(request, response)) {
			response.setContentType(getContentType());
			if (logger.isDebugEnabled()) {
				logger.debug("Including [" + getUrl() + "]");
			}
			rd.include(request, response);
		}

		else {
			// Note: The forwarded resource is supposed to determine the content type itself.
			if (logger.isDebugEnabled()) {
				logger.debug("Forwarding to [" + getUrl() + "]");
			} // RequestDispatcher.forward直接转发,就这么简单粗暴
			rd.forward(request, response);
		}
	}

上面代码就讲model中所有的值都设置到了request的Attribute属性当中,实际执行的是这句代码exposeModelAsRequestAttributes(model, request);

protected void exposeModelAsRequestAttributes(Map<String, Object> model,
			HttpServletRequest request) throws Exception {
		// 将model解析到request的attribute中
		model.forEach((name, value) -> {
			if (value != null) {
				request.setAttribute(name, value);
			}
			else {
				request.removeAttribute(name);
			}
		});
	}

上面代码就是讲model解析到request的attribute属性中。回到renderMergedOutputModel方法。然后就是设置一些国际化资源,然后执行RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);拿到RequestDispatcher对象。最后调用rd.forward(request, response);进行请求转发。回到processDispatchResult方法,最后执行最后的拦截器afterCompletion方法。

if (mappedHandler != null) {
			// Exception (if any) is already handled..   拦截器:AfterCompletion
			mappedHandler.triggerAfterCompletion(request, response, null);
		}

以上SpringMVC的大致流程就讲解完了。

2. 细节补充

SpringMVC启动时会将所有的@RequestMapping解析出来,解析流程图。HandlerMapping有很多种,这里我们就看一下用的最多的RequestMappingHandlerMapping

public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping
		implements MatchableHandlerMapping, EmbeddedValueResolverAware {

	private boolean useSuffixPatternMatch = false;

	private boolean useRegisteredSuffixPatternMatch = false;

	private boolean useTrailingSlashMatch = true;

	private Map<String, Predicate<Class<?>>> pathPrefixes = Collections.emptyMap();

	private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager();

	@Nullable
	private StringValueResolver embeddedValueResolver;

	private RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration();
	@Deprecated
	public void setUseSuffixPatternMatch(boolean useSuffixPatternMatch) {
		this.useSuffixPatternMatch = useSuffixPatternMatch;
	}
	@Deprecated
	public void setUseRegisteredSuffixPatternMatch(boolean useRegisteredSuffixPatternMatch) {
		this.useRegisteredSuffixPatternMatch = useRegisteredSuffixPatternMatch;
		this.useSuffixPatternMatch = (useRegisteredSuffixPatternMatch || this.useSuffixPatternMatch);
	}
	public void setUseTrailingSlashMatch(boolean useTrailingSlashMatch) {
		this.useTrailingSlashMatch = useTrailingSlashMatch;
		if (getPatternParser() != null) {
			getPatternParser().setMatchOptionalTrailingSeparator(useTrailingSlashMatch);
		}
	}
	public void setPathPrefixes(Map<String, Predicate<Class<?>>> prefixes) {
		this.pathPrefixes = (!prefixes.isEmpty() ?
				Collections.unmodifiableMap(new LinkedHashMap<>(prefixes)) :
				Collections.emptyMap());
	}

	public Map<String, Predicate<Class<?>>> getPathPrefixes() {
		return this.pathPrefixes;
	}
	public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) {
		Assert.notNull(contentNegotiationManager, "ContentNegotiationManager must not be null");
		this.contentNegotiationManager = contentNegotiationManager;
	}
	public ContentNegotiationManager getContentNegotiationManager() {
		return this.contentNegotiationManager;
	}

	@Override
	public void setEmbeddedValueResolver(StringValueResolver resolver) {
		this.embeddedValueResolver = resolver;
	}

	@Override
	@SuppressWarnings("deprecation")
	public void afterPropertiesSet() {

		this.config = new RequestMappingInfo.BuilderConfiguration();
		this.config.setTrailingSlashMatch(useTrailingSlashMatch()); // 尾部斜杠
		this.config.setContentNegotiationManager(getContentNegotiationManager());

		if (getPatternParser() != null) {
			this.config.setPatternParser(getPatternParser());
			Assert.isTrue(!this.useSuffixPatternMatch && !this.useRegisteredSuffixPatternMatch,
					"Suffix pattern matching not supported with PathPatternParser.");
		}
		else {
			this.config.setSuffixPatternMatch(useSuffixPatternMatch());
			this.config.setRegisteredSuffixPatternMatch(useRegisteredSuffixPatternMatch());
			this.config.setPathMatcher(getPathMatcher());
		}

		super.afterPropertiesSet();
	}
	@Deprecated
	public boolean useSuffixPatternMatch() {
		return this.useSuffixPatternMatch;
	}
	@Deprecated
	public boolean useRegisteredSuffixPatternMatch() {
		return this.useRegisteredSuffixPatternMatch;
	}
	public boolean useTrailingSlashMatch() {
		return this.useTrailingSlashMatch;
	}
	@Nullable
	@Deprecated
	@SuppressWarnings("deprecation")
	public List<String> getFileExtensions() {
		return this.config.getFileExtensions();
	}
	@Override
	protected boolean isHandler(Class<?> beanType) {
		return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
				AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
	}
	@Override
	@Nullable
	protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
		// 如果方法上面有@RequestMapping:解析出RequestMappingInfo
		// RequestMappingInfo 是用来在请求的时候做匹对的
		RequestMappingInfo info = createRequestMappingInfo(method);
		if (info != null) {
			// 如果方法上面有@RequestMapping,看看类上面是不是有@RequestMapping
			RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
			// 类上面也有@RequestMapping  那就合并
			// 比如 类:/user  方法:/info 合并为 /user/info
			if (typeInfo != null) {
				info = typeInfo.combine(info);
			}

			// 合并前缀   5.1新增  默认null
			// 可通过 WebMvcConfigurer#configurePathMatch 进行定制
			String prefix = getPathPrefix(handlerType);
			if (prefix != null) {
				info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info);
			}
		}
		return info;
	}

	@Nullable
	String getPathPrefix(Class<?> handlerType) {
		for (Map.Entry<String, Predicate<Class<?>>> entry : this.pathPrefixes.entrySet()) {
			if (entry.getValue().test(handlerType)) {
				String prefix = entry.getKey();
				if (this.embeddedValueResolver != null) {
					prefix = this.embeddedValueResolver.resolveStringValue(prefix);
				}
				return prefix;
			}
		}
		return null;
	}
	@Nullable
	private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
		// 获取RequestMapping注解
		RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
		// 获取请求调解:[可扩展], 如果有:该条件会在请求时匹对
		RequestCondition<?> condition = (element instanceof Class ?
				getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));
		// 如果有RequestMapping注解,封装成RequestMappingInfo
		return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
	}
	@Nullable
	protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
		return null;
	}
	@Nullable
	protected RequestCondition<?> getCustomMethodCondition(Method method) {
		return null;
	}
	protected RequestMappingInfo createRequestMappingInfo(
			RequestMapping requestMapping, @Nullable RequestCondition<?> customCondition) {
		// 将@RequestMapping注解属性的值构建成一个 RequestMappingInfo
		RequestMappingInfo.Builder builder = RequestMappingInfo
				//构建路径
				.paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
				//构建方法(get还是post等)
				.methods(requestMapping.method())
				//参数 对应http request parameter
				.params(requestMapping.params())
				//头部
				.headers(requestMapping.headers())
				//request的提交内容类型content type,如application/json, text/html
				.consumes(requestMapping.consumes())
				//指定返回的内容类型的content type,仅当request请求头中的(Accept)类型中包含该指定类型才返回
				.produces(requestMapping.produces())
				.mappingName(requestMapping.name());
		if (customCondition != null) {
			builder.customCondition(customCondition);
		}
		// 构造RequestMappingInfo:将上面的属性构建成一个个的RequestCondition对象方便在请求的时候组合匹对
		return builder.options(this.config).build();
	}
	protected String[] resolveEmbeddedValuesInPatterns(String[] patterns) {
		if (this.embeddedValueResolver == null) {
			return patterns;
		}
		else {
			String[] resolvedPatterns = new String[patterns.length];
			for (int i = 0; i < patterns.length; i++) {
				resolvedPatterns[i] = this.embeddedValueResolver.resolveStringValue(patterns[i]);
			}
			return resolvedPatterns;
		}
	}

	@Override
	public void registerMapping(RequestMappingInfo mapping, Object handler, Method method) {
		super.registerMapping(mapping, handler, method);
		updateConsumesCondition(mapping, method);
	}

	@Override
	protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
		super.registerHandlerMethod(handler, method, mapping);
		updateConsumesCondition(mapping, method);
	}

	private void updateConsumesCondition(RequestMappingInfo info, Method method) {
		ConsumesRequestCondition condition = info.getConsumesCondition();
		if (!condition.isEmpty()) {
			for (Parameter parameter : method.getParameters()) {
				MergedAnnotation<RequestBody> annot = MergedAnnotations.from(parameter).get(RequestBody.class);
				if (annot.isPresent()) {
					condition.setBodyRequired(annot.getBoolean("required"));
					break;
				}
			}
		}
	}

	@Override
	public RequestMatchResult match(HttpServletRequest request, String pattern) {
		Assert.isNull(getPatternParser(), "This HandlerMapping requires a PathPattern");
		RequestMappingInfo info = RequestMappingInfo.paths(pattern).options(this.config).build();
		RequestMappingInfo match = info.getMatchingCondition(request);
		return (match != null && match.getPatternsCondition() != null ?
				new RequestMatchResult(
						match.getPatternsCondition().getPatterns().iterator().next(),
						UrlPathHelper.getResolvedLookupPath(request),
						getPathMatcher()) : null);
	}

	@Override
	protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mappingInfo) {
		HandlerMethod handlerMethod = createHandlerMethod(handler, method);
		Class<?> beanType = handlerMethod.getBeanType();
		CrossOrigin typeAnnotation = AnnotatedElementUtils.findMergedAnnotation(beanType, CrossOrigin.class);
		CrossOrigin methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, CrossOrigin.class);

		if (typeAnnotation == null && methodAnnotation == null) {
			return null;
		}

		CorsConfiguration config = new CorsConfiguration();
		updateCorsConfig(config, typeAnnotation);
		updateCorsConfig(config, methodAnnotation);

		if (CollectionUtils.isEmpty(config.getAllowedMethods())) {
			for (RequestMethod allowedMethod : mappingInfo.getMethodsCondition().getMethods()) {
				config.addAllowedMethod(allowedMethod.name());
			}
		}
		return config.applyPermitDefaultValues();
	}

	private void updateCorsConfig(CorsConfiguration config, @Nullable CrossOrigin annotation) {
		if (annotation == null) {
			return;
		}
		for (String origin : annotation.origins()) {
			config.addAllowedOrigin(resolveCorsAnnotationValue(origin));
		}
		for (String patterns : annotation.originPatterns()) {
			config.addAllowedOriginPattern(resolveCorsAnnotationValue(patterns));
		}
		for (RequestMethod method : annotation.methods()) {
			config.addAllowedMethod(method.name());
		}
		for (String header : annotation.allowedHeaders()) {
			config.addAllowedHeader(resolveCorsAnnotationValue(header));
		}
		for (String header : annotation.exposedHeaders()) {
			config.addExposedHeader(resolveCorsAnnotationValue(header));
		}

		String allowCredentials = resolveCorsAnnotationValue(annotation.allowCredentials());
		if ("true".equalsIgnoreCase(allowCredentials)) {
			config.setAllowCredentials(true);
		}
		else if ("false".equalsIgnoreCase(allowCredentials)) {
			config.setAllowCredentials(false);
		}
		else if (!allowCredentials.isEmpty()) {
			throw new IllegalStateException("@CrossOrigin's allowCredentials value must be \"true\", \"false\", " +
					"or an empty string (\"\"): current value is [" + allowCredentials + "]");
		}

		if (annotation.maxAge() >= 0 ) {
			config.setMaxAge(annotation.maxAge());
		}
	}

	private String resolveCorsAnnotationValue(String value) {
		if (this.embeddedValueResolver != null) {
			String resolved = this.embeddedValueResolver.resolveStringValue(value);
			return (resolved != null ? resolved : "");
		}
		else {
			return value;
		}
	}

}

这个类在容器启动的时候会被初始化为一个bean,然后在该bean的生命周期中会调用它的回调方法afterPropertiesSet方法。首先我们回顾一下Spring的回调方法的使用:

Spring 框架中的回调方法通常是通过接口或注解实现的,用于在特定的生命周期事件发生时执行相应的逻辑。下面详细介绍一些常见的 Spring 回调方法,并结合案例说明它们的用法。

  1. InitializingBean 和 DisposableBean 接口:
    InitializingBean: 实现该接口的类在初始化时会执行 afterPropertiesSet() 方法。DisposableBean: 实现该接口的类在销毁时会执行 destroy() 方法。
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;

public class MyBean implements InitializingBean, DisposableBean {
    // 初始化逻辑
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("Bean is being initialized...");
    }

    // 销毁逻辑
    @Override
    public void destroy() throws Exception {
        System.out.println("Bean is being destroyed...");
    }
}

  1. @PostConstruct 和 @PreDestroy 注解:
    @PostConstruct: 用于在构造函数执行之后、依赖注入完成之后执行初始化逻辑。@PreDestroy: 用于在 Bean 销毁之前执行清理逻辑。
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

public class MyBean {
    // 初始化逻辑
    @PostConstruct
    public void init() {
        System.out.println("Bean is being initialized...");
    }

    // 销毁逻辑
    @PreDestroy
    public void cleanUp() {
        System.out.println("Bean is being destroyed...");
    }
}
  1. ApplicationListener 接口:
    实现该接口可以监听 Spring 应用中的事件,执行相应的逻辑。
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;

public class MyListener implements ApplicationListener<ApplicationEvent> {
    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof ContextRefreshedEvent) {
            // Spring 上下文初始化完成时的逻辑
            System.out.println("Context is refreshed...");
        }
    }
}

这些回调方法可以用于执行一些特定于应用程序生命周期的逻辑,确保在初始化和销毁阶段执行必要的操作。通过实现相应的接口或使用注解,开发者可以方便地与 Spring 框架的生命周期集成。

@Override
	@SuppressWarnings("deprecation")
	public void afterPropertiesSet() {

		this.config = new RequestMappingInfo.BuilderConfiguration();
		this.config.setTrailingSlashMatch(useTrailingSlashMatch()); // 尾部斜杠
		this.config.setContentNegotiationManager(getContentNegotiationManager());

		if (getPatternParser() != null) {
			this.config.setPatternParser(getPatternParser());
			Assert.isTrue(!this.useSuffixPatternMatch && !this.useRegisteredSuffixPatternMatch,
					"Suffix pattern matching not supported with PathPatternParser.");
		}
		else {
			this.config.setSuffixPatternMatch(useSuffixPatternMatch());
			this.config.setRegisteredSuffixPatternMatch(useRegisteredSuffixPatternMatch());
			this.config.setPathMatcher(getPathMatcher());
		}

		super.afterPropertiesSet();
	}

然后调用super.afterPropertiesSet();方法,我们进入该方法。

@Override
	public void afterPropertiesSet() {
		initHandlerMethods();
	}

该方法中就调用了一个initHandlerMethods方法,我们进入该方法

protected void initHandlerMethods() {
		// 获得所有候选beanName—— 当前容器所有的beanName
		for (String beanName : getCandidateBeanNames()) {
			if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
				// *处理候选bean——即解析@RequestMapping和映射路径
				processCandidateBean(beanName);
			}
		}
		// 解析完所有@RequestMapping的时候调用
		handlerMethodsInitialized(getHandlerMethods());
	}

上面方法首先获得了Spring容器中所有bean得名称,然后调用processCandidateBean方法处理所有以scopedTarget.开头的bean,即解析@RequestMapping和映射路径。

protected void processCandidateBean(String beanName) {
		Class<?> beanType = null;
		try {
		//获得当前bean得类型
			beanType = obtainApplicationContext().getType(beanName);
		}
		catch (Throwable ex) {
			if (logger.isTraceEnabled()) {
				logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
			}
		}
		// 这一步判断是关键  是否有Controller 或 RequestMapping注解
		if (beanType != null && isHandler(beanType)) {
			// 解析HandlerMethods
			detectHandlerMethods(beanName);
		}
	}

首先根据bean的名称获取bean得类型,然后执行if(beanType != null && isHandler(beanType)),首先判断beanType != null不为空,然后调用isHandler(beanType)判断是否有Controller 或 RequestMapping注解 。我们进入该方法:

protected boolean isHandler(Class<?> beanType) {
		return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
				AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
	}

回到processCandidateBean方法,如果有Controller.classRequestMapping.class注解,然后就是执行detectHandlerMethods方法,解析HandlerMethods。我们进入该方法:

protected void detectHandlerMethods(Object handler) {
        //判断传入进来的handler的类型是否为String,如果为String就获取对应的bean的名称,如果本来就是bean,就直接获取bean得类型。
		Class<?> handlerType = (handler instanceof String ?
				obtainApplicationContext().getType((String) handler) : handler.getClass());
          //如果bean的类型不为空
		if (handlerType != null) {
			Class<?> userType = ClassUtils.getUserClass(handlerType);
			// 循环所有方法
			Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
					(MethodIntrospector.MetadataLookup<T>) method -> {
						try {
							return getMappingForMethod(method, userType);
						}
						catch (Throwable ex) {
							throw new IllegalStateException("Invalid mapping on handler class [" +
									userType.getName() + "]: " + method, ex);
						}
			});
			if (logger.isTraceEnabled()) {
				logger.trace(formatMappings(userType, methods));
			}
			else if (mappingsLogger.isDebugEnabled()) {
				mappingsLogger.debug(formatMappings(userType, methods));
			}
			methods.forEach((method, mapping) -> {
				Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
				registerHandlerMethod(handler, invocableMethod, mapping);
			});
		}
	}

上面代码首先执行下面代码:

Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
	(MethodIntrospector.MetadataLookup<T>) method -> {
		try {
			 return getMappingForMethod(method, userType);
		}catch (Throwable ex) {
			 throw new IllegalStateException("Invalid mapping on handler class [" +
			userType.getName() + "]: " + method, ex);
		}
});

这里就是调用了一个selectMethods方法,然后参数一个是bean的类型以及一个函数式接口,我们先看一下这个函数式接口。其实就是调用了一个getMappingForMethod方法。

@Override
	@Nullable
	protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
		// 如果方法上面有@RequestMapping:解析出RequestMappingInfo
		// RequestMappingInfo 是用来在请求的时候做匹对的
		RequestMappingInfo info = createRequestMappingInfo(method);
		if (info != null) {
			// 如果方法上面有@RequestMapping,看看类上面是不是有@RequestMapping
			RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
			// 类上面也有@RequestMapping  那就合并
			// 比如 类:/user  方法:/info 合并为 /user/info
			if (typeInfo != null) {
				info = typeInfo.combine(info);
			}

			// 合并前缀   5.1新增  默认null
			// 可通过 WebMvcConfigurer#configurePathMatch 进行定制
			String prefix = getPathPrefix(handlerType);
			if (prefix != null) {
				info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info);
			}
		}
		return info;
	}

上面方法首先调用createRequestMappingInfo方法来解析@RequestMapping注解。进入该方法如下:

@Nullable
	private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
		// 获取RequestMapping注解
		RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
		// 获取请求调解:[可扩展], 如果有:该条件会在请求时匹对
		RequestCondition<?> condition = (element instanceof Class ?
				getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));
		// 如果有RequestMapping注解,封装成RequestMappingInfo
		return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
	}

上面代码就是解析了@RequestMapping注解,然后调用 createRequestMappingInfo(requestMapping, condition)封装为一个 RequestMappingInfo对象返回,这个对象就封装了注解的所有信息。

protected RequestMappingInfo createRequestMappingInfo(
			RequestMapping requestMapping, @Nullable RequestCondition<?> customCondition) {
		// 将@RequestMapping注解属性的值构建成一个 RequestMappingInfo
		RequestMappingInfo.Builder builder = RequestMappingInfo
				//构建路径
				.paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
				//构建方法(get还是post等)
				.methods(requestMapping.method())
				//参数 对应http request parameter
				.params(requestMapping.params())
				//头部
				.headers(requestMapping.headers())
				//request的提交内容类型content type,如application/json, text/html
				.consumes(requestMapping.consumes())
				//指定返回的内容类型的content type,仅当request请求头中的(Accept)类型中包含该指定类型才返回
				.produces(requestMapping.produces())
				.mappingName(requestMapping.name());
		if (customCondition != null) {
			builder.customCondition(customCondition);
		}
		// 构造RequestMappingInfo:将上面的属性构建成一个个的RequestCondition对象方便在请求的时候组合匹对
		return builder.options(this.config).build();
	}

然后回到detectHandlerMethods方法,调用selectMethods方法。

public static <T> Map<Method, T> selectMethods(Class<?> targetType, final MetadataLookup<T> metadataLookup) {
		final Map<Method, T> methodMap = new LinkedHashMap<>();
		Set<Class<?>> handlerTypes = new LinkedHashSet<>();
		Class<?> specificHandlerType = null;
		//获取原始的class对象
		if (!Proxy.isProxyClass(targetType)) {
			specificHandlerType = ClassUtils.getUserClass(targetType);
			handlerTypes.add(specificHandlerType);
		}
		//获取class的接口
		handlerTypes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetType));
		//循环我们的class集合
		for (Class<?> currentHandlerType : handlerTypes) {
			final Class<?> targetClass = (specificHandlerType != null ? specificHandlerType : currentHandlerType);

			ReflectionUtils.doWithMethods(currentHandlerType, method -> {
				//获取具体的方法对象
				Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
                //这就是调用了前面的lamda表达时
				T result = metadataLookup.inspect(specificMethod);
				if (result != null) {
					// 看看有没有桥接方法,泛型实现类jvm会自动生成桥接类,不知道有啥意义
					Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
					if (bridgedMethod == specificMethod || metadataLookup.inspect(bridgedMethod) == null) {
						//把方法对象作为key,RequestMappingInfo对象作为value保存到map中
						methodMap.put(specificMethod, result);
					}
				}
			}, ReflectionUtils.USER_DECLARED_METHODS);
		}

		return methodMap;
	}

上面我们就获得了所有加@RequestMapping注解的方法。回到detectHandlerMethods,接下来执行下面代码:

methods.forEach((method, mapping) -> {
	Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
	registerHandlerMethod(handler, invocableMethod, mapping);
});

上面的forEach就遍历了获得的所有方法,然后调用registerHandlerMethod(handler, invocableMethod, mapping);这句代码。

protected void registerHandlerMethod(Object handler, Method method, T mapping) {
		this.mappingRegistry.register(mapping, handler, method);
	}
public void register(T mapping, Object handler, Method method) {
			this.readWriteLock.writeLock().lock();
			try {
			//根据
				HandlerMethod handlerMethod = createHandlerMethod(handler, method);
				validateMethodMapping(handlerMethod, mapping);

				Set<String> directPaths = AbstractHandlerMethodMapping.this.getDirectPaths(mapping);
				for (String path : directPaths) {
					this.pathLookup.add(path, mapping);
				}

				String name = null;
				if (getNamingStrategy() != null) {
					name = getNamingStrategy().getName(handlerMethod, mapping);
					addMappingName(name, handlerMethod);
				}

				CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
				if (corsConfig != null) {
					corsConfig.validateAllowCredentials();
					this.corsLookup.put(handlerMethod, corsConfig);
				}

				this.registry.put(mapping,
						new MappingRegistration<>(mapping, handlerMethod, directPaths, name, corsConfig != null));
			}
			finally {
				this.readWriteLock.writeLock().unlock();
			}
		}

上面代码的核心就是下面这两句代码,将信息封装到了下面两个重要集合中,第一个集合pathLookup就存储了path,也就是我们在@RequestMapping中路径信息,T就是前面的RquestMappingInfo。到此RequestMapping注解就解析完毕了。

this.pathLookup.add(path, mapping);
this.corsLookup.put(handlerMethod, corsConfig);
private final MultiValueMap<String, T> pathLookup = new LinkedMultiValueMap<>();
private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();

上面就已经成功解析了所有的RequestMapping,然后如果我们通过浏览器访问的时候,来到doDispatch方法。在执行该方法时,前面受过它会执行mappedHandler = getHandler(processedRequest);进行映射。

protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		if (this.handlerMappings != null) {
			/** 拿到所有handlerMappings (容器启动阶段初始化:拿到所有实现了HandlerMapping的Bean)
			 * @see DispatcherServlet#initHandlerMappings
			 * 测试发现: 不同的HandlerMapping可以有相同path, 谁先解析到就用哪个
			 * */
			for (HandlerMapping mapping : this.handlerMappings) {
				HandlerExecutionChain handler = mapping.getHandler(request);
				if (handler != null) {
					return handler;
				}
			}
		}
		return null;
	}

继续进入mapping.getHandler方法

@Override
	@Nullable
	public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		Object handler = getHandlerInternal(request);
		if (handler == null) {
			handler = getDefaultHandler();
		}
		if (handler == null) {
			return null;
		}
		// Bean name or resolved handler?
		if (handler instanceof String) {
			String handlerName = (String) handler;
			handler = obtainApplicationContext().getBean(handlerName);
		}

		// Ensure presence of cached lookupPath for interceptors and others
		if (!ServletRequestPathUtils.hasCachedPath(request)) {
			initLookupPath(request);
		}

		HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);

		if (logger.isTraceEnabled()) {
			logger.trace("Mapped to " + handler);
		}
		else if (logger.isDebugEnabled() && !DispatcherType.ASYNC.equals(request.getDispatcherType())) {
			logger.debug("Mapped to " + executionChain.getHandler());
		}

		if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
			CorsConfiguration config = getCorsConfiguration(handler, request);
			if (getCorsConfigurationSource() != null) {
				CorsConfiguration globalConfig = getCorsConfigurationSource().getCorsConfiguration(request);
				config = (globalConfig != null ? globalConfig.combine(config) : config);
			}
			if (config != null) {
				config.validateAllowCredentials();
			}
			executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
		}

		return executionChain;
	}

上面就有一句代码getHandlerInternal,进入该方法

@Override
	@Nullable
	protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
		// 通过UrlPathHelper对象,用于来解析从们的request中解析出请求映射路径
		String lookupPath = initLookupPath(request);
		this.mappingRegistry.acquireReadLock();
		try {
			// 通过lookupPath解析最终的handler——HandlerMethod对象
			HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
			return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
		}
		finally {
			this.mappingRegistry.releaseReadLock();
		}
	}

protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
		List<Match> matches = new ArrayList<>();
		// 根据uri从mappingRegistry.pathLookup获取 RequestMappingInfo
		// pathLookup<path,RequestMappingInfo>会在初始化阶段解析好
		List<T> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath);
		if (directPathMatches != null) {
			// 如果根据path能直接匹配的RequestMappingInfo 则用该mapping进行匹配其他条件(method、header等)
			addMatchingMappings(directPathMatches, matches, request);
		}
		if (matches.isEmpty()) {
			// 如果无path匹配,用所有的RequestMappingInfo  通过AntPathMatcher匹配
			addMatchingMappings(this.mappingRegistry.getRegistrations().keySet(), matches, request);
		}
		if (!matches.isEmpty()) {
			// 选择第一个为最匹配的
			Match bestMatch = matches.get(0);
			/**
			 * 如果匹配到多个
			 @RequestMapping(value="/mappin?")
			 @RequestMapping(value="/mappin*")
			 @RequestMapping(value="/{xxxx}")
			 @RequestMapping(value="/**")
			 */
			if (matches.size() > 1) {
				//创建MatchComparator的匹配器对象
				Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));

				/** 根据精准度排序  大概是这样的: ? > * > {} >**   具体可以去看:
				 * @see org.springframework.util.AntPathMatcher.AntPatternComparator#compare(java.lang.String, java.lang.String)*/
				matches.sort(comparator);

				// 排完序后拿到优先级最高的
				bestMatch = matches.get(0);
				if (logger.isTraceEnabled()) {
					logger.trace(matches.size() + " matching mappings: " + matches);
				}
				// 是否配置CORS并且匹配
				if (CorsUtils.isPreFlightRequest(request)) {
					for (Match match : matches) {
						if (match.hasCorsConfig()) {
							return PREFLIGHT_AMBIGUOUS_MATCH;
						}
					}
				}
				else {
					//获取第二最匹配的
					Match secondBestMatch = matches.get(1);
					//若第一个和第二个是一样的 抛出异常
					if (comparator.compare(bestMatch, secondBestMatch) == 0) {
						Method m1 = bestMatch.getHandlerMethod().getMethod();
						Method m2 = secondBestMatch.getHandlerMethod().getMethod();
						String uri = request.getRequestURI();
						throw new IllegalStateException(
								"Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
					}
				}
			}
			//把最匹配的设置到request中
			request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.getHandlerMethod());
			handleMatch(bestMatch.mapping, lookupPath, request);
			//返回最匹配的
			return bestMatch.getHandlerMethod();
		}
		else { // return null
			return handleNoMatch(this.mappingRegistry.getRegistrations().keySet(), lookupPath, request);
		}
	}

可以看见上面代码就是请求过程中如何获得HandlerMethod对象的流程。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/326599.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Leetcode】82. 删除排序链表中的重复元素 II

文章目录 题目思路代码 题目 82. 删除排序链表中的重复元素 II 题目&#xff1a;给定一个已排序的链表的头 head &#xff0c; 删除原始链表中所有重复数字的节点&#xff0c;只留下不同的数字 。返回 已排序的链表 。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,3,4,…

uni-app的项目创建和环境搭建

uni-app 是一个使用 Vue.js 开发所有前端应用的框架&#xff0c;开发者编写一套代码&#xff0c;可发布到iOS、Android、Web&#xff08;响应式&#xff09;、以及各种小程序&#xff08;微信/支付宝/百度/头条/飞书/QQ/快手/钉钉/淘宝&#xff09;、快应用等多个平台。 第一步…

Linux的权限(1)

目录 操作系统的"外壳"程序 外壳程序是什么&#xff1f; 为什么存在外壳程序&#xff1f; 外壳程序怎么运行操作&#xff1f; 权限 什么是权限&#xff1f; 权限的本质&#xff1f; Linux中的&#xff08;人&#xff09;用户权限&#xff1f; su和su -的区别…

C for Graphic:Sliced Circle Image

不做UI不知道&#xff0c;没想到时至今日&#xff0c;ugui居然没有sliced filled image模式&#xff0c;用circle做filled&#xff0c;不能用sliced九宫格图&#xff0c;导致每次使用这个效果必须一张新图&#xff0c;何其浪费资源。 原始功能如下&#xff1a; 我…

Python数据分析案例34——IMDB电影评论情感分析(Transformer)

电影评论的情感分析 案例背景 很多同学对电影系列的数据都比较喜欢&#xff0c;那我就补充一下这个最经典的文本分类数据集&#xff0c;电影情感评论分析。用神经网络做。对国外的英文评论文本进行分类&#xff0c;看是正面还是负面情感。 数据集介绍 数据集&#xff1a;IMDb…

PY32C613单片机简单介绍,高性能 32 位 ARM M0+内核,主频最高48M

PY32C613单片机是普冉新推出的高性能的 32 位 ARM Cortex-M0 内核&#xff0c;宽电压工作范围的 MCU。价格在市场上非常有竞争性&#xff0c;外设非常的丰富。PY32C613嵌入高达 64 Kbytes flash 和 8 Kbytes SRAM 存储器&#xff0c;最高工作频率 48 MHz&#xff0c;QFN20封装。…

uniapp 图片保持宽高比,撑满屏幕宽度

image 标签添加 mode"widthFix" <image mode"widthFix" :src"detailData.coverImageURL" />image 标签添加样式 image {width: 100%;height: auto; }

x-cmd pkg | sd - sed 命令的现代化替代品

目录 简介首次用户快速上手主要特点进一步阅读 简介 sd 是一个基于正则表达式的搜索和替换文本的命令行工具&#xff0c;类似于 sed&#xff0c;但 sd 使用更简单&#xff0c;对用户更为友好。 首次用户快速上手 使用 x sd 即可自动下载并使用 在终端运行 eval "$(curl …

【分布式技术】分布式存储ceph部署

目录 一、存储的介绍 单机存储设备 单机存储的问题 商业存储 分布式存储 二、分布式存储 什么是分布式存储 分布式存储的类型 三、ceph简介 四、ceph的优点 五、ceph的架构 六、ceph的核心组件 七、OSD存储后端 八、Ceph 数据的存储过程 九、Ceph 版本发行生命周…

单片机和Linux嵌入式区别

1.单片机 单片机是一种集成电路&#xff0c;它能够在一个芯片上完成各种计算、控制和管理任务。单片机没有明确的分层&#xff0c;这是因为它通常被用来设计嵌入式系统&#xff0c;其程序结构和功能要根据具体的应用需求来设计。 在单片机的程序设计中&#xff0c;可以通过一…

SQL Server Management Studio (SSMS) 备份数据库

文章目录 前言一、在界面上操作二、使用sql 代码操作总结 前言 之前的文章记录过如何使用sqlserver复制远程数据库到本地。这里补充下如何使用SQL Server Management Studio (SSMS) 备份。 传送门&#xff1a;sqlserver复制远程数据库到本地 一、在界面上操作 在 SQL Server …

AP上线配置流程

AP工作模式 相应地&#xff0c;AR路由器的WLAN工作模式分为FAT AP和AC两种模式&#xff0c;不同的模式对应不同的使用场景。 FAT AP模式&#xff1a;AR路由器作为FAT AP&#xff0c;独立为用户提供WLAN接入服务&#xff0c;无线网络的配置在FAT AP上单独配置。FAT AP模式主要…

链上繁荣,合作不断,外媒热议波场 TRON 2023 年度成绩

近日&#xff0c;权威外媒Theblock、美联社和Decrypt等就波场 TRON 2023大事件进行了年度盘点。报道指出&#xff0c;波场TRON网络在2023年取得了一系列的发展和合作&#xff0c;提升了其在Web3领域的地位。其中&#xff0c;波场TRON网络账户数量增加了 54&#xff05;&#xf…

微信视频号提取视频工具和提取器有什么不同?怎么下载和使用!

微信视频号怎么提取视频和视频号提取器有啥区别&#xff1f;这个是最近问我特别多的小伙伴&#xff0c;今天就和大家讲讲两者有什么不同怎么使用&#xff01; 视频号视频提取工具和提取器在早期区分的是网页工具和电脑上软件使用的一种方式&#xff0c;用户通过这些方式解决自…

深度系统QT 环境搭建

1.QT安装 不折腾最新版直接去商店搜索QT安装。 2.修改su密码&#xff0c;安装需要权限 打开一个终端&#xff0c;然后输入下面的命令&#xff1a;按照提示输入密码按回车就行。 sudo passwd 回车后会出现让你输入现在这个账户的密码&#xff1a; 3.编译环境安装。 安…

DHCP详解(配置小实验)

目录 一、DHCP 1、了解DHCP 2、使用DHCP的好处 3、HDCP的分配方式 4、DHCP协议中的报文 5、DHCP的租约过程 6、安装和配置DHCP服务 一、DHCP 1、了解DHCP DHCP(Dynamic Host Configuration Protocol&#xff0c;动态主机配置协议)由Internet工作任务小组设计开发专门用…

计算机毕业设计 | vue+springboot高校宿舍管理系统(附源码)

1&#xff0c;绪论 研究背景 学生管理是学校教育系统的一个十分重要的部分&#xff0c;其中学生宿舍的管理又是学校管理中较复杂的一部分。学生宿舍不只是简单的一个居住场所&#xff0c;而是高校实施教育过程&#xff0c;培养人才不可或缺的一个硬件条件&#xff0c;是大学文…

docker-consul部署

目录 一、环境 二、consul服务器 三、registrator服务器 四、consul-template 一、环境 consul服务器 192.168.246.10 运行consul服务、nginx服务、consul-template守护进程 registrator服务器 192.168.246.11 运行registrator容器、运行ngi…

烟火检测/周界入侵/视频智能识别AI智能分析网关V4如何配置ONVIF摄像机接入

AI边缘计算智能分析网关V4性能高、功耗低、检测速度快&#xff0c;易安装、易维护&#xff0c;硬件内置了近40种AI算法模型&#xff0c;支持对接入的视频图像进行人、车、物、行为等实时检测分析&#xff0c;上报识别结果&#xff0c;并能进行语音告警播放。算法可按需组合、按…

uniapp中uview组件库Toast 消息提示 的使用方法

目录 #基本使用 #配置toast主题 #toast结束跳转URL #API #Props #Params #Methods 此组件表现形式类似uni的uni.showToastAPI&#xff0c;但也有不同的地方&#xff0c;具体表现在&#xff1a; uView的toast有5种主题可选可以配置toast结束后&#xff0c;跳转相应URL目…
最新文章