Skip to main content

Web服务组件 (Web)

API文档​

https://tech.rydeen.com.cn/apidocs/web/4.6.0/index.html

组件作用​

主要集成了Spring Web MVC框架,主要功能是提供HTTP服务,包括请求参数校验、异常处理、日志记录等。

组件用法​

Server-sent events (SSE) 管理器​

:::success 版本提示

该功能自4.3.0版本开始可用 :::

什么是SSE?​

请参考:https://developer.mozilla.org/zh-CN/docs/Web/API/EventSource

SSE和WebSocket有什么区别?​

通信方式:

  • SSE:单向通信,服务器可以向客户端发送消息,但客户端不能向服务器发送消息。
  • WebSocket:双向通信,客户端和服务器可以相互发送消息。

协议:

  • SSE:基于HTTP协议,使用text/ event-stream MIME类型。
  • WebSocket:独立的协议,使用ws:// 或wss:// 作为URL前缀。

连接保持:

  • SSE:保持HTTP连接,服务器可以持续发送事件。
  • WebSocket:保持TCP连接,支持全双工通信。

浏览器支持:

  • SSE:大多数现代浏览器都支持,但不支持IE。
  • WebSocket:大多数现代浏览器都支持,包括IE10及以上版本。

使用场景:

  • SSE:适用于服务器向客户端推送实时更新,如新闻、股票价格等。
  • WebSocket:适用于需要双向实时通信的场景,如在线聊天、实时游戏等。

实现复杂度:

  • SSE:实现相对简单,基于HTTP协议,容易与现有的HTTP基础设施集成。
  • WebSocket:实现较复杂,需要专门的服务器和客户端支持。

SSE 适用于服务器向客户端推送实时更新的场景,但不适用于需要双向实时通信的场景。并且,SSE 与 WebSocket 之间并不是竞争关系,而是互补关系,可以根据具体的业务需求选择合适的通信方式。

当SSE所在的服务需要多实例部署的时候,SseEmitter无法序列化仅能在单实例部署的情况下使用,如果需要多实例部署,需要使用Redis Stream/MQ等消息队列来广播当前实例收到的ClientID,然后由该服务的全部实例来消费该消息,由当前ClientID所在的服务实例去发送消息,其它不存在该的服务实例则直接不处理该消息即可,这样就可以实现多实例部署的情况下的SSE推送。

SSE管理器管理器如何使用?​

本组件核心实现类是:

note

cn.com.rydeen.component.web.sse.SseManager

该类是一个单例类,使用它仅需2步:

  1. 客户端注册
  2. 发送消息

SSE管理器API清单如下: https://tech.rydeen.com.cn/apidocs/http/4.6.0/cn/com/rydeen/component/web/sse/SseManager.html

我们通常需要使用的只有2个API,分别是:

SseManager.getInstance().createEmitter(id); SseManager.getInstance().send(id, content);

具体使用案例:

package cn.com.rydeen.component.template.sse.controller;

import cn.com.rydeen.base.common.api.vo.ResultVO;
import cn.com.rydeen.component.web.sse.SseManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;

/**
* SSE 服务控制器
*
* @author richie696
* @version 1.0
* @since 2024-08-12 16:12:14
*/
@Slf4j
@RestController
@RequestMapping("/sse")
public class SseController {

public record SseTextMessage(String id, Object content) {
}

/**
* 创建 SSE 事件流
* @param id 客户端标识
* @return 返回事件流对象
* @throws IOException 事件流创建失败,则抛出该异常
*/
@GetMapping(value = "/stream/{id}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter streamMessage(@PathVariable String id) throws IOException {
return SseManager.getInstance().createEmitter(id);
}

/**
* 发送消息测试接口(该接口正常是在业务代码中或业务代码中调用,此处仅为演示)
* @param message 发送的消息提
* @return 返回发送结果
*/
@PostMapping("/send")
public ResultVO<?> sendMessage(@RequestBody SseTextMessage message) {
boolean result = SseManager.getInstance().send(message.id(), message.content());
return result ? ResultVO.getSuccess() : ResultVO.getError("The client is not connected.");
}
}

至此,服务端的使用基本结束。客户端的实现下面使用的Angular作为示例,其它语言请自行参考写法进行实现:

import {ChangeDetectorRef, Component} from '@angular/core';
import {RouterOutlet} from '@angular/router';
import {FormsModule} from "@angular/forms";
import {v4} from "uuid";

@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, FormsModule],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent {
/**
* 客户端ID的存储键
* @private
*/
private readonly CLIENT_ID_KEY = 'sse_clientId';
connectMessage = 'Not connected';
message = 'None';
title = 'rydeen-component-frontend';
inputId: string = '';
eventSource: EventSource | null = null;

constructor(private cdr: ChangeDetectorRef) {
// 获取当前设备的客户端ID
let clientId = localStorage.getItem(this.CLIENT_ID_KEY);
// 如果不存在则生成一个新的并本地保存
if (clientId == undefined) {
clientId = v4();
localStorage.setItem(this.CLIENT_ID_KEY, clientId);
}
this.inputId = clientId;
}

/**
* 建立连接
*/
connect() {
if (this.eventSource != null && this.eventSource.readyState == EventSource.OPEN) {
return;
}
// 建立链接
this.eventSource = new EventSource(`http://localhost:8881/sse/stream/${this.inputId}`);

// 绑定各种事件
this.eventSource.onopen = event => {
console.log('Connection is opened.', event);
this.connectMessage = 'SSE 连接成功';
this.cdr.detectChanges();
}

this.eventSource.onmessage = event => {
console.info('Message received: ' + event.data, event);
this.message = event.data;
this.cdr.detectChanges();
}

this.eventSource.onerror = event => {
console.error('Error occurred:', event);
this.connectMessage = 'SSE 连接已关闭';
// 当连接关闭时,则自动重新连接
if (this.eventSource!.readyState == EventSource.CLOSED) {
console.error('Connection was closed.');
setTimeout(() => this.connect(), 3000);
}
}
}

/**
* 断开连接
*/
disconnect() {
if (this.eventSource != null) {
this.eventSource.close();
this.connectMessage = 'SSE 连接已关闭';
this.message = '';
}
}

/**
* 生成客户端ID
*/
generateClientId() {
this.disconnect();
let clientId = v4();
localStorage.setItem(this.CLIENT_ID_KEY, clientId);
this.inputId = clientId;
}
}

页面代码片段

<body>
<!-- 其他代码 -->
<h2>Server-sent events 演示</h2>
<div>
<input

type="text"
readonly="readonly"
placeholder="请生成客户端ID"
[(ngModel)]="inputId"/>
</div>
<div>
<button

(click)="generateClientId()"
>生成ClientID</button>
<button

(click)="connect()"
>连接</button>
<button

(click)="disconnect()"
>断开连接</button>
</div>
<div>
SSE连接状态:{{ connectMessage }}
</div>
<div>
您接收到的消息:{{ message }}
</div>
</div>
<!-- 其他代码 -->
</body>

执行成功后的效果

image.png

统一消息体类型转换处理​

该函数用于配置消息转换器(HttpMessageConverter)的列表,以定制JSON和XML的序列化和反序列化过程。主要功能包括:

  1. 创建JavaTimeModule、SimpleModule、GeoModule和JacksonXmlModule等模块,用于注册自定义的序列化器和反序列化器。
  2. 在SimpleModule中添加自定义的序列化器和反序列化器,包括将Long和BigInteger类型转换为字符串,以及配置日期类型的序列化和反序列化格式。
  3. 使用parallelStream()和filter()方法筛选出MappingJackson2HttpMessageConverter类型的转换器。
  4. 遍历筛选后的转换器列表,将每个MappingJackson2HttpMessageConverter中的objectMapper替换为自定义的策略。
  5. 配置自定义的objectMapper,包括设置时区和注册之前创建的模块。

平台全局异常切面处理器​

自定义了三个http header

  • X-RD-API-Response-Status:异常状态编码
  • X-RD-API-Response-Message:异常消息
  • X-RD-API-Response-Code:异常消息编码(国际化使用)

主要处理这三种异常情况:

  • PlatformRuntimeException异常:这是一种自定义的运行时异常,处理器会记录异常信息,并设置响应头X-RD-API-Response-Status、X-RD-API-Response-Message和X-RD-API-Response-Code,然后返回一个NO_CONTENT状态码的响应。
  • org.springframework.web.bind.MissingServletRequestParameterException异常: 这种异常是Spring Web框架在缺少必需的请求参数时抛出的。处理器会记录异常信息,并设置相应的响应头,然后返回一个NO_CONTENT状态码的响应。
  • 其他未捕获的异常:对于其他类型的异常,处理器会记录异常信息,并设置响应头为内部服务器错误相关信息,然后返回一个INTERNAL_SERVER_ERROR状态码的响应。

Feign客户端请求拦截器​

用于在Feign客户端发送请求之前,将当前请求的Header中的参数传递到Feign请求中。所以使用feign调用时,可以自动将当前请求的Header中的参数传递到Feign请求中。 若获取到了HttpServletRequest对象,则遍历请求头中的参数,过滤掉不需要传递的参数(如"content-length","content-type"),并将其他参数添加到Feign请求的Header中。

本地化和时区解析​

  1. 定义了SessionLocaleResolver对象作为本地化解析器,主要涉及到本地化和时区的解析配置,
  2. 创建了一个AcceptLanguageHeaderLocaleResolver对象,并设置了支持的本地化语言列表(中国、美国、日本、印度尼西亚)。
  3. 将headerLocaleResolver的解析结果设置为resolver的默认本地化语言。
  4. 配置时区解析函数,首先从请求头中获取X-RD-Request-APIToken或X-RD-Request-tenantCode来获取租户代码,再根据租户代码和请求头中的X-RD-Request-shopCode拼接成一个键值。
  5. 利用shopTimezoneProvider(如果非空)根据键值获取店铺的时区,如果获取不到则使用请求头中的X_RD_REQUEST_TIMEZONE来获取时区。
  6. 如果上述时区都获取不到,则使用系统的默认时区。
  7. 最后返回配置好的resolver对象作为本地化和时区的解析器。

如果需要自定义映射门店和时区的关系,实现cn.com.rydeen.component.web.i18n.ShopTimezoneProvider类

需要实现该方法,返回门店和时区的映射

public abstract Map<String, String> getShopCodeTimezoneMap();

HTTP/2的支持​

HTTP/2是HTTP协议的第二个主要版本,它的目标是减少页面加载时间,提高性能。HTTP/2的主要特性包括:

  • TCP多路复用:允许客户端和服务器之间的多个请求和响应同时交错传输,提高了网络利用率。
  • 头部压缩:使用HPACK算法对HTTP头部进行压缩,减少了数据传输量。
  • 服务器推送:服务器可以在客户端请求之前主动推送资源,提高了性能。
  • 二进制分帧:HTTP/2将所有传输的信息分割为更小的消息和帧,并采用二进制格式传输,提高了传输效率。
  • 流量优先级:HTTP/2允许客户端指定请求的优先级,服务器可以根据优先级调整资源的传输顺序。
  • 连接复用:HTTP/2允许多个请求和响应共享一个TCP连接,减少了连接建立和关闭的开销。
  • 安全性:HTTP/2要求使用TLS加密,提高了通信的安全性。
  • 兼容性:HTTP/2兼容HTTP/1.1,可以在不修改现有应用程序的情况下升级到HTTP/2。
  • 性能:HTTP/2的性能比HTTP/1.1更好,可以减少页面加载时间,提高用户体验。

目前,框架中已经默认开启HTTP/2的支持,无需额外配置即可使用。

WebMvc参数配置​

/**
* 平台WebMvc属性配置
*
* @author richie696
* @version 1.0
* @since 2022-10-09 15:42:03
*/
@Data
@ConfigurationProperties(prefix = "platform.component.web")
public class WebMvcProperties {

/**
* IETF BCP 47 标准语言标签,例:
* <p>
* zh-CN
* en-US
* ja-JP
* ko-KR
*/
private List<String> supportedLanguageTags;

/**
* 登录地址清单
*/
private Set<String> loginUrls;

/**
* 令牌签发秘钥
*/
private String tokenSecret;

/**
* 令牌有效时长
*/
private long tokenExpirationDate;

/**
* 跨域配置
*/
private CorsConfig cors = new CorsConfig();
}
  • supportedLanguageTags:IETF BCP 47 标准语言标签,用于指定支持的本地化语言列表。
  • loginUrls:开发模式下(不启用网关的时候)系统登录地址清单,用于指定登录的URL地址(生产环境请删除该配置)。
  • tokenSecret:开发模式下(不启用网关的时候)令牌签发秘钥,用于生成JWT令牌(生产环境请删除该配置)。
  • tokenExpirationDate:开发模式下(不启用网关的时候)令牌有效时长,用于生成JWT令牌(生产环境请删除该配置)。
  • cors:开发模式下(不启用网关的时候)跨域配置,用于指定跨域请求的配置信息(生产环境请删除该配置)。想要启用开发模式令牌签发,必须将cors.enable设置为true。