一尘不染

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

spring-boot

是否可以列出所有使用Spring
Boot配置的其余端点?执行器列出了启动时的所有现有路径,我希望为自定义服务提供类似的内容,因此我可以在启动时检查所有路径的配置是否正确,并将此信息用于客户端调用。

我该怎么做呢?我在服务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应用程序时,我想打印出所有已注册的控制器及其路径,因此我可以停止猜测要使用的端点。


阅读 267

收藏
2020-05-30

共1个答案

一尘不染

可能最好的方法是使用ApplicationEventListener。从那里,你可以听的“应用程序完成初始化”事件,并获得ResourceModelApplicationEvent。在ResourceModel将所有的初始化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<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
Boot中application.properties的属性下进行设置spring.jersey.applicationPath。这将是根路径,就像您要@ApplicationPathResourceConfig子类上使用一样

@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才会在启动时加载。因此,在第一个请求之前,您不会看到侦听器被触发。我打开了一个可能获取配置属性的问题,但与此同时,您有两种选择:

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

    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<String, String> entry : jersey.getInit().entrySet()) {
            registration.addInitParameter(entry.getKey(), entry.getValue());
        }
    }
    

    }


更新

因此,看起来Spring Boot将要添加该load-on-startup属性,因此我们不必覆盖Jersey
ServletRegistrationBean。将在Boot 1.4.0中添加

2020-05-30