pabhb
pabhb

Reputation: 49

Spring AOP - Database Auditing

I'm using Spring AOP trying to define a good approach to have all my tables audited with no much hassle. Example of scenario: I have a table named Person and its respective table PersonLog, which will store the Person values in addition to the user who modified, when and the type of the event, for each update.

Simply put, my question is:

I'm trying to come up with a way that my advice class would be smart enough to handle any new table being audited without any needed modification to it... let's say that I created the table Car and its CarLog table, if I could avoid the need to change anything in my advice implementation (it would automatically identify Car as being audited and would be able to persist a CarLog entity) ---> I can identify table Car as being audited pretty easily (by annotation), but I'm struggling to find a way to create and persist a CarLog instance dynamically.

Can anyone think of a way to accomplish that? Thanks.

Upvotes: 0

Views: 4070

Answers (3)

Jigar Parekh
Jigar Parekh

Reputation: 6273

i had similiar requirement in project where i am suppose to take snapshot of complex object graph before saving.

solution i have applied is 1) developed custom annotation @Archivable with certain attribute like nullify,ignore, orignal, setArchiveFlag

2) written hiberante deep cloner utility which create replica of object and insert into same table. deep cloner works on simple trick searlize and then desearlize object this will create new instances and then set id and version to null.

3) used cloner utility in entity interceptor to take decision weather to archive or not.

below is some of that code.

@Retention(RetentionPolicy.RUNTIME)
@Target( { ElementType.TYPE })
public @interface Archivable {

/** This will mark property as null in clone */
public String[] nullify() default {};

/**
 * If property is archivable but not from enclosing entity then specify as
 * ignore.
 */
public String[] ignore() default {};

/**
 * sets original reference to clone for back refer data. This annotation is
 * applicable to only root entity from where archiving started.
 * 
 * @return
 */
public String original() default "";

/**
 * if marks cloned entity to archived, assumes flag to be "isArchived". 
 * @return
 */
public boolean setArchiveFlag() default false;
}


@Component
public class ClonerUtils {

private static final String IS_ARCHIVED = "isArchived";
@Autowired
private SessionFactory sessionFactory;

public Object copyAndSave(Serializable obj) throws Exception {
    List<BaseEntity> entities = new ArrayList<BaseEntity>();
    Object clone=this.copy(obj,entities);
    this.save(clone, entities);
    return clone;
}

public Object copy(Serializable obj,List<BaseEntity> entities) throws Exception{
    recursiveInitliaze(obj);
    Object clone = SerializationHelper.clone(obj);
    prepareHibernateObject(clone, entities);
    if(!getOriginal(obj).equals("")){
        PropertyUtils.setSimpleProperty(clone, getOriginal(obj), obj);
    }
    return clone;
}


private void save(Object obj,List<BaseEntity> entities){
    for (BaseEntity baseEntity : entities) {
        sessionFactory.getCurrentSession().save(baseEntity);
    }
}

@SuppressWarnings("unchecked")
public void recursiveInitliaze(Object obj) throws Exception {
    if (!isArchivable(obj)) {
        return;
    }
    if(!Hibernate.isInitialized(obj))
        Hibernate.initialize(obj);
    PropertyDescriptor[] properties = PropertyUtils.getPropertyDescriptors(obj);
    for (PropertyDescriptor propertyDescriptor : properties) {
        Object origProp = PropertyUtils.getProperty(obj, propertyDescriptor.getName());
        if (origProp != null && isArchivable(origProp) && !isIgnore(propertyDescriptor, obj)) {
            this.recursiveInitliaze(origProp);
        }
        if (origProp instanceof Collection && origProp != null) {               
            for (Object item : (Collection) origProp) {
                this.recursiveInitliaze(item);
            }
        }
    }
}


@SuppressWarnings("unchecked")
private void prepareHibernateObject(Object obj, List entities) throws Exception {
    if (!isArchivable(obj)) {
        return;
    }
    if (obj instanceof BaseEntity) {
        ((BaseEntity) obj).setId(null);
        ((BaseEntity) obj).setVersion(null);
        if(hasArchiveFlag(obj)){
            PropertyUtils.setSimpleProperty(obj, IS_ARCHIVED, true);
        }
        entities.add(obj);
    }
    String[] nullifyList = getNullifyList(obj);
    for (String prop : nullifyList) {
        PropertyUtils.setProperty(obj, prop, null);
    }
    PropertyDescriptor[] properties = PropertyUtils.getPropertyDescriptors(obj);
    for (PropertyDescriptor propertyDescriptor : properties) {
        if (isIgnore(propertyDescriptor, obj)) {
            continue;
        }
        Object origProp = PropertyUtils.getProperty(obj, propertyDescriptor.getName());         
        if (origProp != null && isArchivable(origProp)) {
            this.prepareHibernateObject(origProp, entities);
        }   
        /**  This code is for element collection */
        if(origProp instanceof PersistentBag){
            Collection elemColl=createNewCollection(origProp);
            PersistentBag pColl=(PersistentBag) origProp;
            elemColl.addAll(pColl.subList(0, pColl.size()));
            PropertyUtils.setSimpleProperty(obj, propertyDescriptor.getName(), elemColl);
            continue;
        }
        if (origProp instanceof Collection && origProp != null) {
            Collection newCollection  = createNewCollection(origProp);
            PropertyUtils.setSimpleProperty(obj, propertyDescriptor.getName(),    newCollection);
            for (Object item : (Collection) origProp) {
                this.prepareHibernateObject(item, entities);
            }
        }
    }
}



@SuppressWarnings("unchecked")
private Collection createNewCollection(Object origProp) {
    try {
        if(List.class.isAssignableFrom(origProp.getClass()))
            return new ArrayList((Collection)origProp);
        else if(Set.class.isAssignableFrom(origProp.getClass()))
                return new HashSet((Collection)origProp);
        else{
            Collection tempColl=(Collection) BeanUtils.cloneBean(origProp);
            tempColl.clear();
            return tempColl;
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return new ArrayList();     
}

private boolean isIgnore(PropertyDescriptor propertyDescriptor,Object obj){
    String propertyName=propertyDescriptor.getName();
    String[] ignores=getIgnoreValue(obj);
    return ArrayUtils.contains(ignores, propertyName);
}

private String[] getIgnoreValue(Object obj) {
    String[] ignore=obj.getClass().getAnnotation(Archivable.class).ignore();
    return ignore==null?new String[]{}:ignore;
}

private String[] getNullifyList(Object obj) {
    String[] nullify=obj.getClass().getAnnotation(Archivable.class).nullify();
    return nullify==null?new String[]{}:nullify;
}

public boolean isArchivable(Object obj) {
    return obj.getClass().isAnnotationPresent(Archivable.class);
}


private String getOriginal(Object obj) {
    String original=obj.getClass().getAnnotation(Archivable.class).original();
    return original==null?"":original;
}

private boolean hasArchiveFlag(Object obj) {        
    return obj.getClass().getAnnotation(Archivable.class).setArchiveFlag();
}

@SuppressWarnings({ "unchecked", "unused" })
private Collection getElemColl(Object obj, Object origProp) {
    Collection elemColl=createNewCollection(origProp);
    for (Object object : (Collection)origProp) {
        elemColl.add(object);
    }
    return elemColl;
}

@SuppressWarnings("unused")
private boolean isElementCollection(Object obj, String name) {
    try {
        Annotation[] annotations=obj.getClass().getDeclaredField(name).getAnnotations();
        for (Annotation annotation : annotations) {
            if(annotation.annotationType() == ElementCollection.class)
                return true;
        }
    } catch (Exception e) {
        e.printStackTrace();            
    }
    return false;
}
}

Upvotes: 1

Aravind Yarram
Aravind Yarram

Reputation: 80166

Envers is what you require for auditing purposes

Upvotes: 0

duffymo
duffymo

Reputation: 308743

This is called "change data capture" or CDC.

Personally, I don't think this is a good use for Spring or AOP. I think it would be better done in the database itself, especially if the database is shared/modified by more than one application.

You don't say which database you're using, but I'd recommend digging into your vendor's docs to find out what they have out of the box to support CDC.

Upvotes: 2

Related Questions