Mais si vous pouviez créer une interface, par exemple, comme ceci:
@Service public interface GoogleSearchApi { @Uri("https://www.google.com") int mainPageStatus(); }
Et puis il suffit de l'injecter et d'appeler ses méthodes:
@SpringBootApplication public class App implements CommandLineRunner { private static final Logger LOG = LoggerFactory.getLogger(App.class); private final GoogleSearchApi api; public App(GoogleSearchApi api) { this.api = api; } @Override public void run(String... args) { LOG.info("Main page status: " + api.mainPageStatus()); } public static void main(String[] args) { SpringApplication.run(App.class, args); } }
Ceci est tout Ă fait possible Ă mettre en Ćuvre (et pas trĂšs difficile). Ensuite, je montrerai comment et pourquoi le faire.
RĂ©cemment, j'ai eu la tĂąche de simplifier l'interaction des dĂ©veloppeurs avec l'un des frameworks utilisĂ©s. Il Ă©tait nĂ©cessaire de leur donner un moyen encore plus simple et plus pratique de travailler avec lui que celui qui avait dĂ©jĂ Ă©tĂ© mis en Ćuvre.
Propriétés que je souhaitais obtenir d'une telle solution:
- description déclarative de l'action souhaitée
- quantité minimale de code nécessaire
- intégration avec le framework d'injection de dépendances utilisé (dans notre cas, Spring)
Ceci est implĂ©mentĂ© dans les bibliothĂšques Spring Data Repository et Retrofit . L'utilisateur y dĂ©crit l'interaction souhaitĂ©e sous la forme d'une interface java, complĂ©tĂ©e d'annotations. L'utilisateur n'a pas besoin d'Ă©crire lui-mĂȘme l'implĂ©mentation - la bibliothĂšque la gĂ©nĂšre lors de l'exĂ©cution en fonction des signatures des mĂ©thodes, annotations et types.
Lorsque j'ai étudié le sujet, j'ai eu beaucoup de questions, dont les réponses ont été dispersées sur Internet. à ce moment, un article comme celui-ci ne me ferait pas de mal. Par conséquent, ici, j'ai essayé de rassembler toutes les informations et mon expérience en un seul endroit.
Dans cet article, je montrerai comment vous pouvez implĂ©menter cette idĂ©e, en utilisant l'exemple d'un wrapper pour un client http. Un exemple de jouet, conçu non pas pour une utilisation rĂ©elle, mais pour dĂ©montrer l'approche. Le code source du projet peut ĂȘtre Ă©tudiĂ© sur bitbucket .
Ă quoi cela ressemble-t-il pour l'utilisateur
L'utilisateur dĂ©crit le service dont il a besoin sous la forme d'une interface. Par exemple, pour effectuer des requĂȘtes http sur google:
@Service public interface GoogleSearchApi { @Uri("https://www.google.com") int mainPageStatus(); @Uri("https://www.google.com") HttpGet mainPageRequest(); @Uri("https://www.google.com/search?q={query}") CloseableHttpResponse searchSomething(String query); @Uri("https://www.google.com/doodles/?q={query}&hl={language}") int searchDoodleStatus(String query, String language); }
La finalitĂ© de l'implĂ©mentation de cette interface est dĂ©terminĂ©e par la signature. Si le type de retour est int, une requĂȘte http sera exĂ©cutĂ©e et le statut sera retournĂ© sous forme de code de rĂ©sultat. Si le type de retour est CloseableHttpResponse, la rĂ©ponse entiĂšre sera retournĂ©e, etc. Lorsque la demande sera faite, nous prendrons l'URI de l'annotation, en substituant les mĂȘmes valeurs transfĂ©rĂ©es au lieu des espaces rĂ©servĂ©s dans son contenu.
Dans cet exemple, je me suis limité à prendre en charge trois types de retour et une annotation. Vous pouvez également utiliser des noms de méthode, des types de paramÚtres pour choisir une implémentation, utiliser toutes sortes de combinaisons d'entre eux, mais je n'ouvrirai pas cette rubrique dans cet article.
Lorsqu'un utilisateur souhaite utiliser cette interface, il l'intĂšgre dans son code Ă l'aide de Spring:
@SpringBootApplication public class App implements CommandLineRunner { private static final Logger LOG = LoggerFactory.getLogger(App.class); private final GoogleSearchApi api; public App(GoogleSearchApi api) { this.api = api; } @Override @SneakyThrows public void run(String... args) { LOG.info("Main page status: " + api.mainPageStatus()); LOG.info("Main page request: " + api.mainPageRequest()); LOG.info("Doodle search status: " + api.searchDoodleStatus("tesla", "en")); try (CloseableHttpResponse response = api.searchSomething("qweqwe")) { LOG.info("Search result " + response); } } public static void main(String[] args) { SpringApplication.run(App.class, args); } }
L'intégration avec Spring était nécessaire dans mon projet de travail, mais ce n'est bien sûr pas le seul possible. Si vous n'utilisez pas l'injection de dépendance, vous pouvez obtenir l'implémentation, par exemple, via la méthode d'usine statique. Mais dans cet article, je considérerai le printemps.
Cette approche est trĂšs pratique: il suffit de marquer votre interface en tant que composant de Spring (annotation de service dans ce cas), et elle est prĂȘte Ă ĂȘtre implĂ©mentĂ©e et utilisĂ©e.
Comment amener Spring Ă soutenir cette magie
Une application Spring typique analyse le chemin de classe au démarrage et recherche tous les composants marqués d'annotations spéciales. Pour eux, il enregistre BeanDefinitions, des recettes par lesquelles ces composants seront créés. Mais si dans le cas de classes concrÚtes, Spring sait comment les créer, quels constructeurs appeler et quoi y passer, alors pour les classes abstraites et les interfaces, il ne dispose pas de telles informations. Par conséquent, pour notre GoogleSearchApi Spring ne créera pas BeanDefinition. Pour cela, il aura besoin de notre aide.
Afin de terminer la logique de traitement des BeanDefinitions, il existe une interface BeanDefinitionRegistryPostProcessor au printemps. Avec lui, nous pouvons ajouter à BeanDefinitionRegistry toute définition de beans que nous voulons.
Malheureusement, je n'ai pas trouvé de moyen d'intégrer dans la logique Spring du scan de chemin de classe afin de traiter à la fois les beans ordinaires et nos interfaces en une seule passe. Par conséquent, j'ai créé et utilisé le descendant de la classe ClassPathScanningCandidateComponentProvider pour trouver toutes les interfaces marquées avec l'annotation Service:
Code de numérisation de package complet et enregistrement de BeanDefinitions:
DynamicProxyBeanDefinitionRegistryPostProcessor @Component public class DynamicProxyBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
C'est fait! Au démarrage de l'application, Spring exécutera ce code et enregistrera toutes les interfaces nécessaires, comme les beans.
La création d'une implémentation des beans trouvés est déléguée à un composant distinct de DynamicProxyBeanFactory:
@Component(DYNAMIC_PROXY_BEAN_FACTORY) public class DynamicProxyBeanFactory { public static final String DYNAMIC_PROXY_BEAN_FACTORY = "repositoryProxyBeanFactory"; private final DynamicProxyInvocationHandlerDispatcher proxy; public DynamicProxyBeanFactory(DynamicProxyInvocationHandlerDispatcher proxy) { this.proxy = proxy; } @SuppressWarnings("unused") public <T> T createDynamicProxyBean(Class<T> beanClass) {
Pour créer l'implémentation, le bon vieux mécanisme Dynamic Proxy est utilisé. Une implémentation est créée à la volée à l'aide de la méthode Proxy.newProxyInstance. Beaucoup d'articles ont déjà été écrits à son sujet, donc je ne m'attarderai pas ici sur les détails.
Trouver le bon gestionnaire et le traitement des appels
Comme vous pouvez le voir, DynamicProxyBeanFactory redirige le traitement de la mĂ©thode vers DynamicProxyInvocationHandlerDispatcher. Ătant donnĂ© que nous avons potentiellement de nombreuses implĂ©mentations de gestionnaires (pour chaque annotation, pour chaque type renvoyĂ©, etc.), il est logique d'Ă©tablir un emplacement central pour leur stockage et recherche.
Afin de déterminer si le gestionnaire convient au traitement de la méthode appelée, j'ai développé l'interface standard InvocationHandler avec une nouvelle méthode
public interface HandlerMatcher { boolean canHandle(Method method); } public interface ProxyInvocationHandler extends InvocationHandler, HandlerMatcher { }
Le résultat est l'interface ProxyInvocationHandler, dont les implémentations seront nos gestionnaires. De plus, les implémentations de gestionnaire seront marquées comme composant afin que Spring puisse les collecter pour nous dans une grande liste à l'intérieur de DynamicProxyInvocationHandlerDispatcher:
DynamicProxyInvocationHandlerDispatcher package com.bachkovsky.dynproxy.lib.proxy; import lombok.SneakyThrows; import org.springframework.stereotype.Component; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.util.List; @Component public class DynamicProxyInvocationHandlerDispatcher implements InvocationHandler { private final List<ProxyInvocationHandler> proxyHandlers; public DynamicProxyInvocationHandlerDispatcher(List<ProxyInvocationHandler> proxyHandlers) { this.proxyHandlers = proxyHandlers; } @Override public Object invoke(Object proxy, Method method, Object[] args) { switch (method.getName()) {
Dans la mĂ©thode findHandler, nous passons en revue tous les gestionnaires et retournons le premier qui peut gĂ©rer la mĂ©thode passĂ©e. Ce mĂ©canisme de recherche peut ne pas ĂȘtre trĂšs efficace lorsqu'il existe de nombreuses implĂ©mentations de gestionnaires. Vous devrez peut-ĂȘtre alors penser Ă une structure plus appropriĂ©e pour les stocker quâune liste.
Implémentation du gestionnaire
Les tĂąches des gestionnaires incluent la lecture d'informations sur la mĂ©thode appelĂ©e de l'interface et le traitement de l'appel lui-mĂȘme.
Que doit faire le gestionnaire dans ce cas:
- Lire l'annotation Uri, obtenir son contenu
- Remplacer les espaces réservés Uri dans la chaßne par des valeurs réelles
- Type de retour de méthode de lecture
- Si le type de retour convient, traitez la méthode et renvoyez le résultat.
Les trois premiers points sont nécessaires pour tous les types retournés, j'ai donc mis le code général dans une superclasse abstraite
HttpInvocationHandler:
public abstract class HttpInvocationHandler implements ProxyInvocationHandler { final HttpClient client; private final UriHandler uriHandler; HttpInvocationHandler(HttpClient client, UriHandler uriHandler) { this.client = client; this.uriHandler = uriHandler; } @Override public boolean canHandle(Method method) { return uriHandler.canHandle(method); } final String getUri(Method method, Object[] args) { return uriHandler.getUriString(method, args); } }
La classe d'assistance UriHandler implémente le travail avec l'annotation Uri: lecture de valeurs, remplacement des espaces réservés. Je ne donnerai pas le code ici, car c'est assez utilitaire.
Mais il convient de noter que pour lire les noms de paramÚtres à partir de la signature de la méthode java, vous devez ajouter l'option "-parameters" lors de la compilation .
HttpClient - un wrapper sur Apachevsky CloseableHttpClient, est un backend pour cette bibliothĂšque.
à titre d'exemple d'un gestionnaire spécifique, je vais donner un gestionnaire qui renvoie un code de réponse d'état:
@Component public class HttpCodeInvocationHandler extends HttpInvocationHandler { public HttpCodeInvocationHandler(HttpClient client, UriHandler uriHandler) { super(client, uriHandler); } @Override @SneakyThrows public Integer invoke(Object proxy, Method method, Object[] args) { try (CloseableHttpResponse resp = client.execute(new HttpGet(getUri(method, args)))) { return resp.getStatusLine().getStatusCode(); } } @Override public boolean canHandle(Method method) { return super.canHandle(method) && method.getReturnType().equals(int.class); } }
Les autres gestionnaires sont fabriquĂ©s de la mĂȘme maniĂšre. L'ajout de nouveaux gestionnaires est simple et ne nĂ©cessite pas de modification du code existant - crĂ©ez simplement un nouveau gestionnaire et marquez-le en tant que composant Spring.
Câest tout. Le code est Ă©crit et prĂȘt Ă l'emploi.
Conclusion
Plus je pense à un tel design, plus j'y vois de défauts. Faiblesses que je constate:
- Tapez SĂ©curitĂ©, ce qui n'est pas le cas. DĂ©finissez incorrectement l'annotation - avant de rencontrer RuntimeException. UtilisĂ© la mauvaise combinaison de type de retour et d'annotation - mĂȘme chose.
- Faible support de l'IDE. Manque d'auto-complétion. L'utilisateur ne peut pas voir quelles actions sont disponibles pour lui dans sa situation (comme s'il avait mis un «point» aprÚs l'objet et vu une liste des méthodes disponibles)
- Il y a peu de possibilitĂ©s d'application. Le client http dĂ©jĂ mentionnĂ© vient Ă l'esprit et le client va Ă la base de donnĂ©es. Mais pourquoi cela peut-il ĂȘtre appliquĂ© autrement?
Cependant, dans mon projet de travail, l'approche a pris racine et est populaire. Les avantages que j'ai déjà mentionnés - la simplicité, une petite quantité de code, la déclarativité, permettent aux développeurs de se concentrer sur l'écriture de code plus important.
Que pensez-vous de cette approche? Vaut-il la peine? Quels problÚmes voyez-vous dans cette approche? Pendant que j'essaie encore de donner un sens à cela, alors qu'il est déployé dans notre production, j'aimerais entendre ce que les autres en pensent. J'espÚre que ce matériel a été utile à quelqu'un.