SpringBoot项目中HTTP请求体只能读一次?试试这方案

问题描述

在基于Spring开发Java项目时,可能需要重复读取HTTP请求体中的数据,例如使用拦截器打印入参信息等,但当我们重复调用getInputStream()或者getReader()时,通常会遇到类似以下的错误信息:
在这里插入图片描述
大体的意思是当前request的getInputStream()已经被调用过了。那为什么会出现这个问题呢?

原因分析

主要原因有两个,一是Java自身的设计中,InputStream作为数据管道本身只支持读取一次,如果要支持重复读取的话就需要重新初始化;二是Servlet容器中Request的实现问题,我们以默认的Tomcat为例,可以发现在Request有两个boolean类型的属性,分别是usingReader和usingInputStream,当调用getInputStream()或getReader()时会分别检查两个属性的值,并在执行后将对应的属性设置为true,如果在检查时变量的值已经为true了,那么就会报出以上错误信息。
在这里插入图片描述

解决方案

不太可行的方案:简单粗暴的反射机制

涉及到变量的修改,我们首先想到的就是有没有提供方法进行修改,不过可惜的是usingReader和usingInputStream并未提供,所以想要在使用过程中修改这两个属性估计只能靠反射了,在使用过程中每次调用后通过反射将usingReader和usingInputStream设置为false,每次根据读取出的内容把数据流初始化回去,理论上就可以再次读取了。

首先说反射机制本身就是通过破坏类的封装来实现动态修改的,有点过于粗暴了,其次也是主要原因,我们只能针对我们自己实现的代码进行处理,框架本身如果调用getInputStream()和getReader()的话,我们就没法通过这个办法干预了,所以这个方案在给予Spring的Web项目中并不可行。

理论上可行的方案:HttpServletRequest接口

HttpServletRequest是一个接口,理论上我们只需要创建一个实现类就可以自定义getInputStream()和getReader()的行为,自然也就能解决RequestBody不能重复读取的问题,但这个方案的问题在于HttpServletRequest有70个方法,而我们只需要修改其中两个而已,通过这种方式去解决有点得不偿失。

部分场景可行的方案:ContentCachingRequestWrapper

Spring本身提供了一个Request包装类来处理重复读取的问题,即ContentCachingRequestWrapper,其实现思路就是在读取RequestBody时将内存缓存到它内部的一个字节流中,后续读取可以通过调用getContentAsString()或getContentAsByteArray()获取到缓存下来的内容。

之所以说这个方案是部分场景可行主要是两个方面,一是ContentCachingRequestWrapper没有重写getInputStream()和getReader()方法,所以框架中使用这两个方法的地方依然获取不到缓存下来的内容,仅支持自定义的业务逻辑;第二点和第一点有所关联,因为其没有修改getInputStream()和getReader()方法,所以我们在使用时只能在使用RequestBody注解后使用ContentCachingRequestWrapper,否则就会出现RequestBody注解修饰的参数无法正常读取请求体的问题,也就限定了它的使用范围如下图所示:
在这里插入图片描述 如果仅需要在业务代码后再次读取请求体内容,那么使用ContentCachingRequestWrapper也足以满足需求,具体使用方法请参考下一节的说明。

目前的最佳实践:继承HttpServletRequestWrapper

之前我们提到实现HttpServletRequest需要实现70个方法,所以不太可能自行实现,这个方案算是进阶版本,继承HttpServletRequest的实现类,之后再自定义我们需要修改的两个方法。

HttpServletRequest作为一个接口,肯定会有其实现去支撑它的业务功能,因为Servlet容器的选择较多,我们也不能使用某一方提供的实现,所以选择的范围也就被限制到了Java EE(现在叫Jakarta EE)标准范围内,通过查看HttpServletRequest的实现,可以发现在标准内提供了一个包装类:HttpServletRequestWrapper,我们的方案也是围绕它展开。

思路简述

  1. 自定义子类,继承HttpServletRequestWrapper,在子类的构造方法中将RequestBody缓存到自定义的属性中。
  2. 自定义getInputStream()和getReader()的业务逻辑,不再校验usingReader和usingInputStream,且在调用时读取缓存下来的内容。
  3. 自定义Filter,将默认的HttpServletRequest替换为自定义的包装类。

代码展示

  1. 继承HttpServletRequestWrapper,实现子类CustomRequestWrapper,并自定义getInputStream()和getReader()的业务逻辑
// 1.继承HttpServletRequestWrapper
public class CustomRequestWrapper extends HttpServletRequestWrapper {

    // 2.定义final属性,用于缓存请求体内容
    private final byte[] content;

    public CustomRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        // 3.构造方法中将请求体内容缓存到内部属性中
        this.content = StreamUtils.copyToByteArray(request.getInputStream());
    }

    // 4.重新getInputStream()
    @Override
    public ServletInputStream getInputStream() {
        // 5.将缓存下来的内容转换为字节流
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(content);
        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener listener) {

            }

            @Override
            public int read() {
                // 6.读取时读取第5步初始化的字节流
                return byteArrayInputStream.read();
            }
        };
    }

    // 7.重写getReader()方法,这里复用getInputStream()的逻辑
    @Override
    public BufferedReader getReader() {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }
}
  1. 自定义Filter将默认的HttpServletRequest替换为自定义的CustomRequestWrapper
// 1.实现Filter接口,此处也可以选择继承HttpFilter
public class RequestWrapperFilter implements Filter {
    // 2. 重写或实现doFilter方法
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 3.此处判断是为了缩小影响范围,本身CustomRequestWrapper只是针对HttpServletRequest,不进行判断可能会影响其他类型的请求
        if (request instanceof HttpServletRequest) {
            // 4.将默认的HttpServletRequest转换为自定义的CustomRequestWrapper
            CustomRequestWrapper requestWrapper = new CustomRequestWrapper((HttpServletRequest) request);
            // 5.将转换后的request传递至调用链中
            chain.doFilter(requestWrapper, response);
        } else {
            chain.doFilter(request, response);
        }
    }
}
  1. 将Filter注册到Spring容器,这一步可以通过多种方式执行,这里采用比较传统但比较灵活的Bean方式注册,如果图方便可以通过ServletComponentScan注解+ WebFilter注解的方式。
/**
 * 过滤器配置,支持第三方过滤器
 */
@Configuration
public class FilterConfigure {
    /**
     * 请求体封装
     * @return
     */
    @Bean
    public FilterRegistrationBean<RequestWrapperFilter> filterRegistrationBean(){
        FilterRegistrationBean<RequestWrapperFilter> bean = new FilterRegistrationBean<>();
        bean.setFilter(new RequestWrapperFilter());
        bean.addUrlPatterns("/*");
        return bean;
    }
}

至此我们就可以在项目中重复读取请求体了,如果选择使用Spring提供的ContentCachingRequestWrapper,那么在Filter中将CustomRequestWrapper替换为ContentCachingRequestWrapper即可,不过需要注意在上一节提到的可用范围较小的问题。

文章内的代码可以参考 https://gitee.com/itartisans/itartisans-framework,这是我开源的一个SpringBoot项目脚手架,我会不定期加入一些通用功能,欢迎关注。

最后欢迎大家关注我的个人公众号“IT匠心工坊”,我会更新一些IT相关的知识,目前主要是Java方向,希望可以对你有所帮助

<think>好的,用户让我写一篇关于“SpringBoot怎么集成DeepSeek”的博客,要求阅性好,结合实例,用通俗易懂的文字。首先,我需要确定DeepSeek是什么。可能是指深度求索公司的AI服务,比如他们的API接口。假设用户是想在SpringBoot项目中集成DeepSeek的某个API,比如大模型或者智能客服功能。 接下来,我得考虑用户的需求。他们可能是一个开发者,想在自己的SpringBoot应用中使用DeepSeek的功能,但不知道如何开始。需要步骤清晰的指南,包括配置、代码示例和常见问题解决。用户可能对SpringBoot有一定了解,但对集成第三方API不太熟悉,所以需要详细说明每一步。 然后,结构方面,标题要明确。分几个部分:前置准备、创建项目、集成步骤、功能测试、扩展优化。每个部分下要有小标题,让者容易浏览。实例部分需要代码片段,比如使用RestTemplate或WebClient调用API,以及如何处理响应。 需要注意用词通俗,避免太多专业术语,或者解释清楚术语。比如解释什么是RestTemplate,为什么用JSON格式化。结合实例时,可以举一个智能对话的例子,展示如何发送请求和接收回复,让者更容易理解。 还要考虑可能的常见问题,比如依赖冲突、网络超时、鉴权失败,给出解决方案。这部分能帮助者避免踩坑,提升博客的实用性。 最后,扩展部分可以提到异步调用或异常处理,让有兴趣的者进一步学习。结尾鼓励动手实践,强调集成过程的简单性,增强者的信心。 检查是否需要假设DeepSeek的具体API接口,比如假设他们有一个对话接口,可能需要API密钥和特定的请求格式。在示例中统一这些假设,保持一致性。确保代码示例正确,比如正确的包导入和配置方式。 总之,博客需要结构清晰,步骤详细,示例具体,语言易懂,解决问题实际,帮助开发者顺利集成DeepSeek到SpringBoot项目中。</think># SpringBoot轻松集成DeepSeek:三步实现智能对话功能 ![header-image](https://example.com/springboot-deepseek.png) 你是否想过为自己的SpringBoot应用添加类似ChatGPT的对话能力?通过集成DeepSeek大模型API,只需1小时就能让项目获得智能对话功能!本文将以实际项目为例,手把手教你实现这个酷炫的功能。 --- ## 一、准备工作:备齐三样工具 在开始编码前,我们需要准备以下三样必需品: 1. **DeepSeek API账号**(前往官网注册获取API Key) 2. **JDK 17+**(推荐使用Amazon Corretto) 3. **IntelliJ IDEA**(社区版即可) > 小贴士:测试阶段建议选择DeepSeek的免费套餐,足够完成开发测试 --- ## 二、创建SpringBoot项目(5分钟) 使用Spring Initializr快速搭建项目: 1. 访问 [start.spring.io](https://start.spring.io) 2. 选择: - Project: Maven - Language: Java - Spring Boot: 3.2.0 3. 添加依赖: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> ``` --- ## 三、核心集成步骤(30分钟) ### 3.1 配置API密钥 在`application.yml`中添加: ```yaml deepseek: api: key: your_api_key_here url: https://api.deepseek.com/v1/chat/completions ``` ### 3.2 创建对话请求类 ```java @Data @AllArgsConstructor public class ChatRequest { private String model = "deepseek-chat"; private List<Message> messages; @Data @AllArgsConstructor public static class Message { private String role; private String content; } } ``` ### 3.3 实现API调用服务 ```java @Service @RequiredArgsConstructor public class DeepSeekService { private final RestTemplate restTemplate; @Value("${deepseek.api.key}") private String apiKey; @Value("${deepseek.api.url}") private String apiUrl; public String chat(String userMessage) { ChatRequest request = new ChatRequest(List.of( new ChatRequest.Message("user", userMessage) )); HttpHeaders headers = new HttpHeaders(); headers.setBearerAuth(apiKey); headers.setContentType(MediaType.APPLICATION_JSON); return restTemplate.postForObject( apiUrl, new HttpEntity<>(request, headers), String.class ); } } ``` --- ## 四、实战测试:创建智能对话接口 ### 4.1 编写测试控制器 ```java @RestController @RequiredArgsConstructor public class ChatController { private final DeepSeekService deepSeekService; @PostMapping("/chat") public ResponseEntity<?> chat(@RequestBody String message) { try { String response = deepSeekService.chat(message); return ResponseEntity.ok(response); } catch (Exception e) { return ResponseEntity.internalServerError().body("服务暂时不可用"); } } } ``` ### 4.2 发送测试请求 使用Postman测试: ```http POST http://localhost:8080/chat Content-Type: text/plain 帮我用Java写一个快速排序算法 ``` 响应示例: ```json { "id": "chatcmpl-123", "object": "chat.completion", "created": 1677652288, "choices": [{ "index": 0, "message": { "role": "assistant", "content": "以下是Java实现的快速排序...(具体代码)" } }] } ``` --- ## 五、常见问题排雷指南 1. **401鉴权失败** ✅ 检查API密钥是否正确 ✅ 确认请求头是否正确携带Bearer Token 2. **响应速度慢** ✅ 添加连接超时配置: ```java @Bean public RestTemplate restTemplate() { return new RestTemplateBuilder() .setConnectTimeout(Duration.ofSeconds(10)) .build(); } ``` 3. **中文乱码问题** ✅ 在配置中添加编码设置: ```yaml spring: http: encoding: charset: UTF-8 enabled: true force: true ``` --- ## 六、功能扩展方向 1. **对话上下文保持** 在ChatRequest中追加历史消息: ```java messages.add(new Message("assistant", lastResponse)); messages.add(new Message("user", newMessage)); ``` 2. **流式响应处理** 使用WebClient实现SSE: ```java WebClient.create().post() .uri(apiUrl) .bodyValue(request) .retrieve() .bodyToFlux(String.class); ``` --- ## 七、项目优化建议 1. **敏感词过滤** 在发送请求前添加过滤逻辑: ```java public String chat(String message) { if (containsSensitiveWords(message)) { throw new IllegalArgumentException("包含违规内容"); } // 原有逻辑 } ``` 2. **异步处理** 使用@Async提升并发能力: ```java @Async public CompletableFuture<String> asyncChat(String message) { return CompletableFuture.completedFuture(chat(message)); } ``` --- 通过以上步骤,我们成功为SpringBoot应用接入了DeepSeek的智能对话能力。建议者根据实际需求调整参数配置,例如设置temperature参数控制回答的随机性(0-2之间,值越大越有创意)。 > 项目完整代码已上传GitHub:https://github.com/example/springboot-deepseek-demo **动手试试吧!** 你会发现集成AI功能比想象中简单得多。如果遇到任何问题,欢迎在评论区留言讨论~
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值