当前位置: 首页 > news >正文

实战 - 利用 ThreadLocal 线程局部变量实现数据缓存

文章目录

    • 1. 利用 ThreadLocal 缓存 AssetBranchCache 数据
      • 1. 定义 AssetBranchCache 类
      • 2. 定义 BranchContext 类操作 AssetBranchCache 对象
      • 3. 配置拦截器实时更新和清除缓存数据
      • 4. 定义 SaasThreadContextDataHolderBranch 类持有 AssetBranchCache 对象
      • 5. 定义 SaasThreadContextHolder 接口
      • 6. 定义 SaasThreadContextHolderBranch 组件
      • 7. 定义 SaasThreadContextUtil 工具类
      • 8. 定义 BranchNameCache 组件获取 AssetBranchCache 数据
    • 2. 业务使用
    • 1. 请求入口 IncidentController
      • 1. 请求参数的封装
      • 2. 响应实体的封装
    • 2. 获取延迟加载数据 IEventDelayLoadService

业务逻辑:安全事件,安全告警,风险主机列表页面会需要一些延迟加载数据,因此当我们进入这个页面时就会请求该接口获取延迟加载数据。

1. 利用 ThreadLocal 缓存 AssetBranchCache 数据

ThreadLocal 是 Java 中的一个类,它提供了一种线程局部变量的机制。线程局部变量是指只能被同一个线程访问和修改的变量,不同线程之间互不干扰。ThreadLocal 可以用来解决多线程并发访问共享变量的问题。

ThreadLocal可以用来实现数据缓存,即在一个线程中缓存一些上下文相关的数据,以便在该线程的后续操作中使用。在使用ThreadLocal实现上下文缓存时,可以将需要缓存的数据存储在ThreadLocal对象中,然后在需要使用这些数据的地方,通过ThreadLocal对象获取数据。由于每个线程都有自己的ThreadLocal对象,因此不同线程之间的数据不会相互干扰,从而保证了线程安全性。

1. 定义 AssetBranchCache 类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class AssetBranchCache {private UnmodifiableMap<Integer, String> idToNameMap;
}

2. 定义 BranchContext 类操作 AssetBranchCache 对象

BranchContext 类定义了一个静态的 ThreadLocal 变量 BRANCH_CACHE_THREAD_LOCAL,用于存储当前线程的 AssetBranchCache 对象:

public class BranchContext {private static final ThreadLocal<@Nullable AssetBranchCache> BRANCH_CACHE_THREAD_LOCAL = new TransmittableThreadLocal<>();/*** 设置 assetBranchCache*/public static void load(@Nullable AssetBranchCache assetBranchCache) {BRANCH_CACHE_THREAD_LOCAL.set(assetBranchCache);}/*** 获取 assetBranchCache*/@Nullablepublic static AssetBranchCache save() {return BRANCH_CACHE_THREAD_LOCAL.get();}/*** 清除 assetBranchCache 信息*/public static void remove() {BRANCH_CACHE_THREAD_LOCAL.remove();}
}

3. 配置拦截器实时更新和清除缓存数据

在使用ThreadLocal时需要注意内存泄漏问题,因为ThreadLocal对象是存储在每个线程的ThreadLocalMap中的,如果不及时清除ThreadLocal对象,可能会导致内存泄漏。

@Data
@Configuration
@CustomLog
public class IncidentInterceptorConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors( @NotNull InterceptorRegistry registry) {registry.addInterceptor(new HandlerInterceptorAdapter() {// preHandle方法:在请求处理前会执行@Overridepublic boolean preHandle( @NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler) throws Exception {BranchContext.remove();return true;}// afterCompletion方法:在请求处理后会执行@Overridepublic void afterCompletion(@NotNull HttpServletRequest request,@NotNull HttpServletResponse response, @NotNull Object handler,@Nullable Exception ex) throws Exception {BranchContext.remove();}});}
}

该类实现WebMvcConfigurer接口,用于配置拦截器。在addInterceptors方法中,注册了一个HandlerInterceptorAdapter类型的拦截器,该拦截器在请求处理前和请求处理后都会执行。在preHandle方法中,调用了BranchContext类的remove方法,用于清除ThreadLocal 缓存的数据。在afterCompletion方法中也调用了BranchContext类的remove方法,确保在请求处理完成后也清除当前线程的ThreadLocal 缓存的数据。

使用拦截器可以保证ThreadLocal中缓存的数据是实时更新的,每次请求进来都会清除缓存中的数据,重新加载。

4. 定义 SaasThreadContextDataHolderBranch 类持有 AssetBranchCache 对象

为了在项目的各个地方获取ThreadLocal中缓存的资产组数据,可以定义一个线程上下文资产组数据持有者类 SaasThreadContextDataHolderBranch

public interface SaasThreadContextDataHolder {}
@Data
@AllArgsConstructor
public class SaasThreadContextDataHolderBranch implements SaasThreadContextDataHolder {@Nullableprivate final AssetBranchCache assetBranchCache;
}

5. 定义 SaasThreadContextHolder 接口

SaasThreadContextHolder 接口中定义了一些方法来管理 SaasThreadContextDataHolder 对象

public interface SaasThreadContextHolder<T extends SaasThreadContextDataHolder> {// 获取 SaasThreadContextDataHolder 类@NotNullClass<T> getSaasThreadContextDataHolderClass();// 尝试加载SaasThreadContextDataHolderdefault boolean tryLoad(@NotNull SaasThreadContextDataHolder holder) {if (!getSaasThreadContextDataHolderClass().isInstance(holder)) {return false;}this.load((T) holder);return true;}// 加载 SaasThreadContextDataHoldervoid load(@NotNull T holder);// 存储 SaasThreadContextDataHolder@NotNullT save();// 清理 SaasThreadContextDataHoldervoid remove();
}

6. 定义 SaasThreadContextHolderBranch 组件

// 应用程序中自动发现和注册该组件
@AutoService(SaasThreadContextHolder.class)
public class SaasThreadContextHolderBranch implements SaasThreadContextHolder<SaasThreadContextDataHolderBranch> {@NotNull@Overridepublic Class<SaasThreadContextDataHolderBranch> getSaasThreadContextDataHolderClass() {return SaasThreadContextDataHolderBranch.class;}// 加载 SaasThreadContextDataHolderBranch 设置 ThreadLocal 缓存数据@Overridepublic void load(@NotNull SaasThreadContextDataHolderBranch holder) {AssetBranchCache assetBranchCache = holder.getAssetBranchCache();if (assetBranchCache != null) {BranchContext.load(assetBranchCache);}}// 获取 ThreadLocal 缓存数据填充 SaasThreadContextDataHolderBranch @NotNull@Overridepublic SaasThreadContextDataHolderBranch save() {return new SaasThreadContextDataHolderBranch(BranchContext.save());}// 清除 ThreadLocal 缓存数据@Overridepublic void remove() {BranchContext.remove();}
}

7. 定义 SaasThreadContextUtil 工具类

public class SaasThreadContextUtil {// 获取Spring容器中所有实现了SaasThreadContextHolder接口的组件@NotNullstatic List<SaasThreadContextHolder<?>> getSaasThreadContextHolders() {return (List) IterableUtils.toList(ServiceLoader.load(SaasThreadContextHolder.class));}@NotNullpublic static List<SaasThreadContextDataHolder> save() {List<SaasThreadContextHolder<?>> saasThreadContextHolders = getSaasThreadContextHolders();List<SaasThreadContextDataHolder> saasThreadContextDataHolders = new ArrayList<>(saasThreadContextHolders.size());for (SaasThreadContextHolder<?> saasThreadContextHolder : saasThreadContextHolders) {saasThreadContextDataHolders.add(saasThreadContextHolder.save());}return saasThreadContextDataHolders;}public static void load(@NotNull List<SaasThreadContextDataHolder> saasThreadContextDataHolders) {for (SaasThreadContextHolder<?> saasThreadContextHolder : getSaasThreadContextHolders()) {for (SaasThreadContextDataHolder saasThreadContextDataHolder : saasThreadContextDataHolders) {if (saasThreadContextHolder.tryLoad(saasThreadContextDataHolder)) {break;}}}}public static void remove() {for (SaasThreadContextHolder<?> saasThreadContextHolder : getSaasThreadContextHolders()) {saasThreadContextHolder.remove();}}
}

8. 定义 BranchNameCache 组件获取 AssetBranchCache 数据

@Component
@CustomLog
public class BranchNameCache {@Getter@Setter(onMethod_ = @Autowired)private IBranchService branchService;@Setter(onMethod_ = @Autowired)private ApplicationContext applicationContext;// 查询缓存数据 AssetBranchCachepublic AssetBranchCache getAssetBranchCache() {// 从ThreadLocal对象中获取缓存数据,如果不为null,直接返回AssetBranchCache result = BranchContext.save();if (result != null) {return result;}// 查询缓存数据BranchNameCache bean = applicationContext.getBean(BranchNameCache.class);String cacheKey = String.format(RedisKey.ASSET_BRANCH, Objects.requireNonNull(TenantInfoContext.getTenantInfo()).getTenantId())+ CacheUtil.getCacheUserRegion();result = bean.getAssetBranchInfo(cacheKey);// 将缓存数据设置到ThreadLocal对象中BranchContext.load(result);return result;}// Spring Cache 注解@Cacheable(value = "assetBranchCache", key = "#cacheKey")public AssetBranchCache getAssetBranchInfo(String cacheKey) {List<Branch> branches = this.getBranchService().listAll();Map<Integer, String> idToNameMap = new HashMap<>(branches.size());branches.forEach(branch -> {idToNameMap.put(branch.getId().intValue(), branch.getName());});return new AssetBranchCache( (UnmodifiableMap<Integer, String>) UnmodifiableMap.unmodifiableMap(idToNameMap));}public Map<Integer, String> getBranchIdToNameMap() {return this.getAssetBranchCache().getIdToNameMap();}
}

① getAssetBranchCache方法:获取 AssetBranchCache数据,先从ThreadLocal对象中查询,如果查询结果不为null,直接返回,否则调用资产服务查询,最后将查询结果设置到ThreadLocal对象中。

② @Cacheable注解:Spring框架的注解,用于缓存方法的返回值。具体来说,@Cacheable注解表示该方法的返回值应该被缓存,value属性指定了缓存的名称,key属性指定了缓存的键值,即用于查找缓存的唯一标识符。在这个例子中,缓存的名称为"assetBranchCache",缓存的键值为cacheKeycacheKey是一个方法参数或者是一个表达式,用于生成缓存的键值。如果缓存中已经存在相同的键值,则直接返回缓存中的值,否则执行方法并将返回值存入缓存中。

2. 业务使用

1. 请求入口 IncidentController

@Api("安全事件信息")
@CustomLog
@Validated
@ResponseResult
@RestController
@RequestMapping("/api/v1/incidents")
public class IncidentController {@CheckValidateAble@PreAuthorize("hasAnyAuthority('superAdmin','incidentQuery')")@PostMapping("/delayloaddata")@ApiOperation("安全事件额外数据延迟加载")@OperateLog(handle = { LoggerEnum.operation }, target = "operate.incident.log", action = "operate.incident.delayloaddata.log")public IncidentDelayLoadData delayLoadData(@RequestBody @Validated IncidentDelayLoadDataQo qo) {Map<String, IncidentDelayLoadData.EachIncidentDelayLoadData> incidentMap = delayLoadService.richInfo(qo);return IncidentDelayLoadData.builder().incidentMap(incidentMap).build();}
}

1. 请求参数的封装

@Data
@Validated
@Builder
@ApiModel(description = "事件延迟加载数据")
@NoArgsConstructor
@AllArgsConstructor
public class IncidentDelayLoadDataQo {@ApiModelProperty(value = "数据类型", example = "INCIDENT")RiskTypeEnum riskType = RiskTypeEnum.INCIDENT;@Valid@Size(max = 1000, message = "延迟加载的数据最大1000条")@NotNullprivate List<SecurityEntry> data;
}

① 风险类型:安全事件,安全告警,风险资产

public enum RiskTypeEnum {/*** 安全事件*/INCIDENT,/*** 安全告警*/ALERT,MOCK,/*** 风险资产*/RISK_ASSET;
}

② 事件延迟加载数据:

@Data
@Builder
@ApiModel(description = "事件延迟加载数据")
@NoArgsConstructor
@AllArgsConstructor
public class SecurityEntry {// 安全事件id、安全告警id、风险主机id@NotBlank@Pattern(regexp = "[^\"'&<>()+%\\\\]+")private String uuId;@Nullableprivate Long assetId;@Nullableprivate Long lastTime;@Nullableprivate List<@NotBlank @Pattern(regexp = "[^\"'&<>()+%\\\\]+") String> alertIds;}

@Nullable 注解是一种用于 Java 代码中的注解,它用于标记一个方法的返回值、参数或字段可以为 null。

③ 请求参数示例:

{"data": [{"uuId": "alert-ad41da97-94b2-4091-9a6d-a6972a7ce919","assetId": 17582,"lastTime": 1690460941},{"uuId": "alert-a6a2d31f-34df-4176-bca5-1ee368e41006","assetId": 17581,"lastTime": 1690460896}],"riskType": "ALERT"
}

2. 响应实体的封装

@Data
@Builder
@ApiModel(description = "事件延迟加载数据")
@NoArgsConstructor
@AllArgsConstructor
public class IncidentDelayLoadData {@ApiModelProperty("事件延迟加载数据")private Map<String, EachIncidentDelayLoadData> incidentMap;@Data@Builder@ApiModel(description = "每条事件的延迟加载数据")@NoArgsConstructor@AllArgsConstructorpublic static class EachIncidentDelayLoadData {@ApiModelProperty("一键遏制禁用标记")private Boolean oneClickDisposeDisabled;private String oneClickDisposeStatus;private String newestUsername;private String checkOutUsername;private String responsible;private AlertSeverityNumber alertSeverityNumber;private Long remarkNumber;private Integer connectStatus;@ApiModelProperty("当前资产类别信息")private MagnitudeInfo magnitude;@ApiModelProperty("处置入口威胁根除判断")private TableCellDataVo entryDisposal;@ApiModelProperty("主机IP")private HostIpDelayVo hostIp;@ApiModelProperty("主机资产组")private HostBranchDelayVo hostBranchId;@ApiModelProperty("主机业务组")private HostGroupDelayVo hostGroupIds;@ApiModelProperty("目的IP")private SrcDstIpDelayVo dstIp;@ApiModelProperty("源IP")private SrcDstIpDelayVo srcIp;@ApiModelProperty("源IP")private AssetUserDelayVo assetUser;}
}

① 外部类 IncidentDelayLoadData:包含一个名为incidentMapMap对象,其中键为事件ID,值为EachIncidentDelayLoadData对象,表示每个事件的延迟加载数据。

@Data
@Builder
@ApiModel(description = "事件延迟加载数据")
@NoArgsConstructor
@AllArgsConstructor
public class IncidentDelayLoadData {@ApiModelProperty("事件延迟加载数据")private Map<String, EachIncidentDelayLoadData> incidentMap;// ....
}

② 静态内部类 EachIncidentDelayLoadData:包含了事件的各种属性,如一键遏制禁用标记、最新用户名、检出用户名、责任人、告警严重性等等。其中,MagnitudeInfoTableCellDataVoHostIpDelayVoHostBranchDelayVoHostGroupDelayVoSrcDstIpDelayVoAssetUserDelayVo都是其他自定义类的对象,用于表示不同的属性。

③ 静态内部类的使用:

Java中的静态内部类是指在一个类的内部定义的静态类。静态内部类与非静态内部类的区别在于,静态内部类不依赖于外部类的实例,可以直接通过外部类名访问,而非静态内部类必须依赖于外部类的实例才能访问。

静态内部类可以访问外部类的静态成员和方法,但不能访问外部类的非静态成员和方法。静态内部类也可以定义静态成员和方法,这些静态成员和方法与外部类的静态成员和方法类似,可以直接通过类名访问。

public class OuterClass {// 外部类的成员和方法public static class StaticInnerClass {// 静态内部类的成员和方法}
}

静态内部类的实例化方式如下:

OuterClass.StaticInnerClass inner = new OuterClass.StaticInnerClass();

④ 响应数据示例:

{"strCode": null,"message": "成功","data": {"incidentMap": {"lqlapp35test3-452799db-2557-424b-ba86-aa1f2d72eb3e": {"oneClickDisposeDisabled": true,"oneClickDisposeStatus": "WAIT_DEAL","newestUsername": null,"checkOutUsername": null,"responsible": null,"alertSeverityNumber": null,"remarkNumber": 0,"connectStatus": null,"magnitude": null,"entryDisposal": {"originalValue": true,"renderValue": null},"hostIp": {"originalValue": "6.6.6.7","renderValue": "6.6.6.7(美国)"},"hostBranchId": {"originalValue": 0,"renderValue": ""},"hostGroupIds": {"count": 0,"data": []},"dstIp": null,"srcIp": null,"assetUser": null},"16c28c0d-649a-dispose-entity111-5d419e8790129": {"oneClickDisposeDisabled": false,"oneClickDisposeStatus": "WAIT_DEAL","newestUsername": null,"checkOutUsername": null,"responsible": null,"alertSeverityNumber": null,"remarkNumber": 0,"connectStatus": null,"magnitude": null,"entryDisposal": {"originalValue": true,"renderValue": null},"hostIp": {"originalValue": "192.168.40.29","renderValue": "192.168.40.29(管理IP范围)"},"hostBranchId": {"originalValue": 1,"renderValue": ""},"hostGroupIds": {"count": 0,"data": []},"dstIp": null,"srcIp": null,"assetUser": null}}},"code": 0
}

2. 获取延迟加载数据 IEventDelayLoadService

利用线程池管理并发任务的执行。通过将任务提交到线程池中,让线程池自动分配线程来执行任务,从而实现并发执行。线程池还可以控制并发任务的数量,避免系统资源被过度占用,从而提高系统的稳定性和可靠性。

@CustomLog
@Service
public class EventDelayLoadServiceImpl implements IEventDelayLoadService, ApplicationListener<ContextStoppedEvent> {// 定义一个线程池private static final ThreadPoolExecutor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(5,20,4,TimeUnit.SECONDS,new LinkedBlockingQueue<>(),new ThreadFactoryBuilder().setNameFormat(IncidentResponseDispositionServiceImpl.class.getSimpleName() + "-pool-%d").setDaemon(true).build(),new ThreadPoolExecutor.DiscardOldestPolicy());// 监听应用程序上下文停止事件,用于关闭线程资源@Overridepublic void onApplicationEvent(@NotNull ContextStoppedEvent ignored) {try {THREAD_POOL_EXECUTOR.shutdown();} catch (Exception e) {log.error("停止线程池失败", e);}}@Overridepublic Map<String, IncidentDelayLoadData.EachIncidentDelayLoadData> richInfo(IncidentDelayLoadDataQo delayLoadDataQo) {List<SecurityEntry> securityEntryList = delayLoadDataQo.getData();if (CollectionUtils.isEmpty(securityEntryList)) {return Collections.emptyMap();}RiskTypeEnum riskType = delayLoadDataQo.getRiskType();ConcurrentHashMap<String, IncidentDelayLoadData.EachIncidentDelayLoadData> data = new ConcurrentHashMap<>(securityEntryList.size());securityEntryList.forEach(entry -> data.put(entry.getUuId(), new IncidentDelayLoadData.EachIncidentDelayLoadData()));List<Callable<Void>> callables;// 根据riskType的不同,调用不同的方法获取Callable列表callablesswitch (riskType) {case INCIDENT:callables = getIncidentCallableList(securityEntryList, data);break;       case ALERT:callables = getAlertCallableList(securityEntryList, data);break;       case RISK_ASSET:callables = getRiskAssetCallableList(securityEntryList, data);break;default:return Collections.emptyMap();}// 通过线程池执行callables中的任务,并将结果存储在ConcurrentHashMap类型的数据data中try {List<Future<Void>> futures = THREAD_POOL_EXECUTOR.invokeAll(callables, 1, TimeUnit.MINUTES);for (Future<Void> future : futures) {try {future.get();} catch (Exception e) {log.warn("incident delay data,future get warn", e);}}} catch (Exception e) {log.warn("incident delay data warn ", e);}return data;}
}

以获取安全告警页面延迟加载数据的线程执行任务为例:getAlertCallableList

private List<Callable<Void>> getAlertCallableList(List<SecurityEntry> securityEntryList, ConcurrentHashMap<String, IncidentDelayLoadData.EachIncidentDelayLoadData> data) {HashMap<@NotNull String, @Nullable Long> uuIdToAssetId = new HashMap<>(securityEntryList.size());for (SecurityEntry securityEntry : securityEntryList) {uuIdToAssetId.put(securityEntry.getUuId(), securityEntry.getAssetId());}try {List<Alert> alerts = alertDao.getAlerts(securityEntryList.stream().map(SecurityEntry::getUuId).collect(Collectors.toList()));if (CollectionUtils.isEmpty(alerts)) {log.warn("no alert data to deal");data.clear();return List.of();}// 尝试加载ThreadLocal对象中的缓存数据List<SaasThreadContextDataHolder> contextDataHolders = SaasThreadContextUtil.save();Callable<Void> assetCallable = getAssetCallable(securityEntryList, data, uuIdToAssetId, contextDataHolders);Callable<Void> assetUserCallable = getAssetUserCallable(securityEntryList, data, uuIdToAssetId, contextDataHolders);Callable<Void> assetBranchCallable = getAlertAssetBranchCallable(securityEntryList, alerts, data, contextDataHolders);Callable<Void> remarkNumberCallable = getRemarkNumberCallable(securityEntryList, ALERT, data, contextDataHolders);return Arrays.asList(assetCallable,assetUserCallable,assetBranchCallable,remarkNumberCallable);} catch (IOException | JsonSerializeException e) {throw new IncidentRuntimeException(I18nUtils.i18n(I18nConstant.AlertOperateConstant.EXCEPTION_TO_BE_TRANSFERRED), e);}
}

在该方法中定义了一个获取安全告警资产组信息线程体,继续看下 getAlertAssetBranchCallable 方法:

@CustomLog
@Service
public class EventDelayLoadServiceImpl implements IEventDelayLoadService, ApplicationListener<ContextStoppedEvent> {@Setter(onMethod_ = { @Autowired })private BranchNameCache branchNameCache; private Callable<Void> getAssetBranchCallable(List<SecurityEntry> securityEntryList, List<Incident> incidentList, ConcurrentHashMap<String, IncidentDelayLoadData.EachIncidentDelayLoadData> data,List<SaasThreadContextDataHolder> contextDataHolders) {return () -> {try {// 设置ThreadLocal对象中的缓存数据SaasThreadContextUtil.load(contextDataHolders);// BranchNameCache 获取 AssetBranchCache 数据Map<Integer, String> branchIdToNameMap = branchNameCache.getBranchIdToNameMap();for (SecurityEntry securityEntry : securityEntryList) {//设置响应数据eachIncidentDelayLoadData.setHostBranchId(new HostBranchDelayVo(branchId, HostIpUtils.getHostBranch(assetId, branchId, branchIdToFullNameMap)));}return null;} finally {// 清除ThreadLocal对象中的缓存数据SaasThreadContextUtil.remove();}};}
}
http://www.lryc.cn/news/107761.html

相关文章:

  • wxwidgets Ribbon使用简单实例
  • 2023年第四届“华数杯”数学建模思路 - 案例:最短时间生产计划安排
  • LeetCode404. 左叶子之和
  • Nginx 高性能内存池 ----【学习笔记】
  • iOS--frame和bounds
  • docker logs 使用说明
  • Ceph入门到精通-Ceph PG状态详细介绍(全)
  • 【数据结构】二叉树、二叉搜索树、平衡二叉树、红黑树、B树、B+树
  • 【JVM】(二)深入理解Java类加载机制与双亲委派模型
  • npm i 报错项目启动不了解决方法
  • 【从零开始学习JAVA | 第三十七篇】初识多线程
  • 微信新功能,你都知道吗?
  • Android 中 app freezer 原理详解(二):S 版本
  • Vue3_04_ref 函数和 reactive 函数
  • 05 Ubuntu下安装.deb安装包方式安装vscode,snap安装Jetbrains产品等常用软件
  • 性能测试jmeter连接数据库jdbc(sql server举例)
  • 8.3 C高级 Shell脚本
  • 2023年华数杯A题
  • 【零基础学Rust | 基础系列 | 函数,语句和表达式】函数的定义,使用和特性
  • 加解密算法+压缩工具
  • FeignClient接口的几种方式总结
  • springBoot多数据源使用tdengine(3.0.7.1)+MySQL+mybatisPlus+druid连接池
  • 剑指Offer 05.替换空格
  • ChatGPT的功能与特点
  • Vue2.0基础
  • rust 如何定义[u8]数组?
  • 关于Hive的使用技巧
  • 【C++】BSTree 模拟笔记
  • 5分钟快手入门laravel邮件通知
  • iOS——Block two