This document describes the core functionality of Spif in detail. Eeven more details are found in the Javadoc.
The core of Spif is a simple API for accessing objects called
StoreChain:
public class StoreChain {
public Object get(Id id,boolean writable);
public Id put(Id id,Object object);
public boolean remove(Id id);
}
The Id class is also a part of Spif. You will probably want to subclass it to make id classes for your business objects.
By employing a general and carefully thought out interface for the
most common task of every distributed oo application, a lot of code is
saved since there is no need to make separate mechanisms for different
object types. But more importantly, it provides great opportunity for
changing the infrastructure carrying out the access requests without
changing the business code making the requests. This is done by
supporting chain of resposibilities in the API:
public class StoreChain {
public Object get(Id id,boolea writable);
public Id put(Id id,Object object);
public boolean remove(Id id);
public StoreChain setChained(StoreChain chain);
}
So, the business code makes calls to a single, well known StoreChain implementation, without having to worry about which store chain is actually carrying out the requests. The well known implementation is called Store and contains static versions of the interface methods for convenience. It contains no behaviour apart from forwarding to whatever is chained to it.
So to use Spif, you write business code which uses Store:
And you set up a store chain on startup which carries out the desired
operations as described in the following.
import net.sf.spif.Store;
....
public void increaseSalary(EmployeeId id) {
boolean writable=true;
Employee employee=(Employee)Store.get(id,writable);
employee.setSalary(employee.getSalary()*1.5);
Store.put(id,employee);
}
The functionality carrying out get, put and remove requests may be a simple hashtable or a distributed, persistent, multiuser infrastructure with lots of application-specific behaviour for different objects. Complicated functionality is achieved by combining simpler store chain classes into longer chains. This makes it possible to change the behaviour of an entire application just by changing the store chain. It also makes it possible to obtain complicated customized behaviour just by combining standard store chains classes. Some such commonly needed classes are included in the Spif distribution.
Here are some example chains using classes included in the Spif distribution:
The simplest possible chain:
Store.setChained(new HashStore());
A chain for a client vm which accumulates changes up to a commit
point, caches the last 1000 objects for read requests and
forwards other requests to a session bean at a server:
Note that setChained returns the input store chain to make chaining
convenient.
Store.setChained(new StoreAccumulator()).
setChained(new StoreCache(1000,false)).
setChained(new StoreClient());
A server chain caching read and write requests, using
pessimistic locking on reads and forwarding changes and non-cached
reads to a database:
Store.setChained(new StoreLocker()).
setChained(new StoreCache(1000,true)).
setChained(new StorePersister());
This uses the relational persistence store chain class supplied with Spif. Some prefer to write chain classes adapting to other persistence layers, e.g. entity beans or Hibernate.
In addition to the supplied store chains, all projects write their own. In fact you'll find that you can add lots of functionality in no time by writing store chains. Typical tasks which has successfully been handles by Spif chains are:
You can probably think of many other uses. Being able to intercept any operational object access by writing a few lines of code is a very powerful thing. If you write some reusable chain class, please send it to me, or ask to be granted Spif cvs checkin rights.
The following subclasses of StoreChain are included. You do not need to use any of them to use Spif, you just need to be model centric, have Id's for you main classes and access them through Store. But skim through the list before you consider writing your own ones.
HashStore | Keeps an unlimited amount of objects. Backed by a regular HashMap. |
StoreCache | Keeps a limited number of objects for get requests. Put and remove passes through (but affects the cache as expected). Configured to only return from the cache on read gets or on both read and write gets. The latter is only suitable if no objects may change without passing through the cache, that is a server which own it's data storage. This is backed by a LinkedHashMap, so it requires Java 1.4. |
StoreLocker | Applies pessimistic locking on objects. Objects accessed for writing are locked until they are written back by the same user. Other users attempting to change the objects or access them for writing will receive an exception. The locks has a configurable time-out. |
StoreProfiler | Collects and/or prints information on the time spent reading and writing objects. |
StoreLogger | Simply prints a java.util.logging info message for each request passing through. |
StoreDispatcher | Routes store requests to different store chains based on the object type of the request. |
IndexedFileStore | Stores objects on a hashed file. Backed by Jisp.jar |
StorePersister |
Stores objects to a relational database. This is the worlds least developer work demanding database layer. Instead of making the developer supply a boring database mapping, it uses reflection and sql metadata to automaticaly make a mapping between objects and db tables. The first time some class is encountered by the persister it makes a mapping and outputs a report on what it found to match. Subsequent accesses to objects of the same class use the same mapping. The Store Persister matches class names to table names and field names to column names. It supports collections and is able to match fields in objects reachable from the main objects to columns in the table of the main object. This works faster than you might think. Reflection times are negible compared to db accesses, at least on Java 1.4, and the db accesses are reduced by batching. Of course it requires a good match between the objects and relations, which probably means that it is not usable if the relations allready exists (unless you write views, but that's just another way of doing the manual mapping we try to avoid). If you control your db tables you should check it out. You'll know if it works for you or not in a few minutes. |
Thats all! Spif isn't just the simples infrastructure framework around, it is also the smallest. Its most effective feature is not the supplied chain implementations but that it helps organizing a system in a beneficial way, not by force, but by being the simplest way to get things done.
For completeness, I include an example model object, as it would typically be implemented when used with Spif. The thing of importance here is that the id of the model object extends net.sf.spif.Id and is named >Modelclassname<Id. This fact makes it possible for methods of Id to infer the model class type from the id, which is often useful for Spif chain classes.
The full source of the example classes is included in the source of the Spif distribution.
package net.sf.spif.example;
import java.util.List;
import java.util.Iterator;
import java.util.Collections;
public class Person {
private PersonId id=null;
private String name;
private Address address;
private List children=new java.util.ArrayList();
/**
* Create a person and assign a unique id in some way I am
* not going into here. You may also have some store chain create
* the id on put: PersonId newId=Store.put(null,newPerson);
*/
public Person(String name) {
if (name==null)
throw new NullPointerException("A persons name can not be null");
this.name=name;
setId(new PersonId(name)); // Not suited for production
}
/** Public for state restoration only, i.e. persistence */
public void setId(PersonId id) {
if (this.id!=null)
throw new IllegalStateException("Refusing to change id of " + this);
this.id=id;
}
public PersonId getId() {
return id;
}
public void setName(String name) {
this.name=name;
}
public String getName() {
return name;
}
/**
* (The address is a dependent object -
* part of the same edit bubble)
*/
public void setAddress(Address address) {
this.address=address;
}
public Address getAddress() {
return address;
}
/**
* Returns an iterator of the ids of the children of this class.
* (So this is not part of the sane edit bubble, although
* collections may very well be)
*
* @return a read-only iterator of children personId's,
* or an empty iterator if there is no children
*/
public Iterator getChildren() {
return Collections.unmodifiableList(children).iterator();
}
public void addChild(PersonId childId) {
if (childId.equals(id))
throw new IllegalArgumentException("Come on! Not even " + this +
" is it's own child");
children.add(childId);
}
/** @return true if the child was removed, false if it wasn't present */
public boolean removeChild(PersonId childId) {
return children.remove(childId);
}
// If the class is used by rich clients you'll often need positional
// methods for collections, at least getChildCount()
// getChild(int position):PersonId and indexOf(PersonId):int
// and some order determinism (sorting)
public String toString() {
return name + " ( " + id + ")";
}
}
package net.sf.spif.example;
public class PersonId extends net.sf.spif.Id {
public PersonId(String idString) {
super(idString);
}
}
package net.sf.spif.example;
/**
* Notice that dependent objects does not need an id.
* Apart from that, I know the simplicity here will offend anyone
* who has worked with customer addresses in real systems.
*/
public class Address {
/** The street, including street number */
private String street;
private int zip;
private String city;
public Address(String street,String city,int zip) {
// TODO: Validate
this.street=street;
this.city=city;
this.zip=zip;
}
// TODO: Simple getters and setters
}