Vraptor3: sugestão de exception handler

12 respostas
G

O assunto já havia sido discutido em dois tópicos (http://guj.com.br/posts/list/136307.java e http://guj.com.br/posts/list/143727.java) e também há uma issue no github para o caso.

Relato aqui de novo minhas considerações sobre o uso de um elegante exception handler no vraptor. Resolvi ressuscitar a discução porque recebi algumas MP me pedindo o código do exception handler, e achei que é bem melhor termos uma solução assim build-in no próprio vraptor do que ficar fazendo isso em cada projeto separadamente, já que o vraptor ainda não suporta tal recurso.

Diz a regra que todas as especificações devem ser tratadas na sua camada mais externa com os nossos amigos try and catch. Porém muitas vezes tratar isso a cada controller e fazer simplesmente um try and catch para simplesmente redirecionar para uma view e exibir uma mensagem já tratada torna-se bem repetitivo.

Um exemplo: minhas aplicações usam EJB remoto, e o vraptor atua apenas como controlador. Se estou em um método de editar usuário, e quando vou salvar um usuário dá alguma regra de negócio, por exemplo usuário já existente, eu normalmente volto para a tela de edição e exibo a mensagem de erro. Manualmente eu faria isso aqui:

public void edit(Long id) {
    [...]
}

public void store(User user) {
    try {
        // podem ocorrer erros de negócio aqui
        userService.storeNewUser(user);
        // se der certo redireciona para a listagem
        result.redirectTo(getClass()).listAll();
    } catch(MyBusinessException e) {
        // deu erro, adiciono manualmente nas mensagens
        validator.add(new ValidationMessages(e.getMessage(), "error");
        // redireciono para a tela de edição para o usuario corrigir o problema
        result.forwardTo(getClass()).edit(user.getId());
    }
    // posso tratar outras exceptions aqui
}

Mas como meus projetos normalmente são muito grandes (um deles possui ~350 controllers) ficaria muito cansativo fazer isso método por método. Nesses outros tópicos tirei minhas dúvidas com a galera do Vraptor sobre como fazer um bom exception-handler, estilo ao que o nosso velho amigo Struts 1 fazia elegantemente. Usando minha proposta o código acima fica reduzido em algo como isso:

public void edit(Long id) {
    [...]
}

public void store(User user) {
    // se ocorrer um erro não tratado vai para a tela de edição
    result.onErrorUse(getClass()).edit(user.getId());

    // podem ocorrer erros de negócio aqui
    userService.storeNewUser(user);

    // se der certo redireciona para a listagem
    result.redirectTo(getClass()).listAll();
}

O código não apenas ficou bem menor como mais legível do que um monte de try and catch aninhado. Obvio que nada me impede de tratar as exceptions manualmente, porém o exception handler irá atuar sempre o programador não tratar tal exception. Fiz uns testes e até mesmo erro de JSP é capturado aqui. Há mais relatos nesse meu post: http://guj.com.br/posts/list/45/136307.java#804730

Nas discuções anteriores houve preocupações que isso pode parecer um try/catch mascarando todos os erros. Porém esse erro é repassado para a tela, exibindo a mensagem e não a mascarando. http://guj.com.br/posts/list/45/136307.java#804743

Inclusive a sugestão do Paulo e do Sérgio é de usar o web.xml e o atributo error-page, o que na minha opinião não é bom porque assim só posso exibir uma tela bonita, ao invés de poder voltar para a tela para que o erro possa ser tratado. (http://guj.com.br/posts/list/45/136307.java#805689)

(edição) Os códigos seguem abaixo já que não consigo enviar meus gists. A minha sugestão é adicionar o método onErrorUse na interface Result e suas implementações, e alterar o ExecuteMethodInterceptor para trabalhar com essa classe, conforme o que escrevi abaixo.

12 Respostas

G

Seguem os códigos, já que não estou conseguindo autenticar no github para colocar meus gists.

CustomResult, que substitui o DefaultResult e deve ser recebida no construtor das classes. Ela disponibiliza o método onErrorUse para indicar onde ir em caso de erro.

@Component
@RequestScoped
public class CustomResult
    implements Result {

    private final Result delegate;
    private final Proxifier proxifier;
    private Object[] args;
    private Method method;

    public CustomResult(Result result, Proxifier proxifier) {
        this.delegate = result;
        this.proxifier = proxifier;
    }

    public <T> T onErrorUse(Class<T> controller) {
        return proxifier.proxify(controller, new MethodInvocation<T>() {

            @Override
            public Object intercept(T proxy, Method method, Object[] args, SuperMethod superMethod) {
                CustomResult.this.method = method;
                CustomResult.this.args = args;
                return null;
            }

        });
    }

    public Method getErrorMethod() {
        return this.method;
    }

    public Object[] getErrorArgs() {
        return this.args;
    }

    // implemente aqui os demais métodos da classe mãe fazendo delegate dos métodos
}

E esta é a classe CustomExecuteMethodInterceptor que sobrescreve a ExecuteMethodInterceptor adicionando o controle do exception handler.

@Component
@RequestScoped
public class CustomExecuteMethodInterceptor
    extends ExecuteMethodInterceptor {

    private final CustomResult result;
    private final MethodInfo info;
    private final Validator validator;
    private final Outjector outjector;

    public CustomExecuteMethodInterceptor(CustomResult result, MethodInfo info, Validator validator, Outjector outjector) {
        super(info, validator);
        this.result = result;
        this.info = info;
        this.validator = validator;
        this.outjector = outjector;
    }

    @Override
    public void intercept(InterceptorStack stack, ResourceMethod method, Object resourceInstance)
        throws InterceptionException {
        try {
            Method reflectionMethod = method.getMethod();
            Object[] parameters = info.getParameters();
            Object result = reflectionMethod.invoke(resourceInstance, parameters);
            if (validator.hasErrors()) { // method should have thrown ValidationError
                throw new InterceptionException("There are validation errors and you forgot to specify where to go.");
            }

            if (!reflectionMethod.getReturnType().equals(Void.TYPE)) {
                info.setResult(result);
            }

            stack.next(method, resourceInstance);
        } catch (Exception e) {
            if (result.getErrorMethod() == null) {
                throw new RuntimeException(ExceptionUtils.getRootCause(e));
            }

            final List<ValidationMessage> messages = new LinkedList<ValidationMessage>();
            final Throwable cause = ExceptionUtils.getRootCause(e);

            if (cause instanceof ConstraintViolationException) {
                ConstraintViolationException constrains = (ConstraintViolationException) cause;
                for (ConstraintViolation<?> violation : constrains.getConstraintViolations()) {  
                    messages.add(new ValidationMessage(violation.getMessage(), violation.getPropertyPath()));  
                }
            } else {
                messages.add(new ValidationMessage(cause.getMessage(), "error"));
            }

            result.include("errors", messages);

            outjector.outjectRequestMap();

            try {
                final Method errorMethod = result.getErrorMethod();
                final Object target = result.forwardTo(errorMethod.getDeclaringClass());

                errorMethod.invoke(target, result.getErrorArgs());
            } catch (Exception e0) {
                throw new InterceptionException(e0);
            }
        }
    }
}
Paulo_Silveira

garcia-jj:

Inclusive a sugestão do Paulo e do Sérgio é de usar o web.xml e o atributo error-page, o que na minha opinião não é bom porque assim só posso exibir uma tela bonita, ao invés de poder voltar para a tela para que o erro possa ser tratado. (http://guj.com.br/posts/list/45/136307.java#805689)

Oi Garcia

Entendo seu ponto.

Mas se ha como “corrigir” o problema voltando na pagina e digitando alguma outra coisa, isso nao seria um erro de validacao em vez de um erro grave?

G

Paulo Silveira:
Oi Garcia

Entendo seu ponto.

Mas se ha como “corrigir” o problema voltando na pagina e digitando alguma outra coisa, isso nao seria um erro de validacao em vez de um erro grave?

Sendo validação ou erro grave é interessante ter uma forma automagica de fazer isso, ao invés de fazer try and catch manual. Em projetos grandes como os que tenho (6 atualmente usando vraptor), torna-se bem cansativo.

Mas não entendi, nesse caso, a diferença entre ser um erro grave ou validação. De qualquer forma, o que você não tratar na mão ele captura e devolve para onde você configurou. Sendo assim, se der até mesmo um NoEntityFound do JPA ele retorna para a tela e exibe o erro.

J
garcia-jj:
Seguem os códigos, já que não estou conseguindo autenticar no github para colocar meus gists.

CustomResult, que substitui o DefaultResult e deve ser recebida no construtor das classes. Ela disponibiliza o método onErrorUse para indicar onde ir em caso de erro.

@Component
@RequestScoped
public class CustomResult
    implements Result {

    private final Result delegate;
    private final Proxifier proxifier;
    private Object[] args;
    private Method method;

    public CustomResult(Result result, Proxifier proxifier) {
        this.delegate = result;
        this.proxifier = proxifier;
    }

    public <T> T onErrorUse(Class<T> controller) {
        return proxifier.proxify(controller, new MethodInvocation<T>() {

            @Override
            public Object intercept(T proxy, Method method, Object[] args, SuperMethod superMethod) {
                CustomResult.this.method = method;
                CustomResult.this.args = args;
                return null;
            }

        });
    }

    public Method getErrorMethod() {
        return this.method;
    }

    public Object[] getErrorArgs() {
        return this.args;
    }

    // implemente aqui os demais métodos da classe mãe fazendo delegate dos métodos
}

E esta é a classe CustomExecuteMethodInterceptor que sobrescreve a ExecuteMethodInterceptor adicionando o controle do exception handler.

@Component
@RequestScoped
public class CustomExecuteMethodInterceptor
    extends ExecuteMethodInterceptor {

    private final CustomResult result;
    private final MethodInfo info;
    private final Validator validator;
    private final Outjector outjector;

    public CustomExecuteMethodInterceptor(CustomResult result, MethodInfo info, Validator validator, Outjector outjector) {
        super(info, validator);
        this.result = result;
        this.info = info;
        this.validator = validator;
        this.outjector = outjector;
    }

    @Override
    public void intercept(InterceptorStack stack, ResourceMethod method, Object resourceInstance)
        throws InterceptionException {
        try {
            Method reflectionMethod = method.getMethod();
            Object[] parameters = info.getParameters();
            Object result = reflectionMethod.invoke(resourceInstance, parameters);
            if (validator.hasErrors()) { // method should have thrown ValidationError
                throw new InterceptionException("There are validation errors and you forgot to specify where to go.");
            }

            if (!reflectionMethod.getReturnType().equals(Void.TYPE)) {
                info.setResult(result);
            }

            stack.next(method, resourceInstance);
        } catch (Exception e) {
            if (result.getErrorMethod() == null) {
                throw new RuntimeException(ExceptionUtils.getRootCause(e));
            }

            final List<ValidationMessage> messages = new LinkedList<ValidationMessage>();
            final Throwable cause = ExceptionUtils.getRootCause(e);

            if (cause instanceof ConstraintViolationException) {
                ConstraintViolationException constrains = (ConstraintViolationException) cause;
                for (ConstraintViolation<?> x : constrains.getConstraintViolations()) {
                    String s = x.getRootBeanClass().getSimpleName() + "." + x.getPropertyPath() + " " + x.getMessage();
                    messages.add(new ValidationMessage(s, "error"));
                }
            } else {
                messages.add(new ValidationMessage(cause.getMessage(), "error"));
            }

            result.include("errors", messages);

            outjector.outjectRequestMap();

            try {
                final Method errorMethod = result.getErrorMethod();
                final Object target = result.forwardTo(errorMethod.getDeclaringClass());

                errorMethod.invoke(target, result.getErrorArgs());
            } catch (Exception e0) {
                throw new InterceptionException(e0);
            }
        }
    }
}

Garcia achei muito boa esta opcao pelo menos pode se padronizar mensagens mais amigaveis para o seu sistema nao esperando aquelas exception enormes, e diz qual programador que consegue criar um sistema 100% sem erros.

G

[email removido], há um erro nesse código. Agora relendo notei que no CustomExecuteMethodInterceptor o correto é usar (na linha 45):

for (ConstraintViolation<?> violation : constrains.getConstraintViolations()) { messages.add(new ValidationMessage(violation.getMessage(), violation.getPropertyPath())); }

Lucas_Cavalcanti

garcia, comita essas classes no seu fork do vraptor por favor?

G

Lucas, faço isso logo a noite. Abraços

D

garcia-jj:

Mas não entendi, nesse caso, a diferença entre ser um erro grave ou validação. De qualquer forma, o que você não tratar na mão ele captura e devolve para onde você configurou. Sendo assim, se der até mesmo um NoEntityFound do JPA ele retorna para a tela e exibe o erro.

Garcia, concordo contigo em gênero, grau e número!!! :thumbup: Considero eXtremamente salutar (especialmente em Sistemas (em regime de) [color=red]Intranet[/color]) apresentar a MSG completa de Erro, até p/o seu depuramento/correção + rápido (o q é de todo interesse do ususário e interessados)!!

G

Lucas, a idéia está ainda bem verde. Após uma série de outras conversas aqui no fórum, acho melhor eu publicar no meu gist e a partir dele começar a fazer crescer a idéia. Na minha opinião o exception handler está bem bacana, PORÉM aquela parte de como trabalhar com as mensagens ainda não me agrada, e não sei como fazer de outra forma.

Como o derlon já citou em outro tópico, não é bom ficar misturando validações com regras de negócio que vem das suas business-classes, embora não deixam de serem erros de validação também.

Assim que chegar em casa publico no meu gist do github e publico por aqui o endereço do gist.

Lucas_Cavalcanti

blz… vou então deixar o exception_handler pra versão 3.1.3 então, ok?

G

Sure. Quando sai a 3.1.2? Aquelas alterações do BeanValidator e LocalizedConverters caem nela?

Já estou ansioso pela nova versão, notei várias coisinhas interessantes lá.

Lucas_Cavalcanti

creio que na próxima semana… e suas alterações estarão lá =)

Criado 15 de abril de 2010
Ultima resposta 23 de abr. de 2010
Respostas 12
Participantes 5