本文项目已发布到github,后续学习项目也会添加到此工程下,欢迎fork点赞。
https://github.com/wangyuheng/spring-boot-sample
你的java应用在运行时对你来说是黑盒吗?你可以查看到springboot运行时的各种信息吗?
Spring Boot Actuator
springboot提供了用于健康检测的endpoint,提供了查看系统信息、内存、环境等。
1. 添加依赖
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
|
2. 访问链接
应用启动后访问 http://localhost:8080/health 查看应用启动状态。
端口号可以通过配置文件management.port=9527
变更
常用endpoint
- http://localhost:8080/env 环境变量
- http://localhost:8080/info 应用信息
- http://localhost:8080/metrics 内存等应用基本指标
- http://localhost:8080/dump 线程栈
- http://localhost:8080/configprops 配置项
3. 开启全部endpoint & 关闭验权
部分请求会返回401, 这是因为endpoint未开启,或者开启了登录验权,可以通过配置文件进行配置
1 2
| management.security.enabled=false endpoints.enabled=true
|
自定义endpoint
一、实现Endpoint接口
1 2 3 4 5 6 7 8 9
| public interface Endpoint<T> { String getId();
boolean isEnabled();
boolean isSensitive();
T invoke(); }
|
- getId(), 指定了endpoint访问url
- isEnabled(), 表示是否启用
- isSensitive(), 表示是否验权
- invoke(), 页面返回值
实现类通过@Bean的形式注入后,再次启动应用,即可通过url访问,并返回invoke返回值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class CustomEndpoint implements Endpoint {
@Override public String getId() { return "custom"; }
@Override public boolean isEnabled() { return true; }
@Override public boolean isSensitive() { return false; }
@Override public Object invoke() { return "hello endpoint"; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| @SpringBootApplication public class EndpointApplication {
public static void main(String[] args) { SpringApplication.run(EndpointApplication.class, args); }
@Bean public CustomEndpoint customEndpoint() { return new CustomEndpoint(); } }
|
访问http://localhost:8080/custom 可以看到invoke返回的内容。
但是这样,每个endpoint都需要单独注入,且没有层级、通配符,不方便管理,为满足需求,尝试做了如下改造
二、继承EndpointMvcAdapter
2.1 自定义 EndpointAction 接口
用于定制endpoint处理行为,可以理解为invoke方法的具体实施者,名称用来管理访问路径
1 2 3 4 5
| public interface EndpointAction extends Serializable { Object execute();
String getName(); }
|
2.2 EndpointAction的多种实现
需要实现的功能,如:读取配置文件、查看内存信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Component public class PropertiesAction implements EndpointAction {
@Override public Object execute() { try { return PropertiesLoaderUtils.loadAllProperties("application.properties"); } catch (IOException e) { return "read application fail! error: " + e.getMessage(); } }
@Override public String getName() { return "properties"; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| @Component public class VMAction implements EndpointAction {
private static final VMAction INSTANCE = new VMAction();
private String version; private String startTime; private String initHeap; private String maxHeap; private Set<String> arguments;
@Override public Object execute() { INSTANCE.version = System.getProperty("java.runtime.version"); INSTANCE.startTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(ManagementFactory.getRuntimeMXBean().getStartTime()); INSTANCE.initHeap = String.valueOf(ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getInit() / 1024 / 1024).concat("MB"); INSTANCE.maxHeap = String.valueOf(ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getMax() / 1024 / 1024).concat("MB"); INSTANCE.arguments = new HashSet<>(ManagementFactory.getRuntimeMXBean().getInputArguments()); return INSTANCE; }
@Override public String getName() { return "vm"; }
public String getVersion() { return version; }
public String getStartTime() { return startTime; }
public String getInitHeap() { return initHeap; }
public String getMaxHeap() { return maxHeap; }
public Set<String> getArguments() { return arguments; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Component public class DefaultAction implements EndpointAction {
private static final DefaultAction INSTANCE = new DefaultAction();
public static DefaultAction getInstance() { return INSTANCE; }
@Override public Object execute() { return "try /help for action list"; }
@Override public String getName() { return "default"; }
}
|
2.3 继承EndpointMvcAdapter
- 注入action map,根据name获取bean实现
- 通过url mapping匹配action
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| public class CustomEndpointAdapter extends EndpointMvcAdapter {
private Map<String, EndpointAction> endpointActionMap = new HashMap<>();
@Autowired public void setEndpointActionMap(List<EndpointAction> endpointActionList) { endpointActionList.forEach(endpointAction -> endpointActionMap.put(endpointAction.getName(), endpointAction)); }
public CustomEndpointAdapter() { super(new CustomEndpoint()); }
@RequestMapping(value = "/{name:.*}", method = RequestMethod.GET, produces = { ActuatorMediaTypes.APPLICATION_ACTUATOR_V1_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE} ) @ResponseBody @HypermediaDisabled public Object dispatcher(@PathVariable String name) { if ("help".equalsIgnoreCase(name)) { return endpointActionMap.keySet().stream().map(key -> getName() + "/" + key).collect(toSet()); } else { return endpointActionMap.getOrDefault(name, DefaultAction.getInstance()).execute(); } }
}
|
2.4 定制endpoint作为入口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class CustomEndpoint implements Endpoint {
@Override public String getId() { return "custom"; }
@Override public boolean isEnabled() { return true; }
@Override public boolean isSensitive() { return false; }
@Override public Object invoke() { return DefaultAction.getInstance().execute(); } }
|
2.5 启动时注入
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @SpringBootApplication public class EndpointApplication {
public static void main(String[] args) { SpringApplication.run(EndpointApplication.class, args); }
@Bean @ConditionalOnClass(CustomEndpoint.class) public CustomEndpointAdapter customEndpointAdapter() { return new CustomEndpointAdapter(); } }
|
测试
揪心的测试环节,启动webEnvironment环境,通过TestRestTemplate访问endpoint的mapping,比对返回值即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class PropertiesActionTest {
private TestRestTemplate restTemplate;
@Value("${management.port}") private String managementPort;
@Before public void setupMockMvc() { restTemplate = new TestRestTemplate(); }
@Test public void test_properties_action() throws Exception { String path = "http://localhost:" + managementPort + "/custom/properties"; Map<String, String> result = restTemplate.getForObject(path, HashMap.class); assertEquals(result.get("management.port"), managementPort); }
@Test public void test_help() throws Exception { String path = "http://localhost:" + managementPort + "/custom/help"; Set<String> result = restTemplate.getForObject(path, Set.class); assertTrue(result.contains("custom/properties")); }
@Test public void test_rand() throws Exception { String path = "http://localhost:" + managementPort + "/custom/" + new Random().nextInt(); String result = restTemplate.getForObject(path, String.class); assertEquals("try /help for action list", result);
}
}
|