记一次@PathVariable特殊参数会丢失的排查问题

本文探讨了在RESTful API设计中,如何处理请求参数中包含特殊字符(如'.', '.sh', '.bat'等)所引发的数据丢失或类型误判问题。提供了两种解决方案:一是通过调整路径变量匹配规则,二是禁用或修改框架根据扩展名自动选择内容类型的机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  • 请求参数中如果包含.,会造成参数丢失,请看如下代码
    以下代码,省略@RestController控制层类代码
@RequestMapping(value = "hello/{name}")
public Map<String, Object> sayHello(@PathVariable("name") String name, HttpServletRequest request) {
    Map<String, Object> rtnMap = new HashMap<>();
    rtnMap.put("msg", "hello " + name);
    return rtnMap;
}

请求地址: hello/ddf,则正常返回{"msg": "hello ddf"}
请求地址: hello/ddf.com,依然还是返回{"msg": "hello ddf"}
如果需要解决上面这个问题,则可以将代码更改如下(该解决方式从网上搜寻)

@RequestMapping(value = "hello/{name:.*}")
public Map<String, Object> sayHello(@PathVariable("name") String name, HttpServletRequest request) {
    Map<String, Object> rtnMap = new HashMap<>();
    rtnMap.put("msg", "hello " + name);
    return rtnMap;
}
  • 如果使用@PathVariable.sh.bat等特殊字符结尾,会影响实际返回数据
    报错如下:
{
    "timestamp": 1541405292119,
    "status": 406,
    "error": "Not Acceptable",
    "exception": "org.springframework.web.HttpMediaTypeNotAcceptableException",
    "message": "Could not find acceptable representation",
    "path": "/HDOrg/user/hello/ddf.sh"
}

还是上面的代码
以下代码,省略@RestController控制层类代码

@RequestMapping(value = "hello/{name:.*}")
public Map<String, Object> sayHello(@PathVariable("name") String name, HttpServletRequest request) {
    Map<String, Object> rtnMap = new HashMap<>();
    rtnMap.put("msg", "hello " + name);
    return rtnMap;
}

如果这时候请求地址为hello/ddf.shhello/ddf.com.sh,只要是以.sh结尾,这时候业务逻辑代码不会受到影响,但走到Spring自己的代码去处理返回数据的时候,有一个功能会根据扩展名来决定返回的类型,而以.sh结尾扩展名为sh,会被解析成对应的Content-Type: application/x-sh
解决办法如下,第一种方法是从网上找到的,可以直接禁用该功能,但有可能会影响到静态资源的访问,不能确定,也没有进行尝试

@Configuration
public class Config extends WebMvcConfigurerAdapter {
    @Override
    public void configureContentNegotiation(
            ContentNegotiationConfigurer configurer) {
        configurer.favorPathExtension(false);
    }
}

然后以下就是闲着没事很想换个思路尝试去看看这到底是怎么回事,由于个人能力有限,不保证以下内容的重要性;
第二种方式解决思路是,既然扩展名以.sh等结尾会有问题,那么能不能不要让程序将扩展名识别为.sh,或者干脆就跳过处理,比如我是否可以加个.sh/这样就会影响到实际的扩展名,但是又不会影响到已有的代码,其实这里有个偷懒的写法,可以直接在@RequestMapping里的value最后直接加一个/,但是这要求客户端必须在原有的条件上最终拼一个/,否则会找不到对应的映射,直接404,我这里碰到这个问题的时候,因为该方法已经上线并且被其它几个系统调用,因此更改起来会有些繁琐,所以寻求了一种麻烦的方式,先将解决方式放在下面,不确定是否会影响其它问题
这种方式解决方式如下:注释中的两行代码二选一都可,推荐前面的写法,直接已经跳过

@RequestMapping(value = "hello/{name:.*}")
public String sayHello(@PathVariable("name") String name) {
    // 该方法跳过通过上面描述的那种方式来确定MediaType
    request.setAttribute(PathExtensionContentNegotiationStrategy.class.getName() + ".SKIP", true);
    // 后面参数的值前半部分必须和该方法的RequestMapping一致,否则无效,不包括ContextPath
    request.setAttribute(WebUtils.INCLUDE_REQUEST_URI_ATTRIBUTE,  "/hello/" + name + "/");
    return "hello " + name;
}

下面依赖源码来看一下为什么可以这么去做,先看一下为什么会造成这个结果?以下步骤只关心与当前问题有关的部分,并只大概关注其中问题,不作细节的深入
经过debug可以看到错误是在处理以下过程报错,首先如下

public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
    @Override
    public void handleReturnValue(Object returnValue, MethodParameter returnType,
            ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
            throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {

        mavContainer.setRequestHandled(true);
        ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
        ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);

        // Try even with null return value. ResponseBodyAdvice could get involved.
        writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
    }
}

出现这个问题,一般的查找思路就是是否是请求或响应的Content-Type是否出现了问题,那么在上面这个方法上无论是inputMessage还是outputMessage都是正常的,重点来看一下writeWithMessageConverters()方法,该方法,部分代码如下

public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver
        implements HandlerMethodReturnValueHandler {

    @SuppressWarnings("unchecked")
    protected <T> void writeWithMessageConverters(T value, MethodParameter returnType,
                                                  ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
        throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {

        Object outputValue;
        Class<?> valueType;
        Type declaredType;

        if (value instanceof CharSequence) {
            outputValue = value.toString();
            valueType = String.class;
            declaredType = String.class;
        }
        else {
            outputValue = value;
            valueType = getReturnValueType(outputValue, returnType);
            declaredType = getGenericType(returnType);
        }

        HttpServletRequest request = inputMessage.getServletRequest();
        List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request);
        List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType);
        // 后面处理MediaType的部分在这里全部省略
    }    
    
    /**
     * Returns the media types that can be produced:
     * <ul>
     * <li>The producible media types specified in the request mappings, or
     * <li>Media types of configured converters that can write the specific return value, or
     * <li>{@link MediaType#ALL}
     * </ul>
     * @since 4.2
     */
    protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> valueClass, Type declaredType) {
        Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
        if (!CollectionUtils.isEmpty(mediaTypes)) {
            return new ArrayList<MediaType>(mediaTypes);
        }
        else if (!this.allSupportedMediaTypes.isEmpty()) {
            List<MediaType> result = new ArrayList<MediaType>();
            for (HttpMessageConverter<?> converter : this.messageConverters) {
                if (converter instanceof GenericHttpMessageConverter && declaredType != null) {
                    if (((GenericHttpMessageConverter<?>) converter).canWrite(declaredType, valueClass, null)) {
                        result.addAll(converter.getSupportedMediaTypes());
                    }
                }
                else if (converter.canWrite(valueClass, null)) {
                    result.addAll(converter.getSupportedMediaTypes());
                }
            }
            return result;
        }
        else {
            return Collections.singletonList(MediaType.ALL);
        }
    }

}

先看方法getAcceptableMediaTypes(),是根据请求来决定当前的HttpServletRequest到底是要请求什么类型的数据,该方法调用链在后面说明;
getProducibleMediaTypes()方法返回可以生成的MediaType,能够生成哪些是看当前项目一共有多少可以被支持的MediaType,当然也能看到也可以通过HttpServletRequest明确设置属性HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE来确定用哪种方式;
拿到这两个列表后,需要判断requestedMediaTypes是否兼容producibleMediaTypes,如*/*则可以兼容所有的可以生成的MediaType,最终将兼容的requestedMediaTypes循环处理,看是否是一个具体的MediaType而不是通配符,那么最终生效的MediaType就是这个,当然存在多个,则也就存在多个不是通配也满足条件的,所以再循环前也做了一次排序,保证优先级最高的一定会生效。

public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver
        implements HandlerMethodReturnValueHandler {
        
        private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException {
        List<MediaType> mediaTypes = this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
        return (mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes);
    }
    
}

MediaType.java

public class MediaType extends MimeType implements Serializable {
    public static final MediaType ALL;

    /**
     * A String equivalent of {@link MediaType#ALL}.
     */
    public static final String ALL_VALUE = "*/*";
    
    // 静态初始化MediaType.ALL的值省略
}

该方法的结果可以看到如果调用的方法返回了一个空的列表,则该方法返回MediaType.ALL的列表,通过代码可以看到它的值为*/*,该方法往下调用部分代码如下:

public class ContentNegotiationManager implements ContentNegotiationStrategy, MediaTypeFileExtensionResolver {
    
    @Override
    public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
        for (ContentNegotiationStrategy strategy : this.strategies) {
            List<MediaType> mediaTypes = strategy.resolveMediaTypes(request);
            if (mediaTypes.isEmpty() || mediaTypes.equals(MEDIA_TYPE_ALL)) {
                continue;
            }
            return mediaTypes;
        }
        return Collections.emptyList();
    }
}

调用如下:

public class WebMvcAutoConfiguration {
    @Override
    public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest)
        throws HttpMediaTypeNotAcceptableException {
        
        private static final String SKIP_ATTRIBUTE = PathExtensionContentNegotiationStrategy.class
                .getName() + ".SKIP";
        
        Object skip = webRequest.getAttribute(SKIP_ATTRIBUTE,
                                              RequestAttributes.SCOPE_REQUEST);
        if (skip != null && Boolean.parseBoolean(skip.toString())) {
            return Collections.emptyList();
        }
        return this.delegate.resolveMediaTypes(webRequest);
    }
}

在这里可以看到有一个属性为skip,如果它的属性为PathExtensionContentNegotiationStrategy的类全名+".SKP"并且它的值为true,那么这里则不继续往下处理直接返回空的集合,而在前面也已经看到如果返回的空的集合,实际上最终返回给调用方的是*/*,结合前面看到的org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#writeWithMessageConverters(T, org.springframework.core.MethodParameter, org.springframework.http.server.ServletServerHttpRequest, org.springframework.http.server.ServletServerHttpResponse)这个方法,*/*是可以匹配任何生成的producibleMediaTypes,所以最终结果能够按照原先应该返回的类型正确返回,而不会被.sh等后缀影响到;

其实最初没有看到skip的时候,看到了一些后面的代码,最终也解决了这个问题,不论正确与否,先把整个过程记录下来,假如在上面的步骤中没有设置skip=true,那么程序继续下去的部分走向如下

// 如果uid以.sh结尾的话,在逻辑处理完成之后框架处理return数据的时候,会根据扩展名来决定返回的content-type,sh结尾
        // 会影响返回的content-type为application/x-sh,这会影响该方法的实际功能,解决办法是:
        // 要么禁用该功能,要么修改该方法的@RequestMapping,禁用不能确定是否会对直接访问的静态资源有影响,
        // 而且该方法调用方项目已上线,不宜轻易修改,只能这里改变这个属性的地址,影响框架
        // 后面获取请求的后缀为null,而避免这个问题,但尚不能确认requestUrl和mappingUrl不一致是否会有别的问题
        // request.setAttribute(WebUtils.INCLUDE_REQUEST_URI_ATTRIBUTE,  "/user/" + uid + "/");
<think>我们正在讨论SpringBoot中如何接收参数。用户特别关心在POST方法中使用@PathVariable接收路径参数(ids)是否正确,并询问是否有官方文档说明。根据引用[2]和引用[3]的内容:-@PathVariable用于从URL路径中获取变量,通常用于RESTful风格的URL。-@RequestParam用于从请求参数(即URL中?后面的部分)获取值。-@RequestBody用于从请求体中获取数据,通常用于POST请求中传递的JSON或XML数据。用户提到的是POST方法中使用@PathVariable接收ids参数。这里的关键点在于:1.@PathVariable是用于从URL模板中提取值的,与HTTP方法(GET、POST等)无关。也就是说,无论是GET还是POST,只要URL路径中包含变量,就可以使用@PathVariable来获取。2.因此,在POST方法中使用@PathVariable接收路径参数是完全可以的,也是常见的做法。官方文档说明:SpringBoot(实际上是SpringMVC)的官方文档中对此有明确说明。我们可以参考SpringFramework官方文档中关于@PathVariable的部分。例如,在SpringFramework5.3.x的文档中(https://docs.spring.io/spring-framework/docs/5.3.x/reference/html/web.html#mvc-ann-requestmapping-uri-templates):>Youcanusethe@PathVariableannotationtobindamethodparametertothevalueofaURItemplatevariable.并且,该注解可以用于任何请求方法(包括POST)。因此,用户的做法是正确的。示例代码:```java@RestController@RequestMapping("/api")publicclassMyController{@PostMapping("/resource/{id}")publicResponseEntity<String>handlePost(@PathVariable("id")Longid,@RequestBodySomeDatadata){//处理逻辑returnResponseEntity.ok("Receivedid:"+id);}}```在这个例子中,我们使用POST方法访问`/api/resource/123`,路径中的`123`会被绑定到`id`参数上。关于用户提到的ids(复数),如果路径中有多个变量,也是可以的:```java@PostMapping("/resource/{category}/{id}")publicResponseEntity<String>handlePost(@PathVariableStringcategory,@PathVariableLongid,@RequestBodySomeDatadata){//...}```总结:1.POST方法使用@PathVariable接收路径参数是正确的。2.官方文档链接(以SpringFramework5.3.x为例):https://docs.spring.io/spring-framework/docs/5.3.x/reference/html/web.html#mvc-ann-requestmapping-uri-templates注意:由于用户要求生成相关问题,我们将在回答后生成相关问题。</think>### Spring Boot POST 方法中使用 `@PathVariable` 接收路径参数的正确性分析 #### 1. **`@PathVariable` 的使用规范** 在 Spring Boot 中,**POST 方法使用 `@PathVariable` 接收路径参数是完全正确的**。该注解的设计与 HTTP 方法无关,仅用于从 URL 路径中提取变量值: ```java @PostMapping("/users/{id}") // POST 方法 + 路径变量 public ResponseEntity<?> updateUser( @PathVariable Long id, // ✅ 正确接收路径参数 @RequestBody UserDto user // 接收请求体 ) { // 业务逻辑 } ``` - **路径参数特性**:`{id}` 是 URL 的一部分(如 `POST /users/123`),`@PathVariable` 会将其绑定到方法参数 `id`。 - **与 HTTP 方法无关**:适用于 GET、POST、PUT、DELETE 等所有请求方法[^2][^3]。 #### 2. **与 `@RequestParam` 和 `@RequestBody` 的区别** | 注解 | 作用位置 | 适用场景 | POST 方法示例 | |------------------|----------------|-----------------------------|--------------------------------| | `@PathVariable` | **URL 路径** | 获取路径中的动态变量 | `POST /orders/{orderId}` | | `@RequestParam` | **查询字符串** | 获取 `?key=value` 形式的参数 | `POST /search?keyword=spring` | | `@RequestBody` | **请求体** | 接收 JSON/XML 等结构化数据 | `POST /users` (Body: JSON) | #### 3. **官方文档说明** Spring 官方文档明确支持在 POST 方法中使用 `@PathVariable`: > 官方文档指出: > **`@PathVariable` is used to bind a URI template variable to a method parameter.** > This applies to any request method, including POST, PUT, etc.[^3] 参考链接: [Spring Framework 官方文档 - URI 模板变量](https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller/ann-methods/uri-template.html) #### 4. **常见错误排查** - **问题 1**:路径变量名与方法参数名不一致 **解决**:显式指定变量名 ```java @PostMapping("/products/{productId}") public void update(@PathVariable("productId") Long id) { ... } ``` - **问题 2**:路径变量未定义 **错误 URL**:`POST /users` (缺少 `/{id}`) **正确 URL**:`POST /users/123` - **问题 3**:类型转换失败 **示例**:`@PathVariable String id` 接收非字符串值会报错,需确保类型匹配(如 `Long` 接收数字)。 #### 5. **最佳实践建议** 1. **RESTful 设计**: 路径变量用于标识资源(如 `PUT /api/users/{id}` 更新指定用户)。 2. **结合 `@RequestBody`**: 路径变量标识资源,请求体携更新数据: ```java @PutMapping("/books/{isbn}") public Book updateBook( @PathVariable String isbn, @RequestBody BookUpdateDto dto ) { ... } ``` 3. **验证路径参数**: 使用 `javax.validation` 校验数据: ```java @PostMapping("/orders/{id}") public void processOrder( @PathVariable @Min(1) Long id // 验证id必须≥1 ) { ... } ``` --- ### 相关问题 1. **`@PathVariable` 和 `@RequestParam` 在接收数组类型参数时有何区别?** (提示:`@PathVariable` 需定义多个路径变量,而 `@RequestParam` 可直接接收列表) 2. **如何在 Spring Boot 中处理 `@PathVariable` 的类型转换错误?** (例如:路径传入非数字字符,但方法参数声明为 `Long`) 3. **Spring Boot 是否支持在 `@PathVariable` 中使用正则表达式?如何实现?** (参考:`@GetMapping("/{id:\\d+}")`) 4. **在微服务架构中,`@PathVariable` 和网关路由配置如何协作?** (涉及 API Gateway 的路径重写规则)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值