Mixing declarative and programmatic transactions with Spring and JPA listeners
I'm using a JPA EntityListener to do some additional audit work and am injecting a Spring-managed AuditService into my AuditEntryListener using @Configurable. The AuditService generates a collection of AuditEntry objects. The AuditService is itself a Singleton scoped bean, and I'd like to gather all the AuditEntry objects under a common key that can then be accessed by the outermost service layer (the one that invoked the persist call which in turn triggered the EntityListener).
I'm looking at using Spring's TransactionSynchronizationManager to set a specific transaction name (using UID() or some other unique strategy) at the beginning of the transaction, and then using that name as a key within the AuditService that will allow me to group all AuditEntry objects created within that transaction.
Is mixing declarative and programmatic transaction management have the potential for trouble? (Though I'm doing nothing more than setting the transaction name). Is there a better way to associate the generated AuditEntry objects with the current transaction? This solution does work for me, but given that the TransactionSynchronizationManager isn't intended for application use, I'd like to make sure that my use of it won't cause some unforseen problems.
Related Question
Finally, a related, but not immediately pertinent question: I know that the documentation for JPA EntityListeners cautions against using the current EntityManager, but if I did want to use it to diff an object against it's persisted self, would I be safe using an @Transactional(propagation=REQUIRES_NEW) annotation around my preUpdate() method?
Prototype Code:
Service Class
@Transactional
public void create(MyEntity e) {
TransactionSynchronizationManager.setCurrentTransactionName(new UID().toString());
th开发者_如何学Gois.em.persist(e);
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
Set<AuditEntry> entries = auditService.getAuditEntries(TransactionSynchronizationManager.getCurrentTransactionName());
if(entries != null) {
for(AuditEntry entry : entries) {
//do some stuff....
LOG.info(entry.toString());
}
}
}
});
}
JPA EntityListener
@Configurable
public class AuditEntryListener {
@Autowired
private AuditService service;
@PreUpdate
public void preUpdate(Object entity) {
service.auditUpdate(TransactionSynchronizationManager.getCurrentTransactionName(), entity);
}
public void setService(AuditService service) {
this.service = service;
}
public AuditService getService() {
return service;
}
}
AuditService
@Service
public class AuditService {
private Map<String, Set<AuditEntry>> auditEntryMap = new HashMap<String, Set<AuditEntry>>();
public void auditUpdate(String key, Object entity) {
// do some audit work
// add audit entries to map
this.auditEntryMap.get(key).add(ae);
}
}
@Filip
As far as I understand, your requirement is:
- Have an unique token generated within each transaction (database transaction of course)
- Keep this unique token easily accessible across all layers
So naturally you're thinking about the TransactionSynchronizationManager provided by Spring as a facility to store the unique token (in this case, an UID)
Be very carefull with this approach, the TransactionSynchronizationManager is the main storage helper to manage all the @Transactional processing for Spring. Under the @Transactional hood, Spring is creating an appropriate EntityManager, an appropriate Synchronization object and attach them to a thread local using TransactionSynchronizationManager.
In your service class code, inside a @Transactional method your are tampering with the Synchronization object, it can end up with undesirable behavior.
I've done an indept analysis of how @Transactional works here, have a look: http://doanduyhai.wordpress.com/2011/11/20/spring-transactional-explained/
Now back to your needs. What you can do is:
- Add a Thread local to the AuditService, containing the unique token when entering the @Transactional method and destroy it when exiting the method. Within this method call, you can access the unique token in any layer. Explanation for ThreadLocal usage can be found here: http://doanduyhai.wordpress.com/2011/12/04/threadlocal-explained/
Create a new annotation, let's say @Auditable(uid="AuditScenario1") to annotate methods that need to be audited and use Spring AOP to intercept these method calls and manage the Thread local processing for you
Example:
Modified AuditService
@Service
public class AuditService {
public uidThreadLocal = new ThreadLocal<String>();
...
...
}
Auditable annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface Auditable
{
String uid();
}
Usage of @Auditable annotation
@Auditable(uid="AuditScenario1")
@Transactional
public void myMethod()
{
// Something
}
Spring AOP part
@Around("execution(public * *(..)) && @annotation(auditableAnnotation))
public Object manageAuditToken(ProceedingJoinPoint jp, Auditable auditableAnnotation)
{
...
...
AuditService.uidThreadLocal.set(auditableAnnotation.uid())...
...
}
Hope this will help.
You can come up with a solution using the TransactionSynchronizationManager. We register a "TransactionInterceptorEntityListener" with JPA as an entity-listener. What we wanted to achieve is the ability to listen to CRUD events such that we can work with a spring managed "listener" that has a lifecycle tied to the current transaction (i.e., spring-managed but instance per transaction). We sub-class the JPATransactionManager and introduce in the prepareSynchronization() method, a hook to setup a "TransactionInterceptorSynchronizer." We also use the same hook for allow code (in programmatic tx) to associate and retrieve arbitrary objects with the current transaction and also register jobs that run before/after transaction commit.
The overall code is complex, but definitely do-able. If you use JPATemplates for programmatic tx, it is tough to achieve this. So we rolled our own template that simply calls the JPA template after taking care of the interceptor work. We plan to open-source our JPA library (written on top of Spring's classes) soon.
You can see a pattern of adding custom transactions and hooks with Spring managed transactions in the following library for Postgresql
精彩评论