Android

26 jun, 2017

Como a engenharia da Uber verifica dados durante o tempo de execução com as annotations que você já utiliza

Publicidade

O hipercrescimento da Uber força nossos desenvolvedores a criar estabilidade em nossos aplicativos usando técnicas engenhosas.

Em 2016, por exemplo, criamos e abrimos o código do Mecanismo de Validação de Anotação de Tempo de Execução/Runtime Annotation Validation Engine (RAVE), um framework de validação do modelo de dados que usa processamento de annotation Java para enfrentar a causa número 1 de falhas nos nossos aplicativos Android: NullPointerExceptions (NPEs). Os NPEs são um problema comum com linguagens como Java que não têm a nulidade incorporada no seu type system. Ao aplicar as annotations que você já usa, o RAVE atua como um escudo que protege contra falhas ou erros difíceis de serem identificados causados por dados inválidos.

Para comemorar o lançamento de RAVE 2 de hoje, exploramos como eliminamos a grande maioria dos NPEs em nossos aplicativos usando essa poderosa ferramenta.

 

NullPointerExceptions nos Modelos da Uber

Em aplicativos Android, os NPEs são frequentemente lançados quando os dados nulos são acessados nos modelos. Ferramentas de análise estática, como Infer, ajudam a capturar NPEs em tempo de compilação, mas não são capazes de determinar se os dados recebidos em tempo de execução (ou seja, de rede ou armazenamento) estão em conformidade com o conjunto de expectativas que são descritas pelas annotations presentes nos modelos.

Figura 1: Uma caixa de diálogo de falha, descrita acima, aparece na interface de usuário (UI) do aplicativo do motorista quando um NPE é lançado.

Um dos maiores contribuintes para NPEs em nossos aplicativos Android foi proveniente das suposições sobre os dados que usamos em nossos modelos. Considere o objeto do modelo Rider mostrado abaixo:

public class Rider {
    
    @NonNull private String firstName;
    @NonNull private String lastName;
    
    public Rider(@NonNull String firstname, @NonNull String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    
    @NonNull
    public String firstName() {
        return firstName;
    }
    
    @NonNull
    public String lastName() {
        return lastName;
    }
}

Este modelo usa annotations de nulidade para informar os consumidores se os tipos de retorno podem ou não ser devolvidos null. Embora essas annotations sejam capazes de alertar os desenvolvedores que trabalham em um ambiente de desenvolvimento integrado (IDE) quando os tipos nulos são desmarcados ou incorretamente utilizados, elas não fornecem nenhuma segurança em tempo de execução. Por exemplo, quando um aplicativo recebe dados e os usa para inflar objetos do modelo, não há uma aplicação que exija que os dados estejam em conformidade com as annotations presentes no modelo.

Este cenário ocorreu com frequência ao desserializar objetos da rede enquanto usava o Gson. Uma vez que a Gson não verifica se os objetos do modelo que ele cria respeitam as annotations de nulidade, um NPE pode ocorrer quando você tenta acessar dados anotados com @NonNull e uma API retorna null para algo que é suposto ser @NonNull. Mesmo quando as APIs se comportam de acordo com suas especificações, às vezes seus casos base são mal documentados, desconhecidos ou se modificam com o tempo, o que pode causar NPEs. (Por exemplo, se você está esperando que uma API retorne uma matriz vazia quando ela não tem dados para retornar, mas em vez disso ela retorna null.)

 

RAVE para o Resgate

Para resolver esse problema, a Uber criou RAVE. Quando se trata de prevenir NPEs e erros resultantes do consumo de dados inválidos, o RAVE possui uma variedade de casos de uso. Algumas aplicações incluem:

  • Validar as respostas da rede para garantir que elas correspondam ao que o cliente espera
  • Evitar erros causados por esquemas obsoletos ao buscar dados do disco
  • Verificar a validade dos modelos após mutação
  • Garantir que APIs de terceiros não falhem seu aplicativo ao fornecer dados inesperados

 

O RAVE valida os objetos do modelo em tempo de execução, realizando isso usando o processamento de annotations para gerar código de validação com base nas annotations de suporte do Android em seus modelos. O código de validação é, então, executado quando os dados são recebidos no tempo de execução.

Para melhor ilustrar como funciona o RAVE, nos deixe definir o limite de um aplicativo como a fronteira onde os dados são recebidos (de requisições de dados de rede, armazenamento de disco do dispositivo, etc.) RAVE garante que os dados que entram em seu aplicativo aderem ao conjunto de expectativas descritas pelas suas annotations do modelo. Isto pode ser feito independentemente de onde os dados vêm, demonstrado abaixo:

Figura 2: RAVE garante que os dados que entram em seu aplicativo aderem ao conjunto de expectativas que são descritas pelas annotations do seu modelo.

 

Usando RAVE

RAVE funciona com as annotationsnullness, value constraint e typedef – que você já usa em seus aplicativos Android. Ele também fornece duas annotations que construímos para suportar a validação personalizada: @MustBeTrue e @MustBeFalse.

Para usar o RAVE, você deve optar por validar seu modelo usando a annotations @Validated, mostrada abaixo:

@Validated(factory = RaveValidatorFactory.class)
public class MyModel {

    @NonNull private String someString;

    public MyModel(@NonNull String someString) {
        this.someString = someString;
    }

    @NonNull
    public String getSomeString() {
        return someString;
    }

    @MustBeTrue
    public boolean customValidationLogic() {
        if (someString.contains("!")) {
            return someString.charAt(someString.length() - 1) == '!'; 
        }
        return true;
    }
}

A annotation @Validated leva uma class literal para um ValidatorFactory do RAVE, uma classe concreta que implementa a interface ValidatorFactory do RAVE. Você precisa criar um ValidatorFactory para cada módulo que tenha classes que você deseja validar com o RAVE, mostrado abaixo:

/**
 * A factory class capable of creating a validator whose implementation generated at annotation processing time.
 */
public final class RaveValidatorFactory implements ValidatorFactory {

    @NonNull
    @Override
    public BaseValidator generateValidator() {
        return new RaveValidatorFactory_Generated_Validator();
    }
}

O generated validator é executado quando você estimula RAVE para validar seu modelo chamando a API Rave.validate(). Você sempre deve acessar RAVE usando a API Rave.getInstance().

Quando Rave.validate() é chamado, o RAVE usa RaveValidatorFactory_Generated_Validator para garantir que MyModel.getSomeString() não retorne null e que MyModel.customValidationLogic() retorne verdadeiro. Caso nenhuma dessas condições seja atendida, RAVE lança a exceção verificada, RaveException. Esta exceção retorna uma mensagem de erro e detalhes que ajudam a identificar erros.

 

RAVE em Aplicativos de Android

Depois de integrar o RAVE nos dois maiores pontos de entrada de dados em nossos aplicativos (disco e rede), os NPEs deixaram de ser o principal motivo pelo qual os nossos aplicativos Android falhavam, a desaparecer da nossa lista dos 10 que mais falhavam por volume. Vamos dar uma olhada em dois casos de uso RAVE específicos para destacar como o framework se integra com aplicativos Android:

 

Validação de disco

Antes de ler ou escrever dados no disco, o RAVE garante que os objetos estejam em conformidade com suas annotations, impedindo assim que as falhas do NPE ocorram quando os esquemas dos modelos mudem entre as versões do aplicativo. Fazemos isso usando a API KeyValueStore, cuja implementação valida objetos após serem lidos e antes de serem escritos.

/**
 * Interface that defines methods for reading and writing objects to a database.
 */
public interface KeyValueStore {

    /**
     * Puts an {@link Object} into the database.
     *
     * @param key the key.
     * @param value the {@link Object}.
     */
    void putObject(@NonNull StoreKey key, @NonNull Object value);

    /**
     * Gets an {@link Object} from the database. Note: {@link T} must have an empty constructor.
     *
     * @param key the key to be read from.
     * @param <T> the object type.
     * @return the object read from the database. {@code null} by default.
     */
    @Nullable
    <T> T getObject(@NonNull StoreKey key);
}

Quando KeyValueStore.putObject() é chamado, nossa implementação do KeyValueStore chama Rave.validate (object). Se o objeto não estiver habilitado para RAVE ou não estiver em conformidade com suas annotations, o RaveException é lançado para que os desenvolvedores gerenciem. Isso é feito para garantir que os modelos inválidos não sejam persistentes.

Nós também chamamos Rave.validate() antes de retornar os dados aos consumidores quando KeyValueStore.getObject() é chamado. Isso garante que, se uma definição de modelo tiver sido atualizada em uma versão mais recente do cliente, os dados lidos a partir do disco ainda estejam em conformidade com as annotations que o modelo contém após a atualização. Se não estiver em conformidade com essas annotations, o RAVE lança uma RaveException para que os consumidores gerenciem.

 

Validação de rede

Usamos o Retrofit 2 como a interface para fazer chamadas de rede em nossos aplicativos Android. Para usar o RAVE com o Retrofit 2, escrevemos um convert factory personalizado, RaveConverterFactory, que valida as respostas de rede retornadas pelo conversor que é capaz de desserializar o JSON.

Este conversor valida modelos desserializados quando Rave.validate() é chamado. No caso de o modelo não passar pela validação, uma RaveException é lançada para o consumidor que invocou a chamada de rede. O aplicativo de amostra incluído no repositório RAVE demonstra o uso desse conversor e valida respostas de rede da API do GitHub usando o Retrofit 2 e o RAVE.

Interessado em contribuir com este projeto? Compartilhe suas próprias annotations de validação personalizadas para o RAVE no GitHub e ajude-nos a aumentar a estabilidade do Android.

Behrooz Khorashadi, Eric Leung e Warren Smith são engenheiros de software na equipe de Desenvolvimento Móvel da Uber.

 

***

Este artigo é do Uber Engineering. Ele foi escrito por Dom Chapman. A tradução foi feita pela Redação iMasters com autorização. Você pode conferir o original em:

https://eng.uber.com/rave/