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

是否可以使用spring boot列出我配置的所有rest端点? 执行器在启动时列出所有现有路径,我想要类似于我的自定义服务,所以我可以检查启动是否所有路径都配置正确并使用此信息进行客户端调用。

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

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

更新:我通过注册REST控制器

 @Bean public ResourceConfig resourceConfig() { return new ResourceConfig() { { register(MyRestController.class); } }; } 

Update2:我希望有类似的东西

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

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

可能最好的方法是使用ApplicationEventListener 。 从那里你可以监听“application finished initializing”事件,并从ApplicationEvent获取ResourceModelResourceModel将具有所有已初始化的Resource 。 然后你可以像其他人提到的那样遍历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 getLinesFromResource(Resource resource) { Set logLines = new HashSet<>(); populate(this.applicationPath, false, resource, logLines); return logLines; } private void populate(String basePath, Class klass, boolean isLocator, Set endpointLogLines) { populate(basePath, isLocator, Resource.from(klass), endpointLogLines); } private void populate(String basePath, boolean isLocator, Resource resource, Set 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 COMPARATOR = Comparator.comparing((EndpointLogLine e) -> e.path) .thenComparing((EndpointLogLine e) -> e.httpMethod); private final Set 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 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默认情况下没有设置load-on-startup。 这意味着泽西岛在第一次请求之前不会在启动时加载。 因此,在第一个请求之前,您不会看到侦听器被触发。 我已经打开了一个可能获得配置属性的问题,但在此期间,您有两个选择:

  1. 将Jersey设置为filter,而不是servlet。 filter将在启动时加载。 使用泽西作为filter,对于大多数post,实际上没有任何不同的行为。 要配置它,您只需在application.properties添加Spring Boot属性

     spring.jersey.type=filter 
  2. 另一个选项是覆盖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 entry : jersey.getInit().entrySet()) { registration.addInitParameter(entry.getKey(), entry.getValue()); } } } 

UPDATE

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

你可以在ResourceConfig对象上使用ResourceConfig#getResources然后通过迭代它返回的Set来获取所需的信息吗?

道歉,会尝试,但我现在没有资源去做。 😛

应用程序完全启动后,您可以询问ServerConfig

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

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

来自javax.ws.rs.core.Configuration 的API文档 :

获取在可配置实例的范围内实例化,注入和使用的不可变的注册JAX-RS组件集(例如提供者或特性)。

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

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

 public Map> scan(Class baseClass) { Builder builder = Resource.builder(baseClass); if (null == builder) return null; Resource resource = builder.build(); String uriPrefix = ""; Map> info = new TreeMap<>(); return process(uriPrefix, resource, info); } private Map> process(String uriPrefix, Resource resource, Map> info) { String pathPrefix = uriPrefix; List 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 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; } 

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

请参阅我的答案如何从控制器访问REST API的所有可用路由? 。