And what if you could create an interface, for example, like this:
@Service public interface GoogleSearchApi { /** * @return http status code for Google main page */ @Uri("https://www.google.com") int mainPageStatus(); }
And then just inject it and call its methods:
@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); } }
This is quite possible to implement (and not very difficult). Then I will show how and why to do it.
Recently, I had a task to simplify for developers the interaction with one of the frameworks used. It was necessary to give them an even simpler and more convenient way to work with it than the one that was already implemented.
Properties that I wanted to achieve from this solution:
This is implemented in the libraries of the Spring Data Repository and Retrofit . In them, the user describes the desired interaction in the form of a java interface, supplemented by annotations. The user does not need to write the implementation himself - it is generated by the library in runtime based on the signatures of methods, annotations and types.
When I studied the topic, I had many questions, the answers to which were scattered throughout the Internet. At that moment I would not be prevented by an article like this. Because here I tried to collect all the information and my experience in one place.
In this post, I will show how you can implement this idea, using the example of a wrapper for an http client. An example of a toy, designed not for real use, but to demonstrate the approach. The source code of the project can be studied on bitbucket .
The user describes the service he needs as an interface. For example, to perform http requests in 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); }
What the implementation of this interface will ultimately do is determined by the signature. If the return type is int, the http request will be executed and the result code will be returned. If the return type is CloseableHttpResponse, then the entire request will be returned, and so on. Where the request will be made - we will take the Uri from the annotation, substituting in the content instead of placeholders the same values ​​passed in.
In this example, I limited myself to the support of three return types and one annotation. You can also use method names, parameter types to choose from the implementation, I will not use any combination of them, but I will not reveal this topic in this post.
When a user wants to use this interface, he injects it into his code using 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); } }
Integration with Spring was needed in my working draft, but it is, of course, not the only possible one. If you do not use dependency injection, you can get an implementation, for example, through a static factory method. But I'm going to look at Spring in this article.
This approach is very convenient: just mark your interface as a Spring component (Service annotation in this case), and it is ready for implementation and use.
A typical Spring application scans the classpath at the start and searches for all components marked with special annotations. For them, it registers BeanDefinitions, the recipes by which these components will be created. But if, in the case of concrete classes, Spring knows how to create them, what constructors to call and what to pass into them, then he does not have such information for abstract classes and interfaces. Therefore, for our GoogleSearchApi Spring will not create BeanDefinition. In this he will need help from us.
In order to finish the logic processing of BeanDefinitions, there is a BeanDefinitionRegistryPostProcessor interface in the spring. Using it, we can add any bean definitions to BeanDefinitionRegistry.
Unfortunately, I did not find a way to integrate into the Spring logic of the scan classpath to process both normal beans and our interfaces in one pass. Therefore, I created and used an inheritor of the ClassPathScanningCandidateComponentProvider class to find all the interfaces annotated with the Service annotation:
The complete code for scanning packages and registering BeanDefinitions:
@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(); } } }
Done! At the start of the application, Spring will execute this code and register all the necessary interfaces as beans.
The creation of the implementation of the found beans is delegated to a separate DynamicProxyBeanFactory component:
@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); } }
To create the implementation of the good old mechanism used Dynamic Proxy. An implementation is created on the fly using the Proxy.newProxyInstance method. There are already a lot of articles written about him, so I will not dwell here in detail.
As you can see, the DynamicProxyBeanFactory redirects method processing to DynamicProxyInvocationHandlerDispatcher. Since we have potentially many implementations of handlers (for each annotation, for each return type, etc.), it is logical to find some central place for their storage and retrieval.
In order to determine whether the handler is suitable for handling the called method, I extended the standard InvocationHandler interface with a new method.
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 { }
The result is the ProxyInvocationHandler interface, the implementations of which will be our handlers. Also, handler implementations will be labeled as Component so that Spring can compile them for us into one large list inside the 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)); } }
In the findHandler method, we go through all the handlers and return the first one that is able to process the passed method. This search mechanism may not be very effective when there are many implementations of handlers. Perhaps, then you will need to think about some more suitable structure for their storage than the list.
The tasks of the handlers include reading information about the called method of the interface and processing the call itself.
What should the handler do in this case:
The first three items are needed for all return types, so I rendered the common code into an abstract superclass
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); } }
In the auxiliary class UriHandler, work with the Uri annotation is implemented: reading the value, replacing placeholders. I will not give you his code here, because it's pretty utility.
But it is worth noting that to read the names of the parameters from the signature of the java method, you need to add the option "-parameters" when compiling .
HttpClient - a wrapper over Apache's CloseableHttpClient, is a backend for this library.
As an example of a specific handler, here is a handler that returns the status of the response code:
@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); } }
The remaining handlers are made similarly. Adding new handlers is simple and does not require modification of the existing code - just create a new handler and mark it as a Spring component.
That's all. The code is written and ready to go.
The more I think about such a design, the more I see flaws in it. The weaknesses that I see are:
However, the approach has taken root in my work project and is popular. The advantages that I have already mentioned - simplicity, small amount of code, declarativeness, allow developers to concentrate on writing more important code.
What do you think about this approach? Is it worth the effort? What problems do you see in this approach? While I still try to comprehend it, while it is driven around in our production, I would like to hear what other people think about it. I hope this material was useful to someone.
Source: https://habr.com/ru/post/458548/
All Articles