Der Cache des Springboots wird in der Arbeit verwendet, was sehr bequem zu verwenden ist. Es führt direkt Cache -Abhängigkeitspakete wie Redis oder EHCACHE und die Starter -Abhängigkeitspakete mit verwandten Caches ein und fügt dann die Annotation von @enableCaching zur Startklasse hinzu. Anschließend können Sie @cacheable und @CacheeVict verwenden, um den Cache zu verwenden, wo erforderlich. Dies ist sehr einfach zu bedienen. Ich glaube, dass diejenigen, die Springboot -Cache verwendet haben, spielen werden, also werde ich hier nicht mehr sagen. Der einzige Nachteil ist, dass Springboot Plug-in-Integration verwendet. Obwohl es sehr bequem zu verwenden ist, verwenden Sie EHCache, wenn Sie EHCache integrieren, und wenn Sie Redis integrieren, verwenden Sie Redis. Wenn Sie beide zusammen verwenden möchten, wird EHCache als lokaler Cache 1 Level 1 verwendet und Redis wird als integrierter Cache Level 2 verwendet. Soweit ich weiß, ist es unmöglich, die Standardmethode zu erreichen (wenn es einen Experten gibt, der sie implementieren kann, geben Sie mir bitte einige Ratschläge). Immerhin erfordern viele Dienste eine Mehrpunktbereitstellung. Wenn Sie EHCache allein wählen, können Sie den lokalen Cache gut realisieren. Wenn Sie jedoch Cache zwischen mehreren Maschinen teilen, dauert es Zeit, um Probleme zu haben. Wenn Sie einen zentralen Redis -Cache wählen, da Sie jedes Mal, wenn Sie Daten erhalten, zum Netzwerk gehen müssen, werden Sie immer das Gefühl haben, dass die Leistung nicht zu gut ist. In diesem Thema wird hauptsächlich erläutert, wie EHCache und Redis nahtlos als Caches der ersten und zweiten Stufe basierend auf Springboot integriert und die Cache-Synchronisation realisiert werden.
Um nicht in die ursprüngliche Cache-Methode von Springboot einzudringen, habe ich hier zwei Cache-bezogene Annotationen definiert, wie folgt
@Target ({Elementtype.method}) @Retention (retentionPolicy.runtime) public @Interface cacheable {String value () Standard ""; String key () Standard ""; // Generic Class Type Class <?> Type () Standardausnahme.Class; } @Target ({Elementtype.method}) @Retention (retentionPolicy.runtime) public @Interface CacheeVict {String value () Standard ""; String key () Standard ""; }Da die oben genannten zwei Anmerkungen im Grunde genommen mit den zwischengespeicherten Annotationen im Frühjahr entsprechen, werden jedoch einige selten verwendete Attribute entfernt. Apropos, ich frage mich, ob Freunde bemerkt haben, dass die von zwischengespeicherbaren und Cacheevict -Annotierungen und CacheeeVict -Annotatoren tatsächlich zu einem Zet -Wertschlüssel in Redis und der ZSET noch leer sind, wie @cacheable (Value = "Cache1", Key = "Key1"). Unter normalen Umständen sollte Cache1 -> MAP (KEY1, Value1) in Redis angezeigt werden, wobei Cache1 als Cache -Name, MAP als Cache -Wert und Schlüssel als Schlüssel in der Karte verwendet wird, die Caches unter verschiedenen Cache -Namen effektiv isolieren kann. Tatsächlich gibt es jedoch Cache1 -> leer (zset) und key1 -> value1, zwei unabhängige Schlüsselwertpaare. Das Experiment ergab, dass der Cache unter verschiedenen Cache -Namen vollständig geteilt ist. Wenn Sie interessiert sind, können Sie es versuchen. Das heißt, dieses Wertattribut ist tatsächlich eine Dekoration, und die Einzigartigkeit des Schlüssels wird nur durch das Schlüsselattribut garantiert. Ich kann nur denken, dass dies ein Fehler in der Cache -Implementierung von Frühling ist, oder es wurde speziell entwickelt (wenn Sie den Grund kennen, geben Sie mir bitte einige Ratschläge).
Zurück zum Thema mit Annotation gibt es auch Annotationsverarbeitungsklassen. Hier verwende ich den Abschnitt von AOP für Abfangen, und die native Implementierung ist tatsächlich ähnlich. Die Abschnittsverarbeitungsklasse lautet wie folgt:
import com.xuanwu.apaas.core.multicache.annotation.cacheevict; import com.xuanwu.apaas.core.multicache.annotation.cacheable; import com.xuanwu.apaas.core.utils.jsonutil; import org.apache.commons.lang3.stringutils; import org.aspespectj.lang.proceedingjoinpoint; import org.aspespectj.lang.annotation.around; import org.aspespectj.lang.annotation.around; import org.aspespectj.lang.annotation.aspep; import org.aspespectj.lang.annotation.pointcut; import org.aspespectj.lang.reflect.methodsignature; import org.json.jsonArray; import org.json.jsonObject; import org.slf4j.logger; import org.slf4j.loggerfactory; import org.springframework.beans.factory.annotation.autowired; import org.springframework.core.localVariablePlePleParameTernamedSiscoverer; import org.springframework.expression.expressionParser; import org.springframework.expression.spel.standard.spelexpressionParser; import org.springframework.expression.spel.support.StandardeValuationContext; import org.springframework.stereotype.comPonent; import Java.lang.reflect.Method; / *** Multilevel -Cache -Abschnitt* @Author Rongdi*/ @aspect @Component Public Class MulticacheAsPect {private static Final Logger logger = loggerfactory.getLogger (MulticAracheaSect.class); @Autowired Private Cachefactory Cachefactory; // Hier wird der Hörer über einen Container initialisiert, und der Cache -Switch wird gemäß dem extern konfigurierten @EnableCaching -Annotation Private Boolean Rangeable gesteuert. @Pointcut("@annotation(com.xuanwu.apaas.core.multicache.annotation.Cacheable)") public void cacheAspect() { } @Pointcut("@annotation(com.xuanwu.apaas.core.multicache.annotation.CacheEvict)") public void cacheEvict() { } @Around ("CacheAbleAsPect ()") öffentlicher Objekt -Cache (ProceedingJoInpoint Joinpoint) {// Erhalten Sie die Parameterliste der Methode, die vom Facettenobjekt [] args = joinpoint.getargs () geändert wird; // Ergebnis ist das endgültige Rückgabeergebnis des Method -Objekt -Ergebniss = NULL; // Wenn der Cache nicht aktiviert ist, rufen Sie direkt die Verarbeitungsmethode auf, um zurückzugeben, wenn (! Reservable) {try {result = joinpoint.procePece (args); } catch (throwable e) {logger.Error ("", e); } Rückgabeergebnis; } // den Rückgabewerttyp der Proxy -Methodenklasse returnType = ((MethodeInt) joinpoint.getSignature ()). GetReturnType (); // Die Proxy -Methode -Methode abrufen Methode = ((MethodeInt) joinpoint.getSignature ()). GetMethod (); // den Kommentar zur Proxy -Methode cacheable ca = methode.getannotation (cacheable.class) erhalten; // Erhalten Sie den Schlüsselwert an el String key = parsekey (ca.key (), methode, args); Klasse <?> ElementClass = ca.type (); // den Cache -Namen aus dem Annotationsstring -Namen = ca.Value () abrufen; Versuchen Sie {// Erinnern Sie Daten von EHCache String cacheValue = cachefactory.ehget (Name, Schlüssel); if (stringutils.isempty (cachevalue)) {// Wenn es keine Daten in EHCache gibt, erhalten Sie Daten von Redis cachevalue = cachefactory.RedIsget (Name, Schlüssel); if (stringutils.isempty (cachevalue)) {// Wenn es keine Daten in EHCache gibt, erhalten Sie Daten von Redis cachevalue = cachefactory.RedIsget (Name, Schlüssel); if (stringutils.isempty (cachevalue)) {// Wenn keine Daten in Redis // die Geschäftsmethode aufrufen, um das Ergebnis zu erhalten, result = joinpoint.procece (args); // serialisieren Sie das Ergebnis und setzen Sie es in Redis Cachefactory.Resput (Name, Schlüssel, Serialize (Ergebnis)); } else {// Wenn Daten aus redis // die im Cache erhaltenen Daten erhalten werden können, und return if (elementclass == exception.class) {result = Deserialize (cachegressive, returnType); } else {result = Deserialize (cachevalue, returntype, elementClass); }} // serialisieren Sie das Ergebnis und setzen Sie es in ehcache cachefactory.ehput (Name, Schlüssel, Serialize (Ergebnis)); } else {// Deserialisieren Sie die im Cache erhaltenen Daten und return if if (elementclass == exception.class) {result = Deserialize (cachevalue, returnType); } else {result = Deserialize (cachevalue, returntype, elementClass); }}} catch (throwable throwable) {Logger.Error ("", Throwable); } Rückgabeergebnis; } / ** * Löschen Sie den Cache, bevor die Methode aufgerufen wird, und rufen Sie dann die Geschäftsmethode * @param JoinPoint * @return * @Throws Throwable * * / @around ("Cacheevict ()") Public Object Evictcache (ProceedingJoInpoint Joinpoint) Throwable {// die Proxy -Methode -Methode -Methode () -Methode (Methodsignature). // Erhalten Sie die Parameterliste der vom Facettenobjekt geänderten Methode [] args = joinpoint.getArgs (); // Die Annotation auf der Proxy -Methode Cacheevict ce = methode.getannotation (cacheevict.class) erhalten; // Erhalten Sie den Schlüsselwert an el String key = parsekey (ce.key (), Methode, args); // den Cache -Namen aus dem Annotationsstring -Namen = ce.Value () abrufen; // Löschen Sie den entsprechenden Cache -Cachefactory.cachedel (Name, Schlüssel); return joinpoint.procece (args); } / ** * den zwischengespeicherten Schlüssel * Taste erhalten * Taste, der in der Annotation definiert ist und Spel -Ausdrücke unterstützt * @return * / private String Parsekey (String -Schlüssel, Methode, Objekt [] args) {if (stringutils.isempy (tasty)) return null; // Die Parameternamenliste der abgefangenen Methode (unter Verwendung der Frühlingsunterstützungsklassenbibliothek) localVariabletableParameTernamedSiscoverer u = new LocalVariabletableParameTernamedSiscoverer () erhalten; String [] paranamearr = u.getParameternames (Methode); // SpelexpressionParser () verwenden // SpelexpressionParser () verwenden. // Spel Context StandardValuationContext context = new StandardValuationContext (); // Die Methodenparameter in den Spel -Kontext für (int i = 0; i <paranamearr.length; i ++) {context.setVariable (paranamearr [i], args [i]) eingeben; } return Parser.Parseexpression (Schlüssel) .GetValue (Kontext, String.class); } // serialisieren private string serialize (Object obj) {String result = null; try {result = jsonUtil.serialize (obj); } catch (Ausnahme e) {result = obj.toString (); } Rückgabeergebnis; } // Deserialize private Object deserialize (string str, class clazz) {Object result = null; try {if (clazz == jsonObject.class) {result = new JsonObject (str); } else if (clazz == jsonArray.class) {result = new JsonArray (str); } else {result = jsonUtil.deserialize (str, clazz); }} catch (Ausnahme E) {} Rückgabeergebnis; } // Deserialisierung, Support -Liste <xxx> privates Objekt Deserialize (String Str, Class Clazz, Klasse ElementClass) {Object ergebnis = null; try {if (clazz == jsonObject.class) {result = new JsonObject (str); } else if (clazz == jsonArray.class) {result = new JsonArray (str); } else {result = jsonUtil.Deserialize (str, clazz, elementclass); }} catch (Ausnahme E) {} Rückgabeergebnis; } public void setCacheEnable (boolean sandable) {this.cacheenable = seltenable; }}Die oben genannte Schnittstelle verwendet eine vom Cacheenableable absichtbare Variable, um zu steuern, ob Cache verwendet werden soll. Um einen nahtlosen Zugang zum Springboot zu erreichen, muss die native @EnableCaching -Annotation gesteuert werden. Hier verwende ich einen vom Federcontainer geladenen Hörer und finde dann im Hörer, ob es eine Klasse gibt, die von der @enableCaching -Annotation geändert wird. Wenn ja, holen Sie sich das Multicacheasspect -Objekt aus dem Federbehälter und setzen Sie dann auf True ein. Dies ermöglicht den nahtlosen Zugang zum Springboot. Ich frage mich, ob es für Freunde eine elegantere Möglichkeit gibt. Willkommen zu kommunizieren! Die Hörerklasse ist wie folgt
import com.xuanwu.apaas.core.multicache.cachefactory; import com.xuanwu.apaas.core.multicache.multicACHACHEACTECT; import org.springframework.cache.annotation.enableCaching; import org.springframework.context.applicationListener; import org.springframework.context.event.contextreFreshedEvent; import org.springframework.stereotype.comPonent; import Java.util.map; / ** * wird verwendet, um festzustellen, ob es Annotation gibt, um Cache im Projekt nach dem Laden des Frühlings zu aktivieren. Container, um das Auftreten von zwei Aufrufen zu verhindern (MVC -Laden wird auch einmal ausgelöst) if (Event.GetApplicationContext (). getParent () == NULL) {// Alle Klassen erhalten, modifiziert von @enableCaching Annotation Map <String, Object> Beans = Event.GetApplicationContext (). if (beans! Multicache.setCacheenable (True); }}}}}Um einen nahtlosen Zugriff zu erzielen, müssen wir auch überlegen, wie Multi-Point-EHCache mit Redis-Cache bei der Bereitstellung von Multi-Point-EHCache übereinstimmt. In normalen Anwendungen ist Redis im Allgemeinen für langfristig zentralisierten Cache geeignet, und EHCache ist für kurzfristige lokale Cache geeignet. Angenommen, es gibt jetzt A-, B- und C -Server, A und B, die Business Services bereitstellen, und C bereitet REDIS -Dienste bereit. Wenn eine Anfrage eingeht, leitet der Front-End-Eintrag, unabhängig davon, ob es sich um Software wie LVS oder NGINX handelt, die Anforderung an einen bestimmten Server. Angenommen, es wird an Server A weitergeleitet und ein bestimmter Inhalt wird geändert, und dieser Inhalt ist sowohl in Redis als auch in EHCache verfügbar. Zu diesem Zeitpunkt ist der EHCACHE -Cache von Server A und Redis von Server C einfacher zu steuern, ob der Cache ungültig oder gelöscht ist. Aber wie kann man den EHCache von Server B zu diesem Zeitpunkt steuern? Die häufig verwendete Methode besteht darin, den Veröffentlichungsabonnementmodus zu verwenden. Wenn Sie den Cache löschen müssen, veröffentlichen Sie eine Nachricht auf einem festen Kanal. Dann zeichnet sich jeder Business -Server für diesen Kanal ab. Nach Erhalt der Nachricht löschen oder verfallen Sie den lokalen EHCACHE -Cache (es ist am besten, abgelaufen zu verwenden, aber Redis unterstützt derzeit nur abgelaufene Vorgänge auf dem Schlüssel. Es gibt keine Möglichkeit, den Ablauf der Mitglieder auf der Karte unter dem Schlüssel zu betreiben. Wenn Sie den Vergleich erzwingen müssen, können Sie. Weniger Schreiben. Zusammenfassend soll der Prozess ein bestimmtes Datenstück aktualisieren, zuerst den entsprechenden Cache in Redis löschen und dann eine Nachricht mit ungültigem Cache in einem bestimmten Kanal von Redis veröffentlichen. Der lokale Business -Service zeichnet sich der Nachricht dieses Kanals ab. Wenn der Geschäftsdienst diese Nachricht erhält, löscht er den lokalen EHCACHE -Cache. Die verschiedenen Konfigurationen von Redis sind wie folgt.
import com.fasterxml.jackson.annotation.jsonAutodetekt; import com.fasterxml.jackson.annotation.propertyAccessor; import com.fasterxml.jackson.databind.objectmapper; import com.xuanwu.apaas.core.multicache.subscriber.messagesUbscriber; import org.springframework.cache.cachemanager; import org.springframework.context.annotation.bean; import org.springframework.context.annotation.configuration; import org.springframework.data.redis.cache.rediscachemanager; import org.springframework.data.redis.connection.redisconnectionFactory; import org.springframework.data.redis.core.redistemplate; import org.springframework.data.redis.core.StringRedistemplate; import org.springframework.data.redis.listener.pattertopic; import org.springframework.data.redis.Listener.Redrismessagelistenercontainer; import org.springframework.data.redis.listener.adapter.messagelisteneradapter; import org.springframework.data.redis.serializer.jackson2jsonRedisserializer; @Configuration Public Class Redisconfig {@Bean public CacheManager CacheManager (redistemplate redistemplate) {rediscaCheManager rcm = new rediscachemanager (redistemplate); // Cache -Ablaufzeit setzen (Sekunden) rcm.setDefaultExpiration (600); RCM zurückgeben; } @Bean public redistemplate <String, String> redISTemplate (redisconnectionFactory Factory) {StringRteMplate Vorlage = new StringRedItemplate (Factory); Jackson2JsonRedisserializer Jackson2JsonRedisserializer = new Jackson2JsonRedisserializer (Objekt.Class); ObjectMapper om = new ObjectMapper (); om.setvissibility (PropertyAccessor.All, jsonAutodetect.vissibility.any); om.EnabledEfaulttyPing (ObjectMapper.DefaulttyPing.non_final); Jackson2JsonRedisserializer.SetObjectMapper (OM); template.setValueSerializer (Jackson2JsonRedisserializer); template.afterProperTieSt (); Rückgabevorlage; } /*** Redis Message Listener Container* Sie können mehrere Redis -Hörer hinzufügen, die verschiedene Themen hören. 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 = neuer redismessagelistenercontainer (); Container.SetConnectionFactory (ConnectionFactory); // Abonnieren Sie einen Channel Container. // Dieser Container kann mehrere Messagelistener -Rückkehrbehälter hinzufügen. } /** * Message -Listener -Adapter, bindet den Nachrichtenprozessor und verwendet Reflexionstechnologie, um die Geschäftsmethoden des Nachrichtenprozessors aufzurufen. MessagelistenerAdapter (Empfänger, "Handle"); }}Die Message Publishing -Klasse lautet wie folgt:
import com.xuanwu.apaas.core.multicache.cachefactory; import org.apache.commons.lang3.stringutils; import org.slf4j.logger; import org.slf4j.loggerfactory; import org.springframework.beans.factory.annotation.autowired; import org.springframework.stereotype.comPonent; @Component public class messagesubscriber {private static final logger logger = loggerfactory.getLogger (messageUbscriber.class); @Autowired Private Cachefactory Cachefactory; / *** Nach Empfang der Nachricht des Redis -Abonnements ist der Cache von EHCache ungültig. if (stringutils.isempty (message)) {return; } String [] strs = message.split ("#"); String name = strs [0]; String key = null; if (strs.Length == 2) {key = strs [1]; } CacheFactory.ehdel (Name, Schlüssel); }}Die spezifischen Operations -Cache -Klassen sind wie folgt:
import com.xuanwu.apaas.core.multicache.publisher.messagepublisher; net.sf.ehcache.cache import; import net.sf.ehcache.cacheManager; net.sf.ehcache.element; import org.apache.commons.lang3.stringutils; import org.slf4j.logger; import org.slf4j.loggerfactory; import org.springframework.beans.factory.annotation.autowired; import org.springframework.data.redis.redisconnectionFailureException; import org.springframework.data.redis.core.hashoperations; import org.springframework.data.redis.core.redistemplate; import org.springframework.stereotype.comPonent; importieren java.io.inputstream; / *** Multi-Level-Cache-Abschnitt* @Author Rongdi*/ @Component Public Class CacheFactory {private static Final Logger logger = loggerfactory.getLogger (cachefactory.class); @Autowired Private Redistemplate Redistemplate; @Autowired private messagePublisher messagePublisher; Privat CacheManager CacheManager; public CacheFactory () {InputStream ist = this.getClass (). getResourceAsStream ("/ehcache.xml"); if (ist! }} public void cachedel (String -Name, String -Taste) {// Löschen Sie den Cache, der Redis entspricht; // Löschen Sie den lokalen EHCache -Cache, der nicht benötigt wird, und der Abonnent löscht // EHDEL (Name, Schlüssel); if (cacheManager! }} public String EHGet (String -Name, String -Schlüssel) {if (cachemanager == null) return null; Cache Cache = CACHEMANAGER.getCache (Name); if (cache == null) return null; cache.acquirereadlockonkey (Schlüssel); try {Element ele = cache.get (Schlüssel); if (ele == null) return null; return (string) ele.getObjectValue (); } endlich {cache.releasereadlockonkey (key); }} public String redusget (String -Name, String -Schlüssel) {Hashoperations <String, String, String> Opera = redISTemplate.opsforHash (); try {return Opera.get (Name, Schlüssel); } catch (redisconnectionFailureException e) {// Die Verbindung schlägt fehl, kein Fehler und der Logger.Error ("Redis -Fehler verbinden", e); null zurückkehren; }} public void eHput (String -Name, String -Schlüssel, String -Wert) {if (cachemanager == null) return; if (! } Cache cache = cachemanager.getCache (Name); // Erhalten Sie die Schreibschloss auf dem Schlüssel, verschiedene Schlüssel beeinflussen sich nicht gegenseitig, ähnlich wie synchronisiert (key.intern ()) {} cache.acquireWriteLockonKey (Schlüssel); try {cache.put (neues Element (Schlüssel, Wert)); } endlich {// veröffentlichen den Schreibschloss cache.ReleaseWriteLockonkey (Schlüssel); }} public void reendisput (String -Name, String -Schlüssel, String -Wert) {Hashoperations <String, String, String> Opera = redISTemplate.opsforHash (); try {operator.put (Name, Schlüssel, Wert); } catch (redisconnectionFailureException e) {// Die Verbindung schlägt fehl, kein Fehler und der Logger.Error ("Redis -Fehler verbinden", e); }} public void EHDEL (String -Name, String -Schlüssel) {if (cacheManager == null) return; if (cacheManager.cacheexists (name)) {// Wenn der Schlüssel leer ist, löschen Sie direkt gemäß dem Cache -Namen if (stringutils.isempty (Schlüssel)) {CacheManager.Removecache (Name); } else {cache cache = cacheManager.getCache (Name); cache.remove (Schlüssel); }}} public void redisdel (String -Name, String -Schlüssel) {Hashoperations <String, String, String> Opera = redistemplate.opsforHash (); Versuchen Sie {// Wenn der Schlüssel leer ist, löschen Sie if (Stringutils.isempty (Schlüssel)) {redItemplate.delete (name); } else {Opera.Delete (Name, Schlüssel); }} catch (redisconnectionFailureException e) {// Die Verbindung ist fehlgeschlagen, kein Fehler wurde geworfen und der logger.Error ("Redis -Fehler verbinden", e); }}}Die Werkzeugkurs lautet wie folgt
import com.fasterxml.jackson.core.type.typeerference; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.javatype; import com.fasterxml.jackson.databind.objectmapper; import org.apache.commons.lang3.stringutils; import org.json.jsonArray; import org.json.jsonObject; import Java.util.*; public class jsonutil {private static ObjectMapper Mapper; static {mapper = new ObjectMapper (); mapper.configure (DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, FALSE); } / ** * Serialisieren Sie das Objekt in JSON * * @param obj obj -Objekt, das serialisiert werden soll } return mapper.writevalueasString (OBJ); } / ** Deserialisierung mit Generika, wie Deserialisierung eines JsonArray in List <Künstlers>* / public static <T> t Deserialize (String JSONSON, Class <? Elementklassen); return mapper.readValue (JSONSON, Javatype); } / *** Deserialisieren Sie den JSON -String in ein Objekt* @param src Die JSON -Zeichenfolge, die von der Deserialisierung des Objekts deserialisiert werden soll. null "); } if ("{}". Equals (src.trim ()) {return null; } return mappper.readValue (src, t); }}Um Cache speziell zu verwenden, achten Sie einfach auf @cacheable und @Cacheevict -Anmerkungen und unterstützen Sie auch Spring EL -Ausdrücke. Darüber hinaus hat der Cache -Name, der hier durch das Wertschöpfungsattribut dargestellt wird, nicht das oben erwähnte Problem. Unter Verwendung von Wert können verschiedene Caches isoliert werden. Beispiele sind wie folgt
@Cacheable (value = "bo", key = "#session
Das Hauptabhängigkeitspaket beigefügt
Das obige ist der gesamte Inhalt dieses Artikels. Ich hoffe, es wird für das Lernen aller hilfreich sein und ich hoffe, jeder wird Wulin.com mehr unterstützen.