Object serialization
Introduction
At a certain point during the development of a program of mine I
decided it would be nice to save certain aspects of the program state
on disk to be able to recall them the next time the program is
started. For simplicity I will explore a simplified version of the
stuff I serialized in real life.
Suppose we want to remember the following things about our program
the moment the program is ended so that we are able to recall them
later:
Location of the main window of the program
Size of the main window of the program
State of the main window of the program (e.g. maximized, minimized and
normal state)
On this page I want to stay focussed on the topic of object
serialization, so I will not go into the techniques needed to keep
track of the properties mentioned above.
First I want to introduce the Options class to you. This is the
class I use to hold the program state which I want to save. The Java
code of this class is displayed below:
Class: Options.java
001 /*
002 * Options.java
003 *
004 * Created on 24 juli 2005, 22:10
005 */
006 package main;
007
008 /**
009 * The <code>Options</code> class holds all program options which need to be
010 * saved when the user exits the program.
011 *
012 * @author Patrick Holthuizen
013 */
014 public class Options {
015 // X-position of the main window
016 private Integer windowX = null;
017 // Y-position of the main window
018 private Integer windowY = null;
019 // Width of the main window
020 private Integer windowWidth = null;
021 // Height of the main window
022 private Integer windowHeight = null;
023 // Extended window state of the main window
024 private Integer windowState = null;
025
026 /**
027 * Creates a new instance of <code>Options</code>.
028 */
029 public Options() {
030 }
031
032 /**
033 * Gets the X-location of the main window.
034 *
035 * @return the X-location of the main window
036 */
037 public Integer getWindowX() {
038 return windowX;
039 }
040
041 /**
042 * Sets the X-location of the main window.
043 *
044 * @param windowX the X-location of the main window
045 */
046 public void setWindowX(Integer windowX) {
047 this.windowX = windowX;
048 }
049
050 /**
051 * Gets the Y-location of the main window.
052 *
053 * @return the Y-location of the main window
054 */
055 public Integer getWindowY() {
056 return windowY;
057 }
058
059 /**
060 * Sets the Y-location of the main window.
061 *
062 * @param windowY the Y-location of the main window
063 */
064 public void setWindowY(Integer windowY) {
065 this.windowY = windowY;
066 }
067
068 /**
069 * Gets the width of the main window.
070 *
071 * @return the width of the main window
072 */
073 public Integer getWindowWidth() {
074 return windowWidth;
075 }
076
077 /**
078 * Sets the width of the main window.
079 *
080 * @param windowWidth the width of the main window
081 */
082 public void setWindowWidth(Integer windowWidth) {
083 this.windowWidth = windowWidth;
084 }
085
086 /**
087 * Gets the height of the main window.
088 *
089 * @return the height of the main window
090 */
091 public Integer getWindowHeight() {
092 return windowHeight;
093 }
094
095 /**
096 * Sets the height of the main window.
097 *
098 * @param windowHeight the height of the main window
099 */
100 public void setWindowHeight(Integer windowHeight) {
101 this.windowHeight = windowHeight;
102 }
103
104 /**
105 * Gets the extended window state of the main window.
106 *
107 * @return the extended window state of the main window
108 */
109 public Integer getWindowState() {
110 return windowState;
111 }
112
113 /**
114 * Sets the extended window state of the main window.
115 *
116 * @param windowState the extended window state of the main window
117 */
118 public void setWindowState(Integer windowState) {
119 this.windowState = windowState;
120 }
121 }
|
The simple but not so flexible implementation
So you see a lot of not so important stuff, it's just a place
holder for the program state with appropriate getters and setters.
Now it's time to save this data to disk. Suppose we are on the verge
to exit our program, for example through the window closing event.
That would be a nice place to save an object of the Options
class.
To save (i.e. serialize) the object we need to do at least one
thing and that is implement the interface Serializable.
The interface is in the package java.io
so I also inlude the statement
in the class. So now the
class looks like this:
01 /*
02 * Options.java
03 *
04 * Created on 24 juli 2005, 22:10
05 */
06 package main;
07
08 import java.io.*;
09
10 /**
11 * The <code>Options</code> class holds all program options which need to be
12 * saved when the user exits the program.
13 *
14 * @author Patrick Holthuizen
15 */
16 public class Options implements Serializable {
17 // X-position of the main window
18 private Integer windowX = null;
19 // Y-position of the main window
20 private Integer windowY = null;
21
22 /*** the rest of the class ***/
|
Note that the interface Serializable is
a special interface as we don't need to implement any methods for it.
The class Options as it is now may be
serialized through the default Java serialization mechanism, for
example like this:
Options options = new Options();
try {
FileOutputStream os = new FileOutputStream("options.ext");
ObjectOutputStream oos = new ObjectOutputStream(os);
oos.writeObject(options);
oos.close();
os.close();
} catch(Exception e) {
e.printStackTrace();
}
|
Keep your class compatible with future versions
The example in the previous section works pretty cool. Just add a
few keywords and the objects of your class can be persisted. The
method used in the previous section has one huge drawback:
serialized classes are not compatible when anything in the
source of the class Options changes. An
important advice: do not stop when you have implemented the
example above, just keep on reading and you will be rewarded with a
full backward compatible serialization mechanism!
The class variable: serialVersionUID
By implementing the interface Serializable you may implement a
special class variable which is called serialVersionUID.
This special variable may best be declared like this:
private static final long serialVersionUID = 1L;
|
The value of the variable (1L) may be
chosen randomly, it just denotes the version of the current class
structure. The Java serialization mechanism uses this variable to
determine the version of the class that is or will be serialized.
If you do not define the serialVersionUID
Java will calculate one for itself based the definition of the class
variables, methods and Java version you are running. This method is
very fragile and produces different versions on a lot of occasions
you do not want the version to change. In fact you actually never
want the version to change because it is impossible to deserialize an
object with a different version as the one you are using. To control
the version of the class you use you must implement the
serialVersionUID.
Sometimes, in examples, you see complicated serialVersionUID
definitions like:
private static final long serialVersionUID = -1814239825517340645L;
|
The use of the long numbers is not necessary. In such examples it is
most likely that an object was serialized with the simple, not
backward compatible method and the author realized that he needed
backward compatibility after all. You may still deserialize the
object if the serialVersionUID of the
new class is the same as the serialVersionUID
of the old class. In this case the new class is defined with that
serialVersionUID. If you are smart, and
you are, you do not have to use this method. In case you have to use
it you may use the program serialver to
determine the serialVersionUID of an
existing class file.
Just implementing the serialVersionUID
gives you a lot of backward compatibility already, because the
default serialization mechanism does not protest as it does not
encounter different serialVersionUID's
as an added bonus the default serialization mechanism deserializes
objects by mapping the field names of the serialized object to the
field names of the runtime version.
Defining the methods readObject and writeObject
After implementing the serialVersionUID
you get the field mapping for free across class versions, but this is
often not good enough for providing real backwards compatibility.
There are two special methods you may implement to further perfect your class
serialization: readObject and writeObject.
If you implement any of these methods you must define them with the following
signature:
private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException
|
private void writeObject(ObjectOutputStream out) throws IOException
|
Attention
You will notice that the special methods readObject and
writeObject do not override anything. This is normal behavior, the
serialization mechanism uses the Java reflection mechanism to determine whether
any of these methods exist and execute them if they do. |
The first statements in both methods are also fixed. The readObject
method should always start with the statement:
The writeObject method should always start with the statement:
out.defaultWriteObject();
|
To explain the use of these calls it is easier to start with the call of
defaultWriteObject(). The method defaultWriteObject()
performs the default serialization mechanism by writing an object descriptor
followed by the serialization of all fields of an object. We are not really
interested in the field serialization because we are going to write these
ourselves, but we definitely are interested in the object descriptor. The
use of in.defaultReadObject() is there for the same reasons.
So, with the following implementations of the methods we have exactly the same
functionality as the default serialization mechanism:
1 private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException {
2 in.defaultReadObject();
3
4 // Enter you user code here
5 }
|
1 private void writeObject(ObjectOutputStream out) throws IOException {
2 out.defaultWriteObject();
3
4 // Enter you user code here
5 }
|
Really implementing the methods readObject and writeObject
Now that we have the definitions of readObject and
writeObject in place we may start to think about implementing them
with our own user code. First we'll have to identify the class variables we want
to serialize, in our case we want to serialize them all. The easiest way is to
serialize them in the default way, by using the readObject and
writeObject methods of each stream. This will give us the following
implementations of our methods:
01 private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException {
02 in.defaultReadObject();
03
04 // Enter you user code here
05 setWindowX((Integer)in.readObject());
06 setWindowY((Integer)in.readObject());
07 setWindowWidth((Integer)in.readObject());
08 setWindowHeight((Integer)in.readObject());
09 setWindowState((Integer)in.readObject());
10 }
|
01 private void writeObject(ObjectOutputStream out) throws IOException {
02 out.defaultWriteObject();
03
04 // Enter your user code here
05 out.writeObject(getWindowX());
06 out.writeObject(getWindowY());
07 out.writeObject(getWindowWidth());
08 out.writeObject(getWindowHeight());
09 out.writeObject(getWindowState());
10 }
|
You might already guessed it, but this implementation of the object serialization
does exactly the same thing as the default serialization mechanism. So we
still have gained nothing and have done a lot of work. But the pay-off comes in
the next section.
Before we move on to the next section the class will be enriched with a
sub-version number of the class. It's use is not explained here but will become
clear in the next section too.
The sub-version will be implemented by a method. The serialization methods
will use this sub-version number to determine the control flow withing each method.
Here you will find the Options class completely again with all
the functionality in it. The last thing we do before moving on to the next section
is to imagine that this version of the class is the one which is distributed with
the first version of our application.
001 /*
002 * Options.java
003 *
004 * Created on 24 juli 2005, 22:10
005 */
006 package main;
007
008 import java.io.*;
009
010 /**
011 * The <code>Options</code> class holds all program options which need to be
012 * saved when the user exits the program.
013 *
014 * @author Patrick Holthuizen
015 */
016 public class Options implements Serializable {
017 // Serial version UID
018 private static final long serialVersionUID = 1L;
019 // X-position of the main window
020 private Integer windowX = null;
021 // Y-position of the main window
022 private Integer windowY = null;
023 // Width of the main window
024 private Integer windowWidth = null;
025 // Height of the main window
026 private Integer windowHeight = null;
027 // Extended window state of the main window
028 private Integer windowState = null;
029
030 /**
031 * Creates a new instance of <code>Options</code>.
032 */
033 public Options() {
034 }
035
036 /**
037 * Gets the serial sub-version UID.
038 *
039 * @return the serial sub-version UID
040 */
041 private Long getSerialSubVersionUID() {
042 return 1L;
043 }
044
045 /**
046 * Gets the X-location of the main window.
047 *
048 * @return the X-location of the main window
049 */
050 public Integer getWindowX() {
051 return windowX;
052 }
053
054 /**
055 * Sets the X-location of the main window.
056 *
057 * @param windowX the X-location of the main window
058 */
059 public void setWindowX(Integer windowX) {
060 this.windowX = windowX;
061 }
062
063 /**
064 * Gets the Y-location of the main window.
065 *
066 * @return the Y-location of the main window
067 */
068 public Integer getWindowY() {
069 return windowY;
070 }
071
072 /**
073 * Sets the Y-location of the main window.
074 *
075 * @param windowY the Y-location of the main window
076 */
077 public void setWindowY(Integer windowY) {
078 this.windowY = windowY;
079 }
080
081 /**
082 * Gets the width of the main window.
083 *
084 * @return the width of the main window
085 */
086 public Integer getWindowWidth() {
087 return windowWidth;
088 }
089
090 /**
091 * Sets the width of the main window.
092 *
093 * @param windowWidth the width of the main window
094 */
095 public void setWindowWidth(Integer windowWidth) {
096 this.windowWidth = windowWidth;
097 }
098
099 /**
100 * Gets the height of the main window.
101 *
102 * @return the height of the main window
103 */
104 public Integer getWindowHeight() {
105 return windowHeight;
106 }
107
108 /**
109 * Sets the height of the main window.
110 *
111 * @param windowHeight the height of the main window
112 */
113 public void setWindowHeight(Integer windowHeight) {
114 this.windowHeight = windowHeight;
115 }
116
117 /**
118 * Gets the extended window state of the main window.
119 *
120 * @return the extended window state of the main window
121 */
122 public Integer getWindowState() {
123 return windowState;
124 }
125
126 /**
127 * Sets the extended window state of the main window.
128 *
129 * @param windowState the extended window state of the main window
130 */
131 public void setWindowState(Integer windowState) {
132 this.windowState = windowState;
133 }
134
135 /**
136 * Reads an instance of <code>Options</code> from the specified input stream.
137 *
138 * @param in the input stream to read the object from
139 * @throws java.lang.ClassNotFoundException
140 * @throws java.io.IOException
141 */
142 private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException {
143 in.defaultReadObject();
144
145 // Enter you user code here
146 Long serialSubVersionUID = (Long)in.readObject();
147 if(serialSubVersionUID == 1L) {
148 // Do not set the serialSubVersionUID
149 setWindowX((Integer)in.readObject());
150 setWindowY((Integer)in.readObject());
151 setWindowWidth((Integer)in.readObject());
152 setWindowHeight((Integer)in.readObject());
153 setWindowState((Integer)in.readObject());
154 } else {
155 throw new ClassNotFoundException("Unsupported object version");
156 }
157 }
158
159 /**
160 * Writes an instance of <code>Options</code> to the specified output stream.
161 *
162 * @param out the output stream to write the object to
163 * @throws java.io.IOException
164 */
165 private void writeObject(ObjectOutputStream out) throws IOException {
166 out.defaultWriteObject();
167
168 // Enter your user code here
169 out.writeObject(getSerialSubVersionUID());
170 out.writeObject(getWindowX());
171 out.writeObject(getWindowY());
172 out.writeObject(getWindowWidth());
173 out.writeObject(getWindowHeight());
174 out.writeObject(getWindowState());
175 }
176 }
|
Code comments (you may skip these if you want)
- line 036 - 043: gets the current sub-version number
- line 146 - 147: the
readObject method extended with the use
of the serial sub-version UID.
- line 148: we explicitly do not set the serial sub-version field because we
always try to upgrade an object to the latest class version during the
deserialization process.
Creating a new version of the Options class
In this section we will see the benefits of all our hard work fall into place.
So our current class is in the field and as time flies by we decided to create a
new version of our Options class. We decided to change the definition
of the class by extending it with an additional property and to change the
definition of the window location and window size. The latter two are going to
be implemented by the Dimension class.
Here comes the complete listing of the Options class. I'll be
discussing the changes later.
001 /*
002 * Options.java
003 *
004 * Created on 24 juli 2005, 22:10
005 */
006 package main;
007
008 import java.awt.*;
009 import java.io.*;
010
011 /**
012 * The <code>Options</code> class holds all program options which need to be
013 * saved when the user exits the program.
014 *
015 * @author Patrick Holthuizen
016 */
017 public class Options implements Serializable {
018 // Serial version UID
019 private static final long serialVersionUID = 1L;
020 // Location of the main window
021 private Dimension windowLocation = null;
022 // Size of the main window
023 private Dimension windowSize = null;
024 // Extended window state of the main window
025 private Integer windowState = null;
026 // The title of the main window
027 private String windowTitle = null;
028
029 /**
030 * Creates a new instance of <code>Options</code>.
031 */
032 public Options() {
033 }
034
035 /**
036 * Gets the serial sub-version UID.
037 *
038 * @return the serial sub-version UID
039 */
040 private Long getSerialSubVersionUID() {
041 return 2L;
042 }
043
044 /**
045 * Gets the location of the main window.
046 *
047 * @return the location of the main window
048 */
049 public Dimension getWindowLocation() {
050 return windowLocation;
051 }
052
053 /**
054 * Sets the location of the main window.
055 *
056 * @param windowLocation the location of the main window
057 */
058 public void setWindowLocation(Dimension windowLocation) {
059 this.windowLocation = windowLocation;
060 }
061
062 /**
063 * Gets the size of the main window.
064 *
065 * @return the size of the main window
066 */
067 public Dimension getWindowSize() {
068 return windowSize;
069 }
070
071 /**
072 * Sets the size of the main window.
073 *
074 * @param windowSize the size of the main window
075 */
076 public void setWindowSize(Dimension windowSize) {
077 this.windowSize = windowSize;
078 }
079
080 /**
081 * Gets the extended window state of the main window.
082 *
083 * @return the extended window state of the main window
084 */
085 public Integer getWindowState() {
086 return windowState;
087 }
088
089 /**
090 * Sets the extended window state of the main window.
091 *
092 * @param windowState the extended window state of the main window
093 */
094 public void setWindowState(Integer windowState) {
095 this.windowState = windowState;
096 }
097
098 /**
099 * Gets the window title of the main window.
100 *
101 * @return the window title of the main window
102 */
103 public String getWindowTitle() {
104 return windowTitle;
105 }
106
107 /**
108 * Sets the window title of the main window.
109 *
110 * @param windowTitle the window title of the main window
111 */
112 public void setWindowTitle(String windowTitle) {
113 this.windowTitle = windowTitle;
114 }
115
116 /**
117 * Reads an instance of <code>Options</code> from the specified input stream.
118 *
119 * @param in the input stream to read the object from
120 * @throws java.lang.ClassNotFoundException if the class has an unrecognizable
121 * format
122 * @throws java.io.IOException if some I/O exception occurs
123 */
124 private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException {
125 in.defaultReadObject();
126
127 // Enter you user code here
128 Long serialSubVersionUID = (Long)in.readObject();
129 if(serialSubVersionUID == 1L) {
130 Integer windowX = (Integer)in.readObject();
131 Integer windowY = (Integer)in.readObject();
132 setWindowLocation(new Dimension(windowX, windowY));
133 Integer windowWidth = (Integer)in.readObject();
134 Integer windowHeight = (Integer)in.readObject();
135 setWindowSize(new Dimension(windowWidth, windowHeight));
136 setWindowState((Integer)in.readObject());
137 setWindowTitle(null);
138 } else if(serialSubVersionUID == 2L) {
139 setWindowLocation((Dimension)in.readObject());
140 setWindowSize((Dimension)in.readObject());
141 setWindowState((Integer)in.readObject());
142 setWindowTitle((String)in.readObject());
143 } else {
144 throw new ClassNotFoundException("Unsupported object version");
145 }
146 }
147
148 /**
149 * Writes an instance of <code>Options</code> to the specified output stream.
150 *
151 * @param out the output stream to write the object to
152 * @throws java.io.IOException if some I/O exception occurs
153 */
154 private void writeObject(ObjectOutputStream out) throws IOException {
155 out.defaultWriteObject();
156
157 // Enter your user code here
158 out.writeObject(getSerialSubVersionUID());
159 out.writeObject(getWindowLocation());
160 out.writeObject(getWindowSize());
161 out.writeObject(getWindowState());
162 out.writeObject(getWindowTitle());
163 }
164 }
|
Code comments
- line 053 - 042: gets the current sub-version number (the current sub-version
is 2)
- line 129 - 137: if a serialized object of sub-version 1 is encountered then
all fields are read in the old format and converted to the new format
- line 138 - 142: a very straightforward method of deserializing an version 2
object
- line 164: we explicitly do not set the serial sub-version field because we
always try to upgrade an object to the latest class version during the
deserialization process.
The sub-version number
As you can see a method has been introduced to get a sub-version from an
object instance. In the first version of our class this method always returned
the number 1 and in the second version of our class this method returns 2.
This method is used in the writeObject method to serialize this
number with the other data of an object. The sub-version number is serialized
first so we can retrieve it first in the readObject method to
determine the control flow.
Remark:
Notice that the special variable serialVersionUID is never touched
and that we use an alternative way to handle class versions. We can not use the
serialVersionUID because if it changes Java refuses to read the
class with a different version number and we are not able to execute our
readObject method successfully.
The method writeObject
Look at the differences between the writeObject method of the
two versions of the classes.
Version 1 of the class writes the following objects
to the stream: Long followed by five Integers.
Version 2 writes: Long, Dimension,
Dimension, Integer and String.
It's also worth noticing that the writeObject method always writes
the object in the latest version format to a stream, all version differences are
handled by the readObject method. Using this technique our class is
always able to read object of older versions and upgrades them automatically to
the latest version when rewriting them.
The method readObject
In the readObject method all version handling is done, which
means that the method readObject of the latest version of the
Options class is able to read every version of a serialized
Options class and upgrade it to the latest version.
If you take a look at the code you will notice a some tests on the
serialSubVersionUID of the class. For each
serialSubVersionUID you want to support (most preferably all) you
must implement code to deserialize the object of that specific version and
upgrade it to the latest version of the class.
serialSubVersionUID is equal to 1
All fields are being read in the version 1 format of the Options
class. The fields windowX and windowY are
converted to a Dimension. The same is done for the fields
windowWidth and windowHeight.
The window title is not available in the first version of our class. Therefore
the window title is initialized to null.
serialSubVersionUID is equal to 2
Because this is the current version of the class no conversion needs to be
done and the code is very straightforward.
Handling null values
Until now everything works properly unless one of the objects is
null. If one of the objects to serialize is null a
NullPointerException is thrown.
Sometimes you want to serialize an object which might be null
and serialize and deserialize it that way. Suppose in the example above that the
window title might be null when an object of the
Options class is serialized.
The most elegant way to handle null values is to put all fields
which may contain null values in a Map which can be
serialized as a whole. Of course you may put fields which are not
null in a Map object.
In our case we are going to use the map implementation HashMap
and we will use it serialize all fields with it (both null
capable or not).
001 /*
002 * Options.java
003 *
004 * Created on 24 juli 2005, 22:10
005 */
006 package main;
007
008 import java.awt.*;
009 import java.io.*;
010 import java.util.*;
011
012 /**
013 * The <code>Options</code> class holds all program options which need to be
014 * saved when the user exits the program.
015 *
016 * @author Patrick Holthuizen
017 */
018 public class Options implements Serializable {
019 // Serial version UID
020 private static final long serialVersionUID = 1L;
021 // Location of the main window
022 private Dimension windowLocation = null;
023 // Size of the main window
024 private Dimension windowSize = null;
025 // Extended window state of the main window
026 private Integer windowState = null;
027 // The title of the main window
028 private String windowTitle = null;
029
030 /**
031 * Creates a new instance of <code>Options</code>.
032 */
033 public Options() {
034 }
035
036 /**
037 * Gets the serial sub-version UID.
038 *
039 * @return the serial sub-version UID
040 */
041 private Long getSerialSubVersionUID() {
042 return 2L;
043 }
044
045 /**
046 * Gets the location of the main window.
047 *
048 * @return the location of the main window
049 */
050 public Dimension getWindowLocation() {
051 return windowLocation;
052 }
053
054 /**
055 * Sets the location of the main window.
056 *
057 * @param windowLocation the location of the main window
058 */
059 public void setWindowLocation(Dimension windowLocation) {
060 this.windowLocation = windowLocation;
061 }
062
063 /**
064 * Gets the size of the main window.
065 *
066 * @return the size of the main window
067 */
068 public Dimension getWindowSize() {
069 return windowSize;
070 }
071
072 /**
073 * Sets the size of the main window.
074 *
075 * @param windowSize the size of the main window
076 */
077 public void setWindowSize(Dimension windowSize) {
078 this.windowSize = windowSize;
079 }
080
081 /**
082 * Gets the extended window state of the main window.
083 *
084 * @return the extended window state of the main window
085 */
086 public Integer getWindowState() {
087 return windowState;
088 }
089
090 /**
091 * Sets the extended window state of the main window.
092 *
093 * @param windowState the extended window state of the main window
094 */
095 public void setWindowState(Integer windowState) {
096 this.windowState = windowState;
097 }
098
099 /**
100 * Gets the window title of the main window.
101 *
102 * @return the window title of the main window
103 */
104 public String getWindowTitle() {
105 return windowTitle;
106 }
107
108 /**
109 * Sets the window title of the main window.
110 *
111 * @param windowTitle the window title of the main window
112 */
113 public void setWindowTitle(String windowTitle) {
114 this.windowTitle = windowTitle;
115 }
116
117 /**
118 * Reads an instance of <code>Options</code> from the specified input stream.
119 *
120 * @param in the input stream to read the object from
121 * @throws java.lang.ClassNotFoundException if the class has an unrecognizable
122 * format
123 * @throws java.io.IOException if some I/O exception occurs
124 */
125 private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException {
126 in.defaultReadObject();
127
128 // Enter you user code here
129 Long serialSubVersionUID = (Long)in.readObject();
130 if(serialSubVersionUID == 1L) {
131 Integer windowX = (Integer)in.readObject();
132 Integer windowY = (Integer)in.readObject();
133 setWindowLocation(new Dimension(windowX, windowY));
134 Integer windowWidth = (Integer)in.readObject();
135 Integer windowHeight = (Integer)in.readObject();
136 setWindowSize(new Dimension(windowWidth, windowHeight));
137 setWindowState((Integer)in.readObject());
138 setWindowTitle(null);
139 } else if(serialSubVersionUID == 2L) {
140 Map<String, Object> map = (Map<String, Object>)in.readObject();
141 setWindowLocation((Dimension)map.get("windowLocation"));
142 setWindowSize((Dimension)map.get("windowSize"));
143 setWindowState((Integer)map.get("windowSize"));
144 setWindowTitle((String)map.get("windowTitle"));
145 } else {
146 throw new ClassNotFoundException("Unsupported object version");
147 }
148 }
149
150 /**
151 * Writes an instance of <code>Options</code> to the specified output stream.
152 *
153 * @param out the output stream to write the object to
154 * @throws java.io.IOException if some I/O exception occurs
155 */
156 private void writeObject(ObjectOutputStream out) throws IOException {
157 out.defaultWriteObject();
158
159 // Enter your user code here
160 out.writeObject(getSerialSubVersionUID());
161 Map<String, Object> map = new HashMap<String, Object>();
162 map.put("windowLocation", getWindowLocation());
163 map.put("windowSize", getWindowSize());
164 map.put("windowState", getWindowState());
165 map.put("windowTitle", getWindowTitle());
166 out.writeObject(map);
167 }
168 }
|
Note: I didn't know which map class was best to use, finally wanted to choose
between the EnumMap and the HashMap. Although the
EnumMap provides more type safety it introduces a lot of
compatibility problems opposed to the HashMap.
Serializing in a secure way
Suppose version three of the Options class contains a password
field we wish to serialize. First we expand the Options class
with the field and extend the methods readObject and
writeObject. The result is shown below:
001 /*
002 * Options.java
003 *
004 * Created on 24 juli 2005, 22:10
005 */
006 package main;
007
008 import java.awt.*;
009 import java.io.*;
010 import java.util.*;
011
012 /**
013 * The <code>Options</code> class holds all program options which need to be
014 * saved when the user exits the program.
015 *
016 * @author Patrick Holthuizen
017 */
018 public class Options implements Serializable {
019 // Serial version UID
020 private static final long serialVersionUID = 1L;
021 // Location of the main window
022 private Dimension windowLocation = null;
023 // Size of the main window
024 private Dimension windowSize = null;
025 // Extended window state of the main window
026 private Integer windowState = null;
027 // The title of the main window
028 private String windowTitle = null;
029 // The user-id to be used to connect to the database
030 private String userId = null;
031 // The password related to the user-id
032 private String password = null;
033
034 /**
035 * Creates a new instance of <code>Options</code>.
036 */
037 public Options() {
038 }
039
040 /**
041 * Gets the serial sub-version UID.
042 *
043 * @return the serial sub-version UID
044 */
045 private Long getSerialSubVersionUID() {
046 return 3L;
047 }
048
049 /**
050 * Gets the location of the main window.
051 *
052 * @return the location of the main window
053 */
054 public Dimension getWindowLocation() {
055 return windowLocation;
056 }
057
058 /**
059 * Sets the location of the main window.
060 *
061 * @param windowLocation the location of the main window
062 */
063 public void setWindowLocation(Dimension windowLocation) {
064 this.windowLocation = windowLocation;
065 }
066
067 /**
068 * Gets the size of the main window.
069 *
070 * @return the size of the main window
071 */
072 public Dimension getWindowSize() {
073 return windowSize;
074 }
075
076 /**
077 * Sets the size of the main window.
078 *
079 * @param windowSize the size of the main window
080 */
081 public void setWindowSize(Dimension windowSize) {
082 this.windowSize = windowSize;
083 }
084
085 /**
086 * Gets the extended window state of the main window.
087 *
088 * @return the extended window state of the main window
089 */
090 public Integer getWindowState() {
091 return windowState;
092 }
093
094 /**
095 * Sets the extended window state of the main window.
096 *
097 * @param windowState the extended window state of the main window
098 */
099 public void setWindowState(Integer windowState) {
100 this.windowState = windowState;
101 }
102
103 /**
104 * Gets the window title of the main window.
105 *
106 * @return the window title of the main window
107 */
108 public String getWindowTitle() {
109 return windowTitle;
110 }
111
112 /**
113 * Sets the window title of the main window.
114 *
115 * @param windowTitle the window title of the main window
116 */
117 public void setWindowTitle(String windowTitle) {
118 this.windowTitle = windowTitle;
119 }
120
121 /**
122 * Gets the user-id which can be used to connect to the dabase.
123 *
124 * @return the user-id
125 */
126 public String getUserId() {
127 return userId;
128 }
129
130 /**
131 * Sets the user-id which can be used to connect to the dabase.
132 *
133 * @param userId the user-id
134 */
135 public void setUserId(String userId) {
136 this.userId = userId;
137 }
138
139 /**
140 * Gets the password related to the user-id.
141 *
142 * @return the password related to the user-id
143 */
144 public String getPassword() {
145 return password;
146 }
147
148 /**
149 * Gets the password related to the user-id.
150 *
151 * @param password the password related to the user-id
152 */
153 public void setPassword(String password) {
154 this.password = password;
155 }
156
157 /**
158 * Reads an instance of <code>Options</code> from the specified input stream.
159 *
160 * @param in the input stream to read the object from
161 * @throws java.lang.ClassNotFoundException if the class has an unrecognizable
162 * format
163 * @throws java.io.IOException if some I/O exception occurs
164 */
165 private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException {
166 in.defaultReadObject();
167
168 // Enter you user code here
169 Long serialSubVersionUID = (Long)in.readObject();
170 if(serialSubVersionUID == 1L) {
171 Integer windowX = (Integer)in.readObject();
172 Integer windowY = (Integer)in.readObject();
173 setWindowLocation(new Dimension(windowX, windowY));
174 Integer windowWidth = (Integer)in.readObject();
175 Integer windowHeight = (Integer)in.readObject();
176 setWindowSize(new Dimension(windowWidth, windowHeight));
177 setWindowState((Integer)in.readObject());
178 setWindowTitle(null);
179 setUserId(null);
180 setPassword(null);
181 } else if(serialSubVersionUID == 2L) {
182 Map<String, Object> map = (Map<String, Object>)in.readObject();
183 setWindowLocation((Dimension)map.get("windowLocation"));
184 setWindowSize((Dimension)map.get("windowSize"));
185 setWindowState((Integer)map.get("windowSize"));
186 setWindowTitle((String)map.get("windowTitle"));
187 setUserId(null);
188 setPassword(null);
189 } else if(serialSubVersionUID == 3L) {
190 Map<String, Object> map = (Map<String, Object>)in.readObject();
191 setWindowLocation((Dimension)map.get("windowLocation"));
192 setWindowSize((Dimension)map.get("windowSize"));
193 setWindowState((Integer)map.get("windowSize"));
194 setWindowTitle((String)map.get("windowTitle"));
195 setUserId((String)map.get("userId"));
196 setPassword((String)map.get("password"));
197 } else {
198 throw new ClassNotFoundException("Unsupported object version");
199 }
200 }
201
202 /**
203 * Writes an instance of <code>Options</code> to the specified output stream.
204 *
205 * @param out the output stream to write the object to
206 * @throws java.io.IOException if some I/O exception occurs
207 */
208 private void writeObject(ObjectOutputStream out) throws IOException {
209 out.defaultWriteObject();
210
211 // Enter your user code here
212 out.writeObject(getSerialSubVersionUID());
213 Map<String, Object> map = new HashMap<String, Object>();
214 map.put("windowLocation", getWindowLocation());
215 map.put("windowSize", getWindowSize());
216 map.put("windowState", getWindowState());
217 map.put("windowTitle", getWindowTitle());
218 map.put("userId", getUserId());
219 map.put("password", getPassword());
220 out.writeObject(map);
221 }
222 }
|
I think the extension is pretty straightforward and doesn't need additional
explanation. Technically the code above works properly the only problem we want
to address is the storage of the new fields (user-id and password). These fields
are written to the output stream in plain text format and therefore readable.
For example the following code:
Options options = new Options();
options.setUserId("patrick");
options.setPassword("verysecretpassword");
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(".options"));
oos.writeObject(options);
oos.close();
} catch(Exception e) {
e.printStackTrace();
}
|
After executing the code above a file with the name .options is
created and written on the file system. If you take a look at the contents of
the file it isn't really hard to locate the strings "patrick" and
"verysecretpassword" in it. This is really a security threat which we going to
solve now. Although the method we use is not really waterproof it is much more
secure in a way that it will be much harder for regular end users to obtain
account sensitive information.
Instead of having the readable password in the serialized file we are going
to move the sensitive information to our program code. As said before this will
not be much safer when you need security against hackers, but it will probably
be good enough to hold of regular end users from obtaining the information.
Providing security we are going to serialize the Options class
in an encrypted way. To encrypt it we are going to use the the
SealedObject class to encrypt the class and write that class to the
object output stream. The last version of the will be extended by the
encryption handling and the code is shown below:
001 /*
002 * Options.java
003 *
004 * Created on 24 juli 2005, 22:10
005 */
006 package main;
007
008 import java.awt.*;
009 import java.io.*;
010 import java.util.*;
011 import javax.crypto.*;
012 import javax.crypto.spec.*;
013
014 /**
015 * The <code>Options</code> class holds all program options which need to be
016 * saved when the user exits the program.
017 *
018 * @author Patrick Holthuizen
019 */
020 public class Options implements Serializable {
021 // Serial version UID
022 private static final long serialVersionUID = 1L;
023 // Location of the main window
024 private transient Dimension windowLocation = null;
025 // Size of the main window
026 private transient Dimension windowSize = null;
027 // Extended window state of the main window
028 private transient Integer windowState = null;
029 // The title of the main window
030 private transient String windowTitle = null;
031 // The user-id to be used to connect to the database
032 private transient String userId = null;
033 // The password related to the user-id
034 private transient String password = null;
035
036 /**
037 * Creates a new instance of <code>Options</code>.
038 */
039 public Options() {
040 }
041
042 /**
043 * Gets the serial sub-version UID.
044 *
045 * @return the serial sub-version UID
046 */
047 private Long getSerialSubVersionUID() {
048 return 3L;
049 }
050
051 /**
052 * Gets the location of the main window.
053 *
054 * @return the location of the main window
055 */
056 public Dimension getWindowLocation() {
057 return windowLocation;
058 }
059
060 /**
061 * Sets the location of the main window.
062 *
063 * @param windowLocation the location of the main window
064 */
065 public void setWindowLocation(Dimension windowLocation) {
066 this.windowLocation = windowLocation;
067 }
068
069 /**
070 * Gets the size of the main window.
071 *
072 * @return the size of the main window
073 */
074 public Dimension getWindowSize() {
075 return windowSize;
076 }
077
078 /**
079 * Sets the size of the main window.
080 *
081 * @param windowSize the size of the main window
082 */
083 public void setWindowSize(Dimension windowSize) {
084 this.windowSize = windowSize;
085 }
086
087 /**
088 * Gets the extended window state of the main window.
089 *
090 * @return the extended window state of the main window
091 */
092 public Integer getWindowState() {
093 return windowState;
094 }
095
096 /**
097 * Sets the extended window state of the main window.
098 *
099 * @param windowState the extended window state of the main window
100 */
101 public void setWindowState(Integer windowState) {
102 this.windowState = windowState;
103 }
104
105 /**
106 * Gets the window title of the main window.
107 *
108 * @return the window title of the main window
109 */
110 public String getWindowTitle() {
111 return windowTitle;
112 }
113
114 /**
115 * Sets the window title of the main window.
116 *
117 * @param windowTitle the window title of the main window
118 */
119 public void setWindowTitle(String windowTitle) {
120 this.windowTitle = windowTitle;
121 }
122
123 /**
124 * Gets the user-id which can be used to connect to the dabase.
125 *
126 * @return the user-id
127 */
128 public String getUserId() {
129 return userId;
130 }
131
132 /**
133 * Sets the user-id which can be used to connect to the dabase.
134 *
135 * @param userId the user-id
136 */
137 public void setUserId(String userId) {
138 this.userId = userId;
139 }
140
141 /**
142 * Gets the password related to the user-id.
143 *
144 * @return the password related to the user-id
145 */
146 public String getPassword() {
147 return password;
148 }
149
150 /**
151 * Gets the password related to the user-id.
152 *
153 * @param password the password related to the user-id
154 */
155 public void setPassword(String password) {
156 this.password = password;
157 }
158
159 /**
160 * Gets a secret DES key used to encrypt the object contents.
161 *
162 * @return the secret DES key
163 */
164 private SecretKey getSecretKey() {
165 SecretKey result = null;
166
167 // Now our security exposure is "reduced" to this method.
168 String keyString = "A4SFR55DSH3J4H3JJJ3H34FGFG34";
169 byte key[] = keyString.getBytes();
170 try {
171 DESKeySpec desKeySpec = new DESKeySpec(key);
172 SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
173 result = keyFactory.generateSecret(desKeySpec);
174 } catch(Exception e) {
175 e.printStackTrace();
176 }
177
178 return result;
179 }
180
181 /**
182 * Reads an instance of <code>Options</code> from the specified input stream.
183 *
184 * @param in the input stream to read the object from
185 * @throws java.lang.ClassNotFoundException if the class has an unrecognizable
186 * format
187 * @throws java.io.IOException if some I/O exception occurs
188 */
189 private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException {
190 in.defaultReadObject();
191
192 // Enter you user code here
193 Long serialSubVersionUID = (Long)in.readObject();
194 if(serialSubVersionUID == 1L) {
195 Integer windowX = (Integer)in.readObject();
196 Integer windowY = (Integer)in.readObject();
197 setWindowLocation(new Dimension(windowX, windowY));
198 Integer windowWidth = (Integer)in.readObject();
199 Integer windowHeight = (Integer)in.readObject();
200 setWindowSize(new Dimension(windowWidth, windowHeight));
201 setWindowState((Integer)in.readObject());
202 setWindowTitle(null);
203 setUserId(null);
204 setPassword(null);
205 } else if(serialSubVersionUID == 2L) {
206 Map<String, Object> map = (Map<String, Object>)in.readObject();
207 setWindowLocation((Dimension)map.get("windowLocation"));
208 setWindowSize((Dimension)map.get("windowSize"));
209 setWindowState((Integer)map.get("windowSize"));
210 setWindowTitle((String)map.get("windowTitle"));
211 setUserId(null);
212 setPassword(null);
213 } else if(serialSubVersionUID == 3L) {
214 SealedObject sealedObject = (SealedObject)in.readObject();
215 try {
216 Map<String, Object> map = (Map<String, Object>)sealedObject.getObject(getSecretKey());
217 setWindowLocation((Dimension)map.get("windowLocation"));
218 setWindowSize((Dimension)map.get("windowSize"));
219 setWindowState((Integer)map.get("windowSize"));
220 setWindowTitle((String)map.get("windowTitle"));
221 setUserId((String)map.get("userId"));
222 setPassword((String)map.get("password"));
223 } catch(Exception e) {
224 throw new IOException("Unable to decrypt the object: " + e.getMessage());
225 }
226 } else {
227 throw new ClassNotFoundException("Unsupported object version");
228 }
229 }
230
231 /**
232 * Writes an instance of <code>Options</code> to the specified output stream.
233 *
234 * @param out the output stream to write the object to
235 * @throws java.io.IOException if some I/O exception occurs
236 */
237 private void writeObject(ObjectOutputStream out) throws IOException {
238 HashMap<String, Object> map = new HashMap<String, Object>();
239 map.put("windowLocation", getWindowLocation());
240 map.put("windowSize", getWindowSize());
241 map.put("windowState", getWindowState());
242 map.put("windowTitle", getWindowTitle());
243 map.put("userId", getUserId());
244 map.put("password", getPassword());
245 // Seal object
246 SealedObject sealedObject = null;
247 try {
248 Cipher desCipher = Cipher.getInstance("DES/ECB/PKCS5Padding");
249 desCipher.init(Cipher.ENCRYPT_MODE, getSecretKey());
250 sealedObject = new SealedObject(map, desCipher);
251 } catch(Exception e) {
252 throw new IOException("Unable to encrypt the object: " + e.getMessage());
253 }
254
255 out.defaultWriteObject();
256 out.writeObject(getSerialSubVersionUID());
257 out.writeObject(sealedObject);
258 }
259 }
|
Code comments
- line 024 - 034: all fields are being marked
transient which
prevents the fields to be serialized by calling the method
defaultWriteObject()
- line 159 - 179: the method
getSecretKey() returns a so called
DES-key which is used to encrypt en decrypt the object
- line 168: the string "
A4SFR55DSH3J4H3JJJ3H34FGFG34" is chosen
randomly, pick whatever you like
- line 214 - 225: the
readObject method now expects a
SealedObject from the input stream which is decrypted by our
secret key
- line 245 - 253: the
HashMap object is encrypted by the DES
algorithm and put into a SealedObject
- line 255 - 257: all I/O stuff is moved to the back to make sure encryption
errors do not interfere with our output activities
Remarks
The solution provided on this page is a lightweight solution. This solution
will probably be insufficient for secure and large scale environments. Consider
using a more centralized mechanism for these situations.
The sources on this page are all implemented using JDK 6.0.
Comments
If you have any comments please send an e-mail to
Patrick Holthuizen.
