1
0

后端知识记忆

2026-04-30
2026-07-23

后端注解

前端传数据用 @RequestBody / @RequestParam / @PathVariable
后端处理用 @Service / @RestController
参数校验用 @Valid + @NotBlank / @Email
自动生成代码用 @Data / @Slf4j
远程调用用 @DubboReference

天天用(必须记住)

注解 一句话记忆
@RestController 告诉Spring我是REST控制器,返回JSON
@GetMapping 告诉Spring只接收GET请求
@PostMapping 告诉Spring只接收POST请求
@RequestBody 告诉Spring从请求体里取数据
@RequestParam 告诉Spring从URL问号后面取数据
@PathVariable 告诉Spring从URL路径里取数据
@Service 告诉Spring我是业务服务
@Autowired 告诉Spring按类型注入
@Data 告诉Lombok生成所有getter/setter等

方法参数注解(接收数据)

注解 一句话记忆 数据来源 前端怎么传
@RequestBody 告诉Spring从请求体里取数据 HTTP请求的Body JSON/XML数据
@Valid 告诉Spring先校验数据再进方法 配合其他注解 自动校验
@Validated 告诉Spring分组校验 配合其他注解 分组校验
@RequestParam 告诉Spring从URL问号后面取数据 URL参数 ?name=张三&age=18
@PathVariable 告诉Spring从URL路径里取数据 URL路径 /user/123
@RequestHeader 告诉Spring从请求头里取数据 HTTP Header token: abc123
@CookieValue 告诉Spring从Cookie里取数据 Cookie JSESSIONID=xxx
@ModelAttribute 告诉Spring从表单/参数绑定到对象 表单/URL参数 表单提交或?name=张三

类注解(标记组件)

注解 一句话记忆 作用 使用场景
@RestController 告诉Spring我是REST控制器,返回JSON 控制层+返回JSON API接口
@Controller 告诉Spring我是控制器,返回页面 控制层+返回视图 传统Web页面
@Service 告诉Spring我是业务服务 业务逻辑层 Service实现类
@Repository 告诉Spring我是数据访问层 数据访问层 DAO/Mapper
@Component 告诉Spring我是通用组件 普通组件 工具类、辅助类
@Configuration 告诉Spring我是配置类 配置类 定义Bean、配置

依赖注入注解

注解 一句话记忆 作用 示例
@Autowired 告诉Spring按类型注入 自动装配Bean @Autowired UserService userService
@Resource 告诉Spring按名称注入 自动装配Bean @Resource(name="userService")
@Qualifier 告诉Spring指定具体Bean 配合@Autowired指定Bean名 @Qualifier("userServiceImpl")
@RequiredArgsConstructor 告诉Lombok生成构造器注入 生成final字段构造器 配合private final使用

请求映射注解(路由)

注解 一句话记忆 作用 示例
@RequestMapping 告诉Spring映射所有请求 通用请求映射 /user
@GetMapping 告诉Spring只接收GET请求 查询数据 GET /user
@PostMapping 告诉Spring只接收POST请求 新增数据 POST /user
@PutMapping 告诉Spring只接收PUT请求 全量更新 PUT /user
@DeleteMapping 告诉Spring只接收DELETE请求 删除数据 DELETE /user/{id}
@PatchMapping 告诉Spring只接收PATCH请求 部分更新 PATCH /user

数据校验注解(验证参数)

注解 一句话记忆 作用 示例
@NotNull 告诉Spring值不能为null 不能为空 @NotNull String name
@NotBlank 告诉Spring不能是空串或空格 字符串必须非空 @NotBlank String name
@NotEmpty 告诉Spring集合/数组不能为空 集合必须非空 @NotEmpty List list
@Size 告诉Spring长度/大小必须在范围内 限制大小 @Size(min=6,max=20)
@Email 告诉Spring必须是邮箱格式 邮箱校验 @Email String email
@Min 告诉Spring最小值 数字最小值 @Min(18) int age
@Max 告诉Spring最大值 数字最大值 @Max(100) int score
@Pattern 告诉Spring必须匹配正则 正则校验 @Pattern(regexp="^1\\d{10}$")
@Past 告诉Spring必须是过去时间 日期校验 @Past Date birthday
@Future 告诉Spring必须是将来时间 日期校验 @Future Date endTime

响应相关注解(返回数据)

注解 一句话记忆 作用 示例
@ResponseBody 告诉Spring返回数据放Body里 返回JSON 返回值转JSON
@ResponseStatus 告诉Spring设置响应状态码 自定义HTTP状态码 @ResponseStatus(HttpStatus.CREATED)
@RestControllerAdvice 告诉Spring我是全局异常处理器,返回JSON 全局异常处理 统一异常拦截
@ControllerAdvice 告诉Spring我是全局异常处理器 全局异常处理 统一异常拦截
@ExceptionHandler 告诉Spring我来处理这个异常 异常处理 @ExceptionHandler(AuthException.class)

配置相关注解(读取配置)

注解 一句话记忆 作用 示例
@Value 告诉Spring从配置文件取单个值 读取配置 @Value("${server.port}")
@ConfigurationProperties 告诉Spring批量读取配置到对象 批量配置映射 @ConfigurationProperties(prefix="redis")
@PropertySource 告诉Spring加载指定配置文件 加载properties文件 @PropertySource("classpath:app.properties")

Bean生命周期注解

注解 一句话记忆 作用 示例
@PostConstruct 告诉SpringBean创建后执行 初始化 加载缓存、连接
@PreDestroy 告诉SpringBean销毁前执行 清理资源 关闭连接、释放资源

数据转换注解

注解 一句话记忆 作用 示例
@Mapper 告诉MapStruct我是转换器 对象转换 @Mapper public interface AuthConvertor
@Mapping 告诉MapStruct字段映射关系 字段映射 @Mapping(source="name", target="userName")
@JsonIgnore 告诉Jackson序列化时忽略我 忽略字段 密码不返回前端
@JsonFormat 告诉Jackson日期格式化 日期格式 @JsonFormat(pattern="yyyy-MM-dd")
@JsonProperty 告诉Jackson字段名映射 字段别名 @JsonProperty("user_name")

Dubbo相关注解

注解 一句话记忆 作用 示例
@EnableDubbo 告诉Spring开启Dubbo功能 启用Dubbo 启动类上加
@DubboReference 告诉Dubbo引用远程服务 调用远程服务 @DubboReference(version="1.0.0")
@DubboService 告诉Dubbo暴露远程服务 暴露服务 Service实现类上加

其他

Entity:数据库表对象,服务内部用
DTO:数据传输对象,跨层/跨服务/前后端传数据用
VO:返回给前端展示的数据对象
Param / Request:接收前端请求参数的对象

不同微服务可以通过 Maven 共享公共 jar,但 Maven 只是共享代码,不等于运行时远程调用。公共工具类、DTO、接口可以通过 Maven 引入后本地使用;如果要调用另一个已经独立启动的微服务的业务能力,仍然需要通过 Dubbo、Feign 或 HTTP 进行远程调用。Dubbo 项目里通常把接口和 DTO 放到 API 包,Provider 实现接口并启动服务,Consumer 引入 API 包后通过 @DubboReference 注入远程代理完成调用。业务实现模块一般不要被其他微服务直接 Maven 依赖。

**API = Application Programming Interface,应用程序编程接口。简单说,API 就是:**别人调用你时需要遵守的入口和规则。

Dubbo3 的基本使用可以理解成“面向接口编程 + 远程代理调用”:先定义一个公共接口,比如 UserService;服务提供者写 UserServiceImpl implements UserService,并在实现类上加 @DubboService,表示把这个实现类作为 Dubbo 远程服务暴露出去;

服务消费者不直接依赖这个实现类,而是在需要调用的地方用 @DubboReference 注入接口类型。这里注入进来的不是当前项目里的真实实现类,而是 Dubbo 生成的远程代理对象;你调用 userFacadeService.register(userRegisterRequest)看起来像本地方法调用,但底层其实是 Dubbo 代理通过注册中心找到用户服务或文件服务的提供者地址,然后通过 RPC 网络请求调用远程服务里的真正实现类。也就是说,提供者那边用 @DubboService 暴露实现类,消费者这边用 @DubboReference 注入接口代理;本地写法像 Spring 注入接口,实际执行的是跨服务远程调用。简单说:提供者用 @DubboService 暴露实现类,消费者用 @DubboReference 注入接口代理,本地写法像普通 Spring 注入,底层其实是远程调用。

Spring 更推荐按接口类型注入,因为这样 Controller / Service 依赖的是抽象接口,而不是某个具体实现类,代码耦合更低。虽然字段写的是 private AuthService authService;,但 Spring 容器真正创建和管理的是实现类对象,比如带有 @ServiceAuthServiceImpl。注入时,Spring 会理解成:“你需要一个 AuthService 类型的 Bean,我就去容器里找有没有实现了 AuthService 接口的对象。”如果找到 AuthServiceImpl,就把这个实现类对象注入进来。所以 Spring Boot 不是只能注入实现类,而是创建实现类对象,可以按接口类型接收和使用。如果同一个接口有多个实现类,则需要通过 @Qualifier@Primary 告诉 Spring 到底注入哪一个。

<T> 是什么,T泛型占位符,代表任意类型,你调用时再把真实类型填进去。

MyBatis 搭配 MyBatis-Plus 开发时,Mapper 接口加 @Mapper、启动类配 @MapperScan 扫描,接口继承 BaseMapper 自带单表 CRUD 无需写 SQL,自定义查询方法要在 XML 用同名 id 绑定,namespace 必须写 Mapper 完整类名;resultMap 是手动定义数据库列与实体属性的映射模板,统一复用解决库下划线 / 大写字段和实体驼峰不匹配问题,还支持一对一、一对多复杂关联映射,查询标签指定 resultMap 后 MyBatis 会按模板把数据库 ResultSet 自动封装成 DO 对象,相比 resultType 更稳定易维护,企业项目统一用 BaseResultMap 做基础字段映射,新增字段仅需修改一处映射即可全局生效。

MyBatis 原生 Mapper 本质是“接口方法 + XML SQL 绑定”:不继承 BaseMapper 时,Mapper 接口里声明的每个数据库方法都要自己写 SQL,比如 User selectById(Long id);,就要在 XML 里写 <select id="selectById">...</select>,并且 XML 的 namespace 必须等于 Mapper 接口完整类名。使用 MyBatis-Plus 后,Mapper 接口继承 BaseMapper<Entity>,就自动拥有单表 CRUD,比如 selectByIdinsertupdateByIddeleteById,这些不用写 XML;只有复杂查询或多表查询,才需要自己在接口里声明方法,并在 XML 中用同名 id 绑定 SQL。

// 不继承 BaseMapper:所有方法基本都要自己写 SQL  
@Mapper  
public interface UserMapper {  
User selectById(Long id);  
int insert(User user);  
}
<!-- namespace = Mapper完整类名,id = 接口方法名 -->
<mapper namespace="com.xxx.mapper.UserMapper">
    <select id="selectById" resultType="User">
        select * from user where id = #{id}
    </select>

    <insert id="insert">
        insert into user(name, age) values(#{name}, #{age})
    </insert>
</mapper>


// 继承 BaseMapper:基础 CRUD 不用写 SQL
@Mapper
public interface UserMapper extends BaseMapper<User> {
    // 复杂查询才自己声明
    List<User> selectActiveUsers();
}
<mapper namespace="com.xxx.mapper.UserMapper">
    <!-- 只写自定义方法的 SQL -->
    <select id="selectActiveUsers" resultType="User">
        select * from user where status = 1
    </select>
</mapper>

一句话:不继承 BaseMapper,CRUD 都要自己声明方法并写 XML;继承 BaseMapper<Entity> 后,单表 CRUD 由 MyBatis-Plus 提供,XML 只负责自定义复杂 SQL。

UserInfo userInfoVO = UserConvertor.INSTANCE.mapToVo(register); 是 MapStruct 对象转换工具的标准用法。UserConvertor 是一个自己写的用注解标记的转换接口,INSTANCE 是它自动生成的全局单例,整个项目统一通过它调用转换方法。mapToVo(register) 接收从数据库查出的 UserDO 对象,将其转换成前端需要的 UserInfo 视图对象(VO)。MapStruct 在编译时会自动生成实现类,直接写死属性赋值的 set 代码,既省去手动编写的繁琐工作,性能也和手写 get/set 完全一样(无反射损耗),还能按配置自动忽略密码等敏感字段。这是企业级 Java 项目中最主流、最规范的 VO/DO/DTO 转换方案。

Pinia store 是前端全局状态管理仓库。普通组件的数据只属于当前页面,而 token、用户信息、登录状态、根目录 ID 等数据需要多个页面共享,所以放在 stores/user.js 中统一管理。useUserStore() 用来获取这个用户仓库,页面通过 userStore.registerAction() 发起注册流程。store 内部再调用 api/auth.jsregister() 方法请求后端,并把后端响应整理成 { success, data/message } 返回给页面。这样页面只负责 UI 和交互,store 负责用户相关状态和业务动作,api 文件负责具体 HTTP 请求。

ElMessage 是 Element Plus 提供的全局消息提示组件,用来给用户显示操作结果。它不会参与业务逻辑,也不会请求后端,只负责页面提示。例如注册成功时调用 ElMessage.success('注册成功,请登录'),注册失败时调用 ElMessage.error(result.message || '注册失败')。它比原生 alert() 更适合项目 UI,因为不会阻塞页面操作,样式也和 Element Plus 保持一致。

前端的 userStore 本身只存储「当前登录用户」,不支持同时登录多个用户(这是前端会话的特性,浏览器同一域名下 localStorage/cookie 是共享的)。如果要支持 “多用户切换”,需要额外设计。注册时用户还没登录,userStore.token 是空,不会往请求头塞 token,后端不会校验登录态,正常接收注册参数。

const token = ref(Cookies.get('token') || '') 的作用是从浏览器 Cookie 中读取登录 token,并保存成 Vue 的响应式变量。登录成功后,后端返回 token,前端把 token 存起来;页面刷新后,前端可以再次从 Cookie 里读出 token,恢复登录状态。如果 Cookie 里没有 token,就说明当前可能未登录,因此默认给空字符串。

请求头里带 token 是因为 HTTP 请求本身无状态,后端不能天然知道当前请求属于哪个用户。前端访问需要登录的接口时,会把 token 放到请求头中一起发给后端;后端从请求头取出 token,解析并校验它是否合法、是否过期、Redis 登录态是否存在,从而确认当前请求对应的用户身份。

鉴权就是后端根据 token 判断用户是否有资格访问当前接口或资源。它通常包括两层含义:认证是判断“你是谁,是否已经登录”;授权是判断“你有没有权限做这件事”。例如登录后才能上传文件,这是认证;只能删除自己的文件,不能删除别人的文件,这是授权。缺少 token 或 token 无效通常返回 401,登录了但权限不够通常返回 403。

token 是“登录通行证”的统称,JWT 是一种“里面自带用户信息和签名”的 token。

在微服务项目中,每个微服务都是独立启动的 Spring Boot 应用,因此每个服务都有自己独立的 Spring 容器。Bean 不是所有服务共享的,而是由各自服务启动时根据 scanBasePackages 扫描生成。networkdisk-auth 只扫描 com.disk.auth,所以它的容器里没有 com.disk.web.aspect.UserInfoLoginAspect 这个切面 Bean,AuthController 不会被该切面拦截。networkdisk-user 扫描 com.disk,所以会扫描到公共模块里的 UserInfoLoginAspect,该切面进入 user 服务容器后,才会拦截匹配的 UserController 方法。结论:切面是否生效是以微服务自己的 Spring 容器为单位判断的,哪个服务容器里有这个切面 Bean,哪个服务的匹配方法才会被拦截。

Vue Router 根据当前 URL 自动控制 router-view 渲染哪个组件

Vite Proxy 是开发环境的前端代理。它不是后端服务,不是 Tomcat,不是 Spring MVC。它只是帮前端把 /api 请求转发到后端,主要解决跨域和接口地址统一问题。生产环境一般不会用 Vite Proxy。生产环境通常是:Vue 项目 build 成静态文件→ Nginx 部署前端页面→ Nginx 或 Gateway 转发 API 请求

Tomcat 和 Spring MVC 的分工要分开记:Tomcat 是 Spring Boot 内置的 Web 服务器,负责监听端口、接收 HTTP 请求、发送 HTTP 响应;Spring MVC 运行在 Tomcat 里面,核心入口是 DispatcherServlet,负责根据 URL 找到 Controller 方法、绑定参数、执行方法、把 Java 返回对象转换成 JSON。返回时不是“Spring MVC 直接发回浏览器”,而是 Controller 返回对象 → Spring MVC 转 JSON → Tomcat 写回 HTTP 响应 → 代理/Gateway/Vite 原路返回浏览器。

@Configuration用来标记当前类为Spring配置类,项目启动时Spring会主动扫描该类,类中带有@Bean注解的方法会自动执行并把生成的对象交给Spring容器全局管理,像你这个SaToken鉴权过滤器,缺少@Configuration就算方法写了@Bean也不会被识别、过滤器无法创建,登录和权限拦截功能直接失效,配置类就是集中统一初始化各类框架组件、全局规则的载体,省去手动创建注册对象的繁琐代码。

面包屑(Breadcrumb) 是前端项目中常用的层级辅助导航组件,名称源自童话《汉赛尔与格莱特》中沿路撒面包屑标记返程路径的典故。它的核心作用是展示当前页面在系统整体架构中的层级位置,用户可点击任意上级层级快速跳转,无需逐级返回,避免在深层页面中迷失位置。在文件管理类业务场景中,面包屑对应文件夹的完整层级路径,一般由后端根据当前目录ID向上递归查询父级生成路径数组,前端渲染为「根目录 > 一级目录 > 当前目录」的分隔式导航条;实现时通常与路由/目录切换逻辑绑定,同时需处理请求竞态问题,防止旧请求返回的路径数据覆盖当前最新状态。

遮罩(也叫蒙层、遮罩层):弹出弹窗 / 抽屉的时候,后面整个页面会蒙上一层半透明的黑灰色,把底下的内容盖住、点不动,让用户只能操作眼前的弹窗 / 抽屉。 抽屉(侧边抽屉):从屏幕左边 / 右边滑出来的一块面板,就像拉家里的抽屉一样:拉出来用,用完推回去,不占主页面空间。

type 是前端业务自定义的查询参数名,不是框架自带;地址栏的 ?key=value 格式由 Vue Router 自动拼接生成,而后端接口的 HTTP 请求格式由 axios 统一封装保证,中间用 typeMap 做前后端参数的语义转换,实现解耦和易维护。

枚举(Enum)就是固定几个值的常量集合枚举常量本身就是全局唯一的单例,不需要你 new 创建。枚举就是"提前定义好的固定选项",直接 类名.常量名 拿来用就行。

事件冒泡: 在 DOM 中,点击一个元素,事件会层层向上传递

用户点击了按钮
    ↓
按钮自己先处理(执行 @click)
    ↓
事件冒泡到父元素(执行父元素的 @click)
    ↓
事件冒泡到爷爷元素(执行爷爷元素的 @click)
    ↓
...一直冒泡到最外层

但事件冒泡的"父元素"指的是 DOM 结构上的所有祖先元素,不仅仅是直接的父元素。事件冒泡会一直向上传到所有祖先元素,只要祖先元素绑定了任何事件(clickdblclickmousedown 等),都会被触发,直到遇到 .stop 阻止或到达顶层。

一般是:后端某一层发现异常后直接 throw,比如 throw new FileException(FILE_NOT_EXIT),当前方法会立刻中断,异常会沿着 Service -> Controller -> Spring MVC 往上冒泡;Controller 没有 try catch 时,就会被项目里的 @ControllerAdvice 全局异常处理器接住。因为 FileException 继承自 BizException,所以会进入业务异常处理方法,被包装成统一 JSON 返回,例如 {"code":"FILE_NOT_EXIT","success":false,"message":"文件不存在","data":null}。普通接口前端 axios 拦截器看到 success=false 会弹出 messagePromise.reject(new Error(message));如果是预览/下载这种 responseType: 'blob' 的文件流接口,错误 JSON 会先被当成 Blob 接收,前端再判断 blob.type 是否是 application/json,是的话读出文本、解析 JSON,再抛出 Error("文件不存在") 给页面提示。

Vue 生命周期是 Vue 框架给组件预设的"人生阶段"——从创建、挂载到更新、销毁,每个阶段 Vue 会自动调用对应的钩子函数,让你插入自己的代码。onMounted 是其中一个钩子,意思是 “组件的 DOM 已经渲染到页面上了”,这时候你才能安全地操作 DOM 元素(比如初始化一个上传组件)。它不是 Vue 最早执行的——最早的是 onBeforeCreateonCreated,但那些时候 DOM 还不存在,所以操作 DOM 必须等 onMounted

挂载(Mount) 就是 Vue 把虚拟 DOM 变成真实 DOM 并插入到页面上的过程。你可以理解为:Vue 先在心里画好了图(虚拟 DOM),然后 onMounted 时真正在网页上"贴"了出来,这时候你才能伸手摸到这些元素。

组件代码分两阶段:

  1. 编译渲染阶段:解析 template 模板、生成页面 DOM 元素,此时 DOM 只存在内存里,页面上看不见;
  2. 挂载 onMounted:内存中的 DOM 真正插入真实网页文档,用户能看到页面按钮、弹窗盒子。 onMounted = 挂载完成钩子 只有页面全部 DOM 渲染插入完毕,才会执行里面的代码。

拖拽事件不是 Vue 现成的——dragdropdragover 这些是浏览器原生的 DOM 事件,任何网页都能用。但 Vue 提供了 v-on(简写 @)语法糖让你方便地绑定,比如 @drop="handleDrop"。不过你代码里的 assignBrowse / assignDrop上传库自己的 API,它内部帮你封装了原生拖拽事件的绑定,不是 Vue 直接提供的。

MD5(f.file, (e, md5) => {})第二个参数就是把一段函数代码,当成普通参数传给 MD5 工具方法,和传数字、字符串本质一样,JS 函数是一等公民,可以随便当参数传递。不是传类,JS 这里传的就是「可执行函数」,业内叫回调函数 callback

:: 是 Java 8 新增的方法引用语法,作用是简化 Lambda 表达式,直接引用已有静态方法、实例方法、构造器,不用手写完整箭头函数;分四种用法:对象::实例方法、类::静态方法、类::实例方法、类::new,本质等价于对应 Lambda,仅做代码简写,不改变原有执行逻辑。

MyBatis-Plus 的 LambdaQueryWrapper 里 实体类::getter方法 是方法引用的专用场景,通过实体的 getter 方法自动反射拿到对应数据库字段名,替代手写字符串字段名(如 “create_user”),优势是编译期校验,写错方法名直接爆红报错,杜绝手写字符串带来的字段名拼写错误,不用硬编码数据库列名

Multipart 是 HTTP 协议规定的一种"多部分打包格式",解决了一次请求只能发一种类型数据的限制。它用 boundary 分隔线把多个"部分"拼在一起,每部分自带标签说明自己是什么字段、什么文件名、什么数据类型,既能装文本键值对,也能装二进制文件流。前端通过 enctype="multipart/form-data"FormData 对象生成这种格式,后端用 MultipartFile 解析拆包,最终提取出完整的文件和参数。

文件上传的本质是网络环境下跨主机的数据持久化传输,其核心在于解决三个工程问题:分片策略(将大文件切分为可独立传输的单元以降低单次失败成本)、流控机制(通过管道化 I/O 避免内存溢出并支持背压调节)、以及容错与一致性(断点续传保证可靠性,最终校验保证完整性)。 具体而言,客户端作为数据生产者,通过 HTTP 协议将文件编码为 multipart/form-data 格式,以 InputStream 或 NIO Channel 的方式逐段泵送;服务端作为消费者,通过流式读取将数据直接写入存储介质,而非在内存中构建完整副本。这一过程中,管道化传输实现了零拷贝或近零拷贝transferTo/transferFrom),将内核缓冲区数据直接定向到目标文件描述符,绕过用户态中间缓冲,显著降低 CPU 参与度和内存占用。 对于高并发或大文件场景,还需引入分片上传秒传机制:前者将文件切分为固定大小的 Chunk,配合服务端维护的已上传分片索引实现断点续传;后者通过文件哈希(如 MD5/SHA-256)在传输前进行存在性校验,避免冗余网络传输。最终,文件实体落盘至本地存储或直传至对象存储(OSS/S3),而关系型数据库仅持久化元信息(文件名、路径、哈希、大小、状态),实现存储与索引的解耦。

流是代码里的"水龙头引用",不是数据本身。每次请求建一个水龙头,用完就关。getInputStream() 是"拿到水龙头"(输入视角:数据流进你的代码),setInputStream() 是"把水龙头交给别人"(输出视角:流出你的代码)。真正的数据搬运是后面循环 read()write() 的过程。

一、输入流、输出流,以谁为视角,一句话完整说明 输入流与输出流完全站在当前Java应用程序内存的角度划分,输入流InputStream代表外部数据流入程序内存,用于执行read读取操作,数据源可以是磁盘文件、前端上传的二进制、网络数据;输出流OutputStream代表程序内存的数据向外流出,用于执行write写入操作,数据会写到磁盘文件、压缩包、HTTP响应等外部载体,简单区分规则就是数据进内存为输入流,数据出内存为输出流。

二、流为什么能当作参数传入其他方法,一段话完整说明 流本质是实现统一顶层接口的资源对象,和普通实体、字符串一样具备作为方法入参的能力,首先依靠InputStream、OutputStream顶层抽象实现多态,不管是文件流、上传分片流、压缩流都能统一传入同一套工具方法,做到代码复用;其次直接传递流不会复制二进制数据,相比转成byte数组大幅节省内存开销;最后实现分层解耦,上层代码只负责打开获取流,底层工具方法只负责读写处理,不用关心流的来源、文件路径,职责拆分更清晰。

三、什么是传统BIO,一段话完整说明 BIO即同步阻塞IO,是Java最基础的IO读写模型,我们前面分段缓冲读写、一次性读取文件、普通压缩代码都属于BIO,它的特点是读写操作同步执行,调用read方法时若无可用数据,当前线程会阻塞卡死等待,数据流转必须经过JVM堆内存完成两次CPU拷贝,代码简单易上手,但高并发处理大量文件或超大文件时,会出现线程资源占用高、内存开销大、性能损耗严重的问题,适合小文件、低并发简单场景。

四、什么是NIO,一段话完整说明 NIO是Java提供的同步非阻塞IO模型,核心由通道Channel、缓冲区Buffer、选择器Selector三大组件构成,通道替代传统单向流支持双向读写,所有数据交互都依托缓冲区完成,非阻塞特性让线程无数据可读时立刻返回不阻塞等待,同时提供transferTo、transferFrom专属通道传输API实现零拷贝,大幅降低CPU与内存损耗,既能处理海量并发网络请求,也能优化超大文件读写场景,适合文件下载、大批量文件传输等对性能要求高的业务。

五、什么是零拷贝,一段话完整说明 零拷贝是NIO通道提供的高性能数据传输机制,传统BIO读写文件需要在内核缓冲区与JVM堆内存之间发生两次CPU数据拷贝,消耗大量CPU资源与堆内存,而transferTo底层直接在内核态完成数据转发,跳过整个应用JVM堆内存,磁盘文件数据从内核缓冲区直接传输到目标输出内核缓冲区,减少一次CPU拷贝和系统调用次数,极大提升大文件读写、下载、转发的速度,缺点是仅适合完整文件一次性传输,无法用于分片追加写入这类分段处理场景。

业务表一般都是提前建好的。这项目里的 shareshare_fileuser_filefile 这些表,不是在 Java Service 里临时创建的,而是提前用 SQL 脚本建表。

数据库表是真正存储数据的地方;DO 实体类是 Java 中用来表示数据库一行记录的对象。 在 MyBatis-Plus 中,实体类通过 @TableName 指定自己对应哪张表,例如 @TableName(“share”) 表示 ShareDO 对应数据库里的 share 表。实体字段通过 @TableField 指定对应哪一列,例如 shareName 字段上的 @TableField(“share_name”) 表示 Java 字段 shareName 对应数据库列 share_name。 DO 通常还会继承公共实体类,例如 BaseEntity,所以一个 DO 的完整字段 = BaseEntity 公共字段 + 自己声明的业务字段。插入数据时,MyBatis-Plus 会把这些字段一起作为数据库字段处理。项目里还有 DataObjectHandler 自动填充器,会在插入时自动填充创建时间、修改时间、逻辑删除标识、乐观锁版本号等公共字段。 Mapper 是和数据库打交道的接口。ShareMapper extends BaseMapper<ShareDO> 表示 ShareMapper 操作 ShareDO;而 ShareDO 对应 share 表,所以 ShareMapper 实际操作的是 share 表。BaseMapper 是 MyBatis-Plus 提供的基础 Mapper,内置 insert、deleteById、updateById、selectById、selectList 等常见 CRUD 方法,所以 Mapper 里即使不写方法,也已经有基础数据库操作能力。 Service 是业务层,用来组织业务逻辑并调用 Mapper。ShareServiceImpl extends ServiceImpl<ShareMapper, ShareDO> 表示这个 Service 使用 ShareMapper,并且主要操作 ShareDO。因为 ShareDO 对应 share 表,所以在这个 Service 里调用 save(shareDO),可以理解为底层调用 ShareMapper 的 insert 能力,最终往 share 表插入一条数据。 MyBatis-Plus 是 MyBatis 的增强工具,它不会替你设计业务表,也不是这里负责建表的工具;表通常提前通过 SQL 脚本创建好。MyBatis-Plus 的作用是根据实体类、Mapper、Service 的泛型关系和注解映射,自动生成常见 SQL,把 Java 对象转换成 INSERT、SELECT、UPDATE、DELETE 等数据库操作。

这个项目里的表不是 Java 运行时创建的,而是提前通过 DDL_network_disk.sql 建好的。Java 代码里的 DO 实体类负责描述“哪个 Java 类对应哪张数据库表”,例如 ShareDO 通过 @TableName("share") 对应 share 表,字段通过 @TableField("share_name") 对应具体数据库列。Mapper 继承 MyBatis-Plus 的 BaseMapper<实体类> 后,就自动拥有基础增删改查能力;ServiceImpl<Mapper, 实体类> 又把这些能力封装成 save()saveBatch() 等方法。所以 save(shareDO) 会根据 ShareDO -> share 的映射自动生成 INSERT INTO share ...,把分享主记录插入 share 表;shareFileService.saveBatch(shareFileDOList) 会根据 ShareFileDO -> share_file 的映射批量生成插入 SQL,把分享和文件的关联关系插入 share_file 表。整个过程中,表结构来自 SQL 脚本,插入数据来自 Java 对象,MyBatis-Plus 负责把对象转换成 SQL 并通过数据库连接写入 MySQL。

Wrapper = SQL 条件构造器它的作用不是执行,而是描述条件。常见写法: queryWrapper.eq("user_id", userId); queryWrapper.like("filename", keyword); queryWrapper.in("file_type", fileTypes); queryWrapper.orderByDesc("gmt_modified"); 对应 SQL:

WHERE user_id = ?
  AND filename LIKE ?
  AND file_type IN (...)
ORDER BY gmt_modified DESC

所以:queryWrapper.eq(...)不是查数据库。它们只是往 queryWrapper 这个对象里继续塞条件。 List<UserFileDO> records = list(queryWrapper);这里的 list(...) 才会真正查数据库。

Wrapper 是 MyBatis-Plus 里的条件构造器,本质是一个“装查询条件的对象”,它本身不会访问数据库,也不会返回查询结果。调用 eqlikeinorderByDesc 这些方法,只是在往 Wrapper 里添加 SQL 条件。真正执行数据库操作的是 ServiceMapper 方法,比如 list(wrapper)getOne(wrapper)selectList(wrapper)。实体类对象表示“一行数据本身”,所以 save(entity) 这种新增操作传实体对象;Wrapper 表示“按什么条件操作数据”,所以查询、条件更新、条件删除经常传 Wrapper。整体流程就是:先创建 Wrapper 组装条件,再把 Wrapper 交给 Service/Mapper,MyBatis-Plus 根据实体映射和 Wrapper 条件生成 SQL,最后由数据库执行并返回结果。

ES 的关键确实是倒排索引。但倒排索引不体现在 search() 方法的业务代码里,而是在 ES 引擎内部完成。项目代码里的 LambdaEsQueryWrapper 只是构造 ES 查询条件,fileEsMapper.selectList(wrapper) 只是把查询发给 ES。真正体现 ES 特性的地方是 UserFileESEntity 上的 @IndexName("user_file_index")filename 字段的 FieldType.TEXT + IK_SMART 分词配置;数据写入 ES 时,ES 会根据这些配置对文件名分词并建立倒排索引,查询时再利用倒排索引快速找到匹配文件。 ES 的 user_file_index 不是每次搜索临时创建的,而是长期存在的搜索索引。MySQL 的 user_file 表数据通过 Canal 同步到 ES 后,ES 会根据 filename 的 TEXT + IK_SMART 配置提前分词并维护倒排索引。用户搜索时只是查询这个已经维护好的索引,因此搜索速度比临时扫描数据库更快。

classpath 不是具体目录名,而是 Java 程序运行时查找类和资源文件的路径集合。Spring Boot 配置里的 classpath:xxx.yml 表示从运行时 classpath 中加载 xxx.yml。Maven 项目里,各模块的 src/main/resources 以及依赖 jar 包中的 resources 都会进入 classpath,所以业务模块可以通过 classpath:datasource.yml 加载公共模块提供的配置文件。

Mapper 是“直接操作某张表的工具”;Service 是“业务层,里面可以调用一个或多个 Mapper/Service”。Controller 通常调 Service,不直接调 Mapper。 为什么 save(shareDO) 可以直接写因为你现在所在的类是 ShareServiceImpl

public class ShareServiceImpl extends ServiceImpl<ShareMapper, ShareDO> implements ShareService {}

这句话的意思是:当前这个 Service 默认使用 ShareMapper,当前这个 Service 默认操作 ShareDO,ShareDO 对应 share 表 所以在 ShareServiceImpl 里面直接写:save(shareDO);其实等价于:this.save(shareDO);再等价理解为:shareMapper.insert(shareDO); 最终就是:INSERT INTO share ...。也就是说,save() 不是凭空来的,它是父类 ServiceImpl 继承下来的方法。 **为什么 list 要写 shareFileService.list(...)因为这里要查的不是 share 表,而是 share_file 表。 这里的list是在public class ShareServiceImpl extends ServiceImpl<ShareMapper, ShareDO> implements ShareService {}里面调用的,这里根据继承,默认操作的是shareMapper和shareDO,ShareDO 自身 @TableName("share") 锁定操作表为 share;所以 this.list()this.save() 全部操作 share 主表。 具体在 listShareFiles 方法里:

List<ShareFileDO> mappings = shareFileService.list(mappingQuery);

shareFileService 对应的是:

public class ShareFileServiceImpl extends ServiceImpl<ShareFileMapper, ShareFileDO> implements ShareFileService {}

所以它默认操作的是:

ShareFileDO -> share_file 表

因此:shareFileService.list(mappingQuery);。等价理解为:

SELECT *
FROM share_file
WHERE share_id = ?
  AND deleted = 0
ORDER BY gmt_create DESC;

如果你在 ShareServiceImpl 里直接写:list(mappingQuery);那它会调用当前 Service 的 list,也就是查 share 表,不是查 share_file 表。 所以区别是:save(shareDO); 默认操作当前 Service 的表,也就是 share 表。 shareFileService.list(mappingQuery);明确操作另一个 Service 的表,也就是 share_file 表。

那什么时候用 Service,什么时候用 Mapper。可以先按这个规则记:

Controller 里:用 Service
Service 里:可以用 Service,也可以用 Mapper
Mapper 里:只写数据库访问

更具体一点:

场景 用什么
前端请求进来,要处理业务 Controller 调 Service
一个操作包含校验、事务、多张表 用 Service
当前类已经继承了 ServiceImpl<Mapper, DO>,操作自己的表 直接用 save/list/getById/update
要操作别的业务表,并且项目里有对应 Service xxxService
只是简单按 ID 查辅助表,没有复杂业务 可以直接用 xxxMapper
需要复杂 SQL、动态 SQL、XML SQL 用 Mapper

Mapper 是数据库访问层,直接对应某张表,负责执行 insert/select/update/delete。Service 是业务层,可以调用一个或多个 Mapper/Service,负责校验、事务、多表操作和业务规则。当前类如果 extends ServiceImpl<ShareMapper, ShareDO>,那么在这个类里直接写 save()list() 默认操作的就是 share 表;如果要操作 share_file 表,就要调用 shareFileService.list();如果只是简单按 ID 查辅助表,也可以直接用 userFileMapper.selectBatchIds()Wrapper 只是查询条件,不执行 SQL。@Around("@annotation(Facade)") 是 AOP 切面,只有方法上加了 @Facade 才会被拦截,主要用于 Dubbo 微服务门面接口,统一做日志、参数校验、异常捕获和响应包装。

不加注解也能写切面拦截,为什么非要 @Facade 切面拦截分两种匹配规则:

  1. 按包 / 类通配拦截(不依赖注解)比如 execution(* com.disk.rpc.facade..*.*(..)),只要在这个包下全部拦截。 弊端:包内工具方法、内部私有方法、测试方法会被无差别拦截;后期拆分门面、新增内部工具类,切面逻辑会乱拦截,容易报错。
  2. 按注解精准拦截(当前项目方案 @annotation(Facade)只有主动打上 @Facade 的门面方法才走切面,精准控制范围:只有对外 Dubbo RPC 暴露的门面方法才需要统一处理;门面类内部工具方法、私有逻辑不打注解,不会进入切面,无多余开销。

核心作用:精准圈定拦截范围,区分「对外 RPC 接口」和「内部普通方法」

@Facade 只是标记,FacadeAspect 才是逻辑载体,二者分工

  1. @Facade 注解:纯标签,无任何业务代码 作用:给方法打标识,告诉切面:这是对外提供 Dubbo 调用的门面接口。
  2. FacadeAspect AOP 切面:统一拦截所有带该标签的方法,封装 RPC 公共横切逻辑 典型切面内置通用逻辑(Dubbo RPC 场景刚需):
    • 统一日志:打印 RPC 入参、出参、耗时、调用方 IP;
    • 全局异常捕获、统一封装 Dubbo 返回 Result;
    • 用户身份透传、上下文 RpcContext 处理;
    • 接口限流、权限校验、埋点统计;
    • pjp.proceed () 放行,执行开发者自己写的业务代码。 二者关系:注解是筛选条件,切面是统一处理逻辑,缺一不可。

和 Dubbo 的关联(关键,普通接口切面不需要这么设计) Dubbo 场景有特殊诉求,才单独抽一套门面切面:

  1. Facade 层 = Dubbo RPC 对外暴露层 项目分层规范:

    • Facade:Dubbo 对外暴露接口,给其他服务远程调用;
    • Service:本地业务实现,内部调用,不对外暴露。 只有 Facade 方法会接收跨服务远程请求,需要统一处理 RPC 相关公共逻辑;本地 Service 方法不需要。
  2. Dubbo 远程调用特有上下文 RpcContext、远程调用方、链路追踪、跨服务异常序列化,只有 RPC 门面才需要处理; 如果不用注解区分,本地 Service 也会执行这套 RPC 逻辑,造成上下文错乱。

  3. 统一对外返回格式 Dubbo 服务对外要固定统一返回体(成功码、失败码、消息),切面统一包装,不用每个门面方法手动 try-catch 封装结果。

HTTP 请求本身是无状态的,后端不会自动记住 “刚才这个浏览器已经验证过提取码了”。要实现 “验证一次就通行”,必须主动做一件事:生成一个临时访问凭证(token),存起来,后续请求让前端带这个凭证。但这个项目没有这么做,而是选择了更简单的方案:

提取码本身就是访问凭证,每次请求都带过来,我每次都比对一次数据库。

这是典型的解决数据库 N+1 查询问题的优化方案,先通过一次批量查询把所有关联文件的完整信息一次性查出来,再在内存中整理成以文件 ID 为键的 Map 映射表,后续遍历分享关联记录时直接从 Map 里按 ID 匹配对应文件信息,把原本 “1 次查关联 + N 次逐行查文件” 的 N+1 次数据库请求,缩减为仅 2 次数据库查询,显著提升查询性能。

axios 及其通过 axios.create() 创建的实例都内置了完整的请求自动处理能力,其中 params 配置项会自动将传入的参数对象序列化为 key=value 格式的查询字符串,拼接在 baseURL 与接口路径组成的完整请求 URL 末尾,同时自动完成特殊字符的 URL 编码,全程无需开发者手动拼接字符串;同类的内置自动处理配置还有很多,比如自动统一拼接请求前缀的 baseURL、超时后自动中断请求的 timeout、自动将响应解析为指定格式的 responseType(如下载场景设为 blob 会直接返回二进制文件对象)、POST 请求中自动将 data 对象转为 JSON 并放入请求体的能力,以及自动控制跨域是否携带 Cookie 的 withCredentials,这些底层 HTTP 协议细节都由 axios 封装完成,只需配置对应字段即可直接生效。

@ConfigurationProperties 本身没有写 application.yml。它不是负责“找文件”的,它只负责:从已经加载好的配置里,找 com.disk.ai.provider 这一段 然后绑定到 AiProviderProperties 类真正决定读哪个 application.yml 的,是 Spring Boot 启动规则。 项目的 AI 服务启动类是: networkdisk-business/networkdisk-ai/src/main/java/com/disk/ai/NetworkdiskAiApplication.java:11

  @SpringBootApplication(scanBasePackages = "com.disk")
  @EnableConfigurationProperties({
          AiProviderProperties.class,
          AiIndexProperties.class,
          PgVectorProperties.class
  })
  public class NetworkdiskAiApplication {

这个服务启动时,Spring Boot 默认会读当前模块的:networkdisk-business/networkdisk-ai/src/main/resources/application.yml:因为它在 networkdisk-ai 模块的 src/main/resources 下面,启动这个模块时会进入 classpath。流程是这样:

  启动 NetworkdiskAiApplication
  ↓
  Spring Boot 自动加载 application.yml
  ↓
  @EnableConfigurationProperties 注册 AiProviderProperties
  ↓
  @ConfigurationProperties(prefix = "com.disk.ai.provider") 找配置前缀
  ↓
  把 yml 里的值塞进 AiProviderProperties 字段

Spring Boot 会自动把 短横线命名 转成 驼峰命名。

并不是“所有注入的都要用 final 修饰”,而是**构造器注入(Constructor Injection)强烈推荐用 final,而字段注入(Field Injection,即 @Autowired 直接写在字段上)**则不能用 final(因为 Spring 是通过反射强行赋值的,final 字段不允许反射修改)

JDBC 是 Java 程序访问数据库的“统一标准”。无论你用的是 MySQL、Oracle、PostgreSQL 还是 SQL Server,只要数据库厂商提供了对应的 JDBC 驱动(Driver),你的 Java 代码就能用同一套 API 去操作它。

JDBC(Java Database Connectivity)是 Java 访问关系型数据库的一套标准接口。它的作用是把 Java 程序和 MySQL、PostgreSQL 等数据库连接起来,让程序能够执行 SQL、传递参数、查询数据并处理返回结果。原理上,Java 代码调用 JDBC 统一定义的 ConnectionPreparedStatementResultSet 等接口,具体数据库厂商提供 JDBC 驱动,把这些调用转换成对应数据库能够理解的网络协议;因此更换数据库时,上层代码通常不需要完全重写。基本用法就是:加载数据库驱动并获取连接 Connection,通过 PreparedStatement 编写和执行 SQL,再通过 ResultSet 读取查询结果,最后关闭资源。实际 Spring Boot 项目中一般不会直接手写完整 JDBC 流程,而是使用 JdbcTemplate、MyBatis 或 JPA,它们底层仍然依赖 JDBC。

Java 业务代码 → JDBC → 数据库,例如MySQL → JDBC → Java 查询结果

MySQL 负责管理数据库,JDBC 负责让 Java 能连接并调用它。MyBatis、JPA、JdbcTemplate 的底层最终也都是通过 JDBC 操作数据库。 JdbcTemplate 是 Spring 对原生 JDBC 的一层封装。原生 JDBC 需要自己获取连接、创建 SQL 对象、处理结果集、捕获异常、关闭资源,代码比较繁琐;JdbcTemplate 帮你处理这些重复工作,你主要负责写 SQL 和参数。JdbcTemplate 的用法就是:先在 Spring Boot 中引入 JDBC 依赖并配置数据库连接,然后在类里注入 JdbcTemplate,直接调用它的方法执行 SQL,例如用 update() 完成新增、修改、删除,用 queryForObject() 查询一条数据,用 query() 查询多条数据;它会自动帮你获取和关闭数据库连接、处理异常,你只需要提供 SQL、参数以及如何把查询结果转换成 Java 对象,例如 jdbcTemplate.update("INSERT INTO user(name) VALUES(?)", name)

JdbcTemplate 是 Spring JDBC 模块提供的轻量级原生 SQL 操作工具,是对原生 JDBC 的封装。

Spring 事务一般是通过代理拦截 public 方法 生效的。saveUserFile(…) 是私有方法:private Long saveUserFile(…),所以它有没有事务,要看调用它的外层方法有没有事务。 TransactionSynchronizationManager.isActualTransactionActive() 不是业务代码,它是 Spring 事务工具类,用来判断“当前线程里有没有 Spring 管理的事务”。

TransactionSynchronizationManager 是 Spring 提供的事务上下文工具类,业务代码可以直接通过类名调用它的方法例如 isActualTransactionActive() 判断当前线程是否存在 Spring 管理的事务,或者通过 registerSynchronization(...) 注册事务提交后的回调。它底层用 ThreadLocal 保存当前线程的事务状态,所以同一个请求调用链中,即使内部方法本身没有 @Transactional,也能感知外层事务是否存在;但这些 ThreadLocal 细节由 Spring 自动维护,业务代码只需要读取状态或注册回调,不应该手动操作。典型用途是:如果当前有事务,就等 afterCommit 后再发送 MQ,避免消费者读到未提交的数据;如果当前没有事务,说明当前数据库操作通常已提交,可以直接执行后续动作。

不同微服务也能监听同一个 RocketMQ topic,前提是它们都连到同一个 RocketMQ NameServer,并且各自配置了对应的 binding。不同微服务可以监听同一个 MQ topic,但 Bean 不通用。每个微服务都有自己的 Spring 容器和自己的 Bean;跨服务靠 RocketMQ topic 通信。相同 group 是负载均衡消费,不同 group 是广播给不同业务各消费一份。

  1. 构建开发流程各个环节的 Agent 和 Skill 的能力 2. 构建多 Agent 协作开发流程和框架的能力 3. 大模型适用场景的判断能力 4. Token 消耗的成本控制能力 5. 业务场景运用 AI 解决方案的想象力

文件操作最重要的概念是这两个:

InputStream  = 输入流 = 读数据,从外面读进程序
OutputStream = 输出流 = 写数据,从程序写出去

方向一定要站在“Java 程序”的角度看:

磁盘文件 -> Java 程序
FileInputStream

Java 程序 -> 磁盘文件
FileOutputStream

Java 程序 -> 内存 byte[]
ByteArrayOutputStream

内存 byte[] -> Java 程序
ByteArrayInputStream

Java 程序 -> 浏览器下载
ServletOutputStream

企业开发里常见的流大概这些:

// 1. 读本地文件,得到字节
byte[] bytes = Files.readAllBytes(Path.of("/tmp/a.pdf"));

小文件最简单就这个。少写很多流代码。

// 2. 上传文件保存到磁盘
try (InputStream in = multipartFile.getInputStream();
     OutputStream out = new FileOutputStream("/tmp/a.pdf")) {
    in.transferTo(out);
}

multipartFile.getInputStream() 是从 HTTP 上传内容里读,FileOutputStream 是写到磁盘。

// 3. 文件下载给浏览器
response.setContentType("application/octet-stream");
try (InputStream in = new FileInputStream(file);
     OutputStream out = response.getOutputStream()) {
    in.transferTo(out);
}

这里是:磁盘文件 -> Java -> HTTP 响应 -> 浏览器

// 4. byte[] 给第三方库读取
byte[] bytes = ...;
try (InputStream in = new ByteArrayInputStream(bytes)) {
    // Tika / POI / 图片库等经常吃 InputStream
}

ByteArrayInputStream 的来源不是磁盘,而是内存里的 byte[]

// 5. 文本文件按行读
try (BufferedReader reader = Files.newBufferedReader(Path.of("a.txt"))) {
    String line;
    while ((line = reader.readLine()) != null) {
        // 处理每一行
    }
}

文本用 Reader,二进制用 InputStream

// 6. 写文本文件
try (BufferedWriter writer = Files.newBufferedWriter(Path.of("a.txt"))) {
    writer.write("hello");
}

文本用 Writer,二进制用 OutputStream

一句话记:

InputStream/OutputStream:处理字节,适合图片、PDF、压缩包、视频、任意文件。
Reader/Writer:处理字符,适合 txt、csv、json、xml 这类文本。

企业开发里你看到“流”,先分清两类就够了:InputStream/OutputStream 是文件/网络的字节流,用来读写数据;stream() 是集合流,用来遍历处理 List。文件流记住“Input 是读进程序,Output 是从程序写出去”;集合流记住“stream 就是更短的 for 循环”。

看集合流只认这几个词:filter 是筛选,map 是转换,toList/collect 是收集结果,forEach 是逐个执行,findFirst 是找一个,anyMatch 是判断有没有。比如 list.stream().map(User::getId).toList() 就是“遍历用户列表,取出每个 id,组成新列表”。

经验规则:简单的筛选、转换、分组用 stream();有复杂 if、try-catch、日志、调用外部接口、需要一步步调试时,用普通 for。能看懂 filter/map/toList,知道它本质就是遍历集合,你在企业代码里看流就够用了。

PostgreSQL 不是 MySQL 的副本。 它是 AI 模块自己的向量库。 启动时建空表。 建索引时才写数据。 数据来源是“文件内容解析 + embedding 模型生成”,不是直接复制 MySQL。

multipart 是 HTTP 一种请求体数据格式,把请求内容拆成多个独立分段(part),每段带自己的头部标识文件名、类型等,专门用来同时传输普通表单文本和二进制文件,常见于文件上传接口。

构造注入就是:一个类依赖什么对象,就把这些对象写在构造方法参数里,Spring 在创建该类时自动传入。当 Spring 创建一个 Bean 时,会尝试解析它构造函数里的依赖参数,并从 Spring 容器中找到能满足这个参数类型的 Bean。

普通对象参数public OrderService(PaymentService paymentService)。Spring理解为:找一个 PaymentService 类型的 Bean。 List 参数public OrderService(List<PaymentService> services) Spring理解为:找出所有 PaymentService 类型的 Bean,组成 List。 例如:

[
    alipayService,
    wechatPayService
]

Map 参数 public OrderService(Map<String, PaymentService> services) Spring理解为:找出所有 PaymentService 类型的 Bean,Bean 名作为 key,Bean 对象作为 value。 例如:

{
    "alipayService"    -> AlipayService对象,
    "wechatPayService" -> WechatPayService对象
}

当一个类构造参数多、参数可选、参数复杂(对象、集合、JSON 配置)时,不用写超长多参数构造方法,分步链式赋值,最后调用build()生成实例。 固定标准格式

对象类.builder()
    .属性1(值)
    .属性2(值)
    .build();
  • builder():静态方法,创建一个建造者中间对象,专门用来配置参数;
  • 中间链式方法:给各个字段赋值;
  • build():组装所有配置,生成最终目标对象类名.builder() 拿到配置构造器,中间链式设置各类属性,最后调用 .build() 把所有配置整合,生成最终可用的对象,这就是建造者模式标准写法。
@AllArgsConstructor 会为 private final List<Tool> tools 生成构造器参数;Spring 创建 JChatMindFactory 时,会从容器中找出所有已经注册为 Bean、且继承/实现了项目 Tool 类型的具体工具对象(如 RAG、终止工具等),自动组装成 List<Tool> 传入构造器并赋值给 tools。final 只表示该列表引用在构造完成后不能换成另一份,不影响 Spring 注入;之后 getToolsByType() 再从这份完整工具列表中按 ToolType 筛选当前 Agent 可用的工具。
字母 全称 含义 大白话解释
S Situation(情境) 当时处于什么背景/环境下 “那时候出了什么问题?”
T Task(任务) 你面临的具体任务/目标是什么 “我要解决什么?”
A Action(行动) 你具体做了什么、怎么做的 “我做了哪些事来解决它?”
R Result(结果) 最终取得了什么成果 “做完之后效果怎么样?”

下面我直接把你那 4 步模板做成一张可打印/可背诵的速查表,面试前扫一眼就能想起来。


八股题通用回答结构 · 速查表

步骤 关键词 核心目标 时间占比 避坑指南 话术模版
下定义 1~2 句话说清“是什么”,直接命中技术本质 10% ❌ 别绕圈子、别讲故事、别加“呃…好像…” “XX 是一种……技术/数据结构/算法,核心目的是……”
讲原理 讲清底层机制(数据结构 / 系统调用 / 源码级逻辑),最好带关键函数或类名 50% ❌ 别只背名词,要讲清楚“数据怎么流转的”<br>❌ 别照抄源码,用自己的话讲 “底层依赖 XX 机制实现。以 XX 为例,流程是:第一步……第二步……”
说场景 说明这项技术解决了什么实际问题,最好关联 Redis / Kafka / 项目经历 25% ❌ 别只说“很多地方都用”,要具体到某个中间件或业务 “这种设计在 XX 场景下特别常见,比如 Kafka 中用来……/我之前的项目中用它来……”
补优缺点 辩证看待,讲出局限性和坑,体现架构师思维 15% ❌ 别只说优点,要讲“什么情况下不能用”<br>❌ 别强行凑缺点 “不过它也不是万能的,主要限制是……(数据不能被修改 / 内存占用大 / 存在死锁风险)”