[{"content":"When I finally gave this site a face — a profile avatar, a social-share preview image, and a favicon — I assumed it would be one setting. It is not. In the Blowfish theme these are three independent things, configured in three different places, and the one param whose name sounds like \u0026ldquo;the homepage picture\u0026rdquo; (homepageImage) controls none of them.\nThis post is the map I wish I\u0026rsquo;d had.\nThe trap: one mental model, three real settings # What you see Blowfish param Lives in Resolved from Profile avatar author.image languages.toml (per language) assets/ via resources.Get OG / social share image defaultSocialImage params.toml assets/ via resources.Get Browser favicon (no param) — fixed filenames static/ copied verbatim The lesson up front: homepageImage is a red herring. It feeds the background / hero homepage layouts as a banner image. If your homepage layout is profile (the portfolio-style landing), the avatar does not come from homepageImage — it comes from the author image. I confirmed this by reading the theme\u0026rsquo;s own partial, layouts/partials/home/profile.html, which pulls .Site.Params.Author.image and nothing else.\n1. The avatar — author.image, per language # Blowfish\u0026rsquo;s author lives under each language block, so the image is set per language (point both at the same file):\n# config/_default/languages.toml [en.params.author] name = \u0026#34;SJ.Wu\u0026#34; image = \u0026#34;img/avatar.jpeg\u0026#34; # relative to assets/ headline = \u0026#34;Software Engineer\u0026#34; [zh-tw.params.author] name = \u0026#34;SJ.Wu\u0026#34; image = \u0026#34;img/avatar.jpeg\u0026#34; headline = \u0026#34;軟體工程師\u0026#34; The path is relative to assets/, so the file lives at assets/img/avatar.jpeg. The theme runs it through Hugo\u0026rsquo;s image pipeline — Fill to a 288×288 square, quality 96 — so a roughly-square source is all you need; it crops to the short side automatically.\n2. The OG image — defaultSocialImage, not the avatar # The social-share preview (the card Facebook / LinkedIn / Slack render from your link) is a separate image and a separate param:\n# config/_default/params.toml defaultSocialImage = \u0026#34;img/og.png\u0026#34; # 1.91:1, relative to assets/ Two things worth knowing about how Blowfish picks the OG image (from layouts/partials/head.html):\nPer-page wins first. If a page bundle contains a resource named *featured*, *cover*, or *thumbnail*, that image becomes the page\u0026rsquo;s og:image. defaultSocialImage is only the fallback when no such resource exists — which is exactly what you want for the homepage and most posts. Aspect ratio is 1.91:1. The social standard is 1200×630. Make the image that shape from the start; cropping after the fact wastes the sides. A practical tip for generating an OG image that matches a stylized avatar: feed the avatar into an image model as a reference and prompt for a 1200×630 banner, same flat-illustration style, character on the left, negative space on the right — and bake no text into the image. Generators mangle small text; if you want a title, overlay it later in Figma/Canva where it stays crisp.\n3. The favicon — it does not come along for free # This is the one that surprises people: setting author.image and defaultSocialImage changes nothing about the favicon. Blowfish serves the favicon from a fixed set of filenames in static/, and a file you place there shadows the theme\u0026rsquo;s default:\nstatic/favicon.ico # multi-size: 48 / 32 / 16 static/favicon-32x32.png static/favicon-16x16.png static/apple-touch-icon.png # 180×180 I verified the site was still on the theme default by comparing checksums — the built public/favicon.ico had the same md5 as the theme\u0026rsquo;s bundled file. Until you drop your own files into static/, you\u0026rsquo;re shipping Blowfish\u0026rsquo;s icon.\nGenerating an \u0026ldquo;SJ\u0026rdquo; favicon from one command # A half-body avatar is unreadable at 16×16, so for the favicon I made a simple letter mark instead. ImageMagick produces the whole set from a single master:\nFONT=/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf # 512px master: rounded square + centered \u0026#34;SJ\u0026#34; magick -size 512x512 xc:none \\ -fill \u0026#34;#1f2a37\u0026#34; -draw \u0026#34;roundrectangle 0,0 511,511 96,96\u0026#34; \\ -font \u0026#34;$FONT\u0026#34; -fill \u0026#34;#f4f1ea\u0026#34; -gravity center -pointsize 250 \\ -annotate +0-10 \u0026#34;SJ\u0026#34; master.png # derivatives magick master.png -resize 180x180 static/apple-touch-icon.png magick master.png -resize 32x32 static/favicon-32x32.png magick master.png -resize 16x16 static/favicon-16x16.png magick master.png -define icon:auto-resize=48,32,16 static/favicon.ico The colours are pulled straight from the avatar — deep slate-navy for the background (the hair/outline colour), warm off-white for the letters (the tee / backdrop) — so the favicon and the avatar read as one brand.\nassets/ vs static/ — why the split matters # This is the quiet reason the three settings live where they do:\nassets/ is Hugo\u0026rsquo;s processing pipeline. resources.Get reads from here, and the theme can resize, crop, and fingerprint the output (that\u0026rsquo;s why the built avatar URL looks like avatar_hu_9d0…jpeg). The avatar and OG image go here because Blowfish transforms them. static/ is copied to the site root verbatim, no processing. Favicons must keep exact filenames and byte content, so they belong here. Put a favicon in assets/ and the theme won\u0026rsquo;t find it; put the avatar in static/ and you lose the automatic resizing. Match the file to the folder.\nVerifying the live site # After the GitHub Actions deploy, I checked the real output rather than trusting the build — including a cache-busting query so I saw the current asset:\n# OG meta tag and that the image actually serves curl -s https://sj-wu.com/ | grep -oE \u0026#39;\u0026lt;meta property=\u0026#34;og:image\u0026#34;[^\u0026gt;]*\u0026gt;\u0026#39; curl -s -o /dev/null -w \u0026#39;%{http_code} %{content_type}\\n\u0026#39; https://sj-wu.com/img/og.png # favicon is no longer the theme default curl -s \u0026#34;https://sj-wu.com/apple-touch-icon.png?nocache=$RANDOM\u0026#34; -o live.png All three returned 200, the OG tag pointed at the right URL, and the downloaded favicon was the new SJ mark.\nThe caches that make it look broken # Everything can be correct on the server and still look stale, because two caches sit in front of you:\nSocial platforms cache OG previews. Facebook / LinkedIn keep an old card until you re-scrape it in their post-inspector / sharing-debugger. Browsers cache favicons aggressively — often beyond a normal refresh. Use a private window, or append ?v=2 to force a re-fetch, before concluding anything is wrong. Takeaways # Avatar, OG image, and favicon are three settings in three places — don\u0026rsquo;t expect one to imply the others. homepageImage is not the avatar on a profile layout; author.image is. defaultSocialImage is only the OG fallback — page-level *featured* / *cover* / *thumbnail* resources override it. Favicons live in static/ as fixed filenames and must be replaced explicitly; nothing else touches them. assets/ = processed (resize/crop/fingerprint); static/ = verbatim. Put each file where its handling lives. When something \u0026ldquo;didn\u0026rsquo;t update,\u0026rdquo; suspect the cache before the config. ","date":"7 June 2026","externalUrl":null,"permalink":"/posts/blowfish-avatar-og-favicon/","section":"Blog","summary":"The profile avatar, the social-share OG image, and the browser favicon look like one ‘site picture’ — but in Blowfish they are three independent settings in three different places. Here’s which param does what, why homepageImage is none of them, and how I generated an SJ favicon from one command.","title":"Avatar, OG Image, and Favicon in Blowfish: Three Things, Three Params","type":"posts"},{"content":"","date":"7 June 2026","externalUrl":null,"permalink":"/posts/","section":"Blog","summary":"","title":"Blog","type":"posts"},{"content":"","date":"7 June 2026","externalUrl":null,"permalink":"/tags/blowfish/","section":"Tags","summary":"","title":"Blowfish","type":"tags"},{"content":"","date":"7 June 2026","externalUrl":null,"permalink":"/tags/favicon/","section":"Tags","summary":"","title":"Favicon","type":"tags"},{"content":"","date":"7 June 2026","externalUrl":null,"permalink":"/tags/hugo/","section":"Tags","summary":"","title":"Hugo","type":"tags"},{"content":"","date":"7 June 2026","externalUrl":null,"permalink":"/tags/open-graph/","section":"Tags","summary":"","title":"Open Graph","type":"tags"},{"content":"I build backend microservices that run in production, and I\u0026rsquo;m currently working on applied machine learning / computer vision for identity verification — liveness detection and deepfake detection. Earlier in my career I was an SSD firmware engineer, so I\u0026rsquo;m comfortable from the bare metal up to distributed systems and ML pipelines.\n","date":"7 June 2026","externalUrl":null,"permalink":"/","section":"SJ.Wu","summary":"I build backend microservices that run in production, and I’m currently working on applied machine learning / computer vision for identity verification — liveness detection and deepfake detection. Earlier in my career I was an SSD firmware engineer, so I’m comfortable from the bare metal up to distributed systems and ML pipelines.\n","title":"SJ.Wu","type":"page"},{"content":"","date":"7 June 2026","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"7 June 2026","externalUrl":null,"permalink":"/tags/cloudflare/","section":"Tags","summary":"","title":"Cloudflare","type":"tags"},{"content":"","date":"7 June 2026","externalUrl":null,"permalink":"/tags/dns/","section":"Tags","summary":"","title":"DNS","type":"tags"},{"content":"","date":"7 June 2026","externalUrl":null,"permalink":"/tags/github-pages/","section":"Tags","summary":"","title":"GitHub Pages","type":"tags"},{"content":"A GitHub Pages site starts life at \u0026lt;user\u0026gt;.github.io. Attaching your own domain is straightforward once you know the order, but a couple of steps have sharp edges — especially if your registrar is Cloudflare, where one default setting will silently stop HTTPS from ever working. Here\u0026rsquo;s the whole flow, written with example.com so you can drop in your own domain.\nRelated: Building This Site with AI in a Day.\nThe decision: apex vs www # You have to pick which form is canonical — the one the address bar settles on.\napex (example.com) — shortest, cleanest. The cost is DNS: an apex can\u0026rsquo;t be a CNAME, so you point it at GitHub\u0026rsquo;s four IPs with A (and AAAA for IPv6). www (www.example.com) — DNS is one tidy CNAME, but the address is longer. Going apex-canonical and letting www redirect to it is the common choice. GitHub handles that redirect automatically once both are configured, so both addresses work and the bar shows the bare apex.\nStep 1 — repo changes # Two things live in the repo, independent of DNS.\nA CNAME file tells GitHub which domain this site answers to. With Hugo, drop it in static/ so it\u0026rsquo;s copied verbatim into the build output:\nstatic/CNAME → example.com The file holds the canonical host only — the apex, not www.\nThe baseURL. Update it so generated absolute links, the sitemap, and RSS use the new host:\n# config/_default/hugo.toml baseURL = \u0026#34;https://example.com/\u0026#34; If you build in CI, check whether the workflow overrides baseURL. A common Pages workflow does:\nrun: hugo --gc --minify --baseURL \u0026#34;${{ steps.pages.outputs.base_url }}/\u0026#34; That base_url output becomes your custom domain after you set it in Pages, so no workflow change is needed — but the value in hugo.toml still matters for local builds, so update it anyway.\nPush these, and the deploy publishes the CNAME into the live output.\nStep 2 — Cloudflare DNS # A domain registered with Cloudflare is already using Cloudflare DNS, so this is just the DNS → Records tab. For an apex-canonical setup:\nType Name Value A @ 185.199.108.153 A @ 185.199.109.153 A @ 185.199.110.153 A @ 185.199.111.153 AAAA @ 2606:50c0:8000::153 AAAA @ 2606:50c0:8001::153 AAAA @ 2606:50c0:8002::153 AAAA @ 2606:50c0:8003::153 CNAME www \u0026lt;user\u0026gt;.github.io (These are GitHub\u0026rsquo;s published Pages IPs — worth confirming against GitHub\u0026rsquo;s docs in case they ever change.)\nThe gotcha: turn the orange cloud OFF # This is the one that bites. Cloudflare proxies records by default — the orange cloud. With proxying on, GitHub can\u0026rsquo;t complete the HTTP challenge it uses to issue a Let\u0026rsquo;s Encrypt certificate, so HTTPS never provisions; and if you force Cloudflare\u0026rsquo;s own TLS in front, you get a redirect loop. Set every record to \u0026ldquo;DNS only\u0026rdquo; — the grey cloud. Click the cloud icon until it\u0026rsquo;s grey.\nKeep it grey at least until the GitHub certificate is issued and \u0026ldquo;Enforce HTTPS\u0026rdquo; works. If you later want Cloudflare\u0026rsquo;s proxy/CDN, you can turn it on — but then set Cloudflare\u0026rsquo;s SSL/TLS mode to \u0026ldquo;Full\u0026rdquo; (never \u0026ldquo;Flexible\u0026rdquo;, which loops). The simplest, most reliable setup is to just leave it grey.\nStep 3 — GitHub Pages + HTTPS # In the site repo: Settings → Pages.\nCustom domain → enter example.com → Save. GitHub runs a DNS check; once your records have propagated it shows a green check. Wait for the certificate. This takes minutes to tens of minutes — the Enforce HTTPS box is greyed out until the cert is ready. Tick Enforce HTTPS. Now every http:// hit 301s to https://. Tip: push the CNAME (Step 1) before you fill in the custom domain, so the domain is already present in the deployed output when the DNS check runs.\nStep 4 — verify the cutover # DNS first:\ndig example.com +short # expect the four GitHub IPs dig www.example.com +short # expect: \u0026lt;user\u0026gt;.github.io. then the IPs Then every entry point should land on the canonical HTTPS URL:\ncurl -sI https://example.com | head -1 # HTTP/2 200 curl -sI https://www.example.com | head -1 # 301 -\u0026gt; https://example.com/ curl -sI http://example.com | head -1 # 301 -\u0026gt; https (Enforce HTTPS) curl -sI https://\u0026lt;user\u0026gt;.github.io | head -1 # 301 -\u0026gt; https://example.com/ Four entry points — apex, www, plain http, and the old github.io — all funnel to the canonical HTTPS URL. That\u0026rsquo;s the cutover done.\nStep 5 — domain verification (stop hijacking) # There\u0026rsquo;s a quieter risk worth closing. If you ever remove the domain from this repo without removing the DNS records, someone else could claim it on their Pages site. GitHub\u0026rsquo;s domain verification prevents that: a verified domain can only be used by your account.\nIt lives in your account settings, not the repo: Settings → Pages → \u0026ldquo;Add a domain\u0026rdquo;. GitHub gives you a TXT record like:\n_github-pages-challenge-\u0026lt;user\u0026gt;.example.com TXT \u0026lt;token\u0026gt; Add that TXT in Cloudflare (grey cloud — TXT isn\u0026rsquo;t proxied anyway), then click Verify. Confirm it resolves with:\ndig _github-pages-challenge-\u0026lt;user\u0026gt;.example.com TXT +short Once verified, the name is locked to your account. You can leave the TXT record in place.\nThe order, in one breath # Repo CNAME + baseURL → push → Cloudflare A/AAAA/www records grey cloud → Pages custom domain → wait for cert → Enforce HTTPS → verify with dig/curl → add the domain-verification TXT. The single thing most likely to waste your afternoon is the orange cloud. Turn it grey.\n","date":"7 June 2026","externalUrl":null,"permalink":"/posts/custom-domain-github-pages/","section":"Blog","summary":"Pointing a freshly registered apex domain at a GitHub Pages site, end to end: the repo changes, the Cloudflare DNS records, the one proxy setting that breaks HTTPS, and how to verify the cutover — plus domain verification to stop anyone hijacking the name.","title":"Moving a GitHub Pages Site to a Custom Domain (Cloudflare Registrar)","type":"posts"},{"content":"","date":"7 June 2026","externalUrl":null,"permalink":"/tags/ai/","section":"Tags","summary":"","title":"AI","type":"tags"},{"content":"This site is itself the product of a day of AI-assisted work (with Claude Code). The starting point was just a PDF résumé, a few scattered tech notes, and a talk write-up; the end point is this bilingual site that goes live the moment I push. Here\u0026rsquo;s the workflow, the discipline, and the gotchas — in case you want to do something similar.\nRelated: A Practical Guide to AI-Assisted Development.\nThe stack # Claude Code — the AI pair, doing most of the mechanical work, translation, and consistency checks. Hugo (extended) + the Blowfish theme — static site generator; Blowfish installed as a Hugo Module. GitHub Actions → GitHub Pages — push to main and it builds and deploys automatically. The workflow # 1. Positioning first, then the README # The first step wasn\u0026rsquo;t code — it was getting the positioning straight. A GitHub profile README is a business card, so I first decided the audience (job search / overseas), the angle (backend + applied ML/CV), and the depth (a minimal card, detail left to the site). I used a \u0026ldquo;drill down every decision branch\u0026rdquo; process to settle each choice before building — so I wouldn\u0026rsquo;t finish and then realize the direction was wrong.\nThe result is a minimal card: a one-line tagline, grouped tech badges, three contact links, and everything else routed to this site.\n2. Scaffold the site with Hugo + Blowfish # Install Blowfish via Hugo Modules (hugo mod init → import the theme → hugo mod get); upgrading later is just hugo mod get -u. Bilingual via Hugo\u0026rsquo;s built-in i18n: language definitions in languages.toml, content with .en.md / .zh-tw.md suffixes. Use Blowfish\u0026rsquo;s profile homepage layout, surfacing the résumé highlights (backend, ML/CV, talks, publications). 3. Automate the deploy with GitHub Actions # One workflow: push to main → install Hugo extended → hugo mod get → hugo --gc --minify → publish to GitHub Pages with the official upload-pages-artifact / deploy-pages actions. After that, publishing a post is just a push, live in a minute or two.\nThe one manual step: in the repo, Settings → Pages → Source = \u0026ldquo;GitHub Actions\u0026rdquo;. Set it once and forget it.\n4. Turning existing docs into posts — discipline is the point # This is the part that most needs a human in the loop; the AI shouldn\u0026rsquo;t decide it alone. I converted the résumé, a talk write-up, self-study notes, and work notes into bilingual posts, holding to a few rules:\nDe-identify (most important): for anything from work, describe only the domain and the technology — no employer name, no internal service names, no business specifics. For example, an internal Feign development guide whose example was a sensitive business case got its entire example domain swapped to a neutral \u0026ldquo;e-commerce order,\u0026rdquo; keeping only the generic architectural pattern. Strip private links: reference links pointing to a personal knowledge base are broken for readers and leak internal IDs — remove them all. Strip tracking params: when pasting course/external links, cut the long gclid / utm_* tail and keep just the clean URL. Bilingual: every post ships in both English and Chinese. After each deploy I run a quick audit (e.g. grep -ri over the published output for any leftover sensitive terms) and only call it done once the live site is clean.\nGotchas # Match the theme\u0026rsquo;s Hugo version: Blowfish has a tested maximum Hugo version; a newer Hugo throws a compatibility warning. Pinning both local and CI to the theme\u0026rsquo;s supported version is the simplest fix. Future dates get skipped: Hugo doesn\u0026rsquo;t build \u0026ldquo;future\u0026rdquo; posts by default. When I dated a post \u0026ldquo;today,\u0026rdquo; my machine\u0026rsquo;s timezone (UTC+8) meant that \u0026ldquo;today at UTC midnight\u0026rdquo; was actually still in the future, so the post wasn\u0026rsquo;t built. Fix: give the date an explicit timezone (or use the previous day). Config file naming: language definitions go in languages.toml; a filename like languages.en.toml makes Hugo treat the .en as a language qualifier and drop the keys. The .\u0026lt;lang\u0026gt; suffix is only for menus.\u0026lt;lang\u0026gt;.toml. Takeaways # AI did the mechanical work — scaffolding, the workflow, translation, formatting, consistency audits — quickly and reliably, which is what made \u0026ldquo;live in a day\u0026rdquo; realistic. But two things stayed firmly my responsibility: the positioning judgment, and policing the disclosure boundary — what can be public and what must be de-identified. The AI can flag it and do it, but you make the final call.\n","date":"7 June 2026","externalUrl":null,"permalink":"/posts/building-this-site-with-ai/","section":"Blog","summary":"Using Claude Code with Hugo + Blowfish + GitHub Actions to turn a PDF résumé and a pile of scattered tech notes into this bilingual, auto-deployed personal site — in a day. The workflow, the disclosure discipline, and the gotchas.","title":"Building This Site with AI in a Day: From Résumé and Notes to an Automated Deploy","type":"posts"},{"content":"","date":"7 June 2026","externalUrl":null,"permalink":"/tags/claude-code/","section":"Tags","summary":"","title":"Claude Code","type":"tags"},{"content":"","date":"7 June 2026","externalUrl":null,"permalink":"/tags/github-actions/","section":"Tags","summary":"","title":"GitHub Actions","type":"tags"},{"content":"AI coding tools like Claude Code are terminal-native — they run right inside the terminal. So a comfortable terminal environment directly amplifies AI collaboration: the AI produces a lot, and the human reviews, tweaks, and gatekeeps in the same all-keyboard environment. This post covers how I set up tmux + NeoVim + Yazi + LazyGit around Claude Code, and the gotchas I hit along the way.\nVersions: NeoVim v0.11.6, Yazi 26.5.6, tmux 3.6, terminal emulator Ghostty, primary language Python. (Personal custom keymaps are omitted — this is about the architecture, config, and workflow.)\n1. Why this combination pairs well with Claude Code # Think of these four tools as the \u0026ldquo;periphery\u0026rdquo; around Claude Code, each covering the part of AI collaboration the human most needs:\nTool Role in the AI-collaboration workflow tmux Run Claude Code in one pane and an editor or test runner in another, side by side with the actual code; the session stays alive so you can return to the same workspace anytime. NeoVim Review, tweak, and test the files the AI changed (neotest); LSP flags type/lint issues instantly, so you can quickly verify whether the AI\u0026rsquo;s change holds up. LazyGit The key gatekeeping stop: review the diff hunk by hunk in a TUI, stage selectively, and write the commit — the AI changes a batch, the human confirms each piece before it enters version control. Yazi Navigate the project and file tree quickly to find the path to hand to Claude Code. The core idea: keep the AI and the human in the same all-keyboard, all-terminal environment, with no switching between a GUI and the terminal. The shorter the review-and-iterate loop, the more AI collaboration pays off. NeoVim is the editing hub; tmux sits on the outside for pane management and handles the terminal capability negotiation between Ghostty and the TUIs (which is where most of the gotchas live — see §7).\n2. NeoVim # A Lua-based config with lazy.nvim as the plugin manager. Directory layout:\n~/.config/nvim/ ├── init.lua # entry: bootstrap lazy.nvim + load config ├── lazy-lock.json # plugin version lock └── lua/ ├── config/ │ ├── options.lua # vim.opt.* basic options │ └── keymaps.lua # keymaps (personal, omitted) └── plugins/ # one plugin spec per file ├── lsp.lua ├── completion.lua ├── telescope.lua ├── treesitter.lua ├── colorscheme.lua ├── motion.lua ├── autopairs.lua ├── testing.lua ├── lazygit.lua └── yazi.lua 2.1 Entry point init.lua # -- Bootstrap lazy.nvim local lazypath = vim.fn.stdpath(\u0026#34;data\u0026#34;) .. \u0026#34;/lazy/lazy.nvim\u0026#34; if not vim.loop.fs_stat(lazypath) then vim.fn.system({ \u0026#34;git\u0026#34;, \u0026#34;clone\u0026#34;, \u0026#34;--filter=blob:none\u0026#34;, \u0026#34;https://github.com/folke/lazy.nvim.git\u0026#34;, \u0026#34;--branch=stable\u0026#34;, lazypath, }) end vim.opt.rtp:prepend(lazypath) require(\u0026#34;config.options\u0026#34;) require(\u0026#34;config.keymaps\u0026#34;) require(\u0026#34;lazy\u0026#34;).setup(\u0026#34;plugins\u0026#34;, { change_detection = { notify = false }, }) Notes:\nOn first launch it auto git clones lazy.nvim (bootstrap) — no manual install. require(\u0026quot;lazy\u0026quot;).setup(\u0026quot;plugins\u0026quot;, ...) auto-loads every file under lua/plugins/. change_detection.notify = false: silence the \u0026ldquo;config changed on disk\u0026rdquo; notification — especially nice with Claude Code, since the AI edits files frequently and the notification would otherwise keep popping up. 2.2 Basic options config/options.lua # local opt = vim.opt opt.number = true -- line numbers opt.relativenumber = true -- relative numbers (pairs with j/k) opt.expandtab = true -- tabs to spaces opt.shiftwidth = 4 -- indent width 4 opt.tabstop = 4 -- tab display width 4 opt.smartindent = true -- smart indent opt.wrap = false -- no line wrap opt.cursorline = true -- highlight current line opt.scrolloff = 8 -- keep 8 lines above/below the cursor opt.signcolumn = \u0026#34;yes\u0026#34; -- always show the sign column (no icon jitter) opt.termguicolors = true -- 24-bit true color opt.updatetime = 250 -- update delay (affects diagnostics / CursorHold) opt.clipboard = \u0026#34;unnamedplus\u0026#34; -- share the system clipboard 2.3 Plugin overview # File Plugin Purpose colorscheme.lua rebelot/kanagawa.nvim color scheme (dragon) lsp.lua mason + mason-lspconfig + nvim-lspconfig + conform.nvim LSP / formatting completion.lua nvim-cmp + LuaSnip completion telescope.lua telescope.nvim fuzzy finding treesitter.lua (disabled, see below) parsing motion.lua hop.nvim + nvim-surround + substitute.nvim jump / surround / swap autopairs.lua nvim-autopairs auto-close pairs testing.lua neotest + neotest-python running tests lazygit.lua lazygit.nvim Git TUI integration yazi.lua yazi.nvim file-manager integration A few choices worth noting that bear directly on AI collaboration:\nLSP (lsp.lua): mason auto-installs pyright (types/completion) and ruff (lint/format). Uses NeoVim 0.11\u0026rsquo;s new API — vim.lsp.config() + vim.lsp.enable() — instead of lspconfig.setup() (avoids the deprecation warning, see §7). conform.nvim formats Python on save with ruff_format, so AI-produced code is normalized the moment it\u0026rsquo;s saved. The live LSP diagnostics are also the fastest gate for reviewing an AI change. Testing (testing.lua): neotest + neotest-python (runner = pytest) — run tests right in the editor after the AI edits, the most important feedback loop in AI collaboration. Completion (completion.lua): nvim-cmp, source priority nvim_lsp → luasnip → (secondary) buffer / path. Search (telescope.lua): find_files, live_grep, document_symbols, buffer switching — quickly locate the spot to hand to the AI. Motion (motion.lua): hop.nvim (two-char jump), nvim-surround, substitute.nvim (swap). Treesitter (treesitter.lua): NeoVim 0.11 ships parsers for common languages, so the nvim-treesitter plugin is disabled in favor of the built-in vim.treesitter: { \u0026#34;nvim-treesitter/nvim-treesitter\u0026#34;, enabled = false }, 3. Yazi (file manager) # Config at ~/.config/yazi/yazi.toml, currently minimal:\n[manager] show_hidden = false # hide dotfiles by default (toggle with . inside yazi) Usage: run yazi standalone in the terminal, or call it from NeoVim via the yazi.nvim plugin (open_for_directories = true so opening a directory uses Yazi too). During AI collaboration I use it to quickly browse the project structure and see which files the AI added or changed.\nRunning Yazi inside tmux needs extra terminal-capability setup, or you get garbled keystrokes — see §7.\n4. tmux # tmux is what lets \u0026ldquo;Claude Code and the editor coexist\u0026rdquo;: one pane runs Claude Code, another edits and runs tests, and the session persists. Config at ~/.tmux.conf:\n# Mouse support (click to switch panes and resize) set -g mouse on # Window index starts at 1 set -g base-index 1 # Vi-style keys setw -g mode-keys vi # Pane index also starts at 1 setw -g pane-base-index 1 # Auto-renumber windows after one is closed set -g renumber-windows on # Allow DCS/CSI passthrough so TUIs\u0026#39; capability probes don\u0026#39;t leak as keystrokes set -g allow-passthrough on # Tell tmux Ghostty\u0026#39;s capabilities so yazi doesn\u0026#39;t have to probe set -g default-terminal \u0026#34;tmux-256color\u0026#34; set -as terminal-features \u0026#34;,xterm-ghostty:RGB:hyperlinks:usstyle\u0026#34; set -as terminal-features \u0026#34;,*:RGB\u0026#34; # New window / split inherits the current pane\u0026#39;s working directory bind c new-window -c \u0026#34;#{pane_current_path}\u0026#34; bind \u0026#39;\u0026#34;\u0026#39; split-window -v -c \u0026#34;#{pane_current_path}\u0026#34; bind % split-window -h -c \u0026#34;#{pane_current_path}\u0026#34; Highlights:\nSetting Effect mouse on switch/resize panes with the mouse base-index 1 / pane-base-index 1 windows and panes both start at 1 renumber-windows on renumber after closing a window allow-passthrough on key: lets TUI capability probes pass through correctly default-terminal \u0026quot;tmux-256color\u0026quot; correct terminfo terminal-features declare RGB / hyperlinks / underline-style support bind c / \u0026quot; / % with -c \u0026quot;#{pane_current_path}\u0026quot; new pane inherits the cwd 5. LazyGit # LazyGit is the gatekeeping stop for AI output: the AI changes a batch, and the human reviews the diff hunk by hunk in the TUI, stages selectively, and writes the commit — so every piece that enters version control has been looked at.\nConfig ~/.config/lazygit/config.yml is currently empty (LazyGit defaults). Usage: run lazygit standalone, or call it from NeoVim via the lazygit.nvim plugin. If you later want to customize (pager, commit templates, custom commands), add them to that yml.\n6. Install / restore steps # Rough order to rebuild the environment on a new machine:\nInstall the four tools (NeoVim ≥ 0.11, Yazi, tmux, LazyGit) — and Claude Code. Restore the config files: ~/.config/nvim/ ~/.config/yazi/yazi.toml ~/.tmux.conf ~/.config/lazygit/config.yml Launch NeoVim; lazy.nvim bootstraps and installs every plugin; mason auto-installs pyright and ruff. Reload tmux: tmux source-file ~/.tmux.conf (or restart the session). Restore exact plugin versions from lazy-lock.json: run :Lazy restore in NeoVim. Health check:\nnvim --headless -c \u0026#34;checkhealth\u0026#34; -c \u0026#34;qa\u0026#34; 7. Gotchas and fixes # Gotcha 1: Yazi shows garbage / injects junk keystrokes inside tmux # Symptom: opening Yazi (or another TUI) inside tmux shows odd characters, or keystrokes get junk sequences injected.\nCause: on startup Yazi emits terminal-capability \u0026ldquo;probe sequences\u0026rdquo; (DCS / CSI); by default tmux doesn\u0026rsquo;t pass these through to the outer terminal (Ghostty), so the responses get treated as keyboard input.\nFix (in .tmux.conf):\nset -g allow-passthrough on set -g default-terminal \u0026#34;tmux-256color\u0026#34; set -as terminal-features \u0026#34;,xterm-ghostty:RGB:hyperlinks:usstyle\u0026#34; set -as terminal-features \u0026#34;,*:RGB\u0026#34; allow-passthrough on: allow DCS/CSI passthrough. Declaring Ghostty\u0026rsquo;s capabilities via terminal-features means Yazi doesn\u0026rsquo;t need to probe, avoiding the leak at the source. Gotcha 2: NeoVim 0.11 LSP deprecation warning # Symptom: the classic require(\u0026quot;lspconfig\u0026quot;).pyright.setup{} throws a deprecation warning on 0.11.\nCause: NeoVim 0.11 introduced a built-in LSP config API; the old lspconfig.setup() flow is now deprecated.\nFix (in lsp.lua): use the new API —\nvim.lsp.config(\u0026#34;pyright\u0026#34;, { capabilities = ..., on_attach = ... }) vim.lsp.config(\u0026#34;ruff\u0026#34;, { capabilities = ..., on_attach = ... }) vim.lsp.enable({ \u0026#34;pyright\u0026#34;, \u0026#34;ruff\u0026#34; }) nvim-lspconfig stays, but only for its server defaults — its setup() is no longer called.\nGotcha 3: nvim-treesitter conflicts with / duplicates the 0.11 built-in parsers # Symptom: installing nvim-treesitter duplicates the built-in parsers and adds maintenance overhead.\nCause: NeoVim 0.11 already ships treesitter parsers for python, markdown, lua, and other common languages.\nFix: disable nvim-treesitter (enabled = false) and use the built-in vim.treesitter.\nGotcha 4: paste gets clobbered by a delete # Symptom: something you just yanked disappears after a delete, then a paste.\nCause: NeoVim\u0026rsquo;s unnamed register is also overwritten by deletes (d / x).\nFix: use the dedicated yank register \u0026quot;0 (it only records yanks and isn\u0026rsquo;t affected by deletes) and bind paste to read from \u0026quot;0 — then paste is stable.\nGotcha 5: clipboard out of sync with the system clipboard # Symptom: what you copy in NeoVim can\u0026rsquo;t be pasted into other apps (e.g. to paste code into an AI chat).\nFix (in options.lua):\nopt.clipboard = \u0026#34;unnamedplus\u0026#34; Note: on Linux you still need a clipboard provider (xclip / xsel / wl-clipboard), or unnamedplus does nothing.\nGotcha 6: signcolumn jitter # Symptom: text shifts left/right as LSP diagnostic icons appear/disappear.\nFix (in options.lua): always show the sign column —\nopt.signcolumn = \u0026#34;yes\u0026#34; Appendix: plugin version lock # NeoVim plugin versions are pinned in ~/.config/nvim/lazy-lock.json (git-commit level). On a new machine, :Lazy restore reproduces the exact same versions. Main plugins:\nlazy.nvim, kanagawa.nvim, mason.nvim / mason-lspconfig.nvim / nvim-lspconfig, nvim-cmp (+ cmp-nvim-lsp / cmp-buffer / cmp-path / LuaSnip / cmp_luasnip), conform.nvim, telescope.nvim (+ plenary.nvim), hop.nvim, nvim-surround, substitute.nvim, nvim-autopairs, neotest (+ nvim-nio / neotest-python), lazygit.nvim, yazi.nvim.\n","date":"1 May 2026","externalUrl":null,"permalink":"/posts/terminal-dev-environment/","section":"Blog","summary":"Claude Code is a terminal-native AI coding tool. Wrapping it in tmux + NeoVim + Yazi + LazyGit gives an all-keyboard, all-terminal workflow where the human focuses on reviewing and gatekeeping what the AI produces — plus the gotchas I hit setting it up.","title":"A Terminal Dev Environment Built for AI Collaboration: NeoVim + Yazi + tmux + LazyGit × Claude Code","type":"posts"},{"content":"","date":"1 May 2026","externalUrl":null,"permalink":"/tags/neovim/","section":"Tags","summary":"","title":"NeoVim","type":"tags"},{"content":"","date":"1 May 2026","externalUrl":null,"permalink":"/tags/terminal/","section":"Tags","summary":"","title":"Terminal","type":"tags"},{"content":"","date":"1 May 2026","externalUrl":null,"permalink":"/tags/tmux/","section":"Tags","summary":"","title":"Tmux","type":"tags"},{"content":"","date":"2026年5月1日","externalUrl":null,"permalink":"/zh-tw/tags/%E7%B5%82%E7%AB%AF%E6%A9%9F/","section":"Tags","summary":"","title":"終端機","type":"tags"},{"content":"This guide collects what I learned adopting AI-assisted development. It explains two features — Agent.md (CLAUDE.md / AGENTS.md) and Skills — and how to use a meta-prompt (letting the AI generate the config files for you) to bring them into a real workflow.\n1. Core concepts # Agent.md = the project\u0026rsquo;s permanent memory (always loaded)\nSkills = expert skills pulled in on demand (loaded when needed)\nAgent.md Skills When it loads Automatically, every conversation Only when invoked What it holds Project architecture, conventions, commands The SOP for a specific task Update cadence Evolves with the project Evolves with the workflow Token impact Fixed cost On-demand cost Cross-platform\nAgent.md (CLAUDE.md) works with Claude Code, Codex, and most tools. SKILL.md follows the Agent Skills Standard and works across 30+ tools (Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, and more). Write a SKILL.md once and use it everywhere. 2. Building an Agent.md / CLAUDE.md # 2.1 Section skeleton # A CLAUDE.md for a development project should usually contain these sections (each with a clear responsibility):\n## Conversation — language preference and reply style ## Architecture Overview — layered architecture, directory structure (table) ## Technology Stack — language versions, frameworks, core dependencies ## Build and Development Commands — build/run/test commands (code block) ## Configuration — environment config, profiles, environment variables ## Development Notes — architectural conventions, naming, common patterns ## Key Business Domains — core business domains ## Testing Strategy — test framework, mocking strategy, known pitfalls (❌/✅) 2.2 Meta-prompt template # You don\u0026rsquo;t have to hand-write CLAUDE.md. Paste the prompt below into an AI tool and let it scan the codebase and generate the file (Claude Code and Codex can also bootstrap a project with /init):\nScan the entire codebase, then produce markdown with the following structure: 1. Conversation — the developer\u0026#39;s language preference and reply style 2. Architecture Overview — project type, directory structure, module purposes (table) 3. Technology Stack — language versions, frameworks, main dependencies 4. Build and Development Commands — common build, run, and test commands 5. Configuration — how environments are configured (profiles, env vars) 6. Development Notes — architectural conventions, naming, common patterns 7. Key Business Domains — a brief description of the core business domains 8. Testing Strategy — test framework, mocking strategy, known pitfalls Requirements: - Present the directory structure as a table (module name | purpose) - Put commands in code blocks with comments - Record pitfalls as ❌/✅ comparisons - Keep the total length to 200–400 lines - Only write what\u0026#39;s genuinely needed for AI collaboration; skip the obvious 2.3 Presentation tips # Using a Java microservices project as an example, a few things that make a CLAUDE.md more useful:\nArchitecture Overview — list the services in a table so they\u0026rsquo;re scannable:\n| Service | Purpose | |-----------------|---------| | account-service | Account management (user accounts, balances, history) | | order-service | Order management (order creation, status tracking) | | ... | ... | Testing Strategy — record pitfalls as ❌/✅ comparisons so the AI follows them every time:\n// ❌ Wrong: null is skipped by MyBatis Plus, so a NOT NULL column errors out config.setRemark(null); // ✅ Right: an empty string is included in the INSERT SQL config.setRemark(\u0026#34;\u0026#34;); Build Commands — code blocks with comments, ready for the AI to copy when it needs to build:\n# Use project Java version source ~/.sdkman/bin/sdkman-init.sh \u0026amp;\u0026amp; sdk use java \u0026lt;version\u0026gt; # Build all modules mvn clean install # Run specific test class mvn test -Dtest=OrderControllerTest 3. Building a Skill # 3.1 SKILL.md structure # Each skill is a .claude/skills/{name}/SKILL.md (or .codex/skills/{name}/SKILL.md) file. The structure is simple:\n--- name: skill-name # required: the invocation name description: A description # required: what it\u0026#39;s for (also the auto-trigger match) --- # Skill title Put the instructions here, in Markdown. Use $ARGUMENTS to receive arguments the user passes in. Optional: put template files in a references/ subdirectory and reference them from SKILL.md.\nAnatomy — the frontmatter of a code-review skill:\n--- name: code-review description: Perform a thorough code review of backend code, covering conformance to architectural conventions, naming, response wrapping, security, business logic, distributed transactions, performance (N+1 queries, caching), null safety, and test coverage. Use when a feature is done, before opening a PR, or when a quality review is needed. Outputs a 🔴🟡🟢 severity-graded report. --- The more precise the description, the more accurate the auto-triggering.\nMinimal example — the grill-me skill (a few lines of body):\n--- name: grill-me description: Interview the user relentlessly about a plan or design until reaching shared understanding. Use when user wants to stress-test a plan. --- Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree. If a question can be answered by exploring the codebase, explore instead. Advanced example — with a references subdirectory + MCP integration:\n.claude/skills/task-flow/ ├── SKILL.md # main instructions (several steps) └── references/ ├── api-change-template.md # HTML template for API change notes └── test-report-template.md # HTML template for the self-test report SKILL.md can reference references/api-change-template.md to keep output consistent — the recommended way to manage templates inside a skill. Any MCP tools a skill integrates can be swapped for ones you prefer (Notion, Obsidian, or other MCP-capable tools).\n3.2 Generating a skill with skill-creator (6 steps) # You don\u0026rsquo;t have to hand-write SKILL.md. Both Claude Code and Codex ship with /skill-creator:\nType /skill-creator. Describe what you need in natural language (e.g. \u0026ldquo;I need a skill that, when I mention a task ID, reads the card from my task system, analyzes the requirements, and produces API docs and a test report\u0026rdquo;). skill-creator asks about the details (trigger conditions? output format? arguments?). It generates .claude/skills/{name}/SKILL.md. Test it with /skill-name. It\u0026rsquo;s cross-tool: the same SKILL.md is read directly by Codex, Cursor, etc. 4. Using Claude Code # 4.1 Setting up CLAUDE.md # Put it in the project root; Claude Code reads it automatically on startup. It supports additional CLAUDE.md files in subdirectories (nearest one wins) — e.g. services/order-service/CLAUDE.md for that service\u0026rsquo;s specific rules. No extra setup needed; it just works once it\u0026rsquo;s there. 4.2 Generating a skill with /skill-creator # A sample interaction:\nYou: /skill-creator AI: What kind of skill do you want to build? Describe what it\u0026#39;s for. You: I need a code review skill that thoroughly checks backend code for architectural conventions, security, business logic, and distributed transactions. AI: Got it — a few questions: 1. How is the review target specified? A path or a PR? 2. Preferred output format? 3. Which review dimensions do you need? You: [answers...] AI: [produces .claude/skills/code-review/SKILL.md] 4.3 Invoking a skill # Explicit invocation (slash commands):\n/code-review services/order-service /write-test OrderServiceImpl.createOrder /task-flow TASK-1234 Auto-triggering (matched against the description text):\nMentioning a task ID → triggers the task-flow skill. Saying \u0026ldquo;do a code review\u0026rdquo; → triggers the code-review skill. Saying \u0026ldquo;I need a new API\u0026rdquo; → triggers the new-api skill. Key point: write the description so the trigger conditions and boundaries are clear.\n4.4 An example set of team skills # In practice you can build a skill for each kind of repetitive team task. Here\u0026rsquo;s an illustrative set:\nSkill Purpose Trigger code-review Thorough code review (multi-dimension, 🔴🟡🟢 report) Before opening a PR write-test Write JUnit 5 unit tests (Given/When/Then) When tests are needed task-flow Full task workflow (analyze → spec → API docs → self-test report) When a task ID is mentioned new-api Scaffold a new API endpoint (Controller→Service→VO→DTO→ErrorCode) When a new API is needed new-service Scaffold a full new microservice When a new service is needed new-feign-client Add a Feign client (looks up the service registry, e.g. Eureka) Cross-service calls review-security Focused security review (IDOR, sensitive-resource access, token validation) Reviewing sensitive operations review-transaction Distributed-transaction review (transaction boundaries, undo logs) Multi-service writes annual-review Broad security scan (OWASP Top 10, P0–P3 grading) Security audits grill-me Deep interview on a plan/design (walks the decision tree) Stress-testing a plan 5. Using Codex # 5.1 Comparison table # Item Claude Code Codex Project memory file CLAUDE.md AGENTS.md Hierarchical override Subdirectory CLAUDE.md Subdirectory AGENTS.md + override files Size limit No explicit limit 32 KiB by default Skills format .claude/skills/*/SKILL.md Same (shared standard) .codex/skills/*/SKILL.md Skill generator /skill-creator built in $skill-creator built in Auto-trigger config Description text match Extra config in agents/openai.yaml 5.2 Key points # Skills are fully portable — no changes needed. Generate one with Claude Code\u0026rsquo;s /skill-creator, and Codex reads the same SKILL.md directly. Codex has agents/openai.yaml, where you can set allow_implicit_invocation: false to turn off auto-triggering. AGENTS.md has a structure similar to CLAUDE.md, but the two are not interchangeable (different filenames; each tool reads its own). Recommended approach: maintain one CLAUDE.md as the source of truth, and copy it to AGENTS.md with small tweaks when needed. 6. Case study: using the grill-me skill to write this guide # This guide was itself produced with the /grill-me skill. Here\u0026rsquo;s a look back at the experience.\nThe flow:\nRun /grill-me with a draft outline. grill-me walks the decision tree branch by branch (target audience → scope → format → depth of content). Each round asks 2–3 questions, each with a recommended answer. Once every branch is resolved, it consolidates everything into a complete plan. Pros:\nForces clear thinking — every \u0026ldquo;I thought I had this figured out\u0026rdquo; part got a follow-up that surfaced a blind spot (e.g. AGENTS.md and CLAUDE.md not being interchangeable). Leaves a decision trail — the series of Q\u0026amp;As becomes a complete record of the design decisions. Less rework — format, emphasis, and how to present examples were settled before any writing began. Self-directed research — during the interview it checked agentskills.io to confirm skill portability. Cons / caveats:\nTime — several rounds take about 15–20 minutes; simple tasks don\u0026rsquo;t need this heavy a process. Token usage — a deep interview plus web search plus codebase exploration uses more tokens. When to use — best for tasks where the direction is unclear or the impact is large; for a clear bug fix or small feature, just do it. Conclusion: grill-me fits situations where you need to think before you write — technical docs, architecture design, planning a new feature.\n7. Further reading # Claude Code docs Claude Code Skills Codex docs Codex AGENTS.md Agent Skills open standard The Complete Guide to Building Skills for Claude ","date":"31 March 2026","externalUrl":null,"permalink":"/posts/ai-collaboration-guide/","section":"Blog","summary":"How to use Agent.md (CLAUDE.md / AGENTS.md) and Skills to make AI-assisted development more effective: core concepts, meta-prompts, skill-creator, and the differences between Claude Code and Codex.","title":"A Practical Guide to AI-Assisted Development: Agent.md + Skills","type":"posts"},{"content":"","date":"31 March 2026","externalUrl":null,"permalink":"/tags/codex/","section":"Tags","summary":"","title":"Codex","type":"tags"},{"content":"","date":"31 March 2026","externalUrl":null,"permalink":"/tags/skills/","section":"Tags","summary":"","title":"Skills","type":"tags"},{"content":"","date":"1 March 2026","externalUrl":null,"permalink":"/tags/computer-vision/","section":"Tags","summary":"","title":"Computer Vision","type":"tags"},{"content":"","date":"1 March 2026","externalUrl":null,"permalink":"/tags/fastapi/","section":"Tags","summary":"","title":"FastAPI","type":"tags"},{"content":"A production identity-verification service that decides whether a face-verification submission comes from a real, live person — or from a spoof or deepfake.\nWhat it does # A multi-stage decision pipeline: quality gate → liveness gate → model consensus, returning an accept / review / reject decision with reasons. Combines physical liveness cues (multi-frame RGB light-reaction checks) with an ensemble of deep models for deepfake detection. Served as an HTTP API for synchronous verification. Tech # Models — convolutional networks (EfficientNet / Xception / CLIP-based) plus a gradient-boosting model, combined by consensus. Stack — Python, PyTorch, ONNX, FastAPI; end-to-end train → evaluate → deploy pipeline with automated retraining and reporting. Results — multi-model AUC ≥ 0.99 on held-out data; tuned to run within KYC latency budgets on CPU. ","date":"1 March 2026","externalUrl":null,"permalink":"/projects/kyc-liveness-detection/","section":"Projects","summary":"A production computer-vision service that decides whether a face-verification submission is a real, live person or a spoof / deepfake.","title":"KYC Liveness \u0026 Deepfake Detection","type":"projects"},{"content":"","date":"1 March 2026","externalUrl":null,"permalink":"/tags/machine-learning/","section":"Tags","summary":"","title":"Machine Learning","type":"tags"},{"content":"","date":"1 March 2026","externalUrl":null,"permalink":"/tags/onnx/","section":"Tags","summary":"","title":"ONNX","type":"tags"},{"content":"A few things I\u0026rsquo;ve built. Detail is kept at the domain-and-technology level.\n","date":"1 March 2026","externalUrl":null,"permalink":"/projects/","section":"Projects","summary":"A few things I’ve built. Detail is kept at the domain-and-technology level.\n","title":"Projects","type":"projects"},{"content":"","date":"1 March 2026","externalUrl":null,"permalink":"/tags/pytorch/","section":"Tags","summary":"","title":"PyTorch","type":"tags"},{"content":"","date":"2026年3月1日","externalUrl":null,"permalink":"/zh-tw/tags/%E6%A9%9F%E5%99%A8%E5%AD%B8%E7%BF%92/","section":"Tags","summary":"","title":"機器學習","type":"tags"},{"content":"","date":"2026年3月1日","externalUrl":null,"permalink":"/zh-tw/tags/%E9%9B%BB%E8%85%A6%E8%A6%96%E8%A6%BA/","section":"Tags","summary":"","title":"電腦視覺","type":"tags"},{"content":"","date":"11 February 2026","externalUrl":null,"permalink":"/tags/feign/","section":"Tags","summary":"","title":"Feign","type":"tags"},{"content":"","date":"11 February 2026","externalUrl":null,"permalink":"/tags/java/","section":"Tags","summary":"","title":"Java","type":"tags"},{"content":"","date":"11 February 2026","externalUrl":null,"permalink":"/tags/microservices/","section":"Tags","summary":"","title":"Microservices","type":"tags"},{"content":"In Spring Cloud microservices, cross-service calls usually go through OpenFeign. The problem with the traditional (legacy) approach is that the server-side controller and the consumer-side Feign client are written separately — the HTTP paths and request/response objects live in two places, so any change to the interface tends to drift between the two sides, and DTOs often get copy-pasted.\nThis post shares an improvement: put the Feign API interface, VOs, and DTOs in a single shared module that serves as the one contract between services. The server implements that interface as its controller; the consumer just @Autowired-injects it and calls it. There is only one interface, both sides depend on it, and nothing drifts.\nThe complete example below uses an e-commerce order service (the order domain).\n1. Architecture overview # ┌─────────────────────────────────────────────────────────────────┐ │ common-api (shared module) │ │ ├── api/ Feign API interface definitions (@FeignClient) │ │ ├── vo/ request objects (Value Object) │ │ └── dto/ response objects (Data Transfer Object) │ └────────────────────────────┬────────────────────────────────────┘ │ Maven dependency ┌───────────────────┼───────────────────┐ ▼ ▼ ┌─────────────────────┐ ┌─────────────────────────┐ │ order-service │ │ consumer microservice │ │ (server impl) │ │ (caller) │ │ ├── controller/ │ │ │ │ │ implements API │◄──── Feign ───│ @Autowired │ │ ├── service/ │ HTTP Call │ OrderApi api; │ │ └── service/impl/ │ │ │ └─────────────────────┘ └─────────────────────────┘ Core idea: the API interface is defined in common-api; the server implements it as a controller, and the consumer @Autowired-injects it to call it over Feign.\n2. Step-by-step # Step 1: Define the VO (request object) in common-api # Directory: common-api/src/main/java/com/example/commonapi/order/vo/\nA VO wraps the request parameters of an API. Conventions:\nImplement Serializable and define serialVersionUID. Use the Lombok quartet: @Data @Builder @AllArgsConstructor @NoArgsConstructor. Validate parameters with javax.validation annotations. Example: CreateOrderVO.java\npackage com.example.commonapi.order.vo; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import java.io.Serializable; /** * Create-order request VO */ @Data @Builder @AllArgsConstructor @NoArgsConstructor public class CreateOrderVO implements Serializable { private static final long serialVersionUID = 1L; @NotBlank(message = \u0026#34;customerId must not be blank\u0026#34;) private String customerId; @NotBlank(message = \u0026#34;productId must not be blank\u0026#34;) private String productId; @NotNull(message = \u0026#34;quantity must not be null\u0026#34;) private Integer quantity; @NotBlank(message = \u0026#34;amount must not be blank\u0026#34;) private String amount; /** optional note */ private String remark; @NotNull(message = \u0026#34;requestTime must not be null\u0026#34;) private Long requestTime; } Example: UpdateOrderStatusVO.java\npackage com.example.commonapi.order.vo; import com.example.commonapi.order.enums.OrderStatusEnum; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import java.io.Serializable; @Data @Builder @AllArgsConstructor @NoArgsConstructor public class UpdateOrderStatusVO implements Serializable { private static final long serialVersionUID = 1L; @NotBlank(message = \u0026#34;orderId must not be blank\u0026#34;) String orderId; @NotNull(message = \u0026#34;status must not be null\u0026#34;) OrderStatusEnum status; } Step 2: Define the DTO (response object) in common-api # Directory: common-api/src/main/java/com/example/commonapi/order/dto/\nA DTO wraps the response data of an API; same conventions as the VO.\nExample: OrderResultDTO.java\npackage com.example.commonapi.order.dto; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; /** * Order-processing result DTO */ @Data @Builder @AllArgsConstructor @NoArgsConstructor public class OrderResultDTO implements Serializable { private static final long serialVersionUID = 1L; /** whether the request was accepted */ private boolean accepted; /** result message */ private String message; /** order ID, for tracking */ private String orderId; } Step 3: Define the Feign API interface in common-api # Directory: common-api/src/main/java/com/example/commonapi/order/api/\nThis is the heart of the architecture — define the interface with @FeignClient, the server implements it, the consumer injects it.\nExample: OrderApi.java\npackage com.example.commonapi.order.api; import com.example.commonapi.order.dto.OrderResultDTO; import com.example.commonapi.order.vo.CreateOrderVO; import com.example.commonapi.order.vo.UpdateOrderStatusVO; import com.example.common.Result; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; /** * Order service API * Provides order creation and status updates. */ @FeignClient(name = \u0026#34;order-service\u0026#34;, path = \u0026#34;/api/order\u0026#34;) public interface OrderApi { @PostMapping(\u0026#34;/createOrder\u0026#34;) Result\u0026lt;OrderResultDTO\u0026gt; createOrder( @RequestBody @Validated CreateOrderVO vo); @PostMapping(\u0026#34;/updateOrderStatus\u0026#34;) Result\u0026lt;Void\u0026gt; updateOrderStatus( @RequestBody @Validated UpdateOrderStatusVO vo); } @FeignClient parameters:\nParameter Meaning Example name The target service\u0026rsquo;s registered name in Eureka \u0026quot;order-service\u0026quot; path The controller\u0026rsquo;s shared path prefix \u0026quot;/api/order\u0026quot; Method annotations:\nAnnotation Meaning @PostMapping Defines the HTTP endpoint path @RequestBody Pass the argument as a JSON body @Validated Enable validation (triggers the @NotBlank etc. in the VO) Step 4: Implement the API interface as a controller on the server # Directory: order-service/src/main/java/.../controller/\nThe controller implements the API interface directly — no need for @RequestMapping / @PostMapping path annotations; they\u0026rsquo;re all inherited from the interface definition.\nExample: OrderController.java\npackage com.example.orderservice.controller; import com.example.commonapi.order.api.OrderApi; import com.example.commonapi.order.dto.OrderResultDTO; import com.example.commonapi.order.vo.CreateOrderVO; import com.example.commonapi.order.vo.UpdateOrderStatusVO; import com.example.common.Result; import com.example.common.ResultUtil; import com.example.orderservice.service.OrderService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.RestController; @Slf4j @RestController @RequiredArgsConstructor public class OrderController implements OrderApi { private final OrderService orderService; @Override public Result\u0026lt;OrderResultDTO\u0026gt; createOrder(CreateOrderVO vo) { log.info(\u0026#34;create-order request: customerId={}, productId={}, amount={}\u0026#34;, vo.getCustomerId(), vo.getProductId(), vo.getAmount()); OrderResultDTO result = orderService.createOrder(vo); return ResultUtil.success(result); } @Override public Result\u0026lt;Void\u0026gt; updateOrderStatus(UpdateOrderStatusVO vo) { log.info(\u0026#34;update-status request: orderId={}, status={}\u0026#34;, vo.getOrderId(), vo.getStatus().getCode()); return orderService.updateOrderStatus( vo.getOrderId(), vo.getStatus()); } } Key points:\n@RestController is enough — no @RequestMapping. The path is the combination of the interface\u0026rsquo;s @FeignClient(path=...) + @PostMapping(...). Use @RequiredArgsConstructor with private final for constructor injection. Step 5: Define the service interface and implementation on the server # Service interface — order-service/src/main/java/.../service/\npackage com.example.orderservice.service; import com.example.commonapi.order.dto.OrderResultDTO; import com.example.commonapi.order.vo.CreateOrderVO; import com.example.commonapi.order.enums.OrderStatusEnum; import com.example.common.Result; public interface OrderService { OrderResultDTO createOrder(CreateOrderVO vo); Result\u0026lt;Void\u0026gt; updateOrderStatus(String orderId, OrderStatusEnum status); } Service implementation — order-service/src/main/java/.../service/impl/\npackage com.example.orderservice.service.impl; import com.example.commonapi.order.dto.OrderResultDTO; import com.example.commonapi.order.vo.CreateOrderVO; import com.example.orderservice.service.OrderService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @Slf4j @Service @RequiredArgsConstructor public class OrderServiceImpl implements OrderService { @Override public OrderResultDTO createOrder(CreateOrderVO vo) { // business logic... } @Override public Result\u0026lt;Void\u0026gt; updateOrderStatus(String orderId, OrderStatusEnum status) { // business logic... } } Step 6: Inject and use it in the consumer microservice # 6.1 Configure @EnableFeignClients on the application class # The consumer\u0026rsquo;s Spring Boot application class must scan com.example.commonapi to auto-wire the Feign client.\nExample: ShopServiceApplication.java\npackage com.example.shopservice; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.cloud.openfeign.EnableFeignClients; @EnableEurekaClient @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) @EnableDiscoveryClient @EnableFeignClients(basePackages = { \u0026#34;com.example.shopservice\u0026#34;, // this service\u0026#39;s own Feign clients \u0026#34;com.example.commonapi\u0026#34; // ← key! scans the API interfaces in common-api }) public class ShopServiceApplication { public static void main(String[] args) { SpringApplication.run(ShopServiceApplication.class, args); } } 6.2 Inject the API interface in a service # Constructor-inject OrderApi and call it like a local method:\npackage com.example.shopservice.service.impl; import com.example.commonapi.order.api.OrderApi; import com.example.commonapi.order.dto.OrderResultDTO; import com.example.commonapi.order.vo.CreateOrderVO; import com.example.common.Result; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor public class CheckoutServiceImpl implements CheckoutService { private final OrderApi orderApi; // ← auto-injected by Feign private void someMethod() { // build the request CreateOrderVO orderVO = CreateOrderVO.builder() .customerId(\u0026#34;12345\u0026#34;) .productId(\u0026#34;SKU-001\u0026#34;) .quantity(2) .amount(\u0026#34;1000\u0026#34;) .requestTime(System.currentTimeMillis()) .build(); // call the remote service (via Feign, looks like a local call) Result\u0026lt;OrderResultDTO\u0026gt; response = orderApi.createOrder(orderVO); // handle the response if (response.getSuccess() \u0026amp;\u0026amp; response.getData() != null) { OrderResultDTO result = response.getData(); if (result.isAccepted()) { // follow-up... } } } } 3. File overview # Layer File Path VO CreateOrderVO.java common-api/.../order/vo/ VO UpdateOrderStatusVO.java common-api/.../order/vo/ DTO OrderResultDTO.java common-api/.../order/dto/ API interface OrderApi.java common-api/.../order/api/ Controller OrderController.java order-service/.../controller/ Service OrderService.java order-service/.../service/ Service Impl OrderServiceImpl.java order-service/.../service/impl/ Consumer usage CheckoutServiceImpl.java shop-service/.../service/impl/ Application class ShopServiceApplication.java shop-service/ 4. Things to watch out for # Package convention # com.example.commonapi.{domain} ├── api/ API interfaces (@FeignClient) ├── vo/ request objects └── dto/ response objects Name {domain} after the functional area, e.g. order, product, notify. Both VO and DTO live in common-api so the server and consumer can both use them. Naming conventions # Type Rule Example API interface {Domain}Api OrderApi Request object {Action}{Domain}VO CreateOrderVO Response object {Domain}{Action}DTO OrderResultDTO Controller {Domain}Controller OrderController Service {Domain}Service OrderService Service Impl {Domain}ServiceImpl OrderServiceImpl Validation # Use javax.validation annotations (@NotBlank, @NotNull, etc.) in the VO. Add @Validated to the API method parameter to enable validation. The controller doesn\u0026rsquo;t need @Validated again — it inherits it from the interface. Serialization # Both VO and DTO must implement Serializable. Define serialVersionUID to ensure version compatibility. For fields that need precision (such as money), avoid floating point (use String or BigDecimal) to prevent precision issues. Use Long for timestamps (millisecond, 13 digits). Return values # All API methods return the Result\u0026lt;T\u0026gt; wrapper. Use Result\u0026lt;Void\u0026gt; when there\u0026rsquo;s no return data. The server builds responses with ResultUtil.success(data) / ResultUtil.error(...). @FeignClient configuration # name must match the target service\u0026rsquo;s registered name in Eureka. path corresponds to the target controller\u0026rsquo;s shared path prefix. Don\u0026rsquo;t put @RequestMapping on the API interface (only @PostMapping at the method level). Required consumer configuration # @EnableFeignClients must include \u0026quot;com.example.commonapi\u0026quot; so the API interfaces in common-api are scanned:\n@EnableFeignClients(basePackages = { \u0026#34;com.your.service\u0026#34;, // this service \u0026#34;com.example.commonapi\u0026#34; // common-api\u0026#39;s APIs }) Missing this causes a NoSuchBeanDefinitionException.\n","date":"11 February 2026","externalUrl":null,"permalink":"/posts/feign-api-shared-module/","section":"Blog","summary":"Put the Feign interface, VOs, and DTOs in one shared module: the server implements the interface as its controller, the consumer just injects and calls it — removing the legacy duplication where the interface and its client drift apart.","title":"Sharing Feign API Contracts via a Common Module in Spring Cloud","type":"posts"},{"content":"","date":"11 February 2026","externalUrl":null,"permalink":"/tags/spring-cloud/","section":"Tags","summary":"","title":"Spring Cloud","type":"tags"},{"content":"","date":"2026年2月11日","externalUrl":null,"permalink":"/zh-tw/tags/%E5%BE%AE%E6%9C%8D%E5%8B%99/","section":"Tags","summary":"","title":"微服務","type":"tags"},{"content":"Building and maintaining a Spring Cloud microservices platform behind a customer-management and transaction system.\nHighlights # Modularization \u0026amp; decoupling — refactored legacy modules to lower coupling and raise cohesion across services. Performance — optimized hot paths and improved concurrent throughput. Distributed transactions — Seata-based consistency across services, with Redis for caching/coordination. Framework upgrade — migrated services from Spring Boot 2.x to 3.1. Quality — raised test coverage from 0% → ~40% and wired the test stage into CI/CD. Operations — Grafana dashboards for live monitoring and fast incident feedback. ","date":"19 May 2025","externalUrl":null,"permalink":"/projects/backend-microservices/","section":"Projects","summary":"Spring Cloud microservices for a customer-management and transaction platform — refactoring, performance, and engineering practices.","title":"Backend Microservices Platform","type":"projects"},{"content":"","date":"19 May 2025","externalUrl":null,"permalink":"/tags/devops/","section":"Tags","summary":"","title":"DevOps","type":"tags"},{"content":"","date":"19 May 2025","externalUrl":null,"permalink":"/tags/redis/","section":"Tags","summary":"","title":"Redis","type":"tags"},{"content":"","date":"27 September 2024","externalUrl":null,"permalink":"/tags/database/","section":"Tags","summary":"","title":"Database","type":"tags"},{"content":"This post is based on the talk I gave at JCConf 2024.\nIn modern software development, version control for source code is a given — but managing changes to the database schema is often a pain point in the workflow. This post shares how I helped a client automate database migration with Liquibase and integrate it seamlessly into their project architecture.\nWhy database version control? # Before diving into tooling, let\u0026rsquo;s clear up two terms that are easily confused:\nData migration — focuses on moving data between different systems, formats, or storage technologies. Database migration — version control for a relational database, covering schema updates and rollback operations. In real projects, developers often face inconsistent versions across multiple test environments, the risk of running SQL by hand, and security requirements that demand strict separation of DDL/DML privileges. Adopting an automated version-control tool is exactly what solves these stability and security problems.\nChoosing a tool: Liquibase vs. Flyway # In the Java ecosystem, Liquibase and Flyway are the two mainstream choices. Both support the major databases and CI/CD pipelines, but their core philosophies differ:\nAspect Liquibase Flyway Script format Supports XML, YAML, JSON, and SQL — flexible across databases. Primarily plain SQL — simple and direct. Rollback Basic rollback supported in the community edition. Rollback only in the enterprise edition. Learning curve Powerful, but configuration is relatively complex. Convention-based naming — lightweight and easy to pick up. Advanced features Database diff to compare differences. Better integration with Spring properties. Given the client\u0026rsquo;s need for flexible cross-environment scripts and their emphasis on rollback, Liquibase was often the more complete choice.\nHow Liquibase works # The heart of Liquibase is the changelog and changeset:\nChangeset — defines a single database change. Changelog — organizes multiple changesets into a file (e.g. changelog-master.yaml) that serves as the entry point. Tracking table — Liquibase creates a DATABASECHANGELOG table in the database, recording each executed ID, author, and MD5SUM checksum. This ensures changes are never run twice and that their contents haven\u0026rsquo;t been tampered with. In practice: integrating Liquibase into a Spring Boot project # When adopting Liquibase in an existing project, our goal was to minimize the complexity of running it on the client side — let the service apply changes automatically on startup, with no extra CLI operations to learn.\n1. Architecture and privilege separation # To meet security requirements, we leaned on the container pattern of an init container:\nInit container — runs the update command using an account with DDL privileges. Main container — once the main service starts, it switches to an account with DQL-only privileges and runs validate to re-confirm the schema version. 2. Custom extensions # Spring Boot\u0026rsquo;s SpringLiquibase only supports update out of the box. For more complex scenarios, you can subclass or extend it to add support for commands like changelogSync and rollback. We also implemented an interceptor that emits SQL*Plus-style log output before and after each change, making changes easy to trace and audit.\nBoosting productivity: JpaBuddy # Hand-writing changesets in XML or YAML is rigorous but slow. With the JpaBuddy plugin for IntelliJ IDEA, you can generate Liquibase scripts directly from your JPA entities. This greatly reduces the developer\u0026rsquo;s burden and the chance of manual mistakes.\nClosing thoughts # Automating database version control isn\u0026rsquo;t just about installing a tool — it\u0026rsquo;s a shift in the development process itself. From requirements gathering and architecture design through to implementation, Liquibase offers a high degree of flexibility together with strong safety guarantees. I hope this experience helps you handle database versioning more gracefully in your own projects.\nResources # GitHub repo: JCConf 2024 Liquibase Demo Common commands: update, rollback, diff, changelogSync ","date":"27 September 2024","externalUrl":null,"permalink":"/posts/liquibase-database-migration/","section":"Blog","summary":"A write-up of my JCConf 2024 talk: how to automate database migration with Liquibase and integrate it seamlessly into a Spring Boot project, without sacrificing stability or security.","title":"From Adoption to Integration: Automating Database Version Control with Liquibase","type":"posts"},{"content":"","date":"27 September 2024","externalUrl":null,"permalink":"/tags/liquibase/","section":"Tags","summary":"","title":"Liquibase","type":"tags"},{"content":"","date":"27 September 2024","externalUrl":null,"permalink":"/tags/spring-boot/","section":"Tags","summary":"","title":"Spring Boot","type":"tags"},{"content":"","date":"2024年9月27日","externalUrl":null,"permalink":"/zh-tw/tags/%E8%B3%87%E6%96%99%E5%BA%AB/","section":"Tags","summary":"","title":"資料庫","type":"tags"},{"content":"The goal: reach a state of \u0026ldquo;human and machine as one\u0026rdquo; — optimize the tools and the process to the limit so that coding speed is no longer held back by physical operations.\n1. Mindset and prerequisites # Before chasing speed, you need the right rhythm and a sense of quality:\nCoding dojo and deliberate practice: sharpen your coding through repeated practice (e.g. the Tennis Kata). TDD (Test-Driven Development): follow the \u0026ldquo;test first, small steps, continuous refactoring\u0026rdquo; cycle to keep functionality correct and maintain code quality. Clean Code principles: code should be easy to understand, concise, maintainable, and clear in intent — the foundation for sustaining speed. 2. Getting the most out of your tools # \u0026ldquo;To do a good job, first sharpen your tools.\u0026rdquo; A modern IDE\u0026rsquo;s features and custom shortcuts are key to efficiency:\nUse the IDE well: lean on code completion and automated refactoring (rename, extract method, etc.) to reduce manual mistakes. Vim and IdeaVim: use Vim commands to reduce reliance on the mouse and keep your hands on the keyboard. Customize .ideavimrc: bind common IDE actions to comfortable keys — rename, extract variable/method, reformat, and so on — so each is a single keystroke. 3. Practice: the Tennis Kata and a timed challenge # The workshop used the classic Tennis Kata for practice, implementing a tennis scoring system (Love, 15, 30, 40, Deuce, Advantage).\nShow Me The Code: a timed 5-minute speed-coding challenge, testing how much test and feature code you can produce in a very short window. 4. Closing: toward flow # Real speed-coding isn\u0026rsquo;t about blindly rushing — it\u0026rsquo;s about reaching a state where \u0026ldquo;your hands keep up with your eyes.\u0026rdquo; By being fluent with your tools and in command of the TDD rhythm, you save physical effort, avoid trivial manual mistakes, and free yourself to focus on higher-level system design and reasoning.\nPractice recordings # The practice recordings from that time are collected in this YouTube playlist:\n91 Speed-Coding practice playlist (YouTube) ","date":"14 November 2023","externalUrl":null,"permalink":"/posts/speed-coding-workshop/","section":"Blog","summary":"Toward ‘human-machine as one’: using the TDD rhythm, deliberate practice, and IDE/Vim mastery so that coding speed is no longer held back by physical operations.","title":"Notes from the 91 Speed-Coding Workshop","type":"posts"},{"content":"","date":"14 November 2023","externalUrl":null,"permalink":"/tags/productivity/","section":"Tags","summary":"","title":"Productivity","type":"tags"},{"content":"","date":"14 November 2023","externalUrl":null,"permalink":"/tags/tdd/","section":"Tags","summary":"","title":"TDD","type":"tags"},{"content":"","date":"14 November 2023","externalUrl":null,"permalink":"/tags/vim/","section":"Tags","summary":"","title":"Vim","type":"tags"},{"content":"","date":"2023年11月14日","externalUrl":null,"permalink":"/zh-tw/tags/%E7%94%9F%E7%94%A2%E5%8A%9B/","section":"Tags","summary":"","title":"生產力","type":"tags"},{"content":"","date":"24 December 2022","externalUrl":null,"permalink":"/tags/notes/","section":"Tags","summary":"","title":"Notes","type":"tags"},{"content":"These are the notes I made while self-studying Spring Boot, following the Hahow course Spring Boot for Java Engineers: A Beginner\u0026rsquo;s Course. I\u0026rsquo;m putting them up as my own cheat sheet — hopefully useful to other beginners too.\nSpring IoC # IoC (Inversion of Control): hand control of your objects to an external Spring container.\nBenefits:\nLoose coupling Lifecycle management More testable DI (Dependency Injection): obtain objects from the external container.\n@Component: put on a class to make it a Spring-managed bean. @Autowired: put on a field to get a bean from the container (dependency injection). Injecting beans # @Autowired: finds a bean in the container by the field\u0026rsquo;s type. @Qualifier: when two classes implement the same interface, use @Qualifier(\u0026quot;{bean}\u0026quot;) to pick one. (The bean name is the class name with its first letter lowercased.) Creating beans # @Configuration + @Bean @Configuration: marks the class as Spring configuration. @Bean: only valid on methods inside a @Configuration class. The @Component approach above. Initializing beans # Use @PostConstruct. Implement InitializingBean\u0026rsquo;s afterPropertiesSet() method. @PostConstruct requirements: the method must be public, return void, and take no parameters.\nBean lifecycle # Create → initialize → use. If a bean depends on others at creation, Spring creates and initializes those dependencies first. Avoid circular dependencies. Configuration file # application.properties, read with @Value(\u0026quot;${key:default_value}\u0026quot;).\nSpring AOP # AOP (Aspect-Oriented Programming):\npom.xml: add spring-boot-starter-aop. @Aspect + @Component: put on a class to declare it as an aspect. Common aspect annotations: @Before / @After / @Around, on the declared methods. Common AOP uses:\nAuthorization: Spring Security Centralized exception handling: @ControllerAdvice Logging Spring MVC # @RequestMapping # Usage: on a class or a method; the URL path goes in the parentheses. Purpose: map a URL path to a method. Note: the class must have @Controller / @RestController. @Controller / @RestController # Usage: on a class only. Purpose: make the class a bean and enable @RequestMapping (a beefed-up @Component). RESTful API # Goal: reduce the communication cost between engineers. An API that follows REST style is a RESTful API. It\u0026rsquo;s not a strict spec — use whatever is most appropriate. Reading request parameters # 1. @RequestParam\nUsage: on a method parameter only. Purpose: read a URL query parameter. Options: name (or value), required (default true), defaultValue. Extra parameters in the URL are ignored by Spring Boot. @RequestMapping(\u0026#34;/test1\u0026#34;) public String test1(@RequestParam Integer id, @RequestParam(defaultValue = \u0026#34;Nick\u0026#34;) String name) { System.out.println(\u0026#34;id: \u0026#34; + id); System.out.println(\u0026#34;name: \u0026#34; + name); return \u0026#34;Hello test1\u0026#34;; } 2. @RequestBody\nUsage: on a method parameter only. Purpose: read the request body (deserialize JSON into a Java object). @RequestMapping(\u0026#34;/test2\u0026#34;) public String test2(@RequestBody Student student) { System.out.println(student); return \u0026#34;Hello test2\u0026#34;; } 3. @RequestHeader\nUsage: on a method parameter only. Purpose: read a request header. Options: name (or value — handy because headers often contain -), required, defaultValue. Common request headers:\nRequest header Meaning Common values Content-Type the format of the request body application/json (most common), application/octet-stream (file upload), multipart/form-data (image upload) Authorization authentication @RequestMapping(\u0026#34;/test3\u0026#34;) public String test3(@RequestHeader String info) { System.out.println(\u0026#34;info: \u0026#34; + info); return \u0026#34;Hello Test3\u0026#34;; } 4. @PathVariable\nUsage: on a method parameter only. Purpose: read a value from the URL path. @RequestMapping(\u0026#34;/test4/{id}\u0026#34;) public String test4(@PathVariable Integer id) { System.out.println(\u0026#34;id: \u0026#34; + id); return \u0026#34;Hello Test4\u0026#34;; } The four can be mixed.\nResponse status # Spring Boot\u0026rsquo;s default HTTP status codes: 200 when a method completes normally; 500 when an exception is thrown.\nResponseEntity\u0026lt;?\u0026gt;: use as the method return type to customize the HTTP response.\n@RequestMapping(\u0026#34;/test\u0026#34;) public ResponseEntity\u0026lt;String\u0026gt; test() { return ResponseEntity.status(HttpStatus.ACCEPTED) .body(\u0026#34;Hello World\u0026#34;); } @ControllerAdvice: on a class, makes it a bean and lets you use @ExceptionHandler inside (implemented under the hood with Spring AOP).\n@ControllerAdvice public class MyExceptionHandler { @ExceptionHandler(RuntimeException.class) public ResponseEntity\u0026lt;String\u0026gt; handle(RuntimeException exception) { return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE) .body(\u0026#34;RuntimeException:\u0026#34; + exception.getMessage()); } } Interceptor # Purpose: decide whether to let an HTTP request through to the controller method.\n@Configuration public class MyConfig implements WebMvcConfigurer { @Autowired private MyInterceptor myInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(myInterceptor).addPathPatterns(\u0026#34;/**\u0026#34;); } } @Component public class MyInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println(\u0026#34;MyInterceptor.preHandle invoked\u0026#34;); response.setStatus(401); return false; } } REST style # Use the HTTP method to express the action:\nHTTP method CRUD Description POST Create create a resource GET Read fetch a resource PUT Update update an existing resource DELETE Delete delete a resource Use the URL path to express the hierarchy (/) between resources:\nHTTP method + URL Description GET /users all users GET /users/123 the user with id 123 GET /users/123/articles all articles written by that user GET /users/123/articles/456 the user\u0026rsquo;s article with id 456 GET /users/123/videos all videos recorded by that user The response body returns JSON or XML.\nValidating request parameters: @Valid / @Validated / @NotNull… # With @RequestBody: add @Valid on the parameter so the validation annotations inside the class take effect. With @RequestParam / @RequestHeader / @PathVariable: add @Validated on the controller so the validation annotations take effect. Annotation Meaning @NotNull must not be null @NotBlank not null and not a blank string (for String) @NotEmpty not null and size \u0026gt; 0 (for List, Set, Map) @Min(value) must be \u0026gt;= value (for numbers) @Max(value) must be \u0026lt;= value (for numbers) RestTemplate # Purpose: make a REST-style HTTP request (GET, POST, PUT, DELETE), and deserialize the JSON response body into a Java object.\nSpring JDBC # NamedParameterJdbcTemplate splits into two kinds by SQL statement:\n1. update (INSERT / UPDATE / DELETE)\n@Autowired private NamedParameterJdbcTemplate namedParameterJdbcTemplate; @DeleteMapping(\u0026#34;/students/{studentId}\u0026#34;) public String delete(@PathVariable Integer studentId) { String sql = \u0026#34;DELETE FROM student WHERE id = :studentId\u0026#34;; Map\u0026lt;String, Object\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); map.put(\u0026#34;studentId\u0026#34;, studentId); namedParameterJdbcTemplate.update(sql, map); return \u0026#34;delete executed\u0026#34;; } 2. query (SELECT)\nDownsides of SELECT *: extra network traffic, no query speedup. query always returns a List: check whether it has rows before reading. RowMapper: convert each queried row into a Java object, using resultSet.getXXX(column). ResultSetExtractor: similar to RowMapper, but can combine data across rows. Controller-Service-Dao three-tier architecture # Controller: receives the HTTP request and validates parameters. Service: business logic. Dao: talks to the database. Notes:\nName classes with the Controller / Service / Dao suffix to indicate their layer. Make all three beans and inject them with @Autowired. The controller must not call the Dao directly — only the Service, which then calls the Dao. The Dao only runs SQL and accesses data; no business logic. Unit testing # Goal: automatically test that your code is correct.\nTraits: test one thing at a time (a method or an API); automatable; tests are independent of each other; results are stable and unaffected by external services.\nNotes: put tests in the test folder; name a test class after the original class plus Test; mirror the original package structure.\nJUnit 5 # Put @Test on a method to make it a unit test (only inside the test folder). The method name can be anything — use it to describe what the test verifies; this is the important part. Common asserts:\nUsage Purpose assertNull(A) A is null assertNotNull(A) A is not null assertEquals(expected, actual) equal (via equals()) assertTrue(A) A is true assertFalse(A) A is false assertThrows(exception, method) running method throws exception Other common annotations:\n@BeforeEach / @AfterEach: run once before/after each @Test. @BeforeAll / @AfterAll: run once before/after all @Tests (must be public static void). @Disabled: skip the @Test. @DisplayName: custom display name. Testing Spring Boot with JUnit 5 # Put @SpringBootTest on the test class; at runtime Spring Boot starts the container and creates all beans (all @Configuration also runs — equivalent to running the whole app).\n@Transactional:\nIn the test folder: on a method or class; rolls back all DB operations after the test, restoring the original state. In main: rolls back already-executed DB operations if an error occurs mid-run. Controller-layer testing: MockMvc # Simulate a real API call: add @AutoConfigureMockMvc on the test class and inject MockMvc.\n@Test public void getById() throws Exception { // Build the request (URL, params, headers) RequestBuilder requestBuilder = MockMvcRequestBuilders.get(\u0026#34;/students/3\u0026#34;); // mockMvc.perform fires the request // .andDo / .andExpect / .andReturn handle and verify the response // jsonPath reference: https://jsonpath.com/ mockMvc.perform(requestBuilder) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath(\u0026#34;$.id\u0026#34;, equalTo(3))); } Mocking (Mockito) # Goal: avoid wiring up a whole bean dependency graph just to test one unit. Approach: create a fake bean to replace the real one in the container. @MockBean: produce a fake bean; undefined methods return null by default. // Stub a return value Mockito.when(...).thenReturn(...); Mockito.doReturn(...).when(...); // Stub a thrown exception Mockito.when(...).thenThrow(...); // non-void method Mockito.doThrow(...).when(...); // void method You can also record call counts and order. Limits: can\u0026rsquo;t mock static methods, private methods, or final classes. @SpyBean: replace only some methods; the rest stay the real bean. Other takeaways:\n\u0026ldquo;Run test with coverage\u0026rdquo; shows what\u0026rsquo;s covered, but don\u0026rsquo;t write tests just to raise the number — think about which scenarios you\u0026rsquo;ve missed; don\u0026rsquo;t be fooled by the figure. Too many @SpyBeans suggests your code isn\u0026rsquo;t well separated. From here you can move toward Test-Driven Development (TDD). Handy IntelliJ shortcuts (Mac) # Action Shortcut Show intention actions ⌥ + Enter Go to definition ⌘ + click Find in files ⌘ + ⇧ + F Back to previous caret position ⌘ + ⌥ + ← Comment / uncomment ⌘ + / Optimize imports ⌃ + ⌥ + O Reformat code ⌘ + ⌥ + L Search Everywhere double-tap ⇧ Recent files ⌘ + E Generate ⌘ + N Column selection ⌥ + drag Also: the Endpoints tool window lists every URL mapping; double-click a tab to maximize it; \u0026ldquo;split to right\u0026rdquo; to split tabs; File → New → Module from Existing Sources → pick pom.xml to load several Spring projects at once.\nMaven lifecycle # clean: delete the target folder. compile: compile the program. test: run unit tests. package: package into a .jar (under target/). install: put the .jar into the local repository (~/.m2/repository). deploy: upload the .jar to a remote repository. ","date":"24 December 2022","externalUrl":null,"permalink":"/posts/spring-boot-notes/","section":"Blog","summary":"My notes from self-studying Spring Boot: IoC/DI, AOP, Spring MVC, RESTful APIs, request validation, Spring JDBC, the three-tier architecture, and unit testing with JUnit 5 and Mockito.","title":"Spring Boot for Beginners: My Study Notes","type":"posts"},{"content":"","date":"2022年12月24日","externalUrl":null,"permalink":"/zh-tw/tags/%E5%AD%B8%E7%BF%92%E7%AD%86%E8%A8%98/","section":"Tags","summary":"","title":"學習筆記","type":"tags"},{"content":"I\u0026rsquo;m a software engineer based in Taipei. My day-to-day is backend microservices — Spring Boot / Spring Cloud services, distributed transactions, caching, observability, and CI/CD — and I\u0026rsquo;m currently building applied machine learning / computer vision for identity verification: face liveness and deepfake detection running as a production service.\nBefore backend I spent several years in SSD firmware (FTL, garbage collection, wear-leveling, power-loss recovery), which is where I got comfortable reasoning about systems from the hardware up.\nWhat I work with # Backend — Java, Spring Boot, Spring Cloud, distributed transactions (Seata), MySQL / PostgreSQL, Redis, message queues Cloud \u0026amp; DevOps — Kubernetes, OpenShift, Docker, GitLab CI/CD, Grafana ML / CV — Python, PyTorch, ONNX, FastAPI; CNN \u0026amp; gradient-boosting models, full train → evaluate → deploy pipelines Also — C/C++ (SSD firmware), database versioning with Liquibase, TDD Talks # JCConf 2024 — Using Liquibase to assist customer data migration: from adoption to integrating it into the project. Publications # Gaze tracking for smart consumer electronics — IEEE International Conference on Consumer Electronics (ICCE), 2014. High speed gaze tracking with visible light — International Conference on System Science and Engineering (ICSSE), 2013. Education # M.S., Electrical Engineering — National Taiwan Normal University B.S., Applied Electronics Technology (EE) — National Taiwan Normal University Certifications # TOEIC 835 (2023) NVIDIA DLI — Fundamentals of Deep Learning for Computer Vision (2019) ","externalUrl":null,"permalink":"/about/","section":"SJ.Wu","summary":"I’m a software engineer based in Taipei. My day-to-day is backend microservices — Spring Boot / Spring Cloud services, distributed transactions, caching, observability, and CI/CD — and I’m currently building applied machine learning / computer vision for identity verification: face liveness and deepfake detection running as a production service.\nBefore backend I spent several years in SSD firmware (FTL, garbage collection, wear-leveling, power-loss recovery), which is where I got comfortable reasoning about systems from the hardware up.\n","title":"About","type":"page"},{"content":"","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"}]