Spring HttpHeaders 中可能存在的错误

Posted

技术标签:

【中文标题】Spring HttpHeaders 中可能存在的错误【英文标题】:Possible bug in Spring HttpHeaders 【发布时间】:2018-12-31 00:46:24 【问题描述】:

我发现我认为可能是 Spring 类 HttpHeadersReadOnlyHttpHeaders 中的一个错误。在使用 Spring 提出 Jira 缺陷之前,我想确认这一点。这是我用来创建空HttpHeaders 对象的代码的 sn-p:

HttpHeaders myHeaders = HttpHeaders.writableHttpHeaders(HttpHeaders.EMPTY);

然后我将标题添加到我的新对象中:

myHeaders.add(HttpHeaders.ACCEPT_ENCODING, "gzip")

在此之后HttpHeaders.EMPTY 不再为空

HttpHeaders.EMPTY.size() == 1

HttpHeaders.EMPTY 的 javadoc 声明:

/** * 空的 @code HttpHeaders 实例(不可变)。 */ public static final HttpHeaders EMPTY

这里的问题是,当 'HttpHeaders.EMPTY' 在别处使用时,它会引入意想不到的标头。

考虑以下单元测试:

@Test
public void testUpdateEmptyHeaders() 
    assertEquals(0, HttpHeaders.EMPTY.size()); // **Success**
    HttpHeaders myHeaders = HttpHeaders.writableHttpHeaders(HttpHeaders.EMPTY);
    myHeaders.add(HttpHeaders.ACCEPT_ENCODING, "gzip");
    assertEquals(0, HttpHeaders.EMPTY.size()); // **Assert Fails**


@Test
// This test will fail if run after the test above, but will be successful if run by itself
public void testEmptyHeaders() 
    assertEquals(0, HttpHeaders.EMPTY.size()); 

这是单元测试的结果:

// testUpdateEmptyHeaders
08:39:28.450 [main] DEBUG org.springframework.test.context.support.AbstractDirtiesContextTestExecutionListener - After test method: context [DefaultTestContext@2e222612, testMethod = testUpdateEmptyHeaders@AuditContextTest, testException = java.lang.AssertionError: expected:<0> but was:<1>

java.lang.AssertionError: 
Expected :0
Actual   :1

// testEmptyHeaders
08:39:28.482 [main] DEBUG org.springframework.test.context.support.AbstractDirtiesContextTestExecutionListener - After test method: context [DefaultTestContext@2e222612, testMethod = testEmptyHeaders@AuditContextTest, testException = java.lang.AssertionError: expected:<0> but was:<1>

java.lang.AssertionError: 
Expected :0
Actual   :1

我觉得这是一个错误,因为HttpHeaders.EMPTY 应该是不可变的。 我还可以通过在 Spring HttpHeaders.javaReadOnlyHttpHeaders.java 中进行两项更改来解决此问题

【问题讨论】:

【参考方案1】:

是的,你是对的,这可能是 Spring 框架的错误 HttpHeaders

public static final HttpHeaders EMPTY

HttpHeaders.EMPTY 这将返回空的 HttpHeaders 实例(不可变)。 (而且是单例)

案例:1 我们来看看HttpHeaders.Empty,它返回的是不可变对象

    HttpHeaders head = HttpHeaders.EMPTY;
    
    System.out.println(System.identityHashCode(head));  //1338668845
    
    System.out.println(head.size());                    //0
    
    HttpHeaders myHeaders = HttpHeaders.writableHttpHeaders(HttpHeaders.EMPTY); 
    
    System.out.println(System.identityHashCode(myHeaders));  //159413332
    
    myHeaders.add(HttpHeaders.ACCEPT_ENCODING, "gzip"); 
    
    head = HttpHeaders.EMPTY; 
    System.out.println(System.identityHashCode(head));       //1338668845
    System.out.println(head.size());                    //1
    System.out.println(head);                            //Accept-Encoding=[gzip]
    
    HttpHeaders head1 = HttpHeaders.EMPTY;
    System.out.println(head1);                          //Accept-Encoding=[gzip]
    System.out.println(System.identityHashCode(head1));   //1338668845

结论:

1 : HttpHeaders.EMPTY 总是返回单例对象

2:问题是当HttpHeaders.EMPTY在内部传递给writableHttpHeaders方法时,返回的对象与HttpHeaders.EMPTY单例对象有关系,请看案例2

案例:2writableHttpHeaders返回对象反映到HttpHeaders.EMPTY单例对象(内部和间接)

    HttpHeaders head = HttpHeaders.EMPTY;           
    
    System.out.println(System.identityHashCode(head));  //1338668845
    
    System.out.println(head.size());                //0
    
    HttpHeaders myHeaders = HttpHeaders.writableHttpHeaders(HttpHeaders.EMPTY); 
    
    System.out.println(System.identityHashCode(myHeaders));  //159413332
    
    myHeaders.add(HttpHeaders.ACCEPT_ENCODING, "gzip"); 
    
    myHeaders.add("hello", "value");
    
    head = HttpHeaders.EMPTY; 
    System.out.println(System.identityHashCode(head));  //1338668845
    System.out.println(head.size());              //2
    System.out.println(head);                     //Accept-Encoding=[gzip], hello=[value]
    
    HttpHeaders head1 = HttpHeaders.EMPTY;
    System.out.println(head1);                    //Accept-Encoding=[gzip], hello=[value]
    System.out.println(System.identityHashCode(head1));   //1338668845
    
    myHeaders.remove("hello");
    
    System.out.println(System.identityHashCode(head));     //1338668845
    System.out.println(head.size());                 //1
    System.out.println(head);                       //Accept-Encoding=[gzip]
    
    System.out.println(head1);                        //Accept-Encoding=[gzip]
    System.out.println(System.identityHashCode(head1));    //1338668845
    

结论:

1 : addremovemyHeaders 对象执行的操作反映到 HttpHeaders.EMPTY 对象

案例:3假设如果我们使用构造函数将 HttpHeaders 对象的空实例传递给writableHttpHeaders,那么一切都不会出现问题

    HttpHeaders head = HttpHeaders.EMPTY;
    
    System.out.println(System.identityHashCode(head));       //1338668845
    
    System.out.println(head.size());                     //0
    
    HttpHeaders myHeaders = HttpHeaders.writableHttpHeaders(new HttpHeaders()); 
    
    System.out.println(System.identityHashCode(myHeaders));       //1323165413
    
    myHeaders.add(HttpHeaders.ACCEPT_ENCODING, "gzip"); 
    
    myHeaders.add("hello", "value");
    
    head = HttpHeaders.EMPTY; 
    System.out.println(System.identityHashCode(head));           //1338668845
    System.out.println(head.size());                    //0
    System.out.println(head);                           //
    
    HttpHeaders head1 = HttpHeaders.EMPTY;
    System.out.println(head1);                           //
    System.out.println(System.identityHashCode(head1));   //1338668845

案例:4虽然间接不可变HttPHeaders.EMPTY可以修改,但是直接修改还是会报错

    HttpHeaders head = HttpHeaders.EMPTY;
    
    System.out.println(System.identityHashCode(head));
    
    System.out.println(head.size());
    
    HttpHeaders myHeaders = HttpHeaders.writableHttpHeaders(HttpHeaders.EMPTY); 
    
    System.out.println(System.identityHashCode(myHeaders));
    
    myHeaders.add(HttpHeaders.ACCEPT_ENCODING, "gzip"); 
    
    head = HttpHeaders.EMPTY; 
    System.out.println(System.identityHashCode(head));
    System.out.println(head.size());
    System.out.println(head);
    
    head.add("hello", "value");

输出:

1338668845
0
159413332
1338668845
1
Accept-Encoding=[gzip]
Exception in thread "main" java.lang.UnsupportedOperationException
at org.springframework.http.ReadOnlyHttpHeaders.add(ReadOnlyHttpHeaders.java:67)
at com.demo.NestedJsonParse.main(NestedJsonParse.java:40)

最终结论:是的,你可以为 spring 项目spring-bug 提出一个错误,不可变对象不能改变状态

【讨论】:

感谢您的评论@deadpool,但我认为您误解了这个问题。正如您所说的那样,当您使用不可变的 HttpHeaders.EMPTY 作为 HttpHeaders.writableHeaders() 的输入时,您会得到一个可以添加标题的可变对象。问题是对不可变 HttpHeaders.EMPTY 的任何进一步调用都不会返回空的 HttpHeaders,而是包含添加到可变对象的标头。例如,如果在将标头添加到可变对象的单元测试之后运行以下单元测试,则会失败 对不起,我没有明白你的意思,你能在帖子中解释更多细节或更新吗?只是粗体点@athom 我在上面的示例中添加了第二个单元测试,它在第一个单元测试后运行时失败,以显示 HttpHeaders.EMPTY 的基础值已更改 我不这么认为,我试过循环,总是得到0和空地图@athom 请注意单元测试中的第一个和最后一个断言 testEmptyHeaders() 获取 HttpHeaders.EMPTY 的值,并且在这两种情况下都应该为空【参考方案2】:

我已经报告了这个错误,并在 Spring 5.1.4 中根据https://jira.spring.io/browse/SPR-17633 进行了修复

【讨论】:

以上是关于Spring HttpHeaders 中可能存在的错误的主要内容,如果未能解决你的问题,请参考以下文章

基于spring的文件上传下载

spring中的ResponseEntity理解

Spring 远程调用工具类RestTemplateUtils

Spring junit 测试模板可能不存在

来自 ReactiveSecurityContext 的 Spring WebClient 标头

Spring MVC 的 input type="hidden" 可能存在的危险和安全问题