用了这么多年 Spring MVC,你真的了解它吗?(三)

简介: 今天,正式介绍一下Java极客技术知识星球Spring 源码分析:不得不重视的 Transaction 事务Spring 源码学习(八) AOP 使用和实现原理这么火的 OKR,你不了解下?Java:控制反转(IoC)与依赖注入(DI)

默认策略

DispatcherServlet#getDefaultStrategies(缩减版)

protected <T> List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) {
    // 策略接口名称
    String key = strategyInterface.getName();
    // 默认策略列表
    String value = defaultStrategies.getProperty(key);
    String[] classNames = StringUtils.commaDelimitedListToStringArray(value);
    List<T> strategies = new ArrayList<>(classNames.length);
    for (String className : classNames) {
        // 实例化
        Class<?> clazz = ClassUtils.forName(className, DispatcherServlet.class.getClassLoader());
        Object strategy = createDefaultStrategy(context, clazz);
        strategies.add((T) strategy);
    }
    return strategies;
}
// 默认策略列表
private static final Properties defaultStrategies;
static {
    // 路径名称是:DispatcherServlet.properties
    try {
        ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class);
        defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
    }
}

从静态默认策略属性 defaultStrategies 的加载过程中,读取的是 DispatcherServlet.properties 文件内容,看完下面列出来的信息,相信你跟我一样恍然大悟,了解 Spring 配置了哪些默认策略:

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

接下来看看它们各自的初始化过程以及使用场景:


multipartResolver 文件上传相关

private void initMultipartResolver(ApplicationContext context) {
    try {
        this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class);
    catch (NoSuchBeanDefinitionException ex) {
        // Default is no multipart resolver.
        this.multipartResolver = null;
    }
}

默认情况下,Spring 是没有 mulitpart 处理,需要自己设定

<!--上传下载-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"/>

注册的 idmultipartResolver


LocalResolver 与国际化相关

LocalResolver 接口定义了如何获取客户端的地区

private void initLocaleResolver(ApplicationContext context) {
    try {
        this.localeResolver = context.getBean(LOCALE_RESOLVER_BEAN_NAME, LocaleResolver.class);
    }
    catch (NoSuchBeanDefinitionException ex) {
        // We need to use the default.
        this.localeResolver = getDefaultStrategy(context, LocaleResolver.class);
    }
}

通过寻找 idlocaleResolverbean,如果没有的话,将会使用默认的策略进行加载 AcceptHeaderLocaleResolver,它是基于 URL 参数来控制国际化,例如使用 <a href="?locale=zh_CN">  来设定简体中文,默认参数名为 locale

当然还有其他两种,基于 session 和基于 cookie 的配置,想要深入了解的可以去细看~


ThemeResolver 主题更换相关

主题是一组静态资源(例如样式表 css 和图片 image),也可以理解为应用皮肤,使用 Theme 更改主题风格,改善用户体验。

默认注册的 idthemeResolver,类型是 FixedThemeResolver,表示使用的是一个固定的主题,以下是它的继承体系图:

26.jpg

工作原理是通过拦截器拦截,配置对应的主题解析器,然后返回主题名称,还是使用上面的解析器作为例子:

FixedThemeResolver#resolveThemeName

public String resolveThemeName(HttpServletRequest request) {
    return getDefaultThemeName();
}
public String getDefaultThemeName() {
    return this.defaultThemeName;
}

HandlerMapping 与匹配处理器相关

首先判断 detectAllHandlerMappings 变量是否为 true,表示是否需要加载容器中所有的 HandlerMappingfalse 将会加载用户配置的。

如注释所说,至少得保证有一个 HandlerMapping,如果前面两个分支都没寻找到,那么就进行默认策略加载。

private void initHandlerMappings(ApplicationContext context) {
    this.handlerMappings = null;
    if (this.detectAllHandlerMappings) {
        // 默认情况下,寻找应用中所有的 HandlerMapping ,包括祖先容器(其实就是 Spring 容器啦)
        Map<String, HandlerMapping> matchingBeans =
                BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
        if (!matchingBeans.isEmpty()) {
            this.handlerMappings = new ArrayList<>(matchingBeans.values());
            // handlerMapping 有优先级,需要排序
            AnnotationAwareOrderComparator.sort(this.handlerMappings);
        }
    }
    else {
        // 从上下文中,获取名称为 handlerMapping 的 bean
        HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
        this.handlerMappings = Collections.singletonList(hm);
    }
    // 需要保证,至少有一个 HandlerMapping
    // 如果前面两步都没找到 mapping,将会由这里加载默认策略
    if (this.handlerMappings == null) {
        this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
    }
}

通过 Debug 得知,之前在加载 Spring 配置时,就已经注入了 RequestMappingHandlerMappingBeanNameUrlHandlerMapping

27.jpg

HandlerAdapter 适配器

套路与前面的一样,使用的默认策略是:HttpRequestHandlerAdapterSimpleControllerHandlerAdapterRequestMappingHandlerAdapterHandlerFunctionAdapter

说到适配器,可以将它理解为,将一个类的接口适配成用户所期待的,将两个接口不兼容的工作类,通过适配器连接起来。


HandlerExceptionResolver 处理器异常解决器

套路也与前面一样,使用的默认策略是:ExceptionHandlerExceptionResolverResponseStatusExceptionResolverDefaultHandlerExceptionResolver

实现了 HandlerExceptionResolver 接口的 resolveException 方法,在方法内部对异常进行判断,然后尝试生成 ModelAndView 返回。

public ModelAndView resolveException(
        HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
    if (shouldApplyTo(request, handler)) {
        prepareResponse(ex, response);
        ModelAndView result = doResolveException(request, response, handler, ex);
        return result;
    }
    else {
        return null;
    }
}

RequestToViewNameTranslator 处理逻辑视图名称

初始化代码逻辑与前面一样,使用的默认策略是:DefaultRequestToViewNameTranslator

使用场景:当 Controller 处理器方法没有返回逻辑视图名称时,Spring 通过该类的约定,提供一个逻辑视图名称。

由于本地测试不出来,所以引用参考资料 7 的例子:

DefaultRequestToViewNameTranslator的转换例子:

http://localhost:8080/gamecast/display.html -> display(视图)


ViewResolver 视图渲染

套路还是跟前面一样,默认策略使用的是:InternalResourceViewResolver

同时,这也是 demo 中,我们手动配置的视图解析器


FlashMapManager 存储属性

默认使用的是:SessionFlashMapManager,通过与 FlashMap 配合使用,用于在重定向时保存/传递参数

例如 Post/Redirect/Get 模式,Flash attribute 在重定向之前暂存(根据类名,可以知道范围是 session 级别有效),以便重定向之后还能使用。


RequestMappingHandler

该类作用:配合 @Controller@RequestMapping 注解使用,通过 URL 来找到对应的处理器。

前面在 spring-mvc.xml 文件加载时,初始化了两个重要配置,其中一个就是下面要说的 RequestMappingHandler,先来看它的继承体系图:

28.jpg

从继承图中看到,它实现了 InitializingBean 接口,所以在初始化时,将会执行 afterPropertiesSet 方法(图片中注释写错方法,请以下面为准),核心调用的初始化方法是父类 AbstractHandlerMethodMapping#initHandlerMethods 方法

AbstractHandlerMethodMapping#initHandlerMethods

protected void initHandlerMethods() {
    // 获取容器中所有 bean 名字
    for (String beanName : this.detectHandlerMethodsInAncestorContexts ?
                BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class) :
                obtainApplicationContext().getBeanNamesForType(Object.class)) {
        if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
            // 如果前缀不是 scopedTarget.
            // 执行 detectHandlerMethods() 方法
            Class<?> beanType = obtainApplicationContext().getType(beanName);
            if (beanType != null && isHandler(beanType)) {
                detectHandlerMethods(beanName);
            }
        }
    }
    // 打印数量,可以当成空实现
    handlerMethodsInitialized(getHandlerMethods());
}
protected void detectHandlerMethods(Object handler) {
    Class<?> handlerType = (handler instanceof String ?
            obtainApplicationContext().getType((String) handler) : handler.getClass());
    if (handlerType != null) {
        Class<?> userType = ClassUtils.getUserClass(handlerType);
        // 通过反射,获取类中所有方法
        // 筛选出 public 类型,并且带有 @RequestMapping 注解的方法
        Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
                (MethodIntrospector.MetadataLookup<T>) method -> {
                    // 通过 RequestMappingHandlerMapping.getMappingForMethod 方法组装成 RequestMappingInfo(映射关系)
                    return getMappingForMethod(method, userType);
            });
        methods.forEach((method, mapping) -> {
            Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
            // 通过 mappingRegistry 进行注册上面获取到的映射关系
            registerHandlerMethod(handler, invocableMethod, mapping);
        });
    }
}

梳理一下代码逻辑,initHandlerMethods 方法将会扫描注册 bean 下所有公共 public 方法,如果带有 @RequestMapping 注解的,将会组装成 RequestMappingInfo 映射关系,然后将它注册到 mappingRegistry 变量中。之后可以通过映射关系,输入 URL 就能够找到对应的处理器 Controller


MappingRegistry

该类是 AbstractHandlerMethodMapping 的内部类,是个工具类,用来保存所有 Mappinghandler method,通过暴露加锁的公共方法,避免了多线程对该类的内部变量的覆盖修改。

下面是注册的逻辑:

public void register(T mapping, Object handler, Method method) {
    this.readWriteLock.writeLock().lock();
    try {
        // 包装 bean 和方法
        HandlerMethod handlerMethod = createHandlerMethod(handler, method);
        // 校验
        validateMethodMapping(handlerMethod, mapping);
        this.mappingLookup.put(mapping, handlerMethod);
        List<String> directUrls = getDirectUrls(mapping);
        for (String url : directUrls) {
            this.urlLookup.add(url, mapping);
        }
        String name = null;
        if (getNamingStrategy() != null) {
            name = getNamingStrategy().getName(handlerMethod, mapping);
            addMappingName(name, handlerMethod);
        }
        // 跨域参数
        CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
        if (corsConfig != null) {
            this.corsLookup.put(handlerMethod, corsConfig);
        }
        // 将映射关系放入  Map<T, MappingRegistration<T>> registry
        this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name));
    }
    finally {
        this.readWriteLock.writeLock().unlock();
    }
}

29.jpg

通过前面的包装和校验方法,最后映射关系将会放入这里 Map<T, MappingRegistration<T>> registry。它是一个泛型的 Mapkey 类型是 RequestMappingInfo,保存了 @RequestMapping 各种属性的集合,value 类型是 AbstractHandlerMethodMapping,保存的是我们的映射关系。

从图中可以看出,如果输入的 URL/plain/{name},将会找到对应的处理方法 web.controller.BookController#plain{String}



相关文章
|
2月前
|
JSON 前端开发 Java
SSM:SpringMVC
本文介绍了SpringMVC的依赖配置、请求参数处理、注解开发、JSON处理、拦截器、文件上传下载以及相关注意事项。首先,需要在`pom.xml`中添加必要的依赖,包括Servlet、JSTL、Spring Web MVC等。接着,在`web.xml`中配置DispatcherServlet,并设置Spring MVC的相关配置,如组件扫描、默认Servlet处理器等。然后,通过`@RequestMapping`等注解处理请求参数,使用`@ResponseBody`返回JSON数据。此外,还介绍了如何创建和配置拦截器、文件上传下载的功能,并强调了JSP文件的放置位置,避免404错误。
|
2月前
|
前端开发 Java 应用服务中间件
【Spring】Spring MVC的项目准备和连接建立
【Spring】Spring MVC的项目准备和连接建立
58 2
|
3月前
|
缓存 前端开发 Java
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
Soring Boot的起步依赖、启动流程、自动装配、常用的注解、Spring MVC的执行流程、对MVC的理解、RestFull风格、为什么service层要写接口、MyBatis的缓存机制、$和#有什么区别、resultType和resultMap区别、cookie和session的区别是什么?session的工作原理
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
|
2月前
|
XML 前端开发 Java
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习
本文阐述了Spring、Spring Boot和Spring MVC的关系与区别,指出Spring是一个轻量级、一站式、模块化的应用程序开发框架,Spring MVC是Spring的一个子框架,专注于Web应用和网络接口开发,而Spring Boot则是对Spring的封装,用于简化Spring应用的开发。
136 0
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习
|
3月前
|
XML 缓存 前端开发
springMVC02,restful风格,请求转发和重定向
文章介绍了RESTful风格的基本概念和特点,并展示了如何使用SpringMVC实现RESTful风格的请求处理。同时,文章还讨论了SpringMVC中的请求转发和重定向的实现方式,并通过具体代码示例进行了说明。
springMVC02,restful风格,请求转发和重定向
|
4月前
|
Java 数据库连接 Spring
后端框架入门超详细 三部曲 Spring 、SpringMVC、Mybatis、SSM框架整合案例 【爆肝整理五万字】
文章是关于Spring、SpringMVC、Mybatis三个后端框架的超详细入门教程,包括基础知识讲解、代码案例及SSM框架整合的实战应用,旨在帮助读者全面理解并掌握这些框架的使用。
后端框架入门超详细 三部曲 Spring 、SpringMVC、Mybatis、SSM框架整合案例 【爆肝整理五万字】
|
4月前
|
XML JSON 数据库
SpringMVC入门到实战------七、RESTful的详细介绍和使用 具体代码案例分析(一)
这篇文章详细介绍了RESTful的概念、实现方式,以及如何在SpringMVC中使用HiddenHttpMethodFilter来处理PUT和DELETE请求,并通过具体代码案例分析了RESTful的使用。
SpringMVC入门到实战------七、RESTful的详细介绍和使用 具体代码案例分析(一)
|
4月前
|
前端开发 应用服务中间件 数据库
SpringMVC入门到实战------八、RESTful案例。SpringMVC+thymeleaf+BootStrap+RestFul实现员工信息的增删改查
这篇文章通过一个具体的项目案例,详细讲解了如何使用SpringMVC、Thymeleaf、Bootstrap以及RESTful风格接口来实现员工信息的增删改查功能。文章提供了项目结构、配置文件、控制器、数据访问对象、实体类和前端页面的完整源码,并展示了实现效果的截图。项目的目的是锻炼使用RESTful风格的接口开发,虽然数据是假数据并未连接数据库,但提供了一个很好的实践机会。文章最后强调了这一章节主要是为了练习RESTful,其他方面暂不考虑。
SpringMVC入门到实战------八、RESTful案例。SpringMVC+thymeleaf+BootStrap+RestFul实现员工信息的增删改查
|
4月前
|
JSON 前端开发 Java
Spring MVC返回JSON数据
综上所述,Spring MVC提供了灵活、强大的方式来支持返回JSON数据,从直接使用 `@ResponseBody`及 `@RestController`注解,到通过配置消息转换器和异常处理器,开发人员可以根据具体需求选择合适的实现方式。
170 4
|
4月前
|
XML 前端开发 Java
Spring MVC接收param参数(直接接收、注解接收、集合接收、实体接收)
Spring MVC提供了灵活多样的参数接收方式,可以满足各种不同场景下的需求。了解并熟练运用这些基本的参数接收技巧,可以使得Web应用的开发更加方便、高效。同时,也是提高代码的可读性和维护性的关键所在。在实际开发过程中,根据具体需求选择最合适的参数接收方式,能够有效提升开发效率和应用性能。
122 3
下一篇
无影云桌面