使用动态代理和Spring IoC创建自己的Spring Data Repository样式库

但是,例如,如果您可以创建一个接口,该怎么办:


@Service public interface GoogleSearchApi { /** * @return http status code for Google main page */ @Uri("https://www.google.com") int mainPageStatus(); } 

然后只需注入它并调用其方法:


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

这是很可能实现的(不是很困难)。 接下来,我将展示如何以及为什么这样做。


最近,我的任务是简化开发人员与使用的框架之一的交互。 与已经实施的方法相比,有必要给他们一种与他一起工作的更简单,更方便的方法。


我想通过这样的解决方案实现的属性:


  • 所需动作的说明性描述
  • 最少的代码量
  • 与使用的依赖注入框架集成(在我们的例子中是Spring)

这在Spring Data RepositoryRetrofit库中实现。 在其中,用户以java接口的形式描述所需的交互,并通过注释进行补充。 用户不需要自己编写实现-库在运行时根据方法,注释和类型的签名生成实现。


当我研究该主题时,我遇到了很多问题,答案遍布整个Internet。 那时,这样的文章不会伤害我。 因此,在这里我试图将所有信息和我的经验收集到一个地方。


在这篇文章中,我将使用http客户端的包装器示例演示如何实现此想法。 玩具的一个例子,不是为实际使用而设计的,而是为了演示这种方法。 该项目的代码可以在bitbucket上找到


它如何寻找用户


用户以界面的形式描述他需要的服务。 例如,要在Google上执行http请求:


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

该接口的实现最终将由签名决定。 如果返回类型为int,则将执行http请求,并将状态作为结果代码返回。 如果返回类型为CloseableHttpResponse,则将返回整个响应,依此类推。 在将提出请求的地方,我们将从Uri注释中获取,在其内容中替换相同的传输值而不是占位符。


在此示例中,我仅限于支持三种返回类型和一种注释。 您还可以使用方法名称,参数类型来选择实现,并使用它们的各种组合,但是在本文中,我不会打开本主题。


当用户想要使用此接口时,可以使用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); } } 

在我的工作项目中需要与Spring集成,但这当然不是唯一的可能。 如果不使用依赖项注入,则可以例如通过静态工厂方法获得实现。 但是在本文中,我将考虑Spring。


这种方法非常方便:只需将您的接口标记为Spring的一个组件(在本例中为Service注释),就可以实现和使用了。


如何让Spring支持这种魔术


一个典型的Spring应用程序在启动时会扫描类路径,并查找所有标有特殊注释的组件。 对于它们,它注册BeanDefinitions,这些组件将通过其创建配方。 但是,如果在具体类的情况下,Spring知道如何创建它们,调用哪些构造函数以及将其传递给什么,那么对于抽象类和接口,它没有此类信息。 因此,对于我们的GoogleSearchApi,Spring不会创建BeanDefinition。 为此,他将需要我们的帮助。


为了完成处理BeanDefinitions的逻辑,在春天有一个BeanDefinitionRegistryPostProcessor接口。 有了它,我们可以将所需的任何bean定义添加到BeanDefinitionRegistry中。


不幸的是,我没有找到一种方法可以集成到类路径扫描的Spring逻辑中,以便一次性处理普通bean和我们的接口。 因此,我创建并使用了ClassPathScanningCandidateComponentProvider类的后代来查找带有Service批注的所有接口:


完整的软件包扫描代码和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(); } } } 

做完了! 在应用程序启动时,Spring将执行此代码并注册所有必需的接口,例如bean。


创建找到的bean的实现将委托给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); } } 

为了创建实现,使用了良好的旧动态代理机制。 使用Proxy.newProxyInstance方法动态创建一个实现。 关于他的文章已经写了很多,因此在此我不再赘述。


找到合适的处理程序并进行呼叫处理


如您所见,DynamicProxyBeanFactory将方法处理重定向到DynamicProxyInvocationHandlerDispatcher。 由于我们可能有许多处理程序的实现(对于每个注释,对于每种返回类型等),因此为它们的存储和搜索建立一些中心位置是合乎逻辑的。


为了确定处理程序是否适合处理被调用的方法,我使用新方法扩展了标准InvocationHandler接口。


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

结果是ProxyInvocationHandler接口,该接口的实现将成为我们的处理程序。 而且,处理程序实现将被标记为Component,以便Spring可以为我们将它们收集到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)); } } 

在findHandler方法中,我们遍历所有处理程序并返回第一个可以处理传递的方法的处理程序。 当有许多处理程序实现时,此搜索机制可能不是很有效。 也许那您需要考虑一些比列表更合适的结构来存储它们。


处理程序实施


处理程序的任务包括读取有关接口的被调用方法的信息并处理调用本身。


在这种情况下,处理程序应该做什么:


  1. 阅读Uri批注,获取其内容
  2. 用实际值替换字符串中的Uri占位符
  3. 读取方法返回类型
  4. 如果返回类型合适,则处理该方法并返回结果。

所有返回的类型都需要前三点,因此我将通用代码放入了一个抽象超类中
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); } } 

UriHandler帮助程序类使用Uri注释实现工作:读取值,替换占位符。 我不会在这里给出代码,因为 这是相当功利的。
但是值得注意的是,要从java方法的签名中读取参数名称, 在编译时需要添加选项“ -parameters”
HttpClient-Apachevsky CloseableHttpClient的包装,是该库的后端。


作为特定处理程序的示例,我将提供一个返回状态响应代码的处理程序:


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

其他处理程序也类似。 添加新的处理程序很简单,不需要修改现有代码-只需创建一个新的处理程序并将其标记为Spring组件即可。


仅此而已。 代码已编写并可以使用。


结论


我对这种设计的思考越多,看到的缺陷就越多。 我看到的弱点:


  • 键入安全,不是。 错误地设置了注释-在遇到RuntimeException之前。 使用了错误的返回类型和注释组合-同样的事情。
  • IDE的支持不足。 缺乏自动完成功能。 用户无法看到在他所处的情况下可以执行哪些操作(就像他在对象后放置一个“点”并看到了可用方法的列表)
  • 应用的可能性很小。 想到了已经提到过的http客户端,该客户端转到了数据库。 但是为什么还可以应用呢?

但是,在我的工作草案中,这种方法已经扎根并且很流行。 我已经提到的优点-简单,代码量少,声明性强,使开发人员可以专注于编写更重要的代码。


您如何看待这种方法? 值得付出努力吗? 您在这种方法中看到什么问题? 尽管我仍在努力理解它,但在我们的产品中正在使用它时,我想听听其他人对此有何看法。 我希望这些材料对某人有用。

Source: https://habr.com/ru/post/zh-CN458548/


All Articles