Remote Lazy Loading mit Spring und Hibernate

Status
Nicht offen für weitere Antworten.

byte

Top Contributor
Hallo allerseits,

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
 
M

maki

Gast
:toll:

Gute Sache byto!

Das ist auf jedenfall nützlich imho, wenn die Performance nicht reichen sollte kann man Analysieren welche SQL Statements die Ursache sind und diese dann anders lösen, aber nur falls nötig.

Danke für's Teilen deiner Lösung :)
 

KSG9|sebastian

Top Contributor
Jaja, das Prloading ist so ne Sache.

Hibernate.isInitialized funktioniert leider nicht auf Collections, da die nach dem Laden (als Proxys) schon als initialisiert gelten.

Insgesamt gibt es imho keinen richtig praktikablen weg. Auch die vorgeschlagene Lösung funktioniert nur im entsprechenden Umfeld.

Wir haben eine extrem modulare Anwendung in welcher gar kein direkte Backendaufruf erfolgen kann. Es ist immer mindestens noch eine Schicht daziwschen gekoppelt. Zusätzlich ist die Backend/Persistence-Logik soweit abgekoppelt (generiert u.s.w.) dass man aus Projektsicht kein sinnvolles Preloading implementieren kann. Vielleicht gibt es in Naher Zukunft mal eine Lösung von JPA/Hibernate dafür
 

byte

Top Contributor
KSG9|sebastian hat gesagt.:
Wir haben eine extrem modulare Anwendung in welcher gar kein direkte Backendaufruf erfolgen kann. Es ist immer mindestens noch eine Schicht daziwschen gekoppelt. Zusätzlich ist die Backend/Persistence-Logik soweit abgekoppelt (generiert u.s.w.) dass man aus Projektsicht kein sinnvolles Preloading implementieren kann. Vielleicht gibt es in Naher Zukunft mal eine Lösung von JPA/Hibernate dafür
Unsere Anwendung ist auch in verschiedene logische Schichten eingeteilt. Der Client stellt Anfragen über eine ServiceFacade, die die Aufrufe an die Remote-Services weiterleitet. Diese benutzen DAOs, die dann wiederum auf die Datenbank gehen.
Für die Umsetzung des Remote Lazy Loadings muss man nun in all diesen Schichten implementieren, daher kann man kein generelles Pattern zu dem Konzept schreiben. Die Realisierung ist immer vom jeweiligen Architekturentwurf abhängig.

Ich habe mittlerweile diverse Ansätze evaluiert, das Problem zu lösen. Und ich gebe Dir recht: es gibt kein Patentrezept. Es hängt imo extrem vom Anwendungsfall, von den Usecases und vor allem auch vom Domainmodell (den Objektgraphen) ab, welcher Lösungsansatz Sinn macht.

Wer übrigens kein Spring bzw. AOP in seinem Projekt nutzen will / kann, der kann das o.g. Pattern auch mit den dynamischen Proxies des JDK realisieren, ist jedoch etwas aufwändiger:

http://java.sun.com/javase/6/docs/api/java/lang/reflect/Proxy.html
 

foobar

Top Contributor
@byto Danke für den Code

Im Entwickler-Forum wurde mittlerweile auch noch eine alternative Lösung auf Basis von Hibernate Events vorgestellt: http://entwickler-forum.de/showpost.php?p=164047&postcount=10

Schade, daß es noch keine All-in-One Lösung von Hibernate für soetwas gibt. Denn jeder Entwickler einer 3-Schichtarchitektur, der Hibernate nutzen möchte, sieht sich mit solchen Problemen konfrontiert. Was fehlt ist nicht nur ein Mechanismus auf Serverseite sondern auch eine API oder ein Toolkit auf der Clientseite, um möglichst transparent Hibernate remote nutzen zu können. Eine Möglichkeit wäre auf dem Client eine API auf Basis von Java Proxies oder EMF zu implementieren, die dann über SOAP auf Hibernate zugreift. Also eine Art Wrapper für eine Remote Hibernate Session oder sowas *g*.
 
H

Hons

Gast
Diese Lösung würde mich für mein Projekt brennend interessieren.

Allerdings weis ich nicht genau wie ich auf load-time-weaving umstellen kann.
Was muss ich dabei beachten? bzw. muss ich dafür meine Domain-Objekte irgendwie annotieren?
 
Status
Nicht offen für weitere Antworten.

Ähnliche Java Themen

Neue Themen


Oben