Heim > Java > javaLernprogramm > Eine kurze Analyse der Caching- und Lazy-Loading-Mechanismen im Hibernate-Framework von Java

Eine kurze Analyse der Caching- und Lazy-Loading-Mechanismen im Hibernate-Framework von Java

高洛峰
Freigeben: 2017-01-23 13:05:21
Original
1363 Leute haben es durchsucht

Der Unterschied zwischen Cache der ersten Ebene und Cache der zweiten Ebene
Der Cache befindet sich zwischen der Anwendung und der physischen Datenquelle. Seine Funktion besteht darin, die Häufigkeit des Zugriffs der Anwendung auf die physische Datenquelle zu verringern und dadurch zu verbessern die Leistung der Anwendung. Die Daten im Cache sind eine Kopie der Daten in der physischen Datenquelle. Die Anwendung liest und schreibt zur Laufzeit Daten aus dem Cache und synchronisiert die Daten im Cache und in der physischen Datenquelle zu einem bestimmten Zeitpunkt oder Ereignis.
Das Cache-Medium ist normalerweise Speicher, daher ist die Lese- und Schreibgeschwindigkeit sehr hoch. Ist die im Cache gespeicherte Datenmenge jedoch sehr groß, wird auch die Festplatte als Cache-Medium verwendet. Bei der Implementierung des Caches muss nicht nur das Speichermedium berücksichtigt werden, sondern auch die Verwaltung des gleichzeitigen Zugriffs auf den Cache und der Lebenszyklus der zwischengespeicherten Daten.
Der Cache von Hibernate umfasst den Session-Cache und den SessionFactory-Cache. Der SessionFactory-Cache kann in zwei Kategorien unterteilt werden: integrierter Cache und externer Cache. Der Sitzungscache ist integriert und kann nicht entladen werden. Er wird auch als Cache der ersten Ebene von Hibernate bezeichnet. Der integrierte Cache von SessionFactory und der Cache von Session sind in der Implementierung ähnlich. Ersterer bezieht sich auf die Daten, die in einigen Sammlungsattributen des SessionFactory-Objekts enthalten sind, und letzterer bezieht sich auf die Daten, die in einigen Sammlungsattributen von Session enthalten sind. Der integrierte Cache von SessionFactory speichert Mapping-Metadaten und vordefinierte SQL-Anweisungen. Die Mapping-Metadaten sind eine Kopie der Daten in der Mapping-Datei, und die vordefinierte SQL-Anweisung wird während der Hibernate-Initialisierungsphase aus den Mapping-Metadaten abgeleitet Der Cache von SessionFactory dient nur zum Lesen. Die Anwendung kann die Zuordnungsmetadaten und vordefinierten SQL-Anweisungen im Cache nicht ändern, sodass SessionFactory den integrierten Cache nicht mit der Zuordnungsdatei synchronisieren muss. Der externe Cache von SessionFactory ist ein konfigurierbares Plug-in. Standardmäßig aktiviert SessionFactory dieses Plug-in nicht. Die Daten im externen Cache sind eine Kopie der Datenbankdaten, und das Medium des externen Caches kann Speicher oder Festplatte sein. Der externe Cache von SessionFactory wird auch als Second-Level-Cache von Hibernate bezeichnet.
Die beiden Cache-Ebenen von Hibernate befinden sich beide in der Persistenzschicht und speichern Kopien von Datenbankdaten. Was ist also der Unterschied zwischen ihnen? Um den Unterschied zwischen den beiden zu verstehen, ist ein tiefes Verständnis der beiden Merkmale des Caches der Persistenzschicht erforderlich: der Umfang des Caches und die gleichzeitige Zugriffsrichtlinie des Caches.
Der Umfang des Caches in der Persistenzschicht
Der Umfang des Caches bestimmt den Lebenszyklus des Caches und wer darauf zugreifen kann. Der Umfang des Caches ist in drei Kategorien unterteilt.
 1 Transaktionsbereich: Auf den Cache kann nur die aktuelle Transaktion zugreifen. Der Lebenszyklus des Caches hängt vom Lebenszyklus der Transaktion ab. Wenn die Transaktion endet, beendet auch der Cache seinen Lebenszyklus. In diesem Bereich ist das Cache-Medium der Speicher. Transaktionen können Datenbanktransaktionen oder Anwendungstransaktionen sein. Die Daten im Cache liegen normalerweise in Form miteinander verbundener Objekte vor.
 2 Prozessumfang: Der Cache wird von allen Transaktionen innerhalb des Prozesses gemeinsam genutzt. Diese Transaktionen können gleichzeitig auf den Cache zugreifen, daher müssen die erforderlichen Transaktionsisolationsmechanismen für den Cache übernommen werden. Der Lebenszyklus des Caches hängt vom Lebenszyklus des Prozesses ab. Wenn der Prozess endet, beendet auch der Cache seinen Lebenszyklus. Der prozessweite Cache kann eine große Datenmenge speichern, sodass das Speichermedium Speicher oder Festplatte sein kann. Die Daten im Cache können in Form verwandter Objekte oder loser Daten von Objekten vorliegen. Die Form der losen Objektdaten ähnelt in gewisser Weise der Form der serialisierten Daten des Objekts, der Algorithmus zur Objektzerlegung in lose Daten ist jedoch schneller als der für die Objektserialisierung erforderliche Algorithmus.
 3 Cluster-Bereich: In einer Cluster-Umgebung wird der Cache von Prozessen auf einem oder mehreren Computern gemeinsam genutzt. Die Daten im Cache werden auf jeden Prozessknoten in der Clusterumgebung kopiert, und die Remotekommunikation zwischen Prozessen wird verwendet, um die Konsistenz der Daten im Cache sicherzustellen. Die Daten im Cache liegen normalerweise in Form loser Objektdaten vor.
Bei den meisten Anwendungen sollten Sie sorgfältig abwägen, ob Sie einen Cluster-weiten Cache verwenden, da die Zugriffsgeschwindigkeit nicht unbedingt viel schneller ist als beim direkten Zugriff auf die Datenbankdaten.
Die Persistenzschicht kann mehrere Cachebereiche bereitstellen. Wenn die entsprechenden Daten nicht im Transaktionsbereich-Cache gefunden werden, können Sie sie auch im Prozessbereich- oder Clusterbereich-Cache abfragen. Wenn sie immer noch nicht gefunden werden, können Sie sie nur in der Datenbank abfragen. Der transaktionsweite Cache ist der Cache der ersten Ebene der Persistenzschicht und normalerweise erforderlich. Der prozessweite oder Cluster-weite Cache ist der Cache der zweiten Ebene der Persistenzschicht und normalerweise optional.
Strategie für gleichzeitigen Zugriff auf den Cache der Persistenzschicht
Wenn mehrere gleichzeitige Transaktionen gleichzeitig auf dieselben in der Persistenzschicht zwischengespeicherten Daten zugreifen, treten Parallelitätsprobleme auf und es müssen die erforderlichen Maßnahmen zur Transaktionsisolation ergriffen werden.
Parallelitätsprobleme können in prozessweiten oder Cluster-weiten Caches, also Caches der zweiten Ebene, auftreten. Daher können die folgenden vier Arten gleichzeitiger Zugriffsstrategien festgelegt werden, wobei jede Strategie einer Transaktionsisolationsstufe entspricht.
Transaktional: Nur anwendbar in verwalteten Umgebungen. Es bietet eine Transaktionsisolationsstufe für wiederholbares Lesen. Für Daten, die häufig gelesen, aber selten geändert werden, kann dieser Isolationstyp verwendet werden, da er Parallelitätsprobleme wie schmutzige Lesevorgänge und nicht wiederholbare Lesevorgänge verhindern kann.
Lese-/Schreibzugriff: Stellt die Transaktionsisolationsstufe „Read Committed“ bereit. Gilt nur in nicht gruppierten Umgebungen. Für Daten, die häufig gelesen, aber selten geändert werden, kann dieser Isolationstyp verwendet werden, da er Parallelitätsprobleme wie fehlerhafte Lesevorgänge verhindern kann.
Nicht strikter Lese-/Schreibtyp: Die Konsistenz der Daten im Cache und in der Datenbank ist nicht garantiert. Besteht die Möglichkeit, dass zwei Transaktionen gleichzeitig auf dieselben Daten im Cache zugreifen, muss eine kurze Datenablaufzeit für die Daten konfiguriert werden, um Dirty Reads zu vermeiden. Diese Strategie des gleichzeitigen Zugriffs kann für Daten verwendet werden, die selten geändert werden und gelegentlich fehlerhafte Lesevorgänge ermöglichen. Schreibgeschützt: Diese Strategie des gleichzeitigen Zugriffs kann für Daten verwendet werden, die niemals geändert werden, z. B. Referenzdaten.
Die transaktionale gleichzeitige Zugriffsstrategie hat die höchste Transaktionsisolationsstufe und die schreibgeschützte Isolationsstufe ist die niedrigste. Je höher die Transaktionsisolationsstufe, desto geringer ist die Parallelitätsleistung.
Welche Art von Daten eignen sich für die Speicherung im Second-Level-Cache?
1. Daten, die selten geändert werden
2. Daten, die nicht sehr wichtig sind, gelegentliche gleichzeitige Daten sind zulässig
3. Daten, auf die nicht gleichzeitig zugegriffen wird
4. Referenzdaten
Daten das ist nicht für die Speicherung im Second-Level-Cache geeignet?
1. Häufig geänderte Daten
2. Finanzdaten, Parallelität ist absolut nicht zulässig
3. Daten, die mit anderen Anwendungen geteilt werden.
Hibernate-Cache der zweiten Ebene
Wie bereits erwähnt, bietet Hibernate einen Cache der zweiten Ebene. Die erste Ebene ist der Sitzungscache. Da der Lebenszyklus des Sitzungsobjekts normalerweise einer Datenbanktransaktion oder einer Anwendungstransaktion entspricht, ist sein Cache ein Cache im Transaktionsbereich. Caching der ersten Ebene ist erforderlich, nicht zulässig und kann auch nicht entfernt werden. Im Cache der ersten Ebene verfügt jede Instanz einer persistenten Klasse über eine eindeutige OID.
Der Second-Level-Cache ist ein steckbares Cache-Plug-in, das von SessionFactory verwaltet wird. Da der Lebenszyklus des SessionFactory-Objekts dem gesamten Prozess der Anwendung entspricht, handelt es sich beim Second-Level-Cache um einen prozessweiten oder Cluster-weiten Cache. Lose Daten von Objekten, die in diesem Cache gespeichert sind. Bei Objekten der zweiten Ebene besteht das Potenzial für Parallelitätsprobleme und sie erfordern eine geeignete Strategie für den gleichzeitigen Zugriff, die eine Transaktionsisolationsstufe für die zwischengespeicherten Daten bereitstellt. Der Cache-Adapter dient zur Integration spezifischer Cache-Implementierungssoftware in Hibernate. Das Caching der zweiten Ebene ist optional und kann mit einer Granularität pro Klasse oder pro Sammlung konfiguriert werden.
Der allgemeine Prozess der Second-Level-Cache-Strategie von Hibernate ist wie folgt:
1) Geben Sie beim Abfragen von Bedingungen immer eine SQL-Anweisung wie „select * from table_name where ...“ (alle Felder auswählen) aus, um die abzufragen Datenbank und erhalten Sie alle Datenobjekte auf einmal.
 2) Legen Sie alle erhaltenen Datenobjekte entsprechend ihrer ID in den Cache der zweiten Ebene.
 3) Wenn Hibernate auf der Grundlage der ID auf das Datenobjekt zugreift, durchsucht es es zunächst im Cache der ersten Ebene der Sitzung, wenn es nicht gefunden werden kann. Wenn der Cache der zweiten Ebene konfiguriert ist, überprüft es es dann im Cache der zweiten Ebene -Level-Cache; wenn es nicht gefunden werden kann, fragt es die Datenbank erneut ab und legt das Ergebnis entsprechend der ID im Cache ab.
 4) Beim Löschen, Aktualisieren oder Hinzufügen von Daten wird gleichzeitig der Cache aktualisiert.
Die Caching-Strategie der zweiten Ebene von Hibernate ist eine Caching-Strategie für ID-Abfragen und hat keine Auswirkungen auf bedingte Abfragen. Zu diesem Zweck bietet Hibernate Abfrage-Caching für bedingte Abfragen.
Der Prozess der Abfrage-Caching-Strategie von Hibernate ist wie folgt:
1) Hibernate bildet zunächst einen Abfrageschlüssel basierend auf diesen Informationen. Der Abfrageschlüssel enthält die allgemeinen Informationen, die von der bedingten Abfrage angefordert werden: SQL, von SQL benötigte Parameter, Datensatzbereich (Startposition), rowStart, maximale Anzahl von Datensätzen (maxRows) usw.
 2) Hibernate durchsucht den Abfragecache nach der entsprechenden Ergebnisliste basierend auf diesem Abfrageschlüssel. Wenn sie vorhanden ist, geben Sie die Ergebnisliste zurück. Wenn sie nicht vorhanden ist, fragen Sie die Datenbank ab, rufen Sie die Ergebnisliste ab und legen Sie die gesamte Ergebnisliste gemäß dem Abfrageschlüssel im Abfragecache ab.
3) Der SQL-Abfrageschlüssel umfasst einige Tabellennamen. Wenn Daten in diesen Tabellen geändert, gelöscht, hinzugefügt usw. werden, werden diese zugehörigen Abfrageschlüssel aus dem Cache gelöscht.

Lazy Loading-Mechanismus im Ruhezustand
Lazy Loading:

Der Lazy Loading-Mechanismus wird vorgeschlagen, um unnötigen Leistungsaufwand zu vermeiden. Das sogenannte Lazy Loading bedeutet, dass die Daten wirklich benötigt werden. Erst dann wird der Datenladevorgang tatsächlich durchgeführt. Hibernate ermöglicht das verzögerte Laden von Entitätsobjekten und das verzögerte Laden von Sammlungen. Darüber hinaus bietet Hibernate3 auch das verzögerte Laden von Eigenschaften. Im Folgenden stellen wir die Details dieser Arten des verzögerten Ladens vor.

A. Lazy Loading von Entitätsobjekten:

Wenn Sie Lazy Loading für Entitätsobjekte verwenden möchten, müssen Sie die entsprechende Konfiguration in der Zuordnungskonfigurationsdatei der Entität vornehmen, wie unten gezeigt:

<hibernate-mapping>
 
<class name=”com.neusoft.entity.User” table=”user” lazy=”true”>
 
  ……
 
</class>
 
</hibernate-mapping>
Nach dem Login kopieren

Aktivieren Sie die Lazy-Loading-Funktion der Entität, indem Sie das Lazy-Attribut der Klasse auf true setzen. Wenn wir den folgenden Code ausführen:

User user=(User)session.load(User.class,”1”);
Nach dem Login kopieren

(1)

System.out.println(user.getName());
Nach dem Login kopieren

(2)

当运行到(1)处时,Hibernate并没有发起对数据的查询,如果我们此时通过一些调试工具(比如JBuilder2005的Debug工具),观察此时user对象的内存快照,我们会惊奇的发现,此时返回的可能是User$EnhancerByCGLIB$$bede8986类型的对象,而且其属性为null,这是怎么回事?还记得前面我曾讲过session.load()方法,会返回实体对象的代理类对象,这里所返回的对象类型就是User对象的代理类对象。在Hibernate中通过使用CGLIB,来实现动态构造一个目标对象的代理类对象,并且在代理类对象中包含目标对象的所有属性和方法,而且所有属性均被赋值为null。通过调试器显示的内存快照,我们可以看出此时真正的User对象,是包含在代理对象的CGLIB$CALBACK_0.target属性中,当代码运行到(2)处时,此时调用user.getName()方法,这时通过CGLIB赋予的回调机制,实际上调用CGLIB$CALBACK_0.getName()方法,当调用该方法时,Hibernate会首先检查CGLIB$CALBACK_0.target属性是否为null,如果不为空,则调用目标对象的getName方法,如果为空,则会发起数据库查询,生成类似这样的SQL语句:select * from user where id='1';来查询数据,并构造目标对象,并且将它赋值到CGLIB$CALBACK_0.target属性中。

这样,通过一个中间代理对象,Hibernate实现了实体的延迟加载,只有当用户真正发起获得实体对象属性的动作时,才真正会发起数据库查询操作。所以实体的延迟加载是用通过中间代理类完成的,所以只有session.load()方法才会利用实体延迟加载,因为只有session.load()方法才会返回实体类的代理类对象。

B、 集合类型的延迟加载:

在Hibernate的延迟加载机制中,针对集合类型的应用,意义是最为重大的,因为这有可能使性能得到大幅度的提高,为此Hibernate进行了大量的努力,其中包括对JDK Collection的独立实现,我们在一对多关联中,定义的用来容纳关联对象的Set集合,并不是java.util.Set类型或其子类型,而是net.sf.hibernate.collection.Set类型,通过使用自定义集合类的实现,Hibernate实现了集合类型的延迟加载。为了对集合类型使用延迟加载,我们必须如下配置我们的实体类的关于关联的部分:

<hibernate-mapping>
 
  <class name=”com.neusoft.entity.User” table=”user”>
 
…..
 
<set name=”addresses” table=”address” lazy=”true” inverse=”true”>
 
<key column=”user_id”/>
 
<one-to-many class=”com.neusoft.entity.Arrderss”/>
 
</set>
 
  </class>
 
</hibernate-mapping>
Nach dem Login kopieren

通过将元素的lazy属性设置为true来开启集合类型的延迟加载特性。我们看下面的代码:

User user=(User)session.load(User.class,”1”);
 
Collection addset=user.getAddresses();
Nach dem Login kopieren

(1)

Iterator it=addset.iterator();
Nach dem Login kopieren

(2)

while(it.hasNext()){
 
Address address=(Address)it.next();
 
System.out.println(address.getAddress());
 
}
Nach dem Login kopieren

当程序执行到(1)处时,这时并不会发起对关联数据的查询来加载关联数据,只有运行到(2)处时,真正的数据读取操作才会开始,这时Hibernate会根据缓存中符合条件的数据索引,来查找符合条件的实体对象。

这里我们引入了一个全新的概念——数据索引,下面我们首先将接一下什么是数据索引。在Hibernate中对集合类型进行缓存时,是分两部分进行缓存的,首先缓存集合中所有实体的id列表,然后缓存实体对象,这些实体对象的id列表,就是所谓的数据索引。当查找数据索引时,如果没有找到对应的数据索引,这时就会一条select SQL的执行,获得符合条件的数据,并构造实体对象集合和数据索引,然后返回实体对象的集合,并且将实体对象和数据索引纳入Hibernate的缓存之中。另一方面,如果找到对应的数据索引,则从数据索引中取出id列表,然后根据id在缓存中查找对应的实体,如果找到就从缓存中返回,如果没有找到,在发起select SQL查询。在这里我们看出了另外一个问题,这个问题可能会对性能产生影响,这就是集合类型的缓存策略。如果我们如下配置集合类型:

<hibernate-mapping>
 
  <class name=”com.neusoft.entity.User” table=”user”>
 
…..
 
<set name=”addresses” table=”address” lazy=”true” inverse=”true”>
 
<cache usage=”read-only”/>
 
<key column=”user_id”/>
 
<one-to-many class=”com.neusoft.entity.Arrderss”/>
 
</set>
 
  </class>
 
</hibernate-mapping>
Nach dem Login kopieren

这里我们应用了配置,如果采用这种策略来配置集合类型,Hibernate将只会对数据索引进行缓存,而不会对集合中的实体对象进行缓存。如上配置我们运行下面的代码:

User user=(User)session.load(User.class,”1”);
 
Collection addset=user.getAddresses();  
 
Iterator it=addset.iterator();       
 
while(it.hasNext()){
 
Address address=(Address)it.next();
 
System.out.println(address.getAddress());
 
}
 
System.out.println(“Second query……”);
 
User user2=(User)session.load(User.class,”1”);
 
Collection it2=user2.getAddresses();
 
while(it2.hasNext()){
 
Address address2=(Address)it2.next();
 
System.out.println(address2.getAddress());
 
}
Nach dem Login kopieren

运行这段代码,会得到类似下面的输出:

Select * from user where id=&#39;1&#39;;
 
Select * from address where user_id=&#39;1&#39;;
 
Tianjin
 
Dalian
 
Second query……
 
Select * from address where id=&#39;1&#39;;
 
Select * from address where id=&#39;2&#39;;
 
Tianjin
 
Dalian
Nach dem Login kopieren

我们看到,当第二次执行查询时,执行了两条对address表的查询操作,为什么会这样?这是因为当第一次加载实体后,根据集合类型缓存策略的配置,只对集合数据索引进行了缓存,而并没有对集合中的实体对象进行缓存,所以在第二次再次加载实体时,Hibernate找到了对应实体的数据索引,但是根据数据索引,却无法在缓存中找到对应的实体,所以Hibernate根据找到的数据索引发起了两条select SQL的查询操作,这里造成了对性能的浪费,怎样才能避免这种情况呢?我们必须对集合类型中的实体也指定缓存策略,所以我们要如下对集合类型进行配置:

<hibernate-mapping>
 
  <class name=”com.neusoft.entity.User” table=”user”>
 
…..
 
<set name=”addresses” table=”address” lazy=”true” inverse=”true”>
 
<cache usage=”read-write”/>
 
<key column=”user_id”/>
 
<one-to-many class=”com.neusoft.entity.Arrderss”/>
 
</set>
 
  </class>
 
</hibernate-mapping>
Nach dem Login kopieren

此时Hibernate会对集合类型中的实体也进行缓存,如果根据这个配置再次运行上面的代码,将会得到类似如下的输出:

Select * from user where id=&#39;1&#39;;
 
Select * from address where user_id=&#39;1&#39;;
 
Tianjin
 
Dalian
 
Second query……
 
Tianjin
 
Dalian
Nach dem Login kopieren

这时将不会再有根据数据索引进行查询的SQL语句,因为此时可以直接从缓存中获得集合类型中存放的实体对象。

C、 属性延迟加载:

在Hibernate3中,引入了一种新的特性——属性的延迟加载,这个机制又为获取高性能查询提供了有力的工具。在前面我们讲大数据对象读取时,在User对象中有一个resume字段,该字段是一个java.sql.Clob类型,包含了用户的简历信息,当我们加载该对象时,我们不得不每一次都要加载这个字段,而不论我们是否真的需要它,而且这种大数据对象的读取本身会带来很大的性能开销。在Hibernate2中,我们只有通过我们前面讲过的面性能的粒度细分,来分解User类,来解决这个问题(请参照那一节的论述),但是在Hibernate3中,我们可以通过属性延迟加载机制,来使我们获得只有当我们真正需要操作这个字段时,才去读取这个字段数据的能力,为此我们必须如下配置我们的实体类:

<hibernate-mapping>
 
<class name=”com.neusoft.entity.User” table=”user”>
 
……
 
<property name=”resume” type=”java.sql.Clob” column=”resume” lazy=”true”/>
 
  </class>
 
</hibernate-mapping>
Nach dem Login kopieren

通过对元素的lazy属性设置true来开启属性的延迟加载,在Hibernate3中为了实现属性的延迟加载,使用了类增强器来对实体类的Class文件进行强化处理,通过增强器的增强,将CGLIB的回调机制逻辑,加入实体类,这里我们可以看出属性的延迟加载,还是通过CGLIB来实现的。CGLIB是Apache的一个开源工程,这个类库可以操纵java类的字节码,根据字节码来动态构造符合要求的类对象。根据上面的配置我们运行下面的代码:

String sql=”from User user where user.name=&#39;zx&#39; ”;
 
Query query=session.createQuery(sql);
Nach dem Login kopieren

(1)

List list=query.list();
 
for(int i=0;i<list.size();i++){
 
User user=(User)list.get(i);
 
System.out.println(user.getName());
 
System.out.println(user.getResume()); }
Nach dem Login kopieren

(2)

当执行到(1)处时,会生成类似如下的SQL语句:

Select id,age,name from user where name=&#39;zx&#39;;
Nach dem Login kopieren

这时Hibernate会检索User实体中所有非延迟加载属性对应的字段数据,当执行到(2)处时,会生成类似如下的SQL语句:

Select resume from user where id=&#39;1&#39;;
Nach dem Login kopieren

这时会发起对resume字段数据真正的读取操作。

更多浅析Java的Hibernate框架中的缓存和延迟加载机制相关文章请关注PHP中文网!

Verwandte Etiketten:
Quelle:php.cn
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage