web-dev-qa-db-fra.com

Liste de tous les points d'extrémité de repos déployés (bottes à ressort, maillot)

Est-il possible de répertorier tous mes points de terminaison rest configurés avec Spring Boot? L'actionneur répertorie tous les chemins existants au démarrage. Je veux quelque chose de similaire pour mes services personnalisés. Je peux donc vérifier au démarrage si tous les chemins sont configurés correctement et utiliser cette information pour les appels client.

Comment puis-je faire cela? J'utilise des annotations @Path/@GET sur mes beans de service et je les enregistre via ResourceConfig#registerClasses.

Existe-t-il un moyen d'interroger la configuration pour tous les chemins?

Mise à jour: J'enregistre les contrôleurs REST via

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

Update2: Je veux avoir quelque chose comme

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

Motivation: lorsque l'application Spring-Boot a été lancée, je souhaite imprimer tous les contrôleurs enregistrés et leurs chemins, afin de pouvoir arrêter de deviner quels points de terminaison utiliser.

17
Jan Galinski

La meilleure façon de faire est probablement d’utiliser ApplicationEventListener . À partir de là, vous pouvez écouter l'événement "application terminée d'initialisation" et obtenir la variable ResourceModel à partir de la variable ApplicationEvent. La ResourceModel aura tous les Resources initialisés. Ensuite, vous pouvez parcourir la Resource comme d'autres l'ont mentionné. Ci-dessous une implémentation. Une partie de l'implémentation provient de l'implémentation 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);
        }
    }
}

Ensuite, il vous suffit d’enregistrer l’auditeur avec Jersey. Vous pouvez obtenir le chemin d’application à partir de la variable JerseyProperties. Vous devrez l'avoir défini dans le Spring Code application.properties sous la propriété spring.jersey.applicationPath. Ce sera le chemin racine, comme si vous utilisiez @ApplicationPath dans votre sous-classe ResourceConfig.

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

Une chose à noter est que la charge au démarrage n'est pas définie par défaut sur le servlet Jersey. Cela signifie que Jersey ne se chargera pas au démarrage jusqu'à la première demande. Vous ne verrez donc pas l'auditeur déclenché avant la première demande. J'ai ouvert un problème pour obtenir une propriété de configuration, mais entre-temps, vous avez plusieurs options:

  1. Configurez Jersey en tant que filtre au lieu d'une servlet. Le filtre sera chargé au démarrage. Utiliser Jersey comme filtre, pour la plupart des publications, ne se comporte vraiment pas différemment. Pour le configurer, il vous suffit d’ajouter une propriété Spring Boot dans le application.properties

    spring.jersey.type=filter
    
  2. L'autre option consiste à remplacer Jersey ServletRegistrationBean et à définir sa propriété loadOnStartup. Voici un exemple de configuration. Une partie de l'implémentation a été prise directement de la 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());
            }
        }
    }
    

METTRE À JOUR

Il semble donc que Spring Boot va ajouter la propriété load-on-startup , de sorte que nous n'avons pas à remplacer la variable Jersey ServletRegistrationBean. Sera ajouté dans Boot 1.4.0

11
Paul Samsotha

Une fois l’application entièrement lancée, vous pouvez demander ServerConfig:

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

classes contient toutes les classes de noeud final mises en cache.

Depuis les documents de l'API pour javax.ws.rs.core.Configuration:

Obtenez le jeu immuable de classes de composant JAX-RS enregistrées (telles que fournisseur ou entité) à instancier, à injecter et à utiliser dans le cadre de l'instance configurable. 

Cependant, vous ne pouvez pas faire cela dans le code init de votre application, les classes ne sont peut-être pas encore entièrement chargées.

Avec les classes, vous pouvez les analyser pour les ressources:

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;
}
1
Johannes Jander

Pouvez-vous utiliser ResourceConfig#getResources sur votre objet ResourceConfig puis obtenir les informations dont vous avez besoin en parcourant le Set<Resource> il revient?

Toutes mes excuses, essayez, mais je n'ai pas le Resources pour le faire maintenant. :-p

1
Carlos Bribiescas

Pourquoi ne pas utiliser RequestMappingHandlerMapping qui contient toutes les informations sur les terminaux.

Voir ma réponse à Comment accéder à toutes les routes disponibles d'une API REST à partir d'un contrôleur? .

0
Thomas Decaux