Android+KSoap – Pt.1: Transcrevendo SoapObjects através de Annotations & Reflection

Como demonstrado por Ricardo Ushisima neste artigo, é possível consumir Web Services em Android utilizando a biblioteca KSoap2. Essa biblioteca é muito útil pois encapsula a geração e leitura dos XMLs (oferecendo os dados em forma de SoapObjects), bem como a comunicação com o serviço, tratando, inclusive, erros levantados pelo servidor e lançando exceções quando apropriado.

Por outor lado, no decorrer do desenvolvimento de um aplicativo baseado no KSoap, com frequência será preciso escrever código que transcreve dados enviados pelo servidor em modelos (VO) da nossa aplicação, assim como feito no código abaixo:

private Cliente parseSimples(SoapObject clienteSoap) {
	SoapPrimitive sp = null;

	Cliente cliente = new Cliente();
	Endereco endereco = new Endereco();

	SoapObject enderecoSoap = (SoapObject)clienteSoap.getProperty("Endereco");
	sp = (SoapPrimitive) enderecoSoap.getProperty("Numero");
	endereco.setNumero(Integer.parseInt(sp.toString()));
	sp = (SoapPrimitive) enderecoSoap.getProperty("Logradouro");
	endereco.setLogradouro(sp.toString());
	sp = (SoapPrimitive) enderecoSoap.getProperty("Complemento");
	endereco.setComplemento(sp.toString());
	sp = (SoapPrimitive) enderecoSoap.getProperty("CEP");
	endereco.setCep(sp.toString());
	sp = (SoapPrimitive) enderecoSoap.getProperty("Cidade");
	endereco.setCidade(sp.toString());
	sp = (SoapPrimitive) enderecoSoap.getProperty("Estado");
	endereco.setEstado(sp.toString());

	sp = (SoapPrimitive)clienteSoap.getProperty("Id");
	cliente.setId(Integer.parseInt(sp.toString()));
	sp = (SoapPrimitive)clienteSoap.getProperty("Nome");
	cliente.setNome(sp.toString());
	sp = (SoapPrimitive)clienteSoap.getProperty("Ativo");
	cliente.setAtivo(Boolean.parseBoolean(sp.toString()));
	cliente.setEndereco(endereco);

	return cliente;
}

Em contextos como esses, é muito natural fazer uso de Annotations e Reflection para generalizar o código dessas transcrições (parse), especialmente no Android que suporta esses recursos, uma vez que é baseado na versão 1.5 do Java. Infelizmente, o KSoap foi originalmente concebido para utilização na plataforma JME (Java Micro Edition), que é baseada na versão 1.4, onde essas ferramentas não estão disponíveis.

Neste artigo, vou apresentar uma abordagem para transcrever SoapObjects em objetos do modelo da nossa aplicação, utilizando Annotations e Reflection, bem como verificar o impacto dessa abordagem em termos de desempenho e uso de memória em geral.

Anotando nossos Modelos

O primeiro passo para conseguirmos fazer a transcrição dos SoapObjects em objetos do nosso modelo, é criar Annotations que possam fornecer os metadados necessários para transcrição. Veja abaixo.

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target (ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
/**
* Annotation for classes that may be sent and/or received through
* SOAP method calls.
*/
public @interface SOAPObject {
 /**
  * The SOAP namespace that defines this class. May be omitted if the class
  * is only received. Attempts to pass classes that have not defined this
  * value as parameters to a SOAP method call will miserably fail.
  */
 String namespace() default "";

 /** The SOAP type identification. If omitted class name is used instead. */
 String typeId() default "";
}

Com a annotation SOAPObject, poderemos marcar o namespace referente àquele tipo, bem como um identificador do tipo como é conhecido pelo servidor. Essa annotation pode ser aplicada apenas a classes e ambos os parâmetros podem ser omitidos, pois são necessários apenas para envio de tipos complexos (mais sobre esse tópico na segunda parte deste artigo).

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target (ElementType.FIELD)
@Retention (RetentionPolicy.RUNTIME)
/**
 * Marker annotation for defining classes that can be deserialized from a SOAPObject
 * @author bruno.silva
 */
public @interface SOAPProperty {
	/**
	 * Define types of fields; this elects the logic of deserialization applied to such field.
	 * @author bruno.silva
	 */
	public static enum Type {
		/** For primitive types. Wrapper classes and String included. */
		Primitive,
		/** For primitive type arrays. Array of Wrapper classes and String included. Currently, only supports byte[]. */
		PrimitiveArray,
		/** For complex types. Causes the parser to be called recursively for such field. Complex types must also be annotated. */
		Complex
	};

	/**	The SOAP property identifier. */
    String name() default "";

    /**
     * Either Type.Primitive, Type.PrimitiveArray, Type.Complex.
     * @see SOAPProperty#Type
     */
    Type type() default Type.Primitive;
}

A annotation SOAPProperty por sua vez, deve ser aplicada nas propriedades do modelo a ser transcrito. Nela é possível atribuir o nome da propriedade como definido no serviço, e um tipo de dado, definido no enumeration Type, que é publico. Vamos fazer tratamentos específicos para cada tipo de dado, por isso precisamos saber do que se trata o dado que nós vamos transcrever.

Utilizando Reflection para transcrever SoapObjects

Agora que temos nossos modelos anotados, vamos ver o código que é capaz de traduzir um SoapObject em um modelo do nosso aplicativo.

/**
 * Parses an KSoap2 SoapObject into an annotated VO.
 *
 * @param <T>
 *            the object type to be created and populated
 * @param klass
 *            the class of the target VO
 * @param soapObject
 *            the SoapObject with the data for being parse
 * @return the created and populated VO of the given class
 * @throws IllegalArgumentException
 *             if the provided class isn't annotated by the SOAPObject; if
 *             the class doesn't provide a public no-args constructor
 */
public static <T> T parseSoapObjectIntoAnnotatedVO(Class<T> klass, SoapObject soapObject) throws IllegalArgumentException {
	T target;
	try {
		target = klass.newInstance();
	} catch (Exception e) {
		throw new IllegalArgumentException(klass.getSimpleName() + " does not provide public no-args constructor", e);
	}

	SOAPObject objectAnnotation = target.getClass().getAnnotation(SOAPObject.class);

	if(objectAnnotation == null) {
		throw new IllegalArgumentException(target.getClass().getSimpleName() + " doesn't have a SOAPObject annotation.");
	}

	Field[] fields = target.getClass().getDeclaredFields();
	for (Field field : fields) {
		try {
			SOAPProperty propertyAnnotation = field.getAnnotation(SOAPProperty.class);

			if(propertyAnnotation != null) {
				String propertyName = propertyAnnotation.name();
				Type propertyType = propertyAnnotation.type();

				if(propertyName == "")
					propertyName = field.getName();

				// check if the property is null
				Object objectPropertyValue = soapObject.getProperty(propertyName);
				if(objectPropertyValue == null) {
					continue;
				}

				Object rawValue = null;
				Class<?> fieldType = field.getType();
				if(propertyType == Type.Primitive) {
					SoapPrimitive propertyValue = (SoapPrimitive) objectPropertyValue;

					if(fieldType == String.class) {
						rawValue = propertyValue.toString();
					} else if(fieldType == Boolean.class || fieldType == boolean.class) {
						rawValue = Boolean.parseBoolean(propertyValue.toString());
					} else if(fieldType == Character.class || fieldType == char.class) {
						rawValue = propertyValue.toString().charAt(0);
					} else if(fieldType == Short.class || fieldType == short.class) {
						rawValue = Short.parseShort(propertyValue.toString());
					} else if(fieldType == Integer.class || fieldType == int.class) {
						rawValue = Integer.parseInt(propertyValue.toString());
					} else if(fieldType == Long.class || fieldType == long.class) {
						rawValue = Long.parseLong(propertyValue.toString());
					} else if(fieldType == Float.class || fieldType == float.class) {
						rawValue = Float.parseFloat(propertyValue.toString());
					} else if(fieldType == Double.class  || fieldType == double.class) {
						rawValue = Double.parseDouble(propertyValue.toString());
					}

				} else if(propertyType == Type.Complex) {
					SoapObject propertyValue = (SoapObject) objectPropertyValue;
					rawValue = parseSoapObjectIntoAnnotatedVO(fieldType, propertyValue);

				} else if(propertyType == Type.PrimitiveArray) {
					SoapPrimitive propertyValue = (SoapPrimitive) objectPropertyValue;

					if(fieldType == byte[].class) {
						// decode byte[] data from Base64 string
						rawValue = Base64.decode(propertyValue.toString());
					} else {
						throw new RuntimeException("Feature not yet implemented: cannot parse primitive array other than byte[] type.");
					}
				}

				field.setAccessible(true);
				field.set(target, rawValue);
			}
		} catch (IllegalAccessException e) {
			// Note: this piece of code is probably never reached
			// as we chance accessibility before setting the field value
			Log.w("parseSoapObjectToAnnotatedDTO", "Field not acessible: "  + target.getClass().getSimpleName() + "." + field.getName());
		}
	}

	return target;
}

Neste trecho de código nós basicamente utilizamos Reflection e os metadados fornecidos pelas Annotations que criamos, para instanciar e popular nosso VO. De posse da classe do nosso VO, criamos uma nova instancia e validamos a presença da anotação SOAPObject. Ela não tem nenhuma função neste caso, mas é importante manter uma consistência do uso das annotations. Em seguida listamos todos os campos disponíveis nessa classe e para cada um tentamos encontrar um valor apropriado baseado na sua anotação. Caso o campo não esteja anotado, ele é ignorado. Quando encontramos um atributo anotado e um valor correspondente, fazemos a conversão do valor proveniente do SoapObject, que sempre consiste de uma String, para o tipo do atributo anotado. Isso funciona bem para tipos primitivos, mas não para tipos complexos. Para tipos complexos, basta chamar o método recursivamente. Claro que o tipo complexo também deve estar devidamente anotado.

Finalmente, o valor convertido ao tipo correto é atribuído ao campo correspondente e o, ao final do loop, objeto preenchido é retornado.

A partir de agora, virtualmente qualquer tipo complexo pode ser transcrito fazendo uso deste mesmo código, fornecendo assim uma solução compacta e elegante para o problema de transcrição dos SoapObjects enviados pelo servidor. Esta implementação não contempla, claro, todos os cenários possíveis. Coleções e vetores (arrays) estão visivelmente excluídos. Mas não há grandes obstáculos para sua extensão.

Performance

Vamos observar e comparar a performance entre as duas abordagens apresentadas e verificar o custo que teremos que pagar para ter esse facilidade na manutenção do nosso código. Para tanto, iremos codificar uma instância de um SoapObject representando uma estrutura simples, escrever um algorítimo de transcrição especializado e comparar a performance (tempo gasto em milissegundos) entre os dois. Para dar uma idéia mais clara da performance, ambos os métodos foram executados 1000 vezes em um loop, e o tempo total do processamento foi computado a partir daí. A estrutura a ser transcrita consiste da representação de um Client, que além de dados básicos (id, nome, etc), contém uma estrutura complexa: o Endereco. Confira no gráfico abaixo os resultados que obtive.

Isso mesmo, o gráfico não está errado! Impressionante, não? Pois é. Como um código inocente como esse pode representar uma diferença da ordem de 100x na performance, eu pergunto… e respondo! Annotations são realmente MUITO lentas para serem processadas… Eu esperava que recuperar uma propriedade definida numa Annotation não fosse representar nada mais do que um método get representa. Observando o gráfico, dá para notar que eu estava claramente enganado. Bom, deixando de lado todo o trabalho de medição de performance que tive, analisando cada chamada de método para chegar a conclusão supracitada, fiz algumas alterações e implementei a seguinte abordagem como solução: criar um cache para que eu só precisasse acessar as Annotations uma única vez. Rápido e sujo, além de extremamente eficiente. vamos dar uma olhada no novo cache. Código!

package br.com.zbra.test.soap.util;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import br.com.zbra.test.soap.annotations.SOAPObject;
import br.com.zbra.test.soap.annotations.SOAPProperty;
import br.com.zbra.test.soap.annotations.SOAPProperty.Type;

public class SoapAnnotationDataCache {

	private static final HashMap<Class<?>, AnnotatedClassData> cache = new HashMap<Class<?>, AnnotatedClassData>();

	public static AnnotatedClassData getAnnotatedClassData(Class<?> clazz) {
		AnnotatedClassData annotatedClassData = cache.get(clazz);

		if (annotatedClassData == null) {
			SOAPObject annotation = clazz.getAnnotation(SOAPObject.class);

			if (annotation == null) {
				throw new IllegalArgumentException(clazz.getSimpleName() + " doesn't have a SOAPObject annotation.");
			}

			annotatedClassData = new AnnotatedClassData();
			annotatedClassData.clazz = clazz;
			annotatedClassData.namespace = annotation.namespace();
			annotatedClassData.typeId = annotation.typeId();

			Field[] fields = clazz.getDeclaredFields();
			for (Field field : fields) {
				SOAPProperty propertyAnnotation = field.getAnnotation(SOAPProperty.class);
				if (propertyAnnotation != null) {
					field.setAccessible(true);

					AnnotatedFieldData spaf = new AnnotatedFieldData();
					spaf.field = field;
					spaf.name = propertyAnnotation.name();
					spaf.type = propertyAnnotation.type();

					if (spaf.name == "")
						spaf.name = field.getName();

					annotatedClassData.fields.add(spaf);
				}
			}

			cache.put(clazz, annotatedClassData);
		}

		return annotatedClassData;
	}

	public static class AnnotatedClassData {
		private Class<?> clazz;
		private String namespace;
		private String typeId;
		private List<AnnotatedFieldData> fields = new ArrayList<AnnotatedFieldData>();

		public Class<?> getClazz() {
			return clazz;
		}

		public String getNamespace() {
			return namespace;
		}

		public String getTypeId() {
			return typeId;
		}

		public List<AnnotatedFieldData> getFields() {
			return fields;
		}
	}

	public static class AnnotatedFieldData {
		private Field field;
		private String name;
		private Type type;

		public Field getField() {
			return field;
		}

		public String getName() {
			return name;
		}

		public Type getType() {
			return type;
		}
	}
}

E abaixo, a nova versão do parser, já utilizando o novo cache.

/**
 * Parses an KSoap2 SoapObject into an annotated VO.
 *
 * @param <T>
 *            the object type to be created and populated
 * @param klass
 *            the class of the target VO
 * @param soapObject
 *            the SoapObject with the data for being parsed
 * @return the created and populated VO of the given class
 * @throws IllegalArgumentException
 *             if the provided class isn't annotated by the SOAPObject; if
 *             the class doesn't provide a public no-args constructor
 */
public static <T> T parseSoapObjectIntoAnnotatedVOCached(Class<T> klass, SoapObject soapObject) throws IllegalArgumentException {
	T target;
	try {
		target = klass.newInstance();
	} catch (Exception e) {
		throw new IllegalArgumentException(klass.getSimpleName() + " does not provide public no-args constructor", e);
	}

	List<AnnotatedFieldData> fields = SoapAnnotationDataCache.getAnnotatedClassData(klass).getFields();
	Field field = null;
	Type propertyType = null;

	try {
		for (AnnotatedFieldData annotatedField : fields) {
			field = annotatedField.getField();
			propertyType = annotatedField.getType();

			// check if the property is null
			Object propertyValue = soapObject.getProperty(annotatedField.getName());
			if(propertyValue == null) {
				continue;
			}

			Object rawValue = null;
			Class<?> fieldType = field.getType();
			if(propertyType == Type.Primitive) {

				if(fieldType == String.class) {
					rawValue = propertyValue.toString();
				} else if(fieldType == Boolean.class || fieldType == boolean.class) {
					rawValue = Boolean.parseBoolean(propertyValue.toString());
				} else if(fieldType == Character.class || fieldType == char.class) {
					rawValue = propertyValue.toString().charAt(0);
				} else if(fieldType == Short.class || fieldType == short.class) {
					rawValue = Short.parseShort(propertyValue.toString());
				} else if(fieldType == Integer.class || fieldType == int.class) {
					rawValue = Integer.parseInt(propertyValue.toString());
				} else if(fieldType == Long.class || fieldType == long.class) {
					rawValue = Long.parseLong(propertyValue.toString());
				} else if(fieldType == Float.class || fieldType == float.class) {
					rawValue = Float.parseFloat(propertyValue.toString());
				} else if(fieldType == Double.class  || fieldType == double.class) {
					rawValue = Double.parseDouble(propertyValue.toString());
				}

			} else if(propertyType == Type.Complex) {
				rawValue = parseSoapObjectIntoAnnotatedVOCached(fieldType, (SoapObject)propertyValue);

			} else if(propertyType == Type.PrimitiveArray) {
				String value = propertyValue.toString();

				if(fieldType == byte[].class) {
					// decode byte[] data from Base64 string
					rawValue = Base64.decode(value);
				} else {
					throw new RuntimeException("Feature not yet implemented: cannot parse primitive array other than byte[] type.");
				}
			}
			field.set(target, rawValue);
		}
	} catch (IllegalAccessException e) {
		// Note: this piece of code is probably never reached
		// as we chance accessibility before setting the field value
		Log.w("parseSoapObjectToAnnotatedDTO", "Field not acessible: "  + target.getClass().getSimpleName() + "." + field.getName());
	}

	return target;
}

Com o novo código, que não é o ótimo, mas é realmente muito eficiente, chegamos no seguinte resultado:

Com a nova implementação, trouxemos o parse genérico para um nível mais do que aceitável. Nessas condições, a diferença de performance entre os dois é simplesmente desprezível, tornando a solução genérica bem atrativa, tanto do ponto de vista da manutenabilidade, quanto em termos de performance.

Conclusão

Nesse artigo foi possível apresentar uma solução simples e prática para transcrição dos objetos gerados como retorno de chamadas a WebServices efetuadas através do KSoap2, fazendo uso de todo poder e flexibilidade oferecidos pela versão 1.5 do Java, que é padrão na plataforma Android. Na parte dois, vamos abordar a passagem de tipos complexos como parâmetro para Soap Method Calls, introduzindo uma solução para encapsular também a chamada desses métodos remotos, aproveitando a estrutura de anotações que criamos neste artigo. Até lá.

23 Comentários

  • Reply

    Por Glauber em 8 de July de 2011 às 9:13

    Muito bom Bruno. São poucos os desenvolvedores que sabem utilizar o poder das anotações de Java e você é um deles.
    Parabéns e fico no aguardo do próximo post (que tal algo com RESTful)

    4br4ç05,
    nglauber

  • Reply

    Por Bruno Vinicius em 12 de July de 2011 às 16:36

    Obrigado, Glauber! Claro, mas antes tenho que acabar com essa série sobre SOAP…

  • Reply

    Por Pedro em 6 de August de 2011 às 14:36

    Muito bom este artigo! Para quando a parte 2? Estou ansioso por lê-lo e por colocá-lo em prática.

    • Reply

      Por Bruno Vinicius em 8 de August de 2011 às 11:57

      Obrigado, Pedro! Vai estar disponível muito em breve. Está apenas aguardando a aprovação do editor-chefe =D

  • Reply

    Por Eduardo Folly em 21 de September de 2011 às 11:08

    Parabéns pelo artigo Bruno. Fiz algumas adições ao código e gostaria de contribuir. Abs e obrigado.

    • Reply

      Por Bruno Vinicius em 21 de September de 2011 às 12:07

      Obrigado Eduardo. Você pode posta-las aqui que eu atualizo o artigo e atribuo os créditos a você. []s

      • Reply

        Por Eduardo Folly em 22 de September de 2011 às 11:11

        No momento a aplicação desenvolvida consome webservices feitos em dotnet.

        Inicialmente verifiquei que quando o retorno do webservice trazia um valor nulo, o atributo estava sendo mapeado como String e valor “anyType”.

        Para solucionar o ocorrido adicionei uma condição para somente mapear um atributo classificado como Type.Primitive caso ele seja um objeto SoapPrimitive, senão será mantida a definição padrão da classe.

        if (propertyType == Type.Primitive) {
        if (propertyValue instanceof SoapPrimitive) {

        Outra dificuldade encontrada no mapeamento dos objetos foram as listas. Para isto foi adicionado o tipo “List” na interface SOAPProperty.

        public static enum Type {
        Primitive, PrimitiveArray, Complex, List
        };

        Complementando, foi adicionado ao parser a operação referente ao tipo “List”.

        else if (propertyType == Type.List) {
        if (fieldType == List.class) {
        java.lang.reflect.Type newType = field.getGenericType();
        ParameterizedType newParamType = (ParameterizedType) newType;
        Class newklazz = (Class) newParamType.getActualTypeArguments()[0];
        int max = ((SoapObject) propertyValue).getPropertyCount();

        List list = new ArrayList();

        for (int i = 0; i < max; i++) {
        SoapObject secondPropertyValue = (SoapObject) ((SoapObject) propertyValue).getProperty(i);
        Object newRawValue = parseSoapObjectIntoAnnotatedVO(newklazz, secondPropertyValue);
        list.add(newRawValue);
        }
        rawValue = list;
        } else {
        throw new RuntimeException("Feature not yet implemented: cannot parse list other than Interface List type.");
        }
        }

        Com certeza existem várias críticas e melhorias para serem implementadas, mas até o presente momento o funcionamento da classe está sendo excelente para as necessidades encontradas.

        Abs.

        • Reply

          Por Bruno Vinicius em 22 de September de 2011 às 12:36

          Muito bom, Eduardo! Parabéns! Muito boas as adições.

          No caso da lista, o algorítimo vai falhar em caso de uma lista de por exemplo. pois o tipo int não é anotado pela @SoapObject e o parseSoapObjectIntoAnnotatedVO vai levantar um exceção.

          Outra preocupação é em relação à performance. Como eu mostrei no artigo essas operações com uso de Reflection podem causar perda significativa de performance em caso de parse de listas grandes por exemplo. O ideal é integrar essa solução com os objetos de cache, para que não seja preciso executar a reflexão múltiplas vezes.

          Assim que possível vou atualizar o artigo com suas contribuições. Obrigado. Fico feliz em estar ajudando.

          • Por Eduardo Folly em 22 de September de 2011 às 13:58

            Perfeito Bruno. Irei modificar para suportar pelo menos listas de tipos primitivos e String.

            Pode ser uma boa solução o aproveitamento do código referente a conversão através do fieldType.

          • Por Bruno Vinicius em 22 de September de 2011 às 14:25

            Certamente! Vale encapsular aquele bloco de código para reuso. Acho que segunda devo ter editado o artigo para adicionar essas mudanças.

  • Reply

    Por Eduardo Folly em 27 de September de 2011 às 11:45

    Bruno, existem algumas implementações necessárias caso seja preciso mapear classes herdadas.

    Será que compete ao propósito deste código?

    Abs.

    • Reply

      Por Bruno Vinicius em 28 de September de 2011 às 15:52

      Com certeza Eduardo. Não fiz testes nesses casos pois a necessidade não se fez presente. Mas com certeza, esse código deveria lidar com herança. Pelo que eu vejo ele trata o caso de agregação mas não de composição, certo?

  • Reply

    Por Bruno Henrique em 14 de November de 2011 às 15:55

    Olá Bruno, primeira mente obrigado pelo post, ele com certeza nos ajudou muito. Mas seguinte, estou com um pequeno problema e não estou conseguindo resolver. Estou recebendo de um webservice alguns objetos, são PlayLists de musicas, nessas playlists existem os nomes delas, os artistas e etc…

    Só que quando eu recebo uma playlist vazia o SOAP se perde, ele diz não encontrar uma propriedade. O que eu poderia fazer para tratar um retorno nulo?

    Muito obrigado desde já.

    • Reply

      Por Bruno Vinicius em 16 de November de 2011 às 13:54

      Ola Bruno!

      Você pode dar mais detalhes sobre a estrutura de dados que você está usando?
      O mais provável é que a chamada ao método getProperty() do SoapObject esteja levantando uma exceção visto que ele não está conseguindo achar uma propriedade com o nome dado.
      Uma dica é debugar o parse para ver como o KSoap montou o SoapObject que representa a resposta, para saber como tratar esse caso em particular.

      []s

  • Reply

    Por Elson em 23 de January de 2012 às 10:09

    Muito bom o artigo. valeu…

    • Reply

      Por Bruno Vinicius em 24 de January de 2012 às 11:03

      Obrigado Elson! Não esqueça de conferir os outros da série =)

  • Reply

    Por Rodrigo Gimenez em 30 de April de 2012 às 11:22

    Salve Bruno.
    Estou ressuscitando seu tópico, rss.
    Uso Web Services em uma aplicação que estamos desenvolvendo aqui na empresa.
    A aplicação é para Android e os Web Services que consumo são em .NET (C#). Fizemos todo um framework e tudo mais, vou colocar um exemplo de como uso e se puder criticar fico muito grato.
    Imagine assim, tenho um objeto que representa o cliente nas classes do lado do Android, ele é mais ou menos assim (Coloquei só três fields por que ele é bem extenso):

    package vtc.ent;

    import java.util.Date;

    public class J1_Pessoa_ENT extends _EntBase implements _IEnt {

    public String vsName = “J1_Pessoa”;
    public String vsLabel = “J1_Pessoa”;

    @_EntAtr(Coluna = “IDPESSOA”, Label = “IDPESSOA”, Pk = true, Nullable = false)
    private Integer _idpessoa;
    private Integer ori_idpessoa;
    public void setIdpessoa(Integer pnIdpessoa) {
    this._idpessoa = pnIdpessoa;
    }
    public Integer getIdpessoa() {
    return this._idpessoa;
    }

    @_EntAtr(Coluna = “NOMERAZAO”, Label = “NOMERAZAO”, Pk = false, Nullable = false)
    private String _nomerazao;
    private String ori_nomerazao;
    public void setNomerazao(String psNomerazao) {
    this._nomerazao = psNomerazao;
    }
    public String getNomerazao() {
    return this._nomerazao;
    }
    }

    Quando vou receber dados (Clientes) via Web Service faço mais ou menos assim:
    //– Lê uma pessoa.
    public void fPesUserGet(J1_Pessoa_ENT poPessoa) throws Exception
    {
    String vsMetodo = “fPesUserGet”;

    SoapObject voRequest = new SoapObject(NAMESPACE, vsMetodo);

    //– Parametros.
    voRequest.addProperty(“pnSeqUsuario”, VarCon.vngSeqUsuario);
    voRequest.addProperty(“psIdDispositivo”, VarCon.vsgIdTablet);
    voRequest.addProperty(“psAccessKey”, “001″);

    SoapSerializationEnvelope voEnvelope = new SoapSerializationEnvelope(SoapEnvelope.VER11);

    voEnvelope.dotNet = true;
    voEnvelope.setOutputSoapObject(voRequest);

    HttpTransportSE voHttpTrans = new HttpTransportSE(URL, vnTimeout);

    try
    {
    voHttpTrans.call(NAMESPACE + “/” + vsMetodo, voEnvelope);
    SoapObject voSO = (SoapObject)voEnvelope.bodyIn;

    SoapObject voSOResult = (SoapObject) voSO.getProperty(0);

    fSoapToEnt(voSOResult, poPessoa);
    }
    catch (Exception e)
    {
    e.printStackTrace();
    throw new Exception(“Erro, WS-fPesUserGet: ” + e.getMessage());
    }
    }
    Bom até ai tudo tranquilo, só pra resumi, eu chamo a função acima passando um objeto do tipo J1_Pessoa_ENT, a função por sua vez consome o serviço e através do resultado (SoapObject) do serviço preenche o objeto J1_Pessoa_ENT.
    Chamo essa função N vezes, eu só paro quando ela me retornar um cliente em branco. O serviço já é preparado pra isso, sempre que eu pedir um cliente, ele retorna o mesmo se houver, se não ele vai retornar um em branco.
    Enfim, com base no que li no seu artigo, peso que eu poderia estar com problema de performance aqui:
    SoapObject voSOResult = (SoapObject) voSO.getProperty(0);
    fSoapToEnt(voSOResult, poPessoa);

    Essa função fSoapToEnt pega o retorno do serviço e preeche o objeto J1_Pessoa_ENT. Fiz ela assim por que tenho N tipos de objetos para N serviços, ai pra todos uso a mesma função e ela faz tudo sozinha.
    Veja o código:
    //– Copia os valores de um objeto SOAP para uma entidade.
    private void fSoapToEnt(SoapObject poSOAP, _IEnt poEnt) throws SecurityException, NoSuchFieldException, NumberFormatException, IllegalArgumentException, IllegalAccessException, ParseException
    {
    for (int i = 0; i < poSOAP.getPropertyCount(); i++)
    {
    PropertyInfo voPi = new PropertyInfo();
    poSOAP.getPropertyInfo(i, voPi);

    if (poSOAP.getProperty(i) != null)
    {
    if (fEntColumExists(poEnt, voPi.name))
    {
    Field voCampo = poEnt.getClass().getDeclaredField("_" + voPi.name.toLowerCase());
    voCampo.setAccessible(true);

    fTipoSet(voCampo, poEnt, poSOAP.getProperty(i).toString());
    }
    }
    else
    {
    if (fEntColumExists(poEnt, voPi.name))
    {
    Field voCampo = poEnt.getClass().getDeclaredField("_" + voPi.name.toLowerCase());
    voCampo.setAccessible(true);

    if (voCampo.getType().toString().equals("int") || voCampo.getType().toString().equals("double"))
    voCampo.set(poEnt, 0);
    else
    voCampo.set(poEnt, null);
    }
    }
    }
    }

    Na realidade ela percorre os campos do objeto SOAP e para cada campo encontrado ela verifica se ele existe na entidade (J1_Pessoa_ENT) que passei, se existir copia o valor dele para a entidade.
    Ao final desse processo vou ter meu objeto J1_Pessoa_ENT preenchido.
    Bom, seguindo esse modelo que fiz, que é bem dinâmico, se eu mudar os serviços ou as entidades não preciso mexe em nada, em questão de performance, do jeito que você fez, será que eu teria um ganho significativo?
    Não consegui compara muito bem o que você fez com o jeito que eu faço.

    Qualquer dúvida me avise.

    • Reply

      Por Bruno Vinicius em 11 de May de 2012 às 13:29

      Ola Rodrigo, tudo blza?

      O ponto crítico, em termos de performance, na sua implementação é certamente a maneira como você recupera a lista de clientes. Se você implementar isso em forma de um serviço que retorna uma lista em uma única chamada você vai ter ganhos muito significativos. Cada invocação de serviço feita pelo KSoap, implica em obter uma nova conexão e isso pode degradar muito sua performance. Claro que digo isso baseado nas poucas informações que eu tenho, talvez esse comportamento seja preciso e esteja de acordo com o requisito do sistema, mas se você quer exibir uma lista de pessoas, recomendo analisar a possibilidade de implementar o serviço da forma que sugeri.

      Com relação ao método genérico para copiar os dados para sua classe de modelo, eu acho que sua solução é muito boa. Recomendo que você implemente o cache com os Fields para que você não precise invocar o getDeclaredFields() sempre. Esse método é bem lentinho, assim como o setAccessible(). Isso deve ajudar melhorar ainda mais sua performance.

      Quanto as vantagens da minha implementação, fora já incorporar as melhorias que recomendei acima, ela proporciona uma flexibilidade maior através do uso das Annotations permitindo aderir aos padrões de código de ambas linguagens (Java e C#), uma vez que os nomes das variáveis não são referenciados diretamente. Uma outra vantagem é que você pode aproveitar as melhorias que construo sobre essa implementação base e que estão descritas em mais 3 artigos (sendo um de autoria do Fabio Falavinha). Além disso, o ultimo artigo apresenta uma biblioteca de códgo aberto que fizemos com base nessa implementação e que você pode usar como ponto de partida. Vou colocar os links aqui pra você dar uma olhada.

      http://zbra.com.br/2011/08/19/androidksoap-pt-2-parametros-complexos/
      http://zbra.com.br/2011/10/19/androidksoap-%e2%80%93-pt-3-diga-nao-a-duplicacao/
      http://zbra.com.br/2011/10/31/android-aop-ksoap-pt-4-construindo-servicos-dinamicos/

  • Reply

    Por Manoel Campos em 18 de September de 2012 às 21:38

    Olá Bruno. Primeiramente, parabéns pelos artigos. Muito bons.

    Estou implementando uma solução semelhante utilizando o KSOAP, e pesquisando na Web achei seus artigos.

    Estou justamente com o problema de desempenho e sua solução de cache foi ideal. A minha implementação usa Reflection também, no entanto, não uso Annotations. Não entendi bem a finalidade delas aqui, uma vez que, usando só Reflection eu consegui extrair todas as informações que preciso usando field.getType() e field.getName(), por exemplo.

    Quanto aos tipos, usei um HashMap para mapear os tipos do KSOAP (que são poucos) para os tipos nativos do Java. Assim, nem tive que usar if’s no meu código para verificar qual o tipo correspondente (a não ser para tipos complexos).

    Desta forma, a meu ver, o uso das anotações aqui ficou redundante e adiciona um trabalho a mais.

    Agora, uma sugestão: seria muito bom disponibilizar o projeto para download em cada artigo. O código completo e rodando é mais fácil pra esclarecer certas coisas.

    Obrigado.

    • Reply

      Por Bruno Vinicius em 19 de September de 2012 às 11:42

      Obrigado pelo feedback Manoel.

      No inicio eu desenhei uma solução como a sua, baseada simplesmente nos nomes dos campos etc. No meu caso especificamente, como o servidor era C# rolou um conflito de padrão de nomeclatura das propriedades.
      No C# elas são declaradas como “PropertyPeteca” enquanto que em java “propertyPeteca”, então começei a usar anotações para resolver esse problema e conseguir seguir os padrões da linguagem. Dai em diante começei
      a usar para colocar o máximo de metadados o possivel. A parte boa é que o framework ficou completamente desacoplado, ou seja não há código especifico para o serviço A ou B.

      Se você ler o artigo do Fabio Falavinha, você vai perceber que foi uma boa decisão, já que baseado nessa estrutura conseguimos implementar um proxy dinamico que facilita ainda mais a vida.

      Obrigado pelo feedback, vou adicionar projetos nos proximos artigos complexos assim que fizer.

      []s,
      Bruno

  • Reply

    Por Rodrigo Michalski em 13 de December de 2013 às 16:39

    Boa tarde Bruno,

    Uma duvida, tenho um webService que retorna 3 objetos (um boolean, um Objeto qualquer e uma String). Como seria o exemplo de um programa para converter esses três retornos em objetos válidos?

    Obrigado.

    • Reply

      Por Bruno Vinicius em 9 de January de 2014 às 14:26

      Rodrigo, a melhor maneira seria criar um Value Object que componha os três valores. Da uma olhada na segunda parte do artigo que eu trato disso la Parte 2

  • Reply

    Por Rogério Souza em 21 de May de 2014 às 11:33

    Bruno Vinicius,

    fiz um algoritmo que faz praticamente um cast do retorno do KSoap para o Objeto original que o WebMethod retorna, ficou muito simples, só não fiz uma comparação de performance, mas acredito que tenha ficado muito leve. Se alguém ficar interessando, me mande um e-mail, vamos conversar! gerimjunior@hotmail.com

    Obrigado.





Desenvolvido por hacklab/ com WordPress