Tới nội dung chính
caltalys.dev
Cái List / Map

application.properties của tôi bị ignore hoàn toàn

Environment không giữ một file mà giữ một List các nguồn. Nguồn đầu tiên có key thì thắng, vòng for dừng, không ai phía sau được hỏi.

Đồ hình trừu tượng của tập 5.2 trong Quyển Một
Caltalys 8 phút đọc

Thằng em nhắn, và lần này nó tự phá án được chín phần - chỉ thiếu cái tên cho thủ phạm:

thang em Anh, em đổi spring.datasource.url trong application.properties, commit, deploy. App vẫn kết nối vào DB cũ. Em tưởng file không được đọc - nhưng không: em đổi thử một property khác trong cùng file thì ăn ngay. Tức là file có được đọc, nhưng riêng cái key này thì lời của em không có giá trị. Nghe quen không anh? Em có cảm giác… có ai đó nói trước em, và người ta chỉ nghe người nói trước.

Cảm giác của nó chuẩn từng chữ. Và nếu bạn đi cùng series này từ đầu, bạn biết cái hình dạng nó vừa mô tả - vì bạn đã gặp nó ở một hiện trường khác hẳn: bài @Controller, cái List<ArgumentResolver>, luật ai giơ tay trước thắng. Hôm nay, luật đó quay lại lần cuối - không phải trên đường đi của request, mà trên đường đi của một dòng config.

Câu hỏi nền: khi bạn viết environment.getProperty("spring.datasource.url") - hay khi Boot tự làm điều đó lúc dựng DataSource - nó đọc từ đâu? Trực giác nói: “từ file properties”. Sai ở chữ file, số ít. Cái Environment của Spring không giữ một file - nó giữ một cái List tên là MutablePropertySources: một danh sách các nguồn, xếp theo thứ tự ưu tiên cố định. getProperty chạy một vòng for dọc List, hỏi từng nguồn “mày có key này không?” - nguồn đầu tiên có, thắng. Vòng for dừng. Không ai phía sau được hỏi.

Lại là một cái List. Lại first-match-wins. Bạn đếm được đây là lần thứ mấy rồi.

Và application.properties của bạn đứng ở đâu trong hàng? Gần cuối. Trước nó - tính sơ những gương mặt hay gây án nhất, từ trên xuống: tham số dòng lệnh (--spring.datasource.url=...), rồi SPRING_APPLICATION_JSON, rồi Java system properties (-D...), rồi biến môi trường của hệ điều hành. Rồi mới tới nhóm file config - và trong nội bộ nhóm này, thứ tự không giống trực giác: vị trí xét trước, profile xét sau. Ngoài jar thắng trong jar đã, rồi mới tới có-profile thắng không-profile. Xếp từ mạnh xuống yếu: application-prod.properties đặt cạnh chỗ chạy (thư mục ./config/), rồi application.properties cũng đặt cạnh chỗ chạy, rồi application-prod.properties đóng sẵn trong jar - và cuối bảng, cái application.properties trong src/main/resources mà bạn vừa sửa. Đọc lại hai dòng giữa một lần nữa, vì đây là chỗ trực giác sai nhiều nhất: một file application.properties trần trụi nằm ngoài jar vẫn đè được application-prod.properties đóng gói sẵn bên trong. File của bạn không bị ignore. Nó chỉ là ứng viên đứng cuối hàng, và hôm nay có người đứng trước.

Người đứng trước của thằng em, mình đoán trúng ngay từ tin nhắn - vì vụ này kinh điển tới mức thành nghi phạm số một của mọi ca “property không ăn”: nó mở docker-compose.yml trên server ra, và kia:

environment:
  SPRING_DATASOURCE_URL: jdbc:postgresql://old-db:5432/app

Một biến môi trường, ai đó đặt từ tám tháng trước “để test tạm”, nằm đó từ bấy tới nay. Và chú ý cái tên: SPRING_DATASOURCE_URL - viết hoa, gạch dưới - trông chẳng giống spring.datasource.url chút nào. Đó là cú tàng hình thứ hai: relaxed binding. Boot biết biến môi trường không cho phép dấu chấm, nên nó tự dịch: hoa thành thường, gạch dưới thành chấm. Tiện - và cũng nghĩa là bạn grep "spring.datasource.url" khắp server sẽ không ra gì, vì kẻ ghi đè mang một cái tên khác hệ chữ viết. Thằng em đã grep. Ba lần.

Không có bug ở đâu cả - điệp khúc của series, lần áp chót. Mỗi nguồn đều trung thực. Cái List đúng thứ tự thiết kế - thứ tự ấy còn rất hợp lý: nguồn càng gần lúc chạy, càng thắng (dòng lệnh của người vận hành phải đè được file đóng trong jar từ ba tuần trước - không thì làm sao vận hành). Vòng for làm đúng việc. Chỉ là bạn viết lời khai vào một ứng viên cuối hàng, trong khi đầu hàng có người đã khai từ tám tháng trước.

Còn cặp bài trùng hôm nay là phiên bản nội bộ gia đình của cùng vụ án - không cần ai bên ngoài: chính các file properties ghi đè lẫn nhau. Thằng em từng dính mà không biết là dính: nó sửa server.port trong application.properties, chạy trên máy dev ăn ngay; deploy môi trường staging - không ăn. Vì staging chạy với --spring.profiles.active=staging, và trong repo có một application-staging.properties ai đó tạo từ lâu, cũng khai server.port. File profile-specific đứng trước file thường trong List. Cùng một repo, hai file, và file cụ thể hơn thắng - hợp lý, cho tới cái ngày bạn quên file kia tồn tại. Biến thể độc hơn: một application.properties nằm trong thư mục config/ cạnh chỗ chạy app trên server - ngoài jar đè trong jar - file mà không một lần grep repo nào tìm thấy, vì nó không nằm trong repo.

Ba thủ phạm, một hình dạng:

giá trị bạn thấy chưa bao giờ là “nội dung của file” - nó là kết quả của một vòng for trên một cái List mà bạn chỉ nhìn thấy đúng một phần tử.

Vậy debug kiểu gì cho khỏi đoán mò? Đừng đọc file - hỏi thẳng cái List. Bật actuator, mở /actuator/env, gõ tên property: nó trả về không chỉ giá trị đang thắng, mà toàn bộ danh sách nguồn theo đúng thứ tự, nguồn nào khai gì, và nguồn nào đang đứng đầu. Toàn bộ bài này nằm trong một trang JSON. Thằng em mở ra, thấy systemEnvironment chễm chệ trên cái nguồn mang tên file application.yml của nó, và vụ án khép trong ba mươi giây - thay vì ba ngày grep.

Nhưng trước khi bạn mở trang đó ra và tin ngay dòng đầu tiên, có một cái bẫy nằm sẵn trong chính nó - và nó đúng hình dạng của cả bài hôm nay, nên đây là chỗ của nó chứ không phải một phụ lục. Đo trên một app servlet chạy trần, không tham số dòng lệnh, danh sách ra thế này:

1. server.ports
2. configurationProperties
3. servletConfigInitParams
4. servletContextInitParams
5. systemProperties
6. systemEnvironment
7. random
8. Config resource 'class path resource [application.yml]' via location 'optional:classpath:/'

Cái đứng thứ hai, configurationProperties, không phải một nguồn. Nó là cái vỏ Boot đặt lên để nhìn xuyên cả danh sách, nên nó chứa mọi key và luôn nằm gần đầu. Ai viết một vòng for kiểu “tìm nguồn đầu tiên có khai key này” sẽ luôn nhận về đúng cái tên đó cho mọi property mình hỏi, và tưởng vừa bắt được thủ phạm - trong khi nó chỉ là tấm gương phản chiếu kết quả của trận đấu, không phải một đấu thủ. Phải bỏ qua nó tường minh thì mới đọc ra tên thật. Một cái tên trông y như một nguồn mà không phải nguồn: bài này có ba thủ phạm, và cái thứ tư thì nấp ngay trong dụng cụ đi bắt ba thằng kia.

Nguyên lý, thứ sống lâu hơn Spring Boot: khi một giá trị có thể đến từ nhiều nguồn, thứ bạn đọc được không phải là “cấu hình” - là kết quả của một trận phân định, và muốn hiểu kết quả thì phải biết luật đấu, không phải chỉ biết một đấu thủ. CSS có specificity. DNS có thứ tự resolver. Shell có PATH. Git config có system/global/local. Ở đâu có lớp phủ lớp, ở đó câu hỏi “giá trị này ở đâu ra” chỉ trả lời được bằng câu hỏi “ai đứng trước ai”.

Thằng em, sau khi xóa cái biến môi trường tám tháng tuổi, nhắn:

thang em Buồn cười anh nhỉ. Bài học đầu tiên anh dạy em là cái List chưa đầy - @Value bằng 0 vì người phụ trách chưa vào danh sách. Bài gần cuối hóa ra vẫn là cái List, nhưng ngược: List đầy quá - em khai một chỗ mà không biết còn sáu chỗ khác cũng đang khai, và người ta chỉ nghe người đứng trước. Từ đầu tới giờ toàn là chuyện đứng đâu trong hàng.

Đúng rồi đó. Series này chưa bao giờ dạy Spring - nó dạy xếp hàng. Còn hai bài nữa. 😃

Tài nguyên tập 19

Cheat-sheet tập 19 Toàn bộ cơ chế trên một trang - in ra hoặc lưu về máy được. Source demo - tập 5.2JDK 17. mvn spring-boot:run → Episode52Runner in khối [EP 5.2]: liệt kê mọi nguồn trong Environment theo đúng thứ tự ưu tiên, rồi chen -Dapp.timeout và APP_TIMEOUT vào xem ai thắng.

Vị trí trong Lớp Trung Gian Vô Hình

Bài này trả lời

  • Cấu hình lấy giá trị từ một nguồn khác nguồn bạn nghĩ
Bản in
Chia sẻ: Facebook X LinkedIn Email
Nhận bài mới qua email
Powered by follow.it