点播测试用例与结论汇总（五端）本文档分三章：一、测试矩阵与用例（测什么）；二、通过用例描述（哪些过了）；三、问题用例与异常现象（哪些有问题、现象是什么）。第一章 测试矩阵与用例§1.1 协议ID协议 / 容器reader 路径关联样本P1local file MP4（H.264 + AAC）ffmpeg_readerM1 / M3 / M4 / M9P2HTTPS progressive MP4http_progressive_reader（含 TLS）M1–M11 + M12~M18P3HLS（VOD + 时移已录完段，TS 分片）hls_readerM19~M23（M22 暂无）不测：FLV / RTMP / RTSP / HTTP-FLV（直播范畴）、纯 HTTP 明文（业务侧全站 HTTPS）。§1.2 基础场景 S1–S11组风险场景触发关键验证点L1 资源🟢S1 open / S2 closeprepare(url) / close()状态序列合法、onMediaInfoReady 阈值、含自然 EOS→ENDED→closeL2 播控🟢S3 play / S4 pausestart() / pause()READY→PLAYING + PAUSED→PLAYING；暂停后位置不增长L3 终止🟢S5 stopstop()（三态触发）任意非 IDLE → IDLE，不跳过中间态L4 seek🟡S6 前向 / S7 后向seek(pos±30s)目标 ±100ms；无音频持续丢弃 / 视频无 late drop（I5-A02 基线）L4 seek🔴S8 高频1s 内 5 次同上；竞态 / callback 队列错位L5 流切换🔴S9 switch_toswitchToUrl(new_url)新流自动 start、旧 config 保留；token/generation 失效（fef0764/a42bcda 修复区）L5 流切换🟡S10 reloadreload()起播定位清空、内部状态重置L6 循环🟡S11 loopsetLoop(true) 播至 EOSEOS→seek(0)→继续 PLAYING，至少 3 轮无异常基础矩阵 = 协议 × 场景 = 33 格；五端展开 165 cell。§1.3 扩展场景（业务回归补充）编号约定：U* UI 行为 / LC* 应用生命周期 / N* 网络中断 / E* 错误注入 / D* 长播稳定性 / NET* 倍速与弱网扩展。UI 行为（U 系列）ID风险场景关键判定U1🟡首帧预渲染显示首帧 + 停在暂停态 + 播放后丝滑、首帧无变动U2🟢全屏 / 取消全屏切换无重载、无黑边 / 拉伸、音视频连续U3🟡横竖屏切换自适应、无重载、控件重排U4 / U5🟢seek 到末尾 / 初始正确跳转、自然 EOS 或从头播U6 / U7🟢seeking / switching 状态显示UI 显示对应状态且不卡住应用生命周期（LC 系列）ID风险场景关键判定LC1🟢秒压后台恢复后正常播放、无崩溃LC2🔴长压后台 10min恢复后正常播放、位置 / 倍速 / 音量 config 保留LC3🟡锁屏解锁 1min恢复后正常播放、UI 状态正确网络中断（N 系列，拔网→恢复）ID风险场景关键判定N1🔴断网恢复重连显示 reconnecting、恢复后自动续播、续播首帧防花屏N2🟢断网 seek 恢复恢复后在 seek 目标位置续播倍速与弱网扩展（NET 系列）ID风险子类场景关键判定NET1🟡倍速单档 2x / 1.5x / 0.75x / 0.5x速度符合预期、无音画不同步、变速无顿挫NET2🔴倍速倍速时 seek正确跳转、继续保持倍速（经典坑：被 seek 重置回 1.0x）NET3🟢倍速倍速 + 高频 seek稳定、流畅、无崩溃NET4🟢倍速倍速 + 压后台保留返回后依旧倍速（config 保留）NET5🔴弱网弱网首次起播（限速 200kbps）能缓冲播放、不卡死 BUFFERING > 30sNET6🟡弱网网络链路切换 Wi-Fi ↔ 4G/5G丝滑、无卡顿、IP 切换不破坏 HTTPS sessionNET7🟢弱网弱网 + seek 恢复允许长缓冲，不卡 > 30s，最终回到 PLAYING错误注入（E 系列）ID风险场景关键判定E1🟢错误 URL无法播放、UI 不崩、错误提示友好E2🟢切换错误 URLURL2 切换失败、URL1 继续E3🟢切换空 URL切换被拒、URL1 继续E4🔴不解密播加密UI 正常、无法播放、不崩、不花屏长播稳定性（D 系列）ID风险场景关键判定D1🟡20 分钟连播末段音画同步无漂移、UI 无崩、内存稳定§1.4 弱网 profile 定义与验证profile 类别特征验证方法通过判据weak1_high / weak1_mid / weak1_low带宽 + 丢包curl 测下载速度实测速度 < profile 带宽上限的 1.5×weak2_jitter_low / weak2_jitter_high固定 delayping 测 RTT实测 RTT ≥ 预期 jitter 的 80%（low ≥160ms / high ≥400ms）weak2_sudden1Mbps 基线 + 周期断网curl 测基线 + 等首次断网基线带宽受限，且 sudden-loop.log 出现至少一条 sudden-downNET5~NET7 跑前先用对应 profile 验证方法确认环境真到位（防透明代理 / 网关绕过限速误判）。工具链复用 network_test_matrix.md §2.2。§1.5 样本媒体样本 ID（M）= 媒体素材编号，与协议（P）/ 场景（S*）正交：一个用例 = 协议 × 场景 × 样本。下表逐 ID 给出含义（源：vod_test_matrix.md §样本表 + vod_test_overview.md §2.3.1 / §2.3.2）。业务侧基础（M1–M11，mp4）ID类别含义分辨率B 帧加密地址M1A音画同步测试视频 1（主跑样本）720p有否https://video.aicastech.com/vod/avsync.mp4M2A音画同步测试视频 2720p有否https://video.aicastech.com/vod/avsync2.mp4 ⚠️ 原表链接 typo 指向 avsync.mp4，待确认M3B多 B 帧（seek 最易暴露缺陷）1080p多否https://video.aicastech.com/vod/MBFrames.mp4M4B无 B 帧（对照 M3）1080p无否https://video.aicastech.com/vod/NoBFrame.mp4M5B多分辨率 + 多帧率多–否https://video.aicastech.com/kun/mr_mf_unencrypt.mp4M6C多分辨率 + 多帧率（加密，对照 M5）多–是https://video.aicastech.com/kun/mr_mf_encrypt.mp4M7C单分辨率 + 单帧率（加密）单–是https://video.aicastech.com/kun/sr_sf_encrypt.mp4M8A音乐 MV720p有否https://video.aicastech.com/vod/mv_00.mp4M9A足球比赛长片段1080p无否https://storage.googleapis.com/mp-f3-assets/recordings/2026/05/11/4357106_20260511_202006.mp4M10D仅视频、无音频––是https://video.aicastech.com/kun/only_video_encrypt.mp4M11D仅音频、无视频––是https://video.aicastech.com/kun/only_audio_encrypt.mp4STS 录制扩展（M12–M18，mp4） — 源：STS 录制后端，复用 kun/* 路径ID类别含义分辨率 × 帧率加密大小地址M12BSTS 未加密多×多多 × 多否300 MBhttps://video.aicastech.com/kun/mr_mf_unencrypt.mp4M13FSTS 加密长样本（单帧率解码路径）单 × 单是1.91 GBhttps://video.aicastech.com/kun/sr_sf_encrypt.mp4M14FSTS 加密长样本（多帧率解码路径）单 × 多是1.99 GBhttps://video.aicastech.com/kun/sr_mf_encrypt.mp4M15CSTS 加密多分辨率多 × 单是585 MBhttps://video.aicastech.com/kun/mr_sf_encrypt.mp4M16CSTS 加密多分辨率多帧率多 × 多是710 MBhttps://video.aicastech.com/kun/mr_mf_encrypt.mp4M17DSTS 加密仅视频（边界）多 × –是–https://video.aicastech.com/kun/only_video_encrypt.mp4M18DSTS 加密仅音频（边界）–是–https://video.aicastech.com/kun/only_audio_encrypt.mp4STS 时移 HLS 扩展（M19–M23，TS 分片）ID类别含义分辨率 × 帧率加密地址M19BHLS 未加密多×多多 × 多否https://video.aicastech.com/timeshift/4459572/playlist.m3u8M20CHLS 加密单×单单 × 单是https://video.aicastech.com/timeshift/4426349/playlist.m3u8M21CHLS 加密单×多单 × 多是https://video.aicastech.com/timeshift/3890419/playlist.m3u8（URL 路径与比赛 ID 不一致，以 URL 为准）M22CHLS 加密多×单（暂无源，占位）多 × 单是—M23C/BHLS 加密多×多（S9 × M23 = P0 高风险）多 × 多是https://video.aicastech.com/timeshift/4393495/playlist.m3u8按类别归并：A 音画同步基线（M1/M2/M8/M9）· B 编码特性（M3/M4/M5/M12/M19）· C 加密路径（M6/M7/M15/M16/M20/M21/M23）· D 边界轨道（M10/M11/M17/M18）· E 弱网组合（A/B 类样本 × 6 个弱网 profile，非新样本）· F 长稳定性（M13/M14）。加密样本走 --decrypt=v2:<HEX64> + crypt_version；跨源需白名单（kun/* = 7.0.10241430，recordings/* = 026530e）。M22 暂无源（保留编号占位）；M10/M11 业务侧未提供。另文中出现的 M-HLS（非加密 HLS 主跑）/ M-ext-enc（跨源加密"足球 2"）/ DEC-HLS（加密 HLS）为移动端证据里的别名，对应上表 HLS 与加密类样本。§1.6 风险分级（裁剪依据）🔴 P0 必跑：S8（高频 seek）· S9（switch_to 流切换）· LC2（长压后台）· N1（断网恢复）· NET2（倍速时 seek）· NET5（弱网首播）· E4（不解密播加密）；高风险组合 S9×M15/M16（流切换 × mp4 加密多分辨率）· S9×M23（流切换 × HLS 加密多分辨率）🟡 P1 建议跑：S6（前向 seek）/ S7（后向 seek）/ S10（reload）/ S11（loop）/ U1（首帧预渲染）/ U3（横竖屏切换）/ LC3（锁屏解锁）/ NET1（单档倍速）/ NET6（网络链路切换）/ D1（20 分钟连播）🟢 P2 选跑：其余稳定路径第二章 通过用例描述口径：桌面 / macOS 走 scenario DSL 自动采集（report.json + events.ndjson + 自动 verdict）；iOS 走 Xcode 手动 Run + 人眼观察（无客观指标）。各端通过项以最大 NNN 的 verdict 为准。§2.1 Windows 11 / Linux Ubuntu 26.04 — 桌面基线 33/33 PASS（各端）P1/P2/P3 × S1–S11 全 PASS，error_count 全 0，first_frame_ms 全端 ~100ms。proto通过场景备注P1 localS1–S11 全 PASSlocal indexed MP4 seek ~100ms；S11 loop 30s 样本 ×2 轮事件计数 Win=Lin（2 eos + 2 seek pair + 3 playing）P2 HTTPS MP4S1–S11 全 PASSS8 高频 seek 调超时到 45s 后通过；seek p50 Win ~2.6s / Lin ~3.3s（Pass*）P3 HLS-VODS1–S11 全 PASSS8 段边界 seek ~2.1–2.4s（Pass*）证据：evidence/windows/5e1c9a4/、evidence/linux/5e1c9a4/。长稳定性（D1）：Windows 用 M13（加密长样本 1.91GB）GUI 连播 ~58.8min、M14（1.99GB）headless ~59.3min，全程无崩溃、跑满时长——这一项已覆盖（长样本不缺）。但加密长连播视频丢帧率 ~80%、内存曲线采集失败，属问题项（见 §3.1 VP06）。证据 evidence/windows/5e1c9a4/long_stability/。§2.2 macOS 26.3.1 — 61/61 PASSscenario DSL 自动采集，与桌面同口径，error_count 全 0，first_frame_ms ~100–104ms。Phase内容结果A33-cell 基线 P1/P2/P3 × S1–S1133/33 PASSB4 个 S9 多组合（M1→M3 / M1→M-HLS / M-HLS 自切 / M-HLS→M1）4/4 PASSC24 个加密 case（M-ext-enc + M7/M14/M15/M16/M17/M18 + M-HLS-enc；含 S9×M15/M16/M-HLS-enc P0 组合）24/24 PASS特记：M18 audio-only first_frame_ms=-1 为预期；HLS S8 seek p95 >500ms 标 Pass*。证据：evidence/macos/e093f7b/。§2.3 Android（Xiaomi 23116PN5BC）— 解阻后通过子集demo 修复 92fc074（load 幂等 + quit/set_loop）后跑通的项：协议 / 样本通过场景备注P3 HLS（M-HLS 非加密）S6（前向 seek）/ S7（后向 seek）/ S9（switch_to）/ S10（reload）/ S11（loop）verdict=pass、quit_fired=yes；⚠️ 但 first_frame=no（见 §3，黑屏问题）加密 HLS（DEC-HLS）S3（play）/ S8（高频 seek）/ S9（switch_to）/ S11（loop）verdict=pass，first_frame=yes（加密 HLS 有首帧）错误注入 E 系列E1a（错误 URL）/ E2（切换错误 URL）/ E3（切换空 URL）/ E4-hls（不解密播加密 HLS）错误路径符合预期VP-fix 验证（P2/P3）P2（HTTPS MP4）S3/S6/S7/S9/S10/S11 + P3（HLS）S3/S6/S7/S9/S10logcat 留证，证明 DSL 解阻（06-08）证据：evidence/android/5e1c9a4/、evidence/android/a1538bd-and-vp-fix/。注：P3 非加密虽 verdict=pass，但无首帧出图，属"pass 但黑屏"，实际不可用（§3 VP-AND06）。§2.4 iOS（iPhone 16e 真机）— 表现正常子集协议 / 样本正常场景观察P1 本地 MBFramesS1（open）/ S2（close）/ S4（pause）/ S5（stop）/ S6（前向 seek）/ S7（后向 seek）/ S10（reload）/ S11（loop）正常播放、seek 位置正确、音画同步P2 非加密 M1S1（open）/ S2（close）/ S4（pause）/ S5（stop）/ S6（前向 seek）/ S7（后向 seek）/ S8（高频 seek）/ S10（reload）/ S11（loop）正常播放E4 不解密播加密E4（不解密播加密）UI 正常、画面黑屏、不崩溃（符合 P0 预期）NET1 单档倍速0.5× / 0.75×音画同步、无爆破音证据：evidence/ios/e093f7b/（observation.md 人眼记录）。iOS 其余场景见第三章。§2.5 通过情况总览端通过口径数量Windows 11scenario DSL 自动 verdict33/33 PASSLinux Ubuntu 26.04同上33/33 PASSmacOS 26.3.1同上61/61 PASSAndroid Xiaomiverdict=pass（P3 非加密含黑屏）P1/P2 基础场景为主iOS iPhone 16e人眼观察"正常"P1/P2 基础场景为主第三章 问题用例与异常现象编号沿用 vod-problem-set.md：通用 / 桌面 VP01–06、Android VP-AND01–07、iOS VP-IOS01–07、macOS VP-MAC01。§3.1 通用 / 桌面（VP01–VP06）编号用例异常现象状态VP01（S8 网络源超时偏紧）S8（高频 seek）·P2（HTTPS MP4）/P3（HLS）网络源默认 scenario-timeout 20s 不足，首跑 verdict=timeout已规避（调 45s）VP02（P2-S11 跨端 displayed 差异）S11（loop）·P2（HTTPS MP4）·Win vs Linuxvideo_displayed Win 1814 / Lin 404，相差 4.5×（不影响 verdict）待定（疑 Wayland render_callback 节流）VP03（长样本长稳定性，原"缺样本"作废）D1（20 分钟连播 / 长稳定性）·M13（加密单分辨率单帧率 1.91GB）/ M14（加密单分辨率多帧率 1.99GB）长样本已测：M13 GUI 连播 ~58.8min（29 张截图）、M14 headless ~19.3min，均无崩溃✅ 已覆盖VP06（加密长连播丢帧 ~80% + 内存采集缺失）D1（长稳定性）·M13 / M14 加密长样本M13 丢帧率 79.90%（55203/69086）、M14 ~79.8%（drop 17290/D 4376）；音频正常、不崩溃；memory_timeline.csv 仅表头（内存采集失败）待评估VP04（WSL2 S8 崩溃无复现）S8（高频 seek）·Linux WSL2issue 描述的 WSL2 高频 seek 崩溃，本机无 WSL2 环境未复现缺环境VP05（seek_latency 测量失真）S6（前向 seek）/S7（后向 seek）/S8（高频 seek）scenario_session.ts_ms 是 drain-time，seek_latency 派生恒为 0（测量失真）已规避（Pass* + interval 代理）§3.2 Android（VP-AND01–07）已修复（commit 92fc074，曾阻塞自动化）：编号用例原异常现象状态VP-AND01（首位 load 触发 invalid_state）DSL 首位 load（各场景启动）与 scheduleInitialAutoLoad 重复触发，第二次 load 被拒 invalid_state，后续 action 中断✅ 已修复（load 幂等）VP-AND03（不支持 quit action）DSL 末尾 quit（退出语义）demo 不支持 quit action，被静默忽略✅ 已修复（加 quit→finish）VP-AND04（不支持 set_loop action）S11（loop）的 set_loopdemo 不支持 set_loop action，S11（loop）跑不通✅ 已修复（加 set_loop）待处理：编号用例异常现象状态VP-AND05（postDelayed schedule 模型差异）网络源场景 DSL 鲁棒性postDelayed 静态 schedule（无 wait_for），HTTPS 抖动下 wait:N 盲等不可靠待评估VP-AND06（P3 HLS 无首帧黑屏）P3（HLS · M-HLS 非加密）S6（前向 seek）/S7（后向 seek）/S8（高频 seek）/S9（switch_to）/S10（reload）/S11（loop）6 个 P3 cell 全部 first_frame=no（黑屏），verdict 却 pass；同机加密 HLS 反而有首帧待修 P0VP-AND07（加密 HLS seek 崩溃）加密 HLS（DEC-HLS）S6（前向 seek）/S7（后向 seek）2 case 崩溃（verdict=crash）待修 P0§3.3 iOS（VP-IOS01–07）编号用例异常现象状态VP-IOS01（P3 HLS 渲染全失败黑屏）P3（HLS）全部场景（14 case）声音正常、画面全程黑屏；S7（后向 seek）/S8（高频 seek）多次 seek 后崩溃待修 P0VP-IOS02（加密样本起播卡顿）加密样本 M16（加密多分辨率多帧率 mp4）/ M-ext-enc（跨源加密 足球2）（7 case）起播后 6–40s 严重卡顿；M16 二次 seek 卡死 loading待修 P0VP-IOS03（加密 switchURL 后黑屏）S9（switch_to）加密（S9-002 / S9-003）切换后画面黑屏不再播放，新流不呈现待修 P0VP-IOS04（断网不自动续播）N1（断网恢复）恢复网络后不自动续播，start 无效，须手动点"载入"待修VP-IOS05（后台画面冻结）LC2（长压后台）后台 2s 即画面冻结（音频继续），pause/start 不恢复，仅 seek 恢复待修VP-IOS06（EDGE 弱网首播失败）NET5（弱网首播）·EDGEEDGE preset 5min 加载不出（LTE/DSL 可播）待评估VP-IOS07（倍速 UI 误显 / 输入爆破音）NET2（倍速时 seek）/ S9（switch_to）输入 URL倍速保持但 UI 误显示 1×；输入 URL 时音频爆破音demo 层（不涉 SDK）证据逐 case 见 evidence/ios/e093f7b/scenarios/<case>/observation.md。§3.4 macOS（VP-MAC01）编号用例异常现象状态VP-MAC01（跨源加密 crypt_version 误判）跨源加密 M-ext-enc（跨源加密 足球2，crypt_version=026530e）首跑 unsupported crypt_version → exit=3；误判非加密去 decrypt 后 verdict=pass 但丢帧 69%；加白名单后正确解密已规避教训：headless verdict=pass ≠ 内容播放正常；dropped > 50% 必须人眼 / ffmpeg 复核。§3.5 跨端共性问题（需联合排查）HLS 视频出图失败（Android + iOS）：VP-AND06（Android P3 HLS 黑屏）与 VP-IOS01（iOS P3 HLS 黑屏）同症状，桌面 / macOS 同协议正常出帧。Android 加密 HLS 有首帧、非加密 HLS 无首帧，说明两条 HLS 渲染 codepath 不同 → 建议合并"HLS 出图链路"专项 issue。加密路径 seek / 切流不稳（Android + iOS）：VP-AND07（加密 HLS seek 崩溃）、VP-IOS02（加密卡顿 / seek 卡死）、VP-IOS03（加密 S9 黑屏）方向一致，桌面 / macOS 加密全 PASS → 移动端 decrypt + decoder / 分片切换协同缺陷。自动化 verdict 盲区：Android first_frame=no 仍判 pass、macOS 丢帧 69% 仍判 pass，说明 verdict 未把"首帧 / 丢帧比例"纳入判定。建议给 verdict 增加 first_frame 必检 + dropped_ratio 阈值。附：状态与下一步项状态桌面 Win / Linux 33+33✅ 完整 PASSmacOS 61✅ 完整 PASS长稳定性（D1）长样本✅ 已覆盖（M13 ~59min / M14 ~19min，不崩溃）；遗留 VP06VP06（加密长连播丢帧 ~80% + 内存采集缺失）⏳ 人眼复核截图 + 修内存采集重跑 + 补非加密长样本对照Android demo 自动化阻塞✅ 已解除（92fc074）Android 完整 66 cell iOS 客观指标⏳ 待 scenario DSL 移植 Swift demoVP-AND06（Android P3 HLS 黑屏）+ VP-IOS01（iOS P3 HLS 黑屏）HLS 出图专项⏳ 建议合并起 issueVP-AND07（加密 HLS seek 崩溃）/ VP-IOS02（加密起播卡顿）/ VP-IOS03（加密切流黑屏）移动端加密缺陷⏳ 联合排查verdict 增加 first_frame / dropped_ratio 判定⏳ 待评估开 issue本文档由 AI 起草于 2026-06-10，基于五端实证证据与 problem-set；草稿状态，待 ryan_code review + 定稿。
离散数学基本概念数理逻辑 命题与联结词（否定￢、析取∨、合取∧、蕴涵→、等价↔） 析取∨ 类同于 或 合取∧ 类同于 且 蕴涵→ 如果就 等价 充分必要条件 命题(非真既假的陈述句) 复合命题(由简单命题通过联结词联结而成的命题) 计算规律双重否定律: A ⇔ ¬¬A 幂等律: A ∨ A ⇔ A, A ∧ A ⇔ A 交换律: A ∧ B ⇔ B ∧ A, A ∨ B ⇔ B ∨ A 结合律: (A ∨ B) ∨ C ⇔ A ∨ (B ∨ C), (A ∧ B) ∧ C ⇔ A ∧ (B ∧ C) 分配律: A ∨ (B ∧ C) ⇔ (A ∨ B) ∧ (A ∨ C), A ∧ (B ∨ C) ⇔ (A ∧ B) ∨ (A ∧ C) 德摩根律: ¬(A ∨ B) ⇔ ¬A ∧ ¬B, ¬(A ∧ B) ⇔ ¬A ∨ ¬B 吸收律: A ∨ (A ∧ B) ⇔ A, A ∧ (A ∨ B) ⇔ A 零律: A ∨ 1 ⇔ 1, A ∧ 0 ⇔ 0, 同一律: A ∧ 1 ⇔ A, A ∨ 0 ⇔ A 排中律: A ∨ ¬A ⇔ 1 矛盾律: A ∧ ¬A ⇔ 0 蕴涵等值式: A → B ⇔ ¬A ∨ B 等价等值式: A ↔ B ⇔ (A → B) ∧ (B → A) 假言易位: A → B ⇔ ¬B → ¬A 等价否定等值式: A ↔ B ⇔ ¬A ↔ ¬B 归谬论: (A → B) ∧ (A → ¬B) ⇔ ¬A 范式：析取范式、合取范式，极大（小）项，主析取范式、主合取范式 公式类型的判别方法：真值表法、等值演算法、主析取/合取范式法 命题逻辑的推理理论有一些重要的重言蕴涵式，称作推理定律。下面给出9条推理定律，它们是： $A \Rightarrow (A \lor B)$附加律 $(A \land B) \Rightarrow A$化简律 $(A \rightarrow B) \land A \Rightarrow B$假言推理 $(A \rightarrow B) \land \neg B \Rightarrow \neg A$拒取式 $(A \lor B) \land \neg B \Rightarrow A$析取三段论 $(A \rightarrow B) \land (B \rightarrow C) \Rightarrow (A \rightarrow C)$假言三段论 $(A \leftrightarrow B) \land (B \leftrightarrow C) \Rightarrow (A \leftrightarrow C)$等价三段论 $(A \rightarrow B) \land (C \rightarrow D) \land (A \lor C) \Rightarrow (B \lor D)$构造性二难$(A \rightarrow B) \land (\neg A \rightarrow B) \Rightarrow B$构造性二难（特殊形式） $(A \rightarrow B) \land (C \rightarrow D) \land (\neg B \lor \neg D) \Rightarrow (\neg A \lor \neg C)$破坏性二难 其中 $A, B, C, D$ 等是元语言符号，表示任意的命题公式。 集合 集合、元素、集合的表示方法（列元素法、谓词表示法）、子集、空集、全集、集合的包含、相等、幂集 定义 幂集 设 $A$ 为集合，把 $A$ 的全体子集构成的集合叫做 $A$ 的幂集，记作 $P(A)$（或 $2^A$），符号化表示为 $$P(A) = \{ x \mid x \subseteq A \}。$$ 若 $A$ 是 $n$ 元集，则 $P(A)$ 有 $2^n$ 个元素。例如：$A = \{ a, b, c \}$，$A$ 的幂集： $$P(A) = \{\emptyset, \{a\}, \{b\}, \{c\}, \{a, b\}, \{a, c\}, \{b, c\}, \{a, b, c\}\}。$$ 集合的交、并、差、补以及对称差等运算及有穷集的计数（文氏（Venn）图、包含排斥原理） 集合恒等式（幂等律、交换律、结合律、分配律、吸收律、矛盾律、德摩根律等）及应用 集合恒等式 名称 等式 恒等律 $A \cup \emptyset = A, \, A \cap E = A$​ 支配律 $A \cup E = E, \, A \cap \emptyset = \emptyset$​ 幂等律 $A \cup A = A, \, A \cap A = A$​ 双重否定律 $\sim(\sim A) = A$​ 交换律 $A \cup B = B \cup A, \, A \cap B = B \cap A$​ 结合律 $A \cup (B \cup C) = (A \cup B) \cup C, \, A \cap (B \cap C) = (A \cap B) \cap C$​ 分配率 $A \cap (B \cup C) = (A \cap B) \cup (A \cap C), \, A \cup (B \cap C) = (A \cup B) \cap (A \cup C)$​ 德摩根律 $\sim(A \cup B) = \sim B \cap \sim A, \, \sim(A \cap B) = \sim A \cup \sim B$​ 吸收律 $A \cup (A \cap B) = A, \, A \cap (A \cup B) = A$​ 补律 $A \cap \sim A = \emptyset, \, A \cup \sim A = E$​ 集合运算定律 幂等律： $A \cup A = A$ $A \cap A = A$ 结合律： $(A \cup B) \cup C = A \cup (B \cup C)$ 交换律： $A \cup B = B \cup A$ $A \cap B = B \cap A$ 分配律： $A \cup (B \cap C) = (A \cup B) \cap (A \cup C)$ $A \cap (B \cup C) = (A \cap B) \cup (A \cap C)$ 同一律： $A \cup \emptyset = A$ $A \cap U = A$ 零律： $A \cup U = U$ $A \cap \emptyset = \emptyset$ 排中律： $A \cup \sim A = U$ 矛盾律： $A \cap \sim A = \emptyset$ 吸收律： $A \cup (A \cap B) = A$ $A \cap (A \cup B) = A$ 德摩根律： $A - (B \cup C) = (A - B) \cap (A - C)$ $A - (B \cap C) = (A - B) \cup (A - C)$ $\sim (B \cup C) = \sim B \cap \sim C$ $\sim (B \cap C) = \sim B \cup \sim C$ $\sim \emptyset = U$ $\sim U = \emptyset$ 双重否定律： $\sim (\sim A) = A$ ‍二元关系 了解序偶与笛卡尔积的概念，掌握笛卡尔积的运算。 理解关系的概念：二元关系、空关系、全域关系、恒等关系；掌握关系的集合表示、关系矩阵和关系图、关系的运算。 定义 7.3 二元关系 一个集合被称为二元关系（记作 RR），如果它满足以下条件之一： 集合非空，且它的元素都是有序对； 集合是空集。 对于二元关系 RR，如果 ⟨x,y⟩∈R⟨x,y⟩∈R，则记作 xRyxRy；如果 ⟨x,y⟩∉R⟨x,y⟩∈/R，则记作 xR̸yxRy。示例： R1={⟨1,2⟩,⟨a,b⟩}R1={⟨1,2⟩,⟨a,b⟩} 是二元关系。 R2={⟨1,2⟩,a,b}R2={⟨1,2⟩,a,b} 不是二元关系，除非将 aa 和 bb 定义为有序对。 定义 7.4 从 AA 到 BB 的二元关系 设 AA 和 BB 为集合，A×BA×B 的任何子集所定义的二元关系称为从 AA 到 BB 的二元关系。特别地，当 A=BA=B 时，称为 AA 上的二元关系。示例： A={0,1}A={0,1}，B={1,2,3}B={1,2,3}。 R1={⟨0,2⟩}R1={⟨0,2⟩}，R2=A×BR2=A×B，R3=∅R3=∅，R4={⟨0,1⟩}R4={⟨0,1⟩} 都是从 AA 到 BB 的二元关系。 R3R3 和 R4R4 同时也是 AA 上的二元关系。 对于任何集合 AA，空集 ∅∅ 是 A×AA×A 的子集，称为 AA 上的空关系。此外，定义 AA 上的以下两种关系： 全域关系 EAEA：包含 A×AA×A 中的所有有序对，即 EA=A×AEA=A×A。 恒等关系 IAIA：包含所有形如 ⟨a,a⟩⟨a,a⟩ 的有序对，其中 a∈Aa∈A，即 IA={⟨a,a⟩∣a∈A}IA={⟨a,a⟩∣a∈A}。 这些关系在集合论和离散数学中具有重要的应用，用于描述集合元素之间的特定联系。 掌握求复合关系与逆关系的方法。 理解关系的性质（自反性、反自反性、对称性、反对称性、传递性），掌握其判别方法（定义、图）。 自反性：全部顶点均有环；反自反性：全部顶点均无环；对称性：有边均双边（无单边，顶点有无环不影响） ；反对称性：有边均单边（顶点有无环不影响）（无平行边）传递性：a到b有边，b到c有边，则a到c也有边，否则不然。 掌握求关系的闭包 （自反闭包、对称闭包、传递闭包）的方法。 换言之：r=加自环 s=单边变双边 t:努力变传递 理解等价关系和划分、掌握等价类和划分的求法 理解偏序关系的概念，掌握画哈斯图的方法，极大/小元、最大/小元的求法。 哈斯图的方法如图7.7：5,9,6,8,7均是极大元，1是极小元，无最大元，最小元为1；右边： {a，b，c}是极大元，∅是极小元，最大元是 {a，b，c}，最小元为∅ ‍函数图论
前情提要:目前项目用的PgSql,订单库所有生产数据都会同步到汇聚库(kettle实现),导致目前汇聚库数据量巨大(目前主单亿级数据)所以就用上了PgSql的表分区(分区因子是数据的创建时间),项目Dao用的是Mybatis,so我得根据业务的相关条件指定查询走哪一个或者哪几个表分区.那就直接上Mybatis的拦截器@Intercepts( { @Signature( type = StatementHandler.class, method = "prepare", args = { Connection.class,Integer.class })}) public class SubTablePlugin implements Interceptor { private Logger logger = LoggerFactory.getLogger(getClass()); public static final String DELEGATE = "delegate"; public static final String MAPPED_STATEMENT = "mappedStatement"; @Override public Object intercept(Invocation ivk) throws Throwable { if(ivk.getTarget().getClass().isAssignableFrom(RoutingStatementHandler.class)) { final RoutingStatementHandler statementHandler = (RoutingStatementHandler) ivk.getTarget(); final BaseStatementHandler delegate = (BaseStatementHandler) Reflections.getFieldValue(statementHandler, DELEGATE); final MappedStatement mappedStatement = (MappedStatement) Reflections.getFieldValue(delegate, MAPPED_STATEMENT); final BoundSql boundSql = statementHandler.getBoundSql(); String sql = SqlConverterFactory.getGlobalInstance().convert(boundSql, mappedStatement); if(StringUtils.isNotBlank(sql)) { logger.debug("[Modified sql] " + sql); Reflections.setFieldValue(statementHandler.getBoundSql(), "sql", sql); } } return ivk.proceed(); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { } }再加上一个自定义的注解 实现功能
IntelliJ IDEA远程Debug Linux的Java程序，找问题不要只会看日志了
1 前言我们习惯于在本地开发的时候debug，能快速定位与解决问题，那部署在服务器上是不是就没有办法了呢？只能通过查看日志来定位？不是的，在远端的服务器上，我们一样可以debug。2 IDEA的debug我们先来看一下在IntelliJ IDEA直接debug是怎样的。先准备一个简单的Java程序：package com.pkslow.basic; import java.util.Map; public class RemoteDebug { public static void main(String[] args) { System.out.println("------------------start------------------"); System.out.println("get all the system environment"); Map<String, String> envs = System.getenv(); System.out.println(" print out the contains `HOME`"); System.out.println("------env HOME------"); envs.entrySet().stream() .filter(env -> env.getKey().contains("HOME")) .forEach(env -> { System.out.println(env.getKey() + ":" + env.getValue()); }); System.out.println("------------------end------------------"); } }功能很简单，获取所有系统环境变量，并打印出含有HOME字段的。Debug很简单，直接点击以下按钮就可以：相信大家都知道这一点，但应该很多人都不会注意，IDEA究竟做了什么，为什么就可以调试了呢？我们看一下控制台的日志就明白了：/Library/Java/JavaVirtualMachines/jdk1.8.0_212.jdk/Contents/Home/bin/java -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:59313,suspend=y,server=n -javaagent:/Users/pkslow/Library/Caches/IntelliJIdea2019.3/captureAgent/debugger-agent.jar -Dfile.encoding=UTF-8 -classpath "/Library/Java/JavaVirtualMachines/jdk1.8.0_212.jdk/Contents/Home/jre/lib/charsets.jar:" com.pkslow.basic.RemoteDebug Connected to the target VM, address: '127.0.0.1:59313', transport: 'socket'简化一下，不重要的参数去掉：java -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:59313,suspend=y,server=n com.pkslow.basic.RemoteDebug这就是可以Debug的原因，利用了Java Agent原理。这个功能很强大，类似一个AOP，代理了Java程序，可以利用它进行调试、热部署等。3 调试本地程序我们先试试如何可以调试本地程序，不是直接在IDEA上调试。先要编译出class文件RemoteDebug.class，然后按package结构放好。我通过mvn clean compile来编译。启动程序，在target/classes/目录执行：$ java -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:50050,suspend=y,server=y com.pkslow.basic.RemoteDebug Listening for transport dt_socket at address: 50050然后程序就会等待调试客户端的连接，不会往下执行。配置IDEA以进行调试：配置完成保存后，点击debug就可以了：程序已经进入debug模式：我们已经执行到了其中一行，现在看看服务端：与IDEA是同步的，并且确实已经控制了服务端Java的执行。4 远程调试Linux Java程序先把程序部署在Linux上：$ scp -P 22 ./com/pkslow/basic/RemoteDebug.class root@xxx.xx.xx.xxx:/root/remoteDebug/com/pkslow/basic/ RemoteDebug.class 100% 2572 282.5KB/s 00:00 通过以下命令在服务端启动程序，这里调试端口改为9999，因为部分端口在远程服务器并没有开启：java -agentlib:jdwp=transport=dt_socket,address=9999,suspend=y,server=y com.pkslow.basic.RemoteDebug 本地电脑IDEA配置如下：开始debug，正常控制了远程服务端的Java：服务端的实时执行情况：$ java -agentlib:jdwp=transport=dt_socket,address=9999,suspend=y,server=y com.pkslow.basic.RemoteDebug Listening for transport dt_socket at address: 9999 ------------------start------------------ get all the system environment print out the contains `HOME` ------env HOME------ JAVA_HOME:/root/jdk1.8.0_131 HOME:/root ------------------end------------------
1. Streams简介今天要讲的Stream指的是java.util.stream包中的诸多类。Stream可以方便的将之前的结合类以转换为Stream并以流式方式进行处理，大大的简化了我们的编程，Stream包中，最核心的就是interface Stream1.1 创建StreamStream的创建有很多方式，java引入Stream之后所有的集合类都添加了一个stream()方法，通过这个方法可以直接得到其对应的Stream。也可以通过Stream.of方法来创建：//Stream Creation String[] arr = new String[]{"a", "b", "c"}; Stream<String> stream = Arrays.stream(arr); stream = Stream.of("a", "b", "c");复制代码1.2 Streams多线程如果我们想使用多线程来处理集合类的数据，Stream提供了非常方便的多线程方法parallelStream()：//Multi-threading List<String> list =new ArrayList(); list.add("aaa"); list.add("bbb"); list.add("abc"); list.add("ccc"); list.add("ddd"); list.parallelStream().forEach(element -> doPrint(element));复制代码1.3 Stream的基本操作Stream的操作可以分为两类，一类是中间操作，中间操作返回Stream，因此可以级联调用。 另一类是终止操作，这类操作会返回Stream定义的类型。//Operations long count = list.stream().distinct().count();复制代码上面的例子中，distinct()返回一个Stream，所以可以级联操作，最后的count()是一个终止操作，返回最后的值。Matching Stream提供了anyMatch(), allMatch(), noneMatch()这三种match方式，我们看下怎么使用： //Matching boolean isValid = list.stream().anyMatch(element -> element.contains("h")); boolean isValidOne = list.stream().allMatch(element -> element.contains("h")); boolean isValidTwo = list.stream().noneMatch(element -> element.contains("h")); 复制代码Filteringfilter() 方法允许我们对Stream中的数据进行过滤，从而得到我们需要的：Stream filterStream = list.stream().filter(element -> element.contains("d"));复制代码上面的例子中我们从list中选出了包含“d”字母的String。Mappingmap就是对Stream中的值进行再加工，然后将加工过后的值作为新的Stream返回。//Mapping Stream<String> mappingStream = list.stream().map(element -> convertElement(element)); private static String convertElement(String element) { return "element"+"abc"; }复制代码上的例子中我们把list中的每个值都加上了“abc”然后返回一个新的Stream。FlatMapflatMap和Map很类似，但是他们两个又有不同，看名字我们可以看到flatMap意思是打平之后再做Map。怎么理解呢？假如我们有一个CustBook类：@Data public class CustBook { List<String> bookName; }复制代码CustBook定义了一个bookName字段。先看一下Map返回的结果：List<CustBook> users = new ArrayList<>(); users.add(new CustBook()); Stream<Stream<String>> userStreamMap = users.stream().map(user -> user.getBookName().stream());复制代码在上面的代码中，map将每一个user都转换成了stream，所以最后的结果是返回Stream的Stream。如果我们只想返回String，则可以使用FlatMap：List<CustBook> users = new ArrayList<>(); users.add(new CustBook()); Stream<String> userStream = users.stream().flatMap(user -> user.getBookName().stream());复制代码简单点讲FlatMap就是将层级关系铺平重来。Reduction使用reduce() 方法可以方便的对集合的数据进行运算，reduce()接收两个参数，第一个是开始值，后面是一个函数表示累计。//Reduction List<Integer> integers = Arrays.asList(1, 1, 1); Integer reduced = integers.stream().reduce(100, (a, b) -> a + b);复制代码上面的例子我们定义了3个1的list，然后调用reduce(100, (a, b) -> a + b)方法，最后的结果是103.Collecting collect()方法可以方便的将Stream再次转换为集合类，方便处理和展示： List<String> resultList = list.stream().map(element -> element.toUpperCase()).collect(Collectors.toList());
处理 Exception 的几种实践，很优雅，被很多团队采纳！
在Java中处理异常并不是一个简单的事情。不仅仅初学者很难理解，即使一些有经验的开发者也需要花费很多时间来思考如何处理异常，包括需要处理哪些异常，怎样处理等等。这也是绝大多数开发团队都会制定一些规则来规范对异常的处理的原因。而团队之间的这些规范往往是截然不同的。本文给出几个被很多团队使用的异常处理最佳实践。在Finally块中清理资源或者使用try-with-resource语句当使用类似InputStream这种需要使用后关闭的资源时，一个常见的错误就是在try块的最后关闭资源。`public void doNotCloseResourceInTry() {` `FileInputStream inputStream = null;``try {` `File file = new File("./tmp.txt");` `inputStream = new FileInputStream(file);``// use the inputStream to read a file``// do NOT do this` `inputStream.close();` `} catch (FileNotFoundException e) {``log.error(e);` `} catch (IOException e) {``log.error(e);` `}``}`上述代码在没有任何exception的时候运行是没有问题的。但是当try块中的语句抛出异常或者自己实现的代码抛出异常，那么就不会执行最后的关闭语句，从而资源也无法释放。合理的做法则是将所有清理的代码都放到finally块中或者使用try-with-resource语句。`public void closeResourceInFinally() {` `FileInputStream inputStream = null;``try {` `File file = new File("./tmp.txt");` `inputStream = new FileInputStream(file);``// use the inputStream to read a file` `} catch (FileNotFoundException e) {``log.error(e);` `} finally {``if (inputStream != null) {``try {` `inputStream.close();` `} catch (IOException e) {``log.error(e);` `}` `}` `}``}``public void automaticallyCloseResource() {` `File file = new File("./tmp.txt");``try (FileInputStream inputStream = new FileInputStream(file);) {``// use the inputStream to read a file` `} catch (FileNotFoundException e) {``log.error(e);` `} catch (IOException e) {``log.error(e);` `}``}`指定具体的异常尽可能的使用最具体的异常来声明方法，这样才能使得代码更容易理解。`public void doNotDoThis() throws Exception {` `...``}``public void doThis() throws NumberFormatException {` `...``}`如上，NumberFormatException字面上即可以看出是数字格式化错误。对异常进行文档说明当在方法上声明抛出异常时，也需要进行文档说明。和前面的一点一样，都是为了给调用者提供尽可能多的信息，从而可以更好地避免/处理异常。异常处理的 10 个最佳实践，这篇也推荐看下。在Javadoc中加入throws声明，并且描述抛出异常的场景。`/**` `* This method does something extremely useful ...` `*` `* @param input` `* @throws MyBusinessException if ... happens` `*/``public void doSomething(String input) throws MyBusinessException {` `...``}`抛出异常的时候包含描述信息在抛出异常时，需要尽可能精确地描述问题和相关信息，这样无论是打印到日志中还是监控工具中，都能够更容易被人阅读，从而可以更好地定位具体错误信息、错误的严重程度等。但这里并不是说要对错误信息长篇大论，因为本来Exception的类名就能够反映错误的原因，因此只需要用一到两句话描述即可。`try {``new Long("xyz");``} catch (NumberFormatException e) {``log.error(e);``}`NumberFormatException即告诉了这个异常是格式化错误，异常的额外信息只需要提供这个错误字符串即可。当异常的名称不够明显的时候，则需要提供尽可能具体的错误信息。首先捕获最具体的异常现在很多IDE都能智能提示这个最佳实践，当你试图首先捕获最笼统的异常时，会提示不能达到的代码。当有多个catch块中，按照捕获顺序只有第一个匹配到的catch块才能执行。因此，如果先捕获IllegalArgumentException，那么则无法运行到对NumberFormatException的捕获。`public void catchMostSpecificExceptionFirst() {``try {` `doSomething("A message");` `} catch (NumberFormatException e) {``log.error(e);` `} catch (IllegalArgumentException e) {``log.error(e)` `}``}`# 不要捕获ThrowableThrowable是所有异常和错误的父类。你可以在catch语句中捕获，但是永远不要这么做。如果catch了throwable，那么不仅仅会捕获所有exception，还会捕获error。而error是表明无法恢复的jvm错误。因此除非绝对肯定能够处理或者被要求处理error，不要捕获throwable。`public void doNotCatchThrowable() {``try {``// do something` `} catch (Throwable t) {``// don't do this!` `}``}`不要忽略异常很多时候，开发者很有自信不会抛出异常，因此写了一个catch块，但是没有做任何处理或者记录日志。`public void doNotIgnoreExceptions() {``try {``// do something` `} catch (NumberFormatException e) {``// this will never happen` `}``}`但现实是经常会出现无法预料的异常或者无法确定这里的代码未来是不是会改动(删除了阻止异常抛出的代码)，而此时由于异常被捕获，使得无法拿到足够的错误信息来定位问题。合理的做法是至少要记录异常的信息。`public void logAnException() {``try {``// do something` `} catch (NumberFormatException e) {``log.error("This should never happen: " + e);` `}``}`不要记录并抛出异常可以发现很多代码甚至类库中都会有捕获异常、记录日志并再次抛出的逻辑。如下：`try {``new Long("xyz");``} catch (NumberFormatException e) {``log.error(e);``throw e;``}`这个处理逻辑看着是合理的。但这经常会给同一个异常输出多条日志。如下：`17:44:28,945 ERROR TestExceptionHandling:65 - java.lang.NumberFormatException: For input string: "xyz"``Exception in thread "main" java.lang.NumberFormatException: For input string: "xyz"``at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)``at java.lang.Long.parseLong(Long.java:589)``at java.lang.Long.(Long.java:965)``at com.stackify.example.TestExceptionHandling.logAndThrowException(TestExceptionHandling.java:63)``at com.stackify.example.TestExceptionHandling.main(TestExceptionHandling.java:58)`如上所示，后面的日志也没有附加更有用的信息。如果想要提供更加有用的信息， `````` 那么可以将异常包装为自定义异常。 `public void wrapException(String input) throws MyBusinessException {``try {``// do something` `} catch (NumberFormatException e) {``throw new MyBusinessException("A message that describes the error.", e);` `}``}`因此，仅仅当想要处理异常时才去捕获，否则只需要在方法签名中声明让调用者去处理包装异常时不要抛弃原始的异常捕获标准异常并包装为自定义异常是一个很常见的做法。这样可以添加更为具体的异常信息并能够做针对的异常处理。需要注意的是，包装异常时，一定要把原始的异常设置为cause(Exception有构造方法可以传入cause)。否则，丢失了原始的异常信息会让错误的分析变得困难。`public void wrapException(String input) throws MyBusinessException {``try {``// do something` `} catch (NumberFormatException e) {``throw new MyBusinessException("A message that describes the error.", e);` `}``}`总结综上可知，当抛出或者捕获异常时，有很多不一样的东西需要考虑。其中的许多点都是为了提升代码的可阅读性或者api的可用性。异常不仅仅是一个错误控制机制，也是一个沟通媒介，因此与你的协作者讨论这些最佳实践并制定一些规范能够让每个人都理解相关的通用概念并且能够按照同样的方式使用它们。
背景在一个成熟可维护的工程中，细分模块后，domian工程最好不要被其他工程依赖，但是实体类一般存于domain之中，这样其他工程想获取实体类数据时就需要在各自工程写model，自定义model可以根据自身业务需要而并不需要映射整个实体属性。mapstruct这个插件就是用来处理domin实体类与model类的属性映射，定义mapper接口，mapstruct就会自动的帮我们实现这个映射接口，避免了麻烦复杂的映射实现。通过类似下面的代码声明一个Mapper，MapStruct会在编译时自动生成实现代码。除了基本的功能外，MapStruct提供很多高级的功能，全面支持业务中可能出现各种映射要求。示例代码如下如何使用？1.简单封装@Mapper public interface ContentConverter { ContentConverter INSTANCE = Mappers.getMapper(ContentConverter.class); @Mappings({ @Mapping(source = "status", target = "statusVo"), @Mapping(source = "isRemove", target = "isRemoveVo"), @Mapping(source = "urlImg", target = "imgUrl"), @Mapping(source = "onDate", target = "onDateVo"), }) Content.ContentVo domain2dto(Content content); List<Content.ContentVo> domains2dto(List<Content> content);简要说明如下 1 - 用Mapper注解标注Mapper接口（也可以是抽象类） 2 - 可以调用其他的Mapper，来转换复杂类型的字段 3 - 可以指定一个Decorator类对Mapper实现做定制 4 - 常用的practice：声明一个静态的INSTANCE，便于调用方引用 5 - 同名的字段会自动映射，不同名的通过Mapping注解指定映射关系和映射方法 6 - 嵌套对象映射，映射到字段的成员，或成员的成员等 7 - 通过源类型和目标类型不能确定mapper的情况下，可以给mapping方法命名，在使用的时候指定特定名字的mapping方法 8 - 用constant来指定映射为一个常量值 9 - 用@MappingTarget注解指定update方法的参数中代表目标对象的参数 10 - 通过@InheritConfiguration重用映射配置，避免重复配置2.整合进Maven<properties> <java.version>1.8</java.version> <mapstruct.version>1.3.1.Final</mapstruct.version> </properties> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-jdk8</artifactId> <version>${mapstruct.version}</version> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${mapstruct.version}</version> </dependency>3.为Maven compile plugin设置annotation processor(本文使用lombok) <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.6.0</version> <configuration> 1.8 <target>1.8</target> <encoding>UTF-8</encoding> <annotationProcessorPaths> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> </path> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${mapstruct.version}</version> </path> </annotationProcessorPaths> </configuration> </plugin>4.maven 编译会生成该接口的实现类5.演示示例 public static void main(String[] args) { Content content = Content.builder() .status(true) .urlImg("test") .rank(4.30) .isRemove(false) .build(); Content.ContentVo contentVo = ContentConverter.INSTANCE.domain2dto(content); System.out.println(contentVo); }5.结果Content.ContentVo(statusVo=true, isRemoveVo=false, imgUrl=test, onDateVo=null, rank=4.3) 另外有一份网友测试对象拷贝工具的测试:https://juejin.im/post/5f05d26df265da23020a847c
一、前言好看的代码千篇一律，恶心的程序升职加薪。该说不说几乎是程序员就都知道或者了解设计模式，但大部分小伙伴写代码总是习惯于一把梭。无论多少业务逻辑就一个类几千行，这样的开发也可以归纳为三步；定义属性、创建方法、调用展示，Done！只不过开发一时爽，重构火葬场。好的代码不只为了完成现有功能，也会考虑后续扩展。在结构设计上松耦合易读易扩展，在领域实现上高内聚不对外暴漏实现细节不被外部干扰。而这就有点像家里三居(MVC)室、四居(DDD)室的装修，你不会允许几十万的房子把走线水管裸漏在外面，也不会允许把马桶放到厨房，炉灶安装到卫生间。谁发明了设计模式？ 设计模式的概念最早是由 克里斯托佛·亚历山大 在其著作 《建筑模式语言》 中首次提出的。 本书介绍了城市设计的 “语言”，提供了253个描述城镇、邻里、住宅、花园、房间及西部构造的模式， 而此类 “语言” 的基本单元就是模式。后来，埃里希·伽玛、 约翰·弗利赛德斯、 拉尔夫·约翰逊 和 理查德·赫尔姆 这四位作者接受了模式的概念。 1994 年， 他们出版了 《设计模式： 可复用面向对象软件的基础》 一书， 将设计模式的概念应用到程序开发领域中。其实有一部分人并没有仔细阅读过设计模式的相关书籍和资料，但依旧可以编写出优秀的代码。这主要是由于在经过众多项目的锤炼和对程序设计的不断追求，从而在多年编程历程上提炼出来的心得体会。而这份经验最终会与设计模式提到的内容几乎一致，同样会要求高内聚、低耦合、可扩展、可复用。你可能也遇到类似的经历，在学习一些框架的源码时，发现它里的某些设计和你在做开发时一样。我怎么学不会设计模式？ 钱也花了，书也买了。代码还是一坨一坨的！设计模式是由多年的经验提炼出来开发指导思想。就像我告诉你自行车怎么骑、汽车怎么开，但只要你没跑过几千公里，你能记住的只是理论，想上道依旧很慌！所以，本设计模式专题系列开始，会带着你使用设计模式的思想去优化代码。从而学习设计模式的心得并融入给自己。当然这里还需要多加练习，一定是人车合一，才能站在设计模式的基础上构建出更加合理的代码。二、工厂方法模式介绍工厂方法模式，图片来自 refactoringguru.cn工厂模式又称工厂方法模式，是一种创建型设计模式，其在父类中提供一个创建对象的方法， 允许子类决定实例化对象的类型。这种设计模式也是 Java 开发中最常见的一种模式，它的主要意图是定义一个创建对象的接口，让其子类自己决定实例化哪一个工厂类，工厂模式使其创建过程延迟到子类进行。简单说就是为了提供代码结构的扩展性，屏蔽每一个功能类中的具体实现逻辑。让外部可以更加简单的只是知道调用即可，同时，这也是去掉众多ifeslse的方式。当然这可能也有一些缺点，比如需要实现的类非常多，如何去维护，怎样减低开发成本。但这些问题都可以在后续的设计模式结合使用中，逐步降低。三、模拟发奖多种商品为了可以让整个学习的案例更加贴近实际开发，这里模拟互联网中在营销场景下的业务。由于营销场景的复杂、多变、临时的特性，它所需要的设计需要更加深入，否则会经常面临各种紧急CRUD操作，从而让代码结构混乱不堪，难以维护。在营销场景中经常会有某个用户做了一些操作；打卡、分享、留言、邀请注册等等，进行返利积分，最后通过积分在兑换商品，从而促活和拉新。那么在这里我们模拟积分兑换中的发放多种类型商品，假如现在我们有如下三种类型的商品接口；序号类型接口1优惠券CouponResult sendCoupon(String uId, String couponNumber, String uuid)2实物商品Boolean deliverGoods(DeliverReq req)3第三方爱奇艺兑换卡void grantToken(String bindMobileNumber, String cardId)从以上接口来看有如下信息：三个接口返回类型不同，有对象类型、布尔类型、还有一个空类型。入参不同，发放优惠券需要仿重、兑换卡需要卡ID、实物商品需要发货位置(对象中含有)。另外可能会随着后续的业务的发展，会新增其他种商品类型。因为你所有的开发需求都是随着业务对市场的拓展而带来的。四、用一坨坨代码实现ifelse实现需求public class PrizeController { private Logger logger = LoggerFactory.getLogger(PrizeController.class); public AwardRes awardToUser(AwardReq req) { String reqJson = JSON.toJSONString(req); AwardRes awardRes = null; try { logger.info("奖品发放开始{}。req:{}", req.getuId(), reqJson); // 按照不同类型方法商品[1优惠券、2实物商品、3第三方兑换卡(爱奇艺)] if (req.getAwardType() == 1) { CouponService couponService = new CouponService(); CouponResult couponResult = couponService.sendCoupon(req.getuId(), req.getAwardNumber(), req.getBizId()); if ("0000".equals(couponResult.getCode())) { awardRes = new AwardRes("0000", "发放成功"); } else { awardRes = new AwardRes("0001", couponResult.getInfo()); } } else if (req.getAwardType() == 2) { GoodsService goodsService = new GoodsService(); DeliverReq deliverReq = new DeliverReq(); deliverReq.setUserName(queryUserName(req.getuId())); deliverReq.setUserPhone(queryUserPhoneNumber(req.getuId())); deliverReq.setSku(req.getAwardNumber()); deliverReq.setOrderId(req.getBizId()); deliverReq.setConsigneeUserName(req.getExtMap().get("consigneeUserName")); deliverReq.setConsigneeUserPhone(req.getExtMap().get("consigneeUserPhone")); deliverReq.setConsigneeUserAddress(req.getExtMap().get("consigneeUserAddress")); Boolean isSuccess = goodsService.deliverGoods(deliverReq); if (isSuccess) { awardRes = new AwardRes("0000", "发放成功"); } else { awardRes = new AwardRes("0001", "发放失败"); } } else if (req.getAwardType() == 3) { String bindMobileNumber = queryUserPhoneNumber(req.getuId()); IQiYiCardService iQiYiCardService = new IQiYiCardService(); iQiYiCardService.grantToken(bindMobileNumber, req.getAwardNumber()); awardRes = new AwardRes("0000", "发放成功"); } logger.info("奖品发放完成{}。", req.getuId()); } catch (Exception e) { logger.error("奖品发放失败{}。req:{}", req.getuId(), reqJson, e); awardRes = new AwardRes("0001", e.getMessage()); } return awardRes; } private String queryUserName(String uId) { return "花花"; } private String queryUserPhoneNumber(String uId) { return "15200101232"; } }如上就是使用 ifelse 非常直接的实现出来业务需求的一坨代码，如果仅从业务角度看，研发如期甚至提前实现了功能。那这样的代码目前来看并不会有什么问题，但如果在经过几次的迭代和拓展，接手这段代码的研发将十分痛苦。重构成本高需要理清之前每一个接口的使用，测试回归验证时间长，需要全部验证一次。这也就是很多人并不愿意接手别人的代码，如果接手了又被压榨开发时间。那么可想而知这样的 ifelse 还会继续增加。五、工厂模式优化代码定义发奖接口public interface ICommodity { void sendCommodity(String uId, String commodityId, String bizId, Map<String, String> extMap) throws Exception; }所有的奖品无论是实物、虚拟还是第三方，都需要通过我们的程序实现此接口进行处理，以保证最终入参出参的统一性。接口的入参包括；用户ID、奖品ID、业务ID以及扩展字段用于处理发放实物商品时的收获地址。实现奖品发放接口优惠券public class CouponCommodityService implements ICommodity { private Logger logger = LoggerFactory.getLogger(CouponCommodityService.class); private CouponService couponService = new CouponService(); public void sendCommodity(String uId, String commodityId, String bizId, Map<String, String> extMap) throws Exception { CouponResult couponResult = couponService.sendCoupon(uId, commodityId, bizId); logger.info("请求参数[优惠券] => uId：{} commodityId：{} bizId：{} extMap：{}", uId, commodityId, bizId, JSON.toJSON(extMap)); logger.info("测试结果[优惠券]：{}", JSON.toJSON(couponResult)); if (!"0000".equals(couponResult.getCode())) throw new RuntimeException(couponResult.getInfo()); } }实物商品 public class GoodsCommodityService implements ICommodity { private Logger logger = LoggerFactory.getLogger(GoodsCommodityService.class); private GoodsService goodsService = new GoodsService(); public void sendCommodity(String uId, String commodityId, String bizId, Map<String, String> extMap) throws Exception { DeliverReq deliverReq = new DeliverReq(); deliverReq.setUserName(queryUserName(uId)); deliverReq.setUserPhone(queryUserPhoneNumber(uId)); deliverReq.setSku(commodityId); deliverReq.setOrderId(bizId); deliverReq.setConsigneeUserName(extMap.get("consigneeUserName")); deliverReq.setConsigneeUserPhone(extMap.get("consigneeUserPhone")); deliverReq.setConsigneeUserAddress(extMap.get("consigneeUserAddress")); Boolean isSuccess = goodsService.deliverGoods(deliverReq); logger.info("请求参数[优惠券] => uId：{} commodityId：{} bizId：{} extMap：{}", uId, commodityId, bizId, JSON.toJSON(extMap)); logger.info("测试结果[优惠券]：{}", isSuccess); if (!isSuccess) throw new RuntimeException("实物商品发放失败"); } private String queryUserName(String uId) { return "花花"; } private String queryUserPhoneNumber(String uId) { return "15200101232"; } }第三方兑换卡public class CardCommodityService implements ICommodity { private Logger logger = LoggerFactory.getLogger(CardCommodityService.class); // 模拟注入 private IQiYiCardService iQiYiCardService = new IQiYiCardService(); public void sendCommodity(String uId, String commodityId, String bizId, Map<String, String> extMap) throws Exception { String mobile = queryUserMobile(uId); iQiYiCardService.grantToken(mobile, bizId); logger.info("请求参数[爱奇艺兑换卡] => uId：{} commodityId：{} bizId：{} extMap：{}", uId, commodityId, bizId, JSON.toJSON(extMap)); logger.info("测试结果[爱奇艺兑换卡]：success"); } private String queryUserMobile(String uId) { return "15200101232"; } }从上面可以看到每一种奖品的实现都包括在自己的类中，新增、修改或者删除都不会影响其他奖品功能的测试，降低回归测试的可能。后续在新增的奖品只需要按照此结构进行填充即可，非常易于维护和扩展。在统一了入参以及出参后，调用方不在需要关心奖品发放的内部逻辑，按照统一的方式即可处理。创建商店工厂public class StoreFactory { public ICommodity getCommodityService(Integer commodityType) { if (null == commodityType) return null; if (1 == commodityType) return new CouponCommodityService(); if (2 == commodityType) return new GoodsCommodityService(); if (3 == commodityType) return new CardCommodityService(); throw new RuntimeException("不存在的商品服务类型"); } }这里我们定义了一个商店的工厂类，在里面按照类型实现各种商品的服务。可以非常干净整洁的处理你的代码，后续新增的商品在这里扩展即可。如果你不喜欢if判断，也可以使用switch或者map配置结构，会让代码更加干净。另外很多代码检查软件和编码要求，不喜欢if语句后面不写扩展，这里是为了更加干净的向你体现逻辑。在实际的业务编码中可以添加括号。总结从上到下的优化来看，工厂方法模式并不复杂，甚至这样的开发结构在你有所理解后，会发现更加简单了。那么这样的开发的好处知道后，也可以总结出来它的优点；避免创建者与具体的产品逻辑耦合、满足单一职责，每一个业务逻辑实现都在所属自己的类中完成、满足开闭原则，无需更改使用调用方就可以在程序中引入新的产品类型。但这样也会带来一些问题，比如有非常多的奖品类型，那么实现的子类会极速扩张。因此也需要使用其他的模式进行优化，这些在后续的设计模式中会逐步涉及到。从案例入手看设计模式往往要比看理论学的更加容易，因为案例是缩短理论到上手的最佳方式，如果你已经有所收获，一定要去尝试实操。
前言异常怎么处理？撸主很久之前的项目都是在 Controller 层一个个 try 的，之后也曾自己写过AOP实现异常拦截处理。不过，这里给小伙伴推荐一款统一处理异常神器。代码案例微服务、前后端分离的时代，应该很少有小伙伴使用模板了吧，大多都是返回Json数据。墙裂推荐大家使用 @RestControllerAdvice，可以用于定义@ExceptionHandler、@InitBinder、@ModelAttribute，并应用到所有@RequestMapping中。异常处理器/** * 异常处理器 */ @RestControllerAdvice public class RrExceptionHandler { private Logger logger = LoggerFactory.getLogger(getClass()); @ExceptionHandler(Exception.class) public Result handleException(Exception e){ logger.error(e.getMessage(), e); return Result.error(); } /** * 自定义异常 */ @ExceptionHandler(RrException.class) public Result handleRRException(RrException e){ Result r = new Result(); r.put("code", e.getCode()); r.put("msg", e.getMessage()); return r; } @ExceptionHandler(DuplicateKeyException.class) public Result handleDuplicateKeyException(DuplicateKeyException e){ logger.error(e.getMessage(), e); return Result.error("数据库中已存在该记录"); } }自定义异常/** * 自定义异常 */ public class RrException extends RuntimeException { private static final long serialVersionUID = 1L; private String msg; private int code = 500; public RrException(String msg) { super(msg); this.msg = msg; } public RrException(String msg, Throwable e) { super(msg, e); this.msg = msg; } public RrException(String msg, int code) { super(msg); this.msg = msg; this.code = code; } public RrException(String msg, int code, Throwable e) { super(msg, e); this.msg = msg; this.code = code; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public int getCode() { return code; } public void setCode(int code) { this.code = code; } }页面响应/** * 页面响应 */ public class Result extends HashMap<String, Object> { private static final long serialVersionUID = 1L; public Result() { put("code", 0); } public static Result error() { return error(500, "未知异常，请联系管理员"); } public static Result error(String msg) { return error(500, msg); } public static Result error(int code, String msg) { Result r = new Result(); r.put("code", code); r.put("msg", msg); return r; } public static Result ok(Object msg) { Result r = new Result(); r.put("msg", msg); return r; } public static Result ok(Map<String, Object> map) { Result r = new Result(); r.putAll(map); return r; } public static Result ok() { return new Result(); } @Override public Result put(String key, Object value) { super.put(key, value); return this; } }演示我们尝试模拟一个经典异常：@RequestMapping("eatChicken") public Result eatChicken() { String 马化腾 = null; if(马化腾.equals("码云")){ System.out.println("一起搞基"); } return Result.ok(); }前台输出：{"msg":"未知异常，请联系管理员","code":500}后台打印：java.lang.NullPointerException: null at com.itstyle.chicken.module.tools.web.ArticleController.get(ArticleController.java:35)小结是不是很爽，再也不用 try 了！！！
如今Java14已经发布许久了，Java15也在路上跑着了，然鹅不少小伙伴Java8的API应该还没用溜吧！今天跟各位小伙伴们聊聊Java Stream API的具体使用方法以及应用场景。举哥简单的例子，定义一个数组：String[] users = new String[]{"张三","李四","王二"};我们来使用之前的方式遍历：for(int i=0;i<users.length;i++){ System.out.println(users[i]); }亦或者使用：for(String u:users){ System.out.println(u); }使用最新的API实现方式：Stream<String> stream = Stream.of(users); stream.forEach(user-> System.out.println(user));然鹅好像并没有多么方便，仅仅少撸了一行代码，下面跟大家分享一个稍微复杂一点的例子。首先定义一个学生类：@Data @Builder public class Student { private String name; private Integer age; private Integer sex; private Double score; }定义基础数据：List<Student> list = new ArrayList<>(); list.add(new Student("张三",28,1,78.9)); list.add(new Student("李四",20,1,98.0)); list.add(new Student("王二",29,0,100.0)); list.add(new Student("张三",28,1,78.5)); Stream<Student> stream = list.stream();遍历：stream.forEach(stu-> System.out.println(stu.getName()));过滤性别：stream.filter(stu -> stu.getSex()==1) .forEach(stu -> System.out.println(stu.getName()));名字去重：stream.distinct().forEach(stu -> System.out.println(stu.getName()));年龄排序：stream.sorted(Comparator.comparingInt(Student::getAge)) .forEach(stu -> System.out.println(stu.getName()));排序反转：stream.sorted(Comparator.comparingInt(Student::getAge).reversed()) .forEach(stu -> System.out.println(stu.getName()));姓名集合：List<String> userList = stream.map(stu -> stu.getName()) .collect(Collectors.toList());分数求和：Optional<Double> totalScore = stream.map(Student::getScore) .reduce((x, y) -> x+y); System.out.println(totalScore.get());求分数最大值：Optional<Double> maxScore = stream.map(Student::getScore).reduce(Double::max);求分数最小值：Optional<Double> minScore = stream.map(Student::getScore).reduce(Double::min);取前三名同学：stream.sorted(Comparator.comparingDouble(Student::getScore).reversed()) .limit(3) .forEach(System.out::println);扔了前三名同学：stream.sorted(Comparator.comparingDouble(Student::getScore).reversed()) .skip(3) .forEach(System.out::println);卧槽太多了，撸主举不过来了，但是基本上常用的就这么几个。函数式编程写出的代码更加简洁且意图明确，使用stream接口让你从此告别for循环，小伙伴们赶紧入坑享受吧！！！