Vraptor3: sugestão de exception handler

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:

[code]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
}[/code]

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:

[code]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();

}[/code]

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.

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.

[code]@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

}[/code]

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

[code]@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);
        }
    }
}

}[/code]

[quote=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)[/quote]

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?

[quote=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?[/quote]

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.

[quote=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.

[code]@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

}[/code]

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

[code]@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);
        }
    }
}

}[/code][/quote]

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.

jvds@bol.com.br, 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())); }

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

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

[quote=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.[/quote]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)!!

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.

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

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á.

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