likes
comments
collection
share

获取Java后端项目的所有controller接口信息(一)

作者站长头像
站长
· 阅读数 13

「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战

前言

在我们进行后端业务开发时,一个经常需要的小功能就是去获取我们所有编写的接口及其请求信息。可以用于接口测试,或者开发一些自定义的工具。

在开发过程中,我们一般会配置swagger来对于我们的controller进行测试,也经常会使用postman、curl等工具来模拟发送请求,但是很多时候这些工具并不能满足我们的所有需求,需要进行定制化开发。例如笔者所在的团队,除了日常使用swagger之外,同时基于所有的接口信息维护了一个工具来方便进行一些定制化的需求开发、日常的业务维护以及线上问题的处理。

那么如何获取所需要的接口信息呢?

结合所查询的信息,以及笔者的亲身实践,大致总结了以下三种方法

  1. 通过 RequestMappingHandlerMapping
  2. 通过 Swagger
  3. 通过 Spring Boot Actuator

作为一个系列文章,本文章首先介绍最基础的方法——通过 RequestMappingHandlerMapping 获取

什么是 RequestMappingHandlerMapping

直接看源码注释

/**
 * Creates {@link RequestMappingInfo} instances from type and method-level
 * {@link RequestMapping @RequestMapping} annotations in
 * {@link Controller @Controller} classes.
 *
 * @author Arjen Poutsma
 * @author Rossen Stoyanchev
 * @author Sam Brannen
 * @since 3.1
 */
public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping
      implements MatchableHandlerMapping, EmbeddedValueResolverAware {
      ......
}

正如注释中所说,该类用于处理被 @Controller 所注解类中类型和方法级别的 @RequestMapping 并创建 RequestMappingInfo 实例,即用来创建并保存所有的请求映射信息。而在参考文档2中对其的描述如下

The RequestMappingHandlerMapping is used to maintain the mapping of the request URI to the handler. Once the handler is obtained, the DispatcherServlet dispatches the request to the appropriate handler adapter, which then invokes the handlerMethod().

即保存了从请求路径到对应handler的映射,DispatcherServlet 将请求交给 RequestMappingHandlerAdapter 来处理,调用对应保存的 handle 方法

RequestMappingInfo

RequestMappingInfo 中保存了哪些东西呢, 我们看源码注释

/**
 * Request mapping information. Encapsulates the following request mapping conditions:
 * <ol>
 * <li>{@link PatternsRequestCondition}
 * <li>{@link RequestMethodsRequestCondition}
 * <li>{@link ParamsRequestCondition}
 * <li>{@link HeadersRequestCondition}
 * <li>{@link ConsumesRequestCondition}
 * <li>{@link ProducesRequestCondition}
 * <li>{@code RequestCondition} (optional, custom request condition)
 * </ol>
 *
 * @author Arjen Poutsma
 * @author Rossen Stoyanchev
 * @since 3.1
 */
public final class RequestMappingInfo implements RequestCondition<RequestMappingInfo> {
    ......
}

可以看出,其封装了如下几个信息

  1. PatternsRequestCondition 请求路径信息
  2. RequestMethodsRequestCondition http方法信息
  3. ParamsRequestCondition query或者表单信息
  4. HeadersRequestCondition 请求头信息
  5. HeadersRequestCondition 请求的Content-Type信息
  6. ProducesRequestCondition 所需的Accept信息
  7. 其他自定义的请求条件

因此一个很简单的想法便可以从脑海中浮现,如果在SpringMVC启动完成之后,我们去获取 RequestMappingHandlerMapping 对象,便可以从中获取到所有的接口信息

如何做到呢?监听容器启动事件!

ApplicationListener@EventListener

ApplicationListener 接口用于实现容器的事件监听,其设计采用观察者模式,需要实现 onApplicationEvent 方法,其中泛型 E 即为所关心的容器事件,其他事件会被过滤

public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {

   /**
    * Handle an application event.
    * @param event the event to respond to
    */
   void onApplicationEvent(E event);
}

为了方便,也可以使用注解 @EventListener 来进行实现

具体实现

直接上代码,其中隐藏自定义处理的内容

@Slf4j
@Component
public class FrontToolListener implements ApplicationListener<ContextRefreshedEvent> {

    private static final String REQUEST_BEAN_NAME = "requestMappingHandlerMapping";

    // 实现事件监听方法
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        // 需要暴露信息,因此线上环境不扫描
        if (判断是否是线上环境) {
            log.info("线上环境,不扫描controller");
            return;
        }

        // 判断RequestMappingHandlerMapping是否存在,若不存在则不扫描
        ApplicationContext applicationContext = event.getApplicationContext();
        if (!applicationContext.containsBean(REQUEST_BEAN_NAME)) {
            log.info("{}不存在, 扫描跳过", REQUEST_BEAN_NAME);
            return;
        }

        log.info("{}存在,开始扫描controller方法", REQUEST_BEAN_NAME);
        // 获取所有的接口方法信息
        RequestMappingHandlerMapping requestMapping =
            applicationContext.getBean(REQUEST_BEAN_NAME, RequestMappingHandlerMapping.class);
        Map<RequestMappingInfo, HandlerMethod> infoMap = requestMapping.getHandlerMethods();
        
        // 省略自定义处理信息
        ......
    }
}

一些需要注意的问题

  1. 笔者自定义工具需要暴露接口信息,故线上环境不允许进行扫描,否则存在安全性问题,预发环境原则上也不允许进行扫描,除非你可以保证预发环境不会被外部访问
  2. 在某些项目内可能不存在 RequestMappingHandlerMapping,需要手动进行注入,什么情况下不存在笔者还在研究中, 注入的代码如下
@Slf4j
@EnableWebMvc
@Configuration
public class FrontToolConfig implements WebMvcConfigurer {

    @Bean
    @ConditionalOnMissingBean(name = "requestMappingHandlerMapping")
    public RequestMappingHandlerMapping requestMappingHandlerMapping() {
        log.info("RequestMappingHandlerMapping不存在,开始注入");
        return new RequestMappingHandlerMapping();
    }
  1. ContextRefreshedEvent 事件可能会触发多次,导致我们的监听函数多次被执行,其本质是因为存在多个ioc容器从而多次触发容器事件,解决方法可以参考这篇文章 实现ApplicationListener 事件被触发两次的问题 或者自己添加一个static的bool值来标识是否已经处理完成

参考文档

  1. Get All Endpoints in Spring Boot | Baeldung
  2. Types of Spring HandlerAdapters | Baeldung
  3. Spring Application Context Events | Baeldung
  4. 实现ApplicationListener 事件被触发两次的问题