Pero, ¿qué pasaría si pudieras crear una interfaz, por ejemplo, así:
@Service public interface GoogleSearchApi { @Uri("https://www.google.com") int mainPageStatus(); }
Y luego simplemente inyectarlo y llamar a sus métodos:
@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); } }
Esto es bastante posible de implementar (y no muy difícil). A continuación, mostraré cómo y por qué hacerlo.
Recientemente, tuve la tarea de simplificar la interacción de los desarrolladores con uno de los marcos utilizados. Era necesario darles una forma aún más simple y conveniente de trabajar con él que la que ya se había implementado.
Propiedades que quería obtener de tal solución:
- Descripción declarativa de la acción deseada.
- cantidad mínima de código necesaria
- integración con el marco de inyección de dependencia utilizado (en nuestro caso, Spring)
Esto se implementa en las bibliotecas Spring Data Repository y Retrofit . En ellos, el usuario describe la interacción deseada en forma de una interfaz java, complementada con anotaciones. El usuario no necesita escribir la implementación él mismo: la biblioteca la genera en tiempo de ejecución en función de las firmas de métodos, anotaciones y tipos.
Cuando estudié el tema, tenía muchas preguntas, cuyas respuestas estaban dispersas por Internet. En ese momento, un artículo como este no me haría daño. Por lo tanto, aquí traté de recopilar toda la información y mi experiencia en un solo lugar.
En esta publicación, le mostraré cómo puede implementar esta idea, utilizando el ejemplo de un contenedor para un cliente http. Un ejemplo de un juguete, diseñado no para uso real, sino para demostrar el enfoque. El código fuente del proyecto se puede estudiar en bitbucket .
¿Cómo se ve el usuario?
El usuario describe el servicio que necesita en forma de interfaz. Por ejemplo, para realizar solicitudes http en 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); }
Lo que finalmente hará la implementación de esta interfaz está determinado por la firma. Si el tipo de retorno es int, se ejecutará una solicitud http y el estado se devolverá como un código de resultado. Si el tipo de retorno es CloseableHttpResponse, se devolverá toda la respuesta, y así sucesivamente. Cuando se realice la solicitud, tomaremos el Uri de la anotación, sustituyendo los mismos valores transferidos en lugar de marcadores de posición en su contenido.
En este ejemplo, me limité a admitir tres tipos de retorno y una anotación. También puede usar nombres de métodos, tipos de parámetros para elegir una implementación, usar todo tipo de combinaciones de ellos, pero no abriré este tema en esta publicación.
Cuando un usuario quiere usar esta interfaz, la incrusta en su código usando 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); } }
La integración con Spring era necesaria en mi proyecto de trabajo, pero, por supuesto, no es la única posible. Si no utiliza la inyección de dependencia, puede obtener la implementación, por ejemplo, a través del método estático de fábrica. Pero en este artículo consideraré la primavera.
Este enfoque es muy conveniente: simplemente marque su interfaz como un componente de Spring (anotación de servicio en este caso), y está listo para su implementación y uso.
Cómo hacer que Spring apoye esta magia
Una aplicación típica de Spring escanea el classpath al inicio y busca todos los componentes marcados con anotaciones especiales. Para ellos, registra BeanDefinitions, recetas mediante las cuales se crearán estos componentes. Pero si en el caso de las clases concretas, Spring sabe cómo crearlas, a qué constructores llamar y qué pasar en ellas, entonces para las clases abstractas e interfaces no tiene esa información. Por lo tanto, para nuestro GoogleSearchApi Spring no creará BeanDefinition. En esto necesitará ayuda de nosotros.
Para finalizar la lógica de procesamiento de BeanDefinitions, hay una interfaz BeanDefinitionRegistryPostProcessor en la primavera. Con él, podemos agregar a BeanDefinitionRegistry cualquier definición de beans que queramos.
Desafortunadamente, no encontré una manera de integrarme en la lógica de Spring del análisis classpath para procesar tanto beans comunes como nuestras interfaces en una sola pasada. Por lo tanto, creé y utilicé el descendiente de la clase ClassPathScanningCandidateComponentProvider para encontrar todas las interfaces marcadas con la anotación del Servicio:
Código de escaneo de paquete completo y registro de BeanDefinitions:
DynamicProxyBeanDefinitionRegistryPostProcessor @Component public class DynamicProxyBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
Hecho Al comienzo de la aplicación, Spring ejecutará este código y registrará todas las interfaces necesarias, como beans.
La creación de una implementación de los beans encontrados se delega a un componente separado 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) {
Para crear la implementación, se utiliza el antiguo mecanismo de proxy dinámico. Se crea una implementación sobre la marcha utilizando el método Proxy.newProxyInstance. Ya se han escrito muchos artículos sobre él, por lo que no me detendré aquí en detalle.
Encontrar el controlador adecuado y el procesamiento de llamadas
Como puede ver, DynamicProxyBeanFactory redirige el procesamiento del método a DynamicProxyInvocationHandlerDispatcher. Dado que tenemos potencialmente muchas implementaciones de controladores (para cada anotación, para cada tipo devuelto, etc.), es lógico establecer un lugar central para su almacenamiento y búsqueda.
Para determinar si el controlador es adecuado para procesar el método llamado, amplié la interfaz estándar InvocationHandler con un nuevo método
public interface HandlerMatcher { boolean canHandle(Method method); } public interface ProxyInvocationHandler extends InvocationHandler, HandlerMatcher { }
El resultado es la interfaz ProxyInvocationHandler, cuyas implementaciones serán nuestros controladores. Además, las implementaciones del controlador se marcarán como Componente para que Spring pueda recopilarlas para nosotros en una gran lista dentro 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()) {
En el método findHandler, revisamos todos los manejadores y devolvemos el primero que puede manejar el método pasado. Este mecanismo de búsqueda puede no ser muy efectivo cuando hay muchas implementaciones de controladores. Quizás entonces deba pensar en una estructura más adecuada para almacenarlos que una lista.
Implementación del controlador
Las tareas de los manejadores incluyen leer información sobre el método llamado de la interfaz y procesar la llamada en sí.
¿Qué debe hacer el controlador en este caso?
- Lea la anotación de Uri, obtenga su contenido
- Reemplazar marcadores de posición Uri en la cadena con valores reales
- Tipo de retorno del método de lectura
- Si el tipo de retorno es adecuado, procese el método y devuelva el resultado.
Los primeros tres puntos son necesarios para todos los tipos devueltos, así que puse el código general en una superclase abstracta
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 clase auxiliar UriHandler implementa el trabajo con la anotación Uri: lectura de valores, reemplazo de marcadores de posición. No voy a dar el código aquí, porque Es bastante utilitario.
Pero vale la pena señalar que para leer los nombres de los parámetros de la firma del método java, debe agregar la opción "-parámetros" al compilar .
HttpClient: un contenedor sobre Apachevsky CloseableHttpClient, es un back-end para esta biblioteca.
Como ejemplo de un controlador específico, le daré un controlador que devuelve un código de respuesta de estado:
@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); } }
Otros manejadores se hacen de manera similar. Agregar nuevos controladores es simple y no requiere la modificación del código existente: simplemente cree un nuevo controlador y márquelo como un componente Spring.
Eso es todo El código está escrito y listo para funcionar.
Conclusión
Cuanto más pienso en ese diseño, más veo defectos en él. Debilidades que veo:
- Escriba Seguridad, que no lo es. Establezca incorrectamente la anotación, antes de reunirse con RuntimeException. Usó la combinación incorrecta de tipo de retorno y anotación, lo mismo.
- Débil apoyo del IDE. Falta de autocompletado. El usuario no puede ver qué acciones están disponibles para él en su situación (como si pusiera un "punto" después del objeto y viera una lista de métodos disponibles)
- Hay pocas posibilidades de aplicación. Me viene a la mente el cliente http ya mencionado y el cliente va a la base de datos. ¿Pero por qué más se puede aplicar esto?
Sin embargo, en mi borrador de trabajo, el enfoque ha echado raíces y es popular. Las ventajas que ya mencioné: simplicidad, una pequeña cantidad de código, capacidad de declaración, permiten a los desarrolladores concentrarse en escribir código más importante.
¿Qué opinas sobre este enfoque? ¿Vale la pena el esfuerzo? ¿Qué problemas ves en este enfoque? Si bien todavía estoy tratando de darle sentido, mientras se está implementando en nuestra producción, me gustaría escuchar lo que otras personas piensan al respecto. Espero que este material sea útil para alguien.