O Java é uma plataforma que se multiplicou exponencialmente, uma de sua grande vantagem é o fato de que você consegue com o mesmo código executar em diferentes plataformas, com isso se 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 utilizem uma linguagem dinâmica, apenas por esse objetivo, 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 calculo tributário, como essa fórmula mudam muito ao ano e variam de município para município, nesse caso é melhor que o código fonte esteja em um banco de dados, por exemplo, para quando for necessário modificar o calculo não seja necessário compilar todo o código fonte e fazer o deploy, correndo o risco do sistema ficar fora do ar mesmo que por alguns instantes.
Sim é possível em Java compilar códigos dinamicamente. Na verdade isso é muito comum do que imaginamos! Por exemplo, o Hibernate para gerenciar das entidades, para facilitar ainda o seu uso a partir da versão 1.6 com a JSR 199 foi criada um API com essa finalidade.
Para demonstrar essa funcionalidade será usado a solução para o problema acima, fazer com que haja troca de fórmula, para facilitar a demonstração e focar na solução serão as 4 operações básicas, sendo que esse código fonte estará no banco de dados, no caso será simulado com arquivos dentro de um txt. Como não podemos referenciar uma classe não compilada, criaremos uma interface Operação, ela será implementada por nossas classes que estarão em nosso “banco de dados”.
public interface Calculo {
Double calcular(Number valorA, Number valorB);
}
Interface na qual será utilizada como referencias as classes compiladas dinamicamente.
Explicando o processo de compilação passo a passo: A classe JavaCompiler, que tem a responsabilidade de fazer a compilação do código-fonte. A chamada ToolProvider.getSystemJavaCompiler() retorna este objeto. 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).
public class JavaDinamicoCompilador {
private JavaCompiler compiler;
private JavaDinamicoManager javaDinamicoManager;
private JavaDinamicoClassLoader classLoader;
private DiagnosticCollector 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();
StandardJavaFileManager standardFileManager = compiler
.getStandardFileManager(diagnostics, null, null);
javaDinamicoManager = new JavaDinamicoManager(standardFileManager, classLoader);
}
@SuppressWarnings("unchecked")
public synchronized Class 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 newClass = (Class) classLoader.loadClass(qualifiedClassName);
return newClass;
}
catch (Exception exception) {
throw new JavaDinamicoException(exception, diagnostics);
}
}
}
Classe responsável pela compilação
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.
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();
}
}
Estrutura de dados que contem o codigo fonte e a classe compilada
Para representar os arquivos envolvidos será utilizado o ForwardingJavaFileManager
que implementa a interface JavaFileManager.
public class JavaDinamicoManager extends ForwardingJavaFileManager {
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;
}
}
Classe responsável por gerenciar as classes compiladas e não compiladas
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.
Com isso foi discutido um pouco sobre a compilação dinâmica do Java no Java, objetivo aqui foi apenas de demonstrar o seu funcionamento básico além de uma pequena explicação do uso da API. Pela sua complexidade o ideal é que ela esteja encapsulado a ponto de ser utilizadas 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 seja compilado dinamicamente.
Referências: