Violando o encapsulamento

O encapsulamento de código possui várias características interessantes para o desenvolvimento de código, como proibir o acesso direto aos atributos da classe evitando assim a atribuição de valores inválidos, permitir verificações e modificações através de métodos acessores e modificadores, diminuir o acoplamento, entre outras. Por exemplo um objeto Endereco não pode ter o atributo numeroRua menor ou igual a zero, por isso, podemos colocar essa verificação em um método modificador, não permitindo valores inválidos. Contudo, em alguns casos o acesso à atributos e métodos privados se faz necessário. Por exemplo, quando achamos um bug em uma biblioteca de terceiros e não temos o código para modificar. Se via depuração de código identificamos que precisamos acessar um método ou modificar um atributo privado para realizar um workaround, é necessário acessar esses atributos. Ou, em outro caso, precisamos saber todos os atributos que são serializáveis de uma determinada classe, incluindo os atributos privados.

Por esses motivos, existe uma maneira de violar o encapsulamento. Atenção: GAMBIARRA WARNING! :D. Utilize somente esse recurso, caso seja realmente necessário, pois por permitir acesso à atributos e métodos privados, temos acesso à coisas proibidas e que não deveriam ser acessadas.

Para demonstrar essa funcionalidade, utilizaremos a seguinte classe:

import java.util.UUID;

public class Produto {
	private String idInterno;
	private int sku;
	private String nome;
	private double valor;

	public int getSku() {
		return sku;
	}

	public void setSku(int sku) {
		this.sku = sku;
		this.idInterno = generateIdInterno(sku);
	}

	public String getNome() {
		return nome;
	}

	public void setNome(String nome) {
		this.nome = nome;
	}

	public double getValor() {
		return valor;
	}

	public void setValor(double valor) {
		if (valor > 0) {
			this.valor = valor;
		}
	}

	private String generateIdInterno(int sku) {
		return UUID.randomUUID() + "-" + sku;
	}

	public String getIdInterno() {
		return idInterno;
	}

	@Override
	public String toString() {
		return "Produto [idInterno=" + idInterno + ", sku=" + sku + ", nome="
				+ nome + ", valor=" + valor + "]";
	}

}

Verifque na linha 31 que o valor só pode ser um double positivo e diferente de zero,  pois o atributo é privado (linha 07) e só é modificado através do setter (linha 30). Verifique também que o método generateIdInterno (linha 36) é privado e somente a própria classe tem acesso (linha 15). Para termos acesso a esses membros privados, criei uma classe utilitária que facilita o trabalho com reflection do Java.

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class ReflectionUtil {
	private Object object;

	public ReflectionUtil(Object obj) {
		this.object = obj;
	}

	public Field[] getAllFields() {
		return this.object.getClass().getDeclaredFields();
	}

	public Method[] getAllMethods() {
		return this.object.getClass().getDeclaredMethods();
	}

	private Field getFieldByName(String fieldName) {
		Field field = null;
		try {
			field = this.object.getClass().getDeclaredField(fieldName);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return field;
	}

	private Method getMethodByName(String methodName,
			Class<?>... parameterTypes) {
		Method method = null;
		try {
			method = this.object.getClass().getDeclaredMethod(methodName, parameterTypes);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return method;
	}

	public Object getValue(String fieldName) {
		return getValue(getFieldByName(fieldName));
	}

	public Object getValue(Field field) {
		try {
			if (field != null && this.object != null) {
				if (!field.isAccessible()) {
					field.setAccessible(true);
				}
				return field.get(this.object);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}

	public void setValue(String fieldName, Object value) {
		setValue(getFieldByName(fieldName), value);
	}

	public void setValue(Field field, Object value) {
		try {
			if (field != null && this.object != null) {
				if (!field.isAccessible()) {
					field.setAccessible(true);
				}
				field.set(this.object, value);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}

	}

	public Object invokeMethodByName(String methodName,
			Object... params) {
		int paramsSize = params.length;
		Class<?>[] paramsTypes = new Class<?>[paramsSize];
		for (int idx = 0; idx < paramsSize; idx++) {
			paramsTypes[idx] = params[idx].getClass();
		}
		return invokeMethod(
				getMethodByName(methodName, paramsTypes),
				params);
	}

	public Object invokeMethod(Method method, Object... params) {
		try {
			if (method != null && this.object != null) {
				if (!method.isAccessible()) {
					method.setAccessible(true);
				}
				return method.invoke(this.object, params);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}
}

O segredo de permitir acesso aos metodos privados é chamar o método setAccessible com o valor true para métodos ou atributos. Dessa forma o JRE ignora a verificação de acesso e podemos acessar métodos private, default e protected que não eram acessados à partir de outras classes.

Nesse utilitário, o método getValue retorna um objeto com o valor de um determinado atributo, independente do modificador de acesso, se o valor for do tipo primitivo ele vai retornar o seu wrapper correspondente. O método setValue, atribui um valor para um atributo, se o valor passado for o wrapper e o campo for primitivo, o valor é convertido automaticamente. O método invokeMethod chama um método, também independente do modificador de acesso passando os devidos parametros. Pode-se utilizar os métodos getAllFields e getAllMethods para listar os atributos e métodos respectivamente. Uma melhoria nessa classe seria lançar o erro ocorrido, mas para a finalidade que pretendo utilizar, o tratamento dentro do método já é suficiente.

Para validar o funcionamento, vamos atribuir um produto um valor negativo e invocar o método privado generateIdInterno, o que não seria possível utilizando o código da maneira convencional.

import java.lang.reflect.Method;

public class PrivateAccessTest {

	public static void main(String[] args) {
		Produto p = new Produto();
		p.setNome("ABCD");
		p.setSku(1234);
		p.setValor(100.0);
		System.out.println(p);
		p.setValor(-50.0);
		System.out.println("Alteração para valor negativo não permitida: " + p);

		ReflectionUtil ru = new ReflectionUtil(p);
		ru.setValue("valor", -50);
		System.out.println("Alteração via reflexão: " + p);
		System.out.println("Recuperação de atributo privado: "
				+ ru.getValue("idInterno"));
		
		Method[] methods = ru.getAllMethods();
		for(Method m: methods) {
			if("generateIdInterno".equals(m.getName())) {
				System.out.println("Invocação de método privado: "
						+ ru.invokeMethod(m, 888));
			}
		}

	}

}

Na linha 11 tentamos alterar o valor do produto para um valor negativo, o setter correspondente não vai permitir, mantendo o valor do produto igual anteriormente. Se invocarmos com reflexão (linha 15), o atributo é acessado diretamente, sem passar pelo método e o valor na classe é alterado. Na linha 18 recuperamos um valor de um atributo privado e na linha 24 invocamos um método privado.

A saída desse código é a seguinte:

Produto [idInterno=14e77645-506c-46b1-94fa-d0c1f7bc6b32-1234, sku=1234, nome=ABCD, valor=100.0]
Alteração para valor negativo não permitida: Produto [idInterno=14e77645-506c-46b1-94fa-d0c1f7bc6b32-1234, sku=1234, nome=ABCD, valor=100.0]
Alteração via reflexão: Produto [idInterno=14e77645-506c-46b1-94fa-d0c1f7bc6b32-1234, sku=1234, nome=ABCD, valor=-50.0]
Recuperação de atributo privado: 14e77645-506c-46b1-94fa-d0c1f7bc6b32-1234
Invocação de método privado: 6a4f73f6-e580-461e-b741-c6fe40f20c95-888

Dessa maneira, conseguimos ter acessos à métodos e atributos privados de maneira simples. Reforçando novamente, só use quando necessário!

Sobre: Thiago Galbiatti Vespa

Thiago Galbiatti Vespa é mestre em Ciências da Computação e Matemática Computacional pela USP e bacharel em Ciências da Computação pela UNESP. Coordenador de projetos do JavaNoroeste, membro do JCP (Java Community Process), consultor Oracle, arquiteto de software de empresas de médio e grande porte, palestrante de vários eventos e colaborador de projetos open source. Possui as certificações: Oracle Certified Master, Java EE 5 Enterprise Architect – Step 1, 2 and 3; Oracle WebCenter Portal 11g Certified Implementation Specialist; Oracle Service Oriented Architecture Infrastructure Implementation Certified Expert; Oracle Certified Professional, Java EE 5 Web Services Developer; Oracle Certified Expert, NetBeans Integrated Development Environment 6.1 Programmer; Oracle Certified Professional, Java Programmer; Oracle Certified Associate, Java SE 5/SE 6