Hallo allerseits,
bzgl. meines alten Threads zum Thema LazyInitializationException, Domain Model und DTOs hier nun eine prototypische aspektorientierte Lösung mit Spring:
OnDemandPreloader:
PreloadService:
PreloadDao:
Das Konzept sollte nicht dazu mißbraucht werden, die Daten nur auf diesem Weg zu Laden, denn sonst könnte ein recht großer Overhead entstehen (RPC + Transaktion pro Lazy Getter). Vielmehr kann es als Auffangnetz fungieren, falls mal Daten noch nicht geladen wurden. Die Anwendung wird dadurch also fehlertoleranter.
Etwaige Performanceprobleme kann man im Nachhinein durch Analyse des OnDemandPreloadings beheben, indem man das OnDemandPreloading in einem spezifischen Use Case nachträglich durch ein Fetch Join ersetzt. Sinnvollerweise sollte das OnDemanPreloading evtl. inkl. Performancemessung mitgeloggt werden, um die Analyse zu erleichtern.
Das Ganze soll ein Lösungsansatz darstellen für Systeme, die nicht auf das "Open Session in View" Pattern setzen können (z.B. 3-Schicht-Anwendungen mit Swing Client).
Kritik oder Anregungen?
Grüße byto
bzgl. meines alten Threads zum Thema LazyInitializationException, Domain Model und DTOs hier nun eine prototypische aspektorientierte Lösung mit Spring:
OnDemandPreloader:
Code:
/**
* Aspekt zum dynamischen transparenten Nachladen von Collections (lazy loading).
*
* @see PreloadService
* @author Benjamin Winterberg
*/
@Aspect
public class OnDemandPreloader {
public OnDemandPreloader() {
getLogger().setLevel(Level.DEBUG);
}
/**
* Aspekt führt Lazy Loading durch. Er sollte um allen Gettern des Domainmodells liegen,
* die eine Collection zurückliefern und im Hibernate Mapping für lazy loading konfiguriert
* sind. Dieser Around-Advice fängt etwaige {@link LazyInitializationException}s ab und holt
* die Daten mit Hilfe des {@link PreloadService} vom Server auf den Client. Danach wird per
* Reflection der entsprechende Setter auf dem Zielobjekt aufgerufen und die vorgeladene
* Collection gesetzt. Es bleibt zu prüfen, ob dieses Vorgehen zwecks Performance sinnvoll ist
* (viele Aufrufe führen zu vielen Remote Procedure Calls und vielen Transaktionen).
* @see PreloadService
* @param pjp
* @return
* @throws Throwable
*/
@Around("execution(public * de.xyz.model..*.*())")
public Object preloadOnDemand(ProceedingJoinPoint pjp) throws Throwable {
Object result = null;
try {
result = pjp.proceed();
if (result instanceof Collection) {
Collection<?> collection = (Collection<?>)result;
collection.iterator(); // könnte LazyInitExc auslösen
}
} catch (LazyInitializationException e) {
Object target = pjp.getTarget();
String getterName = pjp.getSignature().getName();
// getLogger().warn("LazyInitializationException: " + getterName + " on " + target.getClass().getName());
if (target instanceof PersistentEntity) {
PersistentEntity entity = (PersistentEntity)target;
PreloadService preloadService = UtilFactory.getPreloadService();
// getLogger().debug("PreloadService calling...");
Collection<?> preloadedCollection = preloadService.preloadCollection(entity, getterName);
// getLogger().debug("PreloadService result: " + preloadedCollection);
setPreloadedCollection(target, PropertyUtils.getterToSetter(getterName), preloadedCollection);
result = pjp.proceed();
}
}
return result;
}
/**
* Ruft auf dem Zielobjekt den Setter auf und setzt die vorgeladene Collection. Der Aufruf findet
* mittels Reflection statt. Es ist sicherzustellen, dass ein entsprechender Setter im Zielobjekt
* existiert und dass dieser genau einen Parameter vom Typ Collection annimmt. Ansonsten fliegt
* eine RuntimeException.
* @param target Zielobjekt
* @param setterName Name der Setter-Methode
* @param preloadedCollection Argument für die Setter-Methode
* @throws IllegalArgumentException
*/
private void setPreloadedCollection(Object target, String setterName, Collection<?> preloadedCollection) throws IllegalArgumentException {
if (target == null || setterName == null || preloadedCollection == null) {
throw new IllegalArgumentException("Null argument is forbidden!");
}
if (!setterName.startsWith("set") || preloadedCollection.size() == 0) {
throw new IllegalArgumentException("Setter doesn't start with 'set' or collection size is zero");
}
try {
// TODO: Hier wird derzeit nur List, Set, Map unterstützt (ggf. anpassen)
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 if (preloadedCollection instanceof Map) {
Method setter = target.getClass().getMethod(setterName, Map.class);
setter.invoke(target, preloadedCollection);
}
else {
throw new RuntimeException(preloadedCollection.getClass().getName() + " kann nicht gesetzt werden!");
}
} catch (SecurityException e) {
throw new RuntimeException("Internal error while setting preloaded collection", e);
} catch (NoSuchMethodException e) {
throw new RuntimeException("Internal error while setting preloaded collection", e);
} catch (IllegalAccessException e) {
throw new RuntimeException("Internal error while setting preloaded collection", e);
} catch (InvocationTargetException e) {
throw new RuntimeException("Internal error while setting preloaded collection", e);
}
}
private Logger getLogger() {
return Logger.getLogger(getClass());
}
}
PreloadService:
Code:
@Transactional(propagation=Propagation.REQUIRED, rollbackFor=Throwable.class)
public class PreloadServiceImpl implements PreloadService {
private PreloadDao preloadDao;
public PreloadServiceImpl() {
}
public Collection<?> preloadCollection(PersistentEntity entity, String getterName) {
if (entity == null || getterName == null || !getterName.startsWith("get")) {
return null;
}
return preloadDao.preloadCollection(entity, getterName);
}
public void setPreloadDao(PreloadDao preloadDao) {
this.preloadDao = preloadDao;
}
}
PreloadDao:
Code:
public class PreloadDaoHibernate implements PreloadDao {
private HibernateTemplate hibernateTemplate;
public PreloadDaoHibernate() {
}
public Collection<?> preloadCollection(PersistentEntity entity, String getterName) {
if (entity == null || getterName == null) {
return null;
}
hibernateTemplate.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 {
throw new RuntimeException("Ergebnis ist keine Collection!");
}
} catch (SecurityException e) {
Logger.getLogger(getClass()).error("Internal error", e);
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
Logger.getLogger(getClass()).error("Internal error", e);
throw new RuntimeException(e);
} catch (IllegalArgumentException e) {
Logger.getLogger(getClass()).error("Internal error", e);
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
Logger.getLogger(getClass()).error("Internal error", e);
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
Logger.getLogger(getClass()).error("Internal error", e);
throw new RuntimeException(e);
}
}
private Collection<?> fetch(Collection<?> collection) {
collection.iterator();
return collection;
}
public void setHibernateTemplate(HibernateTemplate hibernateTemplate) {
this.hibernateTemplate = hibernateTemplate;
}
}
Das Konzept sollte nicht dazu mißbraucht werden, die Daten nur auf diesem Weg zu Laden, denn sonst könnte ein recht großer Overhead entstehen (RPC + Transaktion pro Lazy Getter). Vielmehr kann es als Auffangnetz fungieren, falls mal Daten noch nicht geladen wurden. Die Anwendung wird dadurch also fehlertoleranter.
Etwaige Performanceprobleme kann man im Nachhinein durch Analyse des OnDemandPreloadings beheben, indem man das OnDemandPreloading in einem spezifischen Use Case nachträglich durch ein Fetch Join ersetzt. Sinnvollerweise sollte das OnDemanPreloading evtl. inkl. Performancemessung mitgeloggt werden, um die Analyse zu erleichtern.
Das Ganze soll ein Lösungsansatz darstellen für Systeme, die nicht auf das "Open Session in View" Pattern setzen können (z.B. 3-Schicht-Anwendungen mit Swing Client).
Kritik oder Anregungen?
Grüße byto