列出所有已部署的休息端点(spring-boot, jersey)

Listing all deployed rest endpoints (spring-boot, jersey)

是否可以使用 spring 启动列出我配置的所有 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 应用程序启动时,我想打印出所有已注册的控制器及其路径,这样我就可以停止猜测要使用哪些端点。

你能用ResourceConfig#getResources on your ResourceConfig object then get the info you need by iterating through the Set<Resource>吗returns?

抱歉,我会尝试一下,但我现在没有 资源 来做这件事。 :-p

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

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

classes 包含所有缓存端点 类.

the API docsjavax.ws.rs.core.Configuration:

Get the immutable set of registered JAX-RS component (such as provider or feature) classes to be instantiated, injected and utilized in the scope of the configurable instance.

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

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

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;
}

可能最好的方法是使用 ApplicationEventListener. From there you can listen for the "application finished initializing" event, and get the ResourceModel from the ApplicationEvent. The ResourceModel will have all the initialized Resources. Then you can traverse the Resource as others have mentioned. Below is an implementation. Some of the implementation has been taken from the 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 以获得配置 属性,但与此同时,您有几个选择:

  1. 将 Jersey 设置为过滤器,而不是 servlet。过滤器将在启动时加载。使用 Jersey 作为过滤器,对于大多数 post,确实没有任何不同。要配置它,你只需要在 application.properties

    中添加一个 Spring Boot 属性
    spring.jersey.type=filter
    
  2. 另一种选择是覆盖球衣 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 引导将要 add the load-on-startup property,因此我们不必覆盖 Jersey ServletRegistrationBean。将在 Boot 1.4.0 中添加

使用保存所有端点信息的 RequestMappingHandlerMapping 怎么样?

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

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

使用 属性 management.endpoints.web.exposure.include

激活映射端点

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