• Spring 系列(三):你真的懂@RequestMapping吗?


    前言

    上篇给大家介绍了Spring MVC父子容器的概念,主要提到的知识点是:

    Spring MVC容器是Spring容器的子容器,当在Spring MVC容器中找不到bean的时候就去父容器找。

    在文章最后我也给大家也留了一个问题,既然子容器找不到就去父容器找,那干脆把bean定义都放在父容器不就行了?是这样吗,我们做个实验。

    我们把<context:component-scan base-package="xx.xx.xx"/> 这条语句从spring-mvc.xml文件中挪到spring.xml中,重启应用。会发现报404,如下图:

    404说明请求的资源没有找到,为什么呢?

    使用Spring MVC的同学一般都会以下方式定义请求地址:

    1. @Controller
    2. @RequestMapping("/test")
    3. public class Test {
    4. @RequestMapping(value="/handle", method=RequestMethod.POST)
    5. public void handle();
    6. }

    @Controller注解用来把一个类定义为Controller。

    @RequestMapping注解用来把web请求映射到相应的处理函数。

    @Controller和@RequestMapping结合起来完成了Spring MVC请求的派发流程。

    为什么两个简单的注解就能完成这么复杂的功能呢?

    这又和<context:component-scan base-package="xx.xx.xx"/>的位置有什么关系呢?

    我们开始从官网和源码两方面展开分析。

    2.@RequestMapping流程分析

    @RequestMapping流程可以分为下面6步:

    (1)注册
    RequestMappingHandlerMapping bean 。

    (2) 实例化
    RequestMappingHandlerMapping bean。

    (3)获取
    RequestMappingHandlerMapping bean实例。

    (4)接收requst请求。

    (5)在
    RequestMappingHandlerMapping实例中查找对应的handler。

    (6)handler处理请求。

    为什么是这6步,我们展开分析。

    2.1 注册
    RequestMappingHandlerMapping bean

    第一步还是先找程序入口。

    使用Spring MVC的同学都知道,要想使@RequestMapping注解生效,必须得在xml配置文件中配置< mvc:annotation-driven/>。因此我们以此为突破口开始分析。

    在Spring系列(一)文中我们知道xml配置文件解析完的下一步就是解析bean。在这里我们继续对那个方法展开分析。如下:

    1. protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
    2. //如果该元素属于默认命名空间走此逻辑。Spring的默认namespace为:http://www.springframework.org/schema/beans“
    3. if (delegate.isDefaultNamespace(root)) {
    4. NodeList nl = root.getChildNodes();
    5. for (int i = 0; i < nl.getLength(); i++) {
    6. Node node = nl.item(i);
    7. if (node instanceof Element) {
    8. Element ele = (Element) node;
    9. //对document中的每个元素都判断其所属命名空间,然后走相应的解析逻辑
    10. if (delegate.isDefaultNamespace(ele)) {
    11. parseDefaultElement(ele, delegate);
    12. }
    13. else {
    14. //如果该元素属于自定义namespace走此逻辑 ,比如AOP,MVC等。
    15. delegate.parseCustomElement(ele);
    16. }
    17. }
    18. }
    19. }
    20. else {
    21. //如果该元素属于自定义namespace走此逻辑 ,比如AOP,MVC等。
    22. delegate.parseCustomElement(root);
    23. }
    24. }

    方法中根据元素的命名空间来进行不同的逻辑处理,如bean、beans等属于默认命名空间执行parseDefaultElement()方法,其它命名空间执行parseCustomElement()方法。

    <mvc:annotation-driven/>元素属于mvc命名空间,因此进入到parseCustomElement()方法。

    1. public BeanDefinition parseCustomElement(Element ele) {
    2. //解析自定义元素
    3. return parseCustomElement(ele, null);
    4. }

    进入parseCustomElement(ele, null)方法。

    1. public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {
    2. //获取该元素namespace url
    3. String namespaceUri = getNamespaceURI(ele);
    4. //得到NamespaceHandlerSupport实现类解析元素
    5. NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
    6. return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
    7. }

    进入NamespaceHandlerSupport类的parse()方法。

    1. @Override
    2. public BeanDefinition parse(Element element, ParserContext parserContext) {
    3. //此处得到AnnotationDrivenBeanDefinitionParser类来解析该元素
    4. return findParserForElement(element, parserContext).parse(element, parserContext);
    5. }

    上面方法分为两步,(1)获取元素的解析类。(2)解析元素。

    (1) 获取解析类

    1. private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
    2. String localName = parserContext.getDelegate().getLocalName(element);
    3. BeanDefinitionParser parser = this.parsers.get(localName);
    4. return parser;
    5. }

    Spring MVC中含有多种命名空间,此方法会根据元素所属命名空间得到相应解析类,其中< mvc:annotation-driven/>对应的是
    AnnotationDrivenBeanDefinitionParser解析类。

    (2) 解析< mvc:annotation-driven/>元素

    进入
    AnnotationDrivenBeanDefinitionParser类的parse()方法。

    1. @Override
    2. public BeanDefinition parse(Element element, ParserContext context) {
    3. Object source = context.extractSource(element);
    4. XmlReaderContext readerContext = context.getReaderContext();
    5. //生成RequestMappingHandlerMapping bean信息
    6. RootBeanDefinition handlerMappingDef = new RootBeanDefinition(RequestMappingHandlerMapping.class);
    7. handlerMappingDef.setSource(source);
    8. handlerMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
    9. handlerMappingDef.getPropertyValues().add("order", 0);
    10. handlerMappingDef.getPropertyValues().add("contentNegotiationManager", contentNegotiationManager);
    11. //此处HANDLER_MAPPING_BEAN_NAME值为:RequestMappingHandlerMapping类名
    12. //容器中注册name为RequestMappingHandlerMapping类名
    13. context.registerComponent(new BeanComponentDefinition(handlerMappingDef, HANDLER_MAPPING_BEAN_NAME));
    14. }

    可以看到上面方法在Spring MVC容器中注册了一个名为“HANDLER_MAPPING_BEAN_NAME”,类型为
    RequestMappingHandlerMapping的bean。

    至于这个bean能干吗,继续往下分析。

    2.2.
    RequestMappingHandlerMapping bean实例化

    bean注册完后的下一步就是实例化。

    在开始分析实例化流程之前,我们先介绍一下
    RequestMappingHandlerMapping是个什么样类。

    2.2.1
    RequestMappingHandlerMapping继承图

    上图信息比较多,我们查找关键信息。可以看到这个类间接实现了HandlerMapping接口,是HandlerMapping类型的实例。

    除此之外还实现了ApplicationContextAware和IntitalzingBean 这两个接口。

    在这里简要介绍一下这两个接口:

    2.2.2 ApplicationContextAware接口

    下面是官方介绍:

    public interface ApplicationContextAware extends Aware

    Interface to be implemented by any object that wishes to be notified of the ApplicationContext that it runs in.

    https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/ApplicationContextAware.html

    该接口只包含以下方法:

    1. void setApplicationContext(ApplicationContext applicationContext)
    2. throws BeansException
    3. 描述:
    4. Set the ApplicationContext that this object runs in.
    5. Normally this call will be used to initialize the object.

    概括一下上面表达的信息:如果一个类实现了ApplicationContextAware接口,Spring容器在初始化该类时候会自动回调该类的setApplicationContext()方法。这个接口主要用来让实现类得到Spring 容器上下文信息。

    2.2.3 IntitalzingBean接口

    下面是官方介绍:

    public interface InitializingBean

    Interface to be implemented by beans that need to react once all their properties have been set by a BeanFactory: e.g. to perform custom initialization, or merely to check that all mandatory properties have been set.

    https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/InitializingBean.html

    该接口只包含以下方法:

    1. void afterPropertiesSet() throws Exception
    2. 描述:
    3. Invoked by the containing BeanFactory after it has set all bean properties and satisfied BeanFactoryAware, ApplicationContextAware etc.

    概括一下上面表达的信息:如果一个bean实现了该接口,Spring 容器初始化bean时会回调afterPropertiesSet()方法。这个接口的主要作用是让bean在初始化时可以实现一些自定义的操作。

    介绍完
    RequestMappingHandlerMapping类后我们开始对这个类的源码进行分析。

    2.2.4
    RequestMappingHandlerMapping类源码分析

    既然
    RequestMappingHandlerMapping实现了ApplicationContextAware接口,那实例化时候肯定会执行setApplicationContext方法,我们查看其实现逻辑。

    1. @Override
    2. public final void setApplicationContext(ApplicationContext context) throws BeansException {
    3. if (this.applicationContext == null) {
    4. this.applicationContext = context;
    5. }
    6. }

    可以看到此方法把容器上下文赋值给applicationContext变量,因为现在是执行Spring MVC容器创建的流程,因此此处设置的值就是Spring MVC容器 。


    RequestMappingHandlerMapping也实现了InitializingBean接口,当设置完属性后肯定会回调afterPropertiesSet方法,再看afterPropertiesSet方法逻辑。

    1. @Override
    2. public void afterPropertiesSet()
    3. super.afterPropertiesSet();
    4. }

    上面调用了父类的afterPropertiesSet()方法,沿调用栈继续查看。

    @Override

    1. public void afterPropertiesSet() {
    2. //初始化handler函数
    3. initHandlerMethods();
    4. }

    进入initHandlerMethods初始化方法查看逻辑。

    1. protected void initHandlerMethods() {
    2. //1.获取容器中所有bean 的name。
    3. //根据detectHandlerMethodsInAncestorContexts bool变量的值判断是否获取父容器中的bean,默认为false。因此这里只获取Spring MVC容器中的bean,不去查找父容器
    4. String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ?
    5. BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) :
    6. getApplicationContext().getBeanNamesForType(Object.class));
    7. //循环遍历bean
    8. for (String beanName : beanNames) {
    9. //2.判断bean是否含有@Controller或者@RequestMappin注解
    10. if (beanType != null && isHandler(beanType)) {
    11. //3.对含有注解的bean进行处理,获取handler函数信息。
    12. detectHandlerMethods(beanName);
    13. }
    14. }

    上面函数分为3步。

    (1)获取Spring MVC容器中的bean。

    (2)找出含有含有@Controller或者@RequestMappin注解的bean。

    (3)对含有注解的bean进行解析。

    第1步很简单就是获取容器中所有的bean name,我们对第2、3步展开分析。

    查看isHandler()方法实现逻辑。

    1. @Override
    2. protected boolean isHandler(Class<?> beanType) {
    3. return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
    4. AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
    5. }

    上面逻辑很简单,就是判断该bean是否有@Controller或@RequestMapping注解,然后返回判断结果。从这里知道为什么@Controller和@RequestMapping需要一起使用了吧。

    继续分析,如果含有这两个注解之一就进入detectHandlerMethods()方法进行处理。

    查看detectHandlerMethods()方法。

    1. protected void detectHandlerMethods(final Object handler) {
    2. //1.获取bean的类信息
    3. Class<?> handlerType = (handler instanceof String ?
    4. getApplicationContext().getType((String) handler) : handler.getClass());
    5. final Class<?> userType = ClassUtils.getUserClass(handlerType);
    6. //2.遍历函数获取有@RequestMapping注解的函数信息
    7. Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
    8. new MethodIntrospector.MetadataLookup<T>() {
    9. @Override
    10. public T inspect(Method method) {
    11. try {
    12. //如果有@RequestMapping注解,则获取函数映射信息
    13. return getMappingForMethod(method, userType);
    14. }
    15. });
    16. //3.遍历映射函数列表,注册handler
    17. for (Map.Entry<Method, T> entry : methods.entrySet()) {
    18. Method invocableMethod = AopUtils.selectInvocableMethod(entry.getKey(), userType);
    19. T mapping = entry.getValue();
    20. //4.注册handler函数
    21. registerHandlerMethod(handler, invocableMethod, mapping);
    22. }
    23. }

    上面方法中用了几个回调,可能看起来比较复杂,其主要功能就是获取该bean和父接口中所有用@RequestMapping注解的函数信息,并把这些保存到methodMap变量中。

    我们对上面方法进行逐步分析,看看对有@RequestMapping注解的函数是如何进行解析的。

    先进入selectMethods()方法查看实现逻辑。

    1. public static <T> Map<Method, T> selectMethods(Class<?> targetType, final MetadataLookup<T> metadataLookup) {
    2. final Map<Method, T> methodMap = new LinkedHashMap<Method, T>();
    3. Set<Class<?>> handlerTypes = new LinkedHashSet<Class<?>>();
    4. Class<?> specificHandlerType = null;
    5. //把自身类添加到handlerTypes中
    6. if (!Proxy.isProxyClass(targetType)) {
    7. handlerTypes.add(targetType);
    8. specificHandlerType = targetType;
    9. }
    10. //获取该bean所有的接口,并添加到handlerTypes中
    11. handlerTypes.addAll(Arrays.asList(targetType.getInterfaces()));
    12. //对自己及所有实现接口类进行遍历
    13. for (Class<?> currentHandlerType : handlerTypes) {
    14. final Class<?> targetClass = (specificHandlerType != null ? specificHandlerType : currentHandlerType);
    15. //获取函数映射信息
    16. ReflectionUtils.doWithMethods(currentHandlerType, new ReflectionUtils.MethodCallback() {
    17. //循环获取类中的每个函数,通过回调处理
    18. @Override
    19. public void doWith(Method method) {
    20. //对类中的每个函数进行处理
    21. Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
    22. //回调inspect()方法给个函数生成RequestMappingInfo
    23. T result = metadataLookup.inspect(specificMethod);
    24. if (result != null) {
    25. //将生成的RequestMappingInfo保存到methodMap中
    26. methodMap.put(specificMethod, result);
    27. }
    28. }
    29. }, ReflectionUtils.USER_DECLARED_METHODS);
    30. }
    31. //返回保存函数映射信息后的methodMap
    32. return methodMap;
    33. }

    上面逻辑中doWith()回调了inspect(),inspect()又回调了getMappingForMethod()方法。

    我们看看getMappingForMethod()是如何生成函数信息的。

    1. protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
    2. //创建函数信息
    3. RequestMappingInfo info = createRequestMappingInfo(method);
    4. return info;
    5. }

    查看createRequestMappingInfo()方法。

    1. private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
    2. //如果该函数含有@RequestMapping注解,则根据其注解信息生成RequestMapping实例,
    3. //如果该函数没有@RequestMapping注解则返回空
    4. RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
    5. //如果requestMapping不为空,则生成函数信息MAP后返回
    6. return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
    7. }

    看看createRequestMappingInfo是如何实现的。

    1. protected RequestMappingInfo createRequestMappingInfo(
    2. RequestMapping requestMapping, RequestCondition<?> customCondition) {
    3. return RequestMappingInfo
    4. .paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
    5. .methods(requestMapping.method())
    6. .params(requestMapping.params())
    7. .headers(requestMapping.headers())
    8. .consumes(requestMapping.consumes())
    9. .produces(requestMapping.produces())
    10. .mappingName(requestMapping.name())
    11. .customCondition(customCondition)
    12. .options(this.config)
    13. .build();
    14. }

    可以看到上面把RequestMapping注解中的信息都放到一个RequestMappingInfo实例中后返回。

    当生成含有@RequestMapping注解的函数映射信息后,最后一步是调用registerHandlerMethod 注册handler和处理函数映射关系。

    1. protected void registerHandlerMethod(Object handler, Method method, T mapping) {
    2. this.mappingRegistry.register(mapping, handler, method);
    3. }

    上面方法中把所有的handler函数都注册到了mappingRegistry这个变量中。

    到此就把
    RequestMappingHandlerMapping bean的实例化流程就分析完了。

    2.3 获取RequestMapping bean

    这里我们回到Spring MVC容器初始化流程,查看initWebApplicationContext方法。

    1. protected WebApplicationContext initWebApplicationContext() {
    2. //1.获得rootWebApplicationContext
    3. WebApplicationContext rootContext =
    4. WebApplicationContextUtils.getWebApplicationContext(getServletContext());
    5. WebApplicationContext wac = null;
    6. if (wac == null) {
    7. //2.创建 Spring 容器
    8. wac = createWebApplicationContext(rootContext);
    9. }
    10. //3.初始化容器
    11. onRefresh(wac);
    12. return wac;
    13. }

    前2步我们在Spring 系列(二):Spring MVC的父子容器一文中分析过,主要是创建Spring MVC容器,这里我们重点看第3步。

    进入onRefresh()方法。

    1. @Override
    2. protected void onRefresh(ApplicationContext context) {
    3. //执行初始化策略
    4. initStrategies(context);
    5. }

    进入initStrategies方法,该方法进行了很多初始化行为,为减少干扰我们只过滤出与本文相关内容。

    1. protected void initStrategies(ApplicationContext context) {
    2. //初始化HandlerMapping
    3. initHandlerMappings(context);
    4. }

    进入initHandlerMappings()方法。

    1. private void initHandlerMappings(ApplicationContext context) {
    2. //容器中查找name为"ANDLER_MAPPING_BEAN_NAME"的实例
    3. HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
    4. //把找到的bean放到hanlderMappings中。
    5. this.handlerMappings = Collections.singletonList(hm);
    6. }

    此处我们看到从容器中获取了name为“HANDLER_MAPPING_BEAN_NAME”的bean,这个bean大家应该还记得吧,就是前面注册并实例化了的
    RequestMappingHandlerMapping bean。

    2.4 接收请求

    DispatchServlet继承自Servlet,那所有的请求都会在service()方法中进行处理。

    查看service()方法。

    1. @Override
    2. protected void service(HttpServletRequest request, HttpServletResponse response) {
    3. //获取请求方法
    4. HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
    5. //若是patch请求执行此逻辑
    6. if (httpMethod == HttpMethod.PATCH || httpMethod == null) {
    7. processRequest(request, response);
    8. }
    9. //其它请求走此逻辑
    10. else {
    11. super.service(request, response);
    12. }
    13. }

    我们以get、post请求举例分析。查看父类service方法实现。

    1. protected void service(HttpServletRequest req, HttpServletResponse resp){
    2. String method = req.getMethod();
    3. if (method.equals(METHOD_GET)) {
    4. //处理get请求
    5. doGet(req, resp);
    6. } else if (method.equals(METHOD_POST)) {
    7. //处理post请求
    8. doPost(req, resp)
    9. }
    10. }

    查看doGet()、doPost()方法实现。

    1. @Override
    2. protected final void doGet(HttpServletRequest request, HttpServletResponse response){
    3. processRequest(request, response);
    4. }
    5. @Override
    6. protected final void doPost(HttpServletRequest request, HttpServletResponse response {
    7. processRequest(request, response);
    8. }

    可以看到都调用了processRequest()方法,继续跟踪。

    1. protected final void processRequest(HttpServletRequest request, HttpServletResponse response){
    2. //处理请求
    3. doService(request, response);
    4. }

    查看doService()方法。

    @Override

    1. protected void doService(HttpServletRequest request, HttpServletResponse response) {
    2. //处理请求
    3. doDispatch(request, response);
    4. }

    2.5 获取handler

    最终所有的web请求都由doDispatch()方法进行处理,查看其逻辑。

    1. protected void doDispatch(HttpServletRequest request, HttpServletResponse response) {
    2. HttpServletRequest processedRequest = request;
    3. // 根据请求获得真正处理的handler
    4. mappedHandler = getHandler(processedRequest);
    5. //用得到的handler处理请求,此处省略
    6. 。。。。
    7. }

    查看getHandler()。

    1. protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    2. //获取HandlerMapping实例
    3. for (HandlerMapping hm : this.handlerMappings) {
    4. //得到处理请求的handler
    5. HandlerExecutionChain handler = hm.getHandler(request);
    6. if (handler != null) {
    7. return handler;
    8. }
    9. }
    10. return null;
    11. }

    这里遍历handlerMappings获得所有HandlerMapping实例,还记得handlerMappings变量吧,这就是前面initHandlerMappings()方法中设置进去的值。

    可以看到接下来调了用HandlerMapping实例的getHanlder()方法查找handler,看其实现逻辑。

    1. @Override
    2. public final HandlerExecutionChain getHandler(HttpServletRequest request) {
    3. Object handler = getHandlerInternal(request);
    4. }

    进入getHandlerInternal()方法。

    1. @Override
    2. protected HandlerMethod getHandlerInternal(HttpServletRequest request) {
    3. //获取函数url
    4. String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
    5. //查找HandlerMethod
    6. handlerMethod = lookupHandlerMethod(lookupPath, request);
    7. }

    进入lookupHandlerMethod()。

    1. protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) {
    2. this.mappingRegistry.getMappingsByUrl(lookupPath);
    3. }

    可以看到上面方法中从mappingRegistry获取handler,这个mappingRegistry的值还记得是从哪里来的吗?

    就是前面
    RequestMappingHandlerMapping 实例化过程的最后一步调用registerHandlerMethod()函数时设置进去的。

    2.6 handler处理请求

    获取到相应的handler后剩下的事情就是进行业务逻辑。处理后返回结果,这里基本也没什么好说的。

    到此整个@RequestMapping的流程也分析完毕。

    3.小结

    认真读完上面深入分析@RequestMapping注解流程的同学,相信此时肯定对Spring MVC有了更深一步的认识。

    现在回到文章开头的那个问题,为什么把<context:component-scan base-package="xx.xx.xx"/>挪到spring.xml文件中后就会404了呢?

    我想看明白此文章的同学肯定已经知道答案了。

    答案是:

    当把<context:component-scan base-package="xx.xx.xx"/>写到spring.xml中时,所有的bean其实都实例化在了Spring父容器中。

    但是在@ReqestMapping解析过程中,initHandlerMethods()函数只是对Spring MVC 容器中的bean进行处理的,并没有去查找父容器的bean。因此不会对父容器中含有@RequestMapping注解的函数进行处理,因此不会生成相应的handler。

    所以当请求过来时找不到处理的handler,导致404。

    4.尾声

    从上面的分析中,我们知道要使用@RequestMapping注解,必须得把含有@RequestMapping的bean定义到spring-mvc.xml中。

    这里也给大家个建议:

    因为@RequestMapping一般会和@Controller搭配使。为了防止重复注册bean,建议在spring-mvc.xml配置文件中只扫描含有Controller bean的包,其它bean的扫描定义到spring.xml文件中。写法如下:

    spring-mvc.xml

    1. <!-- 只扫描@Controller注解 -->
    2. <context:component-scan base-package="com.xxx.controller" use-default-filters="false"
    3. >
    4. <context:include-filter type="annotation"
    5. expression="org.springframework.stereotype.Controller" />
    6. </context:component-scan>

    spring.xml

    1. <!-- 配置扫描注解,不扫描@Controller注解 -->
    2. <context:component-scan base-package="com.xxx">
    3. <context:exclude-filter type="annotation"
    4. expression="org.springframework.stereotype.Controller" />
    5. </context:component-scan>

    use-default-filters属性默认为true,会扫描所有注解类型的bean 。如果配置成false,就只扫描白名单中定义的bean注解。

    本文完。

    推荐阅读:

    Spring系列(一):Spring MVC bean 解析、注册、实例化流程源码剖析

    Spring 系列(二):Spring MVC的父子容器

     

     

  • 相关阅读:
    Leetcode 805. 数组的均值分割
    idea 一直卡在maven正在解析maven依赖
    python判断语句
    痞子衡嵌入式:恩智浦i.MX RT1xxx系列MCU启动那些事(10.A)- FlexSPI NAND启动时间(RT1170)
    DOCTYPE是什么,有何作用、 使用方式、渲染模式、严格模式和怪异模式的区别?
    14基于MATLAB的鲸鱼优化VMD参数,以熵值为适应度函数,对VMD参数惩罚因子和层数进行寻优,确定最优值并进行信号分解,程序已调通,可以直接运行。
    如何使用Goland进行远程Go项目线上调试?
    【Python机器学习】零基础掌握SparsePCA矩阵分解
    学习如何使用最强大的 JavaScript 函数
    【进阶】Spring中的注解与反射
  • 原文地址:https://blog.csdn.net/Firstlucky77/article/details/125427414