面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 05月29日 01:38

DNS 是什么?域名解析的递归查询和迭代查询有什么区别?

DNS(Domain Name System)是将域名映射为 IP 地址的分布式数据库系统,是互联网基础设施的核心组件。当浏览器访问域名时,解析流程为:浏览器缓存 → 系统缓存(hosts)→ 本地 DNS 服务器 → 根域名服务器 → 顶级域服务器 → 权威 DNS 服务器,逐级查询直到获得最终 IP。其中客户端到本地 DNS 是递归查询(服务器代为完成全部查询),本地 DNS 到根/TLD/权威服务器是迭代查询(每级只返回下一级地址,由本地 DNS 继续追问)。追问常见的 DNS 记录类型有哪些?A 记录映射域名到 IPv4,AAAA 到 IPv6,CNAME 将域名指向另一个域名(别名),MX 指定邮件服务器,NS 指定域名服务器,TXT 用于验证和配置(如 SPF、DKIM)。TTL 在 DNS 中起什么作用?TTL(Time To Live)决定解析结果的缓存有效期。短 TTL 可加快 DNS 变更生效(如故障切换),但增加查询量;长 TTL 减少查询但变更传播慢。CDN 切换节点时需要权衡 TTL 设置。DNS 劫持是什么?DNSSEC 如何防护?DNS 劫持是中间人篡改 DNS 响应返回恶意 IP。DNSSEC 通过对 DNS 响应数字签名验证数据来源和完整性,客户端可验证响应未被篡改。CDN 如何利用 CNAME 实现就近访问?用户域名 CNAME 到 CDN 域名后,CDN 的权威 DNS 根据用户 IP 返回最近的边缘节点 IP。部分服务商采用 CNAME Flattening,在权威 DNS 层直接返回 A 记录,避免 CNAME 链过长影响性能。写段代码# 常用 DNS 查询命令dig example.com A # 查 A 记录dig example.com CNAME # 查 CNAMEnslookup example.com # 简单查询host -t MX example.com # 查邮件记录
服务端阅读 05月29日 01:38

Zustand 中间件怎么使用?有哪些内置中间件?

Zustand 中间件以函数组合方式包裹 create 的回调,从内到外依次嵌套。内置三个核心中间件:persist(状态持久化到 localStorage/sessionStorage)、devtools(接入 Redux DevTools 调试)、immer(简化不可变更新,可直接写 state.user.name = 'new')。组合顺序:devtools 在最外层,persist 在内层,中间件顺序影响 set/get 的拦截链。追问persist 的 partialize 怎么过滤不需要持久化的字段?partialize: (state) => ({ user: state.user }) 只持久化 user,token 等敏感或临时字段不会写入存储。反序列化时缺失字段会使用 create 中的初始值填充。immer 中间件解决了什么问题?React 要求状态不可变更新,深层嵌套需逐层展开 {…s, user: {…s.user, name: 'new'}},代码冗长易出错。immer 让你直接赋值 state.user.name = 'new',内部通过 Proxy 生成新对象。中间件组合顺序有影响吗?有。devtools(persist(fn)) 中 persist 在内层,持久化后的状态变化才会被 devtools 捕获;反过来的话 devtools 记录的是持久化前。一般推荐 devtools 在外、persist 在内。如何自定义中间件?中间件本质是高阶函数:(config) => (set, get, api) => config(fnSet, fnGet, api),在 fnSet/fnGet 中插入自定义逻辑(如日志、节流、权限校验),然后调用原 set/get。persist 持久化的状态怎么版本迁移?persist 配置中提供 migrate 函数:migrate: (persisted, version) => { if (version === 0) return { …persisted, newField: 'default' }; return persisted; },配合 version 字段标识当前版本,自动执行迁移。写段代码import { create } from 'zustand'import { persist, devtools } from 'zustand/middleware'const useStore = create( devtools( persist( (set) => ({ token: '', setToken: (t: string) => set({ token: t }), }), { name: 'auth', partialize: (s) => ({ token: s.token }) } ), { name: 'AuthStore' } ))
服务端阅读 05月29日 01:38

Babel 是什么?它的编译流程和 polyfill 方案有哪些?

Babel 是 JavaScript 转译器,将 ES6+ 语法转为向后兼容的 ES5 代码,确保在旧浏览器中正常运行。编译流程分三步:parse(@babel/parser 将源码转 AST)→ transform(插件遍历 AST 并修改节点)→ generate(@babel/generator 将 AST 还原为代码)。注意 Babel 只转换语法,不补齐新 API(如 Promise、Array.includes),需要 polyfill 方案配合。追问Babel 的 plugin 和 preset 是什么关系?plugin 是最小转换单元(如 @babel/plugin-transform-arrow-functions),preset 是插件集合(如 @babel/preset-env 包含所有 ES6+ 转换插件)。执行顺序:plugins 先于 presets,plugins 正序执行,presets 逆序执行。@babel/preset-env 的工作原理是什么?根据 browserslist 配置的目标环境,按需引入转换插件和 polyfill,避免对已支持语法的多余转换。配合 useBuiltIns: 'usage' 可实现按需注入 polyfill。polyfill 方案有哪些?core-js 和 transform-runtime 有什么区别?core-js 直接在全局注入缺失 API,适合应用项目;@babel/plugin-transform-runtime 将 polyfill 抽离为模块引用(不污染全局),适合库开发,还能避免 helpers 重复打包。Babel 能替代 Webpack 吗?不能。Babel 只做语法转译,不做模块打包、代码分割、Tree Shaking、资源处理,这些是 bundler 的职责。两者配合使用。写段代码// babel.config.jsmodule.exports = { presets: [ ['@babel/preset-env', { targets: '> 0.25%, not dead', useBuiltIns: 'usage', corejs: 3 }] ], plugins: ['@babel/plugin-transform-runtime']};
服务端阅读 05月29日 01:38

Spring Boot 中如何实现异步编程?

Spring Boot 通过 @EnableAsync + @Async 实现声明式异步编程。在配置类上标注 @EnableAsync 开启支持,在方法上标注 @Async 即可在独立线程执行;默认使用 SimpleAsyncTaskExecutor(每次新建线程),生产环境应自定义 ThreadPoolTaskExecutor 并通过 @Async("executorName") 指定线程池。有返回值的方法返回 CompletableFuture,调用方可通过 future.get() 或 CompletableFuture.allOf() 组合多个异步结果。异常处理方面,返回 CompletableFuture 的方法异常会传播到 future,void 方法需实现 AsyncUncaughtExceptionHandler。注意同类内部调用 @Async 方法不生效(绕过代理)。追问ThreadPoolTaskExecutor 的核心参数如何设置? IO 密集型核心线程数可设为 CPU 核数 * 2,队列容量适当放大;CPU 密集型核心线程数为 CPU 核数 + 1,队列不宜过大;拒绝策略推荐 CallerRunsPolicy 由提交线程执行降速。@Async 方法为什么同类调用不生效?如何解决? @Async 依赖 AOP 代理,this.method() 绕过代理直接调用原始方法;解决方案是注入自身代理 @Lazy private XxxService self 后通过 self.method() 调用。CompletableFuture 的 thenCombine 和 thenCompose 有何区别? thenCombine 将两个独立异步结果合并(BiFunction),thenCompose 用于异步结果串联(flatMap 语义,前一步结果决定后一步操作)。@Async 和 @Transactional 一起使用有什么陷阱? 事务上下文绑定 ThreadLocal,异步方法在新线程执行导致事务不传播;应在异步方法内部调用另一个 Bean 的事务方法,而非叠加注解。如何将 RequestContext 传播到异步线程? 使用 TaskDecorator 在任务提交时捕获主线程的 RequestAttributes,在异步线程执行前设置,执行后清除。写段代码@EnableAsync@Configurationpublic class AsyncConfig implements AsyncConfigurer { @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor ex = new ThreadPoolTaskExecutor(); ex.setCorePoolSize(5); ex.setMaxPoolSize(20); ex.setQueueCapacity(100); ex.setThreadNamePrefix("async-"); ex.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); ex.initialize(); return ex; }}
服务端阅读 05月29日 01:38

Zustand 中如何用 TypeScript 定义 Store 类型?

定义一个包含状态和方法的 interface,作为 create 的泛型参数传入即可:create((set, get) => ({…}))。TypeScript 会自动推断 set 回调中 state 的类型,方法参数也能正确约束。关键点:状态和方法写在同一个 interface 中,方法参数可引用自身类型(如 setUser: (user: StoreState['user']) => void)。追问中间件会改变 store 类型,怎么处理类型组合?persist/devtools 等中间件会向 store 注入额外属性(如 persist.clearStorage),需定义扩展类型 type Store = StoreState & StorePersist,再用 create() 包裹。zustand v4+ 的中间件泛型签名已支持自动推断,大部分场景无需手动拼接。StateCreator 泛型怎么用?StateCreator 用于拆分 store 逻辑时约束每个切片的类型,Mutators 描述中间件链对 set/get 的改写,U 是未包装的原始类型。手动组合切片时需要用到。selector 的返回类型怎么保证?useStore(s => s.count) 自动推断为 number。自定义 selector 返回派生值时,TypeScript 能推断返回类型;如果需要显式标注,写 useStore((s) => s.items.length)。泛型 store 怎么定义?function createSlice() 返回 StateCreator>,通过泛型参数让切片适配不同实体(如 createSlice()、createSlice()),避免为每个实体写重复逻辑。set 的 partial 参数类型为什么不是 Partial?set 接受 Partial 或 (state) => Partial,但嵌套对象是浅合并,深层更新需展开:set(s => ({ user: { …s.user, name: 'new' } })),否则丢失同级字段。写段代码interface BearState { bears: number inc: (n: number) => void}const useBearStore = create<BearState>((set) => ({ bears: 0, inc: (n) => set((s) => ({ bears: s.bears + n })),}))// 组件中const bears = useBearStore((s) => s.bears) // number
服务端阅读 05月29日 01:38

axios 和 fetch 有什么区别?什么时候该用 axios?

axios 是基于 Promise 的 HTTP 客户端,相比原生 fetch 的核心优势在于:请求/响应拦截器(统一添加 token、错误处理)、自动 JSON 转换(fetch 需手动 .json())、请求超时配置(fetch 需封装 AbortController+setTimeout)、上传进度监控、XSRF 防护,以及 4xx/5xx 自动 reject(fetch 只在网络故障时才 reject)。但 fetch 是浏览器原生 API,零体积开销,且正逐步补齐能力(AbortController 已支持取消)。追问axios 的拦截器机制是怎么实现的?内部维护请求和响应两个拦截器数组(数组链),发送请求时按序执行请求拦截器,收到响应后按序执行响应拦截器,本质是 Promise 链式调用。fetch 如何实现请求超时?用 AbortController 创建 signal,配合 setTimeout 调用 controller.abort(),fetch 接收 signal 参数,超时后抛出 AbortError。axios 直接配置 timeout 字段即可。axios 在浏览器和 Node.js 端分别用什么发送请求?浏览器端基于 XMLHttpRequest,Node.js 端基于 http/https 模块,通过适配器模式统一 API。fetch 在 Node 18+ 才原生支持。axios 如何实现 XSRF 防护?读取指定 cookie(默认 XSRF-TOKEN)的值,自动写入请求头(默认 X-XSRF-TOKEN),配合后端双重 cookie 验证机制防跨站请求伪造。写段代码const instance = axios.create({ baseURL: '/api', timeout: 5000,});instance.interceptors.request.use(cfg => { cfg.headers.Authorization = `Bearer ${token}`; return cfg;});instance.interceptors.response.use( res => res.data, err => { if (err.response?.status === 401) redirectToLogin(); });
服务端阅读 05月29日 01:38

Spring Boot 中如何实现多环境配置?

Spring Boot 通过 Profile 机制实现多环境配置。核心做法是创建 application-{profile}.yml 文件(如 application-dev.yml、application-prod.yml),公共配置放 application.yml,环境特有配置放对应 Profile 文件,同名属性 Profile 文件覆盖默认值。激活方式包括配置文件中 spring.profiles.active、启动参数 --spring.profiles.active=prod、环境变量 SPRING_PROFILES_ACTIVE。代码层面用 @Profile 注解按环境条件注册 Bean,Spring Boot 2.4+ 还支持 spring.profiles.group 将多个 Profile 组合激活。配置优先级为:命令行参数 > 环境变量 > Profile 文件 > 默认文件。追问application.yml 和 application-dev.yml 同名属性如何取舍? Profile 文件优先,覆盖默认文件中的同名配置;若同时激活多个 Profile,后激活的覆盖先激活的。@Profile 注解标注在类和方法上有什么区别? 标注在 @Configuration 类上整个配置类按条件加载,标注在 @Bean 方法上只控制单个 Bean 的注册。单文件多文档块(---分隔)和多文件方式如何选择? 多文档块适合配置较少的项目,管理简单;多文件适合配置量大、环境差异明显的项目,避免单文件过长且减少合并冲突。生产环境敏感信息如何保护? 使用环境变量 ${DB_PASSWORD} 引用,或通过 Jasypt 对配置值加密存储为 ENC(密文),运行时由加密器解密。Profile 分组解决了什么问题? 一个环境可能需要同时激活多个维度(如 prod + prod-db + prod-mq),分组将它们聚合为一个名称,简化启动参数。写段代码@Configurationpublic class DataSourceConfig { @Bean @Profile("dev") public DataSource devDs() { return new HikariDataSource(new HikariConfig() {{ setJdbcUrl("jdbc:mysql://localhost/dev"); }}); } @Bean @Profile("prod") public DataSource prodDs() { HikariConfig c = new HikariConfig(); c.setJdbcUrl("jdbc:mysql://prod-db/prod"); c.setUsername(System.getenv("DB_USER")); return new HikariDataSource(c); }}
服务端阅读 05月29日 01:38

Workbox 是什么?它如何简化 Service Worker 的缓存策略?

Workbox 是 Google 推出的 Service Worker 工具库,将常见的缓存策略、路由匹配、预缓存等能力封装为开箱即用的模块,大幅降低 SW 开发复杂度。核心提供三种缓存策略:CacheFirst(缓存优先,适合静态资源)、NetworkFirst(网络优先,适合API数据)、StaleWhileRevalidate(先用缓存再后台更新,适合非关键资源)。配合 workbox-precaching 可在 install 阶段批量预缓存,配合 ExpirationPlugin 可控制缓存条目数和过期时间。追问CacheFirst 和 StaleWhileRevalidate 分别适合什么场景?CacheFirst 适合字体、样式等不变资源,命中缓存直接返回,速度最快;StaleWhileRevalidate 适合图片或第三方脚本,先返回缓存保证响应速度,同时后台更新缓存,下次请求拿到新版本。workbox-precaching 和运行时缓存有什么区别?precache 在 SW install 时一次性缓存资源清单(self._WBMANIFEST),确保离线可用;运行时缓存在 fetch 事件触发时按策略写入,首次请求仍需网络。Workbox 如何与构建工具集成?Webpack 用 workbox-webpack-plugin(GenerateSW 自动生成或 InjectManifest 注入清单);Vite 用 vite-plugin-pwa,在构建时自动生成 SW 并注入运行时缓存配置。Workbox 的 BackgroundSyncPlugin 解决了什么问题?当 POST 请求因离线失败时,自动将请求放入队列,待网络恢复后重试,确保数据不丢失。适用于表单提交、数据上报等场景。写段代码import { registerRoute } from 'workbox-routing';import { CacheFirst, StaleWhileRevalidate } from 'workbox-strategies';registerRoute( ({ request }) => request.destination === 'style', new CacheFirst({ cacheName: 'styles' }));registerRoute( ({ request }) => request.destination === 'image', new StaleWhileRevalidate({ cacheName: 'images' }));
服务端阅读 05月29日 01:38

Zustand 是什么?相比 Redux 有什么优势?

Zustand 是一个极简 React 状态管理库,核心 API 只有 create():传入一个返回状态对象的函数,返回一个 hook,组件通过 const count = useStore(s => s.count) 按需订阅,未变化的部分不会触发重渲染。不需要 Provider 包裹,不需要 reducer/action 分发,setState 直接更新。体积约 1KB,零依赖。追问Zustand 的 selector 如何避免不必要的重渲染?useStore(selector) 只订阅 selector 返回的切片,内部用 Object.is 浅比较判断是否变化。引用类型可传第二个参数 shallow 比较函数,或用 useShallow。和 Jotai/Recoil 的原子化方案有什么区别?Zustand 是单一 store(可拆分),状态集中管理;Jotai/Recoil 是原子化,每个状态独立。Zustand 更适合关联性强的状态,原子化适合独立派生状态。没有 Provider 怎么实现组件间共享状态?Zustand store 本质是一个模块级闭包,所有引用同一 useStore 的组件共享同一个状态引用,不依赖 React Context 传递。create() 返回的 hook 能在组件外使用吗?可以。useStore.getState() 获取快照、useStore.setState() 更新状态,均可在非组件代码(如 axios 拦截器、WebSocket 回调)中使用,这是相比 useContext 的显著优势。多 store 还是单 store?Zustand 不限制,实践中按领域拆分多个 store 更常见,避免单个 store 臃肿,也便于按需加载和测试。写段代码import { create } from 'zustand'const useStore = create((set) => ({ count: 0, inc: () => set((s) => ({ count: s.count + 1 })),}))function Counter() { const count = useStore((s) => s.count) return <button onClick={useStore.getState().inc}>{count}</button>}
服务端阅读 05月29日 01:37

Spring Boot 中如何实现全局异常处理?

Spring Boot 通过 @RestControllerAdvice + @ExceptionHandler 实现全局异常处理。前者是 AOP 组件,拦截所有 Controller 抛出的异常;后者按异常类型匹配处理方法,返回统一的响应体。典型做法是定义自定义 BusinessException 携带错误码和消息,在异常处理类中分别处理业务异常、参数校验异常(MethodArgumentNotValidException)和兜底异常(Exception),对外返回结构化的错误响应,对内记录日志并隐藏堆栈细节。RFC 7807 的 application/problem+json 是业界推荐的错误响应格式。追问@RestControllerAdvice 和 @ControllerAdvice 有何区别? 前者是后者的 @ResponseBody 组合注解,方法返回值直接序列化为 JSON;后者适用于返回 ModelAndView 的传统 MVC 场景。多个 @ExceptionHandler 匹配同一个异常时如何选择? Spring 选择异常类型最具体的那个方法,即继承链中最靠近抛出异常类型的处理器优先。如何处理 404 异常? 默认 Spring Boot 不抛出 404 异常而是返回白标签页,需设置 throw-exception-if-no-handler-found=true 并关闭 add-mappings=false,再用 @ExceptionHandler(NoHandlerFoundException.class) 捕获。BusinessException 为什么用 RuntimeException 而非 Checked Exception? 避免 try-catch 侵入业务代码,全局处理器统一兜底,保持代码整洁。参数校验异常 MethodArgumentNotValidException 和 ConstraintViolationException 分别在何时触发? 前者发生在 @RequestBody + @Valid 校验请求体时,后者发生在 @RequestParam + @Validated 校验单参数时。写段代码@RestControllerAdvicepublic class GlobalExHandler { @ExceptionHandler(BusinessException.class) public ResponseEntity<Result<Void>> onBiz(BusinessException e) { return ResponseEntity.badRequest().body(Result.error(e.getCode(), e.getMessage())); } @ExceptionHandler(Exception.class) public ResponseEntity<Result<Void>> onEx(Exception e) { return ResponseEntity.status(500).body(Result.error(500, "系统繁忙")); }}
服务端阅读 05月29日 01:37

Service Worker 是什么?它如何实现离线缓存和后台同步?

Service Worker 是运行在浏览器后台的独立 JS 线程,充当应用与网络之间的代理服务器。它能拦截所有网络请求、管理缓存资源,从而实现离线访问、推送通知和后台同步等能力,是 PWA 的核心技术。其生命周期为 register → install → activate → fetch,浏览器会在空闲时自动终止 SW 线程以节省资源。SW 无法直接操作 DOM,只能通过 postMessage 与主线程通信,且必须在 HTTPS 环境下运行。追问SW 的 install 和 activate 阶段分别适合做什么?install 阶段适合预缓存关键资源(App Shell),调用 event.waitUntil() 确保资源缓存完成后才完成安装;activate 阶段适合清理旧版本缓存,避免残留数据冲突。SW 如何拦截和处理网络请求?通过监听 fetch 事件,可以决定从缓存返回、从网络请求或组合策略(如 Stale-While-Revalidate)。event.respondWith() 用于返回自定义响应。为什么 SW 必须在 HTTPS 下运行?因为 SW 能拦截和篡改所有网络请求,若在 HTTP 下运行,中间人攻击者可注入恶意 SW 劫持全部流量。localhost 是唯一例外。SW 与 Web Worker 有什么区别?Web Worker 由页面创建且随页面销毁,用于 CPU 密集计算;SW 生命周期独立于页面,由浏览器管理,专门处理网络代理和后台任务。写段代码self.addEventListener('install', (e) => { e.waitUntil( caches.open('v1').then(c => c.addAll(['/index.html', '/app.js'])) );});self.addEventListener('fetch', (e) => { e.respondWith( caches.match(e.request).then(r => r || fetch(e.request)) );});
服务端阅读 05月29日 01:37

Spring Boot 如何整合 MyBatis 进行数据库操作?

引入 mybatis-spring-boot-starter 后,通过 @MapperScan 扫描接口、XML 或注解编写 SQL 即可完成整合。核心三要素:Mapper 接口(定义方法签名)、映射文件(编写 SQL 与 ResultMap)、自动配置(驼峰映射、数据源)。简单 CRUD 用注解(@Select/@Insert),复杂动态 SQL 用 XML(where/set/foreach)。PageHelper.startPage 即可实现分页,@Transactional 管理事务。追问@Mapper 和 @MapperScan 有什么区别?@Mapper 逐个标注接口,适合 Mapper 少的场景;@MapperScan 在启动类统一指定包路径,批量注册,推荐使用。两者二选一,不可重复注册。#{} 和 ${} 的区别是什么?{} 使用预编译参数(PreparedStatement 占位符),防止 SQL 注入;${} 直接字符串拼接,有注入风险,仅用于表名、列名等不可预编译的位置。ResultMap 和 ResultType 怎么选?字段名与属性一致时用 resultType 自动映射(配合 map-underscore-to-camel-case);不一致或有关联查询(association/collection)时必须用 resultMap 手动映射。动态 SQL 的 where 标签解决了什么问题?自动去除首个多余 AND/OR,并在条件全空时不生成 WHERE 子句,避免语法错误。set 标签同理,自动去除末尾多余逗号。PageHelper 分页的原理和坑是什么?PageHelper 基于 MyBatis Interceptor 拦截 SQL,自动拼接 LIMIT。坑:startPage 必须紧挨查询语句,中间不能有其他查询,否则分页错乱;只对紧跟的第一条查询生效。写段代码@SpringBootApplication@MapperScan("com.example.mapper")public class App { public static void main(String[] args) { SpringApplication.run(App.class, args); } }// XML 动态查询<select id="selectByCondition" resultMap="BaseMap"> SELECT * FROM user <where> <if test="name != null">AND name LIKE CONCAT('%',#{name},'%')</if> </where></select>
服务端阅读 05月29日 01:37

Spring Boot 中如何实现安全认证?

Spring Boot 通过 spring-boot-starter-security 集成 Spring Security,核心流程为:请求进入 SecurityFilterChain,依次经过认证过滤器(如 UsernamePasswordAuthenticationFilter 或自定义 JWT Filter),由 AuthenticationManager 委托 UserDetailsService 加载用户并校验凭证,认证成功后将 Authentication 存入 SecurityContextHolder;授权阶段通过 @PreAuthorize 或 URL 规则判断权限。JWT 场景下需自定义 Filter 从 Header 提取 Token 并验证,同时将 SessionCreationPolicy 设为 STATELESS。追问SecurityFilterChain 的执行顺序如何控制? 通过 http.addFilterBefore()/after() 指定过滤器位置,Spring Security 内置过滤器有固定顺序(如 UsernamePasswordAuthenticationFilter 位于 BasicAuthenticationFilter 之前)。UserDetailsService 和 UserDetails 的职责分别是什么? UserDetailsService 负责从数据源加载用户信息,UserDetails 是用户信息的载体接口,包含密码、权限和账户状态。JWT 无状态方案下如何实现 Token 注销? 可维护黑名单(Redis 存储已注销 Token 直至过期)或采用短期 Token + Refresh Token 轮换机制。@PreAuthorize 和 URL 授权规则各自适合什么场景? URL 规则适合粗粒度的路径级控制,@PreAuthorize 适合方法级细粒度控制,支持 SpEL 表达式如 @userSecurity.canAccess(authentication, #id)。为什么密码必须用 BCrypt 而非 MD5? BCrypt 内置盐值且计算成本可调,抗彩虹表和暴力破解;MD5 是快速哈希,易被暴力穷举。写段代码@BeanSecurityFilterChain chain(HttpSecurity http) throws Exception { http.csrf(c -> c.disable()) .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(a -> a .requestMatchers("/api/auth/**").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN") .anyRequest().authenticated()) .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); return http.build();}
服务端阅读 05月29日 01:37

Spring Boot 微服务如何实现服务注册与发现?

服务注册与发现是微服务架构的基础设施:服务启动时将自身地址注册到注册中心,消费方从注册中心拉取可用实例列表并调用。主流方案有 Eureka(AP、已停维)、Nacos(AP/CP 可切换、国产生态)、Consul(CP、云原生友好)。核心流程三步:注册(实例启动时上报 IP/端口)、心跳(定时续约保活)、拉取(客户端缓存实例列表并定时刷新)。Nacos 是当前新项目首选,临时实例走客户端心跳,持久实例走服务端探活,还内置配置中心。追问Eureka 自我保护机制触发后,实例下线能否被感知?不能立即感知。自我保护模式下 Eureka 不再剔除过期实例,客户端可能拿到已下线的地址,需配合 Ribbon 重试或熔断兜底。Nacos 临时实例和持久实例的区别是什么?临时实例(ephemeral=true)由客户端发心跳,不续约则自动剔除,适合微服务;持久实例由服务端主动探活,不会自动删除,适合数据库等基础设施。客户端发现与服务端发现有什么区别?客户端发现:消费方从注册中心拉取列表,本地负载均衡(Eureka/Nacos);服务端发现:请求先到网关/代理,由代理侧转发(Consul + Nginx)。前者少一跳,后者解耦消费方。注册中心选型时 CAP 如何取舍?Eureka 牺牲一致性保可用(AP),适合网络分区时仍需可用的场景;Consul/ZK 保一致性(CP),适合不能容忍脏数据的场景;Nacos 支持按实例切换 AP/CP。优雅下线如何避免请求打到已注销实例?先从注册中心注销,再等待正在处理的请求完成(如 5s),最后关闭服务。Spring Boot 可监听 ContextClosedEvent 触发注销,配合 sleep 等待在途请求。写段代码@SpringBootApplication@EnableDiscoveryClientpublic class OrderApp { public static void main(String[] args) { SpringApplication.run(OrderApp.class, args); } }@FeignClient(name = "user-service")public interface UserClient { @GetMapping("/users/{id}") User getById(@PathVariable Long id);}
服务端阅读 05月29日 01:37

Spring Boot Starter 的作用和原理是什么?

Starter 是 Spring Boot 提供的依赖聚合与自动配置的组合机制。它将某项功能所需的所有依赖打包成一个 POM 依赖描述符,同时通过 @EnableAutoConfiguration 扫描 META-INF/spring.factories(或 2.7+ 的 AutoConfiguration.imports)中注册的自动配置类,配合 @ConditionalOnClass、@ConditionalOnMissingBean 等条件注解,实现按需装配 Bean。因此引入一个 starter 即可获得完整的依赖集合和零配置的功能启用。追问@ConditionalOnClass 和 @ConditionalOnMissingBean 分别解决什么问题? 前者在 classpath 存在指定类时才装配,避免缺少依赖导致启动失败;后者在容器中无同名 Bean 时才创建,允许用户覆盖默认配置。官方 starter 和第三方 starter 的命名规范有何不同? 官方为 spring-boot-starter-*,第三方为 *-spring-boot-starter,反过来便于区分来源。Spring Boot 2.7 后为什么用 AutoConfiguration.imports 替代 spring.factories? spring.factories 职责过重,AutoConfiguration.imports 专用于自动配置注册,加载更高效且语义更清晰。自定义 starter 时 @ConfigurationProperties 有什么作用? 将 application.yml 中以指定前缀开头的属性映射到 Java 对象,实现外部化配置的类型安全绑定。Starter 依赖传递和 BOM 版本管理是什么关系? Starter 聚合依赖但不管理版本,版本由 spring-boot-dependencies BOM 统一控制,确保兼容性。写段代码@Configuration@ConditionalOnClass(DataSource.class)@EnableConfigurationProperties(MyProps.class)public class MyAutoConfiguration { @Bean @ConditionalOnMissingBean public MyService myService(MyProps p) { return new MyService(p.getHost(), p.getPort()); }}
服务端阅读 05月29日 01:22

React Query 性能优化的常见瓶颈和解决方案有哪些?

React Query 最大性能陷阱是组件过度渲染:query 数据变化时所有订阅组件都重渲染。解法是用 select 提取组件关心的子字段,select 返回值用浅比较去重,相等则跳过渲染。第二陷阱是缓存策略不当:staleTime 控制数据何时标记为过期触发重新请求(默认0即立即过期),gcTime(v5 前叫 cacheTime)控制未使用的缓存何时被垃圾回收(默认5分钟)。不常变的数据应设较长 staleTime 避免无谓请求。第三是 queryKey 设计:key 变化就触发新请求,key 中包含频繁变化的值(如时间戳)会导致缓存失效。queryKey 应稳定且分层:['users', 'list', { status: 'active' }]。追问staleTime 和 gcTime 有什么区别?staleTime 决定数据是否需要重新获取(过期前用缓存,过期后下次挂载或窗口聚焦时 refetch);gcTime 决定缓存数据在内存中保留多久,观察者归零后开始倒计时,到期彻底删除。一个管新鲜度,一个管生命周期。select 怎么避免不必要的渲染?select 每次查询都会执行,但只有返回值与上次浅比较不同时才触发渲染。返回新对象/数组每次都是新引用会失效,需确保返回原始值或用结构化分享的子集。useInfiniteQuery 如何优化?每页数据独立缓存,默认所有页面变化都触发渲染。可用 select 只取当前视口需要的数据;配合虚拟滚动(如 react-virtual)避免渲染长列表;getNextPageParam 返回 undefined 自动停止加载。如何实现乐观更新?用 useMutation 的 onMutate 在请求发出前乐观修改缓存(queryClient.setQueryData),onError 时用 onMutate 保存的快照回滚,onSettled 时 invalidate 相关 query 保证最终一致。写段代码// select + staleTime 减少渲染和请求const { data: name } = useQuery({ queryKey: ['user', id], queryFn: () => fetchUser(id), staleTime: 5 * 60 * 1000, select: (data) => data.name,});
服务端阅读 05月29日 01:22

SSH 连接失败如何排查?常见原因有哪些?

SSH 故障按报错分四类:Connection refused 说明目标端口未监听(sshd 未运行或防火墙阻断);Connection timeout 说明网络不可达(路由/防火墙丢弃包);Permission denied 是认证失败(密钥不匹配或权限错误);Host key verification failed 是 knownhosts 中记录的指纹与服务器当前密钥不一致。排查第一步始终是 ssh -vvv 看详细握手过程,日志会精确显示卡在哪个阶段。密钥权限是最常见的低级错误:私钥必须 600,.ssh 目录 700,authorizedkeys 600,权限过宽 sshd 会拒绝认证。追问ssh -vvv 输出中各阶段分别对应什么?三个 v 递增详细度。-v 显示连接建立、密钥交换、认证尝试;-vv 增加配置解析和 IO 细节;-vvv 再增加包级调试。重点关注 debug1: Authentications that can continue 和 debug1: Offering public key 两行,前者看服务端支持哪些认证方式,后者看客户端尝试了哪些密钥。连接频繁断开怎么解决?通常是 NAT/防火墙空闲连接超时淘汰。客户端配置 ServerAliveInterval 60(每60秒发心跳),服务端配置 ClientAliveInterval 300。autossh 可自动重连断开的会话。known_hosts 冲突一定是重装系统吗?不一定。IP 被复用、服务器更新了主机密钥、或中间人攻击都会导致。确认安全后用 ssh-keygen -R host 删除旧记录,不可盲目忽略否则失去 MITM 保护。如何不用密码只允许密钥登录?服务端 /etc/ssh/sshd_config 设置 PasswordAuthentication no 和 PubkeyAuthentication yes,改完 sshd -t 验证语法再 systemctl restart sshd。写段代码# 客户端 SSH 配置 ~/.ssh/configHost myserver HostName 10.0.0.1 User deploy Port 2222 IdentityFile ~/.ssh/deploy_key ServerAliveInterval 60
服务端阅读 05月29日 01:22

Cookie 和 Session 有什么区别?何时用 Cookie,何时用 Session?

Cookie 存在客户端浏览器,每次请求自动携带,上限约 4KB,可被用户查看和篡改;Session 存在服务端(内存/Redis/数据库),客户端只持有 Session ID(通常通过 Cookie 传递),数据大小无硬性限制,安全性更高。核心区别是状态存储位置:Cookie 是客户端状态,Session 是服务端状态。选择依据:非敏感偏好数据(主题、语言)用 Cookie;登录态、权限等敏感数据用 Session。Session 的局限在于服务端需存储状态,分布式场景需用 Redis 等集中式存储共享 Session,否则粘性Session限制了水平扩展。JWT 是第三条路:将状态编码进 Token 本身,服务端无状态验证签名即可,但 Token 体积大且无法主动失效。追问Session ID 被窃取会怎样?如何防范?攻击者可用截获的 Session ID 冒充用户(Session 劫持)。防范:Cookie 标记 HttpOnly+Secure,绑定 IP/UA 校验,使用 SameSite 属性,Session 定期轮换 ID,设置合理超时。分布式系统中 Session 如何共享?三种方案:1) 粘性 Session(Nginx ip_hash),简单但不利于负载均衡;2) 集中存储(Redis/Memcached),最常用;3) Session 复制(Tomcat 集群广播),数据量大时性能差。JWT 相比 Session 的优劣?优势:无状态,服务端不存数据,天然支持分布式。劣势:无法主动注销(需黑名单机制又回到有状态),Token 体积大于 Session ID,续期机制复杂(双 Token 方案)。Cookie 被禁用怎么办?Session ID 可通过 URL 参数传递(;jsessionid=xxx),但会暴露在日志和 Referer 中,安全性差。更推荐在登录页提示用户启用 Cookie。写段代码// Express 中 Session 存入 Redisconst session = require('express-session');const RedisStore = require('connect-redis');app.use(session({ store: new RedisStore({ client: redisClient }), secret: 'your-secret', resave: false, cookie: { httpOnly: true, secure: true, maxAge: 3600000 }}));
服务端阅读 05月29日 01:21

OpenCV.js 在移动端和 Web 应用中有哪些最佳实践?

OpenCV.js 在移动端和 Web 应用中有广泛的应用,但需要考虑性能、兼容性和用户体验。以下是移动端和 Web 应用的最佳实践:1. 移动端优化策略响应式设计class MobileImageProcessor { constructor() { this.isMobile = this.detectMobile(); this.processingSize = this.getOptimalSize(); } detectMobile() { return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); } getOptimalSize() { if (this.isMobile) { // 移动端使用较小尺寸 return { width: Math.min(window.innerWidth, 640), height: Math.min(window.innerHeight, 480) }; } else { // 桌面端可以使用较大尺寸 return { width: 1280, height: 720 }; } } resizeImage(src) { let dst = new cv.Mat(); try { cv.resize(src, dst, new cv.Size(this.processingSize.width, this.processingSize.height)); return dst; } catch (error) { console.error('Resize error:', error); return src.clone(); } }}触摸事件处理class TouchHandler { constructor(canvasId) { this.canvas = document.getElementById(canvasId); this.setupTouchEvents(); } setupTouchEvents() { let startX, startY; this.canvas.addEventListener('touchstart', (e) => { e.preventDefault(); const touch = e.touches[0]; startX = touch.clientX; startY = touch.clientY; }); this.canvas.addEventListener('touchmove', (e) => { e.preventDefault(); const touch = e.touches[0]; const deltaX = touch.clientX - startX; const deltaY = touch.clientY - startY; // 处理触摸移动 this.handleTouchMove(deltaX, deltaY); startX = touch.clientX; startY = touch.clientY; }); this.canvas.addEventListener('touchend', (e) => { e.preventDefault(); this.handleTouchEnd(); }); } handleTouchMove(deltaX, deltaY) { // 实现触摸移动逻辑 console.log(`Touch move: ${deltaX}, ${deltaY}`); } handleTouchEnd() { // 实现触摸结束逻辑 console.log('Touch end'); }}2. PWA(渐进式 Web 应用)集成Service Worker 缓存 OpenCV.js// sw.jsconst CACHE_NAME = 'opencv-pwa-v1';const urlsToCache = [ '/', '/index.html', 'https://docs.opencv.org/4.8.0/opencv.js'];self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHE_NAME) .then((cache) => cache.addAll(urlsToCache)) );});self.addEventListener('fetch', (event) => { event.respondWith( caches.match(event.request) .then((response) => { if (response) { return response; } return fetch(event.request); }) );});离线支持class OfflineImageProcessor { constructor() { this.isOnline = navigator.onLine; this.setupOfflineSupport(); } setupOfflineSupport() { window.addEventListener('online', () => { this.isOnline = true; console.log('Back online'); }); window.addEventListener('offline', () => { this.isOnline = false; console.log('Gone offline'); }); } async processImage(image) { if (!this.isOnline) { // 离线模式:使用本地处理 return this.processLocally(image); } else { // 在线模式:可以选择使用云端处理 return this.processWithFallback(image); } } processLocally(image) { let src = cv.imread(image); let dst = new cv.Mat(); try { cv.cvtColor(src, dst, cv.COLOR_RGBA2GRAY); cv.Canny(dst, dst, 50, 100); return dst; } finally { src.delete(); } } processWithFallback(image) { try { // 尝试云端处理 return this.processCloud(image); } catch (error) { console.warn('Cloud processing failed, falling back to local'); return this.processLocally(image); } }}3. 性能监控和优化实时性能监控class PerformanceMonitor { constructor() { this.metrics = { fps: 0, frameTime: 0, memoryUsage: 0 }; this.frameCount = 0; this.lastTime = performance.now(); this.startMonitoring(); } startMonitoring() { setInterval(() => { this.updateMetrics(); this.displayMetrics(); }, 1000); } updateMetrics() { const currentTime = performance.now(); const deltaTime = currentTime - this.lastTime; this.metrics.fps = Math.round(this.frameCount * 1000 / deltaTime); this.metrics.frameTime = deltaTime / this.frameCount; if (performance.memory) { this.metrics.memoryUsage = Math.round(performance.memory.usedJSHeapSize / 1024 / 1024); } this.frameCount = 0; this.lastTime = currentTime; } recordFrame() { this.frameCount++; } displayMetrics() { console.table(this.metrics); } getMetrics() { return { ...this.metrics }; }}自适应质量调整class AdaptiveQualityProcessor { constructor() { this.quality = 1.0; this.monitor = new PerformanceMonitor(); this.adjustQuality(); } adjustQuality() { setInterval(() => { const metrics = this.monitor.getMetrics(); if (metrics.fps < 20) { // 性能差,降低质量 this.quality = Math.max(0.5, this.quality - 0.1); console.log(`Reducing quality to ${this.quality}`); } else if (metrics.fps > 50 && this.quality < 1.0) { // 性能好,提高质量 this.quality = Math.min(1.0, this.quality + 0.1); console.log(`Increasing quality to ${this.quality}`); } }, 2000); } processImage(src) { let dst = new cv.Mat(); const size = new cv.Size( Math.round(src.cols * this.quality), Math.round(src.rows * this.quality) ); try { cv.resize(src, dst, size); this.monitor.recordFrame(); return dst; } finally { // dst 由调用者负责释放 } }}4. 电池优化电池状态感知class BatteryAwareProcessor { constructor() { this.batteryLevel = 1.0; this.isCharging = false; this.setupBatteryListener(); } setupBatteryListener() { if ('getBattery' in navigator) { navigator.getBattery().then((battery) => { this.batteryLevel = battery.level; this.isCharging = battery.charging; battery.addEventListener('levelchange', () => { this.batteryLevel = battery.level; this.adjustProcessing(); }); battery.addEventListener('chargingchange', () => { this.isCharging = battery.charging; this.adjustProcessing(); }); }); } } adjustProcessing() { if (this.batteryLevel < 0.2 && !this.isCharging) { // 低电量且未充电,降低处理强度 this.setProcessingMode('low'); } else if (this.batteryLevel > 0.5 || this.isCharging) { // 电量充足或正在充电,正常处理 this.setProcessingMode('normal'); } } setProcessingMode(mode) { console.log(`Setting processing mode to: ${mode}`); // 根据模式调整处理参数 }}5. Web Worker 集成后台图像处理// 主线程class WorkerImageProcessor { constructor() { this.worker = new Worker('image-processor-worker.js'); this.pendingTasks = new Map(); this.taskId = 0; } processImage(imageData) { return new Promise((resolve, reject) => { const taskId = this.taskId++; this.pendingTasks.set(taskId, { resolve, reject }); this.worker.postMessage({ taskId, imageData, operation: 'edge-detection' }, [imageData.data.buffer]); }); } processVideoFrame(videoElement) { const canvas = document.createElement('canvas'); canvas.width = videoElement.videoWidth; canvas.height = videoElement.videoHeight; const ctx = canvas.getContext('2d'); ctx.drawImage(videoElement, 0, 0); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); return this.processImage(imageData); }}// image-processor-worker.jsself.onmessage = function(e) { const { taskId, imageData, operation } = e.data; try { let src = cv.matFromImageData(imageData); let dst = new cv.Mat(); switch (operation) { case 'edge-detection': cv.cvtColor(src, dst, cv.COLOR_RGBA2GRAY); cv.Canny(dst, dst, 50, 100); break; case 'blur': cv.GaussianBlur(src, dst, new cv.Size(15, 15), 0); break; } const result = new ImageData( new Uint8ClampedArray(dst.data), dst.cols, dst.rows ); self.postMessage({ taskId, result }, [result.data.buffer]); src.delete(); dst.delete(); } catch (error) { self.postMessage({ taskId, error: error.message }); }};6. 移动端特定优化摄像头访问优化class MobileCameraHandler { constructor() { this.stream = null; this.constraints = this.getOptimalConstraints(); } getOptimalConstraints() { const isMobile = /Android|iPhone|iPad/i.test(navigator.userAgent); if (isMobile) { return { video: { facingMode: 'environment', // 使用后置摄像头 width: { ideal: 640 }, height: { ideal: 480 }, frameRate: { ideal: 30 } }, audio: false }; } else { return { video: { width: { ideal: 1280 }, height: { ideal: 720 }, frameRate: { ideal: 60 } }, audio: false }; } } async startCamera() { try { this.stream = await navigator.mediaDevices.getUserMedia(this.constraints); return this.stream; } catch (error) { console.error('Camera access error:', error); // 降级方案 if (this.constraints.video.width.ideal > 640) { this.constraints.video.width.ideal = 640; this.constraints.video.height.ideal = 480; return this.startCamera(); } throw error; } } stopCamera() { if (this.stream) { this.stream.getTracks().forEach(track => track.stop()); this.stream = null; } }}7. 完整的移动端应用示例class MobileCVApp { constructor() { this.processor = new MobileImageProcessor(); this.camera = new MobileCameraHandler(); this.battery = new BatteryAwareProcessor(); this.monitor = new PerformanceMonitor(); this.isRunning = false; } async init() { await this.camera.startCamera(); this.setupUI(); } setupUI() { const video = document.getElementById('video'); const canvas = document.getElementById('canvas'); video.srcObject = this.camera.stream; video.onloadedmetadata = () => { canvas.width = video.videoWidth; canvas.height = video.videoHeight; this.startProcessing(); }; } startProcessing() { this.isRunning = true; this.processFrame(); } processFrame() { if (!this.isRunning) return; const video = document.getElementById('video'); const canvas = document.getElementById('canvas'); let src = cv.imread(video); let dst = new cv.Mat(); try { // 根据电池状态调整处理 if (this.battery.batteryLevel < 0.2) { cv.resize(src, src, new cv.Size(src.cols / 2, src.rows / 2)); } // 图像处理 cv.cvtColor(src, dst, cv.COLOR_RGBA2GRAY); cv.Canny(dst, dst, 50, 100); cv.imshow(canvas.id, dst); this.monitor.recordFrame(); requestAnimationFrame(() => this.processFrame()); } finally { src.delete(); dst.delete(); } } stop() { this.isRunning = false; this.camera.stopCamera(); }}// 使用const app = new MobileCVApp();app.init();总结移动端和 Web 应用中使用 OpenCV.js 需要考虑:性能优化:降低处理分辨率,使用 Web Worker用户体验:响应式设计,触摸事件处理资源管理:电池优化,内存管理离线支持:PWA 集成,Service Worker 缓存兼容性:检测设备能力,提供降级方案监控和调试:实时性能监控,自适应质量调整通过这些最佳实践,可以在移动端和 Web 应用中提供流畅的 OpenCV.js 体验。
服务端阅读 05月29日 01:21

Session Cookie 和 Persistent Cookie 有什么区别?

Session Cookie 不设 Expires 和 Max-Age 属性,浏览器将其存在内存中,关闭浏览器即消失;Persistent Cookie 设有明确的过期时间,存储在磁盘上,过期前跨会话持久存在。服务端通过 Set-Cookie 响应头控制类型:不设过期属性是 Session Cookie,设 Max-Age 或 Expires 则为 Persistent Cookie。安全层面两者都应标记 HttpOnly 和 Secure,Persistent Cookie 因长期驻留磁盘更易被窃取,敏感数据应避免持久化。SameSite 属性(Strict/Lax/None)对两者同样适用,防止 CSRF 攻击。追问浏览器关闭后 Session Cookie 一定被清除吗?不一定。现代浏览器有恢复会话功能(如 Chrome 的继续浏览上次打开的网页),会将 Session Cookie 写入磁盘以便恢复,实际表现等同于 Persistent Cookie。依赖关闭即清除做安全假设是不可靠的。Max-Age 和 Expires 有什么区别?Max-Age 是相对秒数,从收到 Cookie 时起算;Expires 是绝对时间点,依赖客户端时钟。Max-Age 优先级更高(RFC 6265),两者都不设则为 Session Cookie。Session Cookie 能存多少数据?和 Persistent Cookie 一样受 4KB 限制,区别仅在于生命周期,不在容量。大量数据应改用 localStorage 或 IndexedDB。如何强制 Session Cookie 在标签页关闭时清除?无法可靠实现。可用的替代方案:用 sessionStorage(标签页级)或服务端维护有效期并主动使 Session 失效。写段代码// 服务端设置两种 Cookieres.setHeader('Set-Cookie', [ 'sid=abc123; HttpOnly; Secure; SameSite=Lax', // Session Cookie 'theme=dark; Max-Age=31536000; HttpOnly; SameSite=Lax' // Persistent Cookie]);