Mas e se você pudesse criar uma interface, por exemplo, assim:
@Service public interface GoogleSearchApi { @Uri("https://www.google.com") int mainPageStatus(); }
E então apenas injete e chame seus 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); } }
Isso é bem possível de implementar (e não muito difícil). A seguir, mostrarei como e por que fazê-lo.
Recentemente, tive a tarefa de simplificar a interação dos desenvolvedores com uma das estruturas usadas. Era necessário dar a eles uma maneira ainda mais simples e conveniente de trabalhar com ele do que a que já havia sido implementada.
Propriedades que eu queria obter com essa solução:
- descrição declarativa da ação desejada
- quantidade mínima de código necessária
- integração com a estrutura de injeção de dependência usada (no nosso caso, Spring)
Isso é implementado nas bibliotecas Spring Data Repository e Retrofit . Neles, o usuário descreve a interação desejada na forma de uma interface java, complementada por anotações. O usuário não precisa escrever a implementação pessoalmente - a biblioteca a gera em tempo de execução com base nas assinaturas de métodos, anotações e tipos.
Quando estudei o tópico, tive muitas perguntas, cujas respostas estavam espalhadas pela Internet. Naquele momento, um artigo como esse não me machucaria. Portanto, aqui tentei coletar todas as informações e minha experiência em um só lugar.
Neste post, mostrarei como você pode implementar essa ideia, usando o exemplo de um wrapper para um cliente http. Um exemplo de brinquedo, projetado não para uso real, mas para demonstrar a abordagem. O código fonte do projeto pode ser estudado no bitbucket .
Como é a aparência do usuário
O usuário descreve o serviço que ele precisa na forma de uma interface. Por exemplo, para executar solicitações http no 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); }
O que a implementação dessa interface fará, em última análise, é determinado pela assinatura. Se o tipo de retorno for int, uma solicitação http será executada e o status será retornado como um código de resultado. Se o tipo de retorno for CloseableHttpResponse, a resposta inteira será retornada e assim por diante. Onde a solicitação será feita, tiraremos o Uri da anotação, substituindo os mesmos valores transferidos em vez de espaços reservados em seu conteúdo.
Neste exemplo, me limitei a oferecer suporte a três tipos de retorno e uma anotação. Você também pode usar nomes de métodos, tipos de parâmetros para escolher uma implementação, usar todos os tipos de combinações deles, mas não vou abrir este tópico neste post.
Quando um usuário deseja usar essa interface, ele a incorpora em seu código usando o 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); } }
A integração com o Spring era necessária no meu projeto de trabalho, mas, é claro, não é o único possível. Se você não usar a injeção de dependência, poderá obter a implementação, por exemplo, através do método estático de fábrica. Mas neste artigo vou considerar a primavera.
Essa abordagem é muito conveniente: basta marcar sua interface como um componente do Spring (neste caso, anotação de serviço) e ela está pronta para implementação e uso.
Como obter o Spring para apoiar essa mágica
Um aplicativo Spring típico varre o caminho de classe na inicialização e procura todos os componentes marcados com anotações especiais. Para eles, ele registra BeanDefinitions, receitas pelas quais esses componentes serão criados. Mas se, no caso de classes concretas, o Spring sabe como criá-las, quais construtores chamar e o que passar nelas, então, para classes e interfaces abstratas, ela não possui essas informações. Portanto, para o GoogleSearchApi Spring, não será criado o BeanDefinition. Nisto, ele precisará de ajuda de nós.
Para concluir a lógica do processamento de BeanDefinitions, há uma interface BeanDefinitionRegistryPostProcessor na primavera. Com ele, podemos adicionar ao BeanDefinitionRegistry qualquer definição de beans que desejarmos.
Infelizmente, não encontrei uma maneira de integrar a lógica Spring da verificação do caminho de classe para processar os beans comuns e nossas interfaces em uma única passagem. Portanto, criei e usei o descendente da classe ClassPathScanningCandidateComponentProvider para encontrar todas as interfaces marcadas com a anotação Service:
Código completo de verificação do pacote e registro do BeanDefinitions:
DynamicProxyBeanDefinitionRegistryPostProcessor @Component public class DynamicProxyBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
Feito! No início do aplicativo, o Spring executará esse código e registrará todas as interfaces necessárias, como beans.
A criação de uma implementação dos beans encontrados é delegada a um componente separado do 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 criar a implementação, o bom e velho mecanismo de Proxy Dinâmico é usado. Uma implementação é criada dinamicamente usando o método Proxy.newProxyInstance. Muitos artigos já foram escritos sobre ele, então não vou me deter aqui em detalhes.
Localizando o manipulador certo e o processamento de chamadas
Como você pode ver, DynamicProxyBeanFactory redireciona o processamento do método para DynamicProxyInvocationHandlerDispatcher. Como temos potencialmente muitas implementações de manipuladores (para cada anotação, para cada tipo retornado etc.), é lógico estabelecer um local central para o armazenamento e a pesquisa.
Para determinar se o manipulador é adequado para processar o método chamado, expandi a interface InvocationHandler padrão com um novo método
public interface HandlerMatcher { boolean canHandle(Method method); } public interface ProxyInvocationHandler extends InvocationHandler, HandlerMatcher { }
O resultado é a interface ProxyInvocationHandler, cujas implementações serão nossos manipuladores. Além disso, as implementações do manipulador serão marcadas como Component, para que o Spring possa coletá-las para nós em uma grande lista no 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()) {
No método findHandler, examinamos todos os manipuladores e retornamos o primeiro que pode manipular o método passado. Esse mecanismo de pesquisa pode não ser muito eficaz quando há muitas implementações de manipulador. Talvez você precise pensar em alguma estrutura mais adequada para armazená-las do que em uma lista.
Implementação do manipulador
As tarefas dos manipuladores incluem a leitura de informações sobre o método chamado da interface e o processamento da própria chamada.
O que o manipulador deve fazer neste caso:
- Leia a anotação Uri, obtenha seu conteúdo
- Substitua os espaços reservados de Uri na string por valores reais
- Tipo de retorno do método de leitura
- Se o tipo de retorno for adequado, processe o método e retorne o resultado.
Os três primeiros pontos são necessários para todos os tipos retornados, então eu coloquei o código geral em uma superclasse abstrata
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); } }
A classe auxiliar UriHandler implementa o trabalho com a anotação Uri: lendo valores, substituindo espaços reservados. Eu não vou dar o código aqui, porque é bastante utilitário.
Mas é importante notar que, para ler os nomes dos parâmetros na assinatura do método java, é necessário adicionar a opção "-parameters" ao compilar .
HttpClient - um wrapper sobre o Apachevsky CloseableHttpClient, é um back-end para esta biblioteca.
Como exemplo de um manipulador específico, darei um manipulador que retorna um código de resposta de status:
@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); } }
Outros manipuladores são feitos de maneira semelhante. Adicionar novos manipuladores é simples e não requer modificação do código existente - basta criar um novo manipulador e marcá-lo como um componente Spring.
Isso é tudo. O código está escrito e pronto para ser usado.
Conclusão
Quanto mais eu penso sobre esse design, mais vejo falhas nele. Fraquezas que vejo:
- Digite Safety, que não é. Defina a anotação incorretamente - antes de encontrar o RuntimeException. Utilizou a combinação incorreta de tipo de retorno e anotação - a mesma coisa.
- Suporte fraco do IDE. Falta de preenchimento automático. O usuário não pode ver quais ações estão disponíveis para ele em sua situação (como se ele colocasse um "ponto" após o objeto e visse uma lista de métodos disponíveis)
- Existem poucas possibilidades de aplicação. O cliente http já mencionado vem à mente e o cliente vai para o banco de dados. Mas por que mais isso pode ser aplicado?
No entanto, no meu rascunho de trabalho, a abordagem criou raízes e é popular. As vantagens que eu já mencionei - simplicidade, uma pequena quantidade de código, declaratividade, permitem que os desenvolvedores se concentrem em escrever códigos mais importantes.
O que você acha dessa abordagem? Vale a pena o esforço? Que problemas você vê nessa abordagem? Enquanto eu ainda estou tentando entender, enquanto está sendo produzido em nossa produção, eu gostaria de ouvir o que as outras pessoas pensam sobre isso. Espero que este material tenha sido útil para alguém.