找回密码
 立即注册
首页 资源区 代码 SpringBoot3整合AI

SpringBoot3整合AI

后仲舒 2025-6-4 22:42:33
玩一下AI

1. SSE协议

我们都知道tcp,ip,http,https,websocket等等协议,今天了解一个新的协议SSE协议(Server-Sent Events)
SSE(Server-Sent Events) 是一种允许服务器主动向客户端推送数据的轻量级协议,基于 HTTP 长连接,实现 单向通信(服务器→客户端)。它是 W3C 标准,浏览器原生支持,无需额外插件(如 EventSource API)
核心特点与工作原理

  • 单向通信:仅服务器向客户端发送数据,适合实时通知、日志流、实时更新等场景。
  • 基于 HTTP:客户端通过 GET 请求建立连接,服务器返回特殊格式的文本流(text/event-stream),连接保持打开状态,直到服务器主动关闭或超时。
  • 自动重连:浏览器内置重连机制,连接断开后自动尝试重新连接。
  • 数据格式:每条消息以 \n 分隔,支持事件类型、数据内容、重试时间等字段,例如:
  1. data: Hello, SSE!  // 数据内容
  2. event: customEvent // 自定义事件类型(可选)
  3. id: 123            // 消息ID(可选)
  4. retry: 5000        // 重连时间(毫秒,可选)
  5. \n
复制代码
适用于无需双向通信,仅需服务器单向推送数据。【比如现在的 gpt,豆包这个问答形式】
前端客户端可以使用原生的 EventSource API:
  1. // 创建EventSource实例,连接服务器
  2. const eventSource = new EventSource('/sse-endpoint');
  3. // 监听默认事件("message")
  4. eventSource.onmessage = (event) => {
  5.   console.log('Received:', event.data);
  6. };
  7. // 监听自定义事件(如"customEvent")
  8. eventSource.addEventListener('customEvent', (event) => {
  9.   console.log('Custom Event:', event.data);
  10. });
  11. // 处理错误
  12. eventSource.onerror = (error) => {
  13.   console.error('SSE Error:', error);
  14.   // 浏览器会自动重连,无需手动处理
  15. };
复制代码
服务端可用的就太多了。(本文以SpringBoot3.4.2为例子)
在知道这个协议之前,我们想要达到gpt这种问答形式,输出内容是一点一点拼接的,该怎么弄呢?是不是还可以用websocket。
特性SSEWebSocket通信方向单向(服务器→客户端)双向(全双工)协议基于 HTTP(升级为长连接)独立协议(ws:// 或 wss://)二进制支持仅文本(text/event-stream)支持文本和二进制自动重连浏览器内置需手动实现复杂度简单(服务端实现轻量)较复杂(需处理握手、心跳)适用场景服务器单向推送数据双向交互(聊天、实时协作)下面结合Spring Boot 简单用一下SSE
  1. // sse协议测试
  2. @PostMapping(value = "/chat", produces = "text/event-stream;charset=UTF-8")
  3. public SseEmitter streamSseMvc() {
  4.     SseEmitter emitter = new SseEmitter(30_000L);
  5.     // 模拟发送消息
  6.     System.out.println("SSE connection started");
  7.     ScheduledFuture<?> future = service.scheduleAtFixedRate(() -> {
  8.         try {
  9.             String message = "Message at " + System.currentTimeMillis();
  10.             emitter.send(SseEmitter.event().data(message));
  11.         } catch (IOException e) {
  12.             try {
  13.                 emitter.send(SseEmitter.event().name("error").data(Map.of("error", e.getMessage())));
  14.             } catch (IOException ex) {
  15.                 // ignore
  16.             }
  17.             emitter.completeWithError(e);
  18.         }
  19.     }, 0, 5, TimeUnit.SECONDS);
  20.     emitter.onCompletion(() -> {
  21.         System.out.println("SSE connection completed");
  22.     });
  23.     emitter.onTimeout(() -> {
  24.         System.out.println("SSE connection timed out");
  25.         emitter.complete();
  26.     });
  27.     emitter.onError((e) -> {
  28.         System.out.println("SSE connection error: " + e.getMessage());
  29.         emitter.completeWithError(e);
  30.     });
  31.     return emitter;
  32. }
复制代码
在SpringBoot中,用SseEmitter就可以达到这个效果了,它也和Websocket一样有onXXX这种类似的方法。上面是使用一个周期性的任务,来模拟AI生成对话的效果的。emitter.send(SseEmitter.event().data(message)); 这个就是服务端向客户端推送数据。
2. okhttp3+sse+deepseek

简单示例:就问一句话
申请deepseekKey这里就略过了,各位读者自行去申请。【因为deepseek官网示例是用的okhttp,所以我这里也用okhttp了】
我们先准备一个接口
  1. @RestController
  2. @RequestMapping("/deepseek")
  3. public class DeepSeekController {
  4.     @Resource
  5.     private DeepSeekUtil deepSeekUtil;
  6.     /**
  7.      * 访问deepseek-chat
  8.      */
  9.     @PostMapping(value = "/chat", produces = "text/event-stream;charset=UTF-8")
  10.     public SseEmitter chatSSE() throws IOException {
  11.         SseEmitter emitter = new SseEmitter(60000L);
  12.         deepSeekUtil.sendChatReqStream("123456", "你会MySQL数据库吗?", emitter);
  13.         return emitter; // 这里把该sse对象返回了
  14.     }
  15.     private boolean notModel(String model) {
  16.         return !"deepseek-chat".equals(model) && !"deepseek-reasoner".equals(model);
  17.     }
  18. }
复制代码
可以看到我们创建了一个SseEmitter对象,传给了我们自定义的工具
  1. @Component
  2. public class DeepSeekUtil {
  3.     public static final String DEEPSEEK_CHAT = "deepseek-chat";
  4.     public static final String DEEPSEEK_REASONER = "deepseek-reasoner";
  5.     public static final String url = "https://api.deepseek.com/chat/completions";
  6.     // 存储每个用户的消息列表
  7.     private static final ConcurrentHashMap<String, List<Message>> msgList = new ConcurrentHashMap<>();
  8.     // 1.调用api,然后以以 SSE(server-sent events)的形式以流式发送消息增量。消息流以 data: [DONE] 结尾。
  9.     public void sendChatReqStream(String uid, String message, SseEmitter sseEmitter) throws IOException {
  10.         // 1.构建一个普通的聊天body请求
  11.         AccessRequest tRequest = buildNormalChatRequest(uid, message);
  12.         OkHttpClient client = new OkHttpClient().newBuilder()
  13.                 .build();
  14.         // 封装请求体参数
  15.         MediaType mediaType = MediaType.parse("application/json; charset=utf-8");
  16.         RequestBody body = RequestBody.create(JSON.toJSONString(tRequest), mediaType);
  17.         // 构建请求和请求头
  18.         Request request = new Request.Builder()
  19.                 .url(url)
  20.                 .method("POST", body)
  21.                 .addHeader("Content-Type", "application/json")
  22.                 .addHeader("Accept", "text/event-stream")
  23.                     // 比如你的key是:s-123456
  24.                     // .addHeader("Authorization", "Bearer s-123456")
  25.                 .addHeader("Authorization", "Bearer 你的key")
  26.                 .build();
  27.                 // 创建一个监听器
  28.         SseChatListener listener = new SseChatListener(sseEmitter);
  29.         RealEventSource eventSource = new RealEventSource(request, listener);
  30.         eventSource.connect(client);
  31.     }
  32.     private AccessRequest buildNormalChatRequest(String uid, String message) {
  33.         // 这里,我们messages,添加了一条“你会MySQL数据库吗?",来达到一种对话具有上下文的效果
  34.         List<Message> messages = msgList.computeIfAbsent(uid, k -> new ArrayList<>());
  35.         messages.add(new Message("user", message));
  36.         /*
  37.         [
  38.                 {"system", "你好, 我是DeepSeek-AI助手!"},       
  39.                 {"user", "你会MySQL数据库吗?"}
  40.         ]
  41.         */
  42.         AccessRequest request = new AccessRequest();
  43.         request.setMessages(messages);
  44.         request.setModel(DEEPSEEK_CHAT);
  45.         request.setResponse_format(Map.of("type", "text"));
  46.         request.setStream(true); // 设置为true
  47.         request.setTemperature(1.0);
  48.         request.setTop_p(1.0);
  49.         return request;
  50.     }
  51.     @PostConstruct
  52.     public void init() {
  53.         List<Message> m = new ArrayList<Message>();
  54.         m.add(new Message("system", "你好, 我是DeepSeek-AI助手!"));
  55.         // 初始化消息列表
  56.         msgList.put("123456", m);
  57.     }
  58. }
  59. // 请求体,参考deepseek官网
  60. public class AccessRequest {
  61.     private List<Message> messages;
  62.     private String model; // 默认模型为deepseek-chat
  63.     private Double frequency_penalty = 0.0;
  64.     private Integer max_tokens;
  65.     private Double presence_penalty = 0.0;
  66.     //{
  67.     //    "type": "text"
  68.     //}
  69.     private Map<String, String> response_format;
  70.     private Object stop = null; // null
  71.     private Boolean stream; //如果设置为 True,将会以 SSE(server-sent events)的形式以流式发送消息增量。消息流以 data: [DONE] 结尾。
  72.     private Object stream_options = null;
  73.     private Double temperature; // 1
  74.     private Double top_p; // 1
  75.     private Object tools; // null
  76.     private String tool_choice = "none";
  77.     private Boolean logprobs = false;
  78.     private Integer top_logprobs = null;
  79.     // get set
  80. }
复制代码
监听器
  1. @Slf4j
  2. public class SseChatListener extends EventSourceListener {
  3.     private SseEmitter sseEmitter;
  4.     public SseChatListener( SseEmitter sseEmitter) {
  5.        this.sseEmitter = sseEmitter;
  6.     }
  7.     /**
  8.      * 事件
  9.      */
  10.     @Override
  11.     public void onEvent(EventSource eventSource, String id, String type, String data) {
  12.         //log.info("sse数据:{}", data);
  13.         DeepSeekResponse deepSeekResponse = JSON.parseObject(data, DeepSeekResponse.class);
  14.         DeepSeekResponse.Choice[] choices = deepSeekResponse.getChoices();
  15.         try {
  16.             // 发送给前端【客户端】
  17.             sseEmitter.send(SseEmitter.event().data(choices[0]));
  18.         } catch (IOException e) {
  19.             log.error("数据发送异常");
  20.             throw new RuntimeException(e);
  21.         }
  22.     }
  23.     /**
  24.      * 建立sse连接
  25.      */
  26.     @Override
  27.     public void onOpen(final EventSource eventSource, final Response response) {
  28.         log.info("建立sse连接... {}");
  29.     }
  30.     /**
  31.      * sse关闭
  32.      */
  33.     @Override
  34.     public void onClosed(final EventSource eventSource) {
  35.         log.info("sse连接关闭:{}");
  36.     }
  37.     /**
  38.      * 出错了
  39.      */
  40.     @Override
  41.     public void onFailure(final EventSource eventSource, final Throwable t, final Response response) {
  42.         log.error("使用事件源时出现异常......");
  43.     }
  44. }
  45. // DeepSeekResponse.java
  46. @Data
  47. @NoArgsConstructor
  48. @AllArgsConstructor
  49. public class DeepSeekResponse {
  50.     private String id;
  51.     private String object;
  52.     private Long created;
  53.     private String model;
  54.     private String system_fingerprint;
  55.     private Choice[] choices;
  56.     @Data
  57.     @AllArgsConstructor
  58.     @NoArgsConstructor
  59.     public static class Choice {
  60.         private Integer index;
  61.         private Delta delta;
  62.         private Object logprobs;
  63.         private String finish_reason;
  64.     }
  65.     @Data
  66.     @AllArgsConstructor
  67.     @NoArgsConstructor
  68.     public static class Delta {
  69.         private String content;
  70.     }
  71. }
复制代码
然后我们用apifox测试一下:
1.png

返回这些信息,然后把ai返回的存起来,具体怎么存,就靠读者自行发挥了,添加到该对话,使该对话具有上下文。【DeepSeek /chat/completions API 是一个“无状态” API,即服务端不记录用户请求的上下文,用户在每次请求时,需将之前所有对话历史拼接好后,传递给对话 API。】
  1. [
  2.     {"system", "你好, 我是DeepSeek-AI助手!"},       
  3.     {"user", "你会MySQL数据库吗?"},
  4.     {"ststem", "是的,我熟悉........"} // 把ai返回的存起来
  5. ]
复制代码
下一次对话的时候,请求体AccessRequest里面的List messages就向上面那样,再往后添加用户问的消息。
上面的例子还有一些小问题,比如说什么时候断开连接那些的。
3. SpringAI

Spring AI 是一个专注于 AI 工程的应用框架,其目标是将 Spring 生态的 “POJO 构建块” 和模块化设计引入 AI 场景,简化企业数据与第三方模型的对接和使用。
下面快速接入deepseek
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3.          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  4.     <modelVersion>4.0.0</modelVersion>
  5.     <parent>
  6.         <groupId>org.springframework.boot</groupId>
  7.         spring-boot-starter-parent</artifactId>
  8.         <version>3.4.3</version>
  9.         <relativePath/>
  10.     </parent>
  11.     <groupId>com.feng.ai</groupId>
  12.     spring-ai-chat</artifactId>
  13.     <version>0.0.1-SNAPSHOT</version>
  14.     <name>spring-ai-chat</name>
  15.     <description>spring-ai-chat</description>
  16.    
  17.     <properties>
  18.         <java.version>21</java.version>
  19.         <spring-ai.version>1.0.0-M6</spring-ai.version>
  20.     </properties>
  21.     <dependencies>
  22.         <dependency>
  23.             <groupId>org.springframework.boot</groupId>
  24.             spring-boot-starter-web</artifactId>
  25.         </dependency>
  26.         <dependency>
  27.             <groupId>org.springframework.boot</groupId>
  28.             spring-boot-starter-webflux</artifactId>
  29.         </dependency>
  30.         
  31.         <dependency>
  32.             <groupId>org.springframework.ai</groupId>
  33.             spring-ai-openai-spring-boot-starter</artifactId>
  34.         </dependency>
  35.         <dependency>
  36.             <groupId>com.alibaba.fastjson2</groupId>
  37.             fastjson2</artifactId>
  38.             <version>2.0.44</version>
  39.         </dependency>
  40.     </dependencies>
  41.     <repositories>
  42.         <repository>
  43.             <id>spring-snapshots</id>
  44.             <name>Spring Snapshots</name>
  45.             <url>https://repo.spring.io/snapshot</url>
  46.             <releases>
  47.                 <enabled>false</enabled>
  48.             </releases>
  49.             <snapshots>
  50.                 <enabled>true</enabled>
  51.             </snapshots>
  52.         </repository>
  53.         <repository>
  54.             <name>Central Portal Snapshots</name>
  55.             <id>central-portal-snapshots</id>
  56.             <url>https://central.sonatype.com/repository/maven-snapshots/</url>
  57.             <releases>
  58.                 <enabled>false</enabled>
  59.             </releases>
  60.             <snapshots>
  61.                 <enabled>true</enabled>
  62.             </snapshots>
  63.         </repository>
  64.     </repositories>
  65.     <dependencyManagement>
  66.         <dependencies>
  67.             <dependency>
  68.                 <groupId>org.springframework.ai</groupId>
  69.                 spring-ai-bom</artifactId>
  70.                 <version>${spring-ai.version}</version>
  71.                 <type>pom</type>
  72.                 <scope>import</scope>
  73.             </dependency>
  74.         </dependencies>
  75.     </dependencyManagement>
  76. </project>
复制代码
然后是配置文件
  1. spring:
  2.   application:
  3.     name: spring-ai-chat
  4.   ai:
  5.     # The DeepSeek API doesn't support embeddings, so we need to disable it.
  6.     openai:
  7.       embedding:
  8.         enabled: false
  9.       base-url: https://api.deepseek.com
  10.       api-key: 你的key
  11.       chat:
  12.         options:
  13.           model: deepseek-reasoner # 使用推理模型
  14.           stream-usage: true
复制代码
controller
  1. @Slf4j
  2. @RestController
  3. @RequestMapping("/sp/deepseek")
  4. public class SpDeepseekController {
  5.     @Resource( name = "openAiChatModel")
  6.     private OpenAiChatModel deepseekModel;
  7.    
  8.     // 直接回答 --- stream-usage: false
  9.     //@GetMapping("/simpleChat")
  10.     //public R chat() {
  11.     //    String call = deepseekModel.call("你好, 你会java吗?");
  12.     //    return R.success().setData("call", call);
  13.     //}
  14.     // 流式回答
  15.     @PostMapping(value = "/streamChat", produces = "text/event-stream;charset=UTF-8")
  16.     public Flux<SpMessage> streamChat(@RequestBody Map<String, String> p) {
  17.         String userMessage = p.get("userMessage");
  18.         String sessionId = p.get("sessionId");
  19.         
  20.         Prompt prompt = new Prompt(new UserMessage(userMessage));
  21.         StringBuilder modelStr = new StringBuilder();
  22.         return deepseekModel.stream(prompt)
  23.                 .doOnSubscribe(subscription -> log.info("SSE 连接已启动: {}", sessionId))
  24.                 .doOnComplete(() -> log.info("SSE 连接已关闭: {}", sessionId))
  25.                 .doOnCancel(() -> log.info("SSE 连接已取消: {}", sessionId))
  26.                 .timeout(Duration.ofSeconds(60)) // 超时设置
  27.                 .filter(chatResponse -> chatResponse.getResult().getOutput().getText() != null) // 过滤掉空的响应
  28.                 .map(chatResponse -> {
  29.                         //log.info("SSE 响应: {}", chatResponse.getResult().getOutput());
  30.                     modelStr.append(chatResponse.getResult().getOutput().getText());
  31.                         return SpMessage.builder()
  32.                             .role("system")
  33.                             .content(chatResponse.getResult().getOutput().getText())
  34.                             .build();
  35.                     }
  36.                 );
  37.     }
  38. }
复制代码
TODO:上面的对话没有记忆,新的请求来了,ai模型并不会带上以前的场景,故需要记忆化。 记忆化的同时还要注意如果把该会话历史中所有的对话全部传给deepseek的话,可能导致 token 超限,故还需要做一个窗口,避免把太多历史对话传过去了。
4. 延伸-Http远程调用

在不讨论微服务架构模式下,我们平时开发难免会碰到需要远程调用接口的情况,【比如说上面调用deepseek的服务】,那么,我们怎么做才是比较好的方式呢?
一次良好的调用过程,我们应该要考虑这几点:超时处理、重试机制、异常处理、日志记录
此外,于性能来说,我们要避免频繁创建连接带来的开销,可以使用连接池管理
① RestTemplate

RestTemplate 是一个同步的 HTTP 客户端,提供了简单的方法来发送 HTTP 请求并处理响应。它支持常见的 HTTP 方法(GET、POST、PUT、DELETE 等),并能自动处理 JSON/XML 的序列化和反序列化,这个也是我们非常熟悉的。
下面由于是基于SpringBoot3.4.3,所以httpclient的版本是httpclient5.
  1. @Configuration
  2. public class RestConfig {
  3.     @Bean("restTemplate")
  4.     public RestTemplate restTemplate() {
  5.         // 使用Apache HttpClient连接池(替代默认的 SimpleClientHttpRequestFactory)
  6.         PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
  7.         connectionManager.setMaxTotal(100);      // 最大连接数
  8.         connectionManager.setDefaultMaxPerRoute(20); // 每个路由的最大连接数
  9.         CloseableHttpClient httpClient = HttpClients.custom()
  10.                 .setConnectionManager(connectionManager)
  11.                 .evictIdleConnections(TimeValue.of(10, TimeUnit.SECONDS))// 清理空闲连接
  12.                 .build();
  13.         HttpComponentsClientHttpRequestFactory factory =
  14.                 new HttpComponentsClientHttpRequestFactory(httpClient);
  15.         factory.setConnectTimeout(3000);  // 连接超时(ms)
  16.         factory.setReadTimeout(5000);  // 读取超时(ms)
  17.         RestTemplate restTemplate = new RestTemplate(factory);
  18.                 // 添加自定义的错误处理器
  19.         restTemplate.setErrorHandler(new CustomErrorHandler());
  20.         // 添加日志拦截器
  21.         restTemplate.getInterceptors().add(new LoggingInterceptor());
  22.         return restTemplate;
  23.     }
  24. }
  25. @Slf4j
  26. public class LoggingInterceptor implements ClientHttpRequestInterceptor {
  27.     @NotNull
  28.     @Override
  29.     public ClientHttpResponse intercept(HttpRequest request, @NotNull byte[] body, ClientHttpRequestExecution execution) throws IOException {
  30.         log.info("请求地址: {} {}", request.getMethod(), request.getURI());
  31.         log.info("请求头: {}", request.getHeaders());
  32.         log.info("请求体: {}", new String(body, StandardCharsets.UTF_8));
  33.         ClientHttpResponse response = execution.execute(request, body);
  34.         log.info("响应状态码: {}", response.getStatusCode());
  35.         return response;
  36.     }
  37. }
  38. @Slf4j
  39. public class CustomErrorHandler implements ResponseErrorHandler {
  40.     @Override
  41.     public boolean hasError(@NotNull ClientHttpResponse response) throws IOException {
  42.         // 获取 HTTP 状态码
  43.         HttpStatusCode statusCode = response.getStatusCode();
  44.         return statusCode.isError(); // 判断状态码是否为错误状态码 【4xx、5xx是true,执行下面的handleError,其他的就false】
  45.     }
  46.     @Override
  47.     public void handleError(@NotNull URI url, @NotNull HttpMethod method, @NotNull ClientHttpResponse response) throws IOException {
  48.         log.info("请求地址: {}  Method: {}",url, method);
  49.         HttpStatusCode code = response.getStatusCode();
  50.         if (code.is4xxClientError()) {
  51.             log.info("客户端错误:{}", code.value());
  52.             // xxx
  53.         } else {
  54.             log.info("服务器错误:{}", code.value());
  55.             // xxx
  56.         }
  57.     }
  58. }
复制代码
重试降级机制:
  1. @Configuration
  2. @EnableRetry // 开启重试 -- 需要引入AOP
  3. public class RetryConfig {
  4. }
  5. // 在service层调用的时候
  6. @Service
  7. public class OrderService {
  8.     @Resource
  9.     private RestTemplate restTemplate;
  10.     @Retryable(
  11.         maxAttempts = 3,
  12.         backoff = @Backoff(delay = 1000, multiplier = 2), // 重试间隔 1s, 2s, 4s
  13.         retryFor = {Exception.class} // 默认重试所有异常
  14.         //retryFor = {ResourceAccessException.class} // 仅在网络异常时重试
  15.     )
  16.     public String queryOrder(String orderId) {
  17.         return restTemplate.getForObject("/orders/" + orderId, String.class); // 远程调用
  18.     }
  19.     @Recover // 重试全部失败后的降级方法
  20.     public String fallbackQueryOrder(ResourceAccessException e, String orderId) {
  21.         return "默认订单";
  22.     }
  23. }
复制代码
当然还可以再远程调用那里try catch起来,有异常的时候,throw出去可以被@Retryable捕获。
② RestClient

Spring Framework 6.1 引入了全新的同步 HTTP 客户端 RestClient,它在底层使用了与 RestTemplate 相同的基础设施(比如消息转换器和拦截器),但提供了如同 WebClient 一样的现代、流式(fluent)API,兼顾了简洁性与可复用性。与传统的阻塞式 RestTemplate 相比,RestClient 更加直观易用,同时也保持了对同步调用语境的全量支持
同步调用:RestClient 是一个阻塞式客户端,每次 HTTP 请求都会阻塞调用线程直到响应完成。
流式 API:借鉴 WebClient 的设计风格,所有操作均可链式调用,代码更具可读性和可维护性。
复用基础组件:与 RestTemplate 共用 HTTP 请求工厂、消息转换器、拦截器等组件,便于平滑迁移与统一配置
  1. @Configuration
  2. @Slf4j
  3. public class RestClientConfig {
  4.     @Bean("serviceARestClient")
  5.     public RestClient restClientA(@Value("${api-service.a-base-url}") String baseUrl) {
  6.         // 创建连接池
  7.         PoolingHttpClientConnectionManager manager = new PoolingHttpClientConnectionManager();
  8.         manager.setMaxTotal(100);
  9.         manager.setDefaultMaxPerRoute(20);
  10.         // 创建HttpClient
  11.         HttpClient httpClient = HttpClientBuilder.create()
  12.                 .setConnectionManager(manager)
  13.                 .build();
  14.         // 创建HttpComponentsClientHttpRequestFactory
  15.         HttpComponentsClientHttpRequestFactory factory =
  16.                 new HttpComponentsClientHttpRequestFactory(httpClient);
  17.         factory.setConnectTimeout(3000);
  18.         factory.setReadTimeout(5000);
  19.         return RestClient.builder()
  20.                 .baseUrl(baseUrl)
  21.                 .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
  22.                 .defaultCookie("myCookie", "1234")
  23.                 .requestInterceptor(clientRequestInterceptor())
  24.                 .requestFactory(factory) // 连接池与超时
  25.                 .build();
  26.     }
  27.     @Bean("serviceBRestClient")
  28.     public RestClient restClientB(@Value("${api-service.b-base-url}") String baseUrl) {
  29.         return RestClient.builder()
  30.                 .baseUrl(baseUrl)
  31.                 .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
  32.                 .defaultCookie("myCookie", "1234")
  33.                 .requestInterceptor(clientRequestInterceptor())
  34.                 .build();
  35.     }
  36.     private ClientHttpRequestInterceptor clientRequestInterceptor() {
  37.         return (request, body, execution) -> {
  38.             // 添加统一请求头(如认证信息)
  39.             request.getHeaders().add("my-head", "head-gggggg");
  40.             // 日志记录
  41.             log.debug("Request: {} {}", request.getMethod(), request.getURI());
  42.             request.getHeaders().forEach((name, values) ->
  43.                     values.forEach(value -> log.debug("Header: {}={}", name, value)));
  44.             ClientHttpResponse response = execution.execute(request, body);
  45.             log.debug("Response status: {}", response.getStatusCode());
  46.             return response;
  47.         };
  48.     }
  49. }
复制代码
简单调用:
  1. @Service
  2. public class AService {
  3.     @Resource(name = "serviceARestClient")
  4.     private RestClient restClientA;
  5.     public String queryA(String a) {
  6.         return restClientA.get()
  7.                 .uri("/api/a?a={a}", a)
  8.                 .retrieve()
  9.                 .onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {
  10.                     throw new HttpClientErrorException(response.getStatusCode());
  11.                 })
  12.                 .onStatus(HttpStatusCode::is5xxServerError, (request, response) -> {
  13.                     throw new ServerErrorException(response.getStatusCode().toString(), null);
  14.                 })
  15.                 .body(String.class);
  16.     }
  17.     // 复杂query参数
  18.     public String queryA(String a, String b) {
  19.         return restClientA.get()
  20.                 .uri(  uriBuilder ->
  21.                         uriBuilder.path("/api/bbb")
  22.                         .queryParam("a", 25)
  23.                         .queryParam("b", "30")
  24.                         .build()
  25.                 )
  26.                 .retrieve()
  27.                 .onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {
  28.                     throw new HttpClientErrorException(response.getStatusCode());
  29.                 })
  30.                 .onStatus(HttpStatusCode::is5xxServerError, (request, response) -> {
  31.                     throw new ServerErrorException(response.getStatusCode().toString(), null);
  32.                 })
  33.                 .body(String.class);
  34.     }
  35.     // post
  36.     public String postA(String a) {
  37.         HashMap<String, Object> map = new HashMap<>();
  38.         map.put("a", a); map.put("page", 1); map.put("size", 10);
  39.         return restClientA.post()
  40.                 .uri("/api/post")
  41.                 .body(map)
  42.                 .retrieve()
  43.                 .onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {
  44.                     throw new HttpClientErrorException(response.getStatusCode());
  45.                 })
  46.                 .onStatus(HttpStatusCode::is5xxServerError, (request, response) -> {
  47.                     throw new ServerErrorException(response.getStatusCode().toString(), null);
  48.                 })
  49.                 .body(String.class);
  50.     }
  51. }
复制代码
③ WebClient

Spring框架中包含的原始web框架Spring web MVC是专门为Servlet API和Servlet容器构建的。响应式堆栈web框架Spring WebFlux是在5.0版本中添加的。它是完全非阻塞的,支持响应式流回压,并运行在诸如Netty、Undertow和Servlet容器之类的服务器上。
这两个web框架都镜像了它们的源模块的名字(Spring-webmvc和Spring-webflux 他们的关系图如下,节选自官网),并在Spring框架中共存。每个模块都是可选的。应用程序可以使用其中一个或另一个模块,或者在某些情况下,两者都使用——例如,Spring MVC控制器与响应式WebClient。它对同步和异步以及流方案都有很好的支持。
非阻塞异步模型:基于 Reactor 库(Mono/Flux)实现异步调用,避免线程阻塞,通过少量线程处理高并发请求,显著提升性能
函数式编程:支持链式调用(Builder 模式)与 Lambda 表达式,代码更简洁
流式传输:支持大文件或实时数据的分块传输(Chunked Data),减少内存占用。
2.png

这里就不介绍了。
特性RestTemplateRestClientWebClient模型阻塞,同步阻塞,同步,流式 API非阻塞,响应式【学习曲线较为陡峭】API 风格模板方法 (getForObject, exchange 等)链式流式 (get().uri()...retrieve())链式流式,支持 Mono/Flux可扩展性依赖大量重载方法可配置拦截器、初始器,支持自定义消息转换器强大的过滤器、拦截器与背压支持性能受限于线程池同 RestTemplate,但更简洁更佳,适合高并发场景迁移成本低较低,可自然承接现有 RestTemplate 配置较高,需要重构为响应式编程end. 参考


  • https://segmentfault.com/a/1190000021133071 【思否-Spring5的WebClient使用详解】
  • https://docs.spring.io/spring-framework/reference/integration/rest-clients.html 【Spring官网】

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册