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 salvar como arquivo ou armazenar no banco de dados para uso posterior. A 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 muito fácil de usar a princípio, mas vem com alguns problemas triviais de segurança e integridade que veremos mais adiante neste artigo. Vamos abordar os seguintes tópicos neste tutorial.
- Serializable em Java
- Refatoração de Classe com Serialização e serialVersionUID
- Interface Externalizable Java
- Métodos de Serialização Java
- Serialização com Herança
- Padrão de Proxy de Serialização
Serializable em Java
Se você deseja que um objeto de classe seja serializável, tudo o que 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 “Opt-In” através do qual tornamos nossas classes serializáveis. A serialização em java é implementada por meio de ObjectInputStream
e ObjectOutputStream
, então tudo o 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 java bean com algumas propriedades e métodos getter-setter. Se você deseja que uma propriedade do objeto não seja serializada para o fluxo, 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 trabalham com Object, que é a classe base de qualquer objeto Java. Foi escrito dessa forma para ser genérico por natureza. Agora, vamos escrever um programa de teste para ver a Serialização em ação em Java.
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 das 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 se puderem 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ória para não transitória, 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 alterações 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 “password” e seus métodos getter-setter da classe Employee e execute. Você receberá a seguinte exceção;
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
A razão é clara de que 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 utiliza variáveis de classe, métodos, nome da classe, pacote, etc. para gerar este número longo único. Se estiver trabalhando com algum IDE, você automaticamente receberá um aviso de que “A classe serializável Employee não declara um campo serialVersionUID estático final do tipo long”. Podemos usar a utilidade java “serialver” para gerar o serialVersionUID da classe, para a classe Employee 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 desejarmos. 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 Employee
e execute o programa SerializationTest
. Agora descomente o campo password da classe Employee e execute o programa DeserializationTest
e você verá que o fluxo de objeto é desserializado com sucesso porque a alteração na classe Employee é compatível com o processo de serialização.
Interface Externalizable do Java
Se você observar o processo de serialização em Java, ele é feito automaticamente. Às vezes, queremos obscurecer os dados do objeto para manter sua integridade. Podemos fazer isso implementando a interface java.io.Externalizable
e fornecer a implementação dos métodos writeExternal() e readExternal() para 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();
// ler na mesma ordem que foi escrita
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 do fluxo, 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) {
// TODO Bloco de captura automática
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 executarmos 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á o porquê.
Métodos de Serialização em Java
Nós 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 forma como estamos salvando dados, por exemplo, se tivermos algumas informações sensíveis no objeto e antes de salvar/recuperar quisermos criptografar/descriptografar isso. É 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.
- readObject(ObjectInputStream ois): Se este método estiver presente na classe, o método ObjectInputStream readObject() usará este método para ler o objeto do fluxo.
- writeObject(ObjectOutputStream oos): Se este método estiver presente na classe, o método ObjectOutputStream writeObject() 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.
- 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.
- 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 sobrescrevê-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 confiarmos no comportamento automático de serialização e a superclasse tiver algum estado, eles não serão convertidos para stream e, portanto, não serão recuperados posteriormente. Este é um caso em que os métodos readObject() e writeObject() realmente ajudam. Ao fornecer sua implementação, podemos salvar o estado da superclasse na stream e depois recuperá-lo. 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 para o fluxo deve ser a mesma. Podemos adicionar alguma lógica na leitura e escrita de dados para torná-la segura. Também observe que a classe está implementando a interface ObjectInputValidation
. Ao implementar o método validateObject(), podemos adicionar algumas validações de negócios para garantir que a integridade dos dados não seja comprometida. 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();
}
}
}
Ao executarmos a classe acima, obtemos a seguinte saída.
SubClass read = SubClass{id=10,value=Data,name=Pankaj}
Assim, dessa forma, podemos serializar o estado da superclasse mesmo que ela não esteja implementando a interface Serializable. Esta 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 alguns sérios problemas, como;
- A estrutura da classe não pode ser alterada muito sem quebrar o processo de serialização 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 segurança maior com a Serialização. Neste padrão, uma classe interna privada e estática é 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 ao implementar adequadamente os métodos readResolve() e writeReplace(). Vamos primeiro escrever uma classe que implementa o padrão de proxy de serialização e depois a analisaremos para um 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){
//ocultando dados por motivos de 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
eDataProxy
devem implementar a interface Serializable. - A classe
DataProxy
deve ser capaz de manter o estado do objeto Data. - A classe
DataProxy
é uma classe interna privada e estática, de modo que outras classes não podem acessá-la. - A classe
DataProxy
deve ter um único construtor que recebe Data como argumento. - A classe
Data
deve fornecer o método writeReplace() que retorna uma instância deDataProxy
. Assim, 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() que retorna o objetoData
. Assim, quando a classe Data é desserializada, internamente o DataProxy é desserializado e quando seu método readResolve() é chamado, obtemos o objeto Data. - Finalmente implemente o método readObject() na classe Data e lance
InvalidObjectException
para evitar ataques de hackers tentando fabricar um fluxo de objeto 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, verá que o objeto DataProxy está salvo como fluxo no arquivo.
Baixe o Projeto de Serialização Java
Isso é tudo para a Serialização em Java, parece simples, mas devemos usá-la com cuidado e é sempre melhor não depender da implementação padrão. Baixe o projeto no link acima e explore-o para aprender mais.
Source:
https://www.digitalocean.com/community/tutorials/serialization-in-java