列出所有已部署的 REST 端点(spring-boot、jersey)

Posted

技术标签:

【中文标题】列出所有已部署的 REST 端点(spring-boot、jersey)【英文标题】:Listing all deployed rest endpoints (spring-boot, jersey) 【发布时间】:2015-12-08 03:31:13 【问题描述】:

是否可以使用 spring boot 列出我所有配置的 rest-endpoints?执行器在启动时列出了所有现有路径,我希望我的自定义服务有类似的东西,所以我可以在启动时检查所有路径是否配置正确,并将此信息用于客户端调用。

我该怎么做?我在我的服务 bean 上使用@Path/@GET 注释并通过ResourceConfig#registerClasses 注册它们。

有没有办法查询所有路径的配置?

更新:我通过

注册了 REST 控制器
@Bean
public ResourceConfig resourceConfig() 
   return new ResourceConfig() 
      
      register(MyRestController.class);
    
   ;

Update2:我想要类似的东西

GET /rest/mycontroller/info
POST /res/mycontroller/update
...

动机:当 spring-boot 应用程序启动时,我想打印出所有注册的控制器及其路径,这样我就可以停止猜测要使用哪些端点。

【问题讨论】:

如何用tomcat实现某事 【参考方案1】:

可能最好的方法是使用ApplicationEventListener。从那里您可以监听“应用程序完成初始化”事件,并从ApplicationEvent 获取ResourceModelResourceModel 将拥有所有已初始化的 Resources。然后你可以像其他人提到的那样遍历Resource。下面是一个实现。部分实现取自DropwizardResourceConfig 实现。

import com.fasterxml.classmate.ResolvedType;
import com.fasterxml.classmate.TypeResolver;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;
import org.glassfish.jersey.server.model.Resource;
import org.glassfish.jersey.server.model.ResourceMethod;
import org.glassfish.jersey.server.model.ResourceModel;
import org.glassfish.jersey.server.monitoring.ApplicationEvent;
import org.glassfish.jersey.server.monitoring.ApplicationEventListener;
import org.glassfish.jersey.server.monitoring.RequestEvent;
import org.glassfish.jersey.server.monitoring.RequestEventListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class EndpointLoggingListener implements ApplicationEventListener 

    private static final TypeResolver TYPE_RESOLVER = new TypeResolver();

    private final String applicationPath;

    private boolean withOptions = false;
    private boolean withWadl = false;

    public EndpointLoggingListener(String applicationPath) 
        this.applicationPath = applicationPath;
    

    @Override
    public void onEvent(ApplicationEvent event) 
        if (event.getType() == ApplicationEvent.Type.INITIALIZATION_APP_FINISHED) 
            final ResourceModel resourceModel = event.getResourceModel();
            final ResourceLogDetails logDetails = new ResourceLogDetails();
            resourceModel.getResources().stream().forEach((resource) -> 
                logDetails.addEndpointLogLines(getLinesFromResource(resource));
            );
            logDetails.log();
        
    

    @Override
    public RequestEventListener onRequest(RequestEvent requestEvent) 
        return null;
    

    public EndpointLoggingListener withOptions() 
        this.withOptions = true;
        return this;
    

    public EndpointLoggingListener withWadl() 
        this.withWadl = true;
        return this;
    

    private Set<EndpointLogLine> getLinesFromResource(Resource resource) 
        Set<EndpointLogLine> logLines = new HashSet<>();
        populate(this.applicationPath, false, resource, logLines);
        return logLines;
    

    private void populate(String basePath, Class<?> klass, boolean isLocator,
            Set<EndpointLogLine> endpointLogLines) 
        populate(basePath, isLocator, Resource.from(klass), endpointLogLines);
    

    private void populate(String basePath, boolean isLocator, Resource resource,
            Set<EndpointLogLine> endpointLogLines) 
        if (!isLocator) 
            basePath = normalizePath(basePath, resource.getPath());
        

        for (ResourceMethod method : resource.getResourceMethods()) 
            if (!withOptions && method.getHttpMethod().equalsIgnoreCase("OPTIONS")) 
                continue;
            
            if (!withWadl && basePath.contains(".wadl")) 
                continue;
            
            endpointLogLines.add(new EndpointLogLine(method.getHttpMethod(), basePath, null));
        

        for (Resource childResource : resource.getChildResources()) 
            for (ResourceMethod method : childResource.getAllMethods()) 
                if (method.getType() == ResourceMethod.JaxrsType.RESOURCE_METHOD) 
                    final String path = normalizePath(basePath, childResource.getPath());
                    if (!withOptions && method.getHttpMethod().equalsIgnoreCase("OPTIONS")) 
                        continue;
                    
                    if (!withWadl && path.contains(".wadl")) 
                        continue;
                    
                    endpointLogLines.add(new EndpointLogLine(method.getHttpMethod(), path, null));
                 else if (method.getType() == ResourceMethod.JaxrsType.SUB_RESOURCE_LOCATOR) 
                    final String path = normalizePath(basePath, childResource.getPath());
                    final ResolvedType responseType = TYPE_RESOLVER
                            .resolve(method.getInvocable().getResponseType());
                    final Class<?> erasedType = !responseType.getTypeBindings().isEmpty()
                            ? responseType.getTypeBindings().getBoundType(0).getErasedType()
                            : responseType.getErasedType();
                    populate(path, erasedType, true, endpointLogLines);
                
            
        
    

    private static String normalizePath(String basePath, String path) 
        if (path == null) 
            return basePath;
        
        if (basePath.endsWith("/")) 
            return path.startsWith("/") ? basePath + path.substring(1) : basePath + path;
        
        return path.startsWith("/") ? basePath + path : basePath + "/" + path;
    

    private static class ResourceLogDetails 

        private static final Logger logger = LoggerFactory.getLogger(ResourceLogDetails.class);

        private static final Comparator<EndpointLogLine> COMPARATOR
                = Comparator.comparing((EndpointLogLine e) -> e.path)
                .thenComparing((EndpointLogLine e) -> e.httpMethod);

        private final Set<EndpointLogLine> logLines = new TreeSet<>(COMPARATOR);

        private void log() 
            StringBuilder sb = new StringBuilder("\nAll endpoints for Jersey application\n");
            logLines.stream().forEach((line) -> 
                sb.append(line).append("\n");
            );
            logger.info(sb.toString());
        

        private void addEndpointLogLines(Set<EndpointLogLine> logLines) 
            this.logLines.addAll(logLines);
        
    

    private static class EndpointLogLine 

        private static final String DEFAULT_FORMAT = "   %-7s %s";
        final String httpMethod;
        final String path;
        final String format;

        private EndpointLogLine(String httpMethod, String path, String format) 
            this.httpMethod = httpMethod;
            this.path = path;
            this.format = format == null ? DEFAULT_FORMAT : format;
        

        @Override
        public String toString() 
            return String.format(format, httpMethod, path);
        
    

然后你只需要在 Jersey 注册监听器。您可以从JerseyProperties 获取应用程序路径。您需要在属性spring.jersey.applicationPath 下的Spring Boot application.properties 中设置它。这将是根路径,就像您要在 ResourceConfig 子类上使用 @ApplicationPath 一样

@Bean
public ResourceConfig getResourceConfig(JerseyProperties jerseyProperties) 
    return new JerseyConfig(jerseyProperties);

...
public class JerseyConfig extends ResourceConfig 

    public JerseyConfig(JerseyProperties jerseyProperties) 
        register(HelloResource.class);
        register(new EndpointLoggingListener(jerseyProperties.getApplicationPath()));
    

需要注意的一点是,Jersey servlet 上默认未设置启动时加载。这意味着 Jersey 在第一个请求之前不会在启动时加载。所以在第一个请求之前你不会看到监听器被触发。我已经打开 an issue 以获得配置属性,但与此同时,您有几个选择:

    将 Jersey 设置为过滤器,而不是 servlet。过滤器将在启动时加载。使用 Jersey 作为过滤器,对于大多数帖子来说,实际上并没有什么不同。要配置它,您只需在 application.properties 中添加一个 Spring Boot 属性

    spring.jersey.type=filter
    

    另一个选项是覆盖 Jersey ServletRegistrationBean 并设置其 loadOnStartup 属性。这是一个示例配置。部分实现直接取自JerseyAutoConfiguration

    @SpringBootApplication
    public class JerseyApplication 
    
        public static void main(String[] args) 
            SpringApplication.run(JerseyApplication.class, args);
        
    
        @Bean
        public ResourceConfig getResourceConfig(JerseyProperties jerseyProperties) 
            return new JerseyConfig(jerseyProperties);
        
    
        @Bean
        public ServletRegistrationBean jerseyServletRegistration(
            JerseyProperties jerseyProperties, ResourceConfig config) 
            ServletRegistrationBean registration = new ServletRegistrationBean(
                    new ServletContainer(config), 
                    parseApplicationPath(jerseyProperties.getApplicationPath())
            );
            addInitParameters(registration, jerseyProperties);
            registration.setName(JerseyConfig.class.getName());
            registration.setLoadOnStartup(1);
            return registration;
        
    
        private static String parseApplicationPath(String applicationPath) 
            if (!applicationPath.startsWith("/")) 
                applicationPath = "/" + applicationPath;
            
            return applicationPath.equals("/") ? "/*" : applicationPath + "/*";
        
    
        private void addInitParameters(RegistrationBean registration, JerseyProperties jersey) 
            for (Entry<String, String> entry : jersey.getInit().entrySet()) 
                registration.addInitParameter(entry.getKey(), entry.getValue());
            
        
    
    

更新

所以看起来 Spring Boot 会转到 add the load-on-startup property,所以我们不必覆盖 Jersey ServletRegistrationBean。将在 Boot 1.4.0 中添加

【讨论】:

没有工作我得到了泽西岛应用程序的所有端点 GET /rest/application.wadl OPTIONS /rest/application.wadl GET /rest/application.wadl/path OPTIONS /rest/application.wadl /path GET /rest/engine OPTIONS /rest/engine 但我正在寻找“GET /rest/engine/default/processinstance”... 贴一个资源类的例子。就 OPTIONS 和 wadl 而言,将它们过滤掉并没有太多 我修好了。它弄乱了子资源定位器。但我刚刚测试了新的实现,一切正常。我还添加了包含 OPTIONS 和 wadl 的选项,但默认情况下它是禁用的。如果您需要它们,只需在创建侦听器时调用流利的withOptions() 和/或withWadl() 使用 Servlet 3 可以使用 .@Provider 注释注册监听器。在这种情况下,您也必须注入 ServletContext 以获取上下文路径。它也无需上述配置即可立即启动。 @peeskillet 我注意到您编写的侦听器不考虑通过 .@ApplicationPath 指定的路径。如何读取这个注解的值?【参考方案2】:

所有 REST 端点都列在/actuator/mappings 端点中。

使用属性management.endpoints.web.exposure.include 激活映射端点

例如:management.endpoints.web.exposure.include=env,info,health,httptrace,logfile,metrics,mappings

【讨论】:

非常感谢,这太方便了(至少对于开发环境而言)。我还在我的 build.gradle(.kts) 中添加了 implementation("org.springframework.boot:spring-boot-starter-actuator") 并且与您提供的 URL (/actuator/mappings) 完美搭配【参考方案3】:

您能否在您的ResourceConfig 对象上使用ResourceConfig#getResources,然后通过遍历它返回的Set&lt;Resource&gt; 来获取您需要的信息?

抱歉,我想试试,但我现在没有 资源 去做。 :-p

【讨论】:

我尝试通过使用 RunListeners “完成”方法来做到这一点。我得到了 listerer 运行的输出,但是 getResources() 上的循环是空的。查看更新的问题。【参考方案4】:

应用完全启动后,可以问ServerConfig

ResourceConfig instance; 
ServerConfig scfg = instance.getConfiguration();
Set<Class<?>> classes = scfg.getClasses();

classes 包含所有缓存的端点类。

来自the API docs 为javax.ws.rs.core.Configuration

获取要在可配置实例范围内实例化、注入和使用的一组不可变的已注册 JAX-RS 组件(例如提供程序或功能)类。

但是,您不能在应用程序的初始化代码中执行此操作,这些类可能尚未完全加载。

使用这些类,您可以扫描它们以获取资源:

public Map<String, List<InfoLine>> scan(Class baseClass) 
    Builder builder = Resource.builder(baseClass);
    if (null == builder)
        return null;
    Resource resource = builder.build();
    String uriPrefix = "";
    Map<String, List<InfoLine>> info = new TreeMap<>();
    return process(uriPrefix, resource, info);


private Map<String, List<InfoLine>> process(String uriPrefix, Resource resource, Map<String, List<InfoLine>> info) 
    String pathPrefix = uriPrefix;
    List<Resource> resources = new ArrayList<>();
    resources.addAll(resource.getChildResources());
    if (resource.getPath() != null) 
        pathPrefix = pathPrefix + resource.getPath();
    
    for (ResourceMethod method : resource.getAllMethods()) 
        if (method.getType().equals(ResourceMethod.JaxrsType.SUB_RESOURCE_LOCATOR)) 
            resources.add(
                Resource.from(
                    resource.getResourceLocator()
                            .getInvocable()
                            .getDefinitionMethod()
                            .getReturnType()
                )
            );
        
        else 
            List<InfoLine> paths = info.get(pathPrefix);
            if (null == paths) 
                paths = new ArrayList<>();
                info.put(pathPrefix, paths);
            
            InfoLine line = new InfoLine();
            line.pathPrefix = pathPrefix;
            line.httpMethod = method.getHttpMethod();
            paths.add(line);
            System.out.println(method.getHttpMethod() + "\t" + pathPrefix);
        
    
    for (Resource childResource : resources) 
        process(pathPrefix, childResource, info);
    
    return info;



private class InfoLine 
    public String pathPrefix;
    public String httpMethod;

【讨论】:

感谢 Johannes 的提示,但我不是在寻找所有课程,而是在寻找他们注册的已解决路径。由于路径可以通过控制器继承(以及 spring-boot 根路径设置)叠加,因此仅扫描类以获取路径注释不会吗?或者你有具体场景的例子吗?我更新了问题。 我已经用我用来检索该信息的代码修改了答案。我们不使用 Spring-Boot,而是使用 Tomcat/Jersey,但原理应该与您使用的 ResourceConfig 相同。只需尝试一下,看看它是否有效,或者 Spring 控制器继承是否在齿轮中抛出了一个活动扳手。 我刚刚接受并奖励了 peeskillets 的回答。你的看起来非常接近,但他有完整的代码示例。还是谢谢你!【参考方案5】:

如何使用包含所有端点信息的RequestMappingHandlerMapping

在How to access all available routes of a REST API from a controller?查看我的回答。

【讨论】:

不适用于 Jersey 端点(这个问题是关于)。

以上是关于列出所有已部署的 REST 端点(spring-boot、jersey)的主要内容,如果未能解决你的问题,请参考以下文章

REST API - 如何构建端点 url

如何在 Django REST Swagger 中生成响应消息列表?

多个字段解析器使用不同的查询参数解析相同的 REST API

jhipster-gateway API 部分 (swagger-ui) 中未列出 API-first rest 端点

所有可用指标的 Prometheus 端点

在实时环境中部署 NodeJS 和 MySQL REST API [关闭]