[{"content":"ld000 and his friends.\n","date":"9 April 2014","externalUrl":null,"permalink":"/links/","section":"Voiddd","summary":"","title":"友链","type":"page"},{"content":"90后码农，现在帝都搬砖，天天CRUD的过程中还坚持总结与学习新技术\n有空就会分享一些自己的开发总结和实用资源，欢迎关注~\n邮箱：voidd247@outlook.com\n微信：tree_1111\nGitHub 掘金 公众号 ","date":"9 April 2014","externalUrl":null,"permalink":"/about/","section":"Voiddd","summary":"","title":"关于","type":"page"},{"content":"","date":"1 May 2026","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","date":"1 May 2026","externalUrl":null,"permalink":"/tags/ci/cd/","section":"Tags","summary":"","title":"CI/CD","type":"tags"},{"content":"","date":"1 May 2026","externalUrl":null,"permalink":"/tags/devops/","section":"Tags","summary":"","title":"DevOps","type":"tags"},{"content":"","date":"1 May 2026","externalUrl":null,"permalink":"/tags/github-actions/","section":"Tags","summary":"","title":"GitHub Actions","type":"tags"},{"content":"","date":"1 May 2026","externalUrl":null,"permalink":"/tags/hugo/","section":"Tags","summary":"","title":"Hugo","type":"tags"},{"content":"","date":"1 May 2026","externalUrl":null,"permalink":"/posts/","section":"Posts","summary":"","title":"Posts","type":"posts"},{"content":"","date":"1 May 2026","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"1 May 2026","externalUrl":null,"permalink":"/tags/travis-ci/","section":"Tags","summary":"","title":"Travis CI","type":"tags"},{"content":"","date":"1 May 2026","externalUrl":null,"permalink":"/","section":"Voiddd","summary":"","title":"Voiddd","type":"page"},{"content":"最近对博客的部署流程进行了现代化改造，将持续集成和部署从 Travis CI 迁移到了 GitHub Actions。这篇文章记录了整个迁移过程和背后的思考。\n为什么要迁移？ # Travis CI 的现状 # Travis CI 曾经是开源项目 CI/CD 的首选，但近年来情况发生了变化：\n免费额度限制：对开源项目的免费支持大幅缩减 构建速度慢：平均需要 3-5 分钟才能完成部署 配置复杂：需要手动下载 Hugo、配置 Token 等 维护成本高：需要在外部服务中管理配置 GitHub Actions 的优势 # 相比之下，GitHub Actions 提供了更好的体验：\n⚡ 更快的构建速度：1-2 分钟即可完成部署 🆓 完全免费：公开仓库无限制使用 🔧 配置简单：原生集成，无需额外 Token 🚀 生态丰富：大量现成的 Actions 可用 📊 更好的可视化：直接在仓库中查看构建状态 迁移过程 # 1. 分析现有配置 # 首先查看原有的 .travis.yml 配置：\nlanguage: node_js env: HUGO=0.161.1 URL=https://github.com/gohugoio/hugo/releases/download script: - mkdir hugo - cd hugo - curl -L \u0026#34;${URL}/v${HUGO}/hugo_extended_${HUGO}_Linux-64bit.tar.gz\u0026#34; | gunzip | tar xvf - - cd .. - ./hugo/hugo deploy: provider: pages skip_cleanup: true local_dir: public github_token: $GITHUB_TOKEN keep_history: true on: branch: master 可以看到，Travis CI 需要手动下载 Hugo 二进制文件，配置相对繁琐。\n2. 创建 GitHub Actions 工作流 # 在 .github/workflows/deploy.yml 创建新的工作流：\nname: Deploy Hugo site to GitHub Pages on: push: branches: - master workflow_dispatch: permissions: contents: read pages: write id-token: write concurrency: group: \u0026#34;pages\u0026#34; cancel-in-progress: false defaults: run: shell: bash jobs: build: runs-on: ubuntu-latest env: HUGO_VERSION: 0.161.1 steps: - name: Checkout uses: actions/checkout@v4 with: submodules: recursive fetch-depth: 0 - name: Setup Hugo uses: peaceiris/actions-hugo@v3 with: hugo-version: \u0026#39;${{ env.HUGO_VERSION }}\u0026#39; extended: true - name: Setup Pages id: pages uses: actions/configure-pages@v5 - name: Build with Hugo env: HUGO_CACHEDIR: ${{ runner.temp }}/hugo_cache HUGO_ENVIRONMENT: production run: | hugo \\ --gc \\ --minify \\ --baseURL \u0026#34;${{ steps.pages.outputs.base_url }}/\u0026#34; - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: path: ./public deploy: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest needs: build steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 3. 关键改进点 # 使用官方 Actions # 不再手动下载 Hugo，而是使用 peaceiris/actions-hugo@v3，这个 Action 会自动处理 Hugo 的安装和缓存。\n支持 Hugo Extended # 通过 extended: true 参数，确保安装的是 Hugo Extended 版本，支持 SCSS/SASS 处理。\n自动处理子模块 # submodules: recursive fetch-depth: 0 自动拉取 Hugo 主题子模块，并获取完整的 Git 历史（用于 .GitInfo 和 .Lastmod）。\n构建优化 # hugo --gc --minify --gc：清理未使用的缓存文件 --minify：压缩 HTML/CSS/JS，减小文件体积 动态 baseURL # --baseURL \u0026#34;${{ steps.pages.outputs.base_url }}/\u0026#34; 自动获取 GitHub Pages 的 URL，无需硬编码。\n手动触发支持 # workflow_dispatch: 除了自动触发，还支持在 Actions 页面手动运行工作流。\n4. 配置 GitHub Pages # 在仓库设置中：\n进入 Settings → Pages 将 Source 改为 GitHub Actions 保存设置 5. 清理旧配置 # 迁移成功后，可以删除 .travis.yml 文件：\ngit rm .travis.yml git commit -m \u0026#34;Remove Travis CI configuration\u0026#34; git push origin master 效果对比 # 指标 Travis CI GitHub Actions 改进 构建时间 3-5 分钟 1-2 分钟 50-60% 提升 配置复杂度 需要手动下载 Hugo 使用现成 Action 大幅简化 免费额度 有限 无限（公开仓库） 完全免费 集成度 外部服务 原生集成 无缝体验 维护成本 需要管理 Token 自动处理权限 零维护 最佳实践建议 # 1. 版本固定 # 在工作流中明确指定 Hugo 版本：\nenv: HUGO_VERSION: 0.161.1 避免使用 latest，确保构建的可重复性。\n2. 使用缓存 # GitHub Actions 会自动缓存 Hugo 安装，加速后续构建。\n3. 并发控制 # concurrency: group: \u0026#34;pages\u0026#34; cancel-in-progress: false 确保同一时间只有一个部署任务运行，避免冲突。\n4. 环境隔离 # environment: name: github-pages 使用 GitHub Environments 功能，可以添加部署保护规则。\n5. 权限最小化 # permissions: contents: read pages: write id-token: write 只授予必要的权限，提高安全性。\n故障排查 # 构建失败 # 如果遇到构建失败，检查以下几点：\nHugo 版本：确认版本与本地一致 子模块：验证主题子模块已正确初始化 Extended 版本：如果使用 SCSS，必须启用 extended: true 页面 404 # 如果部署后页面显示 404：\n确认 GitHub Pages 设置为 \u0026ldquo;GitHub Actions\u0026rdquo; 检查 baseURL 配置 等待几分钟让 CDN 更新 样式丢失 # 如果样式无法加载：\n确认使用 Hugo Extended 版本 检查静态资源路径 验证 baseURL 设置正确 总结 # 从 Travis CI 迁移到 GitHub Actions 是一次非常值得的升级：\n性能提升：构建速度提升 50% 以上 成本降低：完全免费，无需担心额度 体验改善：配置更简单，维护更轻松 生态更好：丰富的 Actions 市场 如果你的项目还在使用 Travis CI 或其他传统 CI 服务，强烈建议尝试迁移到 GitHub Actions。对于 Hugo 博客来说，这个过程非常简单，只需要创建一个工作流文件即可。\n参考资源 # GitHub Actions 文档 Hugo 官方文档 peaceiris/actions-hugo GitHub Pages 部署指南 完整的配置文件和迁移指南可以在我的 GitHub 仓库 中找到。\n","date":"1 May 2026","externalUrl":null,"permalink":"/posts/migrate-from-travis-ci-to-github-actions/","section":"Posts","summary":"","title":"从 Travis CI 迁移到 GitHub Actions：Hugo 博客部署现代化实践","type":"posts"},{"content":"","date":"1 May 2026","externalUrl":null,"permalink":"/categories/%E6%8A%80%E6%9C%AF/","section":"Categories","summary":"","title":"技术","type":"categories"},{"content":"","date":"1 January 2026","externalUrl":null,"permalink":"/tags/claude/","section":"Tags","summary":"","title":"Claude","type":"tags"},{"content":"近两个月高强度使用Claude Code，在体验了国内各大厂的低价首购 Coding Plan 后，最终还是回归到了原生的Claude Opus模型——毕竟在代码能力上，满血 Opus 依然是第一梯队。\n由于 Anthropic 对地域及 IP 的风控强度远超 OpenAI，国内开发者必须构建一套从账号权重到网络链路的完整合规环境。\n阅前须知 # 本文面向的读者。如果你追求开箱即用的体验，建议直接选择国内 Coding Plan 或中转站商家。\n前置准备 # 在执行部署前，请确认以下环境就位：\nRuntime: Node.js 18.0.0 或更高版本。 Email: 纯净海外邮箱（推荐 Gmail 或 Proton，严禁使用国内厂商邮箱）。 搞定静态住宅IP # 普通的机场节点通常是机房 IP，成千上万人在用，早就被 Anthropic 拉黑了。我们要用的是静态住宅 IP 。静态住宅 IP 给你的是“私人住宅宽带 IP”，是真实家庭用户的网络，只分配给你一个人使用，固定不变。\n对 Claude 来说，这种 IP 就像是一个真实住在美国的普通用户在上网，风险极低。\n我使用的是EqualVPN，支持美国节点，IP 质量高，支持支付宝付款。\nhttps://www.equaldcdn.com/?ref=7afbd4d73a 通过邀请链接注册，可以享受八折优惠。\n4、购买步骤： # 1、打开注册链接，填写邮箱和、密码、验证码完成注册\n2、登录后，在 Dashboard 页面点击「购买套餐」\n3、选择套餐，这两个都行\n4、用支付宝完成付款\n5、购买成功后，进入「开始使用」页面，点击“开始配置”按钮，然后按照页面的提示进行操作\n安装并配置 AdsPower 指纹浏览器 # 注册地址：👉https://www.adspower.net/share/launch\nAdsPower 免费版可以创建最多 2 个浏览器环境，对于个人用户来说也够用了。\n新建一个专属 Claude 的浏览器环境 # 3.1 登录后，点击左上角的「新建浏览器」按钮。\n3.2 基础信息设置：\n名称：填写 Claude（方便识别） 浏览器版本：点击下拉框，选择最新的 Chrome 版本（比如 Chrome 133） 操作系统：选择 Windows 11 或 macOS，随便选一个 其余的按我的图中标注来就行 3.3 代理信息设置（关键步骤）：\n点击顶部的「代理信息」标签，这里有两种填写方式：\n方案 1：使用系统代理（最简单）\n代理类型：选择本地直连\nIP 查询渠道：这个默认即可\n这个方式也就是使用了指纹浏览器的环境，但是节点还是走的本地的节点配置，这个和你本地使用一个新的浏览器是一个道理，只不过指纹浏览器会保持各方面信息的一直，多了一层伪装。\n方案 2：手动填写代理\n可以手动设置代理端口：\n在 Clash 中查看本地代理端口（一般是 127.0.0.1:7890 或 7897）。 在指纹浏览器新建环境时： 代理类型：HTTP 或 SOCKS5 IP:127.0.0.1 端口：Clash 本地端口：7897 保存并启动环境即可。 填写完成后，点击「检查代理」，如果显示「连接测试成功」并且国家显示为「US（美国）」，就说明配置正确了。\n⚠️ 如果显示连接失败，检查一下代理信息是否填写正确，或者联系客服确认代理是否正常。\n其他的信息全部默认就行，最后点击「确定」，保存这个浏览器环境。\n🔒重要原则：以后使用 Claude，永远只在这个指纹浏览器里操作。不要用你本机的 Chrome、Safari 或者 Edge 登录 Claude，哪怕只是看一眼也不行。\n订阅Claude推荐方案：通过 App Store 订阅 # 这是目前公认比较稳定的订阅方式之一。通过苹果的 App Store 支付，Claude 会认为你是一个正规的 iOS 用户，账号权重大幅提升。\n注册美区 Apple ID # 注册美区 Apple ID 和购买 Apple 礼品卡都需要一个美国地址，在这里使用神奇的美国地址生成器来获取。\n注意美国的洲分为免税州和非免税州，如果你的 ChatGPT PLUS 订阅价格超过了 20 美元，大概率是因为填写了非免税州的地址，推荐蒙大拿州，景色秀丽而且免税。\n蒙大拿州地址生成器\n其实要用到的只有这一小部分，保存好，每次购买礼品卡都要用。\n注册 ID # 然后来到苹果官网注册 Apple ID，建议全程使用美国节点科学上网。\n地址选美国，邮箱和手机都可以使用大陆的，填完邮箱和手机的验证码后进入账户管理界面。\n新注册的 ID 想在 Appstore 进行支付，需要先填写一个付款方式和账单地址，点击个人信息\u0026ndash;国家或地区填写。\n国家选美国，付款方式选无，因为我们只通过礼品卡充值，所以不需要绑定支付方式，下面填入我们获取的美国地址和联系方式，如果提示电话号码无效，请去美国地址生成器多刷新几个试试，因为这个号码只是账单地址所以不需要真实有效。保存信息后这个 Apple ID 就可以在 App Store 正常进行支付购买了。\n购买礼品卡 # 第一步，前往苹果官网购买礼品卡。\n邮寄方式选择 Email，面额选择右下角，填入 20（或者 19.99，IOS 端订阅可以节省$0.01）,然后是收件人的名字和邮箱、发件人的名字和邮箱，一定要使用自己能登录的邮箱地址，然后点击 Add to Bag。\n检查邮箱地址无误后 Check Out，下一步因为不使用 Apple Card 支付所以直接点击 Guest 访问。\n重头戏来了，选择 Credit or Debit Card 支付，填入银联账户的卡号、过期时间和 CVV 码。下面地址栏填入之前注册的美国地址和联系方式，再次强调一定要填免税州的地址，不然会被收税。\n勾选同意后下订单，在下一个页面可以看到订单号，点击进入后可以查看订单进度。\n一小时内，收件人邮箱内就能收到邮件，框里面就是兑换码，我们把它复制到手机上。\n在AppStore里面登录美区账号（注意一定不要在系统设置里登录），点击右上角头像，选择兑换充值卡或代码，将兑换码复制进去充值，成功后可以在账户界面看到余额。\n最后打开 Claude app，点击订阅，输入 Apple ID 密码就能成功订阅。取消订阅在 Appstore 的订阅里操作，Claude 同理。\n六、日常使用的注意事项 # 账号注册好了，日常使用也要注意几点，才能长期保持稳定：\n✅ 应该做的：\n每次使用 Claude，都在 AdsPower 指纹浏览器里打开 保持使用同一个静态 IP，不要随意切换 有条件的话，在手机 Claude App 上也保持登录（增加“真实用户”的账号权重），手机端也绑定一个住宅 IP 日常像普通用户一样使用，聊天、写作、编程都可以 ❌ 不应该做的：\n不要在本机浏览器登录 Claude 不要频繁切换 IP 或节点 不要在手机和电脑同时登录，除非确认两边 IP 完全一致 不要尝试让 Claude 生成违规内容（这是最直接的封号原因） 不要使用低质量虚拟卡订阅 💡一个提升账号权重的小技巧：在手机 Claude App 上保持登录，偶尔日常聊聊天，就像用 ChatGPT 一样。来自一线运营者的经验：有手机 App 长期登录记录的账号，即使遇到高负载使用，被封的概率也极低。\n","date":"1 January 2026","externalUrl":null,"permalink":"/posts/2026-1-claude-code-signup/","section":"Posts","summary":"","title":"Claude Code防封号分享","type":"posts"},{"content":"原文：https://medium.com/pinterest-engineering/pinterest-is-now-on-http-3-608fb5581094\nLiang Ma | Software Engineer, Core Eng; Scott Beardsley | Engineering Manager, Traffic; Haowei Yuan | Software Engineer, Traffic\n图1 - Pinterest 的HTTP/3\n现在 Pinterest 正在使用 HTTP/3。我们已经在多个 CDN 边缘网络上启用了 HTTP/3，并升级了客户端应用程序的网络堆栈以支持新协议。这使我们能够跟上行业趋势。最重要的是，更快速和更可靠的网络改进了用户的体验和业务指标。\n背景 # 网络性能（如延迟和吞吐量）对 Pinterest 用户的体验至关重要。\n2021年，Pinterest 的一群客户端网络爱好者开始考虑在 Pinterest 上采用 HTTP/3（曾用名QUIC），从 traffic/CDN 到客户端应用程序。我们在2022年全年都在开发，我们已经实现了我们的初始目标（2023年及以后还需继续工作）。\n术语:\nHTTP/3: 下一代 HTTP 协议。它已经被IETF工作组稳定并最终确定。 QUIC: 由 Chromium/Google 为UDP上的HTTP创建；后来提交给IEFT进行标准化（HTTP/3）。 HTTP/3 怎么帮助 Pinterest # HTTP/3 是一种现代的 HTTP 协议，与 HTTP/2 相比有许多优点，包括但不限于：\n与 HTTP/2 相比没有 TCP 排队阻塞问题 可以在 IP 地址之间迁移连接，这对移动设备非常有用 能够改变/调整丢失检测和拥塞控制 减少连接时间（0-RTT，而 HTTP/2 仍需要 TCP 三次握手） 更有效地处理大负载用例，如图像下载、视频流等。 这些进步很适合Pinterest的场景 - 加快连接建立（第一个请求的第一个字节的时间），改进拥塞控制（我们有大型媒体），无TCP阻塞（同时下载多个文件），并在 Pinner 设备网络/ IP更改时继续进行正在进行的请求。 因此，当 Pinners 在 Pinterest 平台上进行灵感创作时，他们将拥有更快、更可靠的体验。\n在 Pinterest 采用 HTTP/3 # 策略 # 安全和指标优先。虽然 Pinterest 专注于快速执行，但采用 HTTP/3 的方法必须经过深思熟虑。首先，我们升级了客户端网络堆栈，并为每种流量类型（例如图像、视频）创建了端到端的 A/B 测试。然后，在启用 CDN 和客户端的 HTTP/3 之前，我们进行了广泛的实验。\n挑战：\n在 CDN 和客户端应用程序上启用 HTTP/3 的过程并不简单，主要有以下几个原因：\n对于 Web 应用程序，一些浏览器已经支持 HTTP/3 或 QUIC。虽然这些浏览器使用 HTTP/3，但可能存在兼容性问题，这可能会破坏 Pinterest 的 Web 应用程序。 新的 iOS 版本（从 iOS 15 开始）已经支持 QUIC，除非我们在服务器端禁用 QUIC，否则我们无法通过代码来控制。 我们的 CDN 供应商处于 HTTP/3 支持的不同阶段。随着我们逐渐启用 CDN 的 HTTP/3，我们的多 CDN 边缘网络在相当长的一段时间内只能部分支持 HTTP/3，这使得在切换 CDN 流量时确保可靠性和性能成为一个有趣的问题（链接）。 解决方案：\n首先，我们创建了一个 A/B 域级（CDN）测试，其中克隆了一个域以启用 HTTP/3，并彻底验证了客户端（包括 Web）。例如：在图像 HTTP/3 验证计划中，我们使用 i2.pinimg.com 验证 HTTP/3 的图像流量。 对于多 CDN 问题，我们选择了相对较短的 Alt-Svc TTL，以接近 DNS 记录 TTL，并尝试在这些 CDN 上配置相同的协议集。 进行了广泛的测试，以验证使用最常用的浏览器时跨 CDN 的 HTTP/3 行为。 在 A/B 测试通过后，我们逐个 CDN 启用了 HTTP/3，然后使用带功能标志的网络客户端让客户端应用程序来控制 HTTP/3，这更安全，同时也可以收集和比较指标。 现在的状态 # 我们已经在关键的网络流量类型上启用了 HTTP/3，并升级/利用移动客户端的网络栈来利用 HTTP/3。\n网络流量：我们在我们的多 CDN 边缘网络上为主要的 Pinterest 生产域启用了 HTTP/3。\n客户端：\nWeb 将在符合条件的浏览器和网络流量上免费获得它。 iOS - 图像/API 流量 由 Cronet + HTTP/3 提供服务。现在，70%的 iOS 图像流量使用 HTTP/3。 iOS 的本地网络堆栈 可以在网络流量侧启用之后利用 HTTP/3。苹果的 HTTP/3 采用率继续年复一年地提高。通过使用 HTTP/3（通过AVPlayer），我们在我们的视频指标中看到了这方面的显著好处。 Android 视频 通过 Exoplayer+Cronet 利用了 HTTP3。 Showcase # 我们的分析表明，HTTP/3（以及Cronet）已经改善了核心网络指标（往返延迟和可靠性）。改善延迟/吞吐量对于大型媒体功能（如视频、图像）至关重要。更快、更可靠的网络也能够提高用户参与度指标。\n视频指标\n视频特写 GVV（iOS：苹果网络 + HTTP/3）：\n视频特写 GVV（Android：Exoplayer + Cronet + HTTP/3）：\n图2 - HTTP3 对视频启动延迟的直接影响\n参与度指标（iOS）：HTTP/3 的直接影响\n图3 — HTTP3 对用户参与度的影响\n网络指标\n往返延迟（毫秒）： 图4 —— HTTP/3 之前和之后的网络请求往返延迟\n注意：1）从客户端测量，从请求发送到响应接收；2）基于在2022年第三季度使用苹果网络（HTTP/2）和2023年第一季度启用Cronet（HTTP/3）收集的一周网络日志。\n可靠性也有所提高。 未来工作 # 我们将继续投资 HTTP/3 以实现持续影响，包括:\n扩大 HTTP/3 覆盖范围；在 Android 上探索其他网络堆栈。 进一步提高 HTTP/3 采用率；将最大年龄值设置为更高值。 尝试各种拥塞控制算法。 探索 0-RTT 连接建立。 要了解Pinterest的工程技术，请查看我们的Engineering Blog，并访问我们的Pinterest Labs网站。要了解Pinterest的生活，请访问我们的Careers页面。\n","date":"13 March 2023","externalUrl":null,"permalink":"/posts/trans_pinterest-is-now-on-http-3/","section":"Posts","summary":"","title":"[翻译]Pinterest 现在已支持 HTTP/3 协议","type":"posts"},{"content":"","date":"23 August 2022","externalUrl":null,"permalink":"/springweek/","section":"Springweeks","summary":"","title":"Springweeks","type":"springweek"},{"content":"嗨，Spring 粉丝们！欢迎收看另一期 Spring 周报！我们有很多事情要处理，所以让我们直接开始吧！\nHi, Spring fans! Welcome to another installment of This Week in Spring! We’ve got a ton to cover, so let’s dive right into it!\n(播客)A Bootiful Podcast: Flowable founder Joram Barrez on a Bootiful Podcast on workflow, business process management, and more 用 Fauna and Spring 构建 IoT 应用 面向 Spring 开发人员的 Cosmos DB，第 I 部分：将 Cosmos DB 用作 SQL 数据库 如何使用 Paketo Buildpacks 构建 Web 服务器 - Paketo Buildpacks 为 Apache Pulsar 引入实验性 Spring 支持 使用 OpenFeign 和 Spring 传播异常 Spring Authorization Server 0.4.0-M1 发布 Spring Authorization Server 1.0.0-M1 发布 Spring Boot 2.6.11 发布 Spring Boot 2.7.3 发布 Spring Boot Actuator Spring Boot Native image builds with 22.2.0 are failing · Issue #176 paketo-buildpacks/native-image - 这是关于上周刚刚出现的与 Buildpacks 相关的问题。最近部署了一种解决方法. Spring Boot and Spring Security with JWT including Access and Refresh Tokens 🔑 Spring JDBC Batch Inserts Spring Security 5.8.0-M2 发布 Spring Shell 2.1.1 发布 Spring for GraphQL 与 Querydsl. 用于本地开发和集成测试的 Testcontainers 和 Spring Boot ","date":"23 August 2022","externalUrl":null,"permalink":"/springweek/this_week_in_spring_20220823/","section":"Springweeks","summary":"","title":"Spring周报 - 2022.8.23","type":"springweek"},{"content":"嗨，Spring 粉丝们！欢迎来到另一个充满奇迹的 Spring 周报！已经一个星期了！有时我自己都不敢相信。你能相信已经是 8 月 16 日了吗？我女儿这周开始上学了！我们在北半球，她的暑假已经结束了。不过，夏天还有一个月和的一些变化。所以，我希望你们都尽你所能，在黑暗和寒冷的月份到来之前最大限度地享受它。\nHi, Spring fans! Welcome to another wonder-filled installment of This Week in Spring! It’s been a week! Sometimes I can scarcely believe it myself. And can you believe it’s August 16th already?? My daughter’s starting school this week! We’re in the northern hemisphere, and Summer break is already over and done with for her. There’s still another month and some change of summer, officially, though. So, I hope you all are doing whatever you can to maximize your enjoyment of it before the darker and colder months arrive.\nTwitter 帮助我打发时间。我一直在编写一些代码，并希望在应用程序中使用 Twitter 的 OAuth 2 和 PKCE 支持，但无法完全实现。所以我联系了我的朋友（每个人的朋友，真的！）和 Spring Security 负责人 Rob Winch (@rob_winch) 寻找一些线索，他做得更好：他整理了一个示例，展示了这一切！谢谢，罗伯！我喜欢 Spring Security，一个重要的原因是它背后的团队令人惊叹、乐于助人和放纵。而且我喜欢推特（无论如何，大多数时候）。\nTwitter helps me pass the time. I’ve been working on some code and wanted to use Twitter’s OAuth 2 and PKCE support in the application but couldn’t quite make it work. So I pinged my pal (everybody’s pal, really!) and Spring Security lead Rob Winch (@rob_winch) for some clues, and he did me one better: he put together a sample that demonstrates it all in action! Thanks, Rob! I love Spring Security and a huge reason is because of the amazing and helpful and indulgent team behind it. And I love Twitter (most of the time, anyway).\n对我来说，这是 Twitter 上忙碌的一周！在上面，我问人们在 Kubernetes 上使用什么来实现他们的可观测性，发现在我 25 多年的软件写作中，我什么都不知道）等等！在这里，你这个愚蠢的读者，你可能很高兴在你的非 Twitter 生活中保持高效，不是吗？好吧，让我告诉你：你错过了！ 或者，我不能强调这一点，你可能不是。这也可能是真的。所以，继续。 我们应该继续。毕竟，本周我们有一个 heckuva 综述！所以，事不宜迟……\nIt’s been a busy week on Twitter for me! On it, I asked what people are using for their observability stacks on Kubernetes, figured out that for my 25+ years in writing software, I don’t know anything, and so much more! And here, you silly reader you, you’ve probably been happy being all productive in your off-Twitter life, haven’t ya? Well, let me tell you: you’re missing out!\nOr, and I can’t stress this enough, you’re probably not. That’s probably true, too. So, carry on.\nAnd we shall carry on, too. After all, we’ve got one heckuva roundup this week! So, without further ado…\nGradle Enterprise 的新版本, 2022.3, 带来一些新特性. 恭喜, Gradle 团队!\n(播客)In last week’s episode of the podcast, I talked with my friend, the good Dr. Venkat Subramaniam\nApache SkyWalking 看起来是一个有趣的端到端可观测性方案，它们也有一个很好的 Spring 集成故事\nBlog: Enhancing Kubernetes one KEP at a Time\n这篇文章将重点介绍增强子团队以及您如何参与其中。\nKEP - Kubernetes Enhancement Proposal - Kubernetes 增强提案\nSpring Boot 用注解依赖注入\n不要称之为卷土重来：为什么 Java 仍然是冠军\nElastic Search with Spring Boot\n用 Docker Compose 执行多条命令\n介绍在 Spring Boot 里用 JBoss Drools\n查看 JHipster Native 生成器 (jhipster/generator-jhipster-native) 中令人兴奋的新工作，它使构建 Spring Native 和 GraalVM 驱动的 JHipster 应用程序变得轻而易举：v1.3.0\nSpring Boot: 什么是多请求映射\nSpring Cloud Dataflow 2.9.5 发布\nSpring Data MongoDB – 连接配置\nSpring Tools 4.15.3 发布\nSpring Web Flow 3.0 M1 发布\n这是我在五年多前做过的一次演讲，但它展示了当时很常见的很多事情。如果您是最近加入 Spring Boot 社区的一大群人，这可能会很有趣。这里有两个我感兴趣的方面：有多少事情改进了，有多少事情保持不变。享受! The Bootiful Application\nVMware 在开源领域的励志女性：聚焦 Olga Maciaszek-Sharma，她是 Spring Cloud 团队的传奇成员\n","date":"16 August 2022","externalUrl":null,"permalink":"/springweek/this_week_in_spring_20220816/","section":"Springweeks","summary":"","title":"Spring周报 - 2022.8.16","type":"springweek"},{"content":"嗨，Spring 的粉丝们！欢迎收看 Spring 周报的另一期！周二你好吗？我在堪萨斯城参加堪萨斯城开发者大会。这是一个疯狂有趣的节目，我很高兴来到这里。我只希望你们其他人也在这里！然而，我们在 Spring 周报已经有很多内容了，所以让我们直接开始吧。\nHi, Spring fans! Welcome to another installment of This Week in Spring! How are you this fine Tuesday? I’m in Kansas City for the Kansas City Developer Conference. It’s a crazy fun show, and I’m glad to be here. I only wish the rest of you were here, too! We’ve got a packed This Week in Spring, however, so let’s dive right into it.\nsnicoll/demo-aot-native: Demo of a basic webapp using Ahead-Of-Time compilation\n使用提前编译的 demo，一个使用 Spring Boot 3 native image 的 Web 应用程序。展示了如何使用 RuntimeHints 配置反射和资源加载。\n(播客)A Bootiful Podcast: Observability guru Jonatan Ivanov on the future of observability in Spring Boot\nBlog: Kubernetes 1.25 改动\n用 RabbitMQ 和 .NET 6 实现主题消息\nThymeleaf 里显示登录用户信息(Spring Security)\nJava 应用 docker 化\n开始使用 Spring Boot 和 SAML\n使用 Reactor Kafka 的弹性 Kafka 消费者 - DZone Java\nSpring Batch + Karp Rabin = How CRISPR CAS9 Works\n使用 Spring Batch + Karp Rabin 对 DNA 测序数据进行模式匹配\nSpring Tools 4.15.2 发布\nbug 修复\n用 Spring Boot and Thymeleaf 上次图片\n","date":"9 August 2022","externalUrl":null,"permalink":"/springweek/this_week_in_spring_20220809/","section":"Springweeks","summary":"","title":"Spring周报 - 2022.8.9","type":"springweek"},{"content":"Aloha, Spring fans! Welcome to another installment of This Week in Spring!\nI’m still on vacation on the beautiful island of Maui, Hawaii, but I wanted to say hello (“aloha!”) and share this week’s latest roundup of all that’s good and glorious in the wide and wonderful world of Springdom.\nFunny thing, today - the 2nd of August, 2022 - is also my 12-year anniversary on the Spring team. It continues to be a helluva ride, and I so look forward to all that lies ahead. Thank you, Spring team, for everything. Also, sort of coincidentally, I just got a nice promotion. (Thanks, Spring team and VMware). I’m also just about to hit 60,000 followers on Twitter. It’s been a weird, wonderful week, and not just because of all that, but because we’ve got a ton of great stuff to dive into this week (besides the ocean, which I’ll return to promptly after finishing this roundup)!\nSpring Authorization Server 在准备 1.0\nSpring Boot logging 介绍\n16 个 Spring Boot 生产最佳实践\nSpring Data 5种访问数据的方法\n如何用 Spring Boot 创建 Kubernetes Controller, Cora 和我在 Spring I/ O 做得演讲\n如何在 Spring Boot 应用程序中将 Hibernates Multitenant 功能与 Spring Data JPA 集成\n(播客)Java Q\u0026amp;A; Leyden, Valhalla, Amber, and more! - Inside Java Newscast #30\n(演讲预约)Register for this talk at the London Java Comunity to learn about one way to achieve real-time stream processing in Spring\nIntelliJ IDEA 使用 Mockito 的一个插件: Mockitools\n使用 Spring Reactive WebClient 将 Flux 读入单个 InputStream\nSpring Boot 3 和 Spring Framework 6.0 – 有哪些新特性\nJava 17: Records, 文本块, switch 表达式, 模式匹配, 密封类和接口\nJava EE 升级到 Jakarta EE9\nnative image, Spring Observability\nSpring Cloud 2022.0.0-M4 (codename Kilburn) 发布\nSpring Cloud OpenFeign 3.0.8 发布\nbugfix\nSpring Security: 升级弃用的 WebSecurityConfigurerAdapter\n测试 Spring JMS\n在 Spring Boot 的 application.properties 里用环境变量\n[making/tap-automation-on-aks: TAP Automation on AKS](https://github.com/making/tap-automation-on-aks) - 这是我的队友 Toshiaki Maki 的一个很好的解决方案，它使用 Tekton 在 AKS 上设置 Tanzu 应用程序平台。\n","date":"1 August 2022","externalUrl":null,"permalink":"/springweek/this_week_in_spring_20220801/","section":"Springweeks","summary":"","title":"Spring周报 - 2022.8.1","type":"springweek"},{"content":"原文：https://engineering.tumblr.com/post/644763186513444864/how-post-content-is-stored-on-tumblr\n我们目前推出了一个可选测试版的使用 Neue 帖子格式的新帖子编辑器。已经有很长一段时间了 - Neue 帖子格式的工作始于 2015 年，最初的代号为“Poster Child”，它源于我们从以前发布的帖子编辑器上学到的很多东西。多年来，人们在互联网上不同平台上发帖的方式发生了巨大变化。但在 Tumblr 上，我们仍然希望忠于我们的博客根源，同时提供广阔的创意空间，而 Neue 帖子格式让它成为可能。\nTumblr 上有数十亿（数百亿！）的帖子，我们如何在不破坏一切的情况下将这种内容引擎从一种格式转移到另一种格式？它经历了许多阶段，在网络上发布新的编辑器将是最后完成的部分之一。要了解我们已经走了多远以及我们必须面对的挑战，您需要了解我们如何在 Tumblr 上存储帖子内容的秘密。这个我们都喜欢的地狱现场是由胶带、良好的意愿和运气组成的，我们一直在努力让它变得更好！\n帖子看似是一个非常简单的数据模型：它有作者，有内容，并且是在某个时间发布的。每个帖子在创建后都有一个唯一标识符。在转发的情况下，他们还拥有转发它的“父”帖子和博客（更多关于转发如何工作在这里）。在标准规范化数据库表中，这些列如下所示：\n帖子标识符 (一个很大的整数) 作者博客标识符 (一个指向“blogs”数据库表的整数) 父帖子标识符 (如果是转发) 父博客标识符 (如果是转发) 发布时间 (某种时间戳) 帖子内容 (more on this in a minute) 在 Neue 帖子格式之前，帖子有不同的“类型”，所以类型也是一个字段。但是一旦有了这些“类型”，就必须确定如何存储每个“类型”的内容。对于照片帖子，有一组一个或多个图像。对于视频帖子，要么是对上传视频文件的引用，要么是指向外部视频的 URL。对于文本帖子，它只是 HTML 格式的文本。因此，“发布内容”列的实际值可能会根据它的类型而改变。\n这是一个简单的示例，请注意每种帖子类型如何具有不同类型的内容：\n随着 Tumblr 的发展，它的能力也在增长。我们添加了为照片、视频和音频帖子添加标题的功能。我们添加了添加“来源”来引用帖子的功能。我们需要在某个地方存储新的帖子内容。因为当时 Tumblr 发展得如此之快，所以这需要快速发生，所以我们采取了最简单的方法：添加一个新字段！第一个“帖子内容”栏被重命名为“one”，新的帖子内容栏被命名为“two”。随着 Tumblr 帖子越来越多，最终我们添加了“three”。并且每列的值可能会根据帖子类型而有所不同。\n不用说，最终这使得我们很难有一致且易于理解的模式诸如……计算帖子中有多少张图片？由于我们添加了在标题中添加图像的功能，因此“one”、“two”或“three”列中都可能有图像，但根据帖子类型，每个列的格式可能不同。 转发使存储设计进一步复杂化，因为转发将帖子内容从其父帖子复制并重新格式化到新帖子。渲染帖子的代码变得非常复杂且难以向其中添加更多功能。\n更复杂的是，大多数（但不是全部）帖子内容字段都利用 HTML 或 PHP 的内置序列化逻辑作为文字数据格式。在 PHP 7 之前，PHP 中的 HTML 解析（Tumblr 在使用的）非常慢，因此随着帖子的转发路径增加或帖子内容复杂性的增加，渲染帖子变得更加困难。 HTML 和 PHP 的序列化逻辑不容易移植到其他语言，如 Go、Scala、Objective-C、Swift 或 Java，这些我们在其他后端服务和移动应用程序中使用这些语言。\n考虑到这一切，在 2015 年，两个需求融合在一起：需要从数据库一直到应用程序共享更易于理解和可移植的数据格式，以及需要支持更多类型的帖子内容，与帖子类型解耦。 Neue 帖子格式诞生了：一种基于 JSON 的数据模式，用于内容块及其布局。这为我们提供了更快地提供新类型内容的灵活性，而不必担心我们将如何以 HTML 格式存储它，并且使帖子内容格式可以从数据库适应到 Android 应用程序、iOS 应用程序，以及新的基于 React 的 Web 客户端。\n回到帖子的标准规范化数据库表模式，我们现在已经通过“帖子内容”列中的灵活 JSON 结构实现了存储的简单性。存储帖子时，我们根本不再需要帖子类型。帖子可以包含任何和所有内容类型，而不是根据帖子类型单独使用无数令人困惑的选项。现在帖子可以同时是视频和照片帖子！当网络上的新编辑器完全发布时，我们终于可以说这种格式是推动 Tumblr 内容引擎的燃料。它将使我们能够更快地构建以前无法构建的块类型和布局，例如投票、博客卡片块和重叠的图像/视频/文本。完全没有限制。\n","date":"27 July 2022","externalUrl":null,"permalink":"/posts/how_post_content_is_stored_on_tumblr/","section":"Posts","summary":"","title":"[翻译]Tumblr 如何存储帖子内容","type":"posts"},{"content":"Hi，Spring 粉丝们！我正在度假，在天堂般的夏威夷毛伊岛写这篇文章，希望您今天过得愉快！我和我的家人都喜欢夏威夷。它充满了美丽和宁静，虽然夏威夷州的毛伊岛非常小，但这些岛屿令人谦卑。它们让你觉得自己非常渺小。坐在沙滩上，当太阳从地平线上爬下时，会意识到除了几米外的漆黑和水之外，什么都没有，这是超现实的。这是无止境的。它没有尽头。就像代码中的错误一样。没完没了。并且谦卑。\nAloha, Spring fans! I’m on vacation, reporting to you from the paradise-like island of Maui, Hawaii, and hoping that you’re having a wonderful day! My family and I love Hawaii. It’s brimming with beauty and serenity, and while the island of Maui, in the state of Hawaii, is very small, the islands are humbling. They make you feel so very small. It’s surreal to sit there on the beach as the sun creeps down beyond the horizon and to realize there’s nothing but pitch black darkness and water for as far as you can see, starting just a few meters away. It’s endless. It has no end. Like the bugs in code. Endless. And humbling.\n我和我的伴侣和女儿在海滩上度过了很多时间，以至于坐在键盘前写下这个博客真的感觉有点奇怪！但我很乐意这样做。学习新事物是令人欣慰的。因此，让我们深入了解本周的内容：\nI’ve spent so much time on the beach with my partner and our daughter that it actually feels kind of weird just to sit here at the keyboard and write out this blog! But I’m happy to do it. It’s gratifying to learn new things. And so, with that, let’s dive into this week’s installment:\n(播客)A Bootiful Podcast: Spring Cloud and Spring Cloud Kubernetes contributor Ryan Baxter\nJava Source 和 Target 选项指南\n-source, -target 参数和 Java9 的 —release 参数，可以控制编译和打包的 Java 版本\n用 springdoc-openapi 库配置默认全局安全方案\n抢先了解下一版 JavaOne 的工作内容（是的，没错！节目又回来了！）\nJavaOne 是每年一次的 Java 大会，本届即将于 10 月 17 日至 20 日在拉斯维加斯举行。这篇是介绍系列的第 2 部分。\n使用 Spring Data MongoDB 库统计文档数量\n在 Raspberry Pi 和 ClusterHat 上使用 Spring Native 构建 K3s\n我也喜欢我的朋友 Jakub Pilmon 的一个 DDD 示例\nMaven Snapshot Repository 对比 Release Repository\n你有真正的 CI/CD 么?\nVMware Tanzu，一个企业级 K8s 环境\nPart 2: 如何创建一个 Spring Boot Kubernetes Controller. 第一部分链接在博客正文中。这两篇文章都受到了我和我出色的队友 Cora Iberkleid 几个月前在 Spring I/O 上的一次演讲的启发。这是一个非常出色的系列，我希望你能看看。\nRaghav2211/spring-web-flux-todo-app: Spring boot Todo application\nCloud gateway 和 Config server 构建服务的例子\nSpring Boot 2.6.10 发布\nSpring Boot 2.7.2 发布\nSpring Boot 3.0.0-M4 发布\n支持 Elasticsearch’s new Java client，支持 Flyway 9，支持 Hibernate 6.1\nSpring Shell 2.1.0 发布\n动态控制命令， 注解更新，主题，界面组件，GraalVM支持，模板\nSpring Tips: Kubernetes Native Java (Redux, 2022)\n在 Kubernetes 中构建 Spring Boot 应用\nSpring for GraphQL 1.0.1 发布\n你知道有一个 IntelliJ 插件可以帮助你命名吗?\n**Naming Is Hard，**新项目和模块命名\nTagir Valeev 的 Twitter\n为什么 Maven 找不到要运行的 JUnit 测试\nMaven 可能找不到要运行的 JUnit 测试的几种情况\n它来了！ GraalVM 可达性索引现在已发布。该库包含一个社区驱动的 GraalVM 可达性元数据，用于开源库。我们 Spring 团队和其他人都为此做出了贡献。\n因为 Java 的动态特性(包括反射、资源访问、动态代理和序列化)，在构建 Native Image 的时候可能某些元素不包含在可执行文件中。这个工程为 Native Image 构建器提供一些三方库等的可达性元数据。\n我喜欢 Gunnar Morling 的这篇帖子，关于你应该对 Maven 构建做些什么以使它们更平易近人和值得信赖\n","date":"26 July 2022","externalUrl":null,"permalink":"/springweek/this_week_in_spring_20220726/","section":"Springweeks","summary":"","title":"Spring周报 - 2022.7.26","type":"springweek"},{"content":"Hi, Spring 粉丝们! 欢迎收看另一期的 Spring 周报! 这周我正试着结束一些事情，然后和我的家人一起去度假。这将是一个了不起的时刻，真的！但这并不能阻止 Springdom 广阔世界中的新奇事物和新闻泛滥，所以本周我们有很多内容要介绍。让我们开始吧！\nHi, Spring fans! Welcome to another installment of This Week in Spring! This week I’m trying to wind down some threads and take some vacation with my family. It’s going to be an amazing time, indeed! But that doesn’t stop the deluge of novelties and news in the wide world of Springdom, so we’ve got a lot to cover this week. Let’s get to it!\n不过，在开始之前，我有一个新的“Spring 小贴士”剧集将在周三早上午夜播出，所以请注意 :)\nBefore I go, though, I have a new “Spring Tips” episode dropping Wednesday morning, at midnight, so be on the look out :)\n(播客)A Bootiful Podcast: Nate Schutta: The Thinking Person’s Architect, My Friend, and Teammate\nKubernetes Gateway API 发布 Beta\n使用 OAuth2 资源服务器和外部授权服务器进行 JWT 身份验证\n用 Kotlin 和 Spring Boot 构建 Rest API\ntoedter/spring-hateoas-jsonapi v1.5.1 发布\nbug 修改和依赖升级\nSpring Boot – 使用 Testcontainers 进行 Keycloak 集成测试\ntestcontainers 能够让你实现通过编程语言去启动Docker容器，并在程序测试结束后，自动关闭容器\nSpring Boot – 用 Testcontainers 测试 Redis\nSpring Data 2022.0.0-M5 2021.2.2 和 2021.1.6 发布\n依赖升级\nSpring Data Rest – 序列化实体 ID\n解释为什么 Spring Data Rest 默认不序列化实体 ID。此外，讨论改变这种行为的各种解决方案。\nSpring Framework 6.0.0-M5 和 5.3.22 发布\nSpring Native 0.12.1 发布\nbug 修改和依赖升级\nSpring Security 5.8.0-M1 和 6.0.0-M6 发布\nSpring Tips: Kubernetes Native Java (Redux, 2022)\nSpring Boot 和 Spring Cloud 支持 Kubernetes 的一些 feature\n","date":"19 July 2022","externalUrl":null,"permalink":"/springweek/this_week_in_spring_20220719/","section":"Springweeks","summary":"","title":"Spring周报 - 2022.7.19","type":"springweek"},{"content":"Hi，Spring 粉丝们！欢迎收看另一期的 Spring 周报！你好吗？本周，我在阳光明媚的华盛顿州西雅图给您写这个，我们将在那里进行 SpringOne Tour 系列的下一部分。再次看到所有这些有趣和友好的面孔并见到人们真是太有趣了，其中许多人在疫情之前我就再也没有见过！在这里见到一些来自微软和 AWS 等大型云公司的朋友，我也很开心。了解人们如何使用 Spring 的最新和最强大的技术来构建针对这些云平台的惊人系统和软件总是很有趣的。\nHi, Spring fans! Welcome to another installment of This Week in Spring! How are you? This week I’m writing you from sunny Seattle, Washington, where we’re having our next installment of the SpringOne Tour series. It’s been a ton of fun seeing all these fun and friendly faces again and getting to see people, many of whom I haven’t seen since before the pandemic! I’ve also had a lot of fun seeing some friends from some of the big cloud companies here, Microsoft and AWS. It’s always interesting to learn how people are using the latest and greatest from Spring to build amazing systems and software targeting these cloud platforms.\n本周我们有很多内容要介绍，所以让我们开始吧！\nWe’ve got a lot to cover this week so let’s dive right into it!\n(播客)In last week’s podcast, I talked to Kubernetes contributor and fellow Tanzu Developer Advocate Leigh Capili\n用 OpenTelemetry, Spring Cloud Sleuth, Kafka 和 Jaeger 做分布式链路追踪\nKubernetes 里 Ingress 和 Load Balancer 对比\n用 Spring Annotations 实例化同一个 Class 的多个 Bean\nJava 14 Records 和 Lombok 对比\n我整理了这个小例子，感谢 Reactor 负责人 Simon Baslé，展示了如何使用 Project Reactor 中的 expand 扩展运算符处理分页： 反应式应用程序中的分页。它着眼于如何在新数据（在数据分页场景中）可用时优雅地扩展反应流的内容。\nSpring Shell 2.1.0-RC1 发布\n推送 Docker 镜像到自建镜像仓库\nSpring for Apache Kafka 2.9.0-RC1 发布\n使用 kafka-clients 3.2.0\n非堵塞重试 bootstrapping 现在更加健壮\n新的错误处理模式\n默认情况下，发生错误后，DefaultErrorHandler 对上次轮询的剩余记录执行搜索，并在下一次轮询时从代理重新获取它们。错误率高且 max.poll.records 较大的情况下，这可能会对网络造成不必要的压力。出于这个原因，错误处理程序有一个新属性 seekAfterError，当设置为 false 时，剩余的记录保留在内存中，消费者暂停下一次轮询（或者如果错误处理程序配置为使用 ContainerPausingBackOffHandler 则进行多次轮询）。\n暂停容器\n默认情况下，当您 pause() 容器时，它实际上会在上一次轮询的所有记录都已处理后暂停。此版本添加了 pauseImmediate 容器属性，当该属性为 true 时，容器在处理当前记录后暂停。\n这篇 Apache Maven 生存指南非常有趣，至少对我来说是这样！\n","date":"12 July 2022","externalUrl":null,"permalink":"/springweek/this_week_in_spring_20220712/","section":"Springweeks","summary":"","title":"Spring周报 - 2022.7.12","type":"springweek"},{"content":"Hi, Spring 粉丝们! 欢迎收看新一期 Spring 周报！这周对我来说很奇怪。今天是星期二！但在美国，我们刚刚庆祝了 7 月 4 日美国独立日，我和许多美国人一样，度过了一个长周末。花了一些时间和家人一起去北部进行了一次小公路旅行，参观了沙斯塔山、火山口湖、拉森国家公园等。这很有趣，而且开了很久车！无论如何，这一切有点模糊，感觉就像一个周末，而今天感觉就像星期一。我才意识到今天是星期二！你知道那是什么意思吗？现在是我们每周进入 Springdom 狂野而美妙的世界的时候了！\nHi, Spring fans! Welcome to another installment of This Week in Spring! This week’s all sorts of weird for me. It’s Tuesday! But here in the US we just celebrated the 4th of July, and I, like many Americans, took a long weekend. Took some time with the family to do a little road trip up north to visit Mt. Shasta, Crater Lake, Lassen National Park, etc. It was a ton of fun, and a lot of driving! Anyway, it all kinda blurs together and felt like just one weekend, and today feels like Monday. I only just realized it was Tuesday! And you know what that means? It’s time for our weekly dive into the wild and wonderful world of Springdom!\n八篇的最后一篇 用 Spring for GraphQL 构建 GraphQL 应用! 查看该页面并找到所有八个视频的链接，每个视频都介绍了强大而新颖的 Spring for GraphQL 项目的一个有趣维度。\n（播客）In last week’s installment of A Bootiful Podcast, I talk to Spring Developer Advocate Dan Vega\n在 Spring Cloud Gateway 中处理 http 返回体\nSpring Cloud 2020.0.6 发布\nbug 修复和依赖升级\n在 MongoDB 里用 UUID 当实体 ID\n我喜欢 Dzone 上的这篇文章: 所有 Kubernetes Ingresses 都一样么?\n想了解更多如何在 Spring Boot 2.x 里构建 GraalVM native images 么? 看看我写的这篇文章 InfoQ on Spring Native and GraalVM\nVaadin 的好人们整理了一个很好的教程，介绍如何使用他们的 Java Web 框架 Hilla 构建全栈 Spring Boot 应用程序\nSivalabs 的朋友整理了一个有趣的教程，介绍如何一起使用 Spring Boot 和 Kubernetes\nSpring Boot 团队成员 Stéphane Nicoll 最近为 DevFest Lille 做了一个法语演讲，介绍了 Spring AOT\n","date":"5 July 2022","externalUrl":null,"permalink":"/springweek/this_week_in_spring_20220705/","section":"Springweeks","summary":"","title":"Spring周报 - 2022.7.5","type":"springweek"},{"content":"Hi，Spring粉丝们，欢迎来到新一期的Spring周报！我在大苹果(纽约别称)纽约写的这个！我来这里参加 SpringOne Tour 2022 NYC。这是我疫情后第一次回纽约，这真是太有趣了。我见到了很多多年没见的朋友。我甚至碰到了一些我不知道会和我同时在城里的人。纽约就像一块有趣的磁铁，吸引着有趣的人。总之，我们有很多东西要写在这里，所以让我们开始吧。\nHi, Spring fans! Welcome to another installment of This Week in Spring! I’m writing this from the Big Apple, New York City! I’m here for the SpringOne Tour 2022 NYC event. This is my first time back in New York City since before the pandemic and it has been so much fun. I’ve been catching up with people I’ve not seen in years. I even accidentally bumped into people I had no idea was going to also be in town at the same time as I was. New York City is like a magnet for fun, and for fun people. Anyway, we’ve got a lot of stuff to get to this roundup, so let’s dive right into it.\n同时：如果你正在看这个，今天 - 6月28号 - 申请参加 SpringOne 2022 的最后一天，今年12月在我的家乡加州旧金山举办！\nAlso: if you’re just reading this, today - the 28th of June - is the last day to submit to SpringOne 2022, being held in my hometown of San Francisco, California, in December of this year!\nA Bootiful Podcast(播客名): JVM 和 .NET 传奇 Ted Neward 做客… 几乎所有事\n用 Thymeleaf 调用 JavaScript 方法\nSpring Boot 2.6.9 发布\nSpring Boot 2.7.1 发布\nSpring Tips: 学习 Spring 结合 GraphQL (最后两章: 第 7 章和第 8 章)\nSpring Data 中 MongoDB 文档的唯一字段\nJava 19 Build 27, 早期版本, 已发布\n关于这一点, Java 20 Build 2 - 是的, Java 20! - 也已经发布!\nApache 软件基金会发布了 Apache Tomcat 8.5.81\n这是一篇关于 Spring 和 static 代码的第一原则(first principles)的有趣文章\n主要讨论了在 IoC 容器里的静态代码和工具类静态代码放在 IoC 容器里的问题\n我喜欢这篇文章, 使用组合、排列和乘积进行详尽的 JUnit 5 测试\n","date":"28 June 2022","externalUrl":null,"permalink":"/springweek/this_week_in_spring_20220628/","section":"Springweeks","summary":"","title":"Spring周报 - 2022.6.28","type":"springweek"},{"content":"Hi，Spring 粉丝们！欢迎来到新一期的 Spring 周报！你们怎么样？自从我们上次聊天以来，已经过了一段时间。上周这个时候我在德国。现在，我回到了美丽的旧金山。今天天气将攀升至 84 华氏度！在旧金山，一年中的任何时候，这都是非常不寻常的。旧金山的大多数地方都没有空调。有些有暖气。我在 2014 年买了一套全新的公寓，没有空调。你只要打开窗户。当然，我很荣幸今天有空调。我提到这一切是为了说这里很热！我替老人担心！当天气变得如此炎热时，基督教青年会和其他组织通常会邀请老年人进来呼吸凉爽的空气和水。这很危险。有些日子会变得更热。非常罕见，但确实会发生。我希望你们都做得很好。照顾好自己和彼此，我的朋友们。\nHi, Spring fans! Welcome to another installment of This Week in Spring! How are you? It’s been a hot minute since we last chatted. I was in Germany this time last week. Now, I’m back in beautiful San Francisco. Today the weather will climb to a monumental 84 F! That’s very unusual, for any time of the year, here in San Francisco. Most places here in San Francisco don’t have air conditioning. Some have heating. I bought a brand new condo in 2014 and it didn’t have air conditioning. You just open the window. I am privileged enough that I have air conditioning today, of course. I mention all this to say that it’s hot here! I worry for the elderly! When it gets this hot, the YMCA and other organizations typically invite elderly people to come in and get some cool air and water. It’s dangerous. Some days it gets even hotter. Very rare, but it does happen. I hope you’re all doing well. Take care of yourselves and each other, my friends.\n而且，说到火爆，让我们看看本周最新最热门的新闻综述！\nAnd, speaking of being hot, let’s look at this week’s roundup of the latest-and-greatest that’s hot off the press!\nBootiful Podcast(播客): Spring Framework contributor Sébastien Deleuze on GraalVM, AOT, project Leyden, and WebAssembly\nSpring Cloud Function CVE 漏洞报告发布\nDebugging Collections, Streams 和变量监视渲染器\n介绍了在 Idea 里怎么 debug collection 和 stream，还有一些变量监视值怎么渲染的设置技巧\nFlux.create 和 Flux.generate 的区别\n用 @ExceptionHandler 处理 Spring Security 的异常\nSpring Data 里设置 MongoDB 复合主键\nSpring Authorization Server 0.3.1 发布\n从 JDK11 降到 JDK1.8，一些小改进和 bug 修复\n用 Spring Boot Admin 监控 Spring Boot 应用\nSpring Data 2021.2.1 和 2021.1.5 发布\n一些 bug 修复和依赖升级，Spring Boot 2.7.1 和 2.6.9 会在之后升级到这个版本\n此外，还包括对一个漏洞的修复: CVE-2022-22980 SpEL 表达式注入漏洞\nSpring Data JPA – 没有运行的数据库时启动一个应用\nSpring Data MongoDB SpEL 表达式注入漏洞 (CVE-2022-22980)\nSpring Framework 5.3.21 发布\n一些 bug 修复和改进\nSpring Security 5.7.2 and 5.6.6 发布\nbug 修复和改进\nSpring Tools 4.15.0 发布\n主要变化：更新到 Eclipse 2022-06\nSpring Tools 4.15.1 发布\nbug 修复和改进\nSpring Boot 的默认内存配置是什么?\nDocker 私有仓库教程\nJava 有析构函数么?\n","date":"21 June 2022","externalUrl":null,"permalink":"/springweek/this_week_in_spring_20220621/","section":"Springweeks","summary":"","title":"Spring周报 - 2022.6.21","type":"springweek"},{"content":" 软件版本 # Traefik 版本：2.4.0\n背景 # Traefik 是一个 go 实现的高性能 API 网关，本身支持多种配置加载方式，包括 file, k8s, consul 等。\n我们之前用的file作为配置文件，路由配置文件中会配置应用的路由信息和应用的负载信息。一个服务的配置信息类似下面这样：\nhttp: services: dev-service: loadBalancer: servers: - url: http://aliyun-slb-service:8080 passHostHeader: true routers: dev: entryPoints: - test service: dev-service rule: PathPrefix(`/path`) 服务的请求 url 配置的是服务前面挂载的负载均衡(用的阿里云的SLB)，服务上下线等会从SLB上注册反注册。这样所有对外的服务前面都需要挂载一层负载，增加了复杂度和调用链长度。更好的做法是网关能自动从注册中心拉取服务的地址列表，就不需要在服务上再挂载一个负载均衡了。\n我们现在后端注册中心使用的是 nacos，Traefik 现版本是不支持 nacos 的，需要简单修改下源码，添加一个 nacos 的 provider。\nConsul provider # Traefik 虽然没有 nacos 的实现，但是有别的注册中心的实现，比如 consul。在新添加 provider 之前可以先看下 Traefik consul 是怎么实现的。\nconsul 逻辑是，服务信息会从注册的服务上直接读取，路由信息等配置都是tag信息，类似下图这样，需要每个配置字段都设定一个tag。\n(图从网上随便搜的)\n这样的话每个服务都需要在consul中注册很多的tag，使用起来不太方便，也不方便维护和快速查找。\n考虑到方便维护和兼容之前的文件配置，想要实现的效果是路由信息还是从文件中读取，服务地址信息从 nacos 中读取。之后与文件的配置文件合并，为了兼容之前的配置，文件中的服务信息也会读取，如果 nacos 中有重复的服务，就用 nacos 的覆盖文件的。\n最终的配置文件 # 代码修改好后的配置文件就会像下面这样\n静态配置里的 provider 配置 # providers: nacos: file: watch: true filename: \u0026#34;/Users/d/depend/config/route/test.yaml\u0026#34; debugLogGeneratedTemplate: true endpoint: host: devnacos.inc.com port: 8848 group: aaa logDir: \u0026#34;/tmp/nacos/log\u0026#34; cacheDir: \u0026#34;/tmp/nacos/cache\u0026#34; 配置里有两部分：\n第一部分是 file，这个和自带的 file provider 逻辑一样 第二部分是 endpoint，这个是 nacos SDK 的配置 路由配置 # http: routers: dev: entryPoints: - test service: nacos中的服务名 rule: PathPrefix(`/path`) middlewares: - test-retry middlewares: test-retry: retry: attempts: 3 initialInterval: 10ms 去掉了 services 配置，从 nacos 中读取，路由的 service 对应 nacos 中的服务名。\n这里加的retry插件主要是用来failover的，服务短时不可用可以故障转移，请求下个服务实例（比如服务下线时网关还未接到下线通知，请求了已下线服务）\n代码实现 # 接下来看下代码实现，需要为 Traefik 添加一个叫 nacos 的 provider。\n主逻辑 # 首先在 pkg/provider 下添加一个文件夹 nacos，在里面添加个 nacos.go 文件，主要逻辑都会在这个文件里实现。\n首先添加从文件读取配置的代码，这里大部分代码都可以复用 Traefik 自带的文件解析 provider\n// 把文件解析成配置，复用 Traefik 原有代码 fileProvider := file.Provider{ Directory: p.File.Directory, Watch: p.File.Watch, Filename: p.File.Filename, DebugLogGeneratedTemplate: p.File.DebugLogGeneratedTemplate, } fileConfiguration, err := fileProvider.BuildConfiguration() // 注册文件变化监听，因为监听回调逻辑不一样，这里自己实现下 if p.File.Watch { var watchItem string switch { case len(p.File.Directory) \u0026gt; 0: watchItem = p.File.Directory case len(p.File.Filename) \u0026gt; 0: watchItem = filepath.Dir(p.File.Filename) default: return nil, errors.New(\u0026#34;error using file configuration provider, neither filename or directory defined\u0026#34;) } if err := p.addWatcher(pool, watchItem, configurationChan, p.watcherCallback); err != nil { return nil, err } } 回调的代码省略，见下面的完整代码。\n然后添加 nacos 的代码。\n先安装 SDK\ngo get -u github.com/nacos-group/nacos-sdk-go 和 nacos 的交互主要是获取实例信息和注册服务变化监听。\n// 首先初始化 nacos 实例 p.client, err = createClient(p.EndPoint) // 获取需要的服务，循环获取地址。我这里简化了，获取了所有服务 services, err := p.fetchServices() for _, service := range services { instances, err := p.client.SelectAllInstances(vo.SelectAllInstancesParam{ ServiceName: service, GroupName: p.EndPoint.Group, //HealthyOnly: true, }) // ... // 注册服务变化的监听 err = p.client.Subscribe(\u0026amp;vo.SubscribeParam{ ServiceName: service, GroupName: p.EndPoint.Group, SubscribeCallback: func(services []model.SubscribeService, err error) { p.nacosCallback(\u0026amp;services, configurationChan) }, }) // ... } 之后还需要注册个定时任务，来定时拉取最新的服务状态，做个兜底。\npool.GoCtx(func(routineCtx context.Context) { ctxLog := log.With(routineCtx, log.Str(log.ProviderName, \u0026#34;nacos\u0026#34;)) logger := log.FromContext(ctxLog) operation := func() error { ticker := time.NewTicker(15 * time.Second) for { select { case \u0026lt;-ticker.C: _, err = p.nacosProvide(configurationChan) if err != nil { logger.Errorf(\u0026#34;error get nacos service data, %v\u0026#34;, err) return err } configuration := p.buildConfiguration() sendConfigToChannel(configurationChan, configuration) case \u0026lt;-routineCtx.Done(): ticker.Stop() return nil } } } notify := func(err error, time time.Duration) { logger.Errorf(\u0026#34;Provider connection error %+v, retrying in %s\u0026#34;, err, time) } err := backoff.RetryNotify(safe.OperationWithRecover(operation), backoff.WithContext(job.NewBackOff(backoff.NewExponentialBackOff()), ctxLog), notify) if err != nil { logger.Errorf(\u0026#34;Cannot connect to nacos server %+v\u0026#34;, err) } }) 最终的 nacos 文件\npackage nacos import ( \u0026#34;context\u0026#34; \u0026#34;errors\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;github.com/cenkalti/backoff/v4\u0026#34; \u0026#34;github.com/nacos-group/nacos-sdk-go/clients\u0026#34; \u0026#34;github.com/nacos-group/nacos-sdk-go/clients/naming_client\u0026#34; \u0026#34;github.com/nacos-group/nacos-sdk-go/common/constant\u0026#34; \u0026#34;github.com/nacos-group/nacos-sdk-go/common/logger\u0026#34; \u0026#34;github.com/nacos-group/nacos-sdk-go/model\u0026#34; \u0026#34;github.com/nacos-group/nacos-sdk-go/vo\u0026#34; \u0026#34;github.com/traefik/traefik/v2/pkg/config/dynamic\u0026#34; \u0026#34;github.com/traefik/traefik/v2/pkg/job\u0026#34; \u0026#34;github.com/traefik/traefik/v2/pkg/log\u0026#34; \u0026#34;github.com/traefik/traefik/v2/pkg/provider/file\u0026#34; \u0026#34;github.com/traefik/traefik/v2/pkg/safe\u0026#34; \u0026#34;gopkg.in/fsnotify.v1\u0026#34; \u0026#34;os\u0026#34; \u0026#34;path/filepath\u0026#34; \u0026#34;strconv\u0026#34; \u0026#34;strings\u0026#34; \u0026#34;time\u0026#34; ) type Provider struct { File *FileConfig `description:\u0026#34;Nacos file settings\u0026#34; json:\u0026#34;file,omitempty\u0026#34; toml:\u0026#34;file,omitempty\u0026#34; yaml:\u0026#34;file,omitempty\u0026#34; export:\u0026#34;true\u0026#34;` EndPoint *EndpointConfig `description:\u0026#34;Nacos endpoint settings\u0026#34; json:\u0026#34;endpoint,omitempty\u0026#34; toml:\u0026#34;endpoint,omitempty\u0026#34; yaml:\u0026#34;endpoint,omitempty\u0026#34; export:\u0026#34;true\u0026#34;` client naming_client.INamingClient fileConfig *dynamic.Configuration nacosServices *map[string]*dynamic.Service } type FileConfig struct { Directory string `description:\u0026#34;Load dynamic configuration from one or more .toml or .yml files in a directory.\u0026#34; json:\u0026#34;directory,omitempty\u0026#34; toml:\u0026#34;directory,omitempty\u0026#34; yaml:\u0026#34;directory,omitempty\u0026#34; export:\u0026#34;true\u0026#34;` Watch bool `description:\u0026#34;Watch provider.\u0026#34; json:\u0026#34;watch,omitempty\u0026#34; toml:\u0026#34;watch,omitempty\u0026#34; yaml:\u0026#34;watch,omitempty\u0026#34; export:\u0026#34;true\u0026#34;` Filename string `description:\u0026#34;Load dynamic configuration from a file.\u0026#34; json:\u0026#34;filename,omitempty\u0026#34; toml:\u0026#34;filename,omitempty\u0026#34; yaml:\u0026#34;filename,omitempty\u0026#34; export:\u0026#34;true\u0026#34;` DebugLogGeneratedTemplate bool `description:\u0026#34;Enable debug logging of generated configuration template.\u0026#34; json:\u0026#34;debugLogGeneratedTemplate,omitempty\u0026#34; toml:\u0026#34;debugLogGeneratedTemplate,omitempty\u0026#34; yaml:\u0026#34;debugLogGeneratedTemplate,omitempty\u0026#34; export:\u0026#34;true\u0026#34;` } type EndpointConfig struct { Host string `description:\u0026#34;The host of the Nacos server\u0026#34; json:\u0026#34;host,omitempty\u0026#34; toml:\u0026#34;host,omitempty\u0026#34; yaml:\u0026#34;host,omitempty\u0026#34; export:\u0026#34;true\u0026#34;` Port uint64 `description:\u0026#34;The port of the Nacos server\u0026#34; json:\u0026#34;port,omitempty\u0026#34; toml:\u0026#34;port,omitempty\u0026#34; yaml:\u0026#34;port,omitempty\u0026#34; export:\u0026#34;true\u0026#34;` Group string `description:\u0026#34;The group of the Nacos server\u0026#34; json:\u0026#34;group,omitempty\u0026#34; toml:\u0026#34;group,omitempty\u0026#34; yaml:\u0026#34;group,omitempty\u0026#34; export:\u0026#34;true\u0026#34;` LogDir string `description:\u0026#34;The logDir of the Nacos server\u0026#34; json:\u0026#34;logDir,omitempty\u0026#34; toml:\u0026#34;logDir,omitempty\u0026#34; yaml:\u0026#34;logDir,omitempty\u0026#34; export:\u0026#34;true\u0026#34;` CacheDir string `description:\u0026#34;The cacheDir of the Nacos server\u0026#34; json:\u0026#34;cacheDir,omitempty\u0026#34; toml:\u0026#34;cacheDir,omitempty\u0026#34; yaml:\u0026#34;cacheDir,omitempty\u0026#34; export:\u0026#34;true\u0026#34;` } func (p *Provider) Init() error { return nil } func (p *Provider) Provide(configurationChan chan\u0026lt;- dynamic.Message, pool *safe.Pool) error { //logger := log.WithoutContext().WithField(log.ProviderName, \u0026#34;nacos\u0026#34;) _, err := p.fileProvide(configurationChan, pool) if err != nil { return err } p.client, err = createClient(p.EndPoint) if err != nil { return fmt.Errorf(\u0026#34;error create nacos client, %w\u0026#34;, err) } _, err = p.nacosProvide(configurationChan) if err != nil { return err } configuration := p.buildConfiguration() sendConfigToChannel(configurationChan, configuration) pool.GoCtx(func(routineCtx context.Context) { ctxLog := log.With(routineCtx, log.Str(log.ProviderName, \u0026#34;nacos\u0026#34;)) logger := log.FromContext(ctxLog) operation := func() error { ticker := time.NewTicker(15 * time.Second) for { select { case \u0026lt;-ticker.C: _, err = p.nacosProvide(configurationChan) if err != nil { logger.Errorf(\u0026#34;error get nacos service data, %v\u0026#34;, err) return err } configuration := p.buildConfiguration() sendConfigToChannel(configurationChan, configuration) case \u0026lt;-routineCtx.Done(): ticker.Stop() return nil } } } notify := func(err error, time time.Duration) { logger.Errorf(\u0026#34;Provider connection error %+v, retrying in %s\u0026#34;, err, time) } err := backoff.RetryNotify(safe.OperationWithRecover(operation), backoff.WithContext(job.NewBackOff(backoff.NewExponentialBackOff()), ctxLog), notify) if err != nil { logger.Errorf(\u0026#34;Cannot connect to nacos server %+v\u0026#34;, err) } }) return nil } func (p *Provider) buildConfiguration() *dynamic.Configuration { config := p.fileConfig for k, v := range *p.nacosServices { config.HTTP.Services[k] = v } return config } func (p *Provider) nacosProvide(configurationChan chan\u0026lt;- dynamic.Message) (*map[string]*dynamic.Service, error) { services, err := p.fetchServices() if err != nil { return nil, err } serviceMap := make(map[string]*dynamic.Service) for _, service := range services { instances, err := p.client.SelectAllInstances(vo.SelectAllInstancesParam{ ServiceName: service, GroupName: p.EndPoint.Group, //HealthyOnly: true, }) if err != nil { logger.Errorf(\u0026#34;Skip item %s: %v\u0026#34;, service, err) continue } err = p.client.Subscribe(\u0026amp;vo.SubscribeParam{ ServiceName: service, GroupName: p.EndPoint.Group, SubscribeCallback: func(services []model.SubscribeService, err error) { p.nacosCallback(\u0026amp;services, configurationChan) }, }) if err != nil { logger.Errorf(\u0026#34;Skip item %s: %v\u0026#34;, service, err) return nil, err } var servers []dynamic.Server for _, instance := range instances { server := dynamic.Server{ URL: \u0026#34;http://\u0026#34; + instance.Ip + \u0026#34;:\u0026#34; + strconv.FormatUint(instance.Port, 10), } servers = append(servers, server) } b := true serviceMap[service] = \u0026amp;dynamic.Service{ LoadBalancer: \u0026amp;dynamic.ServersLoadBalancer{ Sticky: nil, Servers: servers, HealthCheck: nil, PassHostHeader: \u0026amp;b, ResponseForwarding: nil, }, } } p.nacosServices = \u0026amp;serviceMap return \u0026amp;serviceMap, nil } func (p *Provider) nacosCallback(services *[]model.SubscribeService, configurationChan chan\u0026lt;- dynamic.Message) { serviceSet := make(map[string]bool) for _, service := range *services { nameSplit := strings.Split(service.ServiceName, \u0026#34;@\u0026#34;) serviceName := nameSplit[2] serviceSet[serviceName] = true } for s := range serviceSet { nacosServices := *p.nacosServices serviceName := s instances, err := p.client.SelectInstances(vo.SelectInstancesParam{ ServiceName: serviceName, GroupName: p.EndPoint.Group, HealthyOnly: true, }) if err != nil { logger.Errorf(\u0026#34;Skip item %s: %v\u0026#34;, s, err) continue } var servers []dynamic.Server for _, instance := range instances { server := dynamic.Server{ URL: \u0026#34;http://\u0026#34; + instance.Ip + \u0026#34;:\u0026#34; + strconv.FormatUint(instance.Port, 10), } servers = append(servers, server) } serviceLB := nacosServices[serviceName] if serviceLB == nil { b := true nacosServices[serviceName] = \u0026amp;dynamic.Service{ LoadBalancer: \u0026amp;dynamic.ServersLoadBalancer{ Sticky: nil, Servers: servers, HealthCheck: nil, PassHostHeader: \u0026amp;b, ResponseForwarding: nil, }, } } else { nacosServices[serviceName].LoadBalancer.Servers = servers } } configuration := p.buildConfiguration() sendConfigToChannel(configurationChan, configuration) } func (p *Provider) fileProvide(configurationChan chan\u0026lt;- dynamic.Message, pool *safe.Pool) (*dynamic.Configuration, error) { fileProvider := file.Provider{ Directory: p.File.Directory, Watch: p.File.Watch, Filename: p.File.Filename, DebugLogGeneratedTemplate: p.File.DebugLogGeneratedTemplate, } fileConfiguration, err := fileProvider.BuildConfiguration() if err != nil { return nil, err } //fileConfiguration.HTTP.Services = make(map[string]*dynamic.Service) //fileConfiguration.TCP.Services = make(map[string]*dynamic.TCPService) //fileConfiguration.UDP.Services = make(map[string]*dynamic.UDPService) p.fileConfig = fileConfiguration if p.File.Watch { var watchItem string switch { case len(p.File.Directory) \u0026gt; 0: watchItem = p.File.Directory case len(p.File.Filename) \u0026gt; 0: watchItem = filepath.Dir(p.File.Filename) default: return nil, errors.New(\u0026#34;error using file configuration provider, neither filename or directory defined\u0026#34;) } if err := p.addWatcher(pool, watchItem, configurationChan, p.watcherCallback); err != nil { return nil, err } } return fileConfiguration, nil } func (p *Provider) addWatcher(pool *safe.Pool, directory string, configurationChan chan\u0026lt;- dynamic.Message, callback func(chan\u0026lt;- dynamic.Message, fsnotify.Event)) error { watcher, err := fsnotify.NewWatcher() if err != nil { return fmt.Errorf(\u0026#34;error creating file watcher: %w\u0026#34;, err) } err = watcher.Add(directory) if err != nil { return fmt.Errorf(\u0026#34;error adding file watcher: %w\u0026#34;, err) } // Process events pool.GoCtx(func(ctx context.Context) { defer watcher.Close() for { select { case \u0026lt;-ctx.Done(): return case evt := \u0026lt;-watcher.Events: if p.File.Directory == \u0026#34;\u0026#34; { _, evtFileName := filepath.Split(evt.Name) _, confFileName := filepath.Split(p.File.Filename) if evtFileName == confFileName { callback(configurationChan, evt) } } else { callback(configurationChan, evt) } case err := \u0026lt;-watcher.Errors: log.WithoutContext().WithField(log.ProviderName, \u0026#34;nacos\u0026#34;).Errorf(\u0026#34;Watcher event error: %s\u0026#34;, err) } } }) return nil } func (p *Provider) watcherCallback(configurationChan chan\u0026lt;- dynamic.Message, event fsnotify.Event) { watchItem := p.File.Filename if len(p.File.Directory) \u0026gt; 0 { watchItem = p.File.Directory } logger := log.WithoutContext().WithField(log.ProviderName, \u0026#34;nacos\u0026#34;) if _, err := os.Stat(watchItem); err != nil { logger.Errorf(\u0026#34;Unable to watch %s : %v\u0026#34;, watchItem, err) return } fileProvider := file.Provider{ Directory: p.File.Directory, Watch: p.File.Watch, Filename: p.File.Filename, DebugLogGeneratedTemplate: p.File.DebugLogGeneratedTemplate, } configuration, err := fileProvider.BuildConfiguration() if err != nil { logger.Errorf(\u0026#34;Error occurred during watcher callback: %s\u0026#34;, err) return } p.fileConfig = configuration sendConfigToChannel(configurationChan, p.buildConfiguration()) } func createClient(cfg *EndpointConfig) (naming_client.INamingClient, error) { // server sc := []constant.ServerConfig{ *constant.NewServerConfig(cfg.Host, cfg.Port), } // client cc := *constant.NewClientConfig( //constant.WithNamespaceId(\u0026#34;e525eafa-f7d7-4029-83d9-008937f9d468\u0026#34;), constant.WithTimeoutMs(5000), constant.WithNotLoadCacheAtStart(true), constant.WithLogDir(cfg.LogDir), constant.WithCacheDir(cfg.CacheDir), constant.WithRotateTime(\u0026#34;1h\u0026#34;), constant.WithMaxAge(3), constant.WithLogLevel(\u0026#34;info\u0026#34;), ) namingClient, err := clients.NewNamingClient( vo.NacosClientParam{ ClientConfig: \u0026amp;cc, ServerConfigs: sc, }, ) if err != nil { panic(err) } return namingClient, nil } func (p *Provider) fetchServices() ([]string, error) { i := 1 var filtered []string for { serviceInfos, err := p.client.GetAllServicesInfo(vo.GetAllServiceInfoParam{ GroupName: p.EndPoint.Group, PageNo: uint32(i), PageSize: 100, }) if err != nil { return nil, err } if serviceInfos.Count == 0 { break } for _, serviceInfo := range serviceInfos.Doms { filtered = append(filtered, serviceInfo) } i = i + 1 } return filtered, nil } func sendConfigToChannel(configurationChan chan\u0026lt;- dynamic.Message, configuration *dynamic.Configuration) { configurationChan \u0026lt;- dynamic.Message{ ProviderName: \u0026#34;nacos\u0026#34;, Configuration: configuration, } } 配置解析 # 然后添加静态配置解析的代码\n在pkg/config/static/static_config.go 里添加 nacos 配置结构体：\ntype Providers struct { // 省略原有代码 Nacos *nacos.Provider `description:\u0026#34;Enable Nacos backend with default settings.\u0026#34; json:\u0026#34;nacos,omitempty\u0026#34; toml:\u0026#34;nacos,omitempty\u0026#34; yaml:\u0026#34;nacos,omitempty\u0026#34;` } 在pkg/provider/aggregator/aggregator.go 里添加 nacos 初始化：\nfunc NewProviderAggregator(conf static.Providers) ProviderAggregator { // 省略原有代码 if conf.Nacos != nil { p.quietAddProvider(conf.Nacos) } return p } 结尾 # 修改后 Traefik 就可以从注册中心自动拉取实例信息来负载了，方便了很多。上边的实例做了一些简化，比如从 nacos 获取了全量服务，在服务很多的情况下不太高效，可以只获取需要用的服务、路由信息修改可以放到配置中心或者修改下UI，就可以可视化操作了、等等。实际使用时可以根据自己的情况做下修改。\n","date":"12 June 2021","externalUrl":null,"permalink":"/posts/traefik_nacos_naming/","section":"Posts","summary":"","title":"为 Traefik 添加从 nacos 读取服务地址功能","type":"posts"},{"content":"版本：2.2.8\nTraefik 默认带很多插件，但是可能一些我们的个性化需求原生插件并不支持，这时候就需要自己开发插件了。在 2.3 版本之前 Traefik 不支持外挂插件，所以如果要添加插件的话我们需要修改源码。\n下面就以添加个验证token的插件作为演示。\n这个插件获取请求在header中添加的token，之后请求后端服务校验token是否正确，正确就继续请求后端，错误就直接返回错误信息。\n代码修改 # 我们要修改3个地方，\n添加插件执行文件 # 在 pkg/middleware/auth文件夹中添加插件主逻辑文件，这个位置可以根据自己需求修改。\npackage auth import ( \u0026#34;context\u0026#34; \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;github.com/containous/traefik/v2/pkg/config/dynamic\u0026#34; \u0026#34;github.com/containous/traefik/v2/pkg/log\u0026#34; \u0026#34;github.com/containous/traefik/v2/pkg/middlewares\u0026#34; \u0026#34;github.com/containous/traefik/v2/pkg/tracing\u0026#34; \u0026#34;github.com/opentracing/opentracing-go/ext\u0026#34; \u0026#34;io/ioutil\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;net/url\u0026#34; \u0026#34;strings\u0026#34; \u0026#34;time\u0026#34; ) const ( tokenTypeName = \u0026#34;TokenAuthType\u0026#34; ) type tokenAuth struct { address string next http.Handler name string client http.Client } type commonResponse struct { Status int32 `json:\u0026#34;status\u0026#34;` Message string `json:\u0026#34;message\u0026#34;` } // NewToken creates a passport auth middleware. func NewToken(ctx context.Context, next http.Handler, config dynamic.TokenAuth, name string) (http.Handler, error) { log.FromContext(middlewares.GetLoggerCtx(ctx, name, tokenTypeName)).Debug(\u0026#34;Creating middleware\u0026#34;) // 插件结构体 ta := \u0026amp;tokenAuth{ address: config.Address, next: next, name: name, } // 创建请求其他服务的 http client ta.client = http.Client{ CheckRedirect: func(r *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, Timeout: 30 * time.Second, } return ta, nil } func (ta *tokenAuth) GetTracingInformation() (string, ext.SpanKindEnum) { return ta.name, ext.SpanKindRPCClientEnum } func (ta tokenAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) { logger := log.FromContext(middlewares.GetLoggerCtx(req.Context(), ta.name, tokenTypeName)) errorMsg := []byte(\u0026#34;{\\\u0026#34;code\\\u0026#34;:10000,\\\u0026#34;message\\\u0026#34;:\\\u0026#34;token校验失败！\\\u0026#34;}\u0026#34;) // 从 header 中获取 token token := req.Header.Get(\u0026#34;token\u0026#34;) if token == \u0026#34;\u0026#34; { logMessage := fmt.Sprintf(\u0026#34;Error calling %s. Cause token is empty\u0026#34;, ta.address) traceAndResponseDebug(logger, rw, req, logMessage, []byte(\u0026#34;{\\\u0026#34;statue\\\u0026#34;:10000,\\\u0026#34;message\\\u0026#34;:\\\u0026#34;token is empty\\\u0026#34;}\u0026#34;), http.StatusBadRequest) return } // 以下都是请求其他服务验证 token // 构建请求体 form := url.Values{} form.Add(\u0026#34;token\u0026#34;, token) passportReq, err := http.NewRequest(http.MethodPost, ta.address, strings.NewReader(form.Encode())) tracing.LogRequest(tracing.GetSpan(req), passportReq) if err != nil { logMessage := fmt.Sprintf(\u0026#34;Error calling %s. Cause %s\u0026#34;, ta.address, err) traceAndResponseDebug(logger, rw, req, logMessage, errorMsg, http.StatusBadRequest) return } tracing.InjectRequestHeaders(req) passportReq.Header.Set(\u0026#34;Content-Type\u0026#34;, \u0026#34;application/x-www-form-urlencoded\u0026#34;) // post 请求 passportResponse, forwardErr := ta.client.Do(passportReq) if forwardErr != nil { logMessage := fmt.Sprintf(\u0026#34;Error calling %s. Cause: %s\u0026#34;, ta.address, forwardErr) traceAndResponseError(logger, rw, req, logMessage, errorMsg, http.StatusBadRequest) return } logger.Info(fmt.Sprintf(\u0026#34;Passport auth calling %s. Response: %+v\u0026#34;, ta.address, passportResponse)) // 读 body body, readError := ioutil.ReadAll(passportResponse.Body) if readError != nil { logMessage := fmt.Sprintf(\u0026#34;Error reading body %s. Cause: %s\u0026#34;, ta.address, readError) traceAndResponseError(logger, rw, req, logMessage, errorMsg, http.StatusBadRequest) return } defer passportResponse.Body.Close() if passportResponse.StatusCode != http.StatusOK { logMessage := fmt.Sprintf(\u0026#34;Remote error %s. StatusCode: %d\u0026#34;, ta.address, passportResponse.StatusCode) traceAndResponseDebug(logger, rw, req, logMessage, errorMsg, http.StatusBadRequest) return } // 解析 body var commonRes commonResponse err = json.Unmarshal(body, \u0026amp;commonRes) if err != nil { logMessage := fmt.Sprintf(\u0026#34;Body unmarshal error. Body: %s\u0026#34;, body) traceAndResponseError(logger, rw, req, logMessage, errorMsg, http.StatusBadRequest) return } // 判断返回值，非0代表验证失败 if commonRes.Status != 0 { logMessage := fmt.Sprintf(\u0026#34;Body status is not success. Status: %d\u0026#34;, commonRes.Status) traceAndResponseDebug(logger, rw, req, logMessage, errorMsg, http.StatusBadRequest) return } ta.next.ServeHTTP(rw, req) } func traceAndResponseDebug(logger log.Logger, rw http.ResponseWriter, req *http.Request, logMessage string, errorMsg []byte, status int) { logger.Debug(logMessage) tracing.SetErrorWithEvent(req, logMessage) rw.Header().Set(\u0026#34;Content-Type\u0026#34;, \u0026#34;application/json;charset=UTF-8\u0026#34;) rw.WriteHeader(status) _, _ = rw.Write(errorMsg) } func traceAndResponseInfo(logger log.Logger, rw http.ResponseWriter, req *http.Request, logMessage string, errorMsg []byte, status int) { logger.Info(logMessage) tracing.SetErrorWithEvent(req, logMessage) rw.Header().Set(\u0026#34;Content-Type\u0026#34;, \u0026#34;application/json;charset=UTF-8\u0026#34;) rw.WriteHeader(status) _, _ = rw.Write(errorMsg) } func traceAndResponseError(logger log.Logger, rw http.ResponseWriter, req *http.Request, logMessage string, errorMsg []byte, status int) { logger.Debug(logMessage) tracing.SetErrorWithEvent(req, logMessage) rw.Header().Set(\u0026#34;Content-Type\u0026#34;, \u0026#34;application/json;charset=UTF-8\u0026#34;) rw.WriteHeader(status) _, _ = rw.Write(errorMsg) } 添加动态配置映射 # 这里添加配置文件和实体的映射关系\n// pkg/config/dynamic/middlewares.go package dynamic /* ... */ // Middleware holds the Middleware configuration. type Middleware struct { /* ... */ // TokenAuth *TokenAuth `json:\u0026#34;tokenAuth,omitempty\u0026#34; toml:\u0026#34;tokenAuth,omitempty\u0026#34; yaml:\u0026#34;tokenAuth,omitempty\u0026#34;` } /* ... */ // TokenAuth type TokenAuth struct { Address string `json:\u0026#34;address,omitempty\u0026#34; toml:\u0026#34;address,omitempty\u0026#34; yaml:\u0026#34;address,omitempty\u0026#34;` } 构造插件示例 # 这里写创建插件实体的代码\n// pkg/server/middleware/middlewares.go func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) (alice.Constructor, error) { /* ... */ // TokenAuth if config.TokenAuth != nil { if middleware != nil { return nil, badConf } middleware = func(next http.Handler) (http.Handler, error) { return auth.NewToken(ctx, next, *config.TokenAuth, middlewareName) } } /* ... */ } 打包配置 # 直接用自带的打包命令打linux包(需要安装docker)。\nmake binary 之后会在dist文件夹下生成可执行文件。\n添加插件配置\nhttp: middlewares: # token验证 token-auth: tokenAuth: address: http://xxx.xxx.com/token_info 增加动态路由配置\nhttp: routers: svc: entryPoints: - web middlewares: - token-auth service: svc rule: PathPrefix(`/list`) 这样新添加的插件就能用了。\n","date":"12 October 2020","externalUrl":null,"permalink":"/posts/traefik_add_middleware/","section":"Posts","summary":"","title":"给 traefik 添加插件","type":"posts"},{"content":"原文：https://medium.com/machine-words/writing-technical-design-docs-71f446e42f2e\n软件工程师一个重要的技能就是写技术设计文档(TDDs)，也被称为工程设计文档(EDDs)。这篇文章我会谈些如何写好技术文档的建议和需要避免哪些错误。\n提醒：不同的团队对技术设计会有不同的标准和习惯。对于技术设计没有全行业的标准，也不能有，不同的开发团队对他们的场景会有不同的需求。我要描述的是基于我经验的一个可能的答案。\n设计过程 # 让我们从基本开始：什么是技术设计文档，它如何适合设计过程？\n一个技术设计文档描述了给定技术问题的一个解决方案。他是一个设计说明，或者设计蓝图，对于一个软件程序或者功能。\n技术设计文档的主要功能是和团队成员沟通要完成工作的技术细节。然而，有另一个目的也很重要：写技术设计文档的过程强迫你组织你的想法，考虑设计的每个方面，确认你没有疏漏任何事。\n技术设计文档经常是大型开发步骤如下几步中的一步：\n确定产品需求通常用产品需求文档（PRD）描述。PRD 定义了从用户或外界代理看系统需要做什么。 确定技术需求。产品需求翻译成技术需求 - 之前是系统需要完成什么，现在是怎么完成。这步的输出是技术需求文档（TRD）。 技术设计。这步包含上一步需求实现的技术描述。输出是 TDD。 实现。实现需求的步骤。 测试。基于 PRD TRD 进行测试确保实现了需求。 每步之间通常会有评审过程来保证没有错误。如果发现任何错误，误解或者歧义，必须在进入下一步前纠正。\n这些步骤不是固定的，可以根据实际情况改变。例如：\n对于不是很复杂的小型功能，步骤2和3经常会合并成一个文档。 如果功能需要实现很多未知功能或者需要一些调查，在最后的技术设计前可能需要先进行概念验证。 这个过程会发生在不同的开发粒度上。PRD / TRD / TDD 也许会包括一整个系统的设计，或者只有一个功能。在大多数情况下，这个过程是连环的 - 每个设计/实现基于前一步的工作。\nTRD 和 TDD 的界线有时会比较模糊。例如，假设你要开发一个用 RESTful API 通讯的服务。如果目标是调用一个已经开发完有文档的 API，那么这个 API 的描述是需求的一部分，应该包含在 TRD 中。如果目标是开发一个新 API，那么这个 API 的描述是设计的一部分，应该包含在 TDD 中。（然而，需求文档仍然需要些什么 API 需要完成）\n写 TDD # 目前，最佳实践是在合作文档系统上写技术文档，像 Google Docs 或 Confluence；然而，这不是必须的。重要的是有个方法让你的团队成员能评论文档和指出错误和疏漏。\n大多数 TDD 在1到10页之间。虽然 TDD 没有长度上限，但是太长的文档难以编辑和阅读。考虑把文档拆成单独步骤或者实现阶段。\n图解法很有帮助；有很多在线工具可以在文档中添加图，像 draw.io) 或者 Lucidchart。你也可以用像 Inkscape 的离线工具来生成 SVG 图。\n该文件应详尽；理想情况下，TDD作者以外的其他人应该可以按书面形式实施设计。例如，如果设计指定了一个 API 的实现，每个 API 端点都应该被记录。如果存在细微的设计选择，则应将其标出。\n避免常见写作错误 # 我在TDD中遇到的最常见的错误可能是缺乏上下文。就是说，作者用他们能解决的尽可能少的文字写下了解决问题的方式。但他们没有提供有关问题所在，为什么需要解决或选择该特定解决方案的后果的任何信息。\n而且，请务必记住可能的读者是谁，以及他们的理解水平。如果你用了一个读者可能不懂的术语，应该给它加个定义。\n良好的预发和拼写也是有帮助的。而且，避免文字游戏或者“可爱”的拼写；程序员喜欢玩文字游戏，我见过不止一例过度的文字游戏导致因为误解导致浪费团队精力。可以偶尔为功能和系统选择幽默或者出彩，容易记住的名字，这样可以帮助人们记住他们。但是不要通过它来炫耀你多么聪明。\n既然说到名字，要小心的选择他们；Mark Twain写过，“选择正确的单词，而不是第二个表亲”(Choose the right word, not it’s second cousin.)词汇量不多的工程师有个通病就是把一个词一遍又一遍的用在不同的事物上，导致混乱。例如，给一个类命名为\u0026quot;DataManager\u0026quot;是模糊的并且一点没说明它实际是做什么的；同样，叫\u0026quot;utils\u0026quot; 的包或目录可能包含任何东西。如果你需要一个更好的或更专业的词，可以查阅同义词库例如WordNet。\nTDD模板 # 当写TDD时，有一个标准模板是有用的。下面是我在很多工程中使用的一个模板。记住这个模板根据实际使用的地方做修改；你可以删除不需要的部分，添加其他部分，或者修改标题。\n\u0026lt;标题\u0026gt;TDD # 作者：\u0026lt;你的名字\u0026gt;\n介绍 # 基本原理 # 你想要做什么？现在的东西有什么问题？\n背景 # 描述理解文档所需的历史背景，包括遗留注意事项。\n术语 # 如果文档使用了特殊的单词或术语，列在这里。\n非目标 # 如果有相关问题你决定不在这个设计中解决，但是有些人希望你解决，列在这里。\n设计 # 首先对解决方案进行简要的描述，下面的章节会详细描述。\n系统架构 # 如果设计由多个大型组件之间的协作组成，在此处列出这些组件，或者更好地包括设计图。\n数据模型 # 描述数据怎么存储。这里可能包括数据库设计。\n接口/API定义 # 描述组件之间如何交互。例如，如果有个 REST 接口，写出 URL，传输数据结构和参数。\n业务逻辑 # 如果设计需要特殊的算法或逻辑，描述他们。\n迁移策略 # 如果设计引起对现有系统的非向后兼容更改，请描述依赖于系统的实体将要迁移到新设计的过程。\n影响 # 描述设计对系统整体性能，安全性和其他方面的潜在影响。\n风险 # 如果有任何风险或未知数，请在此处列出。另外，如果还有其他研究要做，也要提及。\n备选方案 # 如果还有其他可能的解决方案被考虑和拒绝，请在此处列出它们，以及未选择它们的原因。\n当然，这些部分只是起点。您可以根据需要添加其他部分，例如“设计注意事项”，“摘要”，“参考”，“致谢”等。\nTDD生命周期 # 在系统构建过程中，TDD将作为参考，以协调从事该项目的团队成员的活动。但是，在构建完成之后，TDD将继续存在并用作系统工作方式的文档。您可能要区分“当前”和“存档” TDD。\n然而，有两个方面需要注意：\n首先，随着系统的不断发展，TDD很快就会过时。使用已有两年历史的TDD作为参考的工程师可能会浪费大量时间来试图理解系统为何无法如所描述的那样工作。理想情况下，陈旧的TDD将被标记为已过时或已取代；实际上，这很少发生，因为团队倾向于专注于当前而不是过去的工作。 （使文档保持最新是每个工程团队都在努力的挑战。）\n其次，TDD可能不包括与系统接口所需的所有信息。 TDD可能仅涵盖对现有系统的一组更改，在这种情况下，您将需要查阅较早的文档（如果存在）以了解整体情况。 TDD主要关注实现细节，这与那些只想调用API的人无关。\n因此，不应将TDD视为实际用户或API参考文档的适当替代品。\n最后 # 网络上还有许多其他文章，解释了如何编写出色的设计文档。不要只读这篇文章！阅读其他内容，然后选择适合您的各种想法。\n","date":"1 September 2020","externalUrl":null,"permalink":"/posts/writing_technical_design_docs/","section":"Posts","summary":"","title":"(翻译)写技术设计文档","type":"posts"},{"content":"","date":"25 May 2020","externalUrl":null,"permalink":"/tags/java/","section":"Tags","summary":"","title":"Java","type":"tags"},{"content":"","date":"25 May 2020","externalUrl":null,"permalink":"/tags/sharding-proxy/","section":"Tags","summary":"","title":"Sharding-Proxy","type":"tags"},{"content":"sharding-proxy版本：3.1.0\n之前公司使用了 sharding-proxy 作为数据库分库分表代理。\n在运行一段时间后，出现大量连接超时，触发了线上报警。观察内存发现内存几乎占满了，频繁触发 fullGC。内存无法释放。\n一时无法确定原因，先将流量都切到一台备用 sharding-proxy，然后执行 jmap 导出内存到MAT里进行分析。\njmap -dump,format=b,file=proxy.hprof [pid] 可以看到1.8G的内存都被 Guava 的 cache 占用了。猜测是用了强引用，缓存没有释放。\n点击Details查看详细信息，可以看到是 ChannelRegistry 这个类里的缓存。\n接下来分析下代码，\npublic final class ChannelRegistry { private static final ChannelRegistry INSTANCE = new ChannelRegistry(); // TODO :wangkai do not use cache, should use map, and add unregister feature public final Cache\u0026lt;String, Integer\u0026gt; connectionIds = CacheBuilder.newBuilder().build(); /** * Get instance of channel registry. * * @return instance of channel registry */ public static ChannelRegistry getInstance() { return INSTANCE; } /** * Put connection id by channel ID. * * @param channelId netty channel ID * @param connectionId database connection ID */ public void putConnectionId(final String channelId, final int connectionId) { connectionIds.put(channelId, connectionId); // 自己加的日志 log.info(\u0026#34;put channel cache, key: {}, value: {}\u0026#34;, channelId, connectionId); } /** * Get connection id by channel ID. * * @param channelId netty channel ID * @return connectionId database connection ID */ public int getConnectionId(final String channelId) { Integer result = connectionIds.getIfPresent(channelId); Preconditions.checkNotNull(result, String.format(\u0026#34;Can not get connection id via channel id: %s\u0026#34;, channelId)); return result; } } 默认构造器创建了一个强引用缓存，里面缓存了netty连接和对应的数据库连接。只有 put 方法，没有释放方法。\n每次有新的netty连接，都会创建一个缓存元素，造成这个缓存越来越大，而且不会释放。\n为了确定是这个问题，我在 putConnectionId 方法里加了日志部署测试\n部署后，发现日志里频繁的 put cache，可以确定就是这个原因导致内存无法释放。连接创建这么频繁是因为运维在前面挂了一层 Aliyun SLB 负载，SLB 会频繁的创建探活连接。\n修复 # 修改也比较简单，sharding-proxy 分为 backend 和 frontend 两部分。backend 负责与 MySQL 示例交互，默认使用 JDBC 连接，不会使用这个缓存，不需要修改。要修改的只有 frontend 部分，这部分负责和连接 sharding-proxy 的客户端交互，使用的 Netty。\n首先修改 io.shardingsphere.shardingproxy.runtime.ChannelRegistry，添加一个释放缓存的方法。\npublic void invalidate(final String channelId) { connectionIds.invalidate(channelId); } 然后修改 io.shardingsphere.shardingproxy.frontend.common.FrontendHandler，这是连接处理的父类，继承的是Netty的ChannelInboundHandlerAdapter。我们修改 channelInactive 方法，添加一个剩余资源处理逻辑，并添加一个抽象方法供子类继承。\n@Override @SneakyThrows public final void channelInactive(final ChannelHandlerContext context) { context.fireChannelInactive(); backendConnection.close(true); ChannelThreadExecutorGroup.getInstance().unregister(context.channel().id()); // 添加的资源处理逻辑 inactive(context); } // 添加的抽象方法 protected abstract void inactive(final ChannelHandlerContext context); 最后，修改FrontendHandler的子类，继承刚刚的抽象方法。\n@Override protected void inactive(ChannelHandlerContext context) { ChannelRegistry.getInstance().invalidate(context.channel().id().asShortText()); } 这样，连接断开后就会自动释放缓存了。\n","date":"25 May 2020","externalUrl":null,"permalink":"/posts/sharding_proxy_cache/","section":"Posts","summary":"","title":"sharding-proxy 连接缓存导致 fullGC 内存不释放","type":"posts"},{"content":"MemoryPoolMXBean 是 Java 内存池的管理接口，如果要做内存监控，就会用到这个类。\n下面整理下各版本内存池的名字。\nCode Cache # JDK7, JDK8 # Code Cache JDK9 后 # CodeHeap \u0026#39;non-nmethods\u0026#39; CodeHeap \u0026#39;profiled nmethods\u0026#39; CodeHeap \u0026#39;non-profiled nmethods\u0026#39; Perm Gen # JDK7 # ParallelGC: PS Perm Gen\nConcMarkSweepGC: CMS Perm Gen\nSerialGC: Perm Gen\nMetaspace # JDK8 之后 # Metaspace Compressed Class Space SerialGC # Eden Space Survivor Space Tenured Gen ParallelGC + ParallelOldGC # PS Eden Space PS Survivor Space PS Old Gen ConcMarkSweepGC + ParNewGC # JDK9后 deprecated，JDK14后移除\nPar Eden Space Par Survivor Space CMS Old Gen G1GC # G1 Eden Space G1 Survivor Space G1 Old Gen ZGC # ZHeap ","date":"13 April 2020","externalUrl":null,"permalink":"/posts/java_memorypoolmxbean/","section":"Posts","summary":"","title":"MemoryPoolMXBean 各内存池名字","type":"posts"},{"content":"","date":"27 March 2020","externalUrl":null,"permalink":"/tags/go/","section":"Tags","summary":"","title":"Go","type":"tags"},{"content":"","date":"27 March 2020","externalUrl":null,"permalink":"/tags/gorm/","section":"Tags","summary":"","title":"Gorm","type":"tags"},{"content":" 软件版本 # sharding-proxy-3.1.0\nMySQL-5.7\n正文 # 最近公司某系统用 go 重构，ORM框架使用 gorm，用上了分库分表，使用 sharding-proxy 作代理。\n但是上了 sharding-proxy 之后之前参数中带 ' 的 sql 全都报错了。\n// 报错信息 Error 3054: Unknown exception: Illegal input, unterminated \u0026#39;\u0026#39;\u0026#39;. // 执行 sql 的代码 // 这里参数 dd\u0026#39;dd 中有个 \u0026#39; db.Exec(\u0026#34;update md_user_info set nickname = ? where user_id = ?\u0026#34;, \u0026#34;dd\u0026#39;dd\u0026#34;, 1000002082) 怀疑可能是没用占位符，直接拼接sql引发的问题。但是看代码又没发现啥问题，只能 debug 看下。\n中间都没发现问题，直接一路 debug 到最底层 sql 库。\n// go/src/database/sql/sql.go func (db *DB) execDC(ctx context.Context, dc *driverConn, release func(error), query string, args []interface{}) (res Result, err error) { /* ... */ if ok { var nvdargs []driver.NamedValue var resi driver.Result withLock(dc, func() { /* ... */ // 这里执行 statmente resi, err = ctxDriverExec(ctx, execerCtx, execer, query, nvdargs) }) /* ... */ } var si driver.Stmt withLock(dc, func() { // 这里执行 preparedStatmente si, err = ctxDriverPrepare(ctx, dc.ci, query) }) /* ... */ } debug 进 ctxDriverExec 方法，走到最后的 mysql 驱动。\n// github.com/go-sql-driver/mysql/connection.go func (mc *mysqlConn) Exec(query string, args []driver.Value) (driver.Result, error) { /* ... */ // 判断 InterpolateParams 参数，如果为 true，就注入参数执行 statmente // 如果为 false，就跳出回 sql.go 执行 preparedStatmente if !mc.cfg.InterpolateParams { return nil, driver.ErrSkip } // 注入参数 // try to interpolate the parameters to save extra roundtrips for preparing and closing a statement prepared, err := mc.interpolateParams(query, args) /* ... */ // 执行 statmente err := mc.exec(query) /* ... */ } // 将参数注入 sql func (mc *mysqlConn) interpolateParams(query string, args []driver.Value) (string, error) { /* ... */ case string: buf = append(buf, \u0026#39;\\\u0026#39;\u0026#39;) // 这里用位与和位左移状态位来判断状态 if mc.status\u0026amp;statusNoBackslashEscapes == 0 { buf = escapeStringBackslash(buf, v) } else { buf = escapeStringQuotes(buf, v) } buf = append(buf, \u0026#39;\\\u0026#39;\u0026#39;) /* ... */ } 这里有两个相关参数：\n一个是 statusNoBackslashEscapes，对应 sql_mode NO_BACKSLASH_ESCAPES，表示将反斜杠当作普通字符，而不是转义字符。开启后生成的sql为update md_user_info set nickname = 'dd''dd' where user_id = 1000002082，会把单引号替换成两个单引号(这样单引号会成对，不破坏结构)。\n一个是 InterpolateParams，对应连接参数interpolateParams，判断是否开启客户端 prepare，进行参数转义拼接，开启可以省掉一次和服务端的prepare交互。\ndebug 往下走发现参数InterpolateParams是true，发出的sql是update md_user_info set nickname = 'dd\\'dd' where user_id = 1000002082，没有问题，然后执行 statement 报错了。\n先返回程序创建数据库连接的地方。\nconnString = fmt.Sprintf(\u0026#34;%s:%s@tcp(%s:%s)/%s?charset=utf8mb4\u0026amp;parseTime=True\u0026amp;loc=Local\u0026amp;interpolateParams=true\u0026#34;, conf.User, conf.Password, conf.Host, conf.Port, conf.Database) 果然，参数里拼上了 \u0026amp;interpolateParams=true，去掉这个参数试下，发出的 prepare sql 是update md_user_info set nickname = ? where user_id = ?，执行正常。看来是 sharding-proxy 不支持客户端预编译，接收 sql 时把反斜杠丢掉了，或者没有处理转义符。\nsql 解析 # 先简单介绍下 sql 解析的处理过程。sql 解析分为，词法分析、语法和语义分析、优化、执行代码生成。词法分析主要是把输入转化成一个个 token，语法分析是生成语法树的过程。\n前端 中部 后端 / \\ | | 词法分析 语法分析 优化 执行代码生成 以 update table set name = a 为例。\n词法分析生成6个 token：\n语法分析语法树：\nsql 解析器分析 # 同样的 sql update md_user_info set nickname = 'dd'dd' where user_id = 1000002082，MySQL 正常执行，sharding-proxy 却报错，接下来分析下两者的 sql 解析器都是怎么处理这条 sql 的。\n这里主要分析下词法分析转化成 token 阶段，sharding-proxy 也是在这个阶段报错的。\n正常解析这条 sql 应该生成10个 token：\nupdate md_user_info set nickname = dd\u0026#39;dd where user_id = 1000002082 sharding-proxy # 首先看下接收到的 sql 是什么，debug 进 SQLParsingEngine\n可以看到接收的 sql 没有问题，接下来看 sql 解析。\nupdate 语句解析在 AbstractUpdateParser.parse() 处，经过 LexerEngine，最后用 Tokenizer 生成 token。\n// src/main/java/io/shardingsphere/core/parsing/lexer/analyzer/Tokenizer.java private Token scanChars(final char terminatedChar) { // 获取 token 的字符串长度 int length = getLengthUntilTerminatedChar(terminatedChar); // 截取字符串，返回新 token return new Token(Literals.CHARS, input.substring(offset + 1, offset + length - 1), offset + length); } // src/main/java/io/shardingsphere/core/parsing/lexer/analyzer/Tokenizer.java // 获取 token 的字符串长度 private int getLengthUntilTerminatedChar(final char terminatedChar) { int length = 1; // 当前字符是否是检测字符 while (terminatedChar != charAt(offset + length) // 判断是否是两个相同字符 || hasEscapeChar(terminatedChar, offset + length)) { if (offset + length \u0026gt;= input.length()) { throw new UnterminatedCharException(terminatedChar); } // 处理是否是两个相同字符 if (hasEscapeChar(terminatedChar, offset + length)) { length++; } length++; } return length + 1; } 看下 dd'dd 的处理过程，这时 terminatedChar 的值是 ' ，剩余要处理的字符串是 'dd\\'dd' where user_id = 1000002082 ，逐个字符向后检测。\n这里' 会成对解析，如果碰上' 没有成对的情况，就会报错。不处理转义字符。\nMySQL # 再看下 MySQL 的解析器。\n看到接收的 sql 和 sharding-proxy 是一样的(多一个反斜杠是因为这里按字符串显示，多一个转义符)\n转化 token 的代码在 sql_lex.cc 的 MYSQLlex → lex_one_token 方法中。\n// sql/sql_lex.cc int MYSQLlex(YYSTYPE *yylval, YYLTYPE *yylloc, THD *thd) { /* ... */ token= lex_one_token(yylval, thd); /* ... */ } 普通字符的解析最后会走进 get_text 方法进行截取。\n// sql/sql_lex.cc static char *get_text(Lex_input_stream *lip, int pre_skip, int post_skip) { /* ... */ // 处理转义字符 if (c == \u0026#39;\\\\\u0026#39; \u0026amp;\u0026amp; !(lip-\u0026gt;m_thd-\u0026gt;variables.sql_mode \u0026amp; MODE_NO_BACKSLASH_ESCAPES)) { // Escaped character found_escape=1; if (lip-\u0026gt;eof()) return 0; lip-\u0026gt;yySkip(); } /* ... */ } 这里有个处理转义字符的逻辑，如果当前字符是反斜杠，就向后跳两个字符，跳过转义字符。\n最终的 token 列表。(token 是个 int，对应关系在 sql_yacc.h 中)\n865 UPDATE_SYM update 484 IDENT_QUOTED md_user_info 757 SET set 484 IDENT_QUOTED nickname 415 EQ = 828 TEXT_STRING dd\u0026#39;dd 891 WHERE where 484 IDENT_QUOTED user_id 415 EQ = 629 NUM 1000002082 411 END_OF_INPUT 可以看到 sharding-proxy 因为没有处理开启客户端预编译的情况，解析报错了。\nsharding-proxy 修改 # 修改也很简单，加上处理转义字符的逻辑就可以了，修改 Tokenizer 的 getLengthUntilTerminatedChar 方法，增加处理转义字符的逻辑。\n// src/main/java/io/shardingsphere/core/parsing/lexer/analyzer/Tokenizer.java private int getLengthUntilTerminatedChar(final char terminatedChar) { int length = 1; while (terminatedChar != charAt(offset + length) || hasEscapeChar(terminatedChar, offset + length) ) { if (offset + length \u0026gt;= input.length()) { throw new UnterminatedCharException(terminatedChar); } // 增加处理转义字符 if (\u0026#39;\\\\\u0026#39; == charAt(offset + length)) { length++; } else if (hasEscapeChar(terminatedChar, offset + length)) { length++; } length++; } return length + 1; } 这里忽略了sql_mode=NO_BACKSLASH_ESCAPES 的情况，因为 sharding-jdbc 里没有传递相关参数，包括客户端预编译的相关参数也没传递。\n其他 # 在没修改的情况下顺手用 Java 也测试下 sharding-jdbc 是否支持客户端预编译。\nClass.forName(\u0026#34;com.mysql.jdbc.Driver\u0026#34;); String url = \u0026#34;jdbc:mysql://localhost:3307/xxx?useServerPrepStmts=false\u0026#34;; String username = \u0026#34;xxx\u0026#34;; String password = \u0026#34;xxx\u0026#34;; Connection conn = DriverManager.getConnection(url, username, password); PreparedStatement st = null; String sql = \u0026#34;update md_user_info set nickname = ? where user_id = ?\u0026#34;; st = conn.prepareStatement(sql); st.setString(1, \u0026#34;dd\u0026#39;dd\u0026#34;); st.setLong(2, 1000002082); System.out.println(st.execute()); Java 库的客户端预编译参数是useServerPrepStmts，默认值就是 false，表示关闭服务端预编译，开启客户端预编译，为了方便看我就写上了。执行一下，果然报错了。\nException in thread \u0026#34;main\u0026#34; java.sql.SQLException: Unknown exception: Illegal input, unterminated \u0026#39;\u0026#39;\u0026#39;. ","date":"27 March 2020","externalUrl":null,"permalink":"/posts/gorm_sharding_proxy_sql_error/","section":"Posts","summary":"","title":"gorm调用sharding-proxy, 参数带单引号sql报错","type":"posts"},{"content":"","date":"27 March 2020","externalUrl":null,"permalink":"/tags/mysql/","section":"Tags","summary":"","title":"MySQL","type":"tags"},{"content":"","date":"26 March 2020","externalUrl":null,"permalink":"/tags/clion/","section":"Tags","summary":"","title":"CLion","type":"tags"},{"content":" 编译安装 MySQL # 首先从 github 下载 mysql 源码\nhttps://github.com/mysql/mysql-server 接下来编译安装\n// 进到源码目录 cd /Users/d/c/mysql-server-5.7 // cmake cmake \\ -DCMAKE_INSTALL_PREFIX=/Users/d/c/mysql_build \\ -DMYSQL_DATADIR=/Users/d/c/mysql_build/data \\ -DSYSCONFDIR=/Users/d/c/mysql_build \\ -DMYSQL_UNIX_ADDR=/Users/d/c/mysql_build/data/mysql.sock \\ -DWITH_DEBUG=1 \\ -DDOWNLOAD_BOOST=1 \\ -DWITH_BOOST=/Users/d/c/boost \\ -DWITH_SSL=/usr/local/opt/openssl@1.1 // install make \u0026amp;\u0026amp; make install // 初始化数据库 cd /Users/d/c/mysql_build bin/mysqld --basedir=/Users/d/c/mysql_build --datadir=/Users/d/c/mysql_build/data --initialize-insecure -DDOWNLOAD_BOOST=1 -DWITH_BOOST=/Users/d/c/boost 这两个参数指定 boost 路径，之后会自动下载 boost。\n-DWITH_SSL=/usr/local/opt/openssl@1.1 指定 openssl 路径，不指定有可能会报以下错误\nCannot find appropriate system libraries for WITH_SSL=system. Make sure you have specified a supported SSL version. Valid options are : system (use the OS openssl library), yes (synonym for system), \u0026lt;/path/to/custom/openssl/installation\u0026gt; CMake Error at cmake/ssl.cmake:63 (MESSAGE): Please install the appropriate openssl developer package. 配置CLion # 打开 mysql 源码目录，修改 cmake 配置\n-DCMAKE_INSTALL_PREFIX=/Users/d/c/mysql_build -DMYSQL_DATADIR=/Users/d/c/mysql_build/data -DSYSCONFDIR=/Users/d/c/mysql_build -DMYSQL_UNIX_ADDR=/Users/d/c/mysql_build/data/mysql.sock -DWITH_DEBUG=1 -DDOWNLOAD_BOOST=1 -DWITH_BOOST=/Users/d/c/boost -DWITH_SSL=/usr/local/opt/openssl@1.1 之后选择 mysqld，然后点击 debug 按钮就可以开始调试了。\n","date":"26 March 2020","externalUrl":null,"permalink":"/posts/mac_clion_debug_mysql_source/","section":"Posts","summary":"","title":"Mac下用CLion debug MySQL5.7源码","type":"posts"},{"content":"最近生产环境的余额系统在扣减余额时经常出现余额够但是提示余额不足无法扣减的情况。查看代码逻辑，发现会先查询一次判断余额是否够，再实际扣减，之后查看日志发现并没有执行查询语句就返回错误了，推测可能是 MyBatis 一级缓存没有关闭引起的脏数据问题。关闭一级缓存后果然恢复正常了。\n所以有了这篇文章，验证下 MyBatis 一级缓存的生效条件。\n一级缓存 # MyBatis 默认会开启一级缓存，在同一次会话中，如果执行多次查询条件相同的 SQL，会进行优化，优先命中一级缓存，避免多次查询数据库。\n这样在单机环境下是没有问题的，可以减少于数据库的交互。但是生成环境一般都是多机部署，这样一级缓存开启的情况下就容易出现脏数据。\n接下来做两个实验验证下。\n测试1 # public void test1() { SqlSession sqlSession = sqlSessionFactory.openSession(true); UserWalletMapperExt mapper = sqlSession.getMapper(UserWalletMapperExt.class); System.out.println(mapper.queryUserBalance(10L, 1)); System.out.println(mapper.queryUserBalance(10L, 1)); System.out.println(mapper.queryUserBalance(10L, 1)); mapper.addBalanceByType(10L, 1, 100L); System.out.println(mapper.queryUserBalance(10L, 1)); System.out.println(mapper.queryUserBalance(10L, 1)); } ...queryUserBalance:debug:181 ==\u0026gt; Preparing: select balance from user_wallet WHERE user_id = ? and type = ? ...queryUserBalance:debug:181 ==\u0026gt; Parameters: 10(Long), 1(Integer) ...queryUserBalance:debug:181 \u0026lt;== Total: 1 200 200 200 ...addBalanceByType:debug:181 ==\u0026gt; Preparing: update user_wallet set balance = balance + ? where user_id = ? and type = ? ...addBalanceByType:debug:181 ==\u0026gt; Parameters: 100(Long), 10(Long), 1(Integer) ...addBalanceByType:debug:181 \u0026lt;== Updates: 1 ...queryUserBalance:debug:181 ==\u0026gt; Preparing: select balance from user_wallet WHERE user_id = ? and type = ? ...queryUserBalance:debug:181 ==\u0026gt; Parameters: 10(Long), 1(Integer) ...queryUserBalance:debug:181 \u0026lt;== Total: 1 300 300 可以看到在同一个session中，相同的查询在调用更新前只会执行一次。\n测试2 # public void test2() { SqlSession sqlSession1 = sqlSessionFactory.openSession(true); UserWalletMapperExt mapper1 = sqlSession1.getMapper(UserWalletMapperExt.class); System.out.println(\u0026#34;session1: \u0026#34; + mapper1.queryUserBalance(10L, 1)); SqlSession sqlSession2 = sqlSessionFactory.openSession(true); UserWalletMapperExt mapper2 = sqlSession2.getMapper(UserWalletMapperExt.class); System.out.println(\u0026#34;session2: \u0026#34; + mapper2.queryUserBalance(10L, 1)); mapper2.addBalanceByType(10L, 1, 100L); System.out.println(\u0026#34;session2: \u0026#34; + mapper2.queryUserBalance(10L, 1)); System.out.println(\u0026#34;session1: \u0026#34; + mapper1.queryUserBalance(10L, 1)); } ...queryUserBalance:debug:181 ==\u0026gt; Preparing: select balance from user_wallet WHERE user_id = ? and type = ? ...queryUserBalance:debug:181 ==\u0026gt; Parameters: 10(Long), 1(Integer) ...queryUserBalance:debug:181 \u0026lt;== Total: 1 session1: 500 ...queryUserBalance:debug:181 ==\u0026gt; Preparing: select balance from user_wallet WHERE user_id = ? and type = ? ...queryUserBalance:debug:181 ==\u0026gt; Parameters: 10(Long), 1(Integer) ...queryUserBalance:debug:181 \u0026lt;== Total: 1 session2: 500 ...addBalanceByType:debug:181 ==\u0026gt; Preparing: update user_wallet set balance = balance + ? where user_id = ? and type = ? ...addBalanceByType:debug:181 ==\u0026gt; Parameters: 100(Long), 10(Long), 1(Integer) ...addBalanceByType:debug:181 \u0026lt;== Updates: 1 ...queryUserBalance:debug:181 ==\u0026gt; Preparing: select balance from user_wallet WHERE user_id = ? and type = ? ...queryUserBalance:debug:181 ==\u0026gt; Parameters: 10(Long), 1(Integer) ...queryUserBalance:debug:181 \u0026lt;== Total: 1 session2: 600 session1: 500 可以看到在 session2 更新了值得情况下，session1 依旧使用了一级缓存查询出旧值。\n关闭一级缓存 # 接下来关闭一级缓存试一下，设置 LocalCache 级别为 statement 或者在语句上设置flushCache=\u0026ldquo;true\u0026rdquo; 都可以。\n// 设置全局一级缓存级别为 STATEMENT org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration(); configuration.setLocalCacheScope(LocalCacheScope.STATEMENT); sqlSessionFactoryBean.setConfiguration(configuration); \u0026lt;!-- 设置单个语句不使用一级缓存 --\u0026gt; \u0026lt;select id=\u0026#34;queryUserBalance\u0026#34; resultType=\u0026#34;java.lang.Long\u0026#34; flushCache=\u0026#34;true\u0026#34;\u0026gt; 测试\npublic void test3() { SqlSession sqlSession = sqlSessionFactory.openSession(true); UserWalletMapperExt mapper = sqlSession.getMapper(UserWalletMapperExt.class); System.out.println(mapper.queryUserBalance(10L, 1)); System.out.println(mapper.queryUserBalance(10L, 1)); System.out.println(mapper.queryUserBalance(10L, 1)); } ...queryUserBalance:debug:181 ==\u0026gt; Preparing: select balance from user_wallet WHERE user_id = ? and type = ? ...queryUserBalance:debug:181 ==\u0026gt; Parameters: 10(Long), 1(Integer) ...queryUserBalance:debug:181 \u0026lt;== Total: 1 600 ...queryUserBalance:debug:181 ==\u0026gt; Preparing: select balance from user_wallet WHERE user_id = ? and type = ? ...queryUserBalance:debug:181 ==\u0026gt; Parameters: 10(Long), 1(Integer) ...queryUserBalance:debug:181 \u0026lt;== Total: 1 600 ...queryUserBalance:debug:181 ==\u0026gt; Preparing: select balance from user_wallet WHERE user_id = ? and type = ? ...queryUserBalance:debug:181 ==\u0026gt; Parameters: 10(Long), 1(Integer) ...queryUserBalance:debug:181 \u0026lt;== Total: 1 600 可以看到关闭一级缓存后所有语句都会实际执行。\n总结 # MyBatis 的一级缓存在单机环境下可以减少与 MySql 的交互，提高性能，但是在分布式环境下容易产生脏数据。建议在生产环境下关闭，使用 Redis，Memcache 等代替。\n","date":"24 November 2019","externalUrl":null,"permalink":"/posts/mybatis_scale_localcache/","section":"Posts","summary":"","title":"MyBatis 一级缓存在分布式下的坑","type":"posts"},{"content":"现在越来越多的人使用 Spring Boot 来开发自己的应用了，Spring Boot 用起来是各种方便，还内置 web 容器，只需要java -jar就能启动应用。但有时候有些公司的 Tomcat 是单独部署，有专人维护的，这时我们就需要将 Spring Boot 应用打包成 war 部署。\n打包方式设置为 war # 修改 pom.xml 文件，将打包方式设置为 war\n\u0026lt;packaging\u0026gt;war\u0026lt;/packaging\u0026gt; 添加 war 插件\n\u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-war-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.0.0\u0026lt;/version\u0026gt; \u0026lt;/plugin\u0026gt; 将 spring-boot-starter-web 的 Tomcat 依赖设为 provided，这样还能继续在 IDE 里调试，而且打包时排除了 tomcat 依赖。\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; \u0026lt;exclusions\u0026gt; \u0026lt;exclusion\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-tomcat\u0026lt;/artifactId\u0026gt; \u0026lt;/exclusion\u0026gt; \u0026lt;/exclusions\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-tomcat\u0026lt;/artifactId\u0026gt; \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; 这样 pom 文件就修改好了。\n修改入口方法 # 继承SpringBootServletInitializer类，并且重写configure方法\n@SpringBootApplication public class Application extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application.sources(Application.class); } public static void main(String[] args) { SpringApplication.run(Application.class, args); } } 这样就修改好了\n测试 # mvn clean package 打包后放入 tomcat webapps 文件夹里测试下\n顺利启动\n启动后运行 # 直接用 Spring Boot 内置容器启动应用时，一些启动后运行的代码可以直接写在 main 方法里\n@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); // 启动后运行的代码 ..... } } 换成 war 包运行后这样写就无法运行了，因为启动不是走得 main 方法。这时可以实现 CommandLineRunner 接口来实现。\n/** * Interface used to indicate that a bean should \u0026lt;em\u0026gt;run\u0026lt;/em\u0026gt; when it is contained within * a {@link SpringApplication}. Multiple {@link CommandLineRunner} beans can be defined * within the same application context and can be ordered using the {@link Ordered} * interface or {@link Order @Order} annotation. * \u0026lt;p\u0026gt; * If you need access to {@link ApplicationArguments} instead of the raw String array * consider using {@link ApplicationRunner}. * * 用来指定 {@link SpringApplication} 中需要运行的 bean。可以包含多个 {@link CommandLineRunner} * 顺序用 {@link Ordered} 接口或 {@link Order @Order} 注解指定 * * @author Dave Syer * @see ApplicationRunner */ @FunctionalInterface public interface CommandLineRunner { /** * Callback used to run the bean. * @param args incoming main method arguments * @throws Exception on error */ void run(String... args) throws Exception; } 实现这个接口后把要运行的代码写在 run 方法中就可以了\n@Component @Order(value = 1) public class MotanSwitcherRunner implements CommandLineRunner { @Override public void run(String... args) { // 启动后运行的代码 ..... } } ","date":"25 October 2019","externalUrl":null,"permalink":"/posts/spring_boot_tomcat/","section":"Posts","summary":"","title":"用外置 Tomcat 部署 Spring Boot 应用","type":"posts"},{"content":"一些需求中经常要我们实现一个排行榜，数据量少的话可以使用 RDB 数据库排序，数据量大可以自己实现算法或者使用 NoSQL 数据库排序，NoSQL 数据库中最方便的可能就是利用 Redis 的 zset 来实现了。 例如要实现一个玩家成就点数的排行榜：\n\u0026gt;zadd r 100 A \u0026#34;1\u0026#34; \u0026gt;zadd r 200 B \u0026#34;1\u0026#34; \u0026gt;zadd r 200 C \u0026#34;1\u0026#34; \u0026gt;zadd r 300 D \u0026#34;1\u0026#34; \u0026gt;zrange r 0 -1 withscores 1) \u0026#34;A\u0026#34; 2) \u0026#34;100\u0026#34; 3) \u0026#34;B\u0026#34; 4) \u0026#34;200\u0026#34; 5) \u0026#34;C\u0026#34; 6) \u0026#34;200\u0026#34; 7) \u0026#34;D\u0026#34; 8) \u0026#34;300\u0026#34; 其中 B 和 C 的分数是一样的，这时我们可能想让先达到对应分数的人排在前面，相当于增加了一个排序维度。这时直接用 score 就不能实现了，需要做一些转换，这里分享几个我会用到的方法。\n实现方法 # 假设我们需要一个三个维度的排序，第一维度是具体的数值，第二个维度是一个是否标志位，第三个维度是时间戳。其中氪金的（0否1是）和时间早的排序靠前。\n以下是原始数据：\n玩家 成就 是否氪金 时间戳 A 100 1 1571819021259 B 200 0 1571819021259 C 200 1 1571819021259 D 400 0 1571819021259 E 200 1 1571810001259 1. 利用 key 排序 # 如果后两个维度初始化后就不再变化的话，可以利用 Redis 的排序特性，将不变的维度写到 key 里，这样 score 相同时会用 key 来进行排序。\n# 这里反转时间戳(9999999999999 - 1571810001259)，让时间早的排在前面 \u0026gt;zadd r 100 A-1-8428180978740 \u0026#34;1\u0026#34; \u0026gt;zadd r 200 B-0-8428180978740 \u0026#34;1\u0026#34; \u0026gt;zadd r 200 C-1-8428180978740 \u0026#34;1\u0026#34; \u0026gt;zadd r 400 D-0-8428180978740 \u0026#34;1\u0026#34; \u0026gt;zadd r 200 E-1-8428189998740 \u0026#34;1\u0026#34; \u0026gt;zrange r 0 -1 withscores 1) \u0026#34;A-1-8428180978740\u0026#34; 2) \u0026#34;100\u0026#34; 3) \u0026#34;B-0-8428180978740\u0026#34; 4) \u0026#34;200\u0026#34; 5) \u0026#34;C-1-8428180978740\u0026#34; 6) \u0026#34;200\u0026#34; 7) \u0026#34;E-1-8428189998740\u0026#34; 8) \u0026#34;200\u0026#34; 9) \u0026#34;D-0-8428180978740\u0026#34; 10) \u0026#34;400\u0026#34; 以上就是第一种方法了，实现起来非常简单。\nscore 存储格式 # 在介绍下两种方法前先来看看 zset 的 score 是怎么存储的，能保留多少精度，直接看跳表 node 的结构。\n/* * 跳跃表节点 */ typedef struct zskiplistNode { // 成员对象 robj *obj; // 分值 double score; // 后退指针 struct zskiplistNode *backward; // 层 struct zskiplistLevel { // 前进指针 struct zskiplistNode *forward; // 跨度 unsigned int span; } level[]; } zskiplistNode; 我们可以看到 score 是用 double 存储的，它是一个 IEEE 754 标准的 double 浮点数。（IEEE 754 标准的详细存储规则可以搜索其他文章，这里重点分析保留的精度）\n一个 64 位浮点数存储时分为3段\nS:符号位，第一段，占1位，0表示正数，1表示负数。 Exp:指数字，第二段，占11位，移码方式表示正负数，排除全0和全1表示零和无穷大的特殊情况，指数部分是−1022～+1023加上1023，指数值的大小从1～2046 Fraction:有效数字，占52位，定点数，小数点放在尾数最前面 实际数字 = (-1)^S * Fraction * 2^Exp (二进制计算) double 存储的有效数据是第三段的52位，超出会损失精度，由于标准规定小数点是在有效数字最前面，所以实际可以存储 53 位数字。比如有个数字是 111___(53个1)，有效数字位存储的是 .11___(52个1)，计算时会在个位补1，得到1.11___(53个1)，最大值为 2^53 = 9007199254740991，大概 log2^53 = 15.95 位。\n可以看到只要保证有效数字小于等于9007199254740991就不会损失精度。\n好了，现在有了理论基础，接下来看看我们可以怎么实现。\n2. 二进制分段 # 第二种方法，可以利用二进制 64 位 long 分段存储各个维度。\n首先时间戳如果全存储就太长了，可以通过一些计算缩小一些，先忽略毫秒，然后和一个大数计算差值。\nint MAX_SECOND = 1861891200; // 2029.1.1 0:0:0 int sts = (MAX_SECOND - (int)(ts / 1000)) 这样时间就缩小到了300000000以内。\n接下来进行划分，首位标志位不用，剩下63位，然后我划分高33位存分数，1位存标志位，最后29位存时间戳，存储结构是这样的：\n如果要不损失精度，第一段可存储位数 = 53 - 1 - 29 = 23。\n第一段最大值 = 2^33 = 8589934591，不损失精度 = 2^23 = 8388607。\n第三段最大值 = 2^29 = 536870911。\n使用时可以适当缩短第三段时间戳的长度，或者不追求时间戳一定精确的话，第一段分数可以超出不损失精度的长度，也只会损失一点时间戳的精度而已。 然后就可以用下面的方法计算 score 了，因为是二进制，可以全用位运算。\nint size1 = 33; int size2 = 1; int size3 = 29; long d1Max = maxBySize(size1); long d2Max = maxBySize(size2); long d3Max = maxBySize(size3); // 计算 score public long genScore(long d1, long d2, long d3) { return ((d1 \u0026amp; d1Max) \u0026lt;\u0026lt; (size2 + size3)) | ((d2 \u0026amp; d2Max) \u0026lt;\u0026lt; size3) | (d3 \u0026amp; d3Max); } // 计算增加 value 值 public void incValue(long d1) { return ((d1 \u0026amp; d1Max) \u0026lt;\u0026lt; (size2 + size3)) | ((0 \u0026amp; d2Max) \u0026lt;\u0026lt; size3) | (0 \u0026amp; d3Max); } // 根据二进制长度计算最大值 private long maxBySize(int len) { long r = 0; while (len-- \u0026gt; 0) { r = ((r \u0026lt;\u0026lt; 1) | 1); } return r; } 增加高位分数的话直接把低维度设为0，计算出分数后调用 ZINCRBY 就行了。\n改变低位维度值的话就不能调用 ZINCRBY 了，需要调用ZADD，我们用自旋加 CAS 更新，防止覆盖掉新更新的值。\n/** * 设置维度值 * * @param key zset key * @param value zset value * @param d2 第二位 * @param d3 第三位 * @return */ public boolean setD(String key, String value, Long d2, Long d3) { long start = System.currentTimeMillis(); Long oldScore; long score; byte[] originBytes; do { if ((System.currentTimeMillis() - start) \u0026gt; 2000) { return false; } long d1 = 0; originBytes = zscoreBytes(key, value); oldScore = convertZscore(originBytes); if (originBytes != null) { // 根据 score 获取原始值 d1 = getD1ByScore(oldScore); } // 如果不设置新值就获取原始值 if (d2 == null) { // 根据 score 获取原始值 d2 = getD2ByScore(oldScore); } // 如果不设置新值就获取原始值 if (d3 == null) { // 根据 score 获取原始值 d3 = getD3ByScore(oldScore); } // 生成新值 score = genScore(d1, d2, d3); } while (!compareAndSetDimension(key, value, originBytes, score)); return true; } Long SUCCESS = 1L; String script = \u0026#34;if (((not (redis.call(\u0026#39;zscore\u0026#39;, KEYS[1], ARGV[1]))) and (ARGV[2] == \u0026#39;\u0026#39;)) \u0026#34; + \u0026#34;or redis.call(\u0026#39;zscore\u0026#39;, KEYS[1], ARGV[1]) == ARGV[2]) \u0026#34; + \u0026#34; then redis.call(\u0026#39;zadd\u0026#39;,KEYS[1],ARGV[3],ARGV[1]) return 1 else return 0 end\u0026#34;; // CAS 设置值 private boolean compareAndSetDimension(String setKey, String key, byte[] oldScore, long newScore) { Long result = redisTemplate.execute((RedisCallback\u0026lt;Long\u0026gt;) connection -\u0026gt; { try { return connection.eval(script.getBytes(), ReturnType.fromJavaType(Long.class), 1, setKey.getBytes(), key.getBytes(), oldScore, om.writeValueAsBytes(newScore)); } catch (JsonProcessingException e) { throw new RuntimeException(e); } }); return SUCCESS.equals(result); } // 获取原始值 public byte[] zscoreBytes(String key, String value) { String script = \u0026#34;return redis.call(\u0026#39;zscore\u0026#39;, KEYS[1], ARGV[1])\u0026#34;; return redisTemplate.execute((RedisCallback\u0026lt;byte[]\u0026gt;) connection -\u0026gt; connection.eval(script.getBytes(), ReturnType.fromJavaType(String.class), 1, key.getBytes(), value.getBytes())); } 这里初始 score 值获取使用的是 zscoreBytes() 而不是redisTemplate.opsForZSet().score()是为了防止损失精度，这个方法返回的 score 值其实是可能损失精度的，我们看源码分析下：\n直接进到最底层的执行方法 zscore\n// class RedisCommandBuilder Command\u0026lt;K, V, Double\u0026gt; zscore(K key, V member) { notNullKey(key); return createCommand(ZSCORE, new DoubleOutput\u0026lt;\u0026gt;(codec), key, member); } DoubleOutput 将 Redis 的返回值转成 Double\n// 将 Redis 返回值转成 Double public class DoubleOutput\u0026lt;K, V\u0026gt; extends CommandOutput\u0026lt;K, V, Double\u0026gt; { public DoubleOutput(RedisCodec\u0026lt;K, V\u0026gt; codec) { super(codec, null); } // 方法参数 bytes 就是 Redis 返回的原始值了，是个字符串 @Override public void set(ByteBuffer bytes) { output = (bytes == null) ? null : parseDouble(decodeAscii(bytes)); } } Redis 原始的返回值其实是个类似 1.3094266712875436e+18 的字符串，DoubleOutput 在转换成 Double 的时候有可能会损失精度，用 BigDecimal 转的话是不会丢精度的。\nString a = \u0026#34;1.3094266712875436e+18\u0026#34;; long value = new BigDecimal(a).longValue(); 所以要不损失精度的话，可以修改下原始方法重新打个包或者用 lua 取出来原始的字符串再转成自己需要的值。\n最后利用 genScore 方法就可以生成score值并排序了\n# genScore(100, 1, 290072179) \u0026gt;zadd r 108201125491 A \u0026#34;1\u0026#34; # genScore(200, 0, 290072179) \u0026gt;zadd r 215038436979 B \u0026#34;1\u0026#34; # genScore(200, 1, 290072179) \u0026gt;zadd r 215575307891 C \u0026#34;1\u0026#34; # genScore(400, 0, 290072179) \u0026gt;zadd r 429786801779 D \u0026#34;1\u0026#34; # genScore(200, 1, 290081199) \u0026gt;zadd r 215575316911 E \u0026#34;1\u0026#34; \u0026gt;zrange r 0 -1 withscores 1) \u0026#34;A\u0026#34; 2) \u0026#34;108201125491\u0026#34; 3) \u0026#34;B\u0026#34; 4) \u0026#34;215038436979\u0026#34; 5) \u0026#34;C\u0026#34; 6) \u0026#34;215575307891\u0026#34; 7) \u0026#34;E\u0026#34; 8) \u0026#34;215575316911\u0026#34; 9) \u0026#34;D\u0026#34; 10) \u0026#34;429786801779\u0026#34; 3. 直接拆分 double # 可以用二进制拆分当然也可以直接拆分十进制数，为了方便，还可以用小数划分维度，比如将分数放在整数位，标志位和时间戳放在小数位。\nA 100.1290072179 B 200.0290072179 C 200.1290072179 D 400.0290072179 E 200.1290081199 不过这样不丢精度存储的分数比二进制拆分小。\n原理和二进制拆分一样，就不赘述了。\n总结 # 数据量不大的情况下直接使用 RDB 就可以了，比较方便。用 Redis 的时候，一级以外的维度不变的情况下可以直接用 key 排序，比较简单，如果维度会更新，可以使用拆分二进制或十进制的方法存储，二进制的优点是存储的数比较大，而且可以用位运算，十进制的优点是计算简单，可读性比较好。各个维度的长度还可以做成配置项，这样就可以满足不同的业务需求了。\n","date":"23 October 2019","externalUrl":null,"permalink":"/posts/make_redis_zset_support_multiple_sort/","section":"Posts","summary":"","title":"让 Redis zset 支持多条件排序","type":"posts"},{"content":"","date":"15 September 2019","externalUrl":null,"permalink":"/tags/automater/","section":"Tags","summary":"","title":"Automater","type":"tags"},{"content":"不知道有没有人经历过重装系统后需要把所有软件都重装一遍的痛苦，而且很有可能落下几个软件没装，等到用的时候才发现，重新下载再重新配置，时间不知不觉就没了。\n所以要是能把安装过的软件列表都备份下来就好了。mac 上正好就有这样一个软件 brew。\n准备 # brew 是 Mac 下的一个包管理工具，类似 Ubuntu 的 apt，可以方便的安装各种软件。\nhttps://brew.sh/\n靠一行命令就可以安装：\n/usr/bin/ruby -e \u0026#34;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)\u0026#34; 安装好后就可以靠命令来快速安装软件了\n# 安装不带界面的命令行下的工具或三方库 brew install wget # 安装带界面的应用程序 brew cask install firefox 还可以安装 mas来管理 App Store 下载的软件\nhttps://github.com/mas-cli/mas\nbrew install mas 之后我们就可以执行命令将安装过的软件列表导出了\nbrew bundle dump --force --describe 执行上面命令后会导出个 Brewfile 文件，里面大概长这样\ntap \u0026#34;homebrew/bundle\u0026#34; tap \u0026#34;homebrew/cask\u0026#34; tap \u0026#34;homebrew/core\u0026#34; brew \u0026#34;bazaar\u0026#34; cask \u0026#34;docker\u0026#34; mas \u0026#34;iMovie 剪辑\u0026#34;, id: 408981434 记录了通过 brew 和 App Store 安装的软件，之后可以通过 brew bundle --file=\u0026quot;./Brewfile\u0026quot;导入。\nAutomator 配置 # Automator 是 masOS 下的一个自动操作工具\n“自动操作”能让电脑上的大部分操作自动进行。有了“自动操作”，要创建自动化操作，您不必了解复杂的编程或脚本语言，只需使用“自动操作”资源库中数百个可用操作中的任意一些，便可以创建工作流程。这些操作能与各种 App 和 macOS 的部分程序进行互动。工作流程可以简单到只有一个操作，也可以包含许多操作以执行一系列复杂的任务。\nhttps://support.apple.com/zh-cn/guide/automator/welcome/mac\n我们可以通过 Automator 创建个自动备份 Brewfile 的程序，还可以同步到 GitHub 上防止丢失。\n首先创建一个日历提醒\n在左侧资源库中选择运行 Shell 脚本\n然后填写上导出 Brewfile 的命令\ncd ~/workspace/github-self/mac-init # 导出 Brewfile，注意 brew 命令要写全路径 /usr/local/bin/brew bundle dump --force --describe # 以下是提交 GitHub git add --all git commit -m \u0026#34;update brewfile\u0026#34; git pull git push 写好后可以点击右上角的运行按钮来测试下，打开结果可以看到运行输出\n之后 ctrl+s 保存，输入文件名后会自动创建一个日历提醒，重复里选上每周，之后就可以让电脑自动备份安装过的软件列表了。\n下次在新电脑上就可以执行brew bundle --file=\u0026quot;./Brewfile\u0026quot;来恢复安装过的软件了，是不是很方便呢。\n如果怕忘了命令还可以写个脚本来自动执行，比如我的 https://github.com/ld000/mac-init。\n","date":"15 September 2019","externalUrl":null,"permalink":"/posts/automator_brewfile/","section":"Posts","summary":"","title":"MacOS 用 automater 自动备份安装的软件列表","type":"posts"},{"content":"","date":"23 July 2018","externalUrl":null,"permalink":"/tags/cas/","section":"Tags","summary":"","title":"Cas","type":"tags"},{"content":"","date":"23 July 2018","externalUrl":null,"permalink":"/tags/discuz/","section":"Tags","summary":"","title":"Discuz","type":"tags"},{"content":"discuz 接入 cas 实现 SSO 的方法。\n版本 # Discuz 3.3 Cas 5.x 下载 casPHP 插件 # http://developer.jasig.org/cas-clients/php/\n我这里用的 1.3.0，下载完后解压拷贝到 discuz 根目录，重命名为 CAS。\n去除登录输入 # 修改 template/default/member/login_simple.htm\n删除8-29行代码，删除31-32行代码\n添加以下代码，支持访问时 force 登录.\n# 需要 CAS 打开允许 iframe, 否则报错: Refused to display \u0026#39;URL\u0026#39; in a frame because it set \u0026#39;X-Frame-Options\u0026#39; to \u0026#39;DENY\u0026#39; # cas 配置：cas.httpWebRequest.header.xframe=false \u0026lt;script type=\u0026#34;text/javascript\u0026#34;\u0026gt; (function () { var _body = document.body || document.getElementsByTagName(\u0026#39;body\u0026#39;)[0]; var iframe = document.createElement(\u0026#39;iframe\u0026#39;); iframe.id = \u0026#39;logcheck\u0026#39;; iframe.src = \u0026#34;member.php?\u0026#34;+((\u0026#34;mod=logging\u0026amp;action=loginCheck\u0026#34;)); iframe.style.display = \u0026#39;none\u0026#39;; _body.appendChild(iframe); })(); \u0026lt;/script\u0026gt; # TODO 现只能在 CAS 登录后才能登录，需做修改 去除弹框登录 # 管理中心 -\u0026gt; 界面 -\u0026gt; 去掉浮动窗口（登录）\n在CAS文件夹中创建 CasClientConfig.php # \u0026lt;?php define ( \u0026#39;CAS_SERVER_HOSTNAME\u0026#39;, \u0026#39;cas.com\u0026#39; ); define ( \u0026#39;CAS_SERVER_PORT\u0026#39;, 8080 ); define ( \u0026#39;CAS_SERVER_APP_NAME\u0026#39;, \u0026#34;cas\u0026#34; ); ?\u0026gt; 在CAS文件夹中创建 CasClient.php # \u0026lt;?php require_once DISCUZ_ROOT.\u0026#39;./CAS/CasClientConfig.php\u0026#39;; // 注意 require_once DISCUZ_ROOT.\u0026#39;./CAS/CAS.php\u0026#39;; // 注意 // 初始化 //phpCAS::setDebug (); // initialize phpCAS phpCAS::client ( CAS_VERSION_2_0, CAS_SERVER_HOSTNAME, CAS_SERVER_PORT, CAS_SERVER_APP_NAME ); // no SSL validation for the CAS server phpCAS::setNoCasServerValidation (); phpCAS::setNoClearTicketsFromUrl (); phpCAS::handleLogoutRequests(); ?\u0026gt; source/class/class_core.php第16行加入 # require_once DISCUZ_ROOT.\u0026#34;CAS/CasClient.php\u0026#34;; uc_client/control/user.php # 134行注释\n// elseif($user[\u0026#39;password\u0026#39;] != md5($passwordmd5.$user[\u0026#39;salt\u0026#39;])) { // $status = -2; // } elseif($checkques \u0026amp;\u0026amp; $user[\u0026#39;secques\u0026#39;] != $_ENV[\u0026#39;user\u0026#39;]-\u0026gt;quescrypt($questionid, $answer)) { // $status = -3; // } 注释 onsynlogin, onsynlogout, onregister方法\nsource/function/function_member.php 加入 # // 新加的方法，用以支持CAS 登录 function userloginCas($username, $ip = \u0026#39;\u0026#39;) { $return = array (); if(!function_exists(\u0026#39;uc_user_login\u0026#39;)) { loaducenter(); } $return[\u0026#39;ucresult\u0026#39;] = uc_user_login(addslashes($username), \u0026#39;\u0026#39;, 0, 0,\u0026#39;\u0026#39;, \u0026#39;\u0026#39;, $ip); $tmp = array (); $duplicate = \u0026#39;\u0026#39;; list ( $tmp [\u0026#39;uid\u0026#39;], $tmp [\u0026#39;username\u0026#39;], $tmp [\u0026#39;password\u0026#39;], $tmp [\u0026#39;email\u0026#39;], $duplicate ) = $return [\u0026#39;ucresult\u0026#39;]; $return [\u0026#39;ucresult\u0026#39;] = $tmp; if ($duplicate \u0026amp;\u0026amp; $return [\u0026#39;ucresult\u0026#39;] [\u0026#39;uid\u0026#39;] \u0026gt; 0 || $return [\u0026#39;ucresult\u0026#39;] [\u0026#39;uid\u0026#39;] \u0026lt;= 0) { $return [\u0026#39;status\u0026#39;] = 0; return $return; } $member = getuserbyuid ( $return [\u0026#39;ucresult\u0026#39;] [\u0026#39;uid\u0026#39;], 1 ); if (! $member || empty ( $member [\u0026#39;uid\u0026#39;] )) { $return [\u0026#39;status\u0026#39;] = - 1; return $return; } $return [\u0026#39;member\u0026#39;] = $member; $return [\u0026#39;status\u0026#39;] = 1; if ($member [\u0026#39;_inarchive\u0026#39;]) { C::t ( \u0026#39;common_member_archive\u0026#39; )-\u0026gt;move_to_master ( $member [\u0026#39;uid\u0026#39;] ); } if ($member [\u0026#39;email\u0026#39;] != $return [\u0026#39;ucresult\u0026#39;] [\u0026#39;email\u0026#39;]) { C::t ( \u0026#39;common_member\u0026#39; )-\u0026gt;update ( $return [\u0026#39;ucresult\u0026#39;] [\u0026#39;uid\u0026#39;], array ( \u0026#39;email\u0026#39; =\u0026gt; $return [\u0026#39;ucresult\u0026#39;] [\u0026#39;email\u0026#39;] ) ); } return $return; } source/class/class_member.php # 51行注释并改为\n// if(!submitcheck(\u0026#39;loginsubmit\u0026#39;, 1, $seccodestatus)) { if (1 == 2) { 92行 $_G['username'] = $_G['member']['username'] = $_G['member']['password'] = '' 后加入\nphpCAS::setNoClearTicketsFromUrl (); // 这里会检测服务器端的退出的通知，就能实现php和其他语言平台间同步登出了 phpCAS::handleLogoutRequests(); $username=\u0026#39;\u0026#39;; if(phpCAS::isAuthenticated()){ $username = phpCAS::getUser (); } else { $service = $_SERVER[\u0026#39;HTTP_REFERER\u0026#39;]; phpCAS::setServerLoginUrl(phpCAS::getServerLoginURL().urlencode(\u0026#34;?service=\u0026#34;.$service)); phpCAS::forceAuthentication (); } // if(!$_GET[\u0026#39;password\u0026#39;] || $_GET[\u0026#39;password\u0026#39;] != addslashes($_GET[\u0026#39;password\u0026#39;])) { // showmessage(\u0026#39;profile_passwd_illegal\u0026#39;); // } // $result = userlogin($_GET[\u0026#39;username\u0026#39;], $_GET[\u0026#39;password\u0026#39;], $_GET[\u0026#39;questionid\u0026#39;], $_GET[\u0026#39;answer\u0026#39;], $this-\u0026gt;setting[\u0026#39;autoidselect\u0026#39;] ? \u0026#39;auto\u0026#39; : $_GET[\u0026#39;loginfield\u0026#39;], $_G[\u0026#39;clientip\u0026#39;]); $result = userloginCas($username, $_G[\u0026#39;clientip\u0026#39;]); 347行 on_logout 方法\nif(defined(\u0026#39;IN_MOBILE\u0026#39;)) { showmessage(\u0026#39;location_logout_succeed_mobile\u0026#39;, dreferer(), array(\u0026#39;formhash\u0026#39; =\u0026gt; FORMHASH, \u0026#39;referer\u0026#39; =\u0026gt; rawurlencode(dreferer()))); } else { $service = $_SERVER[\u0026#39;HTTP_REFERER\u0026#39;]; phpCAS::logoutWithRedirectService ( $service ); // showmessage(\u0026#39;logout_succeed\u0026#39;, dreferer(), array(\u0026#39;formhash\u0026#39; =\u0026gt; FORMHASH, \u0026#39;ucsynlogout\u0026#39; =\u0026gt; $ucsynlogout, \u0026#39;referer\u0026#39; =\u0026gt; rawurlencode(dreferer()))); } 386行 on_register方法中\nif(strpos($url_forward, $this-\u0026gt;setting[\u0026#39;regname\u0026#39;]) !== false) { $url_forward = \u0026#39;forum.php\u0026#39;; } // 修改掉防止当登录成功时无限跳转 $url_forward = \u0026#39;forum.php\u0026#39;; logging_ctl类中 添加异步登录验证方法\nfunction on_loginCheck(){ global $_G; $username=\u0026#39;\u0026#39;; if(phpCAS::isAuthenticated()){ $username = phpCAS::getUser (); } else { phpCAS::setServerLoginUrl(phpCAS::getServerLoginURL()); phpCAS::forceAuthentication (); } $result = userloginCas($username, $_G[\u0026#39;clientip\u0026#39;]); if($data = uc_get_user($username)) { list($uid, $username, $email) = $data; //根据用户名获取uid进行登录 } else { dexit(); } if($uid \u0026gt; 0) { $member = getuserbyuid($uid, 1); //根据uid获取用户表pre_common_member中的所有字段 $_G[\u0026#39;uid\u0026#39;] = intval($uid); $_G[\u0026#39;username\u0026#39;] = $username; $_G[\u0026#39;adminid\u0026#39;] = $member[\u0026#39;adminid\u0026#39;]; $_G[\u0026#39;groupid\u0026#39;] = $member[\u0026#39;groupid\u0026#39;]; $_G[\u0026#39;formhash\u0026#39;] = formhash(); $_G[\u0026#39;session\u0026#39;][\u0026#39;invisible\u0026#39;] = getuserprofile(\u0026#39;invisible\u0026#39;); $_G[\u0026#39;member\u0026#39;] = $member; loadcache(\u0026#39;usergroup_\u0026#39;.$_G[\u0026#39;groupid\u0026#39;]); C::app()-\u0026gt;session-\u0026gt;isnew = true; C::app()-\u0026gt;session-\u0026gt;updatesession(); dsetcookie(\u0026#39;auth\u0026#39;, authcode(\u0026#34;{$member[\u0026#39;password\u0026#39;]}\\t{$member[\u0026#39;uid\u0026#39;]}\u0026#34;, \u0026#39;ENCODE\u0026#39;), $cookietime, 1, true); //这里的passwod是pre_common_member表中经过加密后的密码 dsetcookie(\u0026#39;loginuser\u0026#39;,$username); dsetcookie(\u0026#39;activationauth\u0026#39;); dsetcookie(\u0026#39;pmnum\u0026#39;); dexit(\u0026#39;\u0026lt;script type=\u0026#34;text/javascript\u0026#34;\u0026gt;window.top.location.reload();\u0026lt;/script\u0026gt;\u0026#39;); } else { dexit(); } } source/module/member/member_logging.php 添加允许 loginCheck 方法通过 # 第15行改为 if(!in_array($_GET['action'], array('login', 'logout','loginCheck'))) 额外配置，可选 # 自动https/http # CAS/CAS/Client.php\n299行 $this-\u0026gt;_server['base_url'] = 'https://' . $this-\u0026gt;_getServerHostname(); 改为 $this-\u0026gt;_server['base_url'] = ($this-\u0026gt;_isHttps() ? 'https':'http').'://'. $this-\u0026gt;_getServerHostname();\n关闭gateway # CAS/CAS/Client.php\n1164行 $this-\u0026gt;redirectToCas(true/* gateway */); 改为 $this-\u0026gt;redirectToCas(false/* gateway */);\n","date":"23 July 2018","externalUrl":null,"permalink":"/posts/discuz_cas/","section":"Posts","summary":"discuz 接入 cas 实现 SSO 的方法。\n","title":"discuz 接入 cas","type":"posts"},{"content":"java 调用 c# 程序的几种方式。\nNative Java # Java不能直接调用C#的dll，这是因为C#和Java一样，也是运行在虚拟机上的语言，所生成的dll并不是和C++一样的native dll，因此没有办法使用JNI进行直接调用。\nJava调用C#的基本思路是：\nJava \u0026lt;=== JNI ===\u0026gt; C++ Native dll \u0026lt;=== CLR ===\u0026gt; C# dll\n给C# dll做一个C++的wrapper，Java与C++之间通过JNI实现调用，而C++和C#之间通过CLR实现调用。\n这样做有个缺陷就是不能在 linux 上运行。\n有以下几种方法实现：\njni4net # github: https://github.com/jni4net/jni4net\nimport java.io.File; import java.io.IOException; import testlib.Test; import net.sf.jni4net.Bridge; ... try { Bridge.setVerbose(true); Bridge.init(); Bridge.LoadAndRegisterAssemblyFrom(new File(\u0026#34;testlib.j4n.dll\u0026#34;)); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } Test test = new Test(); test.Hello(); test.Repeat(\u0026#34;It works!\u0026#34;); 缺点：\n很长时间没更新了 不支持在 Mono 和 Linux 上运行。原文：It\u0026rsquo;s not supported at the moment, sorry 性能一般 javonet # 按年收费\npublic void GenerateRandomNumber() throws JavonetException { NObject objRandom = Javonet.New(\u0026#34;System.Random\u0026#34;); int value = objRandom.invoke(\u0026#34;Next\u0026#34;,10,20); System.out.println(value); } 自己实现一个C++ native dll作为你的C# dll的代理 # 基本的实现思路是：\n对应你的C# dll，写一套Java版本的API。 使用javah命令，将Java API翻译成对应的C++ API，并输出到对应的xxxxJNI.h文件中。 创建C++ dll工程，添加xxxxJNI.h以及jdk提供的jni.h和jni_md.h，并启用clr支持。 在C++ dll工程中通过managed C++调用你的C#dll，进而实现所有的C++ API。 操作过程可参考 How-to-wrap-a-Csharp-library-for-use-in-Java 和 Java 调用 C# DLL.\n使用中间文件做交互 # 可以把输入和输出存到一个中间文本文件中(json, csv, txt)，让 c# 程序调用，然后输出一个文本文件。\n缺点：\n过程繁琐。 java 和 c# 方都无法感知对方处理进度，无法方便的确定输入输出完成时间。(可以在文本结尾放置一段特殊字符作为处理结束标志) 程序异常不好发现。 接口调用 # c# 将输入封装成 http 或 socket 接口，供 java 调用。c# 程序编译成 exe，在安装了 Mono 的 Linux 机器上运行，或运行在 Mono Docker 镜像中。\n参考\nhttp://ju.outofmemory.cn/entry/153844\n","date":"28 June 2017","externalUrl":null,"permalink":"/posts/java_call_cnet/","section":"Posts","summary":"java 调用 c# 程序的几种方式。\n","title":"java 调用 c# 的方法","type":"posts"},{"content":"","date":"12 June 2017","externalUrl":null,"permalink":"/tags/aspect/","section":"Tags","summary":"","title":"Aspect","type":"tags"},{"content":"","date":"12 June 2017","externalUrl":null,"permalink":"/tags/spring/","section":"Tags","summary":"","title":"Spring","type":"tags"},{"content":"原文：http://denis-zhdanov.blogspot.com/2009/07/spring-aop-top-problem-1-aspects-are.html\n这篇文章继续讨论从 Spring AOP top problem #2 - java.lang.ClassCastException: $Proxy7 开始的话题。在这个话题里，我想要说明一些现在接触 spring AOP 的 spring 用户(特别是新用户)讨论最多(从我的观点看来)的问题。\n现在我想要聊一聊自身调用(\u0026lsquo;self-calls\u0026rsquo;)，如果你有这方面的经验可以跳过接下来的内容.\n请注意 spring 文档也描述了一个同样的问题 - 8.6.1 Understanding AOP proxies。然而，我发现通过 spring 论坛里的很多文章显示人们并没有发现这个问题。所以，我想要把这个问题解释的更明白些 / 用我的语言。\n让我们创建一个说明这个问题的例子。假设我们是 spring 的新用户而且用 spring aop 写了一个非常酷的代码，并且运行的很好。\nAopService.java\npackage com.spring.aop; import org.springframework.stereotype.Component; @Component public class AopService { public void service() { System.out.println(\u0026#34;AopService.service()\u0026#34;); } public void anotherService() { System.out.println(\u0026#34;AopService.anotherService()\u0026#34;); } } TestAspect.java\npackage com.spring.aop; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Around; import org.springframework.stereotype.Component; @Component @Aspect public class TestAspect { @Around(\u0026#34;execution(* com.spring.aop.AopService.*(..))\u0026#34;) public Object advice(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println(\u0026#34;TestAspect.advice()\u0026#34;); return joinPoint.proceed(); } } spring-config.xml\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;beans xmlns=\u0026#34;http://www.springframework.org/schema/beans\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xmlns:aop=\u0026#34;http://www.springframework.org/schema/aop\u0026#34; xmlns:context=\u0026#34;http://www.springframework.org/schema/context\u0026#34; xsi:schemaLocation=\u0026#34; http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd\u0026#34;\u0026gt; \u0026lt;context:component-scan base-package=\u0026#34;com.spring.aop\u0026#34;/\u0026gt; \u0026lt;aop:aspectj-autoproxy/\u0026gt; \u0026lt;/beans\u0026gt; SpringStart.java\npackage com.spring; import com.spring.aop.AopService; import org.springframework.context.support.ClassPathXmlApplicationContext; public class SpringStart { public static void main(String[] args) throws Exception { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(\u0026#34;spring-config.xml\u0026#34;); AopService service = context.getBeansOfType(AopService.class).values().iterator().next(); service.service(); service.anotherService(); } } 我们创建了一个包含简单的只会打印一些输出来说明被调用过的方法和简单的 aspect 注入这个类中所有 public 方法并且打印信息来说明注入被执行了的 service 类。如果我们执行 SpringStart 类，我们会看到期望的输出 - 方法都被注入成功。\nTestAspect.advice() AopService.service() TestAspect.advice() AopService.anotherService() 让我们稍微拓展下 service 类。\nAopService.java\npackage com.spring.aop; import org.springframework.stereotype.Component; @Component public class AopService { public void service() { System.out.println(\u0026#34;AopService.service()\u0026#34;); } public void anotherService() { System.out.println(\u0026#34;AopService.anotherService(). Calling AopService.service()...\u0026#34;); service(); } } 我们期望 aspect 执行 3次 - service() 和 anotherService() 在 SpringStart.main() 中调用，service() 在 AopService.anotherService() 中调用。然而，如果我们运行这个例子我们发现 aspect 只执行了两次（从 SpringStart.main() 开始执行）i.e. AopService.anotherService() 中调用的 service() 没有被注入。\nTestAspect.advice() AopService.service() TestAspect.advice() AopService.anotherService(). Calling AopService.service()... AopService.service() 通常，problem root is tightly connected to The Law of Leaky Abstractions。I.e. 我们对 aspect 的表现有特别的期待但是没有发现 spring aop 的规则不允许这些期待实现。\n通常的解释是 spring AOP 是基于代理(proxy-based)的，i.e. 他假定当 bean 作为依赖被使用，他的方法应该用 aspect 代理包裹(be advised by particular aspect(s) the container injects aspect-aware bean proxy)而不是 bean 本身。例如，返回的代理使用和和下面代码近似的方法:\npublic class AopServiceProxy { // Assuming that corresponding setters are introduced and the fields are defined. private TestAspect aspect; private AopService rawService; public void service() { aspect.advice(); rawService.service(); } public void anotherService() { aspect.advice(); anotherService(); } } I.e. 这个代理被 aspect 包裹而且在调用 raw bean 前他调用了必要的 aspect 方法。然而，上一个 AopService 实现在 anotherService() 内调用了 service() - 这样代理无法注入因为这样调用违反了 \u0026rsquo;this\u0026rsquo; 原则, i.e. 这个方法是 \u0026lsquo;rawBean\u0026rsquo; 自己调用的。这就是为什么 aspect 在这种情况下不起作用的答案。\n现在，当我们明白了问题原因我们来聊聊怎么解决它。至少有三种方法：\naspect 建议重写代码来避免 self-calls - 非常不方便，特别是如果你在维护遗留代码;\n用 aspect-aware proxy 替换 self-calls, i.e. 在 anotherService() 里用 ((AopService) AopContext.currentProxy()).service() 代替 service() - 也不方便，需要额外设置代理而且和 spring 的代码重复;\n用 aspectj weaving - 这是我喜欢的方案。他会直接注入 aspect 到对应的 class 里， i.e. 不需要所有的方法都必须被代理;\n我会写一篇文章来说明在 spring 中怎么使用各种类型的 aspectj weaving，这样会更加清楚的说明这个问题。\n","date":"12 June 2017","externalUrl":null,"permalink":"/posts/aspects_are_not_applied/","section":"Posts","summary":"原文：http://denis-zhdanov.blogspot.com/2009/07/spring-aop-top-problem-1-aspects-are.html\n这篇文章继续讨论从 Spring AOP top problem #2 - java.lang.ClassCastException: $Proxy7 开始的话题。在这个话题里，我想要说明一些现在接触 spring AOP 的 spring 用户(特别是新用户)讨论最多(从我的观点看来)的问题。\n","title":"翻译: Spring AOP 讨论最多的问题 #1 - aspects 没有生效","type":"posts"},{"content":"","date":"20 December 2016","externalUrl":null,"permalink":"/tags/html/","section":"Tags","summary":"","title":"Html","type":"tags"},{"content":"常见响应模式 # 大体流动模型(mostly fluid) 掉落列模型(column drop) 活动布局模型(layout shifter) 画布溢出模型(off canvas) 掉落列模型 # \u0026lt;div class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;box dark_blue\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;box light_blue\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;box green\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; .container { display: flex; flex-wrap: wrap; } .box { width: 100%; } @media screen and (min-width: 450px) { .dark-blue { width: 25%; } .light-blue { width: 75%; } } @media screen and (min-width: 550px) { .dark-blue, .green { width: 25%; } .light-blue { width: 50%; } } 大体流动模型 # \u0026lt;div class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;box dark_blue\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;box light_blue\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;box green\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;box red\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;box orange\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; .container { display: flex; flex-wrap: wrap; } .box { width: 100%; } @media screen and (min-width: 450px) { .light-blue, .green { width: 50%; } } @media screen and (min-width: 550px) { .dark-blue, .light-blue { width: 50%; } .red, .green, .orange { width: 33.333333%; } } @media screen and (min-width: 700px) { .container { width: 700px; margin-left: auto; margin-right: auto; } } 活动布局模型 # \u0026lt;div class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;box dark_blue\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;container\u0026#34; id=\u0026#34;container2\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;box light_blue\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;box green\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;box red\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; .container { width: 100%; /* .ddd */ display: flex; flex-wrap: wrap; } .box { width: 100%; } @media screen and (min-width: 500px) { .dark-blue { width: 50%; } #container2 { width: 50%; } } @media screen and (min-width: 600px) { .dark-blue { width: 25%; order: 1; } #container2 { width: 50%; } .red { width: 25%; order: -1; } } 画布溢出模型 # \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;!-- saved from url=(0070)http://udacity.github.io/RWDF-samples/Lesson4/patterns/off-canvas.html --\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt;\u0026lt;head\u0026gt;\u0026lt;meta http-equiv=\u0026#34;Content-Type\u0026#34; content=\u0026#34;text/html; charset=UTF-8\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1.0\u0026#34;\u0026gt; \u0026lt;title\u0026gt;\u0026lt;/title\u0026gt; \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; type=\u0026#34;text/css\u0026#34; href=\u0026#34;./off-canvas_files/default-styles.css\u0026#34;\u0026gt; \u0026lt;style type=\u0026#34;text/css\u0026#34;\u0026gt; html, body { height: 100%; width: 100%; } a#menu svg { width: 40px; fill: #000; } nav, main { padding: 1em; box-sizing: border-box; } main { width: 100%; height: 100%; } /* * Off-canvas layout styles. */ /* Since we\u0026#39;re mobile-first, by default, the drawer is hidden. */ nav { width: 300px; height: 100%; position: absolute; /* This trasform moves the drawer off canvas. */ -webkit-transform: translate(-300px, 0); transform: translate(-300px, 0); /* Optionally, we animate the drawer. */ transition: transform 0.3s ease; } nav.open { -webkit-transform: translate(0, 0); transform: translate(0, 0); } /* If there is enough space (\u0026gt; 600px), we keep the drawer open all the time. */ @media (min-width: 600px) { /* We open the drawer. */ nav { position:relative; -webkit-transform: translate(0, 0); transform: translate(0, 0); } /* We use Flexbox on the parent. */ body { display: -webkit-flex; display: flex; -webkit-flex-flow: row nowrap; flex-flow: row nowrap; } main { width: auto; /* Flex-grow streches the main content to fill all available space. */ flex-grow: 1; } } /* If there is space (\u0026gt; 800px), we keep the drawer open by default. */ @media (min-width: 600px) { main \u0026gt; #menu:after { content: \u0026#39;The drawer stays open if width \u0026gt; 600px\u0026#39;; } main p, nav p { text-decoration: line-through; } } \u0026lt;/style\u0026gt; \u0026lt;link rel=\u0026#34;import\u0026#34; href=\u0026#34;chrome-extension://melpgahbngpgnbhhccnopmlmpbmdaeoi/app/templates/feedback.html\u0026#34; id=\u0026#34;udacity-test-widget\u0026#34;\u0026gt;\u0026lt;script id=\u0026#34;ud-grader-options\u0026#34;\u0026gt;UdacityFEGradingEngine.turnOn();\u0026lt;/script\u0026gt;\u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;nav id=\u0026#34;drawer\u0026#34; class=\u0026#34;dark_blue\u0026#34;\u0026gt; \u0026lt;h2\u0026gt;Off Canvas\u0026lt;/h2\u0026gt; \u0026lt;p\u0026gt;Click outside the drawer to close\u0026lt;/p\u0026gt; \u0026lt;/nav\u0026gt; \u0026lt;main class=\u0026#34;light_blue\u0026#34;\u0026gt; \u0026lt;a id=\u0026#34;menu\u0026#34;\u0026gt; \u0026lt;svg xmlns=\u0026#34;http://www.w3.org/2000/svg\u0026#34; viewBox=\u0026#34;0 0 24 24\u0026#34;\u0026gt; \u0026lt;path d=\u0026#34;M2 6h20v3H2zm0 5h20v3H2zm0 5h20v3H2z\u0026#34;\u0026gt;\u0026lt;/path\u0026gt; \u0026lt;/svg\u0026gt; \u0026lt;/a\u0026gt; \u0026lt;p\u0026gt;Click on the menu icon to open the drawer\u0026lt;/p\u0026gt; \u0026lt;/main\u0026gt; \u0026lt;script\u0026gt; /* * Open the drawer when the menu ison is clicked. */ var menu = document.querySelector(\u0026#39;#menu\u0026#39;); var main = document.querySelector(\u0026#39;main\u0026#39;); var drawer = document.querySelector(\u0026#39;#drawer\u0026#39;); menu.addEventListener(\u0026#39;click\u0026#39;, function(e) { drawer.classList.toggle(\u0026#39;open\u0026#39;); e.stopPropagation(); }); main.addEventListener(\u0026#39;click\u0026#39;, function() { drawer.classList.remove(\u0026#39;open\u0026#39;); }); \u0026lt;/script\u0026gt;\u0026lt;script src=\u0026#34;chrome-extension://melpgahbngpgnbhhccnopmlmpbmdaeoi/app/js/libs/GE.js\u0026#34; id=\u0026#34;udacity-front-end-feedback\u0026#34;\u0026gt;\u0026lt;/script\u0026gt;\u0026lt;test-widget\u0026gt;\u0026lt;/test-widget\u0026gt;\u0026lt;/body\u0026gt;\u0026lt;/html\u0026gt; @import url(https://fonts.googleapis.com/css?family=Roboto); html, body { margin: 0; padding: 0; } body { font-family: \u0026#39;Roboto\u0026#39;, sans-serif; } .title { font-size: 2.5em; text-align: center; } .box { min-height: 150px; } .dark_blue { background-color: #2A457A; color: #efefef; } .light_blue { background-color: #099DD9; } .green { background-color: #0C8542; } .lime { background-color: rgb(179, 210, 52); } .seafoam { background-color: rgb(77, 190, 161); } .red { background-color: #EC1D3B; } .orange { background-color: #F79420; }","date":"20 December 2016","externalUrl":null,"permalink":"/posts/xyms/","section":"Posts","summary":"常见响应模式 # 大体流动模型(mostly fluid) 掉落列模型(column drop) 活动布局模型(layout shifter) 画布溢出模型(off canvas) ","title":"响应模式","type":"posts"},{"content":"","date":"16 December 2016","externalUrl":null,"permalink":"/tags/github-pages/","section":"Tags","summary":"","title":"Github-Pages","type":"tags"},{"content":"搭建一个基于 github-pages 和 jekyll 的免费博客\n环境 macOS 10.12.2\n申请 github-pages # 源地址 https://pages.github.com\n创建一个仓库 在 github 建立一个新仓库，命名为 username.github.io，用自己的用户名替换 username。note: 如果用户名不匹配，将会不起作用。\nclone 项目 git clone https://github.com/username/username.github.io\n创建 hello world。cd username.github.io \u0026amp;\u0026amp; echo \u0026quot;Hello World\u0026quot; \u0026gt; index.html\npush 项目。git add --all \u0026amp;\u0026amp; git commit -m \u0026quot;Initial commit\u0026quot; \u0026amp;\u0026amp; git push -u origin master\n访问 http://username.github.io，就能看到刚刚创建的网站了。\n搭建 jekyll 环境 # 输入 ruby --version 验证是否安装了 ruby（Mac 已经预装了）\n安装 jekyll，输入 gem install jekyll\n删除之前的 index.html，运行 jekyll new .，jekyll 就会自动生成一些博客的基础文件和目录。输入 jekyll serve，就可以访问了。默认地址 http://127.0.0.1:4000/。\n还可以在 _config.yml 中进行一些个性化配置。\n文档地址 http://jekyllcn.com/\n安装模板 # 如果用 jekyll 从头开始搭建一个网站就太费时间了，可以先找一个模板，然后再在上面做修改。\n一些模板网站：\nhttp://jekyllthemes.org/ https://mademistakes.com/work/jekyll-themes/ 我现在用的是第二个网站里的 Minimal Mistakes Theme 模板。\n效果图：\n安装过程 # 将项目 clone 下来，解压放入 github-pages 项目路径。\n用以下代码替换 Gemfile 里的内容：\nsource \u0026#34;https://rubygems.org\u0026#34; gem \u0026#34;github-pages\u0026#34;, group: :jekyll_plugins group :jekyll_plugins do gem \u0026#34;jekyll-paginate\u0026#34; gem \u0026#34;jekyll-sitemap\u0026#34; gem \u0026#34;jekyll-gist\u0026#34; gem \u0026#34;jekyll-feed\u0026#34; gem \u0026#34;jemoji\u0026#34; end 然后运行 bundle update 安装依赖。\n移除以下不需要的文件或文件夹：\n.editorconfig .gitattributes .github /docs /test CHANGELOG.md minimal-mistakes-jekyll.gemspec README.md screenshot-layouts.png screenshot.png 最后运行 bundle exec jekyll serve 就能在本地启动服务了。访问 localhost:4000 就能看到效果。\n碰到的坑，安装的时候会碰到以下错误 # Gem::Ext::BuildError: ERROR: Failed to build gem native extension. current directory: /Library/Ruby/Gems/2.0.0/gems/nokogiri-1.6.8.1/ext/nokogiri /System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/ruby -r ./siteconf20161208-38005-ye53g2.rb extconf.rb checking if the C compiler accepts ... yes checking if the C compiler accepts -Wno-error=unused-command-line-argument-hard-error-in-future... no Building nokogiri using packaged libraries. Using mini_portile version 2.1.0 checking for iconv.h... yes checking for gzdopen() in -lz... yes checking for iconv... yes ************************************************************************ IMPORTANT NOTICE: Building Nokogiri with a packaged version of libxml2-2.9.4. Team Nokogiri will keep on doing their best to provide security updates in a timely manner, but if this is a concern for you and want to use the system library instead; abort this installation process and reinstall nokogiri as follows: gem install nokogiri -- --use-system-libraries [--with-xml2-config=/path/to/xml2-config] [--with-xslt-config=/path/to/xslt-config] If you are using Bundler, tell it to use the option: bundle config build.nokogiri --use-system-libraries bundle install Note, however, that nokogiri is not fully compatible with arbitrary versions of libxml2 provided by OS/package vendors. ************************************************************************ Extracting libxml2-2.9.4.tar.gz into tmp/x86_64-apple-darwin16/ports/libxml2/2.9.4... OK Running \u0026#39;configure\u0026#39; for libxml2 2.9.4... OK Running \u0026#39;compile\u0026#39; for libxml2 2.9.4... ERROR, review \u0026#39;/Library/Ruby/Gems/2.0.0/gems/nokogiri-1.6.8.1/ext/nokogiri/tmp/x86_64-apple-darwin16/ports/libxml2/2.9.4/compile.log\u0026#39; to see what happened. Last lines are: ======================================================================== CCLD libxml2.la CC testdso.lo CCLD testdso.la CC xmllint.o CCLD xmllint ld: warning: ignoring file /usr/local/Cellar/xz/5.2.2/lib/liblzma.dylib, file was built for x86_64 which is not the architecture being linked (i386): /usr/local/Cellar/xz/5.2.2/lib/liblzma.dylib Undefined symbols for architecture i386: \u0026#34;_lzma_auto_decoder\u0026#34;, referenced from: _xz_head in libxml2.a(xzlib.o) \u0026#34;_lzma_code\u0026#34;, referenced from: _xz_decomp in libxml2.a(xzlib.o) \u0026#34;_lzma_end\u0026#34;, referenced from: ___libxml2_xzclose in libxml2.a(xzlib.o) \u0026#34;_lzma_properties_decode\u0026#34;, referenced from: _is_format_lzma in libxml2.a(xzlib.o) ld: symbol(s) not found for architecture i386 clang: error: linker command failed with exit code 1 (use -v to see invocation) make[2]: *** [xmllint] Error 1 make[1]: *** [all-recursive] Error 1 make: *** [all] Error 2 ======================================================================== *** extconf.rb failed *** Could not create Makefile due to some reason, probably lack of necessary libraries and/or headers. Check the mkmf.log file for more details. You may need configuration options. Provided configuration options: --with-opt-dir --without-opt-dir --with-opt-include --without-opt-include=${opt-dir}/include --with-opt-lib --without-opt-lib=${opt-dir}/lib --with-make-prog --without-make-prog --srcdir=. --curdir --ruby=/System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/ruby --help --clean --use-system-libraries --enable-static --disable-static --with-zlib-dir --without-zlib-dir --with-zlib-include --without-zlib-include=${zlib-dir}/include --with-zlib-lib --without-zlib-lib=${zlib-dir}/lib --enable-cross-build --disable-cross-build /Library/Ruby/Gems/2.0.0/gems/mini_portile2-2.1.0/lib/mini_portile2/mini_portile.rb:366:in `block in execute\u0026#39;: Failed to complete compile task (RuntimeError) from /Library/Ruby/Gems/2.0.0/gems/mini_portile2-2.1.0/lib/mini_portile2/mini_portile.rb:337:in `chdir\u0026#39; from /Library/Ruby/Gems/2.0.0/gems/mini_portile2-2.1.0/lib/mini_portile2/mini_portile.rb:337:in `execute\u0026#39; from /Library/Ruby/Gems/2.0.0/gems/mini_portile2-2.1.0/lib/mini_portile2/mini_portile.rb:111:in `compile\u0026#39; from /Library/Ruby/Gems/2.0.0/gems/mini_portile2-2.1.0/lib/mini_portile2/mini_portile.rb:150:in `cook\u0026#39; from extconf.rb:365:in `block (2 levels) in process_recipe\u0026#39; from extconf.rb:258:in `block in chdir_for_build\u0026#39; from extconf.rb:257:in `chdir\u0026#39; from extconf.rb:257:in `chdir_for_build\u0026#39; from extconf.rb:364:in `block in process_recipe\u0026#39; from extconf.rb:263:in `tap\u0026#39; from extconf.rb:263:in `process_recipe\u0026#39; from extconf.rb:556:in `\u0026lt;main\u0026gt;\u0026#39; To see why this extension failed to compile, please check the mkmf.log which can be found here: /Library/Ruby/Gems/2.0.0/extensions/universal-darwin-16/2.0.0/nokogiri-1.6.8.1/mkmf.log extconf failed, exit code 1 Gem files will remain installed in /Library/Ruby/Gems/2.0.0/gems/nokogiri-1.6.8.1 for inspection. Results logged to /Library/Ruby/Gems/2.0.0/extensions/universal-darwin-16/2.0.0/nokogiri-1.6.8.1/gem_make.out An error occurred while installing nokogiri (1.6.8.1), and Bundler cannot continue. Make sure that `gem install nokogiri -v \u0026#39;1.6.8.1\u0026#39;` succeeds before bundling. 运行以下命令解决：\nbrew uninstall --ignore-dependencies xz bundle update ","date":"16 December 2016","externalUrl":null,"permalink":"/posts/github-pages-jekyll/","section":"Posts","summary":"","title":"搭建一个基于 github-pages 和 jekyll 的免费博客","type":"posts"},{"content":"List 实现所使用的标记接口，用来表明其支持快速（通常是固定时间）随机访问。此接口的主要目的是允许一般的算法更改其行为，从而在将其应用到随机或连续访问列表时能提供良好的性能。\n##用法\nRandom Access List(随机访问列表)如 ArrayList 要实现此接口，Sequence Access List(顺序访问列表)如 LinkedList 不要实现。 因为两者的高效遍历算法不同\n通常做法，遍历前先判断：\nif (list instance of RandomAccess) { for(int m = 0; m \u0026lt; list.size(); m++){} }else{ Iterator iter = list.iterator(); while(iter.hasNext()){} } 随机访问列表使用循环遍历，顺序访问列表使用迭代器遍历。\n##JDK 定义\nList 实现使用的标记接口，用来表明支持快速(通常是固定时间)随机访问。这个接口的主要目的是允许一般的算法更改它们的行为，从而在随机或连续访问列表时提供更好的性能。\n将操作随机访问列表(比如 ArrayList)的最好的算法应用到顺序访问列表(比如 LinkedList)时，会产生二次项行为。鼓励一般的列表算法检查给定的列表是否 instanceof 这个接口，防止在顺序访问列表时使用较差的算法，如果需要保证可接受的性能时可以更改算法。\n公认的是随机和顺序访问的区别通常是模糊的。例如，当一些 List 实现很大时会提供渐进的线性访问时间，但实际是固定的访问时间。这样的 List 实现通常应该实现此接口。通常来说，一个 List 的实现类应该实现这个接口如果\nfor (int i=0, n=list.size(); i \u0026lt; n; i++) list.get(i); 比\nfor (Iterator i=list.iterator(); i.hasNext(); ) i.next(); 快\n##验证事例\npackage com.ld.practice.arraylist; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.RandomAccess; /** * 测试Random Access List(随机访问列表)如 ArrayList 和 Sequence Access List(顺序访问列表)如 LinkedList \u0026lt;/br\u0026gt; * 不同遍历算法的效率\u0026lt;/br\u0026gt; * 结论：前者用循环，后者用迭代器 */ @SuppressWarnings({\u0026#34;rawtypes\u0026#34;, \u0026#34;unchecked\u0026#34;}) public class ListLoopTest { /** * 初始化 list，添加n个元素 * * @param list * @return */ public static \u0026lt;T\u0026gt; List initList(List list, int n) { for (int i = 0; i \u0026lt; n; i++) list.add(i); return list; } /** * 遍历 list，判断是否实现 RandomAccess 接口来使用不同的遍历方法 * * @param list */ public static void accessList(List list) { long startTime = System.currentTimeMillis(); if (list instanceof RandomAccess) { System.out.println(\u0026#34;实现了 RandomAccess 接口...\u0026#34;); for (int i = 0; i \u0026lt; list.size(); i++) { list.get(i); } } else { System.out.println(\u0026#34;没实现 RandomAccess 接口...\u0026#34;); for (Iterator iterator = list.iterator(); iterator.hasNext();) { iterator.next(); } } long endTime = System.currentTimeMillis(); System.out.println(\u0026#34;遍历时间：\u0026#34; + (endTime - startTime)); } /** * loop 遍历 list */ public static void accessListByLoop(List list) { long startTime = System.currentTimeMillis(); for (int i = 0; i \u0026lt; list.size(); i++) { list.get(i); } long endTime = System.currentTimeMillis(); System.out.println(\u0026#34;loop遍历时间：\u0026#34; + (endTime - startTime)); } /** * 迭代器遍历 */ public static void accessListByIterator(List list) { long startTime = System.currentTimeMillis(); for (Iterator iterator = list.iterator(); iterator.hasNext();) { iterator.next(); } long endTime = System.currentTimeMillis(); System.out.println(\u0026#34;Iterator遍历时间：\u0026#34; + (endTime - startTime)); } public static void main(String[] args) { ArrayList\u0026lt;Integer\u0026gt; aList = (ArrayList\u0026lt;Integer\u0026gt;) initList(new ArrayList\u0026lt;\u0026gt;(), 2000000); LinkedList\u0026lt;Integer\u0026gt; lList = (LinkedList\u0026lt;Integer\u0026gt;) initList(new LinkedList\u0026lt;\u0026gt;(), 50000); accessList(aList); accessList(lList); System.out.println(\u0026#34;ArrayList\u0026#34;); accessListByLoop(aList); accessListByIterator(aList); System.out.println(\u0026#34;LinkedList\u0026#34;); accessListByLoop(lList); accessListByIterator(lList); } /* 实现了 RandomAccess 接口... 遍历时间：8 没实现 RandomAccess 接口... 遍历时间：2 ArrayList loop遍历时间：5 Iterator遍历时间：8 LinkedList loop遍历时间：1532 Iterator遍历时间：1 */ }","date":"19 March 2016","externalUrl":null,"permalink":"/posts/random-access/","section":"Posts","summary":"List 实现所使用的标记接口，用来表明其支持快速（通常是固定时间）随机访问。此接口的主要目的是允许一般的算法更改其行为，从而在将其应用到随机或连续访问列表时能提供良好的性能。\n","title":"RandomAccess 接口使用","type":"posts"},{"content":"","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"}]