SpringBoot3+AI

玩一下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 分隔,支持事件類型、數據內容、重試時間等字段,例如:
data: Hello, SSE!  // 數據內容
event: customEvent // 自定義事件類型(可選)
id: 123            // 消息ID(可選)
retry: 5000        // 重連時間(毫秒,可選)
\n

適用于無需雙向通信,僅需服務器單向推送數據。【比如現在的 gpt,豆包這個問答形式】

前端客戶端可以使用原生的 EventSource API:

// 創建EventSource實例,連接服務器
const eventSource = new EventSource('/sse-endpoint');
// 監聽默認事件("message")
eventSource.onmessage = (event) => {console.log('Received:', event.data);
};
// 監聽自定義事件(如"customEvent")
eventSource.addEventListener('customEvent', (event) => {console.log('Custom Event:', event.data);
});
// 處理錯誤
eventSource.onerror = (error) => {console.error('SSE Error:', error);// 瀏覽器會自動重連,無需手動處理
};

服務端可用的就太多了。(本文以SpringBoot3.4.2為例子)

在知道這個協議之前,我們想要達到gpt這種問答形式,輸出內容是一點一點拼接的,該怎么弄呢?是不是還可以用websocket。

特性SSEWebSocket
通信方向單向(服務器→客戶端)雙向(全雙工)
協議基于 HTTP(升級為長連接)獨立協議(ws:// 或 wss://)
二進制支持僅文本(text/event-stream支持文本和二進制
自動重連瀏覽器內置需手動實現
復雜度簡單(服務端實現輕量)較復雜(需處理握手、心跳)
適用場景服務器單向推送數據雙向交互(聊天、實時協作)

下面結合Spring Boot 簡單用一下SSE

 // sse協議測試
@PostMapping(value = "/chat", produces = "text/event-stream;charset=UTF-8")
public SseEmitter streamSseMvc() {SseEmitter emitter = new SseEmitter(30_000L);// 模擬發送消息System.out.println("SSE connection started");ScheduledFuture<?> future = service.scheduleAtFixedRate(() -> {try {String message = "Message at " + System.currentTimeMillis();emitter.send(SseEmitter.event().data(message));} catch (IOException e) {try {emitter.send(SseEmitter.event().name("error").data(Map.of("error", e.getMessage())));} catch (IOException ex) {// ignore}emitter.completeWithError(e);}}, 0, 5, TimeUnit.SECONDS);emitter.onCompletion(() -> {System.out.println("SSE connection completed");});emitter.onTimeout(() -> {System.out.println("SSE connection timed out");emitter.complete();});emitter.onError((e) -> {System.out.println("SSE connection error: " + e.getMessage());emitter.completeWithError(e);});return emitter;
}

在SpringBoot中,用SseEmitter就可以達到這個效果了,它也和Websocket一樣有onXXX這種類似的方法。上面是使用一個周期性的任務,來模擬AI生成對話的效果的。emitter.send(SseEmitter.event().data(message)); 這個就是服務端向客戶端推送數據。

2. okhttp3+sse+deepseek

簡單示例:就問一句話

申請deepseekKey這里就略過了,各位讀者自行去申請。【因為deepseek官網示例是用的okhttp,所以我這里也用okhttp了】

我們先準備一個接口

@RestController
@RequestMapping("/deepseek")
public class DeepSeekController {@Resourceprivate DeepSeekUtil deepSeekUtil;/*** 訪問deepseek-chat*/@PostMapping(value = "/chat", produces = "text/event-stream;charset=UTF-8")public SseEmitter chatSSE() throws IOException {SseEmitter emitter = new SseEmitter(60000L);deepSeekUtil.sendChatReqStream("123456", "你會MySQL數據庫嗎?", emitter);return emitter; // 這里把該sse對象返回了}private boolean notModel(String model) {return !"deepseek-chat".equals(model) && !"deepseek-reasoner".equals(model);}
}

可以看到我們創建了一個SseEmitter對象,傳給了我們自定義的工具

@Component
public class DeepSeekUtil {public static final String DEEPSEEK_CHAT = "deepseek-chat";public static final String DEEPSEEK_REASONER = "deepseek-reasoner";public static final String url = "https://api.deepseek.com/chat/completions";// 存儲每個用戶的消息列表private static final ConcurrentHashMap<String, List<Message>> msgList = new ConcurrentHashMap<>();// 1.調用api,然后以以 SSE(server-sent events)的形式以流式發送消息增量。消息流以 data: [DONE] 結尾。public void sendChatReqStream(String uid, String message, SseEmitter sseEmitter) throws IOException {// 1.構建一個普通的聊天body請求AccessRequest tRequest = buildNormalChatRequest(uid, message);OkHttpClient client = new OkHttpClient().newBuilder().build();// 封裝請求體參數MediaType mediaType = MediaType.parse("application/json; charset=utf-8");RequestBody body = RequestBody.create(JSON.toJSONString(tRequest), mediaType);// 構建請求和請求頭Request request = new Request.Builder().url(url).method("POST", body).addHeader("Content-Type", "application/json").addHeader("Accept", "text/event-stream")// 比如你的key是:s-123456// .addHeader("Authorization", "Bearer s-123456").addHeader("Authorization", "Bearer 你的key").build();// 創建一個監聽器SseChatListener listener = new SseChatListener(sseEmitter);RealEventSource eventSource = new RealEventSource(request, listener);eventSource.connect(client);}private AccessRequest buildNormalChatRequest(String uid, String message) {// 這里,我們messages,添加了一條“你會MySQL數據庫嗎?",來達到一種對話具有上下文的效果List<Message> messages = msgList.computeIfAbsent(uid, k -> new ArrayList<>());messages.add(new Message("user", message));/*[{"system", "你好, 我是DeepSeek-AI助手!"},	{"user", "你會MySQL數據庫嗎?"}]*/AccessRequest request = new AccessRequest();request.setMessages(messages);request.setModel(DEEPSEEK_CHAT);request.setResponse_format(Map.of("type", "text"));request.setStream(true); // 設置為truerequest.setTemperature(1.0);request.setTop_p(1.0);return request;}@PostConstructpublic void init() {List<Message> m = new ArrayList<Message>();m.add(new Message("system", "你好, 我是DeepSeek-AI助手!"));// 初始化消息列表msgList.put("123456", m);}
}// 請求體,參考deepseek官網
public class AccessRequest {private List<Message> messages;private String model; // 默認模型為deepseek-chatprivate Double frequency_penalty = 0.0;private Integer max_tokens;private Double presence_penalty = 0.0;//{//    "type": "text"//}private Map<String, String> response_format;private Object stop = null; // nullprivate Boolean stream; //如果設置為 True,將會以 SSE(server-sent events)的形式以流式發送消息增量。消息流以 data: [DONE] 結尾。private Object stream_options = null;private Double temperature; // 1private Double top_p; // 1private Object tools; // nullprivate String tool_choice = "none";private Boolean logprobs = false;private Integer top_logprobs = null;// get set
}

監聽器

@Slf4j
public class SseChatListener extends EventSourceListener {private SseEmitter sseEmitter;public SseChatListener( SseEmitter sseEmitter) {this.sseEmitter = sseEmitter;}/*** 事件*/@Overridepublic void onEvent(EventSource eventSource, String id, String type, String data) {//log.info("sse數據:{}", data);DeepSeekResponse deepSeekResponse = JSON.parseObject(data, DeepSeekResponse.class);DeepSeekResponse.Choice[] choices = deepSeekResponse.getChoices();try {// 發送給前端【客戶端】sseEmitter.send(SseEmitter.event().data(choices[0]));} catch (IOException e) {log.error("數據發送異常");throw new RuntimeException(e);}}/*** 建立sse連接*/@Overridepublic void onOpen(final EventSource eventSource, final Response response) {log.info("建立sse連接... {}");}/*** sse關閉*/@Overridepublic void onClosed(final EventSource eventSource) {log.info("sse連接關閉:{}");}/*** 出錯了*/@Overridepublic void onFailure(final EventSource eventSource, final Throwable t, final Response response) {log.error("使用事件源時出現異常......");}
}
// DeepSeekResponse.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DeepSeekResponse {private String id;private String object;private Long created;private String model;private String system_fingerprint;private Choice[] choices;@Data@AllArgsConstructor@NoArgsConstructorpublic static class Choice {private Integer index;private Delta delta;private Object logprobs;private String finish_reason;}@Data@AllArgsConstructor@NoArgsConstructorpublic static class Delta {private String content;}
}

然后我們用apifox測試一下:

在這里插入圖片描述

返回這些信息,然后把ai返回的存起來,具體怎么存,就靠讀者自行發揮了,添加到該對話,使該對話具有上下文。【DeepSeek /chat/completions API 是一個“無狀態” API,即服務端不記錄用戶請求的上下文,用戶在每次請求時,需將之前所有對話歷史拼接好后,傳遞給對話 API。】

[{"system", "你好, 我是DeepSeek-AI助手!"},	{"user", "你會MySQL數據庫嗎?"},{"ststem", "是的,我熟悉........"} // 把ai返回的存起來
]

下一次對話的時候,請求體AccessRequest里面的List<Message> messages就向上面那樣,再往后添加用戶問的消息。

上面的例子還有一些小問題,比如說什么時候斷開連接那些的。

3. SpringAI

Spring AI 是一個專注于 AI 工程的應用框架,其目標是將 Spring 生態的 “POJO 構建塊” 和模塊化設計引入 AI 場景,簡化企業數據與第三方模型的對接和使用。

下面快速接入deepseek

<!--maven的pom.xml-->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.4.3</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.feng.ai</groupId><artifactId>spring-ai-chat</artifactId><version>0.0.1-SNAPSHOT</version><name>spring-ai-chat</name><description>spring-ai-chat</description><properties><java.version>21</java.version><spring-ai.version>1.0.0-M6</spring-ai.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webflux</artifactId></dependency><!--openAI--><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-openai-spring-boot-starter</artifactId></dependency><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>2.0.44</version></dependency></dependencies><repositories><repository><id>spring-snapshots</id><name>Spring Snapshots</name><url>https://repo.spring.io/snapshot</url><releases><enabled>false</enabled></releases><snapshots><enabled>true</enabled></snapshots></repository><repository><name>Central Portal Snapshots</name><id>central-portal-snapshots</id><url>https://central.sonatype.com/repository/maven-snapshots/</url><releases><enabled>false</enabled></releases><snapshots><enabled>true</enabled></snapshots></repository></repositories><dependencyManagement><dependencies><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-bom</artifactId><version>${spring-ai.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement>
</project>

然后是配置文件

spring:application:name: spring-ai-chatai:# The DeepSeek API doesn't support embeddings, so we need to disable it.openai:embedding:enabled: falsebase-url: https://api.deepseek.comapi-key: 你的keychat:options:model: deepseek-reasoner # 使用推理模型stream-usage: true

controller

@Slf4j
@RestController
@RequestMapping("/sp/deepseek")
public class SpDeepseekController {@Resource( name = "openAiChatModel")private OpenAiChatModel deepseekModel;// 直接回答 --- stream-usage: false//@GetMapping("/simpleChat")//public R chat() {//    String call = deepseekModel.call("你好, 你會java嗎?");//    return R.success().setData("call", call);//}// 流式回答@PostMapping(value = "/streamChat", produces = "text/event-stream;charset=UTF-8")public Flux<SpMessage> streamChat(@RequestBody Map<String, String> p) {String userMessage = p.get("userMessage");String sessionId = p.get("sessionId");Prompt prompt = new Prompt(new UserMessage(userMessage));StringBuilder modelStr = new StringBuilder();return deepseekModel.stream(prompt).doOnSubscribe(subscription -> log.info("SSE 連接已啟動: {}", sessionId)).doOnComplete(() -> log.info("SSE 連接已關閉: {}", sessionId)).doOnCancel(() -> log.info("SSE 連接已取消: {}", sessionId)).timeout(Duration.ofSeconds(60)) // 超時設置.filter(chatResponse -> chatResponse.getResult().getOutput().getText() != null) // 過濾掉空的響應.map(chatResponse -> {//log.info("SSE 響應: {}", chatResponse.getResult().getOutput());modelStr.append(chatResponse.getResult().getOutput().getText());return SpMessage.builder().role("system").content(chatResponse.getResult().getOutput().getText()).build();});}
}

**TODO:**上面的對話沒有記憶,新的請求來了,ai模型并不會帶上以前的場景,故需要記憶化。 記憶化的同時還要注意如果把該會話歷史中所有的對話全部傳給deepseek的話,可能導致 token 超限,故還需要做一個窗口,避免把太多歷史對話傳過去了。

4. 延伸-Http遠程調用

在不討論微服務架構模式下,我們平時開發難免會碰到需要遠程調用接口的情況,【比如說上面調用deepseek的服務】,那么,我們怎么做才是比較好的方式呢?

一次良好的調用過程,我們應該要考慮這幾點:超時處理、重試機制、異常處理、日志記錄

此外,于性能來說,我們要避免頻繁創建連接帶來的開銷,可以使用連接池管理

① RestTemplate

RestTemplate 是一個同步的 HTTP 客戶端,提供了簡單的方法來發送 HTTP 請求并處理響應。它支持常見的 HTTP 方法(GET、POST、PUT、DELETE 等),并能自動處理 JSON/XML 的序列化和反序列化,這個也是我們非常熟悉的。

下面由于是基于SpringBoot3.4.3,所以httpclient的版本是httpclient5.

@Configuration
public class RestConfig {@Bean("restTemplate")public RestTemplate restTemplate() {// 使用Apache HttpClient連接池(替代默認的 SimpleClientHttpRequestFactory)PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();connectionManager.setMaxTotal(100);      // 最大連接數connectionManager.setDefaultMaxPerRoute(20); // 每個路由的最大連接數CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(connectionManager).evictIdleConnections(TimeValue.of(10, TimeUnit.SECONDS))// 清理空閑連接.build();HttpComponentsClientHttpRequestFactory factory =new HttpComponentsClientHttpRequestFactory(httpClient);factory.setConnectTimeout(3000);  // 連接超時(ms)factory.setReadTimeout(5000);  // 讀取超時(ms)RestTemplate restTemplate = new RestTemplate(factory);// 添加自定義的錯誤處理器restTemplate.setErrorHandler(new CustomErrorHandler());// 添加日志攔截器restTemplate.getInterceptors().add(new LoggingInterceptor());return restTemplate;}
}@Slf4j
public class LoggingInterceptor implements ClientHttpRequestInterceptor {@NotNull@Overridepublic ClientHttpResponse intercept(HttpRequest request, @NotNull byte[] body, ClientHttpRequestExecution execution) throws IOException {log.info("請求地址: {} {}", request.getMethod(), request.getURI());log.info("請求頭: {}", request.getHeaders());log.info("請求體: {}", new String(body, StandardCharsets.UTF_8));ClientHttpResponse response = execution.execute(request, body);log.info("響應狀態碼: {}", response.getStatusCode());return response;}
}@Slf4j
public class CustomErrorHandler implements ResponseErrorHandler {@Overridepublic boolean hasError(@NotNull ClientHttpResponse response) throws IOException {// 獲取 HTTP 狀態碼HttpStatusCode statusCode = response.getStatusCode();return statusCode.isError(); // 判斷狀態碼是否為錯誤狀態碼 【4xx、5xx是true,執行下面的handleError,其他的就false】}@Overridepublic void handleError(@NotNull URI url, @NotNull HttpMethod method, @NotNull ClientHttpResponse response) throws IOException {log.info("請求地址: {}  Method: {}",url, method);HttpStatusCode code = response.getStatusCode();if (code.is4xxClientError()) {log.info("客戶端錯誤:{}", code.value());// xxx} else {log.info("服務器錯誤:{}", code.value());// xxx}}
}

重試降級機制:

@Configuration
@EnableRetry // 開啟重試 -- 需要引入AOP
public class RetryConfig {
}// 在service層調用的時候
@Service
public class OrderService {@Resourceprivate RestTemplate restTemplate;@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2), // 重試間隔 1s, 2s, 4sretryFor = {Exception.class} // 默認重試所有異常//retryFor = {ResourceAccessException.class} // 僅在網絡異常時重試)public String queryOrder(String orderId) {return restTemplate.getForObject("/orders/" + orderId, String.class); // 遠程調用}@Recover // 重試全部失敗后的降級方法public String fallbackQueryOrder(ResourceAccessException e, String orderId) {return "默認訂單";}
}

當然還可以再遠程調用那里try catch起來,有異常的時候,throw出去可以被@Retryable捕獲。

② RestClient

Spring Framework 6.1 引入了全新的同步 HTTP 客戶端 RestClient,它在底層使用了與 RestTemplate 相同的基礎設施(比如消息轉換器和攔截器),但提供了如同 WebClient 一樣的現代、流式(fluent)API,兼顧了簡潔性與可復用性。與傳統的阻塞式 RestTemplate 相比,RestClient 更加直觀易用,同時也保持了對同步調用語境的全量支持

同步調用RestClient 是一個阻塞式客戶端,每次 HTTP 請求都會阻塞調用線程直到響應完成。

流式 API:借鑒 WebClient 的設計風格,所有操作均可鏈式調用,代碼更具可讀性和可維護性。

復用基礎組件:與 RestTemplate 共用 HTTP 請求工廠、消息轉換器、攔截器等組件,便于平滑遷移與統一配置

@Configuration
@Slf4j
public class RestClientConfig {@Bean("serviceARestClient")public RestClient restClientA(@Value("${api-service.a-base-url}") String baseUrl) {// 創建連接池PoolingHttpClientConnectionManager manager = new PoolingHttpClientConnectionManager();manager.setMaxTotal(100);manager.setDefaultMaxPerRoute(20);// 創建HttpClientHttpClient httpClient = HttpClientBuilder.create().setConnectionManager(manager).build();// 創建HttpComponentsClientHttpRequestFactoryHttpComponentsClientHttpRequestFactory factory =new HttpComponentsClientHttpRequestFactory(httpClient);factory.setConnectTimeout(3000);factory.setReadTimeout(5000);return RestClient.builder().baseUrl(baseUrl).defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE).defaultCookie("myCookie", "1234").requestInterceptor(clientRequestInterceptor()).requestFactory(factory) // 連接池與超時.build();}@Bean("serviceBRestClient")public RestClient restClientB(@Value("${api-service.b-base-url}") String baseUrl) {return RestClient.builder().baseUrl(baseUrl).defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE).defaultCookie("myCookie", "1234").requestInterceptor(clientRequestInterceptor()).build();}private ClientHttpRequestInterceptor clientRequestInterceptor() {return (request, body, execution) -> {// 添加統一請求頭(如認證信息)request.getHeaders().add("my-head", "head-gggggg");// 日志記錄log.debug("Request: {} {}", request.getMethod(), request.getURI());request.getHeaders().forEach((name, values) ->values.forEach(value -> log.debug("Header: {}={}", name, value)));ClientHttpResponse response = execution.execute(request, body);log.debug("Response status: {}", response.getStatusCode());return response;};}
}

簡單調用:

@Service
public class AService {@Resource(name = "serviceARestClient")private RestClient restClientA;public String queryA(String a) {return restClientA.get().uri("/api/a?a={a}", a).retrieve().onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {throw new HttpClientErrorException(response.getStatusCode());}).onStatus(HttpStatusCode::is5xxServerError, (request, response) -> {throw new ServerErrorException(response.getStatusCode().toString(), null);}).body(String.class);}// 復雜query參數public String queryA(String a, String b) {return restClientA.get().uri(  uriBuilder ->uriBuilder.path("/api/bbb").queryParam("a", 25).queryParam("b", "30").build()).retrieve().onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {throw new HttpClientErrorException(response.getStatusCode());}).onStatus(HttpStatusCode::is5xxServerError, (request, response) -> {throw new ServerErrorException(response.getStatusCode().toString(), null);}).body(String.class);}// postpublic String postA(String a) {HashMap<String, Object> map = new HashMap<>();map.put("a", a); map.put("page", 1); map.put("size", 10);return restClientA.post().uri("/api/post").body(map).retrieve().onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {throw new HttpClientErrorException(response.getStatusCode());}).onStatus(HttpStatusCode::is5xxServerError, (request, response) -> {throw new ServerErrorException(response.getStatusCode().toString(), null);}).body(String.class);}}

③ 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),減少內存占用。

在這里插入圖片描述

這里就不介紹了。

特性RestTemplateRestClientWebClient
模型阻塞,同步阻塞,同步,流式 API非阻塞,響應式【學習曲線較為陡峭】
API 風格模板方法 (getForObject, exchange 等)鏈式流式 (get().uri()...retrieve())鏈式流式,支持 Mono/Flux
可擴展性依賴大量重載方法可配置攔截器、初始器,支持自定義消息轉換器強大的過濾器、攔截器與背壓支持
性能受限于線程池RestTemplate,但更簡潔更佳,適合高并發場景
遷移成本較低,可自然承接現有 RestTemplate 配置較高,需要重構為響應式編程

end. 參考

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

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/906079.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/906079.shtml
英文地址,請注明出處:http://en.pswp.cn/news/906079.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

vscode中Debug c++

在vscode中Debug ros c程序 1 在Debug模式下編譯 如果用命令行catkin_make&#xff0c;在輸入catkin_make時加上一個參數&#xff1a; catkin_make -DCMAKE_BUILD_TYPEDebug 或者直接修改CMakelist.txt&#xff0c;添加以下代碼&#xff1a; SET(CMAKE_BUILD_TYPE "D…

【ROS2】 核心概念6——通信接口語法(Interfaces)

古月21講/2.6_通信接口 官方文檔&#xff1a;Interfaces — ROS 2 Documentation: Humble documentation 官方接口代碼實戰&#xff1a;https://docs.ros.org/en/humble/Tutorials/Beginner-Client-Libraries/Single-Package-Define-And-Use-Interface.html ROS 2使用簡化的描…

C#里與嵌入式系統W5500網絡通訊(2)

在嵌入式代碼里,需要從嵌入式的MCU訪問W5500芯片。 這個是通過SPI通訊來實現的,所以要先連接SPI的硬件通訊線路。 接著下來,就是怎么樣訪問這個芯片了。 要訪問這個芯片,需要通過SPI來發送數據,而發送數據又要有一定的約定格式, 于是芯片廠商就定義下面的通訊格式: …

SuperYOLO:多模態遙感圖像中的超分辨率輔助目標檢測之論文閱讀

摘要 在遙感影像&#xff08;RSI&#xff09;中&#xff0c;準確且及時地檢測包含數十像素的多尺度小目標仍具有挑戰性。現有大多數方法主要通過設計復雜的深度神經網絡來學習目標與背景的區分特征&#xff0c;常導致計算量過大。本文提出一種兼顧檢測精度與計算代價的快速準確…

計算機軟件的基本組成

計算機軟件的基本組成 一, 計算機軟件的分類 軟件按其功能分類, 可分為系統軟件和應用軟件 圖解 (1)系統軟件 系統軟件是一組保證計算機系統高效, 正確運行的基礎軟件, 軟件通常作為系統資源提供給用戶使用. 系統軟件主要有操作系統(OS), 數據庫管理系統(DBMS), 語言處理程…

unity開發游戲實現角色篩選預覽

RenderTexture通俗解釋 RenderTexture就像是Unity中的"虛擬相機膠片"&#xff0c;它可以&#xff1a; 捕獲3D內容&#xff1a;將3D場景或對象"拍照"記錄下來 實時更新&#xff1a;不是靜態圖片&#xff0c;而是動態視頻&#xff0c;角色可以動起來 用作…

Spring源碼主線全鏈路拆解:從啟動到關閉的完整生命周期

Spring源碼主線全鏈路拆解&#xff1a;從啟動到關閉的完整生命周期 一文看懂 Spring 框架從啟動到銷毀的主線流程&#xff0c;結合原理、源碼路徑與偽代碼三位一體&#xff0c;系統學習 Spring 底層機制。 1. 啟動入口與環境準備 原理說明 Spring Boot 應用入口是標準 Java 應…

SAP RF 移動屏幕定制

SAP RF 移動屏幕定制 ITSmobile 是 SAP 當前將移動設備連接到 SAP 系統的技術基礎。它基于 SAP Internet Transaction Server (ITS)&#xff0c;從 Netweaver 2004 開始作為 Netweaver 平臺的一部分提供。ITSmobile 提供了一個框架&#xff0c;用于為任何 SAP 事務生成基于 HT…

Spark,數據提取和保存

以下是使用 Spark 進行數據提取&#xff08;讀取&#xff09;和保存&#xff08;寫入&#xff09;的常見場景及代碼示例&#xff08;基于 Scala/Java/Python&#xff0c;不含圖片操作&#xff09;&#xff1a; 一、數據提取&#xff08;讀取&#xff09; 1. 讀取文件數據&a…

如何用mockito+junit測試代碼

Mockito 是一個流行的 Java 模擬測試框架&#xff0c;用于創建和管理測試中的模擬對象(mock objects)。它可以幫助開發者編寫干凈、可維護的單元測試&#xff0c;特別是在需要隔離被測組件與其他依賴項時。 目錄 核心概念 1. 模擬對象(Mock Objects) 2. 打樁(Stubbing) 3. 驗…

最新缺陷檢測模型:EPSC-YOLO(YOLOV9改進)

目錄 引言:工業缺陷檢測的挑戰與突破 一、EPSC-YOLO整體架構解析 二、核心模塊技術解析 1. EMA多尺度注意力模塊:讓模型"看得更全面" 2. PyConv金字塔卷積:多尺度特征提取利器 3. CISBA模塊:通道-空間注意力再進化 4. Soft-NMS:更智能的重疊框處理 三、實…

【Linux網絡與網絡編程】12.NAT技術內網穿透代理服務

1. NAT技術 之前我們說到過 IPv4 協議中IP 地址數量不充足的問題可以使用 NAT 技術來解決。還提到過本地主機向公網中的一個服務器發起了一個網絡請求&#xff0c;服務器是怎么將應答返回到該本地主機呢&#xff1f;&#xff08;如何進行內網轉發&#xff1f;&#xff09; 這就…

uniapp的適配方式

文章目錄 前言? 一、核心適配方式對比&#x1f4cf; 二、rpx 單位&#xff1a;uni-app 的核心適配機制&#x1f9f1; 三、默認設計稿適配&#xff08;750寬&#xff09;&#x1f501; 四、字體 & 屏幕密度適配&#x1f6e0; 五、特殊平臺適配&#xff08;底部安全區、劉海…

JAVA EE(進階)_進階的開端

別放棄浸透淚水的昨天&#xff0c;晨光已為明天掀開新篇 ——陳長生. ?主頁&#xff1a;陳長生.-CSDN博客? &#x1f4d5;上一篇&#xff1a;JAVA EE_HTTP-CSDN博客 1.什么是Java EE Java EE&#xff08;Java Pla…

SQL腳本規范

主要作用&#xff1a;數據庫的備份和遷移 SQL腳本規范 每一個sql語句必須與;結束 腳本結構&#xff1a; { 刪庫&#xff0c;建庫 刪表&#xff0c;建表 插入初始數據 } 建庫語法&#xff1a; CREATE DATABASE 數據庫名CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CHARA…

std::ratio<1,1000> 是什么意思?

author: hjjdebug date: 2025年 05月 14日 星期三 09:45:24 CST description: std::ratio<1,1000> 是什么意思&#xff1f; 文章目錄 1. 它是一種數值嗎&#xff1f;2. 它是一種類型嗎&#xff1f;3. std:ratio 是什么呢&#xff1f;4. 分析一個展開后的模板函數5.小結: …

測試--測試分類 (白盒 黑盒 單元 集成)

一、按照測試目標分類&#xff08;測試目的是什么&#xff09; 主類別細分說明1. 界面測試UI內容完整性、一致性、準確性、友好性&#xff0c;布局排版合理性&#xff0c;控件可用性等2. 功能測試檢查軟件功能是否符合需求說明書&#xff0c;常用黑盒方法&#xff1a;邊界值、…

整理了 2009 - 2025 年的【199 管綜真題 + 解析】PDF,全套共 34 份文件

每年真題原卷 ? 每年詳細解析 ? &#x1f4c2;【管綜真題 2009-2025】 &#x1f4c2;【管綜解析 2009-2025】 目錄樹&#xff1a; ├── 2009-2025管綜真題 PDF │ ├── 2009年199管綜真題.pdf │ ├── 2010年199管綜真題.pdf │ ├── 2011年199管綜真題.pd…

用golang實現二叉搜索樹(BST)

目錄 一、概念、性質二、二叉搜索樹的實現1. 結構2. 查找3. 插入4. 刪除5. 中序遍歷 中序前驅/后繼結點 一、概念、性質 二叉搜索樹&#xff08;Binary Search Tree&#xff09;&#xff0c;簡寫BST&#xff0c;又稱為二叉查找樹 它滿足&#xff1a; 空樹是一顆二叉搜索樹對…

自動化:批量文件重命名

自動化&#xff1a;批量文件重命名 1、前言 2、效果圖 3、源碼 一、前言 今天來分享一款好玩的自動化腳&#xff1a;批量文件重命名 有時候呢&#xff0c;你的文件被下載下來文件名都是亂七八糟毫無規律&#xff0c;但是當時你下載的時候沒辦法重名或者你又不想另存為重新重…