Cache springboot digunakan dalam pekerjaan, yang cukup nyaman digunakan. Ini secara langsung memperkenalkan paket ketergantungan cache seperti Redis atau Ehcache dan paket ketergantungan starter dari cache terkait, dan kemudian menambahkan anotasi @EnableCaching ke kelas startup, dan kemudian Anda dapat menggunakan @cacheable dan @cacheevict untuk menggunakan dan menghapus cache di mana diperlukan. Ini sangat mudah digunakan. Saya percaya bahwa mereka yang telah menggunakan cache springboot akan bermain, jadi saya tidak akan mengatakan lebih banyak di sini. Satu-satunya kelemahan adalah bahwa Springboot menggunakan integrasi plug-in. Meskipun sangat nyaman untuk digunakan, ketika Anda mengintegrasikan EHCACHE, Anda menggunakan EHCACHE, dan ketika Anda mengintegrasikan Redis, Anda menggunakan Redis. Jika Anda ingin menggunakan keduanya bersama -sama, Ehcache digunakan sebagai cache Level 1 lokal dan REDIS digunakan sebagai cache Level 2 yang terintegrasi. Sejauh yang saya tahu, tidak mungkin untuk mencapai metode default (jika ada ahli yang dapat mengimplementasikannya, tolong beri saya beberapa saran). Bagaimanapun, banyak layanan membutuhkan penyebaran multi-poin. Jika Anda memilih Ehcache saja, Anda dapat mewujudkan cache lokal dengan baik. Namun, jika Anda berbagi cache antara beberapa mesin, perlu waktu untuk membuat masalah. Jika Anda memilih cache Redis terpusat, karena Anda harus pergi ke jaringan setiap kali Anda mendapatkan data, Anda akan selalu merasa bahwa kinerjanya tidak akan terlalu bagus. Topik ini terutama membahas bagaimana mengintegrasikan EHCACHE dan Redis sebagai cache tingkat pertama dan kedua berdasarkan springboot, dan menyadari sinkronisasi cache.
Agar tidak menyerbu metode cache asli springboot, saya telah menentukan dua anotasi terkait cache di sini, sebagai berikut
@Target ({elementType.method}) @retention (retentionPolicy.runtime) public @interface Cacheable {string value () default ""; String key () default ""; // kelas tipe kelas generik <?> Type () default exception.class; } @Target ({elementType.method}) @retention (retentionpolicy.runtime) public @interface Cacheevict {string value () default ""; String key () default ""; }Karena dua anotasi di atas pada dasarnya sama dengan anotasi yang di -cache di musim semi, tetapi beberapa atribut yang jarang digunakan dihilangkan. Berbicara tentang hal ini, saya bertanya -tanya apakah ada teman yang memperhatikan bahwa ketika Anda menggunakan cache Redis sendirian di Springboot, nilai atribut yang dianotasi oleh cacheable dan cacheevict sebenarnya menjadi kunci nilai zset di redis, dan zset masih kosong, seperti @cacheable (value = "cache1", key = "Key1"). Dalam keadaan normal, cache1 -> peta (key1, value1) harus muncul di redis, di mana cache1 digunakan sebagai nama cache, peta sebagai nilai cache, dan kunci sebagai kunci dalam peta, yang secara efektif dapat mengisolasi cache dengan nama cache yang berbeda. Namun pada kenyataannya, ada cache1 -> kosong (zset) dan key1 -> value1, dua pasangan nilai kunci independen. Eksperimen menemukan bahwa cache dengan nama cache yang berbeda sepenuhnya dibagikan. Jika Anda tertarik, Anda dapat mencobanya. Dengan kata lain, atribut nilai ini sebenarnya adalah dekorasi, dan keunikan kunci hanya dijamin oleh atribut kunci. Saya hanya bisa berpikir bahwa ini adalah bug dalam implementasi cache musim semi, atau dirancang khusus (jika Anda tahu alasannya, tolong beri saya beberapa saran).
Kembali ke topik, dengan anotasi, ada juga kelas pemrosesan anotasi. Di sini saya menggunakan bagian AOP untuk intersepsi, dan implementasi asli sebenarnya serupa. Kelas pemrosesan bagian adalah sebagai berikut:
impor com.xuanwu.apaas.core.multicache.annotation.cacheevict; impor com.xuanwu.apaas.core.multicache.annotation.caceable; impor com.xuanwu.apaas.core.utils.jsonutil; impor org.apache.commons.lang3.stringutils; impor org.aspectj.lang.proedingjoinpoint; impor org.aspectj.lang.annotation.around; impor org.aspectj.lang.annotation.around; impor org.aspectj.lang.annotation.aspect; impor org.aspectj.lang.annotation.pointcut; impor org.aspectj.lang.reflect.methodsignature; impor org.json.jsonarray; impor org.json.jsonobject; impor org.slf4j.logger; impor org.slf4j.loggerFactory; impor org.springframework.beans.factory.annotation.Autowired; impor org.springframework.core.localvariabletableParameterNamedIscoverer; impor org.springframework.expression.expressionParser; impor org.springframework.expression.spel.standard.spelexpressionParser; impor org.springframework.expression.spel.support.standardevaluationContext; impor org.springframework.stereotype.component; impor java.lang.reflect.method; / *** Bagian cache multilevel* @author rongdi*/ @aspect @component kelas publik multicacheaspect {private static final Logger Logger = loggerFactory.getLogger (multicAcheaspect.class); @Autowired Private Cachefactory Cachefactory; // Di sini pendengar diinisialisasi melalui wadah, dan sakelar cache dikendalikan sesuai dengan anotasi @enablecaching yang dikonfigurasi secara eksternal. @Pointcut ("@annotation (com.xuanwu.apaas.core.multicache.annotation.cacheable)") public void cacheaspect () {} @pointcut ("@annotation (com.xuanwu.apaas.core.multicache.annotation.cacheevict)") @Around ("CacheableAspect ()") Cache Objek Publik (ProsidingJoINPoint gabungan) {// Dapatkan daftar parameter metode yang dimodifikasi oleh objek facet [] args = goinpoint.getArgs (); // Hasil adalah hasil pengembalian akhir dari hasil objek metode = null; // Jika cache tidak diaktifkan, langsung hubungi metode pemrosesan untuk mengembalikan jika (! Cacheenable) {coba {hasil = joinpoint.proed (args); } catch (Throwable e) {logger.error ("", e); } hasil pengembalian; } // Dapatkan tipe nilai pengembalian dari kelas proxy class returnType = ((MethodyIchignature) joinpoint.getSignature ()). GetReturnType (); // Dapatkan metode metode proxy Method = ((Methodyignature) joinpoint.getSignature ()). GetMethod (); // Dapatkan komentar pada metode proxy yang dapat di -cache ca = method.getAnnotation (Cacheable.class); // Dapatkan nilai kunci yang diuraikan oleh EL String Key = parsey (ca.key (), metode, args); Kelas <?> elementclass = ca.type (); // Dapatkan nama cache dari nama string anotasi = ca.value (); coba {// pertama dapatkan data dari string ehcache cachevalue = cachefactory.ehget (name, key); if (stringutils.isempty (cachevalue)) {// Jika tidak ada data di ehcache, dapatkan data dari redis cachevalue = cachefactory.redisget (name, key); if (stringutils.isempty (cachevalue)) {// Jika tidak ada data di ehcache, dapatkan data dari redis cachevalue = cachefactory.redisget (name, key); if (stringutils.isempty (cachevalue)) {// Jika tidak ada data di redis // hubungi metode bisnis untuk mendapatkan hasil hasil = goinpoint.proed (args); // serialize hasilnya dan letakkan di redis cacheefactory.redisput (nama, kunci, serialize (hasil)); } else {// Jika data dapat diperoleh dari redis // deserialize data yang diperoleh dalam cache dan return if (elementclass == exception.class) {result = deserialize (cachevalue, returnType); } else {result = deserialize (cachevalue, returnType, elementclass); }} // Serialisasi hasilnya dan masukkan ke dalam eHcache cacheefactory.ehput (nama, kunci, serialize (hasil)); } else {// deserialize data yang diperoleh dalam cache dan return if (elementclass == exception.class) {result = deserialize (cachevalue, returnType); } else {result = deserialize (cachevalue, returnType, elementclass); }}} catch (Throwable Throwable) {Logger.Error ("", Throwable); } hasil pengembalian; } / ** * Hapus cache sebelum metode dipanggil, dan kemudian panggil metode bisnis * @param goinpoint * @return * @throws Throwable * * / @around ("cacheevict ()") objek publik Evictcache (MethodingjointPoint Joinpoint) melempar {// dapatkan metode metode proxy Method = ((Methodygnature). // Dapatkan daftar parameter metode yang dimodifikasi oleh objek facet [] args = joinpoint.getArgs (); // Dapatkan anotasi pada metode proxy cacheevict ce = method.getAnnotation (cacheevict.class); // Dapatkan nilai kunci diurai oleh EL String Key = parsey (ce.key (), metode, args); // Dapatkan nama cache dari nama string anotasi = ce.value (); // Bersihkan cache cachefactory.cachedel (nama, kunci); return joinpoint.proed (args); } / ** * Dapatkan tombol cacheed * Kunci yang ditentukan pada anotasi, mendukung ekspresi spel * @return * / private string parsey (tombol string, metode metode, objek [] args) {if (stringutils.isempty (key)) return null; // Dapatkan daftar nama parameter dari metode yang dicegat (menggunakan Spring Support Class Library) localvariableTableParameterNamedIscoverer u = new LocalVariableTableParameterNamedIscoverer (); String [] paranamearr = u.getParameternames (metode); // Gunakan SPEL untuk Parser Parsing Parser Parser Key = New SpelExpressionParser (); // SPEL konteks StandardEvaluationContext Context = New StandardEvaluationContext (); // Masukkan parameter metode ke dalam konteks spel untuk (int i = 0; i <paranamearr.length; i ++) {context.setVariable (paranamearr [i], args [i]); } return parser.parseExpression (key) .getValue (context, string.class); } // serialize private string serialize (objek obj) {string result = null; coba {hasil = jsonutil.serialize (obj); } catch (exception e) {result = obj.toString (); } hasil pengembalian; } // Deserialize Private Object Deserialize (String str, class clazz) {Object result = null; coba {if (clazz == jsonobject.class) {result = new jsonObject (str); } lain if (clazz == jsonarray.class) {result = new jsonarray (str); } else {result = jsonutil.deserialize (str, clazz); }} catch (Exception e) {} hasil pengembalian; } // Deserialization, Daftar Dukungan <XXX> Deserialize Objek Pribadi (String str, class clazz, class elementclass) {Object result = null; coba {if (clazz == jsonobject.class) {result = new jsonObject (str); } lain if (clazz == jsonarray.class) {result = new jsonarray (str); } else {result = jsonutil.deserialize (str, clazz, elementclass); }} catch (Exception e) {} hasil pengembalian; } public void setCacheenable (Boolean Cacheenable) {this.cacheenable = cacheeNable; }}Antarmuka di atas menggunakan variabel cacheenable untuk mengontrol apakah akan menggunakan cache. Untuk mencapai akses mulus ke springboot, perlu dikendalikan oleh anotasi @enablecaching asli. Di sini saya menggunakan pendengar yang dimuat oleh wadah pegas, dan kemudian menemukan di pendengar apakah ada kelas yang dimodifikasi oleh anotasi @enablecaching. Jika demikian, dapatkan objek multicacheaspect dari wadah musim semi, dan kemudian setel dapat menjadi true. Ini akan memungkinkan akses mulus ke springboot. Saya ingin tahu apakah ada cara yang lebih elegan untuk teman? Selamat datang untuk berkomunikasi! Kelas pendengar adalah sebagai berikut
impor com.xuanwu.apaas.core.multicache.cachefactory; impor com.xuanwu.apaas.core.multicache.multicacheaspect; impor org.springframework.cache.annotation.enableCaching; impor org.springframework.context.applicationListener; impor org.springframework.context.event.contextrefreshedEvent; impor org.springframework.stereotype.component; impor java.util.map; / ** * Digunakan untuk menemukan apakah ada anotasi untuk mengaktifkan cache dalam proyek setelah pemuatan musim semi selesai @enablecaching * @author rongdi */ @component kelas publik contextrefreshedlistener mengimplementasikan applicationListener (contextrefreshedEvent> { @override void publik void onlication eveneer wadah untuk mencegah terjadinya dua panggilan (pemuatan MVC juga akan memicu satu kali) if (event.getApplicationContext (). getParent () == null) {// Dapatkan semua kelas dimodifikasi oleh @EnableCaching Annotation Map <String, Object> beans = event.getApplicationContext (). getBeanSwithannotation, objek> beans = event.getApplicationContext (). getBeanSwithannoTation = Object> beans = event.getApplicationContext (). getBeanSwithAnnotation = beans = event. if (beans! = null &&! beans.isempty ()) {multicacheaspect multicache = (multicAcheaspect) event.getApplicationContext (). getBean ("multicacheaspect"); multicache.setcacheenable (true); }}}}}Untuk mencapai akses yang mulus, kita juga perlu mempertimbangkan bagaimana EHCache multi-poin konsisten dengan cache Redis saat menggunakan EHCache multi-poin. Dalam aplikasi normal, REDIS umumnya cocok untuk cache terpusat jangka panjang, dan EHCACHE cocok untuk cache lokal jangka pendek. Asumsikan bahwa sekarang ada server A, B dan C, A dan B Deploy Layanan Bisnis, dan C Menyebarkan Layanan Redis. Ketika sebuah permintaan masuk, entri front-end, apakah itu memuat perangkat lunak seperti LVS atau NGINX, akan meneruskan permintaan ke server tertentu. Dengan asumsi diteruskan ke server A dan konten tertentu dimodifikasi, dan konten ini tersedia di Redis dan EHCACHE. Pada saat ini, cache ehcache dari server A dan redis server C lebih mudah untuk mengontrol apakah cache tidak valid atau dihapus. Tapi bagaimana cara mengontrol ehcache server B saat ini? Metode yang umum digunakan adalah menggunakan mode langganan publikasi. Ketika Anda perlu menghapus cache, Anda menerbitkan pesan di saluran tetap. Kemudian setiap server bisnis berlangganan saluran ini. Setelah menerima pesan tersebut, Anda menghapus atau kedaluwarsa cache EHCACHE lokal (yang terbaik adalah menggunakan kedaluwarsa, tetapi Redis saat ini hanya mendukung operasi yang kadaluwarsa pada kunci. Tidak ada cara untuk mengoperasikan kedaluwarsa anggota dalam peta di bawah kunci. Jika Anda harus memaksakan kedaluwarsa, Anda dapat menambah cap waktu untuk mengimplementasikannya sendiri. Namun, masalah Anda harus memaksa Delete, Anda dapat menambahkan semua hal. Lebih sedikit menulis. Singkatnya, prosesnya adalah memperbarui bagian data tertentu, hapus cache yang sesuai di Redis, dan kemudian menerbitkan pesan dengan cache yang tidak valid di saluran Redis tertentu. Layanan bisnis lokal berlangganan pesan saluran ini. Ketika layanan bisnis menerima pesan ini, ia menghapus cache EHCACHE lokal. Berbagai konfigurasi REDI adalah sebagai berikut.
impor com.fasterxml.jackson.annotation.jsonautodetect; impor com.fasterxml.jackson.annotation.propertyaccessor; impor com.fasterxml.jackson.databind.objectmapper; impor com.xuanwu.apaas.core.multicache.subscriber.messageSubscriber; impor org.springframework.cache.cachemanager; impor org.springframework.context.annotation.bean; impor org.springframework.context.annotation.configuration; impor org.springframework.data.redis.cache.reditcachemanager; impor org.springframework.data.redis.connection.redisconnectionFactory; impor org.springframework.data.redis.core.redistemplate; impor org.springframework.data.redis.core.stringredistemplate; impor org.springframework.data.redis.listener.patterntopic; impor org.springframework.data.redis.listener.redismessagelistenercontainer; impor org.springframework.data.redis.listener.adapter.messagelistenerAdapter; impor org.springframework.data.redis.serializer.jackson2jsonredisserializer; @Configuration Public Class Redisconfig {@Bean CacheManager Cachemanager (redistemplate redistemplate) {RediscacheManager rcm = new ReciscacheManager (redistemplate); // Setel Cache Expiration Time (detik) RCM.SetDefaultExpiration (600); mengembalikan RCM; } @Bean redistemplate publik <String, String> redistemplate (RedisconnectionFactory factory) {stringDistemplate template = new StrreDistemplate (factory); Jackson2jsonredisserializer jackson2jsonredisserializer = baru jackson2jsonredisserializer (objek.class); ObjectMapper OM = ObjectMapper baru (); om.setvisibility (properticacessor.all, jsonautodetect.visibility.any); om.enableDefaultTyping (objectMapper.defaultTyping.non_final); jackson2jsonredisserializer.setObjectMapper (OM); template.setValueserializer (jackson2jsonredisserializer); template.AfterPropertiesset (); Template Kembalikan; } /*** Redis pesan pendengar wadah* Anda dapat menambahkan beberapa pendengar Redis yang mendengarkan berbagai topik. You only need to bind the message listener and the corresponding message subscription processor, and the message listener* Calling related methods of message subscription processor through reflection technology for some business processing * @param connectionFactory * @param listenerAdapter * @return */ @Bean public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory, MessageListenerAdapter listenerAdapter) { Redismessagelistenercontainer container = redismessagelistenercontainer baru (); container.setConnectionFactory (ConnectionFactory); // Berlangganan ke saluran container.addmessageListener (listenerAdapter, new Patterntopic ("redis.uncheache")); // wadah ini dapat menambahkan beberapa wadah pengembalian MessageListener; } /** * Message listener adapter, binds the message processor, and uses reflection technology to call the message processor's business methods* @param receiver * @return */ @Bean MessageListenerAdapter listenerAdapter(MessageSubscriber receiver) { //This place is to pass a messageListenerAdapter to the messageListenerAdapter, and use the reflection method to call "handle" return new MessageListenerAdapter (penerima, "pegangan"); }}Kelas penerbitan pesan adalah sebagai berikut:
impor com.xuanwu.apaas.core.multicache.cachefactory; impor org.apache.commons.lang3.stringutils; impor org.slf4j.logger; impor org.slf4j.loggerFactory; impor org.springframework.beans.factory.annotation.Autowired; impor org.springframework.stereotype.component; @Component Public Class MessageSubscriber {private static final Logger Logger = LoggerFactory.getLogger (Messesubscriber.class); @Autowired Private Cachefactory Cachefactory; / *** Setelah menerima pesan langganan Redis, cache ehcache tidak valid* format pesan @param adalah name_key*/ public void handle (pesan string) {logger.debug ("redis.ehcache:"+pesan); if (stringutils.isempty (pesan)) {return; } String [] strs = message.split ("#"); Name string = strs [0]; Tombol string = null; if (strs.length == 2) {key = strs [1]; } cachefactory.ehdel (nama, kunci); }}Kelas cache operasi spesifik adalah sebagai berikut:
impor com.xuanwu.apaas.core.multicache.publisher.messagePublisher; impor net.sf.ehcache.cache; impor net.sf.ehcache.cachemanager; impor net.sf.ehcache.element; impor org.apache.commons.lang3.stringutils; impor org.slf4j.logger; impor org.slf4j.loggerFactory; impor org.springframework.beans.factory.annotation.Autowired; impor org.springframework.data.redis.redisconnectionfailureException; impor org.springframework.data.redis.core.hashoperations; impor org.springframework.data.redis.core.redistemplate; impor org.springframework.stereotype.component; impor java.io.inputstream; / *** Bagian cache multi-level* @author rongdi*/ @component kelas publik Cachefactory {private static final Logger Logger = loggerFactory.getLogger (cachefactory.class); @Autowired private redistemplate redistemplate; @Autowired Private MessagePublisher MessagePublisher; Private Cachemanager Cachemanager; cachefactory public () {inputStream is = this.getClass (). getResourceAsstream ("/ehcache.xml"); if (is! = null) {cacheManager = cacheManager.create (is); }} public void cacachedel (nama string, tombol string) {// hapus cache yang sesuai dengan redis; // hapus cache ehcache lokal, yang tidak diperlukan, dan pelanggan akan menghapus // ehdel (nama, kunci); if (cacheManager! = null) {// Publikasikan pesan yang memberi tahu layanan berlangganan bahwa cache tidak valid messagePublisher.publish (name, key); }} public String ehget (nama string, tombol string) {if (cacheManager == null) return null; Cache cache = cacheManager.getCache (name); if (cache == null) return null; cache.acquirereadlockonkey (kunci); coba {elemen ele = cache.get (key); if (ele == null) return null; return (string) ele.getObjectValue (); } akhirnya {cache.releasereadlockonkey (key); }} public String redisget (nama string, tombol string) {hashoperations <string, string, string> opera = redistemplate.opsforhash (); coba {return opera.get (name, key); } catch (RedisconnectionFailureException e) {// Koneksi gagal, tidak ada kesalahan yang dilemparkan, dan logger.error ("Connect Redis Error", e); kembali nol; }} public void ehput (nama string, tombol string, nilai string) {if (cacheManager == null) return; if (! cachemanager.cacheexists (name)) {cachemanager.addcache (name); } Cache cache = cacheManager.getCache (name); // Dapatkan kunci tulis pada kunci, tombol yang berbeda tidak saling mempengaruhi, mirip dengan disinkronkan (key.intern ()) {} cache.acquireWritelockonkey (key); coba {cache.put (elemen baru (tombol, nilai)); } akhirnya {// lepaskan cache lock write.releasewritelockonkey (key); }} public void redisput (nama string, tombol string, nilai string) {hashoperations <string, string, string> opera = redistemplate.opsforhash (); coba {operator.put (name, key, value); } catch (RedisconnectionFailureException e) {// Koneksi gagal, tidak ada kesalahan yang dilemparkan, dan logger.error ("Connect Redis Error", e); }} public void ehdel (nama string, tombol string) {if (cacheManager == null) return; if (cachemanager.cacheexists (name)) {// Jika kunci kosong, hapus langsung sesuai dengan nama cache if (stringutils.isempty (key)) {cacheManager.removecache (name); } else {cache cache = cacheManager.getCache (name); cache.remove (kunci); }}} public void redisdel (nama string, tombol string) {hashoperations <string, string, string> opera = redistemplate.opsforhash (); coba {// jika kunci kosong, hapus if (stringutils.isempty (key)) {redistemplate.delete (name); } else {opera.delete (name, key); }} catch (RedisconnectionFailureException e) {// Koneksi gagal, tidak ada kesalahan yang dilemparkan, dan Logger.error ("Connect Redis Error", E); }}}Kelas Alat adalah sebagai berikut
impor com.fasterxml.jackson.core.type.typereference; impor com.fasterxml.jackson.databind.deserializationFeature; impor com.fasterxml.jackson.databind.javatype; impor com.fasterxml.jackson.databind.objectmapper; impor org.apache.commons.lang3.stringutils; impor org.json.jsonarray; impor org.json.jsonobject; impor java.util.*; kelas publik jsonutil {private static ObjectMapper mapper; static {mapper = new ObjectMapper (); mapper.configure (deserializationfeature.fail_on_unknown_properties, false); } / ** * Serialize objek menjadi json * * @param obj objek untuk diserialisasi * @return * @throws Exception * / public static string serialize (objek obj) melempar pengecualian {if (obj == null) {lempar baru ilegalArgumentException ("obj seharusnya tidak menjadi nol"); } return mapper.writevalueAsstring (OBJ); } / ** Deserialisasi dengan obat generik, seperti deserialisasi jsonarray ke dalam daftar <user>* / public static <t> t deserialize (string jsonstr, class <?> CollectionClass, class <?> ... element classclasses) ExcleClass () clollyclass (javatype javatype = mappper.gettypeFactory (). return mapper.readvalue (jsonstr, javatype); } / *** Deserialize string JSON ke objek* @param src string json untuk deserialized* @param t jenis kelas dari objek yang deserialized ke* @return* @throws Exception* / public static <t> t deserialize (string src, class <t> lemparan lemparan {if (src (string src, class <t> t) lemparan {IF (src = null {null {null {null {null {null {null {null in (string (string) IllegalargumentException ("SRC tidak boleh nol"); } if ("{}". Equals (src.trim ())) {return null; } return mappper.readValue (src, t); }}Untuk menggunakan cache secara khusus, cukup perhatikan anotasi @cacheable dan @cacheevict, dan juga mendukung Spring El Expressions. Selain itu, nama cache yang diwakili oleh atribut nilai di sini tidak memiliki masalah yang disebutkan di atas. Cache yang berbeda dapat diisolasi menggunakan nilai. Contohnya adalah sebagai berikut
@Cacheable (value = "bo", key = "#session.productVersionCode+''+#session.tenantCode+''+#objectCode")@cacheevict (value = "bo", key = "#session.productVersionCode+''+#Sesi.
Melampirkan paket ketergantungan utama
Di atas adalah semua konten artikel ini. Saya berharap ini akan membantu untuk pembelajaran semua orang dan saya harap semua orang akan lebih mendukung wulin.com.