Serialização em Java – Serialização em Java

A Serialização em Java foi introduzida no JDK 1.1 e é uma das características importantes do Core Java.

A Serialização em Java

permite-nos converter um Objeto em um fluxo que podemos enviar pela rede ou salvá-lo como arquivo ou armazená-lo no banco de dados para uso posterior. Desserialização é o processo de converter o fluxo de Objeto em um Objeto Java real para ser usado em nosso programa. A Serialização em Java parece ser muito fácil de usar no início, mas vem com alguns problemas triviais de segurança e integridade que veremos mais adiante neste artigo. Abordaremos os seguintes tópicos neste tutorial.

  1. Serializable em Java
  2. Refatoração de Classe com Serialização e serialVersionUID
  3. Interface Externalizable Java
  4. Métodos de Serialização Java
  5. Serialização com Herança
  6. Padrão de Proxy de Serialização

Serializable em Java

Se você deseja que um objeto de classe seja serializável, tudo que você precisa fazer é implementar a interface java.io.Serializable. Serializable em Java é uma interface marcadora e não possui campos ou métodos para implementar. É como um processo de aceitação voluntária através do qual tornamos nossas classes serializáveis. A serialização em Java é implementada por meio de ObjectInputStream e ObjectOutputStream, então tudo que precisamos é de um invólucro sobre eles para salvá-lo em um arquivo ou enviá-lo pela rede. Vamos ver um exemplo simples de programa de serialização em Java.

package com.journaldev.serialization;

import java.io.Serializable;

public class Employee implements Serializable {

//	private static final long serialVersionUID = -6470090944414208496L;
	
	private String name;
	private int id;
	transient private int salary;
//	private String password;
	
	@Override
	public String toString(){
		return "Employee{name="+name+",id="+id+",salary="+salary+"}";
	}
	
	// métodos getter e setter
	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public int getSalary() {
		return salary;
	}

	public void setSalary(int salary) {
		this.salary = salary;
	}

//	public String getPassword() {
//		return password;
//	}
//
//	public void setPassword(String password) {
//		this.password = password;
//	}
	
}

Observe que é um simples bean Java com algumas propriedades e métodos getter-setter. Se você quiser que uma propriedade do objeto não seja serializada para o fluxo, você pode usar a palavra-chave transient, como fiz com a variável de salário. Agora, suponha que desejamos escrever nossos objetos em um arquivo e depois desserializá-los do mesmo arquivo. Portanto, precisamos de métodos utilitários que usarão ObjectInputStream e ObjectOutputStream para fins de serialização.

package com.journaldev.serialization;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

/**
 * A simple class with generic serialize and deserialize method implementations
 * 
 * @author pankaj
 * 
 */
public class SerializationUtil {

	// desserializar para Objeto a partir do arquivo fornecido
	public static Object deserialize(String fileName) throws IOException,
			ClassNotFoundException {
		FileInputStream fis = new FileInputStream(fileName);
		ObjectInputStream ois = new ObjectInputStream(fis);
		Object obj = ois.readObject();
		ois.close();
		return obj;
	}

	// serializar o objeto fornecido e salvá-lo no arquivo
	public static void serialize(Object obj, String fileName)
			throws IOException {
		FileOutputStream fos = new FileOutputStream(fileName);
		ObjectOutputStream oos = new ObjectOutputStream(fos);
		oos.writeObject(obj);

		fos.close();
	}

}

Observe que os argumentos do método funcionam com Object, que é a classe base de qualquer objeto Java. Ele é escrito desta forma para ser genérico. Agora, vamos escrever um programa de teste para ver a Serialização em Java em ação.

package com.journaldev.serialization;

import java.io.IOException;

public class SerializationTest {
	
	public static void main(String[] args) {
		String fileName="employee.ser";
		Employee emp = new Employee();
		emp.setId(100);
		emp.setName("Pankaj");
		emp.setSalary(5000);
		
		// serializar para arquivo
		try {
			SerializationUtil.serialize(emp, fileName);
		} catch (IOException e) {
			e.printStackTrace();
			return;
		}
		
		Employee empNew = null;
		try {
			empNew = (Employee) SerializationUtil.deserialize(fileName);
		} catch (ClassNotFoundException | IOException e) {
			e.printStackTrace();
		}
		
		System.out.println("emp Object::"+emp);
		System.out.println("empNew Object::"+empNew);
	}
}

Ao executarmos o programa de teste acima para serialização em Java, obtemos a seguinte saída.

emp Object::Employee{name=Pankaj,id=100,salary=5000}
empNew Object::Employee{name=Pankaj,id=100,salary=0}

Como o salário é uma variável transitória, seu valor não foi salvo no arquivo e, portanto, não foi recuperado no novo objeto. Da mesma forma, os valores de variáveis estáticas também não são serializados, pois pertencem à classe e não ao objeto.

Refatoração de Classe com Serialização e serialVersionUID

A serialização em Java permite algumas alterações na classe Java, desde que possam ser ignoradas. Algumas das alterações na classe que não afetarão o processo de desserialização são:

  • Adição de novas variáveis à classe
  • Alteração das variáveis de transitórias para não transitórias, para a serialização, é como ter um novo campo.
  • Alteração da variável de estática para não estática, para a serialização, é como ter um novo campo.

Mas para que todas essas mudanças funcionem, a classe java deve ter o serialVersionUID definido para a classe. Vamos escrever uma classe de teste apenas para desserializar o arquivo já serializado da classe de teste anterior.

package com.journaldev.serialization;

import java.io.IOException;

public class DeserializationTest {

	public static void main(String[] args) {

		String fileName="employee.ser";
		Employee empNew = null;
		
		try {
			empNew = (Employee) SerializationUtil.deserialize(fileName);
		} catch (ClassNotFoundException | IOException e) {
			e.printStackTrace();
		}
		
		System.out.println("empNew Object::"+empNew);
		
	}
}

Agora descomente a variável “senha” e seus métodos getter-setter da classe Funcionário e execute-o. Você receberá a exceção abaixo;

java.io.InvalidClassException: com.journaldev.serialization.Employee; local class incompatible: stream classdesc serialVersionUID = -6470090944414208496, local class serialVersionUID = -6234198221249432383
	at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:604)
	at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1601)
	at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1514)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1750)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1347)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:369)
	at com.journaldev.serialization.SerializationUtil.deserialize(SerializationUtil.java:22)
	at com.journaldev.serialization.DeserializationTest.main(DeserializationTest.java:13)
empNew Object::null

O motivo é claro: serialVersionUID da classe anterior e da nova classe são diferentes. Na verdade, se a classe não definir serialVersionUID, ele será calculado automaticamente e atribuído à classe. O Java usa variáveis de classe, métodos, nome da classe, pacote, etc., para gerar esse número longo único. Se estiver trabalhando com qualquer IDE, receberá automaticamente um aviso de que “A classe serializável Funcionário não declara um campo serialVersionUID final estático do tipo long”. Podemos usar a utilidade java “serialver” para gerar o serialVersionUID da classe, para a classe Funcionário, podemos executá-lo com o comando abaixo.

SerializationExample/bin$serialver -classpath . com.journaldev.serialization.Employee

Observe que não é necessário que a versão serial seja gerada a partir deste programa em si, podemos atribuir esse valor como quisermos. Apenas precisa estar lá para que o processo de desserialização saiba que a nova classe é a nova versão da mesma classe e deve ser desserializada, se possível. Por exemplo, descomente apenas o campo serialVersionUID da classe Funcionário e execute o programa TesteDeSerialização. Agora descomente o campo de senha da classe Funcionário e execute o programa TesteDeDesserialização e você verá que o fluxo de objetos é desserializado com sucesso porque a mudança na classe Funcionário é compatível com o processo de serialização.

Interface Externalizable Java

Se você observar o processo de serialização em Java, é feito automaticamente. Às vezes, queremos obscurecer os dados do objeto para manter sua integridade. Podemos fazer isso implementando a interface java.io.Externalizable e fornecendo a implementação dos métodos writeExternal() e readExternal() a serem usados no processo de serialização.

package com.journaldev.externalization;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

public class Person implements Externalizable{

	private int id;
	private String name;
	private String gender;
	
	@Override
	public void writeExternal(ObjectOutput out) throws IOException {
		out.writeInt(id);
		out.writeObject(name+"xyz");
		out.writeObject("abc"+gender);
	}

	@Override
	public void readExternal(ObjectInput in) throws IOException,
			ClassNotFoundException {
		id=in.readInt();
		// lido na mesma ordem que foi escrito
		name=(String) in.readObject();
		if(!name.endsWith("xyz")) throw new IOException("corrupted data");
		name=name.substring(0, name.length()-3);
		gender=(String) in.readObject();
		if(!gender.startsWith("abc")) throw new IOException("corrupted data");
		gender=gender.substring(3);
	}

	@Override
	public String toString(){
		return "Person{id="+id+",name="+name+",gender="+gender+"}";
	}
	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getGender() {
		return gender;
	}

	public void setGender(String gender) {
		this.gender = gender;
	}

}

Observe que eu alterei os valores dos campos antes de convertê-los em Stream e, em seguida, ao ler, reverti as alterações. Dessa forma, podemos manter a integridade dos dados de alguma forma. Podemos lançar uma exceção se, após a leitura dos dados da transmissão, as verificações de integridade falharem. Vamos escrever um programa de teste para ver isso em ação.

package com.journaldev.externalization;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class ExternalizationTest {

	public static void main(String[] args) {
		
		String fileName = "person.ser";
		Person person = new Person();
		person.setId(1);
		person.setName("Pankaj");
		person.setGender("Male");
		
		try {
			FileOutputStream fos = new FileOutputStream(fileName);
			ObjectOutputStream oos = new ObjectOutputStream(fos);
		    oos.writeObject(person);
		    oos.close();
		} catch (IOException e) {
			// Bloco catch gerado automaticamente
			e.printStackTrace();
		}
		
		FileInputStream fis;
		try {
			fis = new FileInputStream(fileName);
			ObjectInputStream ois = new ObjectInputStream(fis);
		    Person p = (Person)ois.readObject();
		    ois.close();
		    System.out.println("Person Object Read="+p);
		} catch (IOException | ClassNotFoundException e) {
			e.printStackTrace();
		}
	    
	}
}

Ao executar o programa acima, obtemos a seguinte saída.

Person Object Read=Person{id=1,name=Pankaj,gender=Male}

Então, qual é o melhor a ser usado para serialização em Java? Na verdade, é melhor usar a interface Serializable e, até o final do artigo, você entenderá por quê.

Métodos de Serialização em Java

Vimos que a serialização em java é automática e tudo o que precisamos é implementar a interface Serializable. A implementação está presente nas classes ObjectInputStream e ObjectOutputStream. Mas e se quisermos alterar a maneira como estamos salvando os dados, por exemplo, se tivermos algumas informações sensíveis no objeto e antes de salvar/recuperar quisermos criptografar/descriptografar? É por isso que existem quatro métodos que podemos fornecer na classe para alterar o comportamento de serialização. Se esses métodos estiverem presentes na classe, eles são usados para fins de serialização.

  1. readObject(ObjectInputStream ois): Se este método estiver presente na classe, o método readObject() do ObjectInputStream usará este método para ler o objeto do fluxo.
  2. writeObject(ObjectOutputStream oos): Se este método estiver presente na classe, o método writeObject() do ObjectOutputStream usará este método para escrever o objeto no fluxo. Um dos usos comuns é obscurecer as variáveis do objeto para manter a integridade dos dados.
  3. Object writeReplace(): Se este método estiver presente, então após o processo de serialização este método é chamado e o objeto retornado é serializado para o fluxo.
  4. Object readResolve(): Se este método estiver presente, então após o processo de desserialização, este método é chamado para retornar o objeto final para o programa chamador. Um dos usos deste método é implementar o padrão Singleton com classes Serializadas. Leia mais em Serialização e Singleton.

Normalmente, ao implementar os métodos acima, eles são mantidos como privados para que as subclasses não possam substituí-los. Eles são destinados apenas para fins de serialização, e mantê-los privados evita qualquer problema de segurança.

Serialização com Herança

Às vezes, precisamos estender uma classe que não implementa a interface Serializable. Se dependermos do comportamento automático de serialização e a superclasse tiver algum estado, então eles não serão convertidos para fluxo e, portanto, não serão recuperados posteriormente. Este é um caso em que os métodos readObject() e writeObject() realmente ajudam. Ao fornecer a implementação deles, podemos salvar o estado da superclasse no fluxo e depois recuperá-lo posteriormente. Vamos ver isso em ação.

package com.journaldev.serialization.inheritance;

public class SuperClass {

	private int id;
	private String value;
	
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	public String getValue() {
		return value;
	}
	public void setValue(String value) {
		this.value = value;
	}	
}

SuperClasse é um simples java bean, mas não está implementando a interface Serializable.

package com.journaldev.serialization.inheritance;

import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectInputValidation;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class SubClass extends SuperClass implements Serializable, ObjectInputValidation{

	private static final long serialVersionUID = -1322322139926390329L;

	private String name;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
	
	@Override
	public String toString(){
		return "SubClass{id="+getId()+",value="+getValue()+",name="+getName()+"}";
	}
	
	//adicionando método auxiliar para serialização para salvar/inicializar o estado da superclasse
	private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException{
		ois.defaultReadObject();
		
		//observe que a ordem de leitura e escrita deve ser a mesma
		setId(ois.readInt());
		setValue((String) ois.readObject());	
	}
	
	private void writeObject(ObjectOutputStream oos) throws IOException{
		oos.defaultWriteObject();
		
		oos.writeInt(getId());
		oos.writeObject(getValue());
	}

	@Override
	public void validateObject() throws InvalidObjectException {
		//validar o objeto aqui
		if(name == null || "".equals(name)) throw new InvalidObjectException("name can't be null or empty");
		if(getId() <=0) throw new InvalidObjectException("ID can't be negative or zero");
	}	
}

Observe que a ordem de escrita e leitura dos dados extras no fluxo deve ser a mesma. Podemos inserir alguma lógica na leitura e escrita de dados para torná-la segura. Além disso, observe que a classe está implementando a interface ObjectInputValidation. Ao implementar o método validateObject(), podemos inserir algumas validações de negócios para garantir que a integridade dos dados não seja prejudicada. Vamos escrever uma classe de teste e ver se podemos recuperar o estado da superclasse a partir dos dados serializados ou não.

package com.journaldev.serialization.inheritance;

import java.io.IOException;

import com.journaldev.serialization.SerializationUtil;

public class InheritanceSerializationTest {

	public static void main(String[] args) {
		String fileName = "subclass.ser";
		
		SubClass subClass = new SubClass();
		subClass.setId(10);
		subClass.setValue("Data");
		subClass.setName("Pankaj");
		
		try {
			SerializationUtil.serialize(subClass, fileName);
		} catch (IOException e) {
			e.printStackTrace();
			return;
		}
		
		try {
			SubClass subNew = (SubClass) SerializationUtil.deserialize(fileName);
			System.out.println("SubClass read = "+subNew);
		} catch (ClassNotFoundException | IOException e) {
			e.printStackTrace();
		}
	}
}

Quando executamos a classe acima, obtemos a seguinte saída.

SubClass read = SubClass{id=10,value=Data,name=Pankaj}

Dessa forma, podemos serializar o estado da superclasse mesmo que ela não esteja implementando a interface Serializable. Essa estratégia é útil quando a superclasse é uma classe de terceiros que não podemos alterar.

Padrão de Proxy de Serialização

A serialização em Java vem com algumas sérias armadilhas, tais como;

  • A estrutura da classe não pode ser alterada muito sem quebrar o processo de serialização do Java. Portanto, mesmo que não precisemos de algumas variáveis mais tarde, precisamos mantê-las apenas por compatibilidade com versões anteriores.
  • A serialização causa enormes riscos de segurança, um atacante pode alterar a sequência do fluxo e causar danos ao sistema. Por exemplo, a função do usuário é serializada e um atacante altera o valor do fluxo para torná-lo administrador e executar código malicioso.

O padrão de Proxy de Serialização Java é uma maneira de alcançar uma maior segurança com a Serialização. Neste padrão, uma classe estática privada interna é usada como uma classe de proxy para fins de serialização. Esta classe é projetada de forma a manter o estado da classe principal. Este padrão é implementado através da implementação adequada dos métodos readResolve() e writeReplace(). Vamos primeiro escrever uma classe que implementa o padrão de proxy de serialização e então a analisaremos para melhor entendimento.

package com.journaldev.serialization.proxy;

import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class Data implements Serializable{

	private static final long serialVersionUID = 2087368867376448459L;

	private String data;
	
	public Data(String d){
		this.data=d;
	}

	public String getData() {
		return data;
	}

	public void setData(String data) {
		this.data = data;
	}
	
	@Override
	public String toString(){
		return "Data{data="+data+"}";
	}
	
	 // classe de proxy de serialização 
	private static class DataProxy implements Serializable{
	
		private static final long serialVersionUID = 8333905273185436744L;
		
		private String dataProxy;
		private static final String PREFIX = "ABC";
		private static final String SUFFIX = "DEFG";
		
		public DataProxy(Data d){
			 // obscurecendo dados por segurança 
			this.dataProxy = PREFIX + d.data + SUFFIX;
		}
		
		private Object readResolve() throws InvalidObjectException {
			if(dataProxy.startsWith(PREFIX) && dataProxy.endsWith(SUFFIX)){
			return new Data(dataProxy.substring(3, dataProxy.length() -4));
			}else throw new InvalidObjectException("data corrupted");
		}
		
	}
	
	 // substituindo objeto serializado por objeto DataProxy 
	private Object writeReplace(){
		return new DataProxy(this);
	}
	
	private void readObject(ObjectInputStream ois) throws InvalidObjectException{
		throw new InvalidObjectException("Proxy is not used, something fishy");
	}
}
  • Ambas as classes Data e DataProxy devem implementar a interface Serializable.
  • DataProxy deve ser capaz de manter o estado do objeto Data.
  • DataProxy é uma classe interna privada estática, para que outras classes não possam acessá-la.
  • DataProxy deve ter um único construtor que recebe Data como argumento.
  • A classe Data deve fornecer o método writeReplace() retornando uma instância de DataProxy. Portanto, quando o objeto Data é serializado, o fluxo retornado é da classe DataProxy. No entanto, a classe DataProxy não é visível externamente, portanto não pode ser usada diretamente.
  • A classe DataProxy deve implementar o método readResolve() retornando o objeto Data. Portanto, quando a classe Data é desserializada, internamente DataProxy é desserializada e quando seu método readResolve() é chamado, obtemos o objeto Data.
  • Por fim, implemente o método readObject() na classe Data e lance InvalidObjectException para evitar ataques de hackers tentando fabricar um fluxo de objetos Data e analisá-lo.

Vamos escrever um pequeno teste para verificar se a implementação funciona ou não.

package com.journaldev.serialization.proxy;

import java.io.IOException;

import com.journaldev.serialization.SerializationUtil;

public class SerializationProxyTest {

	public static void main(String[] args) {
		String fileName = "data.ser";
		
		Data data = new Data("Pankaj");
		
		try {
			SerializationUtil.serialize(data, fileName);
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		try {
			Data newData = (Data) SerializationUtil.deserialize(fileName);
			System.out.println(newData);
		} catch (ClassNotFoundException | IOException e) {
			e.printStackTrace();
		}
	}

}

Ao executarmos a classe acima, obtemos a seguinte saída no console.

Data{data=Pankaj}

Se você abrir o arquivo data.ser, poderá ver que o objeto DataProxy está salvo como fluxo no arquivo.

Baixe o Projeto de Serialização em Java

Isso é tudo para a Serialização em Java, parece simples, mas devemos usá-la com sabedoria e é sempre melhor não depender da implementação padrão. Baixe o projeto a partir do link acima e experimente para aprender mais.

Source:
https://www.digitalocean.com/community/tutorials/serialization-in-java