雖然Actuator提供了很多運行中Spring Boot應用程序的內部工作細節,但難免和你的需求有所偏差。也許你並不需要它提供的所有功能,想要關閉一些也說不定。或者,你需要對Actuator稍作擴展,增加一些自定義的度量信息,以滿足你對應用程序的需求。
實際上,Actuator有多種定制方式,包括以下五項。
重命名端點。
啟用和禁用端點。
自定義度量信息。
創建自定義倉庫來存儲跟蹤數據。
插入自定義的健康指示器。
接下來,我們會瞭解如何定制Actuator,以滿足我們的需要。先來看一個最簡單的定制:重命名Actuator端點。
7.4.1 修改端點ID
每個Actuator端點都有一個ID用來決定端點的路徑,比方說,/beans端點的默認ID就是beans
。
如果端點的路徑是由ID決定的,那麼可以通過修改ID來改變端點的路徑。你要做的就是設置一個屬性,屬性名是endpoints.endpoint-id.id
。
我們用/shutdown端點來做個演示,它會響應發往/shutdown的POST
請求。假設你想讓它處理發往/kill的POST
請求,可以通過如下YAML為/shutdown賦予一個新的ID,也就是新的路徑:
endpoints:
shutdown:
id: kill
重命名端點、修改其路徑的理由很多。最明顯的理由就是,端點的命名要和團隊的術語保持一致。你也可能想重命名端點,讓那些熟悉默認名稱的人找不到它,借此增加一些安全感。
遺憾的是,重命名端點並不能真的起到保護作用,頂多是讓黑客慢點找到它們。我們會在7.5節看到如何保護這些Actuator端點。現在先讓我們來看看如何禁用某個(或全部)不希望別人訪問的端點。
7.4.2 啟用和禁用端點
雖然Actuator的端點都很有用,但你不一定需要全部這些端點。默認情況下,所有端點(除了/shutdown)都啟用。我們已經看過如何設置endpoints.shutdown.enabled
為true
,以此開啟/shutdown端點(詳見7.1.1節)。用同樣的方式,你可以禁用其他的端點,將endpoints.endpoint-id.enabled
設置為false
。
例如,要禁用/metrics端點,你要做的就是將endpoints.metrics.enabled
屬性設置為false
。在application.yml裡做如下設置:
endpoints:
metrics:
enabled: false
如果你只想打開一兩個端點,那就先禁用全部端點,然後啟用那幾個你要的,這樣更方便。例如,考慮如下application.yml片段:
endpoints:
enabled: false
metrics:
enabled: true
正如以上片段所示,endpoints.enabled
設置為false
就能禁用Actuator的全部端點,然後將endpoints.metrics.enabled
設置為true
重新啟用/metrics端點。
7.4.3 添加自定義度量信息
在7.1.2節中,你看到了如何從/metrics端點獲得運行中應用程序的內部度量信息,包括內存、垃圾回收和線程信息。這些都是非常有用且信息量很大的度量值,但你可能還想定義自己的度量,用來捕獲應用程序中的特定信息。
比方說,我們想要知道用戶往閱讀列表裡保存了多少次圖書,最簡單的方法就是在每次調用ReadingListController
的addToReadingList
方法時增加計數器值。計數器很容易實現,但這個不斷變化的總計值如何同/metrics端點發佈的度量信息一起發佈出來呢?
再假設我們想要獲得最後保存圖書的時間戳。時間戳可以通過調用System.currentTimeMillis
來獲取,但如何在/metrics端點裡報告該時間戳呢?
實際上,自動配置允許Actuator創建CounterService
的實例,並將其註冊為Spring的應用程序上下文中的Bean。CounterService
這個接口裡定義了三個方法,分別用來增加、減少或重置特定名稱的度量值,代碼如下:
package org.springframework.boot.actuate.metrics;
public interface CounterService {
void increment(String metricName);
void decrement(String metricName);
void reset(String metricName);
}
Actuator的自動配置還會配置一個GaugeService
類型的Bean。該接口與CounterService
類似,能將某個值記錄到特定名稱的度量值裡。GaugeService
看起來是這樣的:
package org.springframework.boot.actuate.metrics;
public interface GaugeService {
void submit(String metricName, double value);
}
你無需實現這些接口。Spring Boot已經提供了兩者的實現。我們所要做的就是把它們的實例注入所需的Bean,在適當的時候調用其中的方法,更新想要的度量值。
針對上文提到的需求,我們需要把CounterService
和GaugeService
Bean注入ReadingListController
,然後在addToReadingList
方法裡調用其中的方法。代碼清單7-9是ReadingListController
裡的相關變動:
代碼清單7-9 使用注入的
CounterService
和GaugeService
@Controller
@RequestMapping(\"/\")
@ConfigurationProperties(\"amazon\")
public class ReadingListController {
...
private CounterService counterService;
@Autowired
public ReadingListController(
ReadingListRepository readingListRepository,
AmazonProperties amazonProperties,
CounterService counterService,
GaugeService gaugeService) { ←---注入CounterService和GaugeService
this.readingListRepository = readingListRepository;
this.amazonProperties = amazonProperties;
this.counterService = counterService;
this.gaugeService = gaugeService;
}
...
@RequestMapping(method=RequestMethod.POST)
public String addToReadingList(Reader reader, Book book) {
book.setReader(reader);
readingListRepository.save(book);
counterService.increment(\"books.saved\"); ←---增加books.saved的值
gaugeService.submit(
\"books.last.saved\", System.currentTimeMillis); ←---記錄books.last.saved的值
return \"redirect:/\";
}
}
修改後的ReadingListController
使用了自動織入機制,通過控制器的構造方法注入CounterService
和GaugeService
,隨後把它們保存在實例變量裡。此後,addToReadingList
方法每次處理請求時都會調用counterService.increment (\"books.saved\")
和gaugeService.submit(\"books.last.saved\")
來調整度量值。
儘管CounterService
和GaugeService
用起來很簡單,但還是有一些度量值很難通過增加計數器或記錄指標值來捕獲。對於那些情況,我們可以實現PublicMetrics
接口,提供自己需要的度量信息。該接口定義了一個metrics
方法,返回一個Metric
對象的集合:
package org.springframework.boot.actuate.endpoint;
public interface PublicMetrics {
Collection<Metric<?>> metrics;
}
為瞭解PublicMetrics
的使用方法,這裡假設我們想報告一些源自Spring應用程序上下文的度量值——應用程序上下文啟動的時間、Bean及Bean定義的數量,這些都包含進來會很有意思。順便再報告一下添加了@Controller
註解的Bean的數量。代碼清單7-10給出了相應PublicMetrics
實現的代碼。
代碼清單7-10 發佈自定義度量信息
package readinglist;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.endpoint.PublicMetrics;
import org.springframework.boot.actuate.metrics.Metric;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
@Component
public class ApplicationContextMetrics implements PublicMetrics {
private ApplicationContext context;
@Autowired
public ApplicationContextMetrics(ApplicationContext context) {
this.context = context;
}
@Override
public Collection<Metric<?>> metrics {
List<Metric<?>> metrics = new ArrayList<Metric<?>>;
metrics.add(new Metric<Long>(\"spring.context.startup-date\", ←---記錄啟動時間
context.getStartupDate));
metrics.add(new Metric<Integer>(\"spring.beans.definitions\", ←---記錄Bean定義數量
context.getBeanDefinitionCount));
metrics.add(new Metric<Integer>(\"spring.beans\",
context.getBeanNamesForType(Object.class).length)); ←---記錄Bean數量
metrics.add(new Metric<Integer>(\"spring.controllers\",
context.getBeanNamesForAnnotation(Controller.class).length)); ←---記錄控制器類型的Bean數量
return metrics;
}
}
Actuator會調用metrics
方法,收集ApplicationContextMetrics
提供的度量信息。該方法調用了所注入的ApplicationContext
上的方法,獲取我們想要報告為度量的數量。每個度量值都會創建一個Metrics
實例,指定度量的名稱和值,將其加入要返回的列表。
創建ApplicationContextMetrics
,並在ReadingListController
裡使用CounterService
和GaugeService
之後,我們可以在/metrics端點的響應中找到如下條目:
{
...
spring.context.startup-date: 1429398980443,
spring.beans.definitions: 261,
spring.beans: 272,
spring.controllers: 2,
books.count: 1,
gauge.books.save.time: 1429399793260,
...
}
當然,這些度量的實際值會根據添加了多少書、何時啟動應用程序及何時保存最後一本書而發生變化。在這個例子裡,你一定會好奇為什麼spring.controllers
是2。因為這裡算上了ReadingListController
以及Spring Boot提供的BasicErrorController
。
7.4.4 創建自定義跟蹤倉庫
默認情況下,/trace端點報告的跟蹤信息都存儲在內存倉庫裡,100個條目封頂。一旦倉庫滿了,就開始移除老的條目,給新的條目騰出空間。在開發階段這沒什麼問題,但在生產環境中,大流量會造成跟蹤信息還沒來得及看就被丟棄。
為了避免這個問題,你可以聲明自己的InMemoryTraceRepository
Bean,將它的容量調整至100以上。如下配置類可以將容量調整至1000個條目:
package readinglist;
import org.springframework.boot.actuate.trace.InMemoryTraceRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ActuatorConfig {
@Bean
public InMemoryTraceRepository traceRepository {
InMemoryTraceRepository traceRepo = new InMemoryTraceRepository;
traceRepo.setCapacity(1000);
return traceRepo;
}
}
倉庫容量翻了10倍,跟蹤信息的保存時間應該會更久。不過,繁忙到一定程度,應用程序還是可能在你查看這些信息前將其丟棄。這是一個內存存儲的倉庫,還要避免容量增長太多,影響應用程序的內存使用。
除了上述方法,我們還可以將那些跟蹤條目存儲在其他地方——既不消耗內存,又能長久保存的地方。只需實現Spring Boot的TraceRepository
接口即可:
package org.springframework.boot.actuate.trace;
import java.util.List;
import java.util.Map;
public interface TraceRepository {
List<Trace> findAll;
void add(Map<String, Object> traceInfo);
}
如你所見,TraceRepository
只要求我們實現兩個方法:一個方法查找所有存儲的Trace
對象,另一個保存了一個Trace
,包含跟蹤信息的Map
對象。
作為演示,假設我們創建了一個使用MongoDB數據庫存儲跟蹤信息的TraceRepository
實例。代碼清單7-11演示了如何實現這個TraceRepository
。
代碼清單7-11 往MongoDB保存跟蹤數據
package readinglist;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.trace.Trace;
import org.springframework.boot.actuate.trace.TraceRepository;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.stereotype.Service;
@Service
public class MongoTraceRepository implements TraceRepository {
private MongoOperations mongoOps;
@Autowired
public MongoTraceRepository(MongoOperations mongoOps) { ←---注入MongoOperations
this.mongoOps = mongoOps;
}
@Override
public List<Trace> findAll {
return mongoOps.findAll(Trace.class); ←---獲取所有跟蹤條目
}
@Override
public void add(Map<String, Object> traceInfo) {
mongoOps.save(new Trace(new Date, traceInfo)); ←---保存一個跟蹤條目
}
}
findAll
方法很直白,用注入的MongoOperations
來查找全部Trace
對象。add
方法稍微有趣一點,用當前時間和含有跟蹤信息的Map
創建了一個Trace
對象,然後通過MongoOperations.save
將其保存下來。唯一的問題是,MongoOperations
是哪裡來的?
為了使用MongoTraceRepository
,我們需要保證Spring應用程序上下文裡先有一個MongoOperations
Bean。得益於Spring Boot的起步依賴和自動配置,做到這一點只需添加MongoDB起步依賴即可。你需要如下Gradle依賴:
compile(\"org.springframework.boot:spring-boot-starter-data-mongodb\")
如果你用的是Maven,則需要如下依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
添加了這個起步依賴後,Spring Data MongoDB和所依賴的庫會添加到應用程序的Classpath裡。Spring Boot會自動配置所需的Bean,以便使用MongoDB數據庫。這些Bean裡就包括MongoOperations
。另外,你需要確保和MongoOperations
通訊的MongoDB服務器正常運行。
7.4.5 插入自定義健康指示器
如前文所述,Actuator自帶了很多健康指示器,能滿足常見需求,比如報告應用程序使用的數據庫和消息代理的健康情況。但如果你的應用程序需要和一些沒有健康指示器的系統交互,那該怎麼辦呢?
我們的閱讀列表裡有指向Amazon的圖書鏈接,可以報告一下Amazon是否可以訪問。當然,Amazon不太可能宕機,但不怕一萬就怕萬一,所以讓我們為Amazon創建一個健康指示器吧。代碼清單7-12演示了相關HealthIndicator
的實現。
代碼清單7-12 自定義一個Amazon健康指示器
package readinglist;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
@Component
public class AmazonHealth implements HealthIndicator {
@Override
public Health health {
try {
RestTemplate rest = new RestTemplate;
rest.getForObject(\"http://www.amazon.com\", String.class); ←---向Amazon發送請求
return Health.up.build;
} catch (Exception e) {
return Health.down.build; ←---報告DOWN狀態
}
}
}
AmazonHealth
類並沒有什麼花哨的地方。health
方法只是使用Spring的RestTemplate
向Amazon首頁發起了一個GET
請求。如果請求成功,則返回一個表明Amazon狀態為UP
的Health
對象。如果請求發生異常,則health
返回一個標明Amazon狀態為DOWN
的Health
對象。
下面是/health端點響應的一個片段。這裡可以看出,如果Amazon不可訪問,你會看到什麼。
{
\"amazonHealth\": {
\"status\": \"DOWN\"
},
...
}
你不會相信我等Amazon宕機等了多久,就為了能看到上面的結果!1
1實際上我並沒有等太久。我只是把電腦的網絡斷開了。沒有網就沒有Amazon。
除了簡單的狀態之外,如果你還想向健康記錄裡添加其他附加信息,可以調用Health
構造器的withDetail
方法。例如,要添加異常消息,將其作為健康記錄的reason
字段,可以讓catch
塊返回這樣一個Health
對像:
return Health.down.withDetail(\"reason\", e.getMessage).build;
修改後,當Amazon無法訪問時,健康記錄看起來是這樣的:
\"amazonHealth\": {
\"reason\": \"I/O error on GET request for
\"http://www.amazon.com\":www.amazon.com;
nested exception is java.net.UnknownHostException:
www.amazon.com\",
\"status\": \"DOWN\"
},
如果有很多附加信息,可以多次調用withDetail
方法,每次設置一個要放入健康記錄的附加字段。