chester's blog

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

Acertando o Horário de Verão em Java

16 Oct 2006 | 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.</p>


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.</p> 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