Agendamento de tarefas em aplicações web: um truque com Quartz

quartz O Quartz é a biblioteca mais utilizada para agendar tarefas Java. Simples e muito superior ao mecanismo de Timer do Java EE. Através de sua API, podemos facilmente criar um Job, como um que vai enviar email lembrando a necessidade de mudança de senha, por exemplo:

public  class EnviaEmail implements Job {
	public void execute(JobExecutionContext context) {
		System.out.println("enviando email para evisar mudanca de senha...");
		// acessar api de e-mail aqui
	}		
}

Se quisermos que esse procedimento seja executado uma vez por dia, à meia noite, utilizamos a API do Quartz:

SchedulerFactory sf = new StdSchedulerFactory();
Scheduler sched = sf.getScheduler();

JobDetail job = new JobDetail("dispara_email", "grupo", EnviaEmail.class);
Trigger aMeiaNoite = TriggerUtils.makeDailyTrigger(0, 0);
sched.scheduleJob(job, aMeiaNoite);
		
sched.start();

Poderíamos também ter usado expressões no formato do cron, usando o CronTrigger, como por exemplo acionar o Job a cada 20 segundos:

CronTrigger trigger = 
   new CronTrigger("20_segundos", "grupo", "0/20 * * * * ?");

A API do Quartz vai muito além desses agendamentos triviais, mas vamos focar nos problemas de criar tarefas dessa maneira. É comum esse código de agendamento inicial aparecer em um ponto de inicialização do contexto da sua aplicação web, como no init() de uma servlet, ou em um ContextListener ou em algo equivalente do seu framework.

A classe EnviaEmail será então instanciada a meia noite, e terá seu método execute(JobExecutionContext context) invocado. Se o seu EnviaEmail precisa abrir conexão com o banco, ou uma session do Hibernate, conversar com o SMTP ou qualquer outro recurso, terá de fazer dentro desse método. Há também a opção de se passar essas dependências para o Job através de um mapa, que pode ser populado pelo jobDetail.getJobDataMap().put(chave, valor) e depois recuperado dentro do Job através de context.getJobDetail().getJobDataMap().

Se estamos trabalhando com injeção de dependências, passar as referências a factories e outros objetos para esse mapa-contexto quebra a inversão de controle. Pior: seu código possuirá partes que trabalham perfeitamente com injeção, e outras partes farão acesso às dependências através de lookups, factories e singletons, diminuindo flexibilidade e testabilidade.

Para resolver essa situação e tratar todo o nosso sistema de maneira uniforme, isto é, com injeção de dependências, faremos com que nosso Job seja apenas responsável por acessar uma URL. Essa URL então executará o trabalho em si, como enviar os emails lembrando a necessidade de mudar a senha de tempos em tempos. Com isso, nosso Job não se preocupará em receber dependências: ele é apenas uma chamada usando, por exemplo, do apache httpclient. A sua própria aplicação web tratará essa requisição e executará a action correspondente. Como o Job fará essa requisição de tanto em tanto tempo, o sistema funciona da mesma forma que anteriormente.

Há algumas preocupações com essa abordagem. Uma é que qualquer pessoa pode acessar a URL (caso descoberta) que inicia a task. Isso pode ser evitado utilizando filtros/interceptadores para aceitar apenas requisições de determinado host ou com determinado role para as URLs de serviços agendados.

Mesmo com essa barreira, é importante que sua tarefa seja idempotente: uma segunda requisição para a mesma URL com os mesmos parâmetros e contexto não deve alterar o resultado. Isso protege mais ainda o sistema para uma eventual falha do Scheduler ou de segurança.

Há também outras alternativas ao próprio Quartz para agendamento de tarefa. Uma delas é subverter o Hudson, utilizando-o não apenas como servidor de integração contínua, mas também para gerenciar suas tarefas, aproveitando suas características, gerenciamento de log das falhas e UI. Ou até mesmo usar o cron diretamente disparando requisições através de curl ou similar. No Google App Engine, por exemplo, há um serviço de cron próprio que funciona de maneira similar, disparando requests para determinada URL no momento agendado.

18 Comentários

  1. Paulo Vitor Braga 25/11/2010 at 14:28 #

    E no caso de quem usa JSF 1.2?

    Não daria pra invocar uma ação em um Managed Bean via URL, já que ele não dá suporte a GET. Teríamos que criar um Servlet para designar a tarefa para um managed bean ou ele mesmo (o servlet) tratar a tarefa, correto?

  2. Paulo Silveira 25/11/2010 at 14:33 #

    Oi Vitor! Exatamente. Eu usaria (e uso) uma servlet para esse caso, e pegaria o que os filtros me injetam no request como dependência.

  3. Daniel 25/11/2010 at 18:55 #

    No inicio do post, voce menciona que o Quartz é melhor que os recursos do Java EE. Quando voce diz isso fala dos possiveis TimerBean agendados ate com @Schedule (Java EE 6) ? E em que sentido sao melhores?

  4. Paulo Silveira 25/11/2010 at 19:28 #

    Oi Daniel. Boa pergunta, acho que acabou vazando uma opinião minha. Eu estava comparando sim com o timer service, que no ejb 3.0 era ainda bem fraco. A versão 3.1 realmente reformulou bem, a anotação @Schedule ficou bem próxima do CronTrigger do Quartz, mas eu ainda gosto de poder fazer o fine tuning de pool de threads, tentativas, opções de persistência e uma série de outros ajustes com o Quartz, que eu só poderia fazer no ambiente Java EE utilizando configurações e anotações específicas de cada container (isso se o container der essa flexibilidade de maneira simples).

    Do lado do ejb, há a grande vantagem de que com timer service nosso bean já possa se aproveitar da injeção de Resources e PersistenceContexts, resolvendo boa parte do problema que discutimos no fim do post, caso você va utilizar Java EE 6.

    Hoje, eu estava empolgado com o anúncio das novas featres do Quartz 2.0, acabei deixando isso passar. Creio que o meu “muito” superior tenha sido exagerado, trocaria por “superior na minha opinião”.

  5. Gama 26/11/2010 at 04:16 #

    Alo Paulo, uso o quartz para umas actividades diárias, mas não consigo fazer com que o procedimento não arranque nos feriados, alguma luz?

    exemplo:

  6. Gama 26/11/2010 at 04:23 #

    estou a ver que o html não gostou da minha tag estou usando o CronTrigger

  7. Marcelo 26/11/2010 at 11:14 #

    Muito bacana seu post sobre o Quartz, fiz um post parecido no meu blog
    http://marcelo.newitsolutions.com.br/?p=106

    segue ai para complementar ou ajudar quem precisar das dicas

    Parabéns

  8. Rafael Ponte 26/11/2010 at 14:46 #

    Vitor,

    Para disparar um evento via GET com JSF você vai precisar de frameworks que auxiliem isso, como Restfaces ou mesmo JBoss Seam. Mas dependendo do tipo de tarefa um Servlet resolveria fácil.

    Paulo Silveira,

    Se você estiver utilizando o Spring você poderá obter todos os benefícios de IoC do Spring, além do que, a integração entre Spring e Quartz é muito simples. Vale muito a pena.

    Enfim, muito bom o post, parabéns! E gostei da solução com o HttpClient da Apache.

    Um abraço.

  9. Paulo Silveira 26/11/2010 at 15:52 #

    Oi Ponte. Muito bem observado: alguns frameworks de IoC vao oferecer integracao com o quartz.

    Mesmo os que nao tiverem, voce pode fazer com que seu Job, a traves do contexto, receba o container e instancie um objeto que sera o real responsavel pelo Job.

    Em vez de uma abordagem assim, eu prefiro o truque de usar um http client, pois ai trabalhamos com todos os objetos de uma maneira uniforme.

  10. neylorsousa 26/11/2010 at 23:01 #

    Senhores,vocês conhecem alguma maneira de realizar um agendamento genérico? Ou seja, algum mecanismo de agendamento onde eu possa registrar o job (uma classe java por exemplo) em uma aplicação web e starta-lo de acordo com os dados do agendamento?

  11. Alberto Souza 04/12/2010 at 18:09 #

    @neylorsousa basta adicionar o job no scheduler do quartz já criado. Não tem muito problema, vai apenas precisar de um cadastro de jobs :). No cadastro vai apenas ter que deixar que alguem passe o cron para executar o job

  12. Vanderlei 18/07/2012 at 16:00 #

    Seguinte: tenho uma aplicação(Wicket 1.5 + spring3.1.1 + hibernate 4.6) onde configuro que horas quero executar um certo processamento(semanal às 23:00, diario as 2:00, etc), não existe hora certa… E ainda, caso queira, posso supender esse processamento e reativar, inclsive em outra hora. É possível fazer esse cenário, usando o spring??

  13. Quartz executando mais de 1 vez a tarefa 04/03/2013 at 11:36 #

    OI podem em ajudar? Eu fiz uma aplicacao com Spring e Quartz a tarefa é executada todo dia as 18:00 ate ai tudo bem mas ela executa 02 vezes. Tentei varrer o codigo e não descobri nada acho que é algum bug, alguem podi me ajudar por favor?

  14. Thiago 11/06/2013 at 23:25 #

    Olá Paulo, sei que este post é antigo mas estou pesquisando sobre tarefas agendadas e achei este post. Fiquei com uma dúvida sobre o Quartz para o caso de eu querer utilizá-lo como serviço (EJB) eu poderia fazer isso? tem algum lugar onde possa estudar?

    Abraço.

  15. Paulo Silveira 12/06/2013 at 14:27 #

    oi Thiago. Pro EJB, é melhor usar o sistema de timer novo dele. Misturar o Quartz dentro de Java EE fere um pouco o modelo, ja que em teoria nao poderiamos nem abrir threads dentro do container. Mas da sim pra usar.

  16. Marcos Felipe 29/12/2013 at 15:10 #

    O meu so aparece os println …

    eu faço uma logica e nem vai …

  17. Sandro 31/07/2014 at 10:37 #

    Alguém tem um exemplo desta ideia de agendar um chamada http para aproveitar as injeções de dependência?

Deixe uma resposta