During my search for a solution to this problem I found a lot of different posts and forum entries dedicated to the problem. Programmers seem to look for the solution, but either they gave up, or if they found a solution they were not willing to share it with the community (at least not the complete solution). Hopefully this post will fill the gap.
As mentioned already before, the solution to the problem is in any case the use of an aspect. Spring offers AOP functionality which allows to intersect method calls in every spring managed beans. For what we want in this case, this is not enough. We do not want to intersect spring managed beans, but hibernate managed entity objects.
The magic word in this case is "load-time-weaver". Spring offers the possibility to weave its aspects into every kind of objects when its load-time-weaving functionality is enabled.
That's basically all on prerequisites that you'll need, so lets see some code. Starting with the Aspect itself
This class is quite simple. The @Pointcut annotated method defines just where the aspect should be applied. In this case to each getter in the model (of course this could be enhanced by avoiding simple types). An improvement to this could be to use a special annotation like "@LazyLoadable" on every getter that could be a candidate for lazy loading, but for simplicity it is stated like this.
@Aspect
@Component
@Configurable(autowire = Autowire.BY_TYPE)
public class OnDemandPreloader {
@Autowired
private PreloadService preloadService;
@Pointcut("execution(public * path.to.model..*.get*(..))")
public void possibleLazyInitializationException() {
}
@Around(value = "possibleLazyInitializationException()")
public Object preloadOnDemand(ProceedingJoinPoint pjp) throws Throwable {
Object result = pjp.proceed();
if (result instanceof AbstractPersistentCollection) {
// The object is either a map, a set or a list (basically @OneToMany or @ManyToMany)
AbstractPersistentCollection collection = (AbstractPersistentCollection) result;
if (!collection.wasInitialized() && collection.getSession() == null) {
// Calling the getter to the object would lead to a LazyInitializationException, therfore reload it
Object target = pjp.getTarget();
String getterName = pjp.getSignature().getName();
if (target instanceof BaseEntity) {
BaseEntity entity = (BaseEntity) target;
Object preloadedCollection = preloadService.preloadCollection(entity, getterName);
setPreloadedResult(target, getterName, preloadedCollection);
result = pjp.proceed();
}
}
} else if (result instanceof BaseEntity) {
// The Object is another BaseEntity
BaseEntity bdo = (BaseEntity) result;
if (bdo instanceof HibernateProxy && !Hibernate.isInitialized(bdo)
&& ((HibernateProxy) bdo).getHibernateLazyInitializer().getSession() == null) {
// Calling the getter to the object would lead to a LazyInitializationException, therfore reload it
Object target = pjp.getTarget();
String getterName = pjp.getSignature().getName();
if (target instanceof BaseEntity) {
BaseEntity entity = (BaseEntity) target;
Object preloadedObject = preloadService.preloadBaseEntity(entity, getterName);
setPreloadedResult(target, getterName, preloadedObject);
result = pjp.proceed();
}
}
}
return result;
}
private void setPreloadedResult(Object target, String getterName, Object preloadedContent) throws IllegalArgumentException {
if (target == null || getterName == null || preloadedContent == null) {
// "Something is null! Could not execute lazy loading"
} else {
String setterName = getterName.replaceFirst("get", "set");
if (!setterName.startsWith("set")) {
// "Setter doesn't start with 'set'"
} else {
try {
Method getter = target.getClass().getMethod(getterName);
Class getterType = (getter != null ? getter.getReturnType() : null);
if (preloadedContent instanceof Collection) {
setPreloadedCollection(target, setterName, (Collection) preloadedContent);
} else if (preloadedContent instanceof Map) {
setPreloadedMap(target, setterName, (Map) preloadedContent);
} else if (preloadedContent instanceof BaseEntity) {
setPreloadedBaseEntity(target, setterName, (BaseEntity) preloadedContent, getterType);
}
} catch (Exception e) {
// Do something
}
}
}
private void setPreloadedBaseEntity(Object target, String setterName, BaseEntity preloadedContent, Class paramType) throws [..] {
Method setter = target.getClass().getMethod(setterName, (paramType != null ? paramType : Hibernate.getClass(preloadedContent)));
setter.invoke(target, preloadedContent);
}
private void setPreloadedMap(Object target, String setterName, Map preloadedMap) throws [..] {
Method setter = target.getClass().getMethod(setterName, Map.class);
setter.invoke(target, preloadedMap);
}
private void setPreloadedCollection(Object target, String setterName, Collection preloadedCollection) throws [..] {
if (preloadedCollection instanceof List) {
Method setter = target.getClass().getMethod(setterName, List.class);
setter.invoke(target, preloadedCollection);
} else if (preloadedCollection instanceof Set) {
Method setter = target.getClass().getMethod(setterName, Set.class);
setter.invoke(target, preloadedCollection);
} else {
[..]
}
}
public void setPreloadService(PreloadService preloadService) {
this.preloadService = preloadService;
}
}
The @Around annotated method first checks if the requested resource is of type "AbstractPersistentCollection"(A list, set or map) or of type "BaseEntity"(the mother of all entity classes in your domain; This class is probably annotated with "@mappedSuperClass" and contains for example an ID, or a date saving the last change of the entity; In short, everything that could be interesting for every entity in your model). Depending on the type it checks whether a call to its getter would cause the LazyInitializationException(check for isInitialized and if the session is there). If so it calls the preloadService to take care of the lazy reloading and with its result it calls an apropriate setPreloaded*() method which puts the preloaded object into place using the dedicated setter (!CAUTION! This process assumes coding standards, like for getter and setter names the same base-method name is used just differing by its prefix which is "get" or "set" eg. getName and setName)
In this way after the aspect the requested resource is in place and the normal call to its getter can return it. This brings us to the preloadService, which is quite a simple spring bean. Its purpose is to open the transaction and forward the call to the DAO.
@Named
public class PreloadServiceImpl implements PreloadService {
@Inject
private PreloadDao preloadDao;
@Override
@Transactional(readOnly = true, propagation = Propagation.REQUIRED)
public Object preloadCollection(BaseEntity entity, String getterName) {
if (entity == null || getterName == null || !getterName.startsWith("get")) {
return null;
}
return preloadDao.preloadCollection(entity, getterName);
}
@Override
@Transactional(readOnly = true, propagation = Propagation.REQUIRED)
public BaseEntity preloadBaseEntity(BaseEntity entity, String getterName) {
if (entity == null || getterName == null || !getterName.startsWith("get")) {
return null;
}
return preloadDao.preloadBaseEntity(entity, getterName);
}
}
Nothing special in here; Just a bean that forwards to the DAO. This structure is just to maintain the structure of the rest of the project.
The last is the DAO which reloads an entity und tries in some way to initialize(fetch) the content in order to return it.
@Named
public class PreloadDaoHibernateImpl implements PreloadDao {
@Inject
SessionFactory sessionFactory;
@Override
public Object preloadCollection(BaseEntity entity, String getterName) {
if (entity != null && getterName != null) {
// Reload current entity
sessionFactory.getCurrentSession().load(entity, entity.getId());
try {
Method getter = entity.getClass().getMethod(getterName, (Class[]) null);
Object answer = getter.invoke(entity, (Object[]) null);
if (answer instanceof Collection) {
return fetch((Collection) answer);
} else if (answer instanceof Map) {
return fetch((Map) answer);
} else {
// "Result is not a Collection!"
}
} catch (Exception e) {
}
}
return null;
}
@Override
public BaseEntity preloadBaseEntity(BaseEntity entity, String getterName) {
if (entity != null && getterName != null) {
// Reload current entity
sessionFactory.getCurrentSession().load(entity, entity.getId());
try {
Method getter = entity.getClass().getMethod(getterName, (Class[]) null);
Object answer = getter.invoke(entity, (Object[]) null);
if (answer instanceof BaseEntity) {
return fetch((BaseEntity) answer);
}else
// "Result is not a BaseEntity!"
} catch (Exception e) {
}
}
return null;
}
private Map fetch(Map map) {
map.values();
return map;
}
private Collection fetch(Collection collection) {
collection.iterator();
return collection;
}
private BaseEntity fetch(BaseEntity answer) {
answer.getId();
return answer;
}
}
The basic idea behind is to load the entity into the session and call the getter that originally would have failed at that call. Depending on the type of object now try to initialize it.
And that is basically all you need to code. Just take it, adapt it to the names and namespaces used in your project and that's it.
Well, not completely. As said before now we need to load-time-weave the application. In Spring there is for sure something like the "application-context.xml" which contains an entry like "</context:annotation-config> as nowadays beans are defined using annotations. To this line you have to add the following:
<aop:aspectj-autoproxy>And if you haven't already put the OnDemadPreLoader.java into a Spring scanned package add it to like
<context:annotation-config>
<context:spring-configured>
<context:load-time-weaver>
<context:component-scan base-package="some.package.lazy" />The latter alows Spring to find your aspekt, whereas the other configurations enable load-time-weaving. Now as a last file you have to create under src/main/java/resources (or wherever your "application-context.xml" is located) a folder META-INF containing a file named aop.xml.
<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>
<weaver options="-verbose -showWeaveInfo">
<include within="path.to.model.hibernate.*" />
<include within="path.to.onDemandPreloader.lazy.*" />
<exclude within="*..*javassist*" />
<exclude within="*..*CGLIB*" />
</weaver>
<aspects>
<aspect name="path.to.onDemandPreloader.lazy.OnDemandPreloader" />
</aspects>
</aspectj>
This just says where you want to weave in your aspect and what aspect it is. Furthermore it uses the configurable annotation together with the autowired one to inject dependencies.
Now we are almost done. In my case there is just one more thing, which is inside the server.xml that tomcat uses you have to put like
<Context docBase="projectName" path="/projectName" reloadable="true" source="org.eclipse.jst.jee.server:projectName">
<Loader loaderClass="org.springframework.instrument.classloading.tomcat.TomcatInstrumentableClassLoader" />
</Context>
which replaces the original TomCat classloader with a spring one. and thats it basically. Just start your tomCat now with the javaagent set to the instrumented one and it should work
-javaagent:./src/main/webapp/WEB-INF/lib/spring-instrument-3.0.2.RELEASE.jarHere are some useful links where I found the concept to this ideas:
Hi,
ReplyDeleteExcellent post! Thank you for summarizing this because I am attempting to do the same thing. I find however that the load time weaving isn't working because hibernate is loading the classes in a javassist proxy which you have excluded in your aop.xml file. Is there something I am overlooking in the spring - hibernate config? Are you declaring your hibernate model classes as spring beans? I would greatly appreciate if you could show some details of your applicationContext.xml for the bean declaration or respond with some more information on how you have declared and instantiate the hibernate entities.
Hi,
ReplyDeleteso, I define my Hibernate entities using annotations, so in spring I've just to set the path where it has to search for them.
The Idea behind this and the solution described in the following post is whenever there is a javaassist that is not connected to the session, the aspect reopens the session and calls the corresponding gettermethod for the attribute.
I can provide you everything you need, just explain me more in detail where you need more explanation