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

45,531

Solution 1

Probably the best way to do this, is to use an 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 implementation.

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

Then you just need to register the listener with Jersey. You can get the application path from the JerseyProperties. You will need to have set it in the Spring Boot application.properties under the property spring.jersey.applicationPath. This will be the root path, just as if you were to use @ApplicationPath on your ResourceConfig subclass

@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()));
    }
}

One thing to note, is that the load-on-startup is not set by default on the Jersey servlet. What this means is that that Jersey won't load on startup until the first request. So you will not see the listener triggered until the first request. I have opened an issue to possible get a configuration property, but in the meantime, you have a couple options:

  1. Set up Jersey as filter, instead of a servlet. The filter will be loaded on start up. Using Jersey as a filter, for the most post, really doesn't behave any differently. To configure this you just need to add a Spring Boot property in the application.properties

    spring.jersey.type=filter
    
  2. The other option is to override the Jersey ServletRegistrationBean and set its loadOnStartup property. Here is an example configuration. Some of the implementation has been taken straight from the 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());
            }
        }
    }
    

UPDATE

So it looks like Spring Boot is going to add the load-on-startup property, so we don't have to override the Jersey ServletRegistrationBean. Will be added in Boot 1.4.0

Solution 2

All REST endpoints are listed in /actuator/mappings endpoint.

Activate the mappings endpoint with the property management.endpoints.web.exposure.include

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

Solution 3

Can you use ResourceConfig#getResources on your ResourceConfig object then get the info you need by iterating through the Set<Resource> it returns?

Apologies, would try it, but I don't have the Resources to do it right now. :-p

Solution 4

After the application is fully started, you can ask ServerConfig:

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

classes contains all the cached endpoint classes.

From the API docs for javax.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.

However, you can't do this in the init code of your application, the classes might not yet be fully loaded.

With the classes, you can scan them for the resources:

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

Solution 5

What about using RequestMappingHandlerMapping that hold all endpoints information.

See my answer at How to access all available routes of a REST API from a controller?.

Share:
45,531
Jan Galinski
Author by

Jan Galinski

I am a Software Consultant living and working in Hamburg, Germany. @github blog about.me

Updated on August 19, 2020

Comments

  • Jan Galinski
    Jan Galinski over 3 years

    Is it possible to list all my configured rest-endpoints with spring boot? The actuator lists all existing paths on startup, I want something similar for my custom services, so I can check on startup if all paths are configured correctly and use this info for client calls.

    How do I do this? I use @Path/@GET annotations on my service beans and register them via ResourceConfig#registerClasses.

    Is there a way to query the Config for all Paths?

    Update: I register the REST Controllers via

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

    Update2: I want to have something like

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

    Motivation: when the spring-boot app started, I want to print out all registered controllers and their paths, so I can stop guessing which endpoints to use.

  • Jan Galinski
    Jan Galinski over 8 years
    I tried to do so by using a RunListeners "finished" method. I get the output that the listerer is run, but the loop over getResources() is empty. See updated question.
  • Jan Galinski
    Jan Galinski about 8 years
    Thanks for the hint, Johannes, but I am not looking for all classes, I am looking for the resolved Paths they are registered on. Since paths can stack up via controller inheritance (and the spring-boot root path settings), just scanning the classes for the path annotations wont do? Or do you have an example for the concrete scenario? I updated the question.
  • Johannes Jander
    Johannes Jander about 8 years
    I have amended the answer with the code I use to retrieve that information. We don't use Spring-Boot, but Tomcat/Jersey, but the principle should be the same as you have a ResourceConfigto work with. Just try it out and see if it works or if the Spring controller inheritance throws a monkey wrench in the gears.
  • Jan Galinski
    Jan Galinski about 8 years
    Did not work I got All endpoints for Jersey application GET /rest/application.wadl OPTIONS /rest/application.wadl GET /rest/application.wadl/{path} OPTIONS /rest/application.wadl/{path} GET /rest/engine OPTIONS /rest/engine but I was looking for "GET /rest/engine/default/processinstance" ...
  • Paul Samsotha
    Paul Samsotha about 8 years
    Post an example of the resource class. As far as the OPTIONS and the wadl, it doesn't really to much to filter those out
  • Paul Samsotha
    Paul Samsotha about 8 years
    I fixed it. it was messing up with subresource locators. But I just tested the new implementation and everything works. I also added the option to include OPTIONS and wadl, but it's disabled by default. If you want them, just call the fluent withOptions() and/or withWadl() when creating the listener.
  • Jan Galinski
    Jan Galinski about 8 years
    I just accepted and rewarded peeskillets answer. Yours is looking very close, but he had the complete code example. Thank you anyway!
  • Sergey
    Sergey about 8 years
    With Servlet 3 the listener can be registered with .@Provider annotation. In this case you have to inject ServletContext too to get the context path. It also started immidiatelly without need of configuration described above.
  • Sergey
    Sergey about 8 years
    @peeskillet I noticed that the listener you wrote does not account for path specified via .@ApplicationPath. How the value of this annotation can be read?
  • Paul Samsotha
    Paul Samsotha about 8 years
    @Sergey It's better to just pass the value to the constructor, as there are multiple ways to create the application path. If you really want to read it from the annotation, you can do YourJerseyConfig.class.getAnnotation(ApplicationPath.class).‌​value(). My example spring boot config class just uses the configuration property from the application.properties file: spring.jersey.applicationPath. The value will end up in the JerseyProperties object, or you can inject it in the Spring @Value
  • theGamblerRises
    theGamblerRises over 7 years
    I am getting exception like this. Parameter 1 of method jerseyServletRegistration in in.playment.ApplicationStarter required a single bean, but 2 were found: - jerseyConfig: defined in file [/Users/playment/git/rutherford/target/classes/in/playment/J‌​‌​erseyConfig.class] - getResourceConfig: defined by method 'getResourceConfig' in in.playment.ApplicationStarter
  • Paul Samsotha
    Paul Samsotha over 7 years
    @theGamblerRises If you use the getResourceConfig bean method, then you should not also have the ResourceConfig as a @Component. This will end up creating two ResourceConfig instances. Choose one or the other.
  • theGamblerRises
    theGamblerRises over 7 years
    Thanks. I had it as @Configuration. Removed but there is another problem, I have two base paths. Could you please have a look into it stackoverflow.com/questions/41312473/… Thanks
  • Drew Stephens
    Drew Stephens over 6 years
    TypeResolver isn't a thing in Jackson anymore (as of v2.9.1)—I don't know what sub-resources are and my app doesn't have any so I just log error in that clause and I'll figure it out if I ever run into it.
  • Paul Samsotha
    Paul Samsotha almost 6 years
    Doesn't work for Jersey endpoints (which this question is about).
  • rodney757
    rodney757 over 5 years
    @PaulSamsotha This fails if a resource method returns Resource... Ex. public Resource myResource() { return Resource.from(MyResource.class); }. Any ideas how to get the routes for MyResource?
  • R. Du
    R. Du about 3 years
    thanks a lot, that's so convenient (for dev environnement at least). I also added implementation("org.springframework.boot:spring-boot-starter‌​-actuator") in my build.gradle(.kts) and that works perfect with the URL you were giving (/actuator/mappings)
  • Anish B.
    Anish B. about 3 years
    @PaulSamsotha Thanks :) for the detailed explanation. Working for me. +1