opt-web — Hướng dẫn đọc source code
CMnnnnn) — đây là điểm mấu chốt khiến codebase này khác một dự án Spring thường.00 Dự án này là gì?
opt-web là PoC (Proof of Concept) chứng minh chiến lược migrate ~256 màn hình VB.NET của Optage sang Spring. Hiện đã hiện thực 4 màn hình mẫu (PoC scope), phần còn lại mới chỉ có khung điều hướng (nav shell).
CM06010
ウイルスバスター申込検索
Tìm kiếm đơn đăng ký virus-buster
CM05110
番ポ対象一覧
Danh sách đối tượng LNP (motel)
CM30020
顧客情報(Netflix sub)
Màn hình khách hàng — tab Netflix
CM71060
電話帳取込
Import phonebook (maintenance)
Mục tiêu thiết kế của codebase: (1) dev VB cũ của Optage navigate được new↔old trong 1 bước, (2) dev Java mới hiểu được Clean Architecture, (3) mọi hành vi reproduce 1:1 so với VB (新旧比較 / so sánh cũ–mới).
01 Bản đồ nhanh — "Cái X nằm ở đâu?"
Bảng tra cứu thần tốc. Mỗi câu hỏi onboarding thường gặp → vị trí chính xác trong source. Các mục sẽ được đào sâu ở phần 7.
| Câu hỏi | Nằm ở đâu | Ghi chú |
|---|---|---|
| Common / dùng chung | …/optweb/shared/ + config/ + view WEB-INF/views/common/ | shared = nav model + annotation; config = web/security/error; không có "util grab-bag" |
| Error lưu ở đâu | config/GlobalErrorController.java → views/common/error.jsp | 404 / 500 render trong nav shell, message M-code tiếng Nhật |
| Xử lý lỗi như thế nào | 2 cơ chế: BindingResult (validate trong controller) + GlobalErrorController (404/500) | Không có @ControllerAdvice — cố ý, PoC scope |
| Config | code: config/*.java · runtime: resources/application*.yaml | profile: local / dev / stg / prod |
| Routing | @RequestMapping trên Controller, pattern /api/<module>/cm<nnnnn> | SoT của route = .nexa/legacy-map.yaml |
| Middleware | Spring Security filter chain (SecurityConfig) + convention shell() trong controller | Không có custom interceptor/filter riêng |
| Service | <module>/application/*Service.java | @Service, gọi qua port, không biết MyBatis |
| DAO (data access) | <module>/infrastructure/persistence/ = Repository + Mapper + Mapper.xml | Stored-proc → infrastructure/procedure/ |
| SQL | resources/com/company/opt/optweb/<module>/infrastructure/persistence/*Mapper.xml | Mirror đúng package của Mapper interface |
| DB migration | resources/db/migration/oracle/V0xx__*.sql | Flyway, chạy lúc startup |
| View (UI) | src/main/webapp/WEB-INF/views/<module>/cm<nnnnn>.jsp | JSP + JSTL, layout dùng layout/*.jspf |
| Entry point | OptWebApplication.java | Spring Boot main() chuẩn |
02 Tech stack & vì sao chọn
Đọc pom.xml là biết ngay bộ công cụ. Đây là stack "enterprise Java cổ điển nhưng bản mới" — chọn JSP thay vì SPA là chủ ý: bám sát mô hình form-render của VB WinForms để migrate cho an toàn.
| Lớp | Thư viện | Vai trò trong dự án |
|---|---|---|
| Runtime | spring-boot-starter-web + tomcat-embed-jasper | Tomcat nhúng, render JSP server-side (không phải REST-JSON thuần) |
| View | jakarta.servlet.jsp.jstl (JSP/JSTL) | HTML render từ Model — tương đương "form" của VB |
| Persistence | mybatis-spring-boot-starter | SQL viết tay trong XML — map 1:1 chuỗi SQL legacy của VB |
| Stored-proc | spring-boot-starter-jdbc (JdbcTemplate) | Gọi Oracle package/proc nguyên xi, không viết lại logic |
| DB schema | flyway-core + flyway-database-oracle | Versioned migration V001…V005, có guard chống sửa bảng dùng chung |
| Driver | ojdbc11 | Oracle 23ai (legacy giữ nguyên signature stored-proc) |
| Security | spring-boot-starter-security | Filter chain — hiện permit-all + CSRF ON (PoC) |
| Validation | spring-boot-starter-validation | Bean Validation trên Form (@Valid) |
| Boilerplate | lombok | @Data cho domain/form — bớt getter/setter |
| Kiến trúc | archunit-junit5 (test) | Bắt buộc đặt tên + dependency rule bằng test — xem phần 8 |
| Chất lượng | Checkstyle · Spotless | Format + lint lúc build |
03 Cây thư mục — nhìn 1 phát hiểu cả repo
Quy tắc vàng: mỗi màn hình legacy = 1 "bounded context" = 1 package con, bên trong chia thành 4 lớp (ring) y hệt nhau. Học thuộc 1 module là đọc được cả 4.
cm… → đó là màn hình nghiệp vụ, mở 4 thư mục con theo thứ tự presentation → application → domain → infrastructure. Thấy config/ hoặc shared/ → đó là khung framework, không gắn màn hình nào.
04 Clean Architecture — 4 vòng & "luật phụ thuộc"
Cốt lõi cần khắc cốt ghi tâm: phụ thuộc luôn hướng vào trong. Lớp ngoài biết lớp trong; lớp trong không bao giờ biết lớp ngoài. domain ở tâm — không import gì của Spring/MyBatis.
(phụ thuộc vào trong)
domain + port
Lớp ④ Infrastructure infrastructure nằm ngoài cùng, nhưng nó không bị Application phụ thuộc vào. Ngược lại:
Cm06010MovirusRepository (infra) implements Cm06010SearchPort (interface ở application). Đây là Dependency Inversion — mũi tên code chạy infrastructure → application.port, KHÔNG phải application → infrastructure.
Kết quả: muốn đổi MyBatis sang JPA, chỉ thay
infrastructure/persistence, application + domain không đụng 1 dòng.
infrastructure Persistence (DAO)
Repository implements Port, ủy thác sang @Mapper interface; SQL nằm trong *Mapper.xml. procedure/ gateway gọi Oracle stored-proc qua JdbcTemplate.
Chỉ có outbound port
Controller gọi thẳng class Service (không qua "inbound port"). Chỉ chiều ra-DB mới có interface Port. Giữ đơn giản — đúng tinh thần PoC.
adapter.in.web → presentation, adapter.out.persistence → infrastructure.persistence, adapter.out.procedure → infrastructure.procedure; use-case service nằm thẳng trong application.
05 Giải phẫu 1 màn hình — CM06010 từ A→Z
Theo dấu nghiệp vụ "tìm kiếm đơn đăng ký" (検索) chạy xuyên qua cả 4 vòng. Đọc xong cái này, 3 màn hình kia bạn đọc trong 5 phút.
presentation Controller — cửa vào HTTP
Nhận request, validate, đổ Model, trả tên JSP. Lưu ý method shell() — "middleware thủ công" gắn nav + combo cho mọi screen.
@Controller @RequestMapping("/api/cm06movirus/cm06010") // route = registry legacy-map.yaml @LegacyOrigin(module="CM06MoVirus", screen="CM06010", source="FrmCM06010.vb") public class Cm06010MovirusController { private final Cm06010MovirusService service; // gọi thẳng Service (không inbound port) private final NavCatalog nav; @PostMapping("/search") // btnSearch_Click (FrmCM06010.vb L256) public String search(@Valid @ModelAttribute("form") Cm06010SearchForm form, BindingResult binding, Model model) { shell(model); // ← gắn nav + combo (convention) if (form.isAllBlank()) // Chk_Form_Pvt: tất cả trống → M99043 binding.reject("M99043", "すべての項目が入力/選択されていません。"); if (binding.hasErrors()) { … return VIEW; } // XỬ LÝ LỖI tại đây model.addAttribute("results", service.search(form.toCriteria())); return VIEW; // "cm06movirus/cm06010" → JSP } }
application Service + Port — nghiệp vụ thuần
Service chỉ điều phối, gọi qua interface Port. Không một dòng MyBatis nào lọt vào đây.
@Service public class Cm06010MovirusService { private final Cm06010SearchPort port; // ← interface, KHÔNG phải Repository cụ thể public List<MoushiInfo> search(MoushiSearchCriteria c) { return port.searchMoushiInfo(c); // ủy thác xuống infra qua port } } // application/port/Cm06010SearchPort.java — "hợp đồng" ra-DB public interface Cm06010SearchPort { List<CodeName> findMenuKbnList(); List<MoushiInfo> searchMoushiInfo(MoushiSearchCriteria criteria); }
domain Entity / Criteria — không framework
@Data @NoArgsConstructor @AllArgsConstructor // Lombok — OK trong domain @LegacyOrigin(module="CM06MoVirus", screen="CM06010", source="FrmCM06010.vb:L332") public class MoushiInfo { private String ukeNo; // 受付番号 — CN.UKENO (cột grid 0) private String custName; // お客さま名 — CASE cá nhân/pháp nhân private String kyakuNo; // 顧客番号 — cột ẩn, dùng khi double-click }
infrastructure Repository → Mapper → XML (DAO thật sự)
@Repository public class Cm06010MovirusRepository implements Cm06010SearchPort { // ← DIP private final Cm06010MovirusMapper mapper; @Override public List<MoushiInfo> searchMoushiInfo(MoushiSearchCriteria c){ return mapper.searchMoushiInfo(c); // ủy thác mỏng sang MyBatis } } @Mapper public interface Cm06010MovirusMapper { // SQL ở file .xml cùng tên List<MoushiInfo> searchMoushiInfo(MoushiSearchCriteria criteria); }
SQL nằm trong resources/com/company/opt/optweb/cm06movirus/infrastructure/persistence/Cm06010MovirusMapper.xml — viết bằng MyBatis dynamic SQL (<if>, <choose>), reproduce 1:1 chuỗi SQL mà VB nối tay. Macro VB (F_LCOMMON, F_LKYAKU…) được giải thích trong comment đầu file XML.
procedure Stored-proc gateway — gọi Oracle nguyên xi
Khi legacy gọi Oracle package/proc, tuyệt đối không viết lại logic bằng Java. Gateway gọi CALL thẳng.
@Component @LegacyOrigin(…, procedure="CCS_COMMON_PAC.SP_GET_VBUSERID") public class Cm06010MovirusProcedureGateway { public String getVbUserId() { return jdbcTemplate.execute( con -> con.prepareCall("{ call CCS_COMMON_PAC.SP_GET_VBUSERID(?) }"), cs -> { cs.registerOutParameter(1, Types.VARCHAR); cs.execute(); return cs.getString(1); }); } }
Lưu ý: chỉ cm06movirus hiện có procedure gateway. 3 màn hình kia chưa cần.
06 Vòng đời 1 request — "検索" đi qua những đâu
Từ lúc user bấm nút đến lúc bảng kết quả hiện ra. Mỗi mũi tên là 1 ranh giới vòng.
domain (MoushiInfo) đi xuyên thẳng lên controller rồi vào JSP. Không có tầng DTO trung gian — PoC giữ tối giản, JSP đọc trực tiếp bằng JSTL EL (${r.ukeNo}).07 Tra cứu chi tiết — đúng các câu hỏi onboarding
Phần này trả lời sâu từng câu trong checklist. Mỗi thẻ: ở đâu · làm gì · ví dụ thật.
🧩 Common nằm ở đâu?
Không có thư mục util/ hổ lốn. Code dùng chung chia theo vai trò:
• shared/web/NavCatalog — model điều hướng (← thay CM90Common của VB)
• shared/annotation/LegacyOrigin — annotation truy vết
• config/ — web/security/error/home
• view chung: WEB-INF/views/common/ + layout/*.jspf
🛑 Error lưu & xử lý thế nào?
2 cơ chế, không hơn:
1. Validate (trong từng controller): @Valid + BindingResult; lỗi nghiệp vụ reject bằng mã M99043… rồi return lại VIEW kèm errors.
2. Lỗi hệ thống / 404: GlobalErrorController bắt /error → render common/error.jsp trong nav shell, message tiếng Nhật M-code (404→「画面が見つかりません」, 500→「M99999…」).
❗ Cố ý không dùng @ControllerAdvice — tránh che giấu lỗi trong PoC.
⚙️ Config như thế nào?
2 nơi:
• Code config: config/*.java (@Configuration như SecurityConfig).
• Runtime config: resources/application.yaml + 4 profile local/dev/stg/prod. Ở đây khai báo Flyway, MyBatis mapper-locations, view prefix/suffix JSP, graceful shutdown.
Profile mặc định = local.
🗺️ Routing ở đâu?
Trên Controller: @RequestMapping("/api/<module>/cm<nnnnn>") + @GetMapping/@PostMapping cho action.
HomeController là "router điều hướng": /, /screen/{cm}, /jump?cm=, /health.
SoT của route = .nexa/legacy-map.yaml; route trên code bị test đối chiếu với registry này.
🧱 "Middleware" thế nào?
Java/Spring không có "middleware" kiểu Express. Ánh xạ trung thực:
• Cross-cutting thật = Spring Security filter chain (SecurityConfig): hiện permitAll() + CSRF ON; mọi POST đổi-trạng-thái cần token.
• Per-request setup = convention shell(model) trong từng controller, gắn nav/activeCm/located + combo.
Không có custom Interceptor/Filter riêng — sự vắng mặt cũng là thông tin.
🧠 Service thế nào?
application/*Service.java (@Service). Nhận domain criteria, điều phối nghiệp vụ, gọi DB qua port. Màn hình god-screen (CM30020) có thể tách thành nhiều Service. Service không import MyBatis/JDBC.
💾 DAO thế nào?
infrastructure/persistence/:
• *Repository (@Repository) — implements Port, ủy thác mỏng.
• *Mapper (@Mapper) — interface MyBatis.
• *Mapper.xml (trong resources/ mirror package) — SQL thật.
Stored-proc → tách riêng infrastructure/procedure/*Gateway (JdbcTemplate).
📋 Form & Validation
presentation/*Form.java giữ input thô của màn hình (vd Cm06010SearchForm, Cm71060ImportForm), gắn Bean Validation. Controller gọi form.toCriteria() để đổi sang domain criteria sạch trước khi đưa xuống Service.
08 Quy định bắt buộc — đây là phần khác biệt nhất
Codebase này có một "luật neo về legacy" mà CI sẽ fail build nếu vi phạm. Hiểu nó trước khi commit dòng đầu tiên.
① @LegacyOrigin — mỗi class neo về 1 màn hình VB
Mọi class trong package bounded-context (controller, service, entity, mapper/repository, gateway) bắt buộc có @LegacyOrigin(module, screen, source). Đây là sợi dây để dev Optage trace new↔old trong 1 bước.
@LegacyOrigin(module="CM06MoVirus", screen="CM06010", source="FrmCM06010.vb")
OptWebApplication, config.*, shared.*, layout JSP — không có gốc legacy nên không cần @LegacyOrigin (khai báo ở legacy-map.yaml :: frameworkOnly).② SoT = .nexa/legacy-map.yaml
File registry này map context ↔ legacyModule ↔ screen ↔ route ↔ label. Nó là chân lý; giá trị trong @LegacyOrigin và route được kiểm ngược lại registry, không phải chiều ngược.
③ Bảng đặt tên (neo = Anchored, idiom = Idiomatic)
| Thành phần | Quy ước | Ví dụ |
|---|---|---|
| Package context | mirror tên module VB viết thường | CM06MoVirus → cm06movirus |
| Controller | Cm<nnnnn><Ctx>Controller | Cm06010MovirusController |
| Service | Cm<nnnnn>…Service | Cm06010MovirusService |
| Route | /api/<lower(module)>/cm<nnnnn>[/action] | /api/cm06movirus/cm06010/search |
| JSP | views/<ctx>/cm<nnnnn>.jsp | cm06movirus/cm06010.jsp |
| Port (interface) | idiom — đặt theo nghiệp vụ | Cm06010SearchPort |
④ Gác cổng tự động (sẽ fail CI nếu sai)
🧪 ArchUnit
LegacyOriginMappingTest.java — đọc bytecode, ép controller phải đặt tên anchored + có @LegacyOrigin hợp lệ. (Vì vậy annotation để RetentionPolicy.CLASS, không phải SOURCE.)
🐍 legacy-map-check.py
Chạy python3 coding-rules/legacy-map-check.py (hoặc /legacy-map) → 0 hard-fail. Đối chiếu mọi @LegacyOrigin với registry; bắt orphan/gap.
application chỉ được biết domain + port — không import MyBatis. Đây là lý do Port tồn tại.09 Quy trình thêm 1 màn hình mới
Khi được giao port màn hình CMxxxxx, đi đúng 5 bước này (rút từ legacy-origin-mapping.md §6).
- Đăng ký vào registryĐảm bảo screen có trong
.nexa/legacy-map.yamlvớiscope: poc. - Tạo package context + 4 ringTạo
<ctx>/{presentation,application,application/port,domain,infrastructure/persistence}. Procedure gateway chỉ tạo khi có stored-proc. - Viết ControllerĐặt tên
Cm<nnnnn><Ctx>Controller, gắn@LegacyOrigin+@RequestMapping= route trong registry. Đừng quênshell(model). - Domain + DAOEntity =
Pascal(Table)có@LegacyOrigin; Port ởapplication/port; Repository implements Port; Mapper.xml co-located trongresources/…/persistence/. - JSP + chạy validatorView ở
WEB-INF/views/<ctx>/cm<nnnnn>.jsp. Chạypython3 coding-rules/legacy-map-check.py→ phải 0 hard-fail trước khi commit.
#{} bind param); logic stored-proc gọi chứ không viết lại. Test kiểu 新旧比較: cùng input → output Java == output VB.10 Gotchas khi migrate (đọc kẻo mất ngày)
Tổng hợp từ coding-rules/CODING-TIPS.md — những cái bẫy đã có người dẫm.
🧮 FarPoint Spread grid
Control thương mại của VB (lưới ở CM30020 sub-tab, CM06010), không có tương đương 1:1 trong Java. Phải chốt thư viện grid sớm — đừng giả định.
🔤 Encoding CP932
Source VB là CP932. Mọi string tiếng Nhật (message §5) phải round-trip đúng — coi chừng mojibake.
📞 Stored-proc 1:1
CWS_SERVICE_PAC.SF_TEL*, SP_GET_VBUSERID… Oracle 23ai giữ nguyên signature. Không re-implement logic bằng Java — gọi proc.
🔗 CM90Common
Lib nội bộ Optage, 65 import xuyên màn hình PoC. Trách nhiệm chưa rõ — confirm trước khi code. (Trong dự án phần này map vào shared/.)
🗄️ 60 bảng dùng chung kakin
Đổi schema Oracle 23ai có thể ảnh hưởng batch kakin ngoài PoC. Không sửa schema các bảng protected (có migration-guard gác).
🎯 Scope CM30020 đã khóa
Chỉ làm parent UI + Netflix sub — KHÔNG đụng cả họ 106-file. Cẩn thận message table §6 lẫn source-noise: re-extract từ FrmCM30020.vb.