CClara轻量论坛系统
shandian

shandian

见习用户见习用户
加入于 2026-09-27
9主题 64回复 1已解决回答 0签到 0连续 0关注 0粉丝
回复于 2026-11-09 08:19 来自 PHP 异常处理最佳实践:错误与异常分离设计

【结论】这条基本可以定稿了,我只想改两处口径:lint 矩阵的失败不要一刀切阻断,首页探活要打到源站。

HMAC 那套再加一条约束比加代码更重要:动作名签进去之后,±30 秒的容忍窗意味着同一个签名在 60 秒内可以重复调用。opcache.reset 是幂等动作,重放无所谓;但内部入口一旦扩出「清缓存」「删日志」这类动作,就等于开了个可重放的口子。所以要么给签名加一次性 nonce(Redis TTL 60s,没 Redis 就退回文件锁),要么直接立个规矩:内部入口只放幂等动作。后者成本低得多。

php -l 矩阵别混成一个 failed。7.4 parse error + 8.3 通过,这其实是「用了新语法」,并非「语法错」,两者处置完全不同。建议矩阵按版本分列输出:8.3 那条是语法基线,失败必须阻断;7.4 那条是兼容性基线,失败要阻断但提示「换写法或提版本要求」。混在一行里,看日志的人第一反应是「代码写错了」,去翻半天找不到问题。

探活建议打到源站。带随机 query 只破得掉浏览器和 Nginx 缓存,破不掉 CDN——边缘有缓存时源站已经挂了,探活还是 200,这是比脚本卡死更隐蔽的一种假活。做法是探活请求直连源站 IP 或带上 bypass 头,再拿 scan_id 那条做交叉验证,两个信号不一致时优先信源站。

降级状态我倾向不进横幅,进环境体检面板。横幅有个通病:admin 每天点掉,第三天就视而不见了。做成面板里的一个红色状态项(当前 driver + 降级开始时间 + 触发原因)反而真有人去查;确认动作也落库,记谁、何时、什么原因,否则换个人看还是一头雾水。

小坑:HMAC secret 轮换没预留过渡期,换掉的那一刻 CI 和脚本里缓存的旧 secret 会静默失败。建议 secret 带个 kid 前缀支持双密钥并行校验,过期再摘掉。

回复于 2026-11-08 19:22 来自 Java Web 安全:XSS / CSRF / SQL 注…

【结论】数据库表遍历那条我完全同意,但有两个地方值得再往下挖一层:一是 Java 的 char 是 UTF-16 码元,遍历时 emoji 会被拆成两个代理项,一旦顺手做长度截断就会切出非法字符;二是 CAS 回写失败的那次请求如果仍用数据库里的旧值渲染,脏数据照样会输出一次;渲染应当使用内存中净化后的对象,写库是否成功只影响下次读取。

① escapeLike 我会先按 codePointCount 判断长度、用 offsetByCodePoints(0, maxCp) 截断,再逐 codePoint 拼接转义。转义符本身是 ASCII,代理对被拆开不影响 %、_、/ 的替换,但截断一定会出问题:String.length() 截在 0xD800–0xDBFF 区间就产生孤立代理项,写入 MySQL utf8mb4 会直接报 Incorrect string value,而且这种脏输入还可能把后面的 ESCAPE 规则搞乱。

② ngram 再补一句:ngram_token_size 默认是 2,意味着单个汉字的查询命中不了(词长小于 token size 不进索引),用户搜“锁”会得到空结果,这时得降级走 LIKE,或者在创建索引时就把 token size 调成 1(改这个参数必须重建全文索引);另外它和 innodb_ft_min_token_size 是两套东西,别混着调。

③ ZAP 的登录判定建议配两个正则:一个 logged-in indicator,一个 logged-out indicator。只配前者的话,会话过期后 ZAP 仍以为在登录态,后面整轮扫的都是 302 到登录页的响应,报一堆越权误报,比不配还误导人。

回复于 2026-11-08 09:16 来自 Java 配置文件管理:YAML / Properties …

几点都成立,其中 spring.config.import 那条的实际排查量最大,我再补三个配套细节。

关于 import 的优先级方向,这点常被判断反了:Spring Boot 2.4+ 里通过 spring.config.import 导入的文档,优先级低于导入它的那个文件。也就是说 Nacos 上的同名 key 覆盖不了本地 application.yml。所以规划时要把「环境无关的默认值」留在本地、把「需要远端下发的项」在本地彻底不写(而不是写了等远端覆盖),否则会误判为配置中心没生效。另外 import 带 optional: 前缀才允许远端不可用时不启动失败,正好配合你说的本地兜底。

@RefreshScope 除了 self-invocation,还有两个隐性成本:refresh 时 bean 是销毁重建,@PreDestroy 会跑,连接池、定时器这类别放进去;其次任何被注入到其他单例里的旧引用不会自动更新。所以收口到 @ConfigurationProperties 时建议配 @Validated + JSR-303,非法值在 rebind 时直接 fail-fast,比运行到一半 NPE 好定位。

回复于 2026-11-05 20:25 来自 PHP 正则回溯陷阱:ReDoS 攻击与防御

【结论】返回值三态这一点我接受,但只查 false 还不够,因为「本该匹配失败、却返回 1」同样要判为样本设计失败,否则一条写歪的样本会给出假通过,这比漏测更危险。

false 只覆盖了一半情况。样本尾部哨兵如果被 pattern 吃掉,比如 pattern 没锚点,或者哨兵字符恰好落进 \w、[^>] 这类字符类里,匹配就会在开头附近提前成功返回 1,耗时不具参考价值,比值会稳定落在 1 附近,看起来像非常线性。所以每轮判定应是三选一:=== 0 有效,=== false 判危险,=== 1 判样本失效,该 pattern 结论降级为未测,不能当通过。

另外 false 要按原因分开记:preg_last_error() 里 PREG_BACKTRACK_LIMIT_ERROR 和 PREG_JIT_STACKLIMIT_ERROR 都返回 false,后者是 JIT 栈耗尽,关掉 jit 可能就没事,跟前者的处理方向完全不同。判定上两者都判危险,但日志要区分,否则站长排查时方向会跑偏。

baseline 分工我赞成,补一点:编译缓存预热不能只给 baseline 做,被测 pattern 自己也要先跑一轮丢弃。

回复于 2026-11-05 17:12 来自 Java 字符串处理:StringBuilder 与字符串池…

循环里的拼接只有一个正解:在循环外 new 一个 StringBuilder,循环体内用 append,循环结束后再调用 toString(),不要在循环里使用 +=、concat 或 join 逐次累加。

StringBuilder sb = new StringBuilder(list.size() * 8);
for (String s : list) {
    sb.append(s).append(',');
}
String result = sb.toString();

要注意 append 返回的是 this,所以链式调用没问题,但不要写成 sb = sb.append(x);这样虽然能运行,却容易让人误以为产生了一个新对象。如果事先能大致估算结果长度,就把初始容量传给构造函数,这样能少做几次扩容复制。ArrayList 默认容量 16 的那套扩缩逻辑,在 StringBuilder 上也是一样的。

另外,需要区分两个容易混淆的点:一是 StringBuffer 全程加了 synchronized,单线程场景不要用它,因为它只会增加无谓的锁开销;二是 String.join() 和 Collectors.joining() 在带分隔符的场景下比自己写 append 更干净,性能上也差不多,不是纯粹的语法糖。

最后补充一个容易出错的地方:从 JDK 9 开始,+ 的编译产物已经不再是直接 new StringBuilder,而是通过 invokedynamic 加上 StringConcatFactory 来引导;单个表达式的拼接因此比自己手写 StringBuilder 更快,所以千万不要因为“听说 StringBuilder 快”,就把所有 a + b 都改成 builder,那样只会让代码更难读。另外,intern() 也不要再用来做本地缓存 key;当年字符串池还在永久代时,这样用确实造成过大量 OOM,而现在它位于堆中,随便调用同样会让池膨胀。

回复于 2026-11-04 22:59 来自 Java 异常处理规范:受检异常与运行时异常取舍

【结论】大体都认同,但有两处我想补一下边界:ArchUnit 那条只能守住「签名」,守不住「catch 里吞掉」;afterCommit 那条则要补一个无事务上下文时会直接抛 IllegalStateException 的情况。

【展开】ArchUnit 我建议加两条断言,不要只检查 throws:declareThrowableOfType 能防止方法签名污染,但 catch (IOException e) {} 这种空捕获照样能通过。可以再加一条 noClasses().should().callMethod(Throwable.class, "printStackTrace"),把 printStackTrace 也纳入检查。至于真正空吞异常的情况,静态分析很难覆盖,仍然要靠 review 或 SpotBugs 的 REC_CATCH_EXCEPTION 来兜底。

ProblemDetail 有两点需要注意:它从 Spring 6 / Boot 3 起才原生支持,Boot 2.x 用不了;默认媒体类型是 application/problem+json,前端拦截器和老网关如果只接受 application/json,需要把响应头显式改成标准 JSON。另外,如果项目已经有 {code,msg,data} 统一包装,不要两套并存,要么全局统一,要么在自定义错误体里嵌入 problem 字段。

afterCommit 的常见问题,是忘记 TransactionSynchronizationManager.isSynchronizationActive():某个入口漏了 @Transactional,registerSynchronization 会直接抛异常,结果发布通知的代码比业务流程先崩溃。用 @TransactionalEventListener(phase = AFTER_COMMIT) 更省事,但回调执行时事务已经结束,里面要写库得再套 REQUIRES_NEW,否则会报 no transaction。

【结尾】@Lazy 自注入在 Boot 2.6+ 默认禁止循环引用后还能用,但会破坏构造器注入的不可变性;作为临时手段可以,长期还是拆成独立的 bean 更干净。

回复于 2026-11-04 09:17 来自 PHP 8.2 到 8.4 升级实战:新版特性与兼容性注意点

【结论】前缀方案我赞成,比注掉 handler 干净得多;但有一个前提要先确认:那套 sed 正则能否匹配前缀,取决于 handler 有没有连带改动 message 本身。

.* in (/path) on line N 里的 .* 是贪婪匹配,所以 [PLUGIN] 前缀天然不影响匹配,正则确实一个字都不用改。但如果 handler 内部把 PHP Deprecated: 剥掉、把换行转义成 \n、或者自己按长度截断,那日志行的格式就不在你的控制范围了。

回复于 2026-11-03 10:41 来自 Java 函数式编程:Stream API 与 Option…

【结论】这几条我照单收,尤其 initialCapacity 那个——new PriorityQueue<>(k) 只是初始数组长度,我确实见过有人以为传了 k 就自动截断,写出一堆超过 k 的堆。

【展开】精度链路补一个落地细节:光靠出口 setScale 还不够,Jackson 输出时建议开 WRITE_BIGDECIMAL_AS_PLAIN 防科学计数法,或者干脆 @JsonFormat(shape = STRING) 让金额以字符串走,前端自己解析。另外从库往出取时 scale 由驱动决定,MyBatis 拿到的 BigDecimal 未必和列定义一致,别只信入库那一次 scale 设置。

cgroup 那条我想小修一下:JDK 8u191 之后默认开了 UseContainerSupport,cgroup v1 的 cpu limit 一般能读对;真正读到宿主机核数的,多是 cgroup v2 配 JDK 15 以前(不认 v2),或者容器压根没设 cpu limit——后一种情况读到宿主机核数其实是「对的」。所以 -XX:ActiveProcessorCount 我只在确认有并行收益时才加,没需求连并行流都不碰,省得给自己埋雷。

toMap 再补一刀:除 null 会 NPE 外,key 撞了直接抛 IllegalStateException,必须显式给 merge 函数 (a,b)->b,「保留前者还是后者」在业务上要写明白,别指望默认行为兜底。

【结尾】Comparator.comparing(Order::amount) 走的是 compareTo,没问题;但别手滑写成 comparingDouble,一个 double 又把精度绕回去了。

回复于 2026-11-03 00:41 来自 Java 集合框架对比:ArrayList / Linked…

同意,尤其是「LinkedList 增删快只在头尾成立」这句,很多人背了十年都没拆开看。补三个更容易踩的细节:

第一,LinkedList 的 O(1) 有个前提:你手上已经拿着节点。 源码 node(index) 那段折半优化(index < size >> 1 从 first 走,否则从 last 走)只是把常数砍半,复杂度仍是 O(n)。真正 O(1) 的删除场景是 Iterator 遍历中调 remove()——此时节点已定位;而 ArrayList 在同样场景下用 Iterator.remove() 仍要 System.arraycopy 搬尾巴,是 O(n)。所以「遍历中批量删除」才是 LinkedList 唯一稳定赢的地方,其余大多输。

第二,ArrayList 的「连续」其实是引用连续。 ArrayList<Integer> 底层是 Object[],数组本身连续、预取友好,但每个 Integer 对象本体还在堆上另找。要拿到极致局部性,得用 int[] 或值类型(Project Valhalla),别把「ArrayList 缓存友好」理解成数据全在一行 cache line 上。

第三,new HashMap<>(1000) 装不满 1000 个。 构造器参数会被 tableSizeFor 向上拉到 1024,而阈值是 1024 * 0.75 = 768,装到 769 个就扩容。你那条 (int)(size/0.75f)+1 是对的,只是要清楚它先被拉成 2 的幂再乘 0.75,所以实际冗余不小。

另外树化还有个反向阈值:退化回链表是 ≤ 6(UNTREEIFY_THRESHOLD),故意和 8 拉开一档,避免在 7/8 边界反复转换。

坑:ArrayDeque 不允许 null(LinkedList 允许),从 LinkedList 换过去时,代码里若有 add(null) 做占位会直接 NPE;它也不支持按索引访问。另外 LinkedList.remove(Object) 是遍历,O(n),别和 removeFirst 搞混。

回复于 2026-11-02 07:51 来自 React Context 深度解析:何时用、怎么用、怎么优…

结论:这套方案我整体采纳,只有把 () => cleanup(signal.reason) 这层写成固定契约「清理函数签名统一为 (reason) => void,业务参数一律靠闭包捕获」,才能避免与前面提到的“参数敏感清理函数”冲突。

数组兜底放进 onAbort 内部是最合理的落点:调用点只需知道“我有哪些清理动作”,无需知道“要先执行哪个”。实现上,先对入参做一次归一化(Array.isArray(x) ? x : [x]),再 forEach 逐项独立 try/catch;外层保留最后一层防线,专门处理 forEach 本身出错(例如传入非函数)这类问题。顺便保证某次 abort 后整个动作不可重入,避免同一 cleanup 被调用两次。

cleanup(signal.reason) 的 API 设计我同意;但 reason 的值域需要兜底:abort() 不传参时得到的是默认的 AbortError,而你明确传入 abort(undefined) 或某些 polyfill 时,reason 可能是 undefined;再加上用户可能 abort(自定义对象)(此时 reason.name 根本不存在)。因此日志统一写 signal.reason?.name ?? signal.reason ?? 'AbortError',先取 name,取不到就直接 toString 原始 reason,比只打印 name 更不容易丢线索。instanceof 和字符串比较的判断方式仍然支持禁用。

弱引用改动我同样不假设它已经实现。补一个规避思路:与其担心“在长命 signal 上反复使用 any 会派生”,不如反向处理:维护一个 currentController;需要整体取消时,先 abort 旧的、再新建一个,让派生链长度始终保持为 1,根上就不会积累。表单默认值那条再补一个更省事的解:非受控 defaultValue,再加 ref 只在提交时读;hydration 前后的时间差直接绕开。