Skip to content

15.2.2 Xem chi tiết cấu hình

Bên cạnh việc nhận thông tin tổng quát về một ứng dụng, việc hiểu được ứng dụng được cấu hình như thế nào cũng rất hữu ích. Những bean nào đang có trong application context? Điều kiện autoconfiguration nào đã được thỏa mãn hoặc bị từ chối? Những thuộc tính môi trường nào đang có sẵn cho ứng dụng? Các yêu cầu HTTP được ánh xạ tới các controller như thế nào? Mức độ logging hiện tại của các package hoặc class là gì?

Những câu hỏi này được trả lời bởi các endpoint của Actuator như: /beans, /conditions, /env, /configprops, /mappings, và /loggers. Và trong một số trường hợp, chẳng hạn như /env/loggers, bạn thậm chí còn có thể điều chỉnh cấu hình của ứng dụng đang chạy ngay lập tức. Chúng ta sẽ xem từng endpoint cung cấp cái nhìn sâu như thế nào về cấu hình của một ứng dụng đang chạy, bắt đầu với endpoint /beans.

NHẬN BÁO CÁO KẾT NỐI CÁC BEAN

Endpoint quan trọng nhất để khám phá Spring application context là /beans. Endpoint này trả về một tài liệu JSON mô tả từng bean trong application context, kiểu Java của nó, và các bean khác mà nó được inject vào.

Một phản hồi đầy đủ từ một yêu cầu GET đến /beans có thể dễ dàng lấp đầy cả chương sách này. Thay vì xem toàn bộ phản hồi, hãy xem xét đoạn trích sau, tập trung vào một bean duy nhất:

text
{
    "contexts": {
        "application-1": {
            "beans": {
   ...
                "ingredientsController": {
                    "aliases": [],
                    "scope": "singleton",
                    "type": "tacos.ingredients.IngredientsController",
                    "resource": "file [/Users/habuma/Documents/Workspaces/
                    TacoCloud/ingredient-service/target/classes/tacos/ingredients/
                    IngredientsController.class]",
                    "dependencies": [
                        "ingredientRepository"
                    ]
                },
        ...
            },
            "parentId": null
        }
    }
}

Ở gốc của phản hồi là phần tử contexts, trong đó bao gồm một phần tử con cho mỗi Spring application context trong ứng dụng. Trong mỗi application context là một phần tử beans chứa chi tiết về tất cả các bean trong context đó.

Trong ví dụ trên, bean được hiển thị là bean có tên ingredientsController. Bạn có thể thấy rằng nó không có alias, được scope là singleton, và có kiểu là tacos.ingredients.IngredientsController. Hơn nữa, thuộc tính resource cung cấp đường dẫn đến file class định nghĩa bean đó. Và thuộc tính dependencies liệt kê tất cả các bean khác được inject vào bean hiện tại. Trong trường hợp này, bean ingredientsController được inject với một bean có tên là ingredientRepository.

GIẢI THÍCH AUTOCONFIGURATION

Như bạn đã thấy, autoconfiguration là một trong những tính năng mạnh mẽ nhất mà Spring Boot cung cấp. Tuy nhiên, đôi khi bạn có thể thắc mắc tại sao một thành phần nào đó lại được autoconfigure. Hoặc bạn mong đợi một thành phần sẽ được autoconfigure, nhưng lại không thấy. Trong những trường hợp như vậy, bạn có thể gửi một yêu cầu GET đến /conditions để nhận được giải thích về những gì đã xảy ra trong quá trình autoconfiguration.

Báo cáo autoconfiguration trả về từ /conditions được chia làm ba phần: positive matches (cấu hình có điều kiện đã được thỏa mãn), negative matches (cấu hình có điều kiện không được thỏa mãn), và unconditional classes (các lớp được cấu hình không điều kiện). Đoạn mã sau từ phản hồi của yêu cầu đến /conditions cho thấy ví dụ cho từng phần:

json
{
  "contexts": {
    "application-1": {
      "positiveMatches": {
      ...
        "MongoDataAutoConfiguration#mongoTemplate": [
          {
            "condition": "OnBeanCondition",
            "message": "@ConditionalOnMissingBean (types
            :org.springframework.data.mongodb.core.MongoTemplate;
            SearchStrategy: all) did not find any beans"
          }
        ],
      ...
    },
    "negativeMatches": {
      ...
      "DispatcherServletAutoConfiguration": {
        "notMatched": [
          {
            "condition": "OnClassCondition",
            "message": "@ConditionalOnClass did not find required
            class 'org.springframework.web.servlet.
            DispatcherServlet'"
          }
        ],
        "matched": []
      },
      ...
    },
    "unconditionalClasses": [
      ...
      "org.springframework.boot.autoconfigure.context.
      ConfigurationPropertiesAutoConfiguration",
      ...
    ]
  }
  }
}

Trong phần positiveMatches, bạn thấy rằng một bean MongoTemplate đã được autoconfigure vì chưa có bean nào cùng loại tồn tại. Cấu hình autoconfigure này có chứa annotation @ConditionalOnMissingBean, cho phép tạo bean mới nếu chưa có sẵn bean tương ứng. Trong trường hợp này, không có bean nào kiểu MongoTemplate được tìm thấy, nên autoconfiguration đã tự động tạo ra một bean như vậy.

Trong phần negativeMatches, Spring Boot autoconfiguration đã cân nhắc cấu hình một DispatcherServlet. Nhưng annotation có điều kiện @ConditionalOnClass đã bị từ chối vì không tìm thấy class DispatcherServlet.

Cuối cùng, một bean ConfigurationPropertiesAutoConfiguration đã được cấu hình không điều kiện, như được thể hiện trong phần unconditionalClasses. Các thuộc tính cấu hình (configuration properties) là nền tảng cho cách Spring Boot hoạt động, nên các cấu hình liên quan đến chúng sẽ luôn được autoconfigure mà không cần điều kiện.

KIỂM TRA MÔI TRƯỜNG VÀ THUỘC TÍNH CẤU HÌNH

Ngoài việc biết cách các bean được liên kết với nhau, bạn cũng có thể muốn biết các thuộc tính môi trường nào đang có sẵn và các thuộc tính cấu hình nào đã được inject vào các bean.

Khi bạn gửi một yêu cầu GET đến endpoint /env, bạn sẽ nhận được một phản hồi khá dài, bao gồm các thuộc tính từ tất cả các nguồn thuộc tính đang được sử dụng trong ứng dụng Spring. Điều này bao gồm các thuộc tính từ biến môi trường, thuộc tính hệ thống JVM, các file application.propertiesapplication.yml, thậm chí cả từ Spring Cloud Config Server (nếu ứng dụng là client của Config Server).

Danh sách sau đây là một ví dụ rút gọn đáng kể về phản hồi bạn có thể nhận được từ endpoint /env, để giúp bạn hình dung về loại thông tin mà nó cung cấp.

Listing 15.1 Kết quả từ endpoint /env

bash
$ curl localhost:8081/actuator/env
{
  "activeProfiles": [
    "development"
  ],
  "propertySources": [
  ...
  {
    "name": "systemEnvironment",
    "properties": {
      "PATH": {
        "value": "/usr/bin:/bin:/usr/sbin:/sbin",
        "origin": "System Environment Property \"PATH\""
      },
      ...
      "HOME": {
        "value": "/Users/habuma",
        "origin": "System Environment Property \"HOME\""
      }
    }
  },
  {
    "name": "applicationConfig: [classpath:/application.yml]",
    "properties": {
      "spring.application.name": {
        "value": "ingredient-service",
        "origin": "class path resource [application.yml]:3:11"
      },
      "server.port": {
        "value": 8081,
        "origin": "class path resource [application.yml]:9:9"
      },
      ...
    }
  },
  ...
  ]
}

Mặc dù phản hồi đầy đủ từ /env cung cấp nhiều thông tin hơn, nhưng những gì được hiển thị trong Listing 15.1 đã chứa một số thành phần đáng chú ý. Đầu tiên, hãy chú ý rằng gần đầu phản hồi là trường activeProfiles. Trong trường hợp này, nó chỉ ra rằng profile development đang được kích hoạt. Nếu có các profile khác đang hoạt động, chúng cũng sẽ được liệt kê tại đây.

Tiếp theo, trường propertySources là một mảng chứa một mục cho mỗi nguồn thuộc tính trong môi trường của ứng dụng Spring. Trong Listing 15.1, chỉ có systemEnvironment và một nguồn thuộc tính applicationConfig tham chiếu đến file application.yml được hiển thị.

Trong mỗi nguồn thuộc tính là danh sách tất cả các thuộc tính mà nguồn đó cung cấp, đi kèm với giá trị tương ứng. Trong trường hợp của nguồn application.yml, trường origin của mỗi thuộc tính cho biết chính xác nơi thuộc tính đó được thiết lập, bao gồm dòng và cột trong file application.yml.

Endpoint /env cũng có thể được sử dụng để truy vấn một thuộc tính cụ thể khi tên thuộc tính được đưa vào như phần tử thứ hai của đường dẫn. Ví dụ, để kiểm tra thuộc tính server.port, bạn gửi một yêu cầu GET đến /env/server.port, như sau:

bash
$ curl localhost:8081/actuator/env/server.port
{
  "property": {
    "source": "systemEnvironment", "value": "8081"
  },
  "activeProfiles": [ "development" ],
  "propertySources": [
    { "name": "server.ports" },
    { "name": "mongo.ports" },
    { "name": "systemProperties" },
    { 
      "name": "systemEnvironment",
      "property": {
        "value": "8081",
        "origin": "System Environment Property \"SERVER_PORT\""
      }
    },
    { "name": "random" },
    {
      "name": "applicationConfig: [classpath:/application.yml]",
      "property": {
        "value": 0,
        "origin": "class path resource [application.yml]:9:9"
      }
    },
    { "name": "springCloudClientHostInfo" },
    { "name": "refresh" },
    { "name": "defaultProperties" },
    { "name": "Management Server" }
  ]
}

Như bạn có thể thấy, tất cả các nguồn thuộc tính vẫn được hiển thị, nhưng chỉ những nguồn nào có thiết lập giá trị cho thuộc tính được chỉ định mới chứa thông tin bổ sung. Trong trường hợp này, cả nguồn thuộc tính systemEnvironment và nguồn thuộc tính từ application.yml đều có giá trị cho thuộc tính server.port. Vì nguồn systemEnvironment có độ ưu tiên cao hơn so với bất kỳ nguồn nào bên dưới, nên giá trị 8080 của nó là giá trị chiến thắng. Giá trị này được phản ánh gần đầu phản hồi trong trường property.

Endpoint /env không chỉ dùng để đọc giá trị thuộc tính. Bằng cách gửi một yêu cầu POST đến endpoint /env, cùng với một tài liệu JSON chứa các trường namevalue, bạn cũng có thể thiết lập các thuộc tính cho ứng dụng đang chạy. Ví dụ, để thiết lập một thuộc tính có tên tacocloud.discount.code với giá trị TACOS1234, bạn có thể dùng lệnh curl sau để gửi yêu cầu POST từ dòng lệnh:

bash
$ curl localhost:8081/actuator/env \
  -d'{"name":"tacocloud.discount.code","value":"TACOS1234"}' \
  -H "Content-type: application/json"
{"tacocloud.discount.code":"TACOS1234"}

Sau khi gửi thuộc tính, thuộc tính vừa được thiết lập và giá trị của nó sẽ được trả về trong phản hồi. Nếu sau đó bạn không còn cần thuộc tính đó nữa, bạn có thể gửi một yêu cầu DELETE đến endpoint /env như sau để xóa tất cả các thuộc tính đã được tạo thông qua endpoint này:

bash
$ curl localhost:8081/actuator/env -X DELETE
{"tacocloud.discount.code":"TACOS1234"}

Tuy việc thiết lập thuộc tính thông qua API của Actuator là rất hữu ích, nhưng điều quan trọng cần lưu ý là các thuộc tính được thiết lập bằng POST đến /env chỉ áp dụng cho instance ứng dụng nhận yêu cầu, có tính tạm thời, và sẽ bị mất khi ứng dụng được khởi động lại.

ĐIỀU HƯỚNG CÁC ÁNH XẠ YÊU CẦU HTTP

Mặc dù mô hình lập trình của Spring MVC (và Spring WebFlux) giúp xử lý các yêu cầu HTTP rất dễ dàng bằng cách đơn giản là sử dụng các annotation ánh xạ trên phương thức, đôi khi bạn vẫn sẽ gặp khó khăn khi muốn có cái nhìn tổng thể về tất cả các loại yêu cầu HTTP mà ứng dụng có thể xử lý và thành phần nào xử lý chúng.

Endpoint /mappings của Actuator cung cấp một cái nhìn tập trung về mọi HTTP request handler trong ứng dụng, dù đó là từ một controller của Spring MVC hay chính các endpoint của Actuator. Để có danh sách đầy đủ tất cả các endpoint trong một ứng dụng Spring Boot, hãy gửi yêu cầu GET đến endpoint /mappings, và bạn có thể nhận được một phản hồi giống như ví dụ rút gọn dưới đây.

Listing 15.2 Các ánh xạ HTTP như được hiển thị bởi endpoint /mappings

bash
$ curl localhost:8081/actuator/mappings | jq
{
  "contexts": {
    "application-1": {
      "mappings": {
        "dispatcherHandlers": {
          "webHandler": [
            ...
            {
              "predicate": "{[/ingredients],methods=[GET]}",
              "handler": "public
              reactor.core.publisher.Flux<tacos.ingredients.Ingredient>
              tacos.ingredients.IngredientsController.allIngredients()",
              "details": {
                "handlerMethod": {
                  "className": "tacos.ingredients.IngredientsController",
                  "name": "allIngredients",
                  "descriptor": "()Lreactor/core/publisher/Flux;"
                },
                "handlerFunction": null,
                "requestMappingConditions": {
                  "consumes": [],
                  "headers": [],
                  "methods": [
                    "GET"
                  ],
                  "params": [],
                  "patterns": [
                    "/ingredients"
                  ],
                  "produces": []
                }
              }
            },
            ...
            ]
          }
        },
        "parentId": "application-1"
      },
      "bootstrap": {
        "mappings": {
          "dispatcherHandlers": {}
        },
        "parentId": null
      }
    }
}

Ở đây, phản hồi từ lệnh curl được truyền qua tiện ích jq https://stedolan.github.io/jq/, công cụ giúp định dạng JSON trả về theo cách dễ đọc hơn. Vì lý do ngắn gọn, phản hồi đã được rút gọn chỉ để hiển thị một request handler duy nhất. Cụ thể, phản hồi cho thấy các yêu cầu GET đến /ingredients sẽ được xử lý bởi phương thức allIngredients() của IngredientsController.

QUẢN LÝ MỨC ĐỘ GHI LOG

Ghi log là một tính năng quan trọng của bất kỳ ứng dụng nào. Log có thể phục vụ cho mục đích kiểm toán cũng như là một cách đơn giản để debug.

Việc thiết lập mức độ ghi log là một hành động cần sự cân bằng. Nếu bạn thiết lập mức độ log quá chi tiết, log có thể trở nên nhiễu và khó tìm thông tin hữu ích. Ngược lại, nếu bạn thiết lập mức độ log quá ít, log có thể không cung cấp đủ thông tin để hiểu được ứng dụng đang làm gì.

Mức độ log thường được áp dụng theo từng package. Nếu bạn từng tự hỏi mức độ log nào đang được thiết lập trong ứng dụng Spring Boot của mình, bạn có thể gửi yêu cầu GET đến endpoint /loggers. Mã JSON dưới đây là một đoạn trích từ phản hồi của /loggers:

bash
{
  "levels": [ "OFF", "ERROR", "WARN", "INFO", "DEBUG", "TRACE" ],
  "loggers": {
    "ROOT": {
      "configuredLevel": "INFO", "effectiveLevel": "INFO"
    },
    ...
    "org.springframework.web": {
      "configuredLevel": null, "effectiveLevel": "INFO"
    },
    ...
    "tacos": {
      "configuredLevel": null, "effectiveLevel": "INFO"
    },
    "tacos.ingredients": {
      "configuredLevel": null, "effectiveLevel": "INFO"
    },
    "tacos.ingredients.IngredientServiceApplication": {
      "configuredLevel": null, "effectiveLevel": "INFO"
    }
  }
}

Phản hồi bắt đầu với danh sách tất cả các mức độ ghi log hợp lệ. Sau đó, phần tử loggers liệt kê chi tiết mức log cho từng package trong ứng dụng. Thuộc tính configuredLevel hiển thị mức log được thiết lập một cách rõ ràng (hoặc null nếu chưa được cấu hình). Thuộc tính effectiveLevel hiển thị mức độ log hiệu lực, có thể được kế thừa từ package cha hoặc từ root logger.

Mặc dù đoạn trích chỉ hiển thị mức log cho root logger và bốn package, phản hồi đầy đủ sẽ bao gồm mức log cho mọi package trong ứng dụng, kể cả các thư viện được sử dụng. Nếu bạn chỉ muốn tập trung vào một package cụ thể, bạn có thể chỉ định tên package như một thành phần bổ sung trong đường dẫn yêu cầu.

Ví dụ, nếu bạn chỉ muốn biết mức log được thiết lập cho package tacocloud.ingredients, bạn có thể gửi yêu cầu đến /loggers/tacos.ingredients như sau:

bash
{
  "configuredLevel": null,
  "effectiveLevel": "INFO"
}

Ngoài việc trả về mức log cho các package ứng dụng, endpoint /loggers cũng cho phép bạn thay đổi mức log đã cấu hình bằng cách gửi một yêu cầu POST. Ví dụ, giả sử bạn muốn thiết lập mức log cho package tacocloud.ingredientsDEBUG. Lệnh curl sau sẽ thực hiện điều đó:

bash
$ curl localhost:8081/actuator/loggers/tacos/ingredients \
  -d'{"configuredLevel":"DEBUG"}' \
  -H"Content-type: application/json"

Bây giờ khi mức log đã được thay đổi, bạn có thể gửi yêu cầu GET đến /loggers/tacos/ingredients như sau để kiểm tra lại:

bash
{
  "configuredLevel": "DEBUG",
  "effectiveLevel": "DEBUG"
}

Lưu ý rằng trước đây configuredLevel là null, thì giờ đây nó đã là DEBUG. Sự thay đổi này cũng được phản ánh trong effectiveLevel. Nhưng điều quan trọng nhất là nếu có bất kỳ đoạn mã nào trong package đó ghi log ở mức debug, thông tin đó giờ sẽ được đưa vào file log.

Released under the MIT License.