Since this is a common problem, and there are a lot of half-bred solutions reachable by searching, let me present what I settled on:
- define two trivial field annotations,
@CreatedDate
and @ModifiedDate
;
- use them to annotate the corresponding fields on your entity;
- register the entity class with the
Traceability
listener, presented below.
This is how your entity class would look:
@EntityListeners(Traceability.class)
public class MyEntity {
@CreatedDate @Temporal(TIMESTAMP) public Date created;
@ModifiedDate @Temporal(TIMESTAMP public Date modified;
....
}
These one-liners define the annotations:
@Retention(RUNTIME) @Target(FIELD) public @interface CreatedDate {}
@Retention(RUNTIME) @Target(FIELD) public @interface ModifiedDate {}
You can put them in their own files, or you can bunch them up inside some existing class. I prefer the former so I get a cleaner fully-qualified name for them.
Here's the Entity listener class:
public class Traceability
{
private final ConcurrentMap<Class<?>, TracedFields> fieldCache = new ConcurrentHashMap<>();
@PrePersist
public void prePersist(Object o) { touchFields(o, true); }
@PreUpdate
public void preUpdate(Object o) { touchFields(o, false); }
private void touchFields(Object o, boolean creation) {
final Date now = new Date();
final Consumer<? super Field> touch = f -> uncheckRun(() -> f.set(o, now));
final TracedFields tf = resolveFields(o);
if (creation) tf.created.ifPresent(touch);
tf.modified.ifPresent(touch);
}
private TracedFields resolveFields(Object o) {
return fieldCache.computeIfAbsent(o.getClass(), c -> {
Field created = null, modified = null;
for (Field f : c.getFields()) {
if (f.isAnnotationPresent(CreatedDate.class)) created = f;
else if (f.isAnnotationPresent(ModifiedDate.class)) modified = f;
if (created != null && modified != null) break;
}
return new TracedFields(created, modified);
});
}
private static class TracedFields {
public final Optional<Field> created, modified;
public TracedFields(Field created, Field modified) {
this.created = Optional.ofNullable(created);
this.modified = Optional.ofNullable(modified);
}
}
// Java's ill-conceived checked exceptions are even worse when combined with
// lambdas. Below is what we need to achieve "exception transparency" (ability
// to let checked exceptions escape the lambda function). This disables
// compiler's checking of exceptions thrown from the lambda, so it should be
// handled with utmost care.
public static void uncheckRun(RunnableExc r) {
try { r.run(); }
catch (Exception e) { sneakyThrow(e); }
}
public interface RunnableExc { void run() throws Exception; }
public static <T> T sneakyThrow(Throwable e) {
return Traceability.<RuntimeException, T> sneakyThrow0(e);
}
@SuppressWarnings("unchecked")
private static <E extends Throwable, T> T sneakyThrow0(Throwable t) throws E {
throw (E) t;
}
}
Finally, if you aren't working with JPA but with classic Hibernate, you need to activate its JPA event model integration. This is very simple, just make sure that the classpath contains the file META-INF/services/org.hibernate.integrator.spi.Integrator
, with the following single line in its contents:
org.hibernate.jpa.event.spi.JpaIntegrator
Typically for a Maven project, you just need to put this under your src/main/resources
directory.