spring-boot 自定义 endpoint,让运行时的 java 应用不再是黑盒

本贴最后更新于 2684 天前,其中的信息可能已经水流花落

本文项目已发布到 github,后续学习项目也会添加到此工程下,欢迎 fork 点赞。
https://github.com/wangyuheng/spring-boot-sample

你的 java 应用在运行时对你来说是黑盒吗?你可以查看到 springboot 运行时的各种信息吗?

Spring Boot Actuator

springboot 提供了用于健康检测的 endpoint,提供了查看系统信息、内存、环境等。

1. 添加依赖

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>

2. 访问链接

应用启动后访问 http://localhost:8080/health 查看应用启动状态。
端口号可以通过配置文件 management.port=9527 变更

常用 endpoint

  1. http://localhost:8080/env 环境变量
  2. http://localhost:8080/info 应用信息
  3. http://localhost:8080/metrics 内存等应用基本指标
  4. http://localhost:8080/dump 线程栈
  5. http://localhost:8080/configprops 配置项

3. 开启全部 endpoint & 关闭验权

部分请求会返回 401, 这是因为 endpoint 未开启,或者开启了登录验权,可以通过配置文件进行配置

management.security.enabled=false endpoints.enabled=true

自定义 endpoint

一、实现 Endpoint 接口

public interface Endpoint<T> { String getId(); boolean isEnabled(); boolean isSensitive(); T invoke(); }
  1. getId(), 指定了 endpoint 访问 url
  2. isEnabled(), 表示是否启用
  3. isSensitive(), 表示是否验权
  4. invoke(), 页面返回值

实现类通过 @Bean 的形式注入后,再次启动应用,即可通过 url 访问,并返回 invoke 返回值。

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"; } }
@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 方法的具体实施者,名称用来管理访问路径

public interface EndpointAction extends Serializable { Object execute(); String getName(); }
2.2 EndpointAction 的多种实现

需要实现的功能,如:读取配置文件、查看内存信息

@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"; } }
@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; } }
@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
  1. 注入 action map,根据 name 获取 bean 实现
  2. 通过 url mapping 匹配 action
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 作为入口
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 启动时注入
@SpringBootApplication public class EndpointApplication { public static void main(String[] args) { SpringApplication.run(EndpointApplication.class, args); } @Bean @ConditionalOnMissingBean @ConditionalOnClass(CustomEndpoint.class) public CustomEndpointAdapter customEndpointAdapter() { return new CustomEndpointAdapter(); } }

测试

揪心的测试环节,启动 webEnvironment 环境,通过 TestRestTemplate 访问 endpoint 的 mapping,比对返回值即可

@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); } }
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3195 引用 • 8215 回帖
  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    946 引用 • 1460 回帖
  • endpoint
    1 引用
  • actuator
    2 引用

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...