O Java é uma plataforma que se multiplicou exponencialmente e uma de sua grande vantagem é o fato de que o desenvolvedor consegue, com o mesmo código, executar em diferentes plataformas, ou seja, pode programar para servidor, dispositivos embarcados, desktop, etc. com a mesma linguagem. Além disso, a JVM possui suporte para diversas linguagens, das quais possuem diversas características e recursos interessantes. Um mito que existe é que o Java não compila em tempo de execução, o que faz muitas pessoas utilizarem uma linguagem dinâmica. Mas será que isso é verdade?

Possuir uma linguagem dinâmica é muito interessante para alguns projetos específicos, por exemplo, quando se faz um projeto que realiza cálculo tributário: como essa fórmula muda muito durante o ano e varia de município para município, nesse caso é melhor que o código fonte esteja em um banco de dados. Assim, quando for necessário modificar o cálculo não será necessário compilar todo o código fonte e fazer o deploy, correndo o risco de o sistema ficar fora do ar, mesmo que por alguns instantes.

Sim é possível em Java compilar códigos dinamicamente. Por exemplo, o Hibernate pode gerenciar as entidades. Para facilitar ainda mais o seu uso, a partir da versão 1.6 foi criada a API JSR 199.

Para demonstrar essa funcionalidade, será criada uma solução para o problema acima demonstrando as quatro operações básicas. Vamos armazenar o código fonte no banco de dados, dentro de arquivos .txt. Como não podemos referenciar uma classe não compilada, criaremos uma interface Operação para implementada nossas classes que estarão em nosso banco de dados. Observe a Listagem 1.

Listagem 1. Interface Calculo


  public interface Calculo { 
              Double calcular(Number valorA, Number valorB);       
  } 

A Interface será utilizada como referência para as classes compiladas dinamicamente.

O processo de compilação vai funcionar através da classe JavaCompiler, que tem a responsabilidade de fazer a compilação do código-fonte. Se tudo der certo, a chamada a classe vai retornar o objeto ToolProvider.getSystemJavaCompiler(). Se um compilador Java não estiver disponível, o retorno será null.Ele conta com o método getTask(), que retorna um objeto CompilationTask. De posse desse objeto, a chamada call() efetua a compilação do código e retorna um booleano indicando se ela foi feita com sucesso (true) ou se houve falha (false). Observe o código da Listagem 2.

Listagem 2. Classe JavaCompiler


  public class JavaDinamicoCompilador<T> {
   
    private JavaCompiler compiler;
   
    private JavaDinamicoManager javaDinamicoManager;
   
    private JavaDinamicoClassLoader classLoader;
   
    private DiagnosticCollector<JavaFileObject> diagnostics;
   
    public JavaDinamicoCompilador() throws JavaDinamicoException {
      compiler = ToolProvider.getSystemJavaCompiler();
      if (compiler == null) { 
              throw new JavaDinamicoException("Compilador não encontrado");
      }
   
      classLoader = new JavaDinamicoClassLoader
       (getClass().getClassLoader());
      diagnostics = new DiagnosticCollector<JavaFileObject>();
   
      StandardJavaFileManager standardFileManager = compiler
          .getStandardFileManager(diagnostics, null, null);
      javaDinamicoManager = new JavaDinamicoManager
      (standardFileManager, classLoader);
    }
   
    @SuppressWarnings("unchecked")
    public synchronized Class<T> compile(String packageName, 
        String className,
        String javaSource) throws JavaDinamicoException
    {
      try {
        String qualifiedClassName = 
          JavaDinamicoUtils.INSTANCE.getQualifiedClassName(
            packageName, className);
        JavaDinamicoBean sourceObj = new JavaDinamicoBean
          (className, javaSource);
        JavaDinamicoBean compiledObj = new JavaDinamicoBean
         (qualifiedClassName);
        javaDinamicoManager.setSources(sourceObj, compiledObj);
   
        CompilationTask task = compiler.getTask
         (null, javaDinamicoManager, diagnostics,
            null, null, Arrays.asList(sourceObj));
        boolean result = task.call();
   
        if (!result) { 
            throw new JavaDinamicoException
            ("A compilação falhou", diagnostics); 
      }
   
        Class<T> newClass = (Class<T>) 
         classLoader.loadClass(qualifiedClassName);
        return newClass;
   
      }
      catch (Exception exception) {
        throw new JavaDinamicoException(exception, diagnostics);
      }
    }
  }

O processo de compilação envolve dois tipos de arquivos: os códigos-fonte escritos em Java e os arquivos compilados (bytecodes). Na Compiler API estes arquivos são representados por objetos de uma única interface, chamada JavaFileObject. Felizmente, a API disponibiliza uma classe que implementa esta interface, chamada SimpleJavaFileObject e, na escrita de código de compilação dinâmica, deve-se criar uma subclasse de SimpleJavaFileObject e sobrescrever os métodos necessários, conforme a Listagem 3.

Listagem 3.Estrutura de dados que contem o código fonte e a classe compilada


  public class JavaDinamicoBean extends SimpleJavaFileObject {
   
    private String source;
   
    private ByteArrayOutputStream byteCode = new ByteArrayOutputStream();
   
   
    public JavaDinamicoBean(String baseName, String source) {
      super(JavaDinamicoUtils.INSTANCE.createURI
       (JavaDinamicoUtils.INSTANCE.getClassNameWithExt
       (baseName)), Kind.SOURCE);
      this.source = source;
    }
               
    public JavaDinamicoBean(String name) {
      super(JavaDinamicoUtils.INSTANCE.createURI(name), Kind.CLASS);
    }
   
    @Override
    public String getCharContent(boolean ignoreEncodingErrors) {
      return source;
    }
   
    @Override
    public OutputStream openOutputStream() {
      return byteCode;
    }
   
    public byte[] getBytes() {
      return byteCode.toByteArray();
    }
  }

Para representar os arquivos envolvidos será utilizado oForwardingJavaFileManager <JavaFileManager> que implementa a interface JavaFileManager. Observe a Listagem 4.

Listagem 4.Classe responsável por gerenciar as classes compiladas e não compiladas


  public class JavaDinamicoManager extends 
   ForwardingJavaFileManager<JavaFileManager> {
    private JavaDinamicoClassLoader classLoader;
    
   
    private JavaDinamicoBean codigoFonte;
   
    private JavaDinamicoBean arquivoCompilado;
    
    public JavaDinamicoManager(JavaFileManager fileManager, 
     JavaDinamicoClassLoader classLoader)
    {
      super(fileManager);
      this.classLoader = classLoader;
    }
   
    public void setSources(JavaDinamicoBean sourceObject, 
     JavaDinamicoBean compiledObject) {
      this.codigoFonte = sourceObject;
      this.arquivoCompilado = compiledObject;
      this.classLoader.addClass(compiledObject);
    }
   
    @Override
    public FileObject getFileForInput(Location location, String packageName,
        String relativeName) throws IOException
    {
      return codigoFonte;
    }
   
    @Override
    public JavaFileObject getJavaFileForOutput(Location location,
        String qualifiedName, Kind kind, FileObject outputFile)
        throws IOException
    {
      return arquivoCompilado;
    }
   
    @Override
    public ClassLoader getClassLoader(Location location) {
      return classLoader;
    }
  }

Para que ela possa ser utilizada, a JVM deve ser capaz de reconhecê-la como uma classe da aplicação, a fim de que possa carregá-la quando chegar o momento.

O componente responsável pelo carregamento das classes das aplicações Java durante a execução é o Class Loader.

Portanto, para que a JVM saiba da existência das novas classes compiladas, é necessário implementar um Class loader customizado, que fica atrelado ao gerenciador de arquivos. Ele deve estender a classe ClassLoader (do pacote java.lang) e tem a responsabilidade de carregar as classes recém-criadas.

Pela complexidade da API, o ideal é que ela esteja encapsulada a ponto de ser utilizada várias vezes em diversos projetos. O objetivo aqui não foi desmerecer em nenhum momento as outras linguagens de programação rodando ou não em cima da JVM, afinal todas elas têm sua importância. O objetivo foi demonstrar que não existe a necessidade de mudar de linguagem caso seu problema seja ter um código que necessite ser compilado dinamicamente.