chester's blog

technology, travel, comics, books, math, web, software and random thoughts

Acertando O Horário De Verão Em Java

| Comments


ATUALIZAÇÃO: O método sugerido aqui não lê a configuração de timezone do servidor (e, portanto, exige atualização a cada ano) . Se o seu servidor é Linux ou assemelhado, sugiro usar o timefix.


Se você desenvolve aplicações Java para rodar em servidores, já deve ter se deparado com este problema: a máquina virtual nunca “acerta” o horário de verão, mesmo que você tenha configurado corretamente o timezone do sistema operacional do servidor para acertar o relógio.

Este artigo aponta algumas soluções para o problema – incluindo uma que não exige recompilação de código e não se restringe à JVM da Sun.

Por que isso acontece?

Dois dos meus vilões favoritos são os culpados deste quiproquó: o governo brasileiro e a Sun.

O governo muda a regra do horário a cada ano – em 2006, a justificativa foram as urnas eletrônicas, e a questão é historicamente complicada no nosso país. Como dizia um administrador de sistemas que conheci, “no mundo todo o horário de verão é uma fórmula; no Brasil é uma lei”.

A Sun, por outro lado, deciciu que a máquina virtual Java deve gerenciar por si só todo o esquema de timezones, essencialmente ignorando o sistema hospedeiro (que se limita a informar o lugar onde o servidor se encontra e o horário UTC) – uma idéia razoável no passado, mas que desconsidera o fato de que Windows, Linux e tantos outros S.O.s modernos já resolvem este problema.

Como eu resolvo?

A solução comummente adotada é alterar o código para criar um timezone com as novas regras, e setá-lo como default. Por exemplo:

import java.util.SimpleTimeZone;
import java.util.TimeZone;
...
TimeZone.setDefault(
    new SimpleTimeZone(
        TimeZone.getDefault().getRawOffset(),
            "America/Sao_Paulo",
            Calendar.NOVEMBER,
            05,
            ,
            3600000*1+60000*,  // 01h00
            Calendar.FEBRUARY,
            25,
            ,
            3600000*2+60000*,  // 02h00
            3600000));

Neste caso, ele está ajustando o horário de verão para iniciar em 05/Nov às 01h00 e terminar em 25/Fev às 02h00 (é preferível usar estes horários para evitar que a mudança de data bagunce outros processos, mas você pode começar/terminar à meia-noite, se preferir).

O problema é que você tem que chamar o código na incialização da aplicação. Se ela for uma aplicação stand-alone até que é fácil – no entanto, se ela residir em um web container (Tomcat, WebSphere, etc.) fica mais difícil. Fora que você tem que recompilar todas as aplicações a cada ano, o que nem sempre é desejável. Mas é um caminho.

Outra solução: mexer na máquina virtual

O Vitor Buitoni dá uma solução coerente para o problema: se a máquina virtual decide agir como um sistema operacional, vamos tratá-la como tal, e trocar a configuração de timezone nela também (brinde: um jeito bem esperto de fazer o Tomcat/WebSphere/qualquer web container executar a atualização sem precisar reiniciar).

Requer uma certa “pedalada”, pois o utilitário necessário para fazer a mudança (JavaZic) não é público, ele usa uma manobra para obter seu fonte e compilar. Além disso, ela se restringe à JVM da Sun. E também há quem alegue que a seção “Java Technology Restrictions” da licença da Sun para o runtime da máquina virtual se aplique neste caso (o texto da versão 5.0 é menos restritivo, mas eu não sou advogado, então não me arrisco aqui).

Se nada disso te atrapalha, essa é a solução mais “limpinha”, sob o ponto de vista técnico, e eu recomendo.

Um caminho intermediário

Num antigo trabalho onde não era viável usar a solução do Buitoni nem alterar todas as aplicações, lancei mão de um expediente, que decidi publicar neste artigo: a criação de uma classe que faça o ajuste do timezone da JVM sem que tenhamos que alterar a aplicação a cada ano.

O código-fonte segue abaixo. Uma vez compilada e colocada no hv.jar (você pode usar o script Ant para fazer isto), você pode alterar o script de incialização do seu servidor para acessá-la. Suponhamos que, neste script, você tenha a chamada:

<br /> java -cp jar1.jar;jar2.jar ClassePrincipal arg1 arg2 arg3…<br />

Basta adicionar a classe no classpath, e colocá-la antes da classe principal, i.e.:

<br /> java -cp <font color=red>hv.jar;</font>jar1.jar;jar2.jar <font color=red>br.inf.chester.hv.HorarioDeVerao</font> ClassePrincipal arg1 arg2 arg3…<br />

Com isso, a máquina virtual irá chamar a classe para fazer o ajuste, e essa carregará o programa original, com os parâmetros originais.

O mais importante: no ano seguinte, bastará gerar a nova versão do hv.jar, substitui-la no servidor e reiniciar. Sem maiores complicações. Se preferir, você pode usar a classe apenas para centralizar os ajustes, chamando o método ajustaTimeZone() de dentro do seu código.

Melhorias

Seria bacana ter uma opção de hot-deployment, como na solução do Buitoni. Além disso, a classe poderia pegar os parâmetros do s.o. hospedeiro (dispensando a substituição anual, ao custo de não ser mais tão multiplataforma assim). Um dia desses eu faço essa mudança, mas quem quiser fica livre pra tentar.

Listagem 1: HorarioDeVerao.java (classe que ajusta o Horário de Verão)

package br.inf.chester.hv;
 
/*
 * Copyright © 2006 Carlos Duarte do Nascimento (Chester)
 * cd@pobox.com
 *
 * Este programa é um software livre; você pode redistribui-lo e/ou
 * modifica-lo dentro dos termos da Licença Pública Geral GNU como
 * publicada pela Fundação do Software Livre (FSF); na versão 2 da
 * Licença, ou (na sua opnião) qualquer versão.
 *
 * Este programa é distribuido na esperança que possa ser util,
 * mas SEM NENHUMA GARANTIA; sem uma garantia implicita de ADEQUAÇÂO
 * a qualquer MERCADO ou APLICAÇÃO EM PARTICULAR. Veja a Licença
 * Pública Geral GNU para maiores detalhes.
 *
 * Você deve ter recebido uma cópia da Licença Pública Geral GNU
 * junto com este programa, se não, escreva para a Fundação do Software
 * Livre(FSF) Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */
 
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Calendar;
import java.util.SimpleTimeZone;
import java.util.TimeZone;
 
/**
 * Permite que as aplicações Java ajustem o timezone da JVM para o horário de
 * verão do Brasil, de duas formas:
 * <p>
 * - Chamando o método <code>ajustaTimeZone()</code><br>
 * - Trocando a chamada da jvm, passando esta classe (que por sua vez irá chamar
 * a classe original), vide método <code>main</code> abaixo.
 *
 * @author chester
 */
 
public class HorarioDeVerao {
 
    /**
     * Timezone para o horário de verão de 2006 Inicio: 05/Nov; Fim: 25/Fev;
     * Hora início: 01h00; Hora fim: 02h00
     */
    public static TimeZone tz = new SimpleTimeZone(TimeZone.getDefault()
            .getRawOffset(), "America/Sao_Paulo", Calendar.NOVEMBER, 05, ,
            3600000 * 1 + 60000 * , Calendar.FEBRUARY, 25, ,
            3600000 * 2 + 60000 * , 3600000);
 
    /**
     * Seta o timezone da máquina virtual Java para o nosso timezone customizado
     */
    public static void ajustaTimeZone() {
        TimeZone.setDefault(tz);
    }
 
    /**
     * Ajusta o timezone e chama o método <code>main</code> de uma classe.
     * <p>
     * A classe é determinada pelo primeiro parâmetro. Na prática, este método
     * permite que se substitua:
     * <p>
     * <code>java minhaClasse p1 p2 p3 </code>
     * <p>
     * por
     * <p>
     * <code>java HorarioDeVerao minhaClasse p1 p2 p3</code>
     * <p>
     * e a classe será executada, só que sem problemas de horário
     *
     * @param args
     *            Argumentos que serão passados para a classe
     */
    public static void main(String args[]) throws Throwable {
 
        // Nome da classe cujo método main() queremos chamar
        String nomeClasse = args[];
 
        // Vamos encontrar o método main
        Method metodoMain;
 
        // Cria um array cujo único elemento é a classe de um array
        // de strings (já que main() recebe apenas um array de strings)
        Class[] tiposArgs = new Class[1];
        tiposArgs[] = String[].class;
 
        // Cria um array de argumentos a passar para o método main
        // cujo único elemento são os argumentos que recebemos
        // (tirando o primeiro, que é o próprio nome da classe)
        Object[] listaArgs = new Object[1];
        listaArgs[] = removeElemento(args, );
 
        // Acerta o timezone
        ajustaTimeZone();
 
        // Recupera a classe que ia ser executada originalmente
        // e o seu método main
        metodoMain = Class.forName(nomeClasse).getDeclaredMethod("main",
                tiposArgs);
        // Executa o método main, passando os argumentos que recebemos
        try {
            metodoMain.invoke(null, listaArgs);
        } catch (InvocationTargetException ex) {
            throw ex.getTargetException();
        }
    }
 
    /**
     * Remove um elemento de um array de strings
     *
     * @param a
     *            Array cujo elemento se quer remover
     * @param pos
     *            Posição do elemento a remover
     * @return Array com o elemento removido
     */
    private static String[] removeElemento(String[] a, int pos) {
        if (pos <  || pos >= a.length)
            return a;
        String[] aa = new String[a.length - 1];
        if (pos > )
            System.arraycopy(a, , aa, , pos);
        if (pos < a.length - 1)
            System.arraycopy(a, pos + 1, aa, pos, aa.length - pos);
        return aa;
    }
 
}

Listagem 2: build.xml (exemplo de script para gerar o hv.jar)

<?xml version="1.0" encoding="UTF-8"?>
<project name="hv" default="gera_tudo" basedir=".">
 
    <property name="SOURCE" value="src" />   <!-- onde ficam os fontes (.java) -->
    <property name="BUILD" value="temp" />   <!-- onde sao gerados os .class -->
 
    <path id="classpath">
        <pathelement path="${java.class.path}" />
    </path>
 
    <target name="gera_tudo" description="Compila todos os fontes e gera o jar">
        <delete dir="${BUILD}" />
        <mkdir dir="${BUILD}" />
        <javac srcdir="${SOURCE}" destdir="${BUILD}/" target="1.3"
          source="1.3">
            <classpath refid="classpath" />
        </javac>
        <jar jarfile="hv.jar" basedir="${BUILD}">
        </jar>
        <delete dir="${BUILD}" />
    </target>
 
</project>

Comments