
1. 拆解SpringMVC的核心工作流程1.1 从一次HTTP请求说起SpringMVC很多新手都学过但真正能把一次请求从进来到出去完整讲清楚的人真不多。我面试了不少候选人问一句输入URL回车之后SpringMVC做了什么很多人能答出DispatcherServlet但再往下追问HandlerMapping怎么找到一个能处理请求的方法就卡住了。这个不是背题能解决的是脑子里没有完整的链路。一个典型的SpringMVC请求从Tomcat接收到HTTP报文开始会被交给DispatcherServlet这个前端控制器。DispatcherServlet拿到请求URL之后先通过HandlerMapping找到对应的Handler也就是Controller里的方法SpringMVC里叫HandlerMethod然后通过HandlerAdapter把这个方法调用起来。方法执行完会返回ModelAndView或者直接通过ResponseBody输出数据。如果是ModelAndView还需要经过ViewResolver解析出具体视图再把模型数据填充进去渲染成HTML返回给浏览器。整个过程可以用一个生活化类比来理解DispatcherServlet就像一个前台接待员你到了公司说我要找市场部的张三接待员不会自己去找而是先查通讯录HandlerMapping找到张三在哪个工位然后告诉你在哪儿能找到他HandlerAdapter调用方法。最终张三给你一个答复返回数据你带着答复离开。所有的找人领路记录工作都是前台统一做的这就是前端控制器模式的核心价值。1.2 为什么非要有一个总入口在SpringMVC之前我们写Java Web用的都是传统的Servlet。一个系统里可能有一百个Servlet每个负责一个功能点。这么做最难受的地方在于公共逻辑没法收口——权限校验、日志记录、编码设置、异常兜底所有Servlet都得自己写一遍。代码重复不说还特别容易漏今天加一个页面忘了配编码过滤器明天就有一堆乱码bug找上门。DispatcherServlet作为总入口解决的正是不一样的问题所有请求先到它手里它可以统一处理编码、统一做拦截器链调用、统一做异常转发。Controller本身不需要关心请求怎么路由过来的只需要关注业务逻辑本身。这种集中式控制组件化执行的设计思想和公司的考勤制度一个道理你每天上下班打卡考勤系统统一记录而不是每个部门各自拿个本子登记这样管理成本低、规则统一、出了问题也好追溯。1.3 核心组件各自扮演什么角色SpringMVC的关键组件其实就五六个把它们的职责搞清楚整个框架就褪去了神秘感。DispatcherServlet是入口负责统筹调度但它本身不做具体事。HandlerMapping负责根据URL找到对应的Handler。HandlerMapping最常用的实现是RequestMappingHandlerMapping它会把Controller类上和方法上的RequestMapping注解解析成一个个映射条目保存URL和方法的对应关系。HandlerAdapter负责真正去调用Controller方法它处理参数解析、方法参数注入、返回值处理。这个组件特别关键因为同样的一个方法有RequestBody这样的注解时怎么解析参数没有注解时又从哪拿参数都是HandlerAdapter在做。ViewResolver负责把逻辑视图名解析成真正的视图。比如Controller返回indexViewResolver会找到resolver配置好的前缀后缀解析成/WEB-INF/views/index.jsp。还有ModelAndView它不是组件是Controller和视图之间的数据载体既携带视图信息也携带模型数据。这几个组件之间的调用关系Spring在DispatcherServlet中早就编排好了我们不需要自己实现只需要理解它们怎么配合。理解清楚之后你在排查问题时会非常快。比如控制器方法明明写了但请求404大概率是HandlerMapping没有注册到这个映射返回了JSON但是浏览器拿到的是乱码多半是HandlerAdapter输出时没有配置好编码处理器。这些都不是玄学链路搞清楚了排查方向立刻就清晰了。2. 核心细节解析与实操要点2.1 从XML配置到注解驱动的演进SpringMVC的配置方式这十年来经历了三个阶段的演进。最早是SSH时代遗留下来的全XML配置web.xml里要配置DispatcherServlet的映射和Spring的监听器Spring配置文件里要配置组件扫描、注解驱动、视图解析器前后可能上百行配置。后来Spring 3.1推出了基于注解WebApplicationInitializer的配置方式再后来Spring Boot出现连配置类都省了约定大于配置直接用。配置方式本身没有绝对的好坏但理解和掌握底层发生了什么始终重要。我现在带团队就要求新人必须用传统XML项目跑一遍SpringMVC不是为了复古是为了让新人理解注解只是省去了写配置的步骤并没有省略配置本身。比如你在Spring Boot里只需要在spring-mvc.properties里写一个prefix和suffix本质上就是把原来XML里的InternalResourceViewResolver配置搬进去逻辑没变。实践中最常见的配置要点有这么几个第一DispatcherServlet的url-pattern配置成/这样才会进入SpringMVC的映射机制。很多老项目里配的是*.do能跑但语义不同会绕开静态资源的自然访问。第二必须开启注解扫描否则Controller写了白写。第三配置文件要引入spring-mvc命名空间开启mvc:annotation-driven /没有这个HandlerAdapter不会启用到时候方法上的注解一个都不好使。2.2 请求映射的各种玩法RequestMapping是整个SpringMVC开发中出现频率最高的注解之一。类级别加方法级别的组合构成了完整的URL映射。类上写RequestMapping(/user)方法上再写RequestMapping(/list)合并出来的访问路径就是/user/list。细化到HTTP方法Spring 4.3开始提供了简化注解GetMapping、PostMapping、PutMapping、DeleteMapping、PatchMapping。它们解决的一个重要问题是语义清晰。一个普通方法用RequestMapping写了method属性限制为GET代码上看起来还是宽松的而GetMapping一眼就知道这方法只处理GET。在实际开发里RESTful接口推荐用这些派生注解避免接口被误用。URL中带参数有两种常见方式。一种叫路径变量GetMapping(/user/{id})配合PathVariable(id) Integer id适用于资源定位。一种叫查询参数GetMapping(/user)配合RequestParam(name) String name适用于过滤场景。路径变量和查询参数在实际项目中都是高频使用我见过不少新手把两种方式混在一起用结果要么URL长得没法看要么传参传不过去。一个简单的取舍原则是标识资源身份用路径变量筛选条件用查询参数。2.3 参数绑定与数据校验Controller方法入参的绑定是SpringMVC让我觉得写起来最爽的一个部分。一个POJO对象比如User类有id、name、age三个字段你接收GET表单请求时只要方法的入参写成User userSpring会自动按字段名把请求参数绑定进去。原理其实不复杂HandlerAdapter通过WebDataBinder把request的参数一个个匹配到POJO的属性上再通过反射设置进去。理解了这个底层动作就能解释为什么POJO字段名和请求参数名不一致时绑不上——因为根本没有可以对齐的桥梁。日期格式的绑定是新手踩坑的重灾区。HTTP请求里传来的日期通常是个字符串比如2024-05-20而POJO字段类型是java.util.Date。SpringMVC默认的日期转换器只能处理特定格式其他格式直接报400。解决办法是给字段加DateTimeFormat(pattern yyyy-MM-dd)或者在全局配置一个转换器统一处理。我更推荐全局处理因为一个项目里日期格式通常是统一的分散到每个字段上太啰嗦。参数校验方面JSR-303规范的Valid注解配合Hibernate Validator是主流方案。在POJO字段上加NotNull、Size、Email这类约束注解方法参数前加ValidSpringMVC就会在参数绑定时自动执行校验。校验失败抛出的BindException或MethodArgumentNotValidException再配合全局异常处理能统一返回错误信息。这个组合是标准做法我强烈建议任何非内部接口都必须做参数校验不要信任任何外部输入。2.4 返回ModelAndView还是直接写JSONController返回数据的方式大致分两个流派。传统服务端渲染项目方法返回String逻辑视图名然后用Model或ModelAndView携带数据最终由ViewResolver解析成JSP渲染HTML。这种方式的优势是页面在服务端组装好对浏览器友好SEO也好处理。前后端分离项目则完全不同方法上加ResponseBody直接返回对象或MapSpringMVC通过HttpMessageConverter把对象序列化成JSON输出。Spring 4.0之后可以直接在类上使用RestController组合注解等价于Controller ResponseBody。需要特别提醒的是很多新手在使用ResponseBody时遇到JSON格式错误原因往往是实体类里有循环引用、Date日期序列化格式不对、或者某个getter方法写得不规范导致序列化异常。处理策略上我建议前后端分离项目统一用RestController实体对象里的日期字段统一加JsonFormat注解循环引用用JsonIgnore或JsonManagedReference处理。JSON序列化这些事情越早定规范后面越省事。3. 拦截器、异常处理与文件上传实战3.1 拦截器认证、日志、请求计时的一站式解决方案SpringMVC的拦截器Interceptor很多人知道它存在但用得并不充分。这个机制对应的是HandlerInterceptor接口包含三个方法preHandle在Controller方法执行之前调用postHandle在方法执行完成后视图渲染之前调用afterCompletion在整个请求处理完成之后调用。三个方法执行的时机不同决定了你能在哪个环节做哪些事。preHandle里做的事情最多也是拦截器最常用的位置。登录校验、权限判断、请求参数预处理、记录请求开始时间这些都在preHandle里做。关键是这个方法有返回值返回true放行返回false就拦住。这就是为什么临时性的登录拦截可以直接在preHandle里写而不需要改动业务方法。postHandle里不太适合做重量级操作因为视图还没有渲染但你仍可以对ModelAndView做最后的调整。afterCompletion不管业务是否抛异常都会执行适合做资源清理、记录请求耗时类似Servlet中Filter的after方法。我曾经用它统计接口响应时间把超过1秒的请求单独打日志优化接口性能时特别有用。拦截器要生效必须注册进SpringMVC的拦截器链。在Spring Boot里写一个WebMvcConfigurer的配置类在addInterceptors方法里registry.addInterceptor(loginInterceptor).addPathPatterns(/).excludePathPatterns(/login, /register, /static/)。拦截器链的执行顺序就是注册顺序先注册的先执行。这个点我吃了不少亏一开始以为和Filter的注解顺序一样结果发现多个拦截器时顺序完全由注册顺序决定这个细节一定要记住。这里还必须区分拦截器和Filter两者不是一回事很多人混为一谈。Filter是Servlet规范里的东西在请求进入DispatcherServlet之前执行属于Web容器层。拦截器是SpringMVC框架的东西在DispatcherServlet已经收到请求、HandlerMapping已定位到具体Handler之后的环节执行。Filter更擅长处理编码、CORS这类与应用逻辑无关的事拦截器更适合做与业务绑定紧密的处理比如登录态校验。3.2 统一异常处理怎么做才优雅Controller里每个方法都try-catch是代码坏味道的一种极其影响阅读。SpringMVC给了我们统一处理的杀手锏ControllerAdvice ExceptionHandler。ControllerAdvice可以理解为一个全局的控制器增强它对所有Controller的方法生效配合ExceptionHandler可以指定处理哪种类型的异常并返回统一的错误响应。举个例子定义一个GlobalExceptionHandler类标注RestControllerAdvice然后在方法上写ExceptionHandler(BusinessException.class)方法返回一个Result对象。这样整个项目里Controller方法可以完全不处理业务异常只管抛出异常信息在全局处理类里统一包装。这个模式在团队开发里特别有价值不同的人写接口时返回错误码格式不统一是家常便饭的事统一异常处理强制约束了格式出问题了也知道从哪里查。还要注意ExceptionHandler可以捕获的参数范围很有讲究。比如Exception这个大类不要轻易捕否则会把系统内部错误也包装成业务错误返回给前端掩盖真实的异常信息。宁可加一个兜底的Exception处理类单独返回系统繁忙之类的提示同时把异常堆栈记录到日志也不要把底层异常直接抛给调用方。我在项目里通常做法是业务异常精确匹配、参数校验异常单独处理、最后留一个Exception兜底并打完整日志方便排查线上问题。3.3 文件上传的完整配置文件上传也是SpringMVC里用得很多的功能但配置不好时会出现各种莫名其妙的问题。首先要明确SpringMVC处理multipart/form-data格式的请求需要MultipartResolver最常用的是StandardServletMultipartResolver它基于Servlet 3.0的Part API实现。在Spring Boot里配置相对简单spring.servlet.multipart.max-file-size和spring.servlet.multipart.max-request-size两个参数就可以控制大小限制。但如果你维护的是一个传统SpringMVC项目步骤会多一些web.xml里DispatcherServlet的multipart-config要配置或者用JavaConfig方式通过MultipartConfigElement来配置否则MultipartResolver根本拿不到上传的Part。我当年在一个老项目里踩过这个坑Spring容器里配置了CommonsMultipartResolver但Tomcat一直报Required Part file not present错误排查了半天才意识到是web.xml里漏配了multipart-config。控制器方法里接收MultipartFile参数方法签名写成(RequestParam(file) MultipartFile file)就行数据会被封装成MultipartFile对象可以直接获得原始文件名、文件大小、输入流。文件上传到服务器务必要做这几件事文件类型白名单校验、文件大小限制、文件名重命名防止路径穿越和重名覆盖、存储目录权限控制。不要直接使用用户上传的文件名拼接路径否则碰到../这种特殊字符时很容易造成安全问题。存文件的目录建议放在项目的应用外部目录避免打包部署时文件丢失同时维护起来也方便。4. 常见问题与排查技巧实录4.1 常见问题速查表这些年在群里答疑SpringMVC的新手问题翻来覆去就那么几个。我把高频问题和排查思路整理成一个速查表希望你能收藏备用现象可能原因排查方法请求404页面提示无映射Controller类没有加注解或没被扫描检查类上注解检查component-scan配置路径请求404访问静态资源失效DispatcherServlet拦截了静态资源配置mvc:resources mapping/static/** location/static//或WebMvcConfigurer的addResourceHandlers中文乱码请求/响应编码不一致检查CharacterEncodingFilter配置检查数据库连接URL的编码参数检查JSP页面编码JSON返回报错页面显示500对象序列化异常检查循环引用、日期格式化、getter方法是否规范参数绑定失败请求参数名和POJO字段名不一致核对字段名或使用RequestParam显式指定参数名RequestParam必传参数缺失前端没传该参数设置requiredfalse或前端补传拦截器不生效没有注册或路径匹配规则错误检查addInterceptors注册配置以及拦截器的path匹配写法日期字段绑定失败格式不匹配无转换器加DateTimeFormat或自定义全局日期转换器上传文件超过大小限制max-file-size配置过小或未生效检查配置文件检查是否正确使用multipart配置方法执行两次拦截器链顺序或Forward跳转重复检查日志确认是哪一层重复调用4.2 踩过的几个经典坑第一个坑是404的模棱两可。一个Controller方法写的没问题启动的时候也加载了但访问永远404。最后发现是类名和方法名都没问题但类的包路径不在组件扫描的范围内。这种情况Spring不会给任何警告你只能通过检查启动日志里有没有注册这个映射来确认。后来我的习惯是新写任何一个Controller先看启动日志是否刷出Mapping路径刷出来了再测能省大量时间。第二个坑是日期格式问题。有个项目里前端统一传“yyyy/MM/dd HH:mm”的字符串但POJO里用DateTimeFormat只配了“yyyy-MM-dd”结果所有带时间的接口全部400。小范围看是少了一个格式本质上是全项目没有统一约定日期格式。后来我明确规定前后端交互的所有时间字段统一用时间戳或ISO格式的字符串JVM层面配置统一时区之后这类问题基本绝迹。第三个坑是拦截器里的异常处理。有一版我在preHandle里做登录校验如果用户没登录直接response.getWriter().write(未登录)并返回false。看起来没毛病但被全局异常处理器捕获不到因为响应已经直接写回去了格式和其他接口不一致。前端拿到的数据结构和登录失效的处理逻辑对不上排查时愣是没往拦截器方向想。现在的做法是拦截器里发现未登录时直接抛出一个自定义的AuthException交给全局异常处理器统一输出。4.3 排查思路的核心方法论排查SpringMVC问题我有一个自己的固定套路。第一步确认请求确实到达了DispatcherServlet在拦截器preHandle里打一条日志就能确认。第二步确认Controller方法有没有被调用在方法入口打日志。第三步确认返回的数据和视图解析有没有问题。三步日志打下来链路中哪个环节断了一目了然。这个方法效率极高比瞎猜强太多。另外一个方法论是先确认配置再确认代码。SpringMVC的很多问题其实都是配置层面的问题比如扫描路径、静态资源配置、拦截器注册。我见过有人对着一个配置错误的项目调试了几个小时Controller代码最后发现只是配置类少写了一个EnableWebMvc。配置文件不是无关紧要的工程脚手架它就是程序的一部分排查问题要把它放在第一优先级。5. 一点更进阶的实战建议当你能熟练使用SpringMVC处理日常开发之后建议从这几个角度继续深挖。其一是拦截器与异步请求的关系SpringMVC从3.2开始支持Servlet 3异步处理异步请求时拦截器会先执行完preHandle再触发实际业务线程此时postHandle和afterCompletion的行为会变得不同。如果你的接口里用了DeferredResult或WebAsyncTask一定要把拦截器的执行时机重新想一遍。其二是对HandlerInterceptor和Filter的使用边界要有清晰的判断。全局的请求日志、防止重复提交这类需求用Filter更合适因为它在最外层而基于登录态的页面跳转、角色权限校验用拦截器更顺手因为它能拿到HandlerMethod反射上的注解信息。我在项目里经常同时使用两者各管一摊相得益彰。其三是数据校验、参数绑定和异常处理的组合使用。当项目规模变大接口数量过百时统一的参数校验规范能极大的降低沟通成本。我会把所有的自定义校验逻辑封装成注解和JSR-303内置约束一起使用配合全局异常处理器统一输出错误码和错误消息。这个体系一旦搭好新增一个接口只需要写好POJO约束和业务逻辑再也不用关心错误响应格式不一致的问题。最后分享一个自己用了很久的小技巧。开发阶段把SpringMVC的日志级别调到DEBUG能看到HandlerAdapter对每个方法的调用细节和处理耗时。生产环境再调回INFO不影响性能。这个做法帮我解决过很多说不清道不明的奇怪问题说白了就是让框架把自己做的事尽量展示出来。SpringMVC的学习没有终点它作为Spring全家桶的基石你今天花在理解DispatcherServlet上的每一分钟未来在理解Spring Boot自动配置和微服务内的Web层时都会加倍还给你。