Java 项目实战: 交友项目 - 搜索附近的功能概述和分析

概述

「搜附近」是交友项目里唯一一个强依赖地理位置的功能:它不关心你和谁互有好感,只回答一个问题,此刻谁在你身边。这里是这一功能的全景分析:产品形态长什么样、整体技术方案怎么搭、每个环节有哪些必须盯住的点。单步的原理推演和逐行代码在同章其他篇里展开。

纲要

  • 功能场景
  • 产品形态:按距离圈出附近的人
  • 接口定义与入参出参
  • 整体技术方案
  • 上报 → 存储 → 检索的三段式链路
  • 为什么用 MongoDB 而不是 MySQL
  • 各环节要点速查表
  • 查询点清单(踩坑高发区)

功能场景:按距离圈出附近的人

项目首页顶部的「搜附近」入口点进去,是一个按距离组织的用户卡片列表。产品和别的推荐位不一样的地方在于排序依据:项目卡片按缘分值随机排,这里的每张卡片都对应一个真实地理坐标落在你设定半径内的人。

交互上有两个可控条件:

  • 距离:默认 2000 米,用户可调
  • 性别:可选筛选,只看男生或只看女生

对应的接口定义(YApi 地址 project/19/interface/api/121):

项 内容
请求路径 GET /mf/search
请求参数 gender(可选,性别筛选)、distance(默认 "2000",单位米)
响应数据 List<NearUserVo>,每项含 userId、avatar、nickname

响应 VO 的结构很瘦,只带展示必需的三个字段:

java 复制代码
//附近的人vo对象
public class NearUserVo {

    private Long userId;
    private String avatar;
    private String nickname;

    public static NearUserVo init(UserInfo userInfo) {
        NearUserVo vo = new NearUserVo();
        vo.setUserId(userInfo.getId());
        vo.setAvatar(userInfo.getAvatar());
        vo.setNickname(userInfo.getNickname());
        return vo;
    }
}

搜索结果页大致长这样:

doc 复制代码
┌──────────────────────────────┐
│  ← 搜附近          [筛选]     │
├──────────────────────────────┤
│  距离: 2km ▼    性别: 全部 ▼  │
├──────────────────────────────┤
│ ┌────────┐ ┌────────┐        │
│ │ 头像    │ │ 头像    │       │
│ │ 玫瑰    │ │ 婉儿    │       │
│ │ 800m   │ │ 1.2km  │       │
│ └────────┘ └────────┘        │
│ ┌────────┐ ┌────────┐        │
│ │ 头像    │ │ 头像    │       │
│ │ 阿哲    │ │ 小鹿    │       │
│ │ 1.5km  │ │ 1.8km  │       │
│ └────────┘ └────────┘        │
└──────────────────────────────┘

要点:

  • 卡片按距离从近到远呈现(产品预期),距离值来自地理位置计算
  • 无位置数据的人不出现在结果里,没上报过位置,就检索不到
  • 筛选条件(性别)不走地理位置库,走 UserInfoApi 的二次过滤

整体技术方案:上报、索引、检索三段式

搜附近不是一个接口能独立完成的功能,它建立在一套前置数据之上。整条链路分三段:
#mermaid-svg-dtNuwdb46IycojkS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dtNuwdb46IycojkS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dtNuwdb46IycojkS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dtNuwdb46IycojkS .error-icon{fill:#552222;}#mermaid-svg-dtNuwdb46IycojkS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dtNuwdb46IycojkS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dtNuwdb46IycojkS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dtNuwdb46IycojkS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dtNuwdb46IycojkS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dtNuwdb46IycojkS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dtNuwdb46IycojkS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dtNuwdb46IycojkS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dtNuwdb46IycojkS .marker.cross{stroke:#333333;}#mermaid-svg-dtNuwdb46IycojkS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dtNuwdb46IycojkS p{margin:0;}#mermaid-svg-dtNuwdb46IycojkS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-dtNuwdb46IycojkS .cluster-label text{fill:#333;}#mermaid-svg-dtNuwdb46IycojkS .cluster-label span{color:#333;}#mermaid-svg-dtNuwdb46IycojkS .cluster-label span p{background-color:transparent;}#mermaid-svg-dtNuwdb46IycojkS .label text,#mermaid-svg-dtNuwdb46IycojkS span{fill:#333;color:#333;}#mermaid-svg-dtNuwdb46IycojkS .node rect,#mermaid-svg-dtNuwdb46IycojkS .node circle,#mermaid-svg-dtNuwdb46IycojkS .node ellipse,#mermaid-svg-dtNuwdb46IycojkS .node polygon,#mermaid-svg-dtNuwdb46IycojkS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dtNuwdb46IycojkS .rough-node .label text,#mermaid-svg-dtNuwdb46IycojkS .node .label text,#mermaid-svg-dtNuwdb46IycojkS .image-shape .label,#mermaid-svg-dtNuwdb46IycojkS .icon-shape .label{text-anchor:middle;}#mermaid-svg-dtNuwdb46IycojkS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-dtNuwdb46IycojkS .rough-node .label,#mermaid-svg-dtNuwdb46IycojkS .node .label,#mermaid-svg-dtNuwdb46IycojkS .image-shape .label,#mermaid-svg-dtNuwdb46IycojkS .icon-shape .label{text-align:center;}#mermaid-svg-dtNuwdb46IycojkS .node.clickable{cursor:pointer;}#mermaid-svg-dtNuwdb46IycojkS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-dtNuwdb46IycojkS .arrowheadPath{fill:#333333;}#mermaid-svg-dtNuwdb46IycojkS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dtNuwdb46IycojkS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dtNuwdb46IycojkS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dtNuwdb46IycojkS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-dtNuwdb46IycojkS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dtNuwdb46IycojkS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-dtNuwdb46IycojkS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dtNuwdb46IycojkS .cluster text{fill:#333;}#mermaid-svg-dtNuwdb46IycojkS .cluster span{color:#333;}#mermaid-svg-dtNuwdb46IycojkS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-dtNuwdb46IycojkS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dtNuwdb46IycojkS rect.text{fill:none;stroke-width:0;}#mermaid-svg-dtNuwdb46IycojkS .icon-shape,#mermaid-svg-dtNuwdb46IycojkS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dtNuwdb46IycojkS .icon-shape p,#mermaid-svg-dtNuwdb46IycojkS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-dtNuwdb46IycojkS .icon-shape .label rect,#mermaid-svg-dtNuwdb46IycojkS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dtNuwdb46IycojkS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dtNuwdb46IycojkS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dtNuwdb46IycojkS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 变化>500m 或间隔5min
GET /mf/search
客户端获取定位
POST /baidu/location
UserLocationApiImpl

save 或 updateFirst
user_location 集合

GeoJsonPoint + 2dsphere 索引
queryNearUser

withinSphere 圆球查询
userId 列表
UserInfoApi 补全昵称头像
NearUserVo 列表

三段各自的职责:

  • 上报段 :客户端在位置变化超过 500 米或每隔 5 分钟时调 POST /baidu/location,服务端把经纬度包成 GeoJsonPoint 写入 MongoDB。这是数据来源,没有这一段,检索是空转。
  • 存储段 :user_location 集合一个用户一条,location 字段存 GeoJsonPoint,靠 @CompoundIndex(def = "{'location': '2dsphere'}) 建地理位置索引。索引是地理查询的硬前提,没建 2dsphere 索引,$geoWithin 这类查询要么报错要么全表扫。
  • 检索段 :以「我的坐标」为圆心、distance 为半径画圆,用 withinSphere 圈出范围内所有位置文档,提取 userId,再回 MySQL 补全用户资料。

一个自然的问题:MySQL 加经纬度两个字段也能算距离,为什么要引 MongoDB?算一笔账就清楚了,MySQL 里查「附近的人」只能用边界矩形粗筛(WHERE lat BETWEEN ... AND lng BETWEEN ...),把所有候选行拉到内存里逐个算勾股距离再排序,数据量一大就是灾难;而且MySQL 对这种查询用不上高效的空间索引(5.7 之前的版本基本无解)。MongoDB 的 2dsphere 索引在写入时就把坐标组织成了球面可检索的结构,withinSphere 一次圈定,且天然支持「按距离排序」的升级路径($geoNear)。地理位置检索是 MongoDB 相对关系型数据库的传统强项,这里用它名正言顺。

各环节要点速查表

环节 所在模块 关键类/字段 关键 API 或注解 产出/约束
位置上报 mf-app-server BaiduController / BaiduService POST /baidu/location 经纬度 + 地址描述
位置写入 mf-dubbo-mongo UserLocationApiImpl.updateLocation mongoTemplate.save / updateFirst 一用户一条,覆盖更新
存储结构 mf-model UserLocation.location GeoJsonPoint + @CompoundIndex(2dsphere) x 存经度,y 存纬度
范围检索 mf-dubbo-mongo UserLocationApiImpl.queryNearUser Distance → Circle → withinSphere 半径单位:米转千米
资料补全 mf-app-server mfService.queryNearUser userInfoApi.findByIds 性别筛选在此层做
VO 组装 mf-model NearUserVo.init 静态工厂 只透出三个字段

查询点清单

实现这条链路时,下面这些点是实测容易出错或留有缺口的地方,先列在这里,细节在同章各篇展开:

  • 坐标顺序 :GeoJsonPoint 构造参数是 (经度, 纬度),即 x 在前。按 (纬度, 经度) 传不报错,但查出来的「附近」全在世界另一头,GeoJSON 标准就是先经后纬,写反了很难从日志里看出来。
  • 单位换算 :queryNearUser 收到的是米,Distance 构造时必须 metre / 1000 转成千米再配 Metrics.KILOMETERS。漏掉换算,搜「2000 米」实际搜的是 2000 千米。
  • 无排序 :withinSphere 只做范围过滤,不保证结果按距离从近到远。要按距离排序,得换 $geoNear(NearQuery + geoNear)。
  • 无分页 :当前实现一次圈定就 find 全量返回,附近人密度高的市中心,一次可能返回成百上千条。
  • 结果含自己 :圆心是自己的坐标,自己必然在圆内。排除逻辑放在了 app-server 层的 VO 组装循环里,dubbo 层返回的 userId 列表是包含调用者本人的。
  • Movement 的经纬度是 String :动态表(movement)的 longitude/latitude 字段是 String 类型,将来做「附近的动态」时需要 Double.parseDouble 转换并改造成 GeoJsonPoint,这是留给后续章节的一个已知改造点。
  • GeoJsonPoint 不能过 Dubbo :GeoJsonPoint 没有实现 Dubbo 可用的序列化,所以 UserLocationApi 的方法签名里只传 Double longitude / Double latitude 原始值,坐标对象只在 dubbo-provider 内部构造。跨服务只传数字,别传坐标对象。

常见误解澄清

常见误解 实际情况
「搜附近」结果里的人是系统推荐匹配的 结果完全由地理坐标圈定,与缘分值、推荐模型无关;没上报位置的人永远不出现
MongoDB 的地理查询不需要建索引也能跑 withinSphere/$geoNear 必须有 2dsphere(或 2d)索引,否则报错或退化为低效扫描
GeoJsonPoint(longitude, latitude) 参数顺序无所谓 必须先经度后纬度;写反不报错,但圆心落在错误的半球,结果整批作废
distance 参数传米,Distance 直接用 Distance 构造的是千数值,必须先 metre / 1000,否则半径放大一千倍
搜附近只能查人,动态是另一套方案 同一套 2dsphere + 范围查询可以直接复用到动态表,只是 Movement 的 String 经纬度需要先改造

总结

搜附近的功能全景可以压成一句话:客户端定期上报,MongoDB 用 2dsphere 索引管住每人的最新坐标,查询时以自己为圆心画圆圈人。方案本身不复杂,复杂性都藏在细节里,单位换算、坐标顺序、排序分页的缺口、String 经纬度的历史包袱。数据从哪来、怎么存进去,是这条链路的第一步。

下一篇上报地理位置:思路分析先解决「位置怎么进来」:上报时机、存储结构设计、一用户一条的覆盖更新策略。

为什么单独拆一篇做功能概述

搜附近这条链路横跨四个模块:客户端负责采集定位,mf-app-server 负责接收上报和对外提供接口,mf-dubbo-mongo 负责与 MongoDB 打交道,mf-model 负责承载存储结构与 VO。任何一处理解偏差,最终都会表现为「搜出来的人不对」这种极难排查的现象------它不报错,只是结果错了。

所以本篇不写代码,只做三件事:

  • 把产品形态和接口契约固定下来,明确「输入是什么、输出是什么」
  • 把三段式链路画清楚,让每个环节的边界和职责一目了然
  • 把已知的坑提前列出来,后续各篇展开时对照着看

数据流视角:一次搜索请求经历了什么

从用户点开「搜附近」到卡片渲染出来,一次请求大致经过下面几步:
"mf-dubbo-user" "MongoDB" "mf-dubbo-mongo" "mf-app-server" "客户端" "mf-dubbo-user" "MongoDB" "mf-dubbo-mongo" "mf-app-server" "客户端" #mermaid-svg-doBqpeH35bziDPF8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-doBqpeH35bziDPF8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-doBqpeH35bziDPF8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-doBqpeH35bziDPF8 .error-icon{fill:#552222;}#mermaid-svg-doBqpeH35bziDPF8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-doBqpeH35bziDPF8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-doBqpeH35bziDPF8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-doBqpeH35bziDPF8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-doBqpeH35bziDPF8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-doBqpeH35bziDPF8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-doBqpeH35bziDPF8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-doBqpeH35bziDPF8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-doBqpeH35bziDPF8 .marker.cross{stroke:#333333;}#mermaid-svg-doBqpeH35bziDPF8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-doBqpeH35bziDPF8 p{margin:0;}#mermaid-svg-doBqpeH35bziDPF8 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-doBqpeH35bziDPF8 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-doBqpeH35bziDPF8 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-doBqpeH35bziDPF8 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-doBqpeH35bziDPF8 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-doBqpeH35bziDPF8 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-doBqpeH35bziDPF8 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-doBqpeH35bziDPF8 .sequenceNumber{fill:white;}#mermaid-svg-doBqpeH35bziDPF8 #sequencenumber{fill:#333;}#mermaid-svg-doBqpeH35bziDPF8 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-doBqpeH35bziDPF8 .messageText{fill:#333;stroke:none;}#mermaid-svg-doBqpeH35bziDPF8 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-doBqpeH35bziDPF8 .labelText,#mermaid-svg-doBqpeH35bziDPF8 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-doBqpeH35bziDPF8 .loopText,#mermaid-svg-doBqpeH35bziDPF8 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-doBqpeH35bziDPF8 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-doBqpeH35bziDPF8 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-doBqpeH35bziDPF8 .noteText,#mermaid-svg-doBqpeH35bziDPF8 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-doBqpeH35bziDPF8 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-doBqpeH35bziDPF8 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-doBqpeH35bziDPF8 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-doBqpeH35bziDPF8 .actorPopupMenu{position:absolute;}#mermaid-svg-doBqpeH35bziDPF8 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-doBqpeH35bziDPF8 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-doBqpeH35bziDPF8 .actor-man circle,#mermaid-svg-doBqpeH35bziDPF8 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-doBqpeH35bziDPF8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} "GET /mf/search?distance=2000&gender=1""从登录态取当前 userId 与坐标""queryNearUser(lng, lat, distance)""metre / 1000 转千米,构造 Circle""withinSphere 查询 user_location""位置文档列表""userId 列表""findByIds(userIds) 补全资料""UserInfo 列表""性别过滤 + 排除自己 + 组装 VO""List<NearUserVo>"

几个值得注意的边界:

  • 坐标从哪来:检索用的圆心不是请求参数,而是服务端根据当前登录用户查出来的最新上报坐标。这意味着没上报过位置的用户,连自己都搜不了。
  • 两次跨服务调用:一次查位置(MongoDB),一次查资料(MySQL)。前者返回的是「谁在附近」,后者补的是「他长什么样」,职责严格分离。
  • 过滤发生在最后一环:性别筛选和排除自己都在 app-server 层完成,不在数据库层。这决定了返回条数会少于数据库命中的条数。

方案选型的取舍

把「附近的人」做出来,技术上至少有四条路可走,各自的代价差别很大:

方案 实现方式 优点 代价
MySQL 矩形粗筛 WHERE lat BETWEEN ... AND lng BETWEEN ... 无需引入新组件 结果是矩形不是圆,需内存二次过滤;数据量大时全表扫
MySQL + 空间扩展 ST_Distance_Sphere / POINT 类型 仍在 MySQL 内 版本依赖强,索引效果有限,函数计算无法走索引
Redis GEO GEOADD / GEOSEARCH 内存级速度,天然支持距离排序 需额外维护一份位置数据,持久化与一致性要自己兜
MongoDB 2dsphere GeoJsonPoint + withinSphere / $geoNear 原生球面索引,写入即建索引,支持排序升级 引入 MongoDB 组件,需处理跨库一致性

项目最终选 MongoDB,核心原因是它把「球面距离」这件事下沉到了索引层:写入时坐标就被组织成可检索结构,查询时一次圈定,不需要把候选行拉到内存里逐个算距离。Redis GEO 在性能上更优,但位置数据本身需要持久化留存(用于后续的「附近的动态」等扩展),MongoDB 的文档模型更贴合。

本篇在整章中的位置

这一章按「先全景、再拆解」的顺序组织:

  • 本篇(功能概述与分析):讲清楚要做什么、整体怎么搭、有哪些坑
  • 上报地理位置:解决「位置怎么进来」,包括上报时机、存储结构、一用户一条的覆盖更新
  • 检索附近的人 :解决「怎么圈出来」,包括 withinSphere 的构造、单位换算、结果处理
  • 扩展与优化 :解决「怎么做得更好」,包括 $geoNear 排序、分页、附近的动态

读完本篇应该能回答三个问题:这个功能的产品边界在哪、数据从哪来到哪去、哪些地方最容易出错。带着这三个问题的答案往下读,每一篇都能对上号。

相关推荐
骑着蜗牛撵大象3272 小时前
大模型 Agent 的沙箱护栏:gVisor 隔离、出口白名单与审计闭环
java·开发语言·jvm·安全·agent沙箱·gvisor·microvm
独泪了无痕2 小时前
Hutool之LocalDateTimeUtil:让时间间隔操作如虎添翼
java
Dxy12393102163 小时前
JS DES 加密详解
开发语言·javascript·arcgis
充值内卷3 小时前
我给桌面加了个鱼缸:C++ 写后端,WebView2 里养一座 Three.js 小岛
开发语言·javascript·c++
云和数据.ChenGuang3 小时前
深入解析 Python 的 dir():从用法到 CPython 源码实现
开发语言·人工智能·python·langchain
三无少女指南3 小时前
从这里开始:Java 后端与 AI 工程的三条阅读路线
java·学习路线·ai工程
香蕉大香蕉(06敬畏市场)4 小时前
MySQL 单点详解(07)
java·数据库·mysql·ai·博客·八股
nice_lcj5204 小时前
深入理解 Java 反射:从 Class 对象到方法调用(含性能实测 + 迷你 IOC 实战 + 面试考点)
java·开发语言·后端·面试
骑着蜗牛撵大象3275 小时前
2026 后端 AI 工程化:Spring AI 管调用、MCP 管工具、Agent 内嵌管自治
java·人工智能·后端·spring·ai·可观测性·mcp