java面试题 4:鉴权

Java相关回答不足点整理

以下专门筛选出两轮面试中Java/后端/数据库/中间件/架构方向回答不到位的地方,按问题类别整理,方便你集中复习。

一、系统架构与微服务

1. 网关鉴权(API权限 + 数据权限)------两场均暴露

你的回答: "鉴权这块可能不太清楚,平时业务开发比较多,不太关注。"

问题所在: 在银行核心做Java开发近5年,即使没亲自写过鉴权模块,也应该知道基本链路。这个回答直接关上了话题,面试官会认为你只盯着自己一亩三分地。

需要掌握的基本链路:

网关层用Spring Security + JWT做统一认证,流程是:

  1. 用户登录 → 认证服务颁发JWT(含用户ID、角色、权限列表)
  2. 后续请求携带JWT → 网关过滤器验证签名/有效期
  3. 验证通过后,在请求头中注入X-User-IdX-Role等信息
  4. 下游微服务通过拦截器获取这些信息做接口权限 (角色/权限码校验)和数据权限 (只查自己部门/机构的数据,通过SQL拦截器动态追加WHERE条件)

如果没实际做过,至少要把这个链路背下来。

问题:Spring Cloud Gateway 如何实现鉴权(GlobalFilter),并结合银行场景说明。

面试回答核心话术(可直接用于面试)

"Gateway 的鉴权我通过自定义 GlobalFilter 实现,核心思路是:在请求转发到下游微服务之前,统一校验身份令牌和权限,不合法直接拦截返回 401/403,不进入内网服务

具体做法是:客户端登录后,认证中心(比如 OAuth2 或自研的统一认证平台)颁发一个 JWT Token,前端每次请求都把 Token 放在 Header 里带过来。Gateway 的 GlobalFilter 拦截所有请求,从 Header 中取出 Token,调用认证中心校验 Token 有效性,或直接解析 JWT 验签。校验通过后,把用户信息(比如 userId、角色)写入请求头,传给下游微服务使用;校验失败直接返回 401,根本不给下游服务增加负担。

在银行项目中,我们还做了接口级权限控制 :在 Gateway 里维护一个路由-角色映射表(或从配置中心动态加载),校验完 Token 后,判断当前用户角色是否有权访问该接口。比如 /loan/approve 只有信贷审批员能访问,普通柜员只能访问 /deposit/query。这样做的好处是把安全防线前置到网关层,避免敏感接口暴露给未授权用户。

与 Sentinel 限流的区别:限流是'你请求合法但量太大,我限制你';鉴权是'你身份不明或没权限,我不让你进'。两者都在 Gateway 层完成,一个靠 GlobalFilter 实现,一个靠 Sentinel Gateway 适配器实现,共同构成银行系统的第一道安全防线。"

详细解析

一、Gateway 鉴权的整体架构

复制代码
客户端(App/Web)
    │  Header: Authorization: Bearer <JWT Token>
    ↓
Spring Cloud Gateway
    │
    ├─ ① GlobalFilter: 鉴权拦截器(自定义)
    │     ├─ 解析 Token
    │     ├─ 校验有效性
    │     ├─ 校验角色权限
    │     ├─ 成功:放行,写入用户信息 Header
    │     └─ 失败:返回 401/403
    │
    ├─ ② Sentinel Gateway 适配器:限流/熔断
    │
    └─ ③ 路由转发到下游微服务
         │  Header: X-User-Id, X-User-Role
         ↓
      deposit-service / loan-service

二、GlobalFilter 鉴权代码实现

1. 自定义鉴权过滤器
java 复制代码
@Component
@Order(-1)  // 优先级最高,在其他过滤器之前执行
public class AuthGlobalFilter implements GlobalFilter, Ordered {

    @Autowired
    private AuthService authService;  // 调用认证中心或本地 JWT 校验

    // 白名单路径(登录接口、健康检查等不需要鉴权)
    private static final List<String> WHITE_LIST = Arrays.asList(
        "/auth/login", "/actuator/health", "/public/");

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String path = exchange.getRequest().getURI().getPath();

        // 1. 白名单直接放行
        if (isWhitePath(path)) {
            return chain.filter(exchange);
        }

        // 2. 从 Header 中获取 Token
        String token = exchange.getRequest().getHeaders().getFirst("Authorization");
        if (StringUtils.isBlank(token) || !token.startsWith("Bearer ")) {
            return unauthorized(exchange, "缺少认证令牌");
        }
        token = token.substring(7); // 去掉 "Bearer " 前缀

        // 3. 校验 Token 有效性
        UserInfo userInfo;
        try {
            userInfo = authService.validateToken(token);
            if (userInfo == null) {
                return unauthorized(exchange, "令牌无效或已过期");
            }
        } catch (Exception e) {
            return unauthorized(exchange, "令牌校验异常");
        }

        // 4. 校验角色权限(接口级)
        if (!hasPermission(userInfo.getRole(), path)) {
            return forbidden(exchange, "无权限访问该接口");
        }

        // 5. 将用户信息写入请求头,传递给下游微服务
        ServerHttpRequest modifiedRequest = exchange.getRequest().mutate()
                .header("X-User-Id", userInfo.getUserId())
                .header("X-User-Name", userInfo.getUserName())
                .header("X-User-Role", userInfo.getRole())
                .build();
        ServerWebExchange modifiedExchange = exchange.mutate()
                .request(modifiedRequest)
                .build();

        return chain.filter(modifiedExchange);
    }

    // 返回 401 未认证
    private Mono<Void> unauthorized(ServerWebExchange exchange, String message) {
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
        byte[] body = ("{\"code\":401,\"message\":\"" + message + "\"}").getBytes();
        return exchange.getResponse()
                .writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(body)));
    }

    // 返回 403 无权限
    private Mono<Void> forbidden(ServerWebExchange exchange, String message) {
        exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
        exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
        byte[] body = ("{\"code\":403,\"message\":\"" + message + "\"}").getBytes();
        return exchange.getResponse()
                .writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(body)));
    }

    private boolean isWhitePath(String path) {
        return WHITE_LIST.stream().anyMatch(path::startsWith);
    }

    private boolean hasPermission(String role, String path) {
        // 简化版:从配置或数据库中查询权限映射
        // 例如:/loan/approve → ROLE_APPROVER
        //       /deposit/* → ROLE_TELLER, ROLE_ADMIN
        return permissionService.check(role, path);
    }

    @Override
    public int getOrder() {
        return -1;
    }
}

三、银行场景下的权限分级

接口路径 功能 允许角色
/deposit/query 查询余额 柜员、客户经理、管理员
/loan/approve 贷款审批 信贷审批员、管理员
/core/transfer 资金转账 柜员、管理员
/admin/config 系统配置 仅管理员
/auth/login 登录接口 白名单,无需鉴权

四、与 Sentinel 限流的配合

yaml 复制代码
# Gateway 配置 Sentinel 限流
spring:
  cloud:
    sentinel:
      scg:
        fallback:
          response-status: 429
          response-body: "{\"code\":429,\"message\":\"请求过多,请稍后重试\"}"

两者区别

机制 解决的问题 实现方式 返回码
鉴权(AuthFilter) 你是谁?你有没有权限? 自定义 GlobalFilter 401 / 403
限流(Sentinel) 你合法但请求太快了 Sentinel Gateway 适配器 429

执行顺序:鉴权先于限流------连身份都没有的请求,根本不值得消耗限流计数器的资源。

五、Token 校验的两种方案

方案一:JWT 本地验签(推荐)
java 复制代码
public UserInfo validateToken(String token) {
    Claims claims = Jwts.parser()
            .setSigningKey(SECRET_KEY)
            .parseClaimsJws(token)
            .getBody();
    return new UserInfo(
        claims.get("userId", String.class),
        claims.get("userName", String.class),
        claims.get("role", String.class)
    );
}

优点 :无需远程调用,性能高。

缺点:无法主动失效 Token(除非加入黑名单)。

方案二:调用认证中心远程校验
java 复制代码
public UserInfo validateToken(String token) {
    // 通过 Feign 或 HTTP 调用统一认证中心
    return authCenterClient.validate(token);
}

优点 :Token 可主动失效。

缺点:多一次远程调用,增加延迟和故障点。

银行实践 :我们采用JWT + Redis 黑名单方案。JWT 短期有效(15 分钟),配合 refresh_token 续期;当用户修改密码或强制下线时,把旧 JWT 的 ID 加入 Redis 黑名单,验签后再查黑名单,实现准实时失效。

六、下游微服务如何获取用户信息

Gateway 已经把用户信息写入 X-User-IdX-User-Role 等 Header,下游服务直接读取:

java 复制代码
@GetMapping("/balance")
public BalanceDTO getBalance(@RequestHeader("X-User-Id") String userId,
                              @RequestHeader("X-User-Role") String role) {
    log.info("用户 {} 角色 {} 查询余额", userId, role);
    // 业务逻辑
}

或通过拦截器统一提取,放入 ThreadLocal:

java 复制代码
public class UserContext {
    private static final ThreadLocal<String> USER_ID = new ThreadLocal<>();
    public static void setUserId(String userId) { USER_ID.set(userId); }
    public static String getUserId() { return USER_ID.get(); }
    public static void clear() { USER_ID.remove(); }
}

七、面试加分点

  • OAuth2 集成:如果面试官追问,可以说"我们用的是 Spring Security + OAuth2 + Gateway 统一鉴权,认证中心颁发 JWT,Gateway 做资源服务器校验"。
  • 跨服务认证传递 :Feign 的 RequestInterceptor 自动从当前请求中获取 Token 并写入下游请求头,实现全链路认证透传。
  • 性能优化:JWT 解析非常快(毫秒级),不会成为网关瓶颈。对于极高并发(10 万+ QPS),可考虑把用户角色信息缓存到本地(Caffeine),避免每次都查 Redis 黑名单。

2. 流量管控(限流、削峰)------第一场

你的回答: 没答上来。

问题所在: 你讲了贷款生命周期,流程很长(合同→计提→回收→账务),面试官自然会问"流量这么大,你们怎么做限流和削峰?"你没接住。

正确方向:

银行核心系统的流量治理通常分几层:

层级 手段 说明
网关层 Sentinel限流 按API路径配置QPS阈值,超出快速失败返回
业务层 线程池隔离 + 熔断降级 不同服务用不同线程池,下游故障时熔断
消息层 MQ削峰填谷 高峰期请求先入MQ,消费者按自身能力消费,避免压垮数据库
批量处理 日切批处理错峰 核心账务类操作放到夜间批处理窗口执行

3. 分库分表的"架构视角"说不清楚------两场均被追问

你的回答: 集中在技术实现上------internal key取模分4个库,加DB number字段。

面试官原话: "你平时和你们架构聊,肯定也不是这么聊的,咱们可以再上升一下。"

问题所在: 面试官想听的是分库分表的前置决策,而不只是取模逻辑。

正确方向(先说业务再说技术):

"我们当时做新核心重构,分库的背景有两个:一是业务上要把贷款、存款、账务拆成独立的领域服务,对应的库天然就分开了;二是数据量太大,单库扛不住。最终定了4个库,分库键选了internal_key,因为它全局唯一且均匀分布,取模后数据分布比较均衡。分库后对上层业务透明------我们在DAO层封装了路由逻辑,业务代码只需要传internal_key,不用关心具体去哪个库。"

先交代业务拆分原因 ,再说技术实现,这就是他们要的"上升"。

二、分布式事务与一致性

4. TCC vs SAGA的区别(同步/异步、适用场景)------第一场

你的回答: "链路长短不一样,TCC链路短、SAGA链路长。"

问题所在: 太浅了。面试官需要你讲清楚同步vs异步、业务侵入性、补偿机制的差异。

正确的对比:

维度 TCC SAGA
同步/异步 同步,全程等待 异步,事件驱动
资源锁定 Try阶段锁定资源,直到Confirm/Cancel释放 不锁定资源,先执行正向操作
适用场景 短链路、一致性要求极高(如扣库存) 长链路、跨多个服务(如贷款+记账+存款)
补偿方式 Cancel回滚已锁定资源 执行反向补偿操作
业务侵入性 高(需实现try/confirm/cancel三个方法) 中(需实现正向+补偿两套逻辑)
优点 强一致性,可回滚 不长时间占用资源,吞吐量高
缺点 资源占用时间长,并发能力受限 最终一致,需处理补偿失败的情况

为什么银行核心用SAGA更多:

因为贷款发放链路涉及合同、计提、记账、存款等多个服务,如果用TCC,Try阶段要锁住所有资源,用户等待时间长且并发能力差。SAGA先做正向操作(默认成功),失败了再补偿,用户体验好,代价是最终一致性------但银行可以通过日切对账来兜底。

5. 幂等设计------第一场

你的回答: 举了Redis宕机导致重复扣款的例子,用数据库唯一索引做兜底。

问题所在: 例子本身没问题,但回答过于局限,没有涵盖幂等的完整方案。

需要掌握的系统性视角:

幂等应该做多层次拦截

层级 方案 说明
业务入口 全局流水号 + Redis去重(SETNX) 请求进来先查Redis是否存在该流水号,存在则直接返回上次结果
数据库层 唯一索引兜底 Redis万一挂了,DB唯一索引拦截重复插入
消息消费端 业务主键去重表 MQ消费时先查去重表,已消费则跳过

注意: 你举的例子里Redis挂了导致重复扣款,说明你们没有做数据库唯一索引兜底(或者索引字段没覆盖到这笔交易)。面试官可能会追问"那你们后来怎么解决Redis单点问题的?"------可以考虑用Redis Cluster或引入本地缓存做二级缓存。

三、数据库与索引

6. 索引设计太"八股",缺少场景化案例------第一场被反复追问

你的回答: B+树结构、聚簇索引/二级索引、覆盖索引、回表、左前缀原则、少用函数、小表驱动大表......

面试官原话: "你说的都没问题,但我听不太出来你是真的在业务里做过。能不能给我讲一个你真实的场景?"

问题所在: 所有知识点都对,但缺少一个你自己的设计案例,听起来像在背书。

你需要准备一个自己的案例,比如这样:

"我当时在数据迁移时遇到一个慢查询:账户交易流水表(trans_hist)4000万数据,高频查询条件是WHERE account_no = ? AND trans_date BETWEEN ? AND ? ORDER BY trans_time DESC。原来三个字段各有一个单列索引,但EXPLAIN发现每次只用到account_no索引,然后回表查trans_date和trans_time,平均耗时1.8秒。

我改成联合索引(account_no, trans_date DESC, trans_time DESC),查询耗时降到40ms。原因是:①覆盖了查询所需字段,不用回表;②trans_date和trans_time按降序排列,省去了额外排序。缺点是写入慢了约15%,但这张表读多写少,业务可接受。"

表名、数据量、优化前后数字、选择的权衡,才是他们要的场景化回答。

7. 笛卡尔积的触发条件------第二场

你的回答: "记不清了。"

这是送分题,不应该丢。

正确答案:

sql 复制代码
-- 1. 没有连接条件
SELECT * FROM table_a, table_b;

-- 2. 连接条件恒为真(1=1 或 无效条件)
SELECT * FROM table_a a JOIN table_b b ON 1 = 1;

-- 3. 显式使用 CROSS JOIN
SELECT * FROM table_a CROSS JOIN table_b;

-- 4. 左连接/右连接时连接条件写错导致匹配到所有行

面试官问这个,不是在考你记不记得语法,而是想确认你对SQL执行计划的基本认知------连表时如果没有有效的ON条件,优化器只能做笛卡尔积,性能灾难。

笛卡尔积触发条件 ------ 问题复盘与清晰答案

你当时的回答

面试官: "连表查询的时候,什么时候会出现笛卡尔积?"

你的回答: "连表查询产生笛卡尔积,一般也是,因为它是SQL的话,它是有三范式的嘛,就是说,有点记不清了。"

为什么这个回答很可惜

这是一个极基础的送分题,答案本身不超过3句话。你如果在这里卡住,面试官的判断会是:

  1. SQL基本功不扎实 ------ 连表查询是后端开发日常操作,触发笛卡尔积属于最基础的认知
  2. 面对简单问题缺乏推导能力 ------ 即使忘了具体场景,也可以根据原理推导出来
  3. 直接放弃("记不清了")而不是用已知知识推理 ------ 面试官会认为你解决问题的意愿不足

实际上,哪怕你完全没背过答案,也可以现场推理出来:

"笛卡尔积就是两表所有行两两组合。那什么情况下SQL会把所有行两两组合?当查询没有告诉数据库怎么关联的时候。所以要么是没写连接条件,要么是连接条件恒成立。"

能说出这句话,就不用背任何代码,面试官也会认可你的逻辑能力。

完整清晰的正确答案

一句话核心定义

笛卡尔积(Cartesian Product) 是指对两张表进行连接查询时,没有有效的关联条件,导致左表的每一行与右表的每一行逐一组合,结果集行数 = 左表行数 × 右表行数。

举例: A表有1000行,B表有500行,产生笛卡尔积 → 返回 500,000 行,即使这50万行里99.99%都是无意义数据。

触发笛卡尔积的4种典型场景

场景1:完全没有连接条件
sql 复制代码
-- 没有任何WHERE或ON来限定关联关系
SELECT * FROM table_a, table_b;

数据库不知道两表怎么关联,只能把所有行交叉组合。

场景2:连接条件无效/恒为真
sql 复制代码
-- ON条件是常量1=1,永远成立
SELECT * FROM table_a a
JOIN table_b b ON 1 = 1;

-- 或者使用了无效表达式
SELECT * FROM table_a a
JOIN table_b b ON a.无效字段 = b.任何值;  -- 字段不存在→被忽略→全连接
场景3:显式使用 CROSS JOIN
sql 复制代码
-- CROSS JOIN 的语义就是笛卡尔积
SELECT * FROM table_a CROSS JOIN table_b;

这是有意为之的笛卡尔积,少数场景(如生成测试数据、穷举组合)会主动使用。

场景4:连接条件写错导致一对多膨胀(近似笛卡尔积)
sql 复制代码
-- 本该用唯一键关联,结果用了非唯一字段
-- 例如 table_a.customer_type 只有2种类型(个人/企业)
-- table_b.customer_type 也有2种类型
-- 每个customer_type下各有大量行,关联后数据量膨胀巨大
SELECT * FROM table_a a
JOIN table_b b ON a.customer_type = b.customer_type;

这种情况下不是严格的笛卡尔积(因为有限制条件),但效果类似------结果行数远超预期,性能同样灾难。它本质上是对连接条件选择性过低缺乏警惕,可以用"近似笛卡尔积"来向面试官说明。

如何发现笛卡尔积

sql 复制代码
-- 在执行SQL之前,先用 EXPLAIN 查看执行计划
EXPLAIN SELECT * FROM table_a, table_b;

EXPLAIN输出中如果看到:

  • Using join buffer (Block Nested Loop) ------ 没有索引可用,在做全表扫描连接
  • filtered 列值极低
  • rows列显示 table_a行数 × table_b行数 级别的扫描行数

基本就是笛卡尔积了。

避免笛卡尔积的方法

方法 说明
写连接条件 多表查询必须带有效的ON或WHERE关联条件
用小表驱动大表 JOIN顺序上先连数据量小的表,再连大表,减少中间结果集
关联字段加索引 被关联的字段(如外键)建索引,让优化器走Index Nested-Loop Join,避免全表扫描
审查执行计划 提交SQL前用EXPLAIN看一眼扫描行数是否合理
避免隐式连接混用 不要把逗号连接和JOIN混在一起写,容易漏条件

如果面试时真的忘了怎么办?(现场推导法)

你可以说:

"我具体场景可能记得不太清了,但我可以推一下。笛卡尔积的本质是两表行两两配对,那产生的原因一定是查询没有给数据库一个有效的连接依据。所以大概率是:①没写WHERE或ON条件;②写了条件但条件恒为真,比如 ON 1=1;③用了CROSS JOIN这种显式全连接。我一般在开发和代码审查时会特别注意这一点,避免生产环境出现慢查询。"

这样即使细节记不全,面试官也会认为你理解原理而不是死记硬背。

与银行核心业务场景的结合(加分)

如果你能把笛卡尔积和你的银行数据迁移经历联系起来,会更有说服力:

"我在做数据迁移时,遇到过一个具体案例:抽数SQL中有一张10亿的交易流水表(trans_hist)和一张账户表(account)做JOIN,连接条件本应是 trans_hist.account_no = account.account_no,但有人把条件写成了 trans_hist.trans_date = account.open_date,导致一条账户记录匹配到几千条流水,跑了40分钟没出结果。我们后来用EXPLAIN发现rows估算显示笛卡尔积量级,修正条件后降到2秒。从那以后,我们团队所有连表SQL都会先过EXPLAIN审查。"

这个案例同时展示了:业务场景 + 问题发现手段(EXPLAIN)+ 优化效果(40分钟→2秒)+ 团队规范改进,比单纯背语法要强十倍。

8. 缓存穿透/击穿/雪崩的概念混淆------第二场

你的回答:

  • 穿透:传一个不存在的值 → 用布隆过滤器 ✅
  • 击穿:查了一堆过期的 → 把过期时间设置大一点 ❌
  • 雪崩:集体过期 → 过期时间加随机数 ✅

问题所在: "击穿"和"穿透"的概念你没区分清楚,把击穿说成了"查了一堆过期的",定义有误。

正确区分:

概念 定义 解决方案
穿透 查一个缓存和DB都不存在的key,每次都打到DB 布隆过滤器 / 缓存空值(如存""并设短过期时间)
击穿 一个热点key恰好过期,大量请求同时打到DB 互斥锁(SETNX)/ 逻辑过期(提前异步刷新)
雪崩 大量key在同一时间过期,大量请求打到DB 过期时间加随机偏移 / 多级缓存(本地+分布式)

"查了一堆过期的"更像是雪崩的极端情况,而不是击穿的定义。

四、中间件

9. MQ消息积压的处理------第一场直接卡住

你的回答: "答不上来。"

基础应对方案(至少要说出来):

  1. 扩容消费者:增加消费者实例,但要确保Topic的分区数≥消费者数,否则多余的消费者拿不到消息
  2. 提高消费速率 :临时调大batchSize(批量拉取)、调高prefetchCount(预取数量)
  3. 排查根因:是下游DB写入慢?还是消费者逻辑中有远程调用超时?针对性优化
  4. 临时降级:先把积压消息转存到新Topic,用更多消费者并行处理,原Topic恢复后回补

即使没实际遇到过,至少要知道这4条思路。完全说"不知道"会让面试官觉得你只关心业务CRUD,不关心系统稳定性。

五、总结:Java方向急需加强的TOP 5

按两场面试暴露的严重程度排序:

优先级 知识点 为什么急 建议行动
1 索引场景化案例 两场都被追问,但都没讲出具体案例 准备一个真实的慢查询优化故事(表名、字段、数据量、优化前后耗时)
2 网关鉴权链路 直接说"不知道",扣分严重 背熟JWT→Gateway→下游拦截器的流程,至少讲到能让人听懂
3 TCC vs SAGA 完整对比 第一场被追问同步/异步,回答太浅 把上表中的对比维度记熟,尤其要能结合你做的贷款业务讲为什么选SAGA
4 缓存三兄弟(穿透/击穿/雪崩) 第二场概念混淆 把定义和各自解决方案背准,这是高频面试题
5 MQ积压处理思路 第一场卡住 把扩容消费者、调参、根因排查、临时降级4条记住即可应急

另外提醒一点:如果下次被问到不会的Java问题,不要直接说"不知道"就结束。试着说"这个具体配置不是我在负责,但我了解整体思路是......"------至少证明你有架构意识,而不是完全空白。

相关推荐
时间的拾荒人2 小时前
C语言字符函数与字符串函数完全指南
c语言·开发语言
帅次2 小时前
Android 高级工程师面试:Java 基础知识 近1年高频追问 22 题
android·java·面试
蓝胖的四次元口袋2 小时前
Java集合(4)
java·哈希算法
2501_948106913 小时前
计算机毕业设计之基于jsp教科研信息共享系统
java·开发语言·信息可视化·spark·课程设计
TanYYF3 小时前
spring ai入门教程二
java·人工智能·spring
SeeYa-J3 小时前
Spring IOC(Inversion of Control)
java·spring·rpc
取经蜗牛3 小时前
Python 第一阶段完全指南:从零到第一个实用工具
开发语言·python
不会c+3 小时前
02-SpringBoot配置文件
java·spring boot·后端
AI 大模型学习不踩坑4 小时前
OpenClaw 完整教程:从安装到使用(官方脚本版)
java·人工智能·神经网络·机器学习·计算机视觉·自然语言处理·openclaw