SpringMVC

原理流程图

https://www.processon.com/view/link/63f1d5cc2f69f86c1f96ee9c

我们在使用SpringMVC时,传统的方式是通过定义web.xml,比如:

xml 复制代码
<web-app>

    <servlet>
        <servlet-name>app</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/spring.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>app</servlet-name>
        <url-pattern>/app/*</url-pattern>
    </servlet-mapping>

</web-app>

我们只要定义这样的一个web.xml,然后启动Tomcat,那么我们就能正常使用SpringMVC了。

SpringMVC中,最为核心的就是DispatcherServlet,在启动Tomcat的过程中:

  1. Tomcat会先创建DispatcherServlet对象
  2. 然后调用DispatcherServlet对象的init()

而在init()方法中,会创建一个Spring容器,并且添加一个ContextRefreshListener监听器,该监听器会监听ContextRefreshedEvent事件(Spring容器启动完成后就会发布这个事件),也就是说Spring容器启动完成后,就会执行ContextRefreshListener中的onApplicationEvent()方法,从而最终会执行DispatcherServlet中的initStrategies(),这个方法中会初始化更多内容:

java 复制代码
protected void initStrategies(ApplicationContext context) {
        initMultipartResolver(context);
        initLocaleResolver(context);
        initThemeResolver(context);
        initHandlerMappings(context);
        initHandlerAdapters(context);
        initHandlerExceptionResolvers(context);
        initRequestToViewNameTranslator(context);
        initViewResolvers(context);
        initFlashMapManager(context);
}

其中最为核心的就是HandlerMappingHandlerAdapter

什么是Handler?

Handler表示请求处理器,在SpringMVC中有四种Handler:

  1. 实现了Controller接口的Bean对象
  2. 实现了HttpRequestHandler接口的Bean对象
  3. 添加了@RequestMapping注解的方法
  4. 一个HandlerFunction对象

比如实现了Controller接口的Bean对象:

java 复制代码
@Component("/test")
public class BeanNameController implements Controller {
	@Override
	public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse 				response) throws Exception {
    	System.out.println("xxx");
    	return new ModelAndView();
    }
}

实现了HttpRequestHandler接口的Bean对象:

java 复制代码
@Component("/test")
public class BeanNameController implements HttpRequestHandler {
    @Override
    public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            System.out.println("xxx");
    }
}

添加了@RequestMapping注解的方法:

java 复制代码
@RequestMapping
@Component
public class Controller {
    @Autowired
    private Service service;

    @RequestMapping(method = RequestMethod.GET, path = "/test")
    @ResponseBody
    public String test(String username) {
        return "xxx";
    }
}

一个HandlerFunction对象(以下代码中有两个):

java 复制代码
@ComponentScan("com.xxx")
@Configuration
public class AppConfig {
    @Bean
    public RouterFunction<ServerResponse> person() {
        return route().GET("/app/person", request -> ServerResponse.status(HttpStatus.OK).body("Hello GET")).POST("/app/person", request -> ServerResponse.status(HttpStatus.OK).body("Hello POST")).build();
    }
}

什么是HandlerMapping?

HandlerMapping负责去寻找Handler,并且保存路径和Handler之间的映射关系。

因为有不同类型的Handler,所以在SpringMVC中会由不同的HandlerMapping来负责寻找Handler,比如:

  1. BeanNameUrlHandlerMapping:负责Controller接口和HttpRequestHandler接口
  2. RequestMappingHandlerMapping:负责@RequestMapping的方法
  3. RouterFunctionMapping:负责RouterFunction以及其中的HandlerFunction

BeanNameUrlHandlerMapping的寻找流程:

  1. 找出Spring容器中所有的beanName
  2. 判断beanName是不是以"/"开头
  3. 如果是,则把它当作一个Handler,并把beanName作为key,bean对象作为value存入handlerMap
  4. handlerMap就是一个Map

RequestMappingHandlerMapping的寻找流程:

  1. 找出Spring容器中所有beanType
  2. 判断beanType是不是有@Controller注解,或者是不是有@RequestMapping注解
  3. 判断成功则继续找beanType中加了@RequestMapping的Method
  4. 并解析@RequestMapping中的内容,比如method、path,封装为一个RequestMappingInfo对象
  5. 最后把RequestMappingInfo对象做为key,Method对象封装为HandlerMethod对象后作为value,存入registry
  6. registry就是一个Map

RouterFunctionMapping的寻找流程会有些区别,但是大体是差不多的,相当于是一个path对应一个HandlerFunction。

各个HandlerMapping除开负责寻找Handler并记录映射关系之外,自然还需要根据请求路径找到对应的Handler,在源码中这三个HandlerMapping有一个共同的父类AbstractHandlerMapping

AbstractHandlerMapping实现了HandlerMapping接口,并实现了getHandler(HttpServletRequest request)方法。

AbstractHandlerMapping会负责调用子类的getHandlerInternal(HttpServletRequest request)方法从而找到请求对应的Handler,然后AbstractHandlerMapping负责将Handler和应用中所配置的HandlerInterceptor整合成为一个HandlerExecutionChain对象。

所以寻找Handler的源码实现在各个HandlerMapping子类中的getHandlerInternal()中,根据请求路径找到Handler的过程并不复杂,因为路径和Handler的映射关系已经存在Map中了。

比较困难的点在于,当DispatcherServlet接收到一个请求时,该利用哪个HandlerMapping来寻找Handler呢?看源码:

java 复制代码
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    if (this.handlerMappings != null) {
        for (HandlerMapping mapping : this.handlerMappings) {
            HandlerExecutionChain handler = mapping.getHandler(request);
            if (handler != null) {
                return handler;
            }
        }
    }
    return null;
}

很简单,就是遍历,找到就返回,默认顺序为:

所以BeanNameUrlHandlerMapping的优先级最高,比如:

java 复制代码
@Component("/test")
public class BeanNameController implements Controller {
    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
        System.out.println("Hello xxx");
        return new ModelAndView();
    }
}


@RequestMapping(method = RequestMethod.GET, path = "/test")
@ResponseBody
public String test(String username) {
    return "Hi xxx";
}

请求路径都是/test,但是最终是Controller接口的会生效。

什么是HandlerAdapter?

找到了Handler之后,接下来就该去执行了,比如执行下面这个test()

java 复制代码
@RequestMapping(method = RequestMethod.GET, path = "/test") 
@ResponseBody 
public String test(String username) {     
    return "xxx"; 
}

但是由于有不同种类的Handler,所以执行方式是不一样的,再来总结一下Handler的类型:

  1. 实现了Controller接口的Bean对象,执行的是Bean对象中的handleRequest()
  2. 实现了HttpRequestHandler接口的Bean对象,执行的是Bean对象中的handleRequest()
  3. 添加了@RequestMapping注解的方法,具体为一个HandlerMethod,执行的就是当前加了注解的方法
  4. 一个HandlerFunction对象,执行的是HandlerFunction对象中的handle()

所以,按逻辑来说,找到Handler之后,我们得判断它的类型,比如代码可能是这样的:

java 复制代码
Object handler = mappedHandler.getHandler(); 
if(handler instanceof Controller)

{
    ((Controller) handler).handleRequest(request, response);
} else if(handler instanceof HttpRequestHandler)

{
    ((HttpRequestHandler) handler).handleRequest(request, response);
} else if(handler instanceof HandlerMethod)

{
    ((HandlerMethod) handler).getMethod().invoke(...);
} else if(handler instanceof HandlerFunction)

{
    ((HandlerFunction) handler).handle(...);
}

但是SpringMVC并不是这么写的,还是采用的适配模式,把不同种类的Handler适配成一个HandlerAdapter,后续再执行HandlerAdapter的handle()方法就能执行不同种类Hanlder对应的方法。

针对不同的Handler,会有不同的适配器:

  1. HttpRequestHandlerAdapter
  2. SimpleControllerHandlerAdapter
  3. RequestMappingHandlerAdapter
  4. HandlerFunctionAdapter

适配逻辑为:

java 复制代码
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");
}

传入handler,遍历上面四个Adapter,谁支持就返回谁,比如判断的代码依次为:

java 复制代码
public boolean supports(Object handler) {
    return (handler instanceof HttpRequestHandler);
}

public boolean supports(Object handler) {
    return (handler instanceof Controller);
}

public final boolean supports(Object handler) {
    return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler));
}

public boolean supports(Object handler) {
    return handler instanceof HandlerFunction;
}

根据Handler适配出了对应的HandlerAdapter后,就执行具体HandlerAdapter对象的handle()方法了,比如:

HttpRequestHandlerAdapter的handle():

java 复制代码
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {     
    ((HttpRequestHandler) handler).handleRequest(request, response);     
    return null; 
}

SimpleControllerHandlerAdapter的handle():

java 复制代码
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {     
    return ((Controller) handler).handleRequest(request, response); 
}

HandlerFunctionAdapter的handle():

java 复制代码
HandlerFunction<?> handlerFunction = (HandlerFunction<?>) handler; serverResponse = handlerFunction.handle(serverRequest);

因为这三个接收的直接就是Requeset对象,不用SpringMVC做额外的解析,所以比较简单,比较复杂的是RequestMappingHandlerAdapter,它执行的是加了@RequestMapping的方法,而这种方法的写法可以是多种多样,SpringMVC需要根据方法的定义去解析Request对象,从请求中获取出对应的数据然后传递给方法,并执行。

@RequestMapping方法参数解析

当SpringMVC接收到请求,并找到了对应的Method之后,就要执行该方法了,不过在执行之前需要根据方法定义的参数信息,从请求中获取出对应的数据,然后将数据传给方法并执行。

一个HttpServletRequest通常有:

  1. request parameter
  2. request attribute
  3. request session
  4. reqeust header
  5. reqeust body

比如如下几个方法:

java 复制代码
public String test(String username) {     
    return "xxx"; 
}

表示要从request parameter中获取key为username的value

java 复制代码
public String test(@RequestParam("uname") String username) {     
    return "xxx"; 
}

表示要从request parameter中获取key为uname的value

java 复制代码
public String test(@RequestAttribute String username) {     
    return "xxx"; 
}

表示要从request attribute中获取key为username的value

java 复制代码
public String test(@SessionAttribute String username) {     
    return "xxx"; 
}

表示要从request session中获取key为username的value

java 复制代码
public String test(@RequestHeader String username) {     
    return "xxx"; 
}

表示要从request header中获取key为username的value

java 复制代码
public String test(@RequestBody String username) {     
    return "xxx"; 
}

表示获取整个请求体

所以,我们发现SpringMVC要去解析方法参数,看该参数到底是要获取请求中的哪些信息。

而这个过程,源码中是通过HandlerMethodArgumentResolver来实现的,比如:

  1. RequestParamMethodArgumentResolver:负责处理@RequestParam
  2. RequestHeaderMethodArgumentResolver:负责处理@RequestHeader
  3. SessionAttributeMethodArgumentResolver:负责处理@SessionAttribute
  4. RequestAttributeMethodArgumentResolver:负责处理@RequestAttribute
  5. RequestResponseBodyMethodProcessor:负责处理@RequestBody
  6. 还有很多其他的...

而在判断某个参数该由哪个HandlerMethodArgumentResolver处理时,也是很粗暴:

java 复制代码
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
    HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
    if (result == null) {
        for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
            if (resolver.supportsParameter(parameter)) {
                result = resolver;
                this.argumentResolverCache.put(parameter, result);
                break;
            }
        }
    }
    return result;
}

就是遍历所有的HandlerMethodArgumentResolver,哪个能支持处理当前这个参数就由哪个处理。

比如:

java 复制代码
@RequestMapping(method = RequestMethod.GET, path = "/test") 
@ResponseBody public String test(@RequestParam @SessionAttribute String username) {     	System.out.println(username);
    return "xxx"; 
}

以上代码的username将对应RequestParam中的username,而不是session中的,因为在源码中RequestParamMethodArgumentResolver更靠前。

当然HandlerMethodArgumentResolver也会负责从request中获取对应的数据,对应的是resolveArgument()方法。

比如RequestParamMethodArgumentResolver:

java 复制代码
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
    HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
    if (servletRequest != null) {
        Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
        if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) {
            return mpArg;
        }
    }
    Object arg = null;
    MultipartRequest multipartRequest = request.getNativeRequest(MultipartRequest.class);
    if (multipartRequest != null) {
        List<MultipartFile> files = multipartRequest.getFiles(name);
        if (!files.isEmpty()) {
            arg = (files.size() == 1 ? files.get(0) : files);
        }
    }
    if (arg == null) {
        String[] paramValues = request.getParameterValues(name);
        if (paramValues != null) {
            arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
        }
    }
    return arg;
}

核心是:

java 复制代码
if (arg == null) {     
    String[] paramValues = request.getParameterValues(name);     
    if (paramValues != null) {         
        arg = (paramValues.length == 1 ? paramValues[0] : paramValues);     
    }
}

按同样的思路,可以找到方法中每个参数所要求的值,从而执行方法,得到方法的返回值。

@RequestMapping方法返回值解析

而方法返回值,也会分为不同的情况。比如有没有加@ResponseBody注解,如果方法返回一个String:

  1. 加了@ResponseBody注解:表示直接将这个String返回给浏览器
  2. 没有加@ResponseBody注解:表示应该根据这个String找到对应的页面,把页面返回给浏览器

在SpringMVC中,会利用HandlerMethodReturnValueHandler来处理返回值:

  1. RequestResponseBodyMethodProcessor:处理加了@ResponseBody注解的情况
  2. ViewNameMethodReturnValueHandler:处理没有加@ResponseBody注解并且返回值类型为String的情况
  3. ModelMethodProcessor:处理返回值是Model类型的情况
  4. 还有很多其他的...

我们这里只讲RequestResponseBodyMethodProcessor,因为它会处理加了@ResponseBody注解的情况,也是目前我们用得最多的情况。

RequestResponseBodyMethodProcessor相当于会把方法返回的对象直接响应给浏览器,如果返回的是一个字符串,那么好说,直接把字符串响应给浏览器,那如果返回的是一个Map呢?是一个User对象呢?该怎么把这些复杂对象响应给浏览器呢?

处理这块,SpringMVC会利用HttpMessageConverter来处理,比如默认情况下,SpringMVC会有4个HttpMessageConverter:

  1. ByteArrayHttpMessageConverter:处理返回值为字节数组的情况,把字节数组返回给浏览器
  2. StringHttpMessageConverter:处理返回值为字符串的情况,把字符串按指定的编码序列号后返回给浏览器
  3. SourceHttpMessageConverter:处理返回值为XML对象的情况,比如把DOMSource对象返回给浏览器
  4. AllEncompassingFormHttpMessageConverter:处理返回值为MultiValueMap对象的情况

StringHttpMessageConverter的源码也比较简单:

java 复制代码
protected void writeInternal(String str, HttpOutputMessage outputMessage) throws IOException {
    HttpHeaders headers = outputMessage.getHeaders();
    if (this.writeAcceptCharset && headers.get(HttpHeaders.ACCEPT_CHARSET) == null) {
        headers.setAcceptCharset(getAcceptedCharsets());
    }
    Charset charset = getContentTypeCharset(headers.getContentType());
    StreamUtils.copy(str, charset, outputMessage.getBody());
}

先看有没有设置Content-Type,如果没有设置则取默认的,默认为ISO-8859-1,所以默认情况下返回中文会乱码,可以通过以下来中方式来解决:

java 复制代码
@ComponentScan("com.xxx")
@Configuration
@EnableWebMvc
public class AppConfig implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        StringHttpMessageConverter messageConverter = new StringHttpMessageConverter();
        messageConverter.setDefaultCharset(StandardCharsets.UTF_8);
        converters.add(messageConverter);
    }
}

不过以上四个Converter是不能处理Map对象或User对象的,所以如果返回的是Map或User对象,那么得单独配置一个Converter,比如MappingJackson2HttpMessageConverter,这个Converter比较强大,能把String、Map、User对象等等都能转化成JSON格式。

java 复制代码
@ComponentScan("com.xxx")
@Configuration
@EnableWebMvc
public class AppConfig implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
        messageConverter.setDefaultCharset(StandardCharsets.UTF_8);
        converters.add(messageConverter);
    }
}

具体转化的逻辑就是Jackson2的转化逻辑。

SpringMVC处理请求核心流程图

https://www.processon.com/view/link/63f4cf1176e6143857799c2a

SpringMVC父子容器

我们可以在web.xml文件中这么来定义:

xml 复制代码
<web-app>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/spring.xml</param-value>
    </context-param>

    <servlet>
        <servlet-name>app</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/spring-mvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>app</servlet-name>
        <url-pattern>/app/*</url-pattern>
    </servlet-mapping>

</web-app>

在这个web.xml文件中,我们定义了一个listener和servlet。

父容器的创建

ContextLoaderListener的作用是用来创建一个Spring容器,就是我们说的SpringMVC父子容器中的父容器,执行流程为:

  1. Tomcat启动,解析web.xml时
  2. 发现定义了一个ContextLoaderListener,Tomcat就会执行该listener中的contextInitialized()方法,该方法就会去创建要给Spring容器
  3. 从ServletContext中获取contextClass参数值,该参数表示所要创建的Spring容器的类型,可以在web.xml中通过来进行配置
  4. 如果没有配置该参数,那么则会从ContextLoader.properties文件中读取org.springframework.web.context.WebApplicationContext配置项的值,SpringMVC默认提供了一个ContextLoader.properties文件,内容为org.springframework.web.context.support.XmlWebApplicationContext
  5. 所以XmlWebApplicationContext就是要创建的Spring容器类型
  6. 确定好类型后,就用反射调用无参构造方法创建出来一个XmlWebApplicationContext对象
  7. 然后继续从ServletContext中获取contextConfigLocation参数的值,也就是一个spring配置文件的路径
  8. 把spring配置文件路径设置给Spring容器,然后调用refresh(),从而启动Spring容器,从而解析spring配置文件,从而扫描生成Bean对象等
  9. 这样Spring容器就创建出来了
  10. 有了Spring容器后,就会把XmlWebApplicationContext对象作为attribute设置到ServletContext中去,key为WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
  11. 把Spring容器存到ServletContext中的原因,是为了给Servlet创建出来的子容器来作为父容器的

子容器的创建

Tomcat启动过程中,执行完ContextLoaderListener的contextInitialized()之后,就会创建DispatcherServlet了,web.xml中定义DispatcherServlet时,load-on-startup为1,表示在Tomcat启动过程中要把这个DispatcherServlet创建并初始化出来,而这个过程是比较费时间的,所以要把load-on-startup设置为1,如果不为1,会在servlet接收到请求时才来创建和初始化,这样会导致请求处理比较慢。

  1. Tomcat启动,解析web.xml时
  2. 创建DispatcherServlet对象
  3. 调用DispatcherServlet的init()
  4. 从而调用initServletBean()
  5. 从而调用initWebApplicationContext(),这个方法也会去创建一个Spring容器(就是子容器)
  6. initWebApplicationContext()执行过程中,会先从ServletContext拿出ContextLoaderListener所创建的Spring容器(父容器),记为rootContext
  7. 然后读取contextClass参数值,可以在servlet中的标签来定义想要创建的Spring容器类型,默认为XmlWebApplicationContext
  8. 然后创建一个Spring容器对象,也就是子容器
  9. 将rootContext作为parent设置给子容器(父子关系的绑定)
  10. 然后读取contextConfigLocation参数值,得到所配置的Spring配置文件路径
  11. 然后就是调用Spring容器的refresh()方法
  12. 从而完成了子容器的创建

SpringMVC初始化

子容器创建完后,还会调用一个DispatcherServlet的onRefresh()方法,这个方法会从Spring容器中获取一些特殊类型的Bean对象,并设置给DispatcherServlet对象中对应的属性,比如HandlerMapping、HandlerAdapter。

流程为:

  1. 会先从Spring容器中获取HandlerMapping类型的Bean对象,如果不为空,那么就获取出来的Bean对象赋值给DispatcherServlet的handlerMappings属性
  2. 如果没有获取到,则会从DispatcherServlet.properties文件中读取配置,从而得到SpringMVC默认给我们配置的HandlerMapping

DispatcherServlet.properties文件内容为:

java 复制代码
# Default implementation classes for DispatcherServlet's strategy interfaces. # Used as fallback when no matching beans are found in the DispatcherServlet context. # Not meant to be customized by application developers.  org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver  org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver  org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\ org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,\ org.springframework.web.servlet.function.support.RouterFunctionMapping  org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\ org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\ org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter,\ org.springframework.web.servlet.function.support.HandlerFunctionAdapter   org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\ org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\ org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver  org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator  org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver  org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager

默认提供了3个HandlerMapping,4个HandlerAdapter,这些概念在后续DispatcherServlet处理请求时都是会用到的。

值得注意的是,从配置文件读出这些类后,是会利用Spring容器去创建出来对应的Bean对象,而不是一个普通的Java对象,而如果是Bean对象,那么就会触发Bean的初始化逻辑,比如RequestMappingHandlerAdapter,后续在分析请求处理逻辑时,会发现这个类是非常重要的,而它就实现了InitializingBean接口,从而Bean对象在创建时会执行afterPropertiesSet()方法。

RequestMappingHandlerAdapter初始化

我们先可以简单理解RequestMappingHandlerAdapter,它的作用就是在收到请求时来调用请求对应的方法的,所以它需要去解析方法参数,方法返回值。

在RequestMappingHandlerAdapter的afterPropertiesSet()方法中,又会做以下事情(这些事情大家可能现在看不懂,可以后面回头再来看,我先列在这):

  1. 从Spring容器中找到加了@ControllerAdvice的Bean对象
    1. 解析出Bean对象中加了@ModelAttribute注解的Method对象,并存在modelAttributeAdviceCache这个Map中
    2. 解析出Bean对象中加了@InitBinder注解的Method对象,并存在initBinderAdviceCache这个Map中
    3. 如果Bean对象实现了RequestBodyAdvice接口或者ResponseBodyAdvice接口,那么就把这个Bean对象记录在requestResponseBodyAdvice集合中
  1. 从Spring容器中获取用户定义的HandlerMethodArgumentResolver,以及SpringMVC默认提供的,整合为一个HandlerMethodArgumentResolverComposite对象,HandlerMethodArgumentResolver是用来解析方法参数
  2. 从Spring容器中获取用户定义的HandlerMethodReturnValueHandler,以及SpringMVC默认提供的,整合为一个HandlerMethodReturnValueHandlerComposite对象,HandlerMethodReturnValueHandler是用来解析方法返回值

以上是RequestMappingHandlerAdapter这个Bean的初始化逻辑。

RequestMappingHandlerMapping初始化

RequestMappingHandlerMapping的作用是,保存我们定义了哪些@RequestMapping方法及对应的访问路径,而RequestMappingHandlerMapping的初始化就是去找到这些映射关系:

  1. 找出容器中定义的所有的beanName
  2. 根据beanName找出beanType
  3. 判断beanType上是否有@Controller注解或@RequestMapping注解,如果有那么就表示这个Bean对象是一个Handler
  4. 如果是一个Handler,就通过反射找出加了@RequestMapping注解的Method,并解析@RequestMapping注解上定义的参数信息,得到一个对应的RequestMappingInfo对象,然后结合beanType上@RequestMapping注解所定义的path,以及当前Method上@RequestMapping注解所定义的path,进行整合,则得到了当前这个Method所对应的访问路径,并设置到RequestMappingInfo对象中去
  5. 所以,一个RequestMappingInfo对象就对应了一个加了@RequestMapping注解的Method,并且请求返回路径也记录在了RequestMappingInfo对象中
  6. 把当前Handler,也就是beanType中的所有RequestMappingInfo都找到后,就会存到MappingRegistry对象中
  7. 在存到MappingRegistry对象过程中,会像把Handler,也就是beanType,以及Method,生成一个HandlerMethod对象,其实就是表示一个方法
  8. 然后获取RequestMappingInfo对象中的path
  9. 把path和HandlerMethod对象存在一个Map中,属性叫做pathLookup
  10. 这样在处理请求时,就可以同请求路径找到HandlerMethod,然后找到Method,然后执行了

WebApplicationInitializer的方式

除开使用web.xml外,我们还可以直接定义一个WebApplicationInitializer来使用SpringMVC,比如:

java 复制代码
public class MyWebApplicationInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletContext) {

        // Load Spring web application configuration
        AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
        context.register(AppConfig.class);

        // Create and register the DispatcherServlet
        DispatcherServlet servlet = new DispatcherServlet(context);
        ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);
        registration.setLoadOnStartup(1);
        registration.addMapping("/*");
    }
}

@ComponentScan("com.xxx")
@Configuration
public class AppConfig  {
}

这种方法我们也能使用SpringMVC,流程为:

  1. Tomcat启动过程中就会调用到我们所写的onStartup()
  2. 从而创建一个Spring容器
  3. 从而创建一个DispatcherServlet对象并初始化
  4. 而DispatcherServlet初始化所做的事情和上述是一样的

那为什么Tomcat启动时能调用到MyWebApplicationInitializer中的onStartup()呢?

这个跟Tomcat的提供的扩展机制有关,在SpringMVC中有这样一个类:

java 复制代码
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {

    @Override
    public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
            throws ServletException {
        // ...
    }

}

这个类实现了javax.servlet.ServletContainerInitializer接口,并且在SpringMVC中还有这样一个文件:META-INF/services/Tomcatjavax.servlet.ServletContainerInitializer,文件内容为org.springframework.web.SpringServletContainerInitializer。

很明显,是SPI,所以Tomcat在启动过程中会找到这个SpringServletContainerInitializer,并执行onStartup(),并且还会找到@HandlesTypes注解中所指定的WebApplicationInitializer接口的实现类,并传递给onStartup()方法,这其中就包括了我们自己定义的MyWebApplicationInitializer。

在SpringServletContainerInitializer的onStartup()中就会调用MyWebApplicationInitializer的onStartup()方法了:

java 复制代码
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
    @Override
    public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {
        List<WebApplicationInitializer> initializers = Collections.emptyList();
        if (webAppInitializerClasses != null) {
            initializers = new ArrayList<>(webAppInitializerClasses.size());
            for (Class<?> waiClass : webAppInitializerClasses) {
                // 过滤掉接口、抽象类                 
                if (!waiClass.isInterface() &&
                        !Modifier.isAbstract(waiClass.getModifiers()) && WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
                    try {
                        // 实例化                         
                        initializers.add((WebApplicationInitializer) ReflectionUtils.accessibleConstructor(waiClass).newInstance());
                    } catch (Throwable ex) {
                        throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
                    }
                }
            }
        }
        if (initializers.isEmpty()) {
            servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
            return;
        }
        servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
        AnnotationAwareOrderComparator.sort(initializers);
        // 调用initializer.onStartup()         
        for (WebApplicationInitializer initializer : initializers) {
            initializer.onStartup(servletContext);
        }
    }
}

方法参数解析

在RequestMappingHandlerAdapter的初始化逻辑中会设置一些默认的HandlerMethodArgumentResolver,他们就是用来解析各种类型的方法参数的。

比如:

  1. RequestParamMethodArgumentResolver,用来解析加了@RequestParam注解的参数,或者什么都没加的基本类型参数(非基本类型的会被ServletModelAttributeMethodProcessor处理)
  2. PathVariableMethodArgumentResolver,用来解析加了@PathVariable注解的参数
  3. RequestHeaderMethodArgumentResolver,用来解析加了@RequestHeader注解的参数

比如RequestParamMethodArgumentResolver中是这么处理的:

java 复制代码
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
    HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
    // ...      
    if (arg == null) {
        String[] paramValues = request.getParameterValues(name);
        if (paramValues != null) {
            arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
        }
    }
    return arg;
}

很简单了,就是把请求中对应的parameterValue拿出来,最为参数值传递给方法。

其他的类似,都是从请求中获取相对应的信息传递给参数。

但是需要注意的是,我们从请求中获取的值可能很多时候都是字符串,那如果参数类型不是String,该怎么办呢?这就需要进行类型转换了,比如代码是这么写的:

java 复制代码
@RequestMapping(method = RequestMethod.GET, path = "/test") 
@ResponseBody public String test(@RequestParam User user) {     	System.out.println(user.getName()); 
return "hello xxx"; 
}

表示要获取请求中user对应的parameterValue,但是我们发请求时是这么发的:

http://localhost:8080/tuling-web/app/test?user=xxx

那么SpringMVC就需要将字符串zhouyu转换成为User对象,这就需要我们自定义类型转换器了,比如:

java 复制代码
public class StringToUserEditor extends PropertyEditorSupport {
    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        User user = new User();
        user.setName(text);
        this.setValue(user);
    }
}

@InitBinder public void initBinder(WebDataBinder binder) {     binder.registerCustomEditor(User.class, new StringToUserEditor()); }

Spring默认提供的Converter:org.springframework.core.convert.support.DefaultConversionService#addCollectionConverters

MultipartFile解析

文件上传代码如下:

java 复制代码
@RequestMapping(method = RequestMethod.POST, path = "/test") 
@ResponseBody public String test(MultipartFile file) {     System.out.println(file.getName());     
return "hello xxx"; }

要理解SpringMVC的文件上传,我们得先回头看看直接基于Servlet的文件上传,代码如下:

java 复制代码
@WebServlet(name = "uploadFileServlet", urlPatterns = "/uploadFile")
@MultipartConfig
public class UploadFileServlet extends HttpServlet {
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        Collection<Part> parts = request.getParts();
        for (Part part : parts) {
            //content-disposition对于的内容为:form-data; name="file"; filename="xxx.xlsx"    
            String header = part.getHeader("content-disposition");
            String fileName = getFileName(header);
            if (fileName != null) {
                part.write("D://upload" + File.separator + fileName);
            } else {
                System.out.println(part.getName());
            }
        }
        response.setCharacterEncoding("utf-8");
        response.setContentType("text/html;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.println("上传成功");
        out.flush();
        out.close();
    }

    public String getFileName(String header) {
        String[] arr = header.split(";");
        if (arr.length < 3) return null;
        String[] arr2 = arr[2].split("=");
        String fileName = arr2[1].substring(arr2[1].lastIndexOf("\\") + 1).replaceAll("\"", "");
        return fileName;
    }
}

可以看到第一行代码是:

Collection parts = request.getParts();

从request中拿到了一个Part集合,而这个集合中Part可以表示一个文件,也可以表示一个字符串。

比如发送这么一个请求:

那么这个请求中就会有两个Part,一个Part表示文件,一个Part表示文本。

有了这个知识点,我们再来看Controller中的代码:

java 复制代码
@RequestMapping(method = RequestMethod.POST, path = "/test") 
@ResponseBody public String test(MultipartFile file, String test) {     System.out.println(file.getName());                                                       System.out.println(test);     
return "hello xxx"; }

方法中的两个参数分别表示:

  1. file对应的是文件Part
  2. test对应的就是文本Part

假如我请求是这么发的呢:

表达里面的test=tuling,请求parameter中的test=zhouyu,那最终test等于哪个呢?

答案是两个:

那如果我只想获取表达里的test呢?可以用@RequestPart注解:

当接收到一个请求后:

  1. SpringMVC利用MultipartResolver来判断当前请求是不是一个multipart/form-data请求
  2. 如果是会把这个请求封装为StandardMultipartHttpServletRequest对象
  3. 并且获取请求中所有的Part,并且遍历每个Part
  4. 判断Part是文件还是文本
  5. 如果是文件,会把Part封装为一个StandardMultipartFile对象(实现了MultipartFile接口),并且会把StandardMultipartFile对象添加到multipartFiles中
  6. 如果是文本,会把Part的名字添加到multipartParameterNames中
  7. 然后在解析某个参数时
  8. 如果参数类型是MultipartFile,会根据参数名字从multipartFiles中获取出StandardMultipartFile对象,最终把这个对象传给方法

方法返回值解析

在RequestMappingHandlerAdapter的初始化逻辑中会设置一些默认的HandlerMethodReturnValueHandler,他们就是用来解析各种类型的方法返回值的。

比如:

  1. ModelAndViewMethodReturnValueHandler,处理的就是返回值类为ModelAndView的情况
  2. RequestResponseBodyMethodProcessor,处理的就是方法上或类上加了@ResponseBody的情况
  3. ViewNameMethodReturnValueHandler,处理的就是返回值为字符串的请求(无@ResponseBody)

我们重点看RequestResponseBodyMethodProcessor。

假如代码如下:

java 复制代码
@Controller public class XController {      
    @RequestMapping(method = RequestMethod.GET, path = "/test")     
    @ResponseBody     
    public User test() {         
        User user = new User();         
        user.setName("xxx");         
        return user;     
    }  
}

方法返回的是User对象,那么怎么把这个User对象返回给浏览器来展示呢?那得看当前请求设置的Accept请求头,比如我用Chrome浏览器发送请求,默认给我设置的就是:Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.9

表示当前这个请求接收的内容格式,比如html格式、xml格式、各种图片格式等等。

如果我们的方法返回的是一个字符串,那么就对应html格式,就没问题,而如果我们不是返回的字符串,那我们就转成字符串,通常就是JSON格式的字符串。

所以,我们需要将User对象转换成JSON字符串,默认SpringMVC是不能转换的,此时请求会报错:

而要完成这件事情,我们需要添加一个MappingJackson2HttpMessageConverter,通过它就能把User对象或者Map对象等转成一个JSON字符串。

XML的添加方式:

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

记得要引入Jackson2的依赖:
com.fasterxml.jackson.core jackson-databind 2.13.2

我们看一下MappingJackson2HttpMessageConverter的构造方法:

java 复制代码
public MappingJackson2HttpMessageConverter() {     this(Jackson2ObjectMapperBuilder.json().build()); }   
public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {     super(objectMapper, MediaType.APPLICATION_JSON, new MediaType("application", "*+json")); }

表示MappingJackson2HttpMessageConverter支持的MediaType为"application/json"、"application/*+json"。

所以如果我们明确指定方法返回的MediaType为"text/plain",那么MappingJackson2HttpMessageConverter就不能处理了,比如:

java 复制代码
@RequestMapping(method = RequestMethod.GET, path = "/test", produces = "text/plain") @ResponseBody public User test() {     
    User user = new User();     
    user.setName("xxx");     
    return user; }

以上代码表示,需要把一个User对象转成一个纯文本字符串,默认是没有这种转换器的。

一个HttpMessageConverter中有一个canWrite()方法,表示这个HttpMessageConverter能把什么类型转成什么MediaType返回给浏览器。

比如SpringMVC自带一个StringHttpMessageConverter,它能够把一个String对象返回给浏览器,支持所有的MediaType。

那为了支持把User对象转成纯文本,我们可以自定义ZhouyuHttpMessageConverter:

java 复制代码
public class XHttpMessageConverter extends AbstractHttpMessageConverter<User> {
    @Override
    public List<MediaType> getSupportedMediaTypes() {
        ArrayList<MediaType> mediaTypes = new ArrayList<>();
        mediaTypes.add(MediaType.ALL);
        return mediaTypes;
    }

    @Override
    protected boolean supports(Class clazz) {
        return User.class == clazz;
    }

    @Override
    protected User readInternal(Class<? extends User> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        return null;
    }

    @Override
    protected void writeInternal(User user, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        StreamUtils.copy(user.getName(), Charset.defaultCharset(), outputMessage.getBody());
    }
}

我定义的这个HttpMessageConverter就能够把User对象转成纯文本。

拦截器解析

我们可以使用HandlerInterceptor来拦截请求:

java 复制代码
package org.springframework.web.servlet;  
import javax.servlet.http.HttpServletRequest; 
import javax.servlet.http.HttpServletResponse;  
import org.springframework.lang.Nullable; 
import org.springframework.web.method.HandlerMethod;  
public interface HandlerInterceptor {      
    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)  throws Exception {          
        return true;     
    }       
    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,   @Nullable ModelAndView modelAndView) throws Exception { 
    }           
    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,  @Nullable Exception ex) throws Exception { 
    }  
} 

具体执行顺序看下图:

https://www.processon.com/view/link/63e9f3e6234df52a1e9303fb

@EnableWebMvc解析

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(DelegatingWebMvcConfiguration.class) public @interface EnableWebMvc { }

导入了一个DelegatingWebMvcConfiguration配置类,这个配置类定义了很多个Bean,比如RequestMappingHandlerMapping,后续在创建RequestMappingHandlerMapping这个Bean对象时,会调用DelegatingWebMvcConfiguration的getInterceptors()方法来获取拦截器:

java 复制代码
@Bean
@SuppressWarnings("deprecation")
public RequestMappingHandlerMapping requestMappingHandlerMapping(...) {
    RequestMappingHandlerMapping mapping = createRequestMappingHandlerMapping();
    mapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider));
    // ...     
    return mapping;
}

而在getInterceptors()方法中会调用addInterceptors()方法,从而会调用WebMvcConfigurerComposite的addInterceptors()方法,然后会遍历调用WebMvcConfigurer的addInterceptors()方法来添加拦截器:

java 复制代码
public void addInterceptors(InterceptorRegistry registry) {     
    for (WebMvcConfigurer delegate : this.delegates) {  
        delegate.addInterceptors(registry);     
    } 
}

那么delegates集合中的值是哪来的呢?在DelegatingWebMvcConfiguration中进行了一次set注入:

java 复制代码
@Autowired(required = false)
public void setConfigurers(List<WebMvcConfigurer> configurers) {
    if (!CollectionUtils.isEmpty(configurers)) {
        this.configurers.addWebMvcConfigurers(configurers);
    }
}

public void addWebMvcConfigurers(List<WebMvcConfigurer> configurers) {
    if (!CollectionUtils.isEmpty(configurers)) {
        this.delegates.addAll(configurers);
    }
}

所以就是把Spring容器中的WebMvcConfigurer的Bean添加到了delegates集合中。

所以,我们可以配置WebMvcConfigurer类型的Bean,并通过addInterceptors()方法来给SpringMvc添加拦截器。

同理我们可以利用WebMvcConfigurer中的其他方法来对SpringMvc进行配置,比如

java 复制代码
@ComponentScan("com.xxx")
@Configuration
@EnableWebMvc
public class AppConfig implements WebMvcConfigurer {
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer.addPathPrefix("/xxx", t -> t.equals(ZhouyuController.class));
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    }
}

所以@EnableWebMvc的作用是提供了可以让程序员通过定义WebMvcConfigurer类型的Bean来对SpringMVC进行配置的功能。

另外值得注意的是,如果加了@EnableWebMvc注解,那么Spring容器中会有三个HandlerMapping类型的Bean:

  1. RequestMappingHandlerMapping
  2. BeanNameUrlHandlerMapping
  3. RouterFunctionMapping

如果没有加@EnableWebMvc注解,那么Spring容器中默认也会有三个HandlerMapping类型的Bean:

  1. BeanNameUrlHandlerMapping
  2. RequestMappingHandlerMapping
  3. RouterFunctionMapping

就顺序不一样而已,源码中是根据DispatcherServlet.properties文件来配置有哪些HandlerMapping的。

java 复制代码
private void initHandlerMappings(ApplicationContext context) {
    this.handlerMappings = null;
    // 默认为true,获取HandlerMapping类型的Bean         
    if (this.detectAllHandlerMappings) {
        // Find all HandlerMappings in the ApplicationContext, including ancestor contexts.             
        Map<String, HandlerMapping> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
        if (!matchingBeans.isEmpty()) {
            this.handlerMappings = new ArrayList<>(matchingBeans.values());
            // We keep HandlerMappings in sorted order.                 
            AnnotationAwareOrderComparator.sort(this.handlerMappings);
        }
    }
    // 获取名字叫handlerMapping的Bean         
    else {
        try {
            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.         
        }
    }
    // 如果从Spring容器中没有找到HandlerMapping类型的Bean         
    // 就根据DispatcherServlet.properties配置来创建HandlerMapping类型的Bean         
    // 默认就有这么一个文件,会创建出来三个HandlerMapping的Bean        
    if (this.handlerMappings == null) {
        this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
        if (logger.isTraceEnabled()) {
            logger.trace("No HandlerMappings declared for servlet '" + getServletName() + "': using default strategies from DispatcherServlet.properties");
        }
    }
    for (HandlerMapping mapping : this.handlerMappings) {
        if (mapping.usesPathPatterns()) {
            this.parseRequestPath = true;
            break;
        }
    }
}

由于加和不加@EnableWebMvc注解之后的HandlerMapping顺序不一样,可能会导致一些问题(工作中很难遇到):

java 复制代码
@Component("/test") public class BeanNameUrlController implements Controller {     		
    @Override     
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {         
        System.out.println("BeanNameUrlController");        
        return null;    
    } 
}

@RestController public class XController {      
    @GetMapping("/test")     
    public String test() {         
        System.out.println("XController");         
        return null;     
    }  
}

这两个Controller访问路径是一样的,但是负责处理的HandlerMapping是不一样的,

  1. BeanNameUrlController对应的是BeanNameUrlHandlerMapping
  2. XController对应的是RequestMappingHandlerMapping

如果加了@EnableWebMvc注解,顺序为:

  1. RequestMappingHandlerMapping
  2. BeanNameUrlHandlerMapping
  3. RouterFunctionMapping

会先由RequestMappingHandlerMapping处理/test请求,最终执行的是ZhouyuController中的test

如果没有加@EnableWebMvc注解,顺序为:

  1. BeanNameUrlHandlerMapping
  2. RequestMappingHandlerMapping
  3. RouterFunctionMapping

会先由BeanNameUrlHandlerMapping处理/test请求,最终执行的是BeanNameUrlController中的test

注意,一个HandlerMapping处理完请求后就不会再让其他HandlerMapping来处理请求了。

相关推荐
艾菜籽6 小时前
Spring MVC入门补充2
java·spring·mvc
为java加瓦9 小时前
Spring 方法注入机制深度解析:Lookup与Replace Method原理与应用
java·数据库·spring
无名客010 小时前
SpringCloud中的网关(Gateway)的作用是什么?
spring·spring cloud·gateway
hrrrrb12 小时前
【Spring Security】Spring Security 概念
java·数据库·spring
小信丶12 小时前
Spring 中解决 “Could not autowire. There is more than one bean of type“ 错误
java·spring
hello 早上好19 小时前
深入 Spring 依赖注入底层原理
数据库·sql·spring
cxyxiaokui0011 天前
🔍 为什么我的日志在事务回滚后也没了?——揭秘 REQUIRES_NEW 的陷阱
java·后端·spring
跟着珅聪学java1 天前
spring boot 整合 activiti 教程
android·java·spring
Java水解1 天前
Spring JDBC与KingbaseES深度集成:构建高性能国产数据库应用实战
后端·spring
低音钢琴1 天前
【SpringBoot从初学者到专家的成长15】MVC、Spring MVC与Spring Boot:理解其差异与联系
spring boot·spring·mvc