Spring-injected Beans in JPA EntityListeners
In Database Encryption Using JPA Listeners I discussed transparent encryption using a JPA EntityListener. This approach was transparent in the sense that the JPA Entity was (nearly) entirely unaware that encryption was occurring and the JPA EntityListener itself was unaware of the details.
There was one big problem. EJB3 can inject resources into an EntityListener. Spring cannot.
(N.B., I am referring to JPA EntityListeners, not AOP methods. An EntityListener has to work under much tighter constraints.)
Skip forward a year and I have a solution. There is one big caveat: this only works if you know that your JPA implementation is Hibernate 4.0.0.Final or better. It’s never fun to introduce implementation details into your design but fortunately we can hide nearly all of them.
Hibernate Callbacks
Hibernate has had callbacks for years. I don’t know how widely they were used – they’re the type of thing that will cause vendor lock-in if you’re not careful. That vendor lock-in is why I, and many other people, try to remain pure JPA.
Sadly sometimes JPA isn’t quite enough.
Enter a blog entry from nearly two years ago: Spring managed event listeners with JPA.
Cutting to the code in post Hibernate 4.0.0.Final world it is easy to add Hibernate listeners. These aren’t JPA EntityListeners but let’s take it one step at a time.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | /** * Configure Spring-aware entity listeners. This implementation is * hibernate-specific. * * /05/spring-managed-event-listeners-with-jpa/ * * Another approach follows, but it doesn't support Spring injection. * -4-0-with-spring-3-1-0-release * * @author louis.gueye@gmail.com (see above) * @author Bear Giles <bgiles@coyotesong.com> */ @Component public class HibernateListenersConfigurer { private static final Logger log = LoggerFactory.getLogger(HibernateListenersConfigurer. class ); @Resource private EntityManagerFactory emf; @Resource private HibernateListenersAdapter listener; @PostConstruct public void registerListeners() { HibernateEntityManagerFactory hemf = (HibernateEntityManagerFactory) emf; SessionFactory sf = hemf.getSessionFactory(); EventListenerRegistry registry = ((SessionFactoryImpl) sf).getServiceRegistry().getService( EventListenerRegistry. class ); registry.getEventListenerGroup(EventType.PRE_INSERT).appendListener(listener); registry.getEventListenerGroup(EventType.POST_COMMIT_INSERT).appendListener(listener); registry.getEventListenerGroup(EventType.PRE_UPDATE).appendListener(listener); registry.getEventListenerGroup(EventType.POST_COMMIT_UPDATE).appendListener(listener); registry.getEventListenerGroup(EventType.PRE_DELETE).appendListener(listener); registry.getEventListenerGroup(EventType.POST_COMMIT_DELETE).appendListener(listener); registry.getEventListenerGroup(EventType.POST_LOAD).appendListener(listener); } } |
Hibernate/JPA Adapter
The prior class is a good start but the spring-injected listener needs to use Hibernate entity listener annotations, not standard JPA javax.persistence annotations. Can we do better?
The quick answer is yes – it’s straightforward with reflection. See below.
The not-as-quick answer is yes – but getting the reflection right can be tricky. You need to consider superclasses and interfaces, e.g., an EntityListener may be written for an Auditable interface instead of a specific class. Ordering can become important, especially when a class and its superclass(es) are annotated. I won’t pretend to address these issues in the code below.
The “oh God what were we thinking?” answer is yes – but a robust solution will also have a way to specify that superclasses and/or interfaces should not be checked, to specify or exclude default listeners, etc.
Few of us will encounter these situations in our own code. Library writers have to worry about them but we can start with the most basic assumptions and only add to them as the need arises.
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 | import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import javax.annotation.PostConstruct; import javax.annotation.Resource; import javax.persistence.Entity; import javax.persistence.EntityManagerFactory; import javax.persistence.PostLoad; import javax.persistence.PostPersist; import javax.persistence.PostRemove; import javax.persistence.PostUpdate; import javax.persistence.PrePersist; import javax.persistence.PreRemove; import javax.persistence.PreUpdate; import org.apache.log4j.Logger; import org.hibernate.SessionFactory; import org.hibernate.ejb.HibernateEntityManagerFactory; import org.hibernate.event.service.spi.EventListenerRegistry; import org.hibernate.event.spi.PostDeleteEvent; import org.hibernate.event.spi.PostDeleteEventListener; import org.hibernate.event.spi.PostInsertEvent; import org.hibernate.event.spi.PostInsertEventListener; import org.hibernate.event.spi.PostLoadEvent; import org.hibernate.event.spi.PostLoadEventListener; import org.hibernate.event.spi.PostUpdateEvent; import org.hibernate.event.spi.PostUpdateEventListener; import org.hibernate.event.spi.PreDeleteEvent; import org.hibernate.event.spi.PreDeleteEventListener; import org.hibernate.event.spi.PreInsertEvent; import org.hibernate.event.spi.PreInsertEventListener; import org.hibernate.event.spi.PreUpdateEvent; import org.hibernate.event.spi.PreUpdateEventListener; import org.hibernate.internal.SessionFactoryImpl; /** * Adapter that allows a Hibernate event listener to call a standard JPA * EntityListener. * * For simplicity only a single bean of each class is supported. It is not * difficult to support multiple beans, just messy. * * Each listener can have multiple methods with the same annotation. * * @author Bear Giles <bgiles@coyotesong.com> */ public class HibernateListenersAdapter implements PostInsertEventListener, PreInsertEventListener, PreUpdateEventListener, PostUpdateEventListener, PreDeleteEventListener, PostDeleteEventListener, PostLoadEventListener { private static final long serialVersionUID = 1L; private static final Logger log = Logger.getLogger(HibernateListenersAdapter. class ); @Resource private List<Object> listeners; @Resource private EntityManagerFactory emf; private Map<Class, Map<Method, Object>> preInsert = new LinkedHashMap<Class, Map<Method, Object>>(); private Map<Class, Map<Method, Object>> postInsert = new LinkedHashMap<Class, Map<Method, Object>>(); private Map<Class, Map<Method, Object>> preUpdate = new LinkedHashMap<Class, Map<Method, Object>>(); private Map<Class, Map<Method, Object>> postUpdate = new LinkedHashMap<Class, Map<Method, Object>>(); private Map<Class, Map<Method, Object>> preRemove = new LinkedHashMap<Class, Map<Method, Object>>(); private Map<Class, Map<Method, Object>> postRemove = new LinkedHashMap<Class, Map<Method, Object>>(); private Map<Class, Map<Method, Object>> postLoad = new LinkedHashMap<Class, Map<Method, Object>>(); private EventListenerRegistry registry; @PostConstruct public void findMethods() { for (Object listener : listeners) { findMethodsForListener(listener); } HibernateEntityManagerFactory hemf = (HibernateEntityManagerFactory) emf; SessionFactory sf = hemf.getSessionFactory(); registry = ((SessionFactoryImpl) sf).getServiceRegistry().getService(EventListenerRegistry. class ); } public void findMethodsForListener(Object listener) { Class<?> c = listener.getClass(); for (Method m : c.getMethods()) { if (Void.TYPE.equals(m.getReturnType())) { Class<?>[] types = m.getParameterTypes(); if (types.length == 1 ) { // check for all annotations now... if (m.getAnnotation(PrePersist. class ) != null ) { if (!preInsert.containsKey(types[ 0 ])) { preInsert.put(types[ 0 ], new LinkedHashMap<Method, Object>()); } preInsert.get(types[ 0 ]).put(m, listener); } if (m.getAnnotation(PostPersist. class ) != null ) { if (!postInsert.containsKey(types[ 0 ])) { postInsert.put(types[ 0 ], new LinkedHashMap<Method, Object>()); } postInsert.get(types[ 0 ]).put(m, listener); } if (m.getAnnotation(PreUpdate. class ) != null ) { if (!preUpdate.containsKey(types[ 0 ])) { preUpdate.put(types[ 0 ], new LinkedHashMap<Method, Object>()); } preUpdate.get(types[ 0 ]).put(m, listener); } if (m.getAnnotation(PostUpdate. class ) != null ) { if (!postUpdate.containsKey(types[ 0 ])) { postUpdate.put(types[ 0 ], new LinkedHashMap<Method, Object>()); } postUpdate.get(types[ 0 ]).put(m, listener); } if (m.getAnnotation(PreRemove. class ) != null ) { if (!preRemove.containsKey(types[ 0 ])) { preRemove.put(types[ 0 ], new LinkedHashMap<Method, Object>()); } preRemove.get(types[ 0 ]).put(m, listener); } if (m.getAnnotation(PostRemove. class ) != null ) { if (!postRemove.containsKey(types[ 0 ])) { postRemove.put(types[ 0 ], new LinkedHashMap<Method, Object>()); } postRemove.get(types[ 0 ]).put(m, listener); } if (m.getAnnotation(PostLoad. class ) != null ) { if (!postLoad.containsKey(types[ 0 ])) { postLoad.put(types[ 0 ], new LinkedHashMap<Method, Object>()); } postLoad.get(types[ 0 ]).put(m, listener); } } } } } /** * Execute the listeners. We need to check the entity's class, parent * classes, and interfaces. * * @param map * @param entity */ private void execute(Map<Class, Map<Method, Object>> map, Object entity) { if (entity.getClass().isAnnotationPresent(Entity. class )) { // check for hits on this class or its superclasses. for (Class c = entity.getClass(); c != null && c != Object. class ; c = c.getSuperclass()) { if (map.containsKey(c)) { for (Map.Entry<Method, Object> entry : map.get(c).entrySet()) { try { entry.getKey().invoke(entry.getValue(), entity); } catch (InvocationTargetException e) { // log it } catch (IllegalAccessException e) { // log it } } } } // check for hits on interfaces. for (Class c : entity.getClass().getInterfaces()) { if (map.containsKey(c)) { for (Map.Entry<Method, Object> entry : map.get(c).entrySet()) { try { entry.getKey().invoke(entry.getValue(), entity); } catch (InvocationTargetException e) { // log it } catch (IllegalAccessException e) { // log it } } } } } } /** * @see org.hibernate.event.spi.PostDeleteEventListener#onPostDelete(org.hibernate * .event.spi.PostDeleteEvent) */ @Override public void onPostDelete(PostDeleteEvent event) { execute(postRemove, event.getEntity()); } /** * @see org.hibernate.event.spi.PreDeleteEventListener#onPreDelete(org.hibernate * .event.spi.PreDeleteEvent) */ @Override public boolean onPreDelete(PreDeleteEvent event) { execute(preRemove, event.getEntity()); return false ; } /** * @see org.hibernate.event.spi.PreInsertEventListener#onPreInsert(org.hibernate * .event.spi.PreInsertEvent) */ @Override public boolean onPreInsert(PreInsertEvent event) { execute(preInsert, event.getEntity()); return false ; } /** * @see org.hibernate.event.spi.PostInsertEventListener#onPostInsert(org.hibernate * .event.spi.PostInsertEvent) */ @Override public void onPostInsert(PostInsertEvent event) { execute(postInsert, event.getEntity()); } /** * @see org.hibernate.event.spi.PreUpdateEventListener#onPreUpdate(org.hibernate * .event.spi.PreUpdateEvent) */ @Override public boolean onPreUpdate(PreUpdateEvent event) { execute(preUpdate, event.getEntity()); return false ; } /** * @see org.hibernate.event.spi.PostUpdateEventListener#onPostUpdate(org.hibernate * .event.spi.PostUpdateEvent) */ @Override public void onPostUpdate(PostUpdateEvent event) { execute(postUpdate, event.getEntity()); } /** * @see org.hibernate.event.spi.PostLoadEventListener#onPostLoad(org.hibernate * .event.spi.PostLoadEvent) */ @Override public void onPostLoad(PostLoadEvent event) { execute(postLoad, event.getEntity()); } } |
@SpringEntityListeners
This approach requires the HibernateListenersConfigurer bean be passed an explicit list of beans. Could we use an annotation on our entity beans instead? Call it @SpringEntityListeners, in contrast to the standard JPA @EntityListeners, and pass it bean classes.
If only there were a way to get a list of managed beans….
There is! The JPA EntityManager provides a way to get a metamodel that includes all managed beans. We can scan this list to find annotated entity classes.
The code:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | @Component public class SpringEntityListenersConfigurer implements ApplicationContextAware { private static final Logger log = LoggerFactory.getLogger(SpringEntityListenersConfigurer. class ); private ApplicationContext context; @Resource private EntityManagerFactory entityManagerFactory; @Override public void setApplicationContext(ApplicationContext context) { this .context = context; } @PostConstruct public void registerListeners() { // get registry so we can add listeners. HibernateEntityManagerFactory hemf = (HibernateEntityManagerFactory) entityManagerFactory; SessionFactory sf = hemf.getSessionFactory(); EventListenerRegistry registry = ((SessionFactoryImpl) sf).getServiceRegistry().getService( EventListenerRegistry. class ); final Set<Object> listeners = new HashSet<Object>(); EntityManager entityManager = null ; try { entityManager = hemf.createEntityManager(); // for every entity known to the system... for (EntityType<?> entity : entityManager.getMetamodel().getEntities()) { // ... register event listeners for it. if (entity.getJavaType().isAnnotationPresent(SpringEntityListeners. class )) { SpringEntityListeners annotation = (SpringEntityListeners) entity.getJavaType().getAnnotation( SpringEntityListeners. class ); for (Class<?> beanClass : annotation.value()) { Map<String, ?> map = context.getBeansOfType(beanClass); listeners.addAll(map.values()); } } } } finally { if (entityManager != null ) { entityManager.close(); } } // register adapter and listeners. HibernateEntityListenersAdapter adapter = new HibernateEntityListenersAdapter( new ArrayList<Object>(listeners), entityManagerFactory); registry.getEventListenerGroup(EventType.PRE_INSERT).appendListener(adapter); registry.getEventListenerGroup(EventType.POST_COMMIT_INSERT).appendListener(adapter); registry.getEventListenerGroup(EventType.PRE_UPDATE).appendListener(adapter); registry.getEventListenerGroup(EventType.POST_COMMIT_UPDATE).appendListener(adapter); registry.getEventListenerGroup(EventType.PRE_DELETE).appendListener(adapter); registry.getEventListenerGroup(EventType.POST_COMMIT_DELETE).appendListener(adapter); registry.getEventListenerGroup(EventType.POST_LOAD).appendListener(adapter); } } |
Code is available at http://code.google.com/p/invariant-properties-blog/source/browse/spring-entity-listener. In addition to simple logger entity listeners and a transparent password encryption entity listener it has a basic spring-data implementation allowing demonstration of the code.