尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SpringMVC响应处理进阶:静态资源映射与响应数据统一修改实战

SpringMVC响应处理进阶:静态资源映射与响应数据统一修改实战 这个系列写到第9篇SpringMVC的“响应”这一块终于到了重头戏。前几篇我们把请求参数绑定、JSON数据交互、异常处理这些请求链路都过了一遍还剩两块最容易被忽视、却在实际项目里最容易出问题的点怎么把静态页面干净利落地返回给浏览器以及怎么在响应到达客户端之前“偷梁换柱”修改响应数据。为什么要花一整篇专门写这个因为在基于Maven构建的SpringMVC模块化项目里通过IDEA配置Tomcat容器启动后我见过太多同事第一次把前端页面丢进src/main/webapp启动一看全是404或者在前后端联调时F12打开一片红报“没有可用于预检”这种让人摸不着头脑的CORS错误。这篇就把静态资源映射、HandlerMapping执行顺序、ResponseBodyAdvice、拦截器和CORS预检这些点一次讲透既有原理也有可直接抄的配置。1. 整体设计拆解请求从进入到响应的完整链路1.1 为什么“返回静态页面”在SpringMVC里是个独立话题先纠正一个常见直觉很多人觉得“返回静态页面不就是把HTML文件放到webapp下浏览器直接访问不就行了吗”。放在纯Servlet容器里确实是这样Tomcat默认就能处理index.html、css、js这些静态资源。但一旦引入SpringMVCDispatcherServlet接管了请求分发情况就变了。DispatcherServlet在web.xml里通常配置为/意思是“除了JSP以外的所有请求都先进来”。它拿到一个/assets/css/app.css的请求后会当成一个Handler来匹配。如果没有任何RequestMapping能匹配到这个路径而你又没配置静态资源处理器那结果就是404。这不是Tomcat的锅是请求根本没走到Tomcat的DefaultServlet那一步。所以“返回静态页面”在SpringMVC语境下本质上是在问如何让SpringMVC在“找不到Controller处理器”时把静态资源的请求交还出去或者用专门的ResourceHandler来处理。这个设计问题在模块化项目里更突出——每个模块都可能自带一套dist目录、一套图片资源、一套index.html资源路径的隔离和映射规则就成了架构层面需要统一的事情。1.2 一条响应请求在SpringMVC内部经过了哪些关卡为了搞清楚静态资源404和响应数据被“篡改”的原理先把这个链路拉出来。一次请求从浏览器发出来经过容器、进入SpringMVC时顺序是这样的请求先通过DispatcherServlet前端控制器。DispatcherServlet拿着请求遍历所有HandlerMapping找到一个能返回HandlerExecutionChain的映射。找到HandlerAdapter执行Controller方法或者ResourceHttpRequestHandler等特殊处理器。如果Controller返回的是ModelAndView走ViewResolver渲染视图如果方法标注了ResponseBody走HttpMessageConverter把对象序列化成JSON/XML文本输出。响应回到DispatcherServlet经过已注册的拦截器afterCompletion再写回客户端。这里面最容易踩坑的是第2步。SpringMVC里HandlerMapping不止一个它们有执行顺序。默认的RequestMappingHandlerMapping负责处理RequestMapping注解和SimpleUrlHandlerMapping负责处理静态资源、欢迎页等它们的order值不一样。如果你在配置类里自己写了Bean返回某种映射器或者不小心覆盖了WebMvcConfigurationSupport顺序就可能被改掉于是出现“Controller正常静态资源全404”或反过来“静态资源能访问Controller被抢了”的怪现象。这个顺序问题后面实操部分我会单独讲。1.3 方案选型静态页面到底该“返回”还是“映射”回到项目需求。这个模块化项目里“返回静态页面”实际有两类诉求一类是纯前端资源比如每个模块的打包产物dist目录里面是index.html、css、js、图片。这些资源不需要经过Controller应该由静态资源映射直接交给容器处理。另一类是服务端渲染出来的页面比如/page/help这个URL要展示帮助页。早期项目习惯用InternalResourceViewResolver配合JSP但现在很多模块化项目的前端是Vue/React打包出来的后端只要把index.html返回给浏览器剩下的路由交给前端Router处理。这两类诉求的解决方案完全不同前者用ResourceHandlerRegistry映射目录后者用Controller返回View对象或直接转发。我见过有人把dist/index.html的内容硬编码成字符串写在Controller里返回这种方案维护成本极高属于典型的反面教材。正确的姿势应该是能交给容器静态资源处理的绝不用Controller能返回视图名的绝不用字符串拼HTML。在具体选型时我建议按这个优先级决策场景推荐方案原因前端打包产物纯静态mvc:resources/addResourceHandlers性能好可加缓存头无需Java代码服务端渲染简单页面Controller返回View名配置InternalResourceViewResolver走完整视图解析链路方便加公共数据需要模板引擎渲染动态数据Thymeleaf/FreemarkerSpring官方支持天然契合MVC少量静态HTML且不想暴露真实路径Controller里forward/redirect可做权限控制但别把HTML内容字符串化2. 返回静态页面的核心实操三种配置方式及细节2.1 方式一Java配置类实现静态资源映射在SpringMVC的Java配置方式下核心就是实现WebMvcConfigurer接口重写addResourceHandlers方法。以一个Maven多模块项目为例假设前端资源统一放在各模块的src/main/resources/static/下打包后会在classes目录里产生static目录那么配置如下Configuration EnableWebMvc public class WebMvcConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/static/**) .addResourceLocations(classpath:/static/) .setCachePeriod(3600) .resourceChain(true); } }这段配置的意思是URL中以/static/开头的请求都从classpath下的static/目录找文件。setCachePeriod(3600)是给响应加Cache-Control头缓存一小时这是性能优化的小地方纯静态资源本来就可以放心缓存。resourceChain(true)启用资源链会先尝试用内存缓存减少重复IO。这里有个细节新手常忽略addResourceLocations的路径结尾必须带/否则Spring会认为它指向一个精确文件而非目录。我调试过别人的代码路径写的classpath:/static少个斜杠结果所有资源全部404排查了很久。2.2 方式二XML配置以及mvc:default-servlet-handler的作用如果项目还在用XML配置老项目挺常见对应配置是mvc:annotation-driven / mvc:resources mapping/static/** location/static/ cache-period3600 /另外还有一个经常被提起的标签mvc:default-servlet-handler /。它的作用是当SpringMVC找不到对应Handler时把请求交回容器默认的Servlet比如Tomcat的DefaultServlet来处理。如果你在配置里看到这个标签说明项目采用的是“兜底路由”策略静态资源找不到就让容器处理。这两种方式的取舍其实反映了一个设计倾向mvc:resources是精确控制告诉Spring哪些路径属于静态资源mvc:default-servlet-handler是“懒人方案”把所有未匹配的请求都交出去。我个人的建议是能用mvc:resources就用它因为如果你把/都交给DefaultServlet遇到/这种路径时控制器映射的优先级会变得不可控而且错误请求也会被静默处理不利于排查问题。2.3 方式三Controller返回页面与forward转发的正确用法补充一个不那么“静态”但是常被问到的场景。Controller方法返回一个String如果配置了InternalResourceViewResolverSpring会把它解析成视图名。例如Controller public class PageController { GetMapping(/page/help) public String helpPage() { return help; } }配合配置Bean public InternalResourceViewResolver viewResolver() { InternalResourceViewResolver resolver new InternalResourceViewResolver(); resolver.setPrefix(/WEB-INF/views/); resolver.setSuffix(.html); return resolver; }这里要注意如果前端是放在WEB-INF下的help.html浏览器直接访问/WEB-INF/views/help.html是访问不到的因为WEB-INF目录对浏览器不可见只能通过服务端转发访问。这反而是好事——不经Controller就无法直接拿页面天然做了访问控制。另外就是forward的用法。比如你想让某个URL直接展示静态资源的某个文件但又不希望用户看到真实的/static/路径可以用GetMapping(/dashboard) public String dashboard() { return forward:/static/index.html; }forward是服务端内部转发URL地址不会变浏览器看到的是/dashboard实际内容是/static/index.html渲染出来的。注意不能用redirect替换redirect是重定向会触发浏览器的二次请求URL会变而且对于异步请求可能破坏原有的请求头。2.4 不能忽视的HandlerMapping执行顺序问题前面说过HandlerMapping不止一个这里把顺序问题彻底讲清楚。在SpringMVC 5.x中默认注册了几个关键映射器HandlerMapping用途order值RequestMappingHandlerMapping处理RequestMapping注解方法0SimpleUrlHandlerMapping处理URL与Handler的显式映射包括静态资源当使用mvc:resources时Integer.MAX_VALUE-1WelcomPageHandlerMapping处理根路径/的欢迎页Integer.MAX_VALUE-2这个顺序的含义是先看RequestMapping能不能匹配匹配不上再看有没有注册的静态资源URL映射最后才是欢迎页。这个设计非常合理——Controller的优先级高于静态资源不会因为有个/assets/目录就抢了某个RequestMapping(/assets/info)接口的生意。但这里有个隐藏的坑如果你在自定义配置中手动声明了SimpleUrlHandlerMapping且没设置order它的默认order值和Spring内部注册的冲突可能导致静态资源映射失效或者更诡异的问题——静态资源能访问但Controller方法全部404。我在实际项目中就遇到过开发同学为了给资源加特殊Header自己声明了一个ResourceHttpRequestHandler的Bean结果没设置优先级把RequestMappingHandlerMapping顶掉了。排查方法也简单启动日志里看RequestMappingHandlerMapping和SimpleUrlHandlerMapping的顺序如果异常打印所有HandlerMapping的order来定位。3. 修改响应数据的完整工具箱从Controller到Filter的四个层级3.1 四个层级的作用时机和适用场景“修改响应数据”这件事在SpringMVC里有四个常见的操作位置很多人分不清用哪个我直接给一个对比表层级生效时机能否修改响应体典型场景Controller内手动修改Controller方法执行时能直接改返回值或操作HttpServletResponse业务逻辑需要不同格式返回HandlerInterceptor的preHandle/postHandle请求处理前后postHandle能改ModelAndView不能改已写入的body加公共Header、打印日志、过滤非法请求ResponseBodyAdviceResponseBody序列化前能可以替换返回对象统一响应包装、增加加密字段、加签名Filter包装响应Servlet过滤器阶段最早能通过包装响应流截获并修改全量日志、响应压缩、全局替换敏感信息需要注意拦截器的postHandle发生在Controller执行完成后、视图渲染之前。此时如果你用ResponseBody返回数据body其实还没写入postHandle里没法直接改。如果非要用拦截器修改JSON响应体只能通过包装HttpServletResponse但这样又绕过了Spring的高层抽象属于方案设计中不太推荐的思路。3.2 在Controller里直接修改响应数据直接、但不是万能的最直接的修改方式当然是在Controller方法里拿到HttpServletResponse对象手动控制响应内容。例如GetMapping(/download) public void download(HttpServletResponse response) throws IOException { response.setContentType(application/octet-stream); response.setHeader(Content-Disposition, attachment; filename\report.csv\); response.getWriter().write(id,name\n1,张三); }或者用ResponseBody搭配ResponseEntity更灵活地设置状态码、响应头、响应体GetMapping(/api/user) public ResponseEntityUserVO getUser() { UserVO user userService.findById(1L); return ResponseEntity.ok() .header(X-Custom-Header, custom) .body(user); }但这里有两个痛点。第一如果每个接口都要自己处理响应数据格式比如统一包装成{ code: 0, data: ... }那Controller里全是模板代码非常恶心。第二直接操作HttpServletResponse对象会失去SpringMVC的消息转换能力——你需要自己处理中文乱码、JSON序列化。所以“Controller里改”适合的是简单、个别的场景而不是全局、统一的修改需求。3.3 用ResponseBodyAdvice统一修改JSON响应体如果要全局修改所有ResponseBody接口的返回结构ResponseBodyAdvice几乎是官方指定的标准做法。它可以在HttpMessageConverter序列化之前偷换成另一个对象。比如项目里要统一返回结构RestControllerAdvice public class ApiResponseAdvice implements ResponseBodyAdviceObject { private static final String[] SKIP_PATHS {/swagger, /v3/api-docs}; Override public boolean supports(MethodParameter returnType, Class? extends HttpMessageConverter? converterType) { // 只处理Controller返回并且是普通对象的情况 return !returnType.hasMethodAnnotation(ResponseBody.class) false; } Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class? extends HttpMessageConverter? selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { // 已经包装过的比如返回类型是ApiResponse直接返回避免二次包装 if (body instanceof ApiResponse) { return body; } return ApiResponse.success(body); } }这里有几个关键点。supports方法决定“哪些返回值需要被处理”通常有两种写法一是判断返回值类型二是判断请求路径。beforeBodyWrite里要注意“避免二次包装”的判断——如果Controller已经返回了ApiResponse你的Advice又把包一层前端拿到的结构就完全不对了。另外特别提醒ResponseBodyAdvice只对带ResponseBody或RestController的返回值生效。如果你想用它去统一修改Controller返回的视图名或者FreeMarker模板渲染结果它根本不会执行。这意味着设计统一响应格式时必须明确分界线哪些接口走JSON数据响应哪些接口走页面响应。在同一个模块里让同一个Advice同时处理两类“响应数据”这种设计天然会埋雷。3.4 拦截器修改响应的局限与正确打开方式热搜词里有“springmvc拦截器”说明很多人对拦截器改响应有期待这里明确一下边界。HandlerInterceptor中preHandle在Controller执行前此时响应还没开始可以设置response.setHeader也可以直接写字符流提前返回。postHandle在Controller执行后、视图渲染前此时可以往ModelAndView里添加公共数据但无法修改已经序列化好的JSON body。afterCompletion在请求完成后只能做资源清理和日志改响应基本没戏。也就是说拦截器最适合做的修改是给所有响应加统一响应头、给异常响应补充日志、打印Controller耗时。如果你想用拦截器改响应体需要自己包装HttpServletResponseWrapper这非常麻烦而且会绕过SpringMVC的视图解析和消息转换我建议不到万不得已别这么做。那如果需求确实是“给所有响应做body层级的处理”呢选Filter而不是拦截器。OncePerRequestFilter可以在请求进入SpringMVC之前就包装好响应也可以拿到真实输出流。举一个实际场景线上排查问题需要把所有接口的响应JSON都打日志但不想在每个接口里写日志代码。实现方式是通过Filter加一个ContentCachingResponseWrapperComponent public class ResponseLogFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { ContentCachingResponseWrapper wrapper new ContentCachingResponseWrapper(response); chain.doFilter(request, wrapper); byte[] body wrapper.getContentAsByteArray(); // 这里打印日志或者做其他处理 wrapper.copyBodyToResponse(); // 必须调用否则响应体会被清空 } }这个Filter是我在几个项目里都在用的。核心细节是copyBodyToResponse()必须调用——ContentCachingResponseWrapper会把响应体先缓存到内存里如果你读完缓存后不复制回原来的响应流客户端拿到的就是空body。我自己第一次写这个Filter时也忘了结果接口状态码正常前端收不到任何数据排查了半小时才反应过来。4. 模块化项目统一响应数据的完整实战4.1 需求梳理各模块返回结构不统一带来的联调灾难我们这里的Maven模块化项目分成了user-module、order-module、gateway-web等几个Maven子模块每个模块独立开发最后通过IDEA配置Tomcat打成一个war包部署。每个模块的Controller写得很随意有的返回Result对象有的直接返回POJO有的在错误时直接抛异常返回Spring默认错误页。前端对接的人苦不堪言——每接一个接口都要看后端代码确认返回结构到底是{code:xxx,msg:xxx,data:xxx}还是{success:true,data:{...}}。所以这次我们要通过“修改响应数据”的手段从后端层面强制统一所有JSON接口要么返回指定的ApiResponseT要么被ApiResponseAdvice自动包装成统一格式。4.2 实操步骤定义统一返回结构并接入ResponseBodyAdvice第一步定义统一返回结构。这里要注意一个设计细节成功和失败都有规范的构造方法禁止在代码里手动new然后set字段public class ApiResponseT { private int code; private String message; private T data; private long timestamp; public static T ApiResponseT success(T data) { ApiResponseT r new ApiResponse(); r.code 0; r.message success; r.data data; r.timestamp System.currentTimeMillis(); return r; } public static T ApiResponseT error(int code, String message) { ApiResponseT r new ApiResponse(); r.code code; r.message message; r.timestamp System.currentTimeMillis(); return r; } }第二步接入ResponseBodyAdvice。这里有一个容易被忽略的坑如果Controller直接返回String类型且没有指定producesSpringMVC会用StringHttpMessageConverter这个转换器的默认字符集是ISO-8859-1中文会乱码。所以建议beforeBodyWrite里对MediaType统一处理一下Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class? extends HttpMessageConverter? selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { if (body instanceof ApiResponse) { return body; } // 修正字符集避免中文乱码 response.getHeaders().setContentType( new MediaType(MediaType.APPLICATION_JSON, StandardCharsets.UTF_8) ); if (body instanceof String) { // 注意字符串走StringHttpMessageConverter需要特殊包装 ObjectMapper mapper new ObjectMapper(); try { return mapper.writeValueAsString(ApiResponse.success(body)); } catch (JsonProcessingException e) { return ApiResponse.error(500, 序列化失败); } } return ApiResponse.success(body); }这里比较关键的一点是字符串特殊处理。如果body是String你直接返回ApiResponse.success(hello)对象SpringMVC会尝试用StringHttpMessageConverter序列化这个ApiResponse对象但转换器声明只支持String类型于是序列化结果会变成对象地址字符串而不是JSON。所以字符串类型要自己先序列化成JSON字符串或者干脆让Controller统一不要返回裸字符串。我在这个项目里采取的是后者——给团队定规范JSON接口一律返回对象不允许返回裸字符串。第三步跳过不需要包装的路径。比如Spring的/error端点、Swagger文档路径、健康检查探活路径这些路径如果也被包装会破坏一些工具的解析。可以在supports方法里判断路径Override public boolean supports(MethodParameter returnType, Class? extends HttpMessageConverter? converterType) { String path ServletUriComponentsBuilder.fromCurrentRequest().getPath(); if (path.startsWith(/error) || path.startsWith(/actuator) || path.startsWith(/v3/api-docs)) { return false; } return true; }4.3 配合CORS配置给前端联调扫雷统一响应结构只是修改响应数据的第一步。这个项目里前端是单独起的开发服务器端口在8088后端Tomcat在8080跨域在所难免。而热词里的“F12无法加载响应数据:没有可用于预检”就是典型的CORS问题导致的所以CORS配置必须和响应修改一起打通。在SpringMVC里配置CORS最推荐的是实现WebMvcConfigurer的addCorsMappingsOverride public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(http://localhost:8088, http://127.0.0.1:8088) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); }这里有个和拦截器的联动点。如果项目里写了自定义的HandlerInterceptor而且你在preHandle里对请求做了路径校验那么在预检请求OPTIONS时拦截器同样会执行。如果拦截器逻辑里不允许OPTIONS方法、或者校验了Token而预检请求不带Token那预检请求就会在到达CORS处理器之前被拦截器拦截返回403。这就是“没有可用于预检”报错最常见的原因之一。解决方案有两种一种是在拦截器里直接放行OPTIONS请求另一种是给addCorsMappings配置一个最高优先级的CorsFilter。我建议两个都做因为拦截器放行只能用preHandle返回false或true控制而CorsFilter更彻底。下面这段是拦截器里的放行逻辑Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (HttpMethod.OPTIONS.matches(request.getMethod())) { return true; // 放行预检请求让CORS处理器处理 } // 其他正常校验逻辑 return true; }5. 常见问题排查与避坑实录5.1 F12报“无法加载响应数据:没有可用于预检”完整排查这个报错是Chrome开发者工具里很典型的一种。它并不是说后端完全没有响应而是指浏览器发起的预检请求OPTIONS没有得到预期的Access-Control-Allow-*响应头所以浏览器认为当前跨域请求不被允许后续的真实GET/POST请求根本不会发出。完整排查思路如下步骤操作判断标准1F12打开Network勾选“Fetch/XHR”看请求类型如果有一个OPTIONS请求说明预检被触发2点击这个OPTIONS请求看Status如果是403或404说明预检没通过如果是200继续看响应头3看响应头里有没有Access-Control-Allow-Origin必须有且值和请求Origin一致或在该模式下被允许4看Access-Control-Allow-Headers里有没有Content-Type如果前端请求头带Content-Type: application/json这个必须包含Content-Type5切换后端日志看OPTIONS请求有没有进Controller/拦截器如果进了拦截器且被拦截就是拦截器问题很多项目是用Spring Security的那CORS问题就更复杂涉及SecurityFilterChain里的CORS配置。但当前这个纯SpringMVC项目没有Security主要矛盾还是集中在拦截器和HandlerMapping上。有一个非常隐蔽的配置错误如果web.xml里DispatcherServlet的url-pattern配的是/*那么OPTIONS /api/xxx会被DispatcherServlet接管但如果Controller没有显式声明RequestMapping(methodOPTIONS)RequestMappingHandlerMapping匹配不到就会沿着链路走到StaticResourceHandler去。如果你没配置/api/**为静态资源就404预检失败。所以配置/而不是/*这是老生常谈但必须重申的。5.2 静态资源404但Controller访问正常的排查这种问题基本都发生在SpringMVC配置不完整或者配置顺序不对上。排查清单如下检查web.xml或AbstractAnnotationConfigDispatcherServletInitializer里的url-pattern/可以/*不行。检查addResourceHandlers是否注册成功可以通过断点或日志确认ResourceHandlerRegistry里有没有对应映射。检查addResourceLocations路径是否以/结尾。检查资源文件是否真的在classpath对应目录下。Maven项目里src/main/resources/static会被打进classes的static目录如果你没加resources配置或者文件放在了src/main/webapp下但IDEA没有把它同步到Tomcat的部署目录就会404。这时候看Tomcat部署的conf/Catalina下实际文件是否存在。我在IDEA里部署项目时最常遇到的是最后一个问题。解决方法是File - Project Structure - Artifacts里查看war包exploded目录有没有包含static目录如果没有手动加一个Directory指向src/main/webapp。IDEA的Tomcat运行配置里Deployment标签页选中war exploded把application context设成/。5.3 修改响应体后Content-Length不一致接口数据被“吃掉”这个问题在自定义Filter里很常见。直接上场景你用HttpServletResponseWrapper缓存了响应体处理完想返回修改后的内容但没设置Content-Length或者设置了错误的Content-Length。浏览器按旧长度读取结果前面的数据正常最后几个字符丢失甚至JSON解析失败。我的经验是只要在Filter里动过响应体就强制删除Content-Length头让容器或客户端根据实际响应字节长度自己算。如果响应压缩比如Gzip也被引入了那还要注意压缩顺序——先压缩再设置长度还是先设置长度再压缩顺序错了都会出问题。最简单的方式是让响应走完整个链路后一次性读取完整内容再重置长度wrapper.copyBodyToResponse(); response.setContentLength(wrapper.getContentAsByteArray().length);这样修改Content-Length就能和实际内容保持一致。但如果链路中没有经过copyBodyToResponse就去计算长度读到的永远是0。5.4 常见问题速查表现象根因一句话解法静态资源404Controller正常静态资源映射没配或url-pattern为/*配addResourceHandlersurl-pattern用/静态资源被Controller“抢走”HandlerMapping顺序问题或URL撞车检查自定义HandlerMapping的order中文响应乱码StringHttpMessageConverter默认ISO-8859-1设置produces为application/json;charsetUTF-8或在ResponseBodyAdvice里统一处理前端拿不到正常JSON结构被ResponseBodyAdvice二次包装或裸String被错误包装在Advice里判断body类型避免重复包装OPTIONS预检失败CORS未配置或拦截器拦截了OPTIONSaddCorsMappings 拦截器放行OPTIONS修改body后客户端数据不全Content-Length未更新删除Content-Length或修改后重新设置实际字节长度IDEA改静态资源不生效Tomcat部署目录没同步On Update Action设为Update classes and resources5.5 一些实操心得上面这些坑我几乎都在真实项目里踩过一遍。印象最深的是CORS预检那次前端同事说接口通了但拿不到数据F12里全是“没有可用于预检”我一开始以为是Nginx配置问题后来排查到是我们在拦截器里写了一个Token校验OPTIONS请求没带Token就被拦截器拦了。从那次以后我在所有项目的拦截器里都会默认加一行放行OPTIONS的代码不管暂时用不用得上。这个习惯帮我后来避免了很多次联调阶段的无谓争吵。另外静态资源映射和响应数据修饰这两个功能看起来是两个方向的事但在架构设计上其实可以统一起来思考静态资源是“尽量提前响应、不过Controller”响应数据修改是“在响应返回前做统一加工”。两者都指向一个核心问题——响应从服务端到客户端之间到底哪些层可以做统一干预。理解了这个SpringMVC的响应链路就尽在掌握了。
返回列表