Créez votre propre bibliothÚque de styles Spring Data Repository avec Dynamic Proxy et Spring IoC

Mais si vous pouviez créer une interface, par exemple, comme ceci:


@Service public interface GoogleSearchApi { /** * @return http status code for Google main page */ @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:


 /** * Some Google requests */ @Service public interface GoogleSearchApi { /** * @return http status code for Google main page */ @Uri("https://www.google.com") int mainPageStatus(); /** * @return request object for Google main page */ @Uri("https://www.google.com") HttpGet mainPageRequest(); /** * @param query search query * @return result of search request execution */ @Uri("https://www.google.com/search?q={query}") CloseableHttpResponse searchSomething(String query); /** * @param query doodle search query * @param language doodle search language * @return http status code for doodle search result */ @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 { // ,     private static final String[] SCAN_PACKAGES = {"com"}; private final InterfaceScanner classpathScanner; public DynamicProxyBeanDefinitionRegistryPostProcessor() { classpathScanner = new InterfaceScanner(); //   .      Service classpathScanner.addIncludeFilter(new AnnotationTypeFilter(Service.class)); } @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { for (String basePackage : SCAN_PACKAGES) { createRepositoryProxies(basePackage, registry); } } @SneakyThrows private void createRepositoryProxies(String basePackage, BeanDefinitionRegistry registry) { for (BeanDefinition beanDefinition : classpathScanner.findCandidateComponents(basePackage)) { Class<?> clazz = Class.forName(beanDefinition.getBeanClassName()); //      bean definition BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(clazz); builder.addConstructorArgValue(clazz); //,          builder.setFactoryMethodOnBean( "createDynamicProxyBean", DynamicProxyBeanFactory.DYNAMIC_PROXY_BEAN_FACTORY ); registry.registerBeanDefinition(ClassUtils.getShortNameAsProperty(clazz), builder.getBeanDefinition()); } } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { } private static class InterfaceScanner extends ClassPathScanningCandidateComponentProvider { InterfaceScanner() { super(false); } @Override protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { return beanDefinition.getMetadata().isInterface(); } } } 

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) { //noinspection unchecked return (T) Proxy.newProxyInstance(beanClass.getClassLoader(), new Class[]{beanClass}, proxy); } } 

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 { /** * @return {@code true} if handler is able to handle given method, {@code false} othervise */ 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; /** * Top level dynamic proxy invocation handler, which finds correct implementation based and uses it for method * invocation */ @Component public class DynamicProxyInvocationHandlerDispatcher implements InvocationHandler { private final List<ProxyInvocationHandler> proxyHandlers; /** * @param proxyHandlers all dynamic proxy handlers found in app context */ public DynamicProxyInvocationHandlerDispatcher(List<ProxyInvocationHandler> proxyHandlers) { this.proxyHandlers = proxyHandlers; } @Override public Object invoke(Object proxy, Method method, Object[] args) { switch (method.getName()) { // three Object class methods don't have default implementation after creation with Proxy::newProxyInstance case "hashCode": return System.identityHashCode(proxy); case "toString": return proxy.getClass() + "@" + System.identityHashCode(proxy); case "equals": return proxy == args[0]; default: return doInvoke(proxy, method, args); } } @SneakyThrows private Object doInvoke(Object proxy, Method method, Object[] args) { return findHandler(method).invoke(proxy, method, args); } private ProxyInvocationHandler findHandler(Method method) { return proxyHandlers.stream() .filter(h -> h.canHandle(method)) .findAny() .orElseThrow(() -> new IllegalStateException("No handler was found for method: " + method)); } } 

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:


  1. Lire l'annotation Uri, obtenir son contenu
  2. Remplacer les espaces réservés Uri dans la chaßne par des valeurs réelles
  3. Type de retour de méthode de lecture
  4. 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.

Source: https://habr.com/ru/post/fr458548/


All Articles