Jmine Tec RPC

Introdução

O jmine-tec-rpc é um módulo do Jmine feito para realizar a comunição entre sistemas utilizando RPC (Remote Procedure Call). No caso do Jmine as chamadas de procedimento são feitas utilizando a técnica de REST que é sigla para REpresentational State Transfer, técnica que utiliza o protocolo HTTP e JSON/XML(entre outros) para troca de mensagens.

Externamente é comum se referir a “REST Services”. Essa nomenclatura não é recomendada no contexto jMine por gerar confusão com a infraestrutura de serviços utilizada para descrição de cenários e integrações. Recomenda-se, portanto, que apenas “REST” seja utilizado.

Em seu nível mais básico, um REST é criado implementando a interface RestService, que provê implementações para os métodos HTTP básicos: GET, POST, PUT, DELETE. Cabe à implementação interpretar a requisição e gerar como resposta um texto que deverá ser interpretado pelo cliente.

O acesso é feito pelo cliente através de uma requisição HTTP, cuja resposta deve ser interpretada adequadamente.

A implementação direta descrita possui como vantagem a total flexibilidade. Em contrapartida é necessário lidar quase que diretamente com chamadas HTTP, dificultando a implementação tanto do lado servidor quanto do lado do cliente. Além disso é necessário ter muito cuidado com mudanças, pois o código cliente e servidor devem estar em “comum acordo” quanto aos parâmetros e retorno para que a comunicação tenha sucesso. Deve ser utilizada apenas quando a implementação via API apresenta limitações impeditivas.

REST com API

Também é possível implementar REST de forma mais próxima a frameworks de RPC. O jmine-tec-rpc é construído utilizando a implementação direta, portanto ambas as formas podem coexistir, e se encarrega dos detalhes da requisição, serialização e deserialização dos parâmetros e da resposta, tornando muito mais simples a utilização de REST pelos clientes e a implementação no lado do servidor.

O “comum acordo” da implementação direta deve ser representado explicitamente através de uma interface Java marcada com a anotação @RestDefinition. Ex.:

@RestDefinition
public interface MyRest {
        String reverse(String string);
        SomeBean calculate(SomeParameter parameter);
}

Esta será a interface de um REST, e deve estar presente em um jar que deve conter apenas a interface do REST e os beans que irão trafegar como parâmetros ou respostas(Mais informações adiante). O projeto que contém a API REST não deve ter dependências além de outras APIs REST e o módulo de RPC.

Importante: É fundamental que as dependências sejam mínimas, pois isso poderia inchar as dependências do cliente desnecessariamente. Isso implica que o tráfego de entidades persistidas não deve ocorrer em hipótese alguma. Mais informações sobre entidades adiante.

A imagem abaixo mostra(como citado acima) que o cliente não possui implementações dos serviços, apenas são criados proxies que são responsáveis por fazer a comunicação com o server.

../../_images/Tec_rpc_01.png

Client não possui a implementação dos serviços

Servidor

Com a API definida em um módulo à parte, o servidor deve possuir alguma implementação da mesma. Ex.:

public class MyRestImplementor implements MyRest {
  String reverse(String string) {
         return string.reverse();
  }
  SomeBean calculate(SomeParameter parameter) {
         ...
  }
}

Esta implementação deve ser cadastrada no Spring da seguinte forma:

<bean id="myRest" parent="abstractRestFactory">
  <property name="implementor">
         <bean parent="transactedImplementorFactory">
           <property name="target">
             <bean class="jmine.tec.sample.rest.MyRestImplementor"/>
           </property>
         </bean>
  </property>
  <property name="prefix" value="myRest"/>
</bean>

O bean mais externo (com parent abstractRestFactory) é um Factory Bean que realiza os registros necessários ao serviço. O bean com parent transactedImplementorFactory é opcional, também é um Factory Bean que irá garantir que o Implementor irá executar seus métodos dentro de uma transação. A propriedade prefix, preenchida com “myRest”, é o nome do serviço REST e deve ser única no servidor.

Cliente

Pelo lado cliente, é necessário configurar um RestProvider que irá prover acesso a um servidor específico. Em geral este objeto é configurado no Spring. Exemplo:

<bean id="sampleRestServiceProvider" class="jmine.tec.rpc.client.provider.ClientRestServiceProvider">
        <property name="stubFactory" ref="sampleRestServiceStubFactory" />
        <property name="discovery" ref="sampleRestServiceDiscovery" />
</bean>

Nota-se que o sampleRestServiceProvider necessita de um objeto stubFactory e um objeto discovery. StubFactory é responsável por gerar os stubs que são intermediários responsáveis por realizar as chamadas remotas. Um stubFactory pode ser configurado da seguinte forma:

<bean id="sampleRestServiceStubFactory" class="jmine.tec.rpc.client.comm.AOPClientStubFactory">
        <property name="connector" ref="sampleConnector" />
        <property name="serializer" ref="sampleRPCSerializer" />
        <property name="basePath" value="/rest" />
</bean>

<bean id="sampleConnector" class="jmine.tec.utils.io.http.impl.URLConnector">
        <constructor-arg index="0" value="http://myserver:8080/"></constructor-arg>
</bean>

<bean id="sampleRPCSerializer" class="jmine.tec.rpc.common.serialization.json.JSONSerializer">
        <property name="converter" ref="sampleJSONConverter" />
</bean>

<bean id="sampleJSONConverter" parent="abstractJSONConverter">
        <property name="provider" ref="sampleRestServiceProvider" />
</bean>

Já o objeto discovery é um stub responsável por fazer uma requisição ao servidor e descobrir informações de um determinado serviço dado sua interface. O serviço de discovery pode ser configurado da seguinte forma:

<bean id="restServiceDiscovery" factory-bean="restServiceStubFactory"
        factory-method="create">
        <constructor-arg index="0"
                value="jmine.tec.rpc.common.provider.RestServiceDiscovery" />
        <constructor-arg index="1" value="discovery" />
</bean>

Tendo o restProvider configurado, basta usa-lo da seguinte forma:

MyRest rest = restProvider.provideByClass(MyRest.class);
String result = rest.reverse("some string");
Assert.assertEquals("gnirts emos", result);

Modularização

É fortemente aconselhável que a API fique em um módulo apartado, para ser importada pelo cliente e pelo módulo que a implementa. Também é aconselhável que os Implementors de uma API fiquem em um módulo exclusivo. O código da aplicação do servidor não deve depender da API ou dos Implementors.

Por exemplo:

../../_images/Tec_rpc_02.png

Client não possui a implementação dos serviços

Entidades

Entidades mapeadas não devem trafegar via REST. A API não deve depender, em hipótese alguma, de qualquer classe que não esteja em uma API de REST. O framework dá suporte à entidades através de Representational States das mesmas.

O Representational State de uma entidade deve estar declarado no mesmo módulo que a API de acesso. Tipicamente o Representational State de uma entidade possui o mesmo nome, com o sufixo “RS”. Ex.: Pessoa e PessoaRS.

public class PessoaRS implements EntityRepresentation {

         private String nome;

         private Serializable pk;

         public void setNome(String nome){
             this.nome = nome;
         }

         public String getNome(){
             return this.nome;
         }

         public void setPk(Serializable pk){
             this.pk = pk;
         }

         public Serializable getPk(){
             return this.pk;
         }
}

O Representational State por padrão possui um subconjunto das propriedades da entidade, desta forma a conversão de uma entidade em uma representação pode ser feita de forma automática pela RepresentationFactory fornecida à implementação do REST, evitando código desnecessário.

A API de acesso a uma entidade pode estender a interface EntityRest, que provê métodos básicos de consulta como “findAll” e “findByNaturalKey”. Tipicamente seu nome é o nome da entidade, com o sufixo Rest. Ex.: PessoaRest. Caso uma API de acesso estenda de EntityRest, o RS da entidade deverá implementar a interface EntityRepresentation como mostrado no exemplo acima de PessoaRS.

@RestDefinition
public interface PessoaRest extends EntityRest<PessoaRS> {

         String getNome();

}
public class PessoaRestImplementor extends BaseEntityRestService<PessoaRS, Pessoa> implements PessoaRest {

         public String getNome(){
             return "Menezes";
         }
}

Serialização

É dado suporte à serialização de diversos tipos básicos, que podem ser utilizados livremente nas APIs:

  • Strings
  • Números (Long, Integer, BigDecimal)
  • Boolean
  • Date (do jMine-tec)
  • Enums
  • Beans
  • Arrays
  • Coleções (List, Set, Map)
  • Classes
  • Exceptions (deve possuir construtor com parâmetro (String) ou (LocalizedMessageHolder).

Beans podem conter qualquer dos tipos acima (inclusive outros beans). Há suporte a referências circulares (bean A referencia bean B e vice-versa, por exemplo).

Por hora, Map deve possuir chave do tipo String (essa limitação deverá ser eliminada no futuro).

Métodos de APIs podem jogar exceções livremente, desde que as exceções sejam serializáveis de acordo com o critério mencionado acima.

Referências a entidades podem ser marcadas com @Lazy no getter. O comportamento é similar ao do Hibernate, será gerado um proxy e a entidade referenciada será carregada apenas se necessário.

Configurando o web.xml

Alguns erros são comuns ao configurar os web.xml, o primeiro deles é configurar o wicketFilter para o padrão de url “/” quando o correto é configurar para “/web/”, ex:

<filter>
        <filter-name>wicketFilter</filter-name>
        <filter-class>org.apache.wicket.protocol.http.WicketFilter</filter-class>
        <init-param>
                <param-name>applicationFactoryClassName</param-name>
                <param-value>org.apache.wicket.spring.SpringWebApplicationFactory</param-value>
        </init-param>
</filter>
<filter-mapping>
        <filter-name>wicketFilter</filter-name>
        <url-pattern>/web/*</url-pattern>
</filter-mapping>

Dessa forma o wicketFilter só filtrará paginas wicket, não interferindo nas requisições REST. Outro problema é a configuração do TransactionFilter, que como o WicketFilter deve ser aplicado ao padrão /web/. Caso contrário será aberta uma transação por requisição REST.ex:

<filter>
        <filter-name>transactionFilter</filter-name>
        <filter-class>jmine.tec.web.servlet.filter.TransactionFilter</filter-class>
</filter>
<filter-mapping>
        <filter-name>transactionFilter</filter-name>
        <url-pattern>/web/*</url-pattern>
</filter-mapping>

Por ultimo é necessário configurar o servlet que ficará a cargo de responder as requisições REST, o que pode ser feito da seguinte forma:

<servlet>
        <servlet-name>restServlet</servlet-name>
        <servlet-class>jmine.tec.web.servlet.rest.servlet.RestLikeServlet</servlet-class>
</servlet>

<servlet-mapping>
        <servlet-name>restServlet</servlet-name>
        <url-pattern>/rest/*</url-pattern>
</servlet-mapping>

Testes

Testes de serialização: O objetivo deste teste é verificar que a serialização dos Representational States funciona de forma correta. O teste deve serializar e deserializar um RS e verificar se o objeto deserializado corresponde ao objeto serializado. Esse processo pode ser trabalhoso e repetitivo para se testar vários Representational States. Para facilitar os testes de serialização, foi criada a classe RepresentationSerializationCheckup que testa a serialização da representação dada.

Exemplo:

public class CalculateRSTest {

         private RepresentationSerializationCheckup<CalculateRS> checkup;

         /**
          * Instantiate a RepresentationSerializationCheckup for CalculateRS
          */
         @Before
         public void init() throws Exception {
             this.checkup = new RepresentationSerializationCheckup<CalculateRS>(CalculateRS.class);
         }

         /**
          * Tests the CalculateRS serialization
          */
         @Test
         public void testSerialization() {
             this.checkup.check();
         }
}

Testes de integração: Cada Implementor deve possuir um teste de integração. No teste de integração, deve-se verificar que um cliente consegue utilizar a API REST, enviando requisições e validando as respostas, se necessário. Estas verificações são fundamentais para evitar versões geradas inutilmente. Além disso o debug do REST com a aplicação em uso é muito mais complexo.

O teste de integração utiliza um servidor Jetty com toda a estrutura necessária para disponibilizar os RESTs, além de instanciar uma BeanFactory apartada para simular o cliente.

O Jmine possui uma classe abstrata para facilitar esse tipo de teste: AbstractRestImplementorTest. Ela é responsável por carregar o servidor e o cliente. A subclasse deve apenas fornecer os XMLs do Spring e de base de dados através da anotação @DBEnv.

Exemplo:

@DBEnv(spring = SPRING_MAIN_XML, refdb = REFDB_XML)
public class CalculateImplementorTest extends AbstractRestImplementorTest {

         private CalculateRest rest;

         @Before
         public void setUp(){
             rest = this.getProvider().provideByClass(CalculateRest.class);
         }

         @Test
         public void testSoma(){
             int res = rest.somar(1, 2);
             Assert.assertEquals(3, res);
         }
}

Compatibilidade

Para que o cliente e o servidor sejam compatíveis é necessários que ambos utilizem APIs idênticas (de preferência na mesma versão).

Também é aconselhável ter versões compatíveis do jmine-tec-rpc, uma vez que é possível que haja perda de compatibilidade entre versões do módulo, o que pode ocorrer por eventuais mudanças de protocolo.

Eficiência

É aconselhável que, no caso de consultas que exigem boa responsividade (como auto-completes), seja criada uma consulta que traz apenas as informações mínimas (List de Strings contendo mnemônicos, por exemplo). Isso diminui a quantidade de dados a ser trafegada, trazendo benefícios ao usuário. Um cache no lado cliente é fundamental para esse tipo de funcionalidade.