SpringBoot의 캐시는 작업에 사용되며 사용하기에 매우 편리합니다. Redis 또는 Ehcache와 같은 캐시 종속성 패키지 및 관련 캐시의 스타터 종속성 패키지를 직접 소개 한 다음 @enablecaching 주석을 시작 클래스에 추가 한 다음 @Cachable 및 @CacheEvict를 사용하여 필요한 경우 캐시를 사용하고 삭제할 수 있습니다. 사용하기가 매우 간단합니다. 나는 SpringBoot 캐시를 사용한 사람들이 재생 될 것이라고 생각합니다. 유일한 단점은 SpringBoot가 플러그인 통합을 사용한다는 것입니다. 사용하기가 매우 편리하지만 Ehcache를 통합 할 때는 ehcache를 사용하고 Redis를 통합하면 Redis를 사용합니다. 둘 다 함께 사용하려면 Ehcache는 로컬 레벨 1 캐시로 사용되며 Redis는 통합 레벨 2 캐시로 사용됩니다. 내가 아는 한, 기본 방법을 달성하는 것은 불가능합니다 (이를 구현할 수있는 전문가가있는 경우 조언을 해주세요). 결국 많은 서비스에는 다중 포인트 배포가 필요합니다. ehcache 만 선택하면 로컬 캐시를 잘 실현할 수 있습니다. 그러나 여러 컴퓨터간에 캐시를 공유하면 문제가 발생하는 데 시간이 걸립니다. 중앙 집중식 Redis 캐시를 선택하면 데이터를 얻을 때마다 네트워크로 이동해야하기 때문에 성능이 너무 좋지 않다고 생각합니다. 이 주제는 주로 스프링 부츠를 기반으로 ehcache 및 redis를 1 레벨 캐시로 원활하게 통합하고 캐시 동기화를 실현하는 방법에 대해 설명합니다.
스프링 부츠의 원래 캐시 메소드를 침략하지 않기 위해 다음과 같이 두 개의 캐시 관련 주석을 정의했습니다.
@TARGET ({ElementType.Method}) @retention (retentionpolicy.runtime) public @Interface Cachable {String value () default ""; 문자열 키 () 기본값 ""; // 제네릭 클래스 유형 클래스 <?> type () 기본 예외 .class; } @TARGET ({ElementType.Method}) @retention (retentionpolicy.runtime) public @interface cacheevict {String value () default ""; 문자열 키 () 기본값 ""; }위의 두 주석은 기본적으로 봄의 캐시 된 주석과 동일하지만 일부는 드물게 사용되는 속성이 제거됩니다. 이것에 대해 말하면, 스프링 부츠에서 Redis 캐시만을 만 사용할 때 캐시 가능하고 Cacheevict가 주석을 달고 Cacheevict가 실제로 Redis에서 ZSET 값 키가되고 @Cachable (value = "Cache1", key = "key1")과 같은 ZSET 값 키가 여전히 비어 있음을 알았습니다. 정상적인 상황에서 Cache1-> Map (key1, value1)은 Cache1이 캐시 이름으로 사용되고, 캐시 값으로 맵을, 맵의 키로 사용되는 Redis에 나타나야하며, 이는 다른 캐시 이름으로 캐시를 효과적으로 분리 할 수 있습니다. 그러나 실제로 CACHE1-> 빈 (ZSET) 및 key1-> value1, 두 개의 독립적 인 키 값 쌍이 있습니다. 실험은 다른 캐시 이름의 캐시가 완전히 공유된다는 것을 발견했습니다. 관심이 있으시면 시도해 볼 수 있습니다. 즉,이 값 속성은 실제로 장식이며 키의 독창성은 키 속성에 의해서만 보장됩니다. 나는 이것이 스프링의 캐시 구현에서 버그라고 생각하거나 구체적으로 설계되었다고 생각할 수 있습니다 (이유를 알고 있다면 조언을 해주세요).
주제로 돌아가서 주석으로 주석 처리 클래스도 있습니다. 여기서는 차단을 위해 AOP 섹션을 사용하며 기본 구현은 실제로 비슷합니다. 섹션 처리 클래스는 다음과 같습니다.
com.xuanwu.apaas.core.multicache.annotation.cacheevict import; com.xuanwu.apaas.core.multicache.annotation.cachable import; com.xuanwu.apaas.core.utils.jsonutil 가져 오기; import org.apache.commons.lang3.stringutils; import org.aspectj.lang.proceedingjoinpoint; import org.aspectj.lang.annotation.around; import org.aspectj.lang.annotation.around; import org.aspectj.lang.annotation.aspect; import org.aspectj.lang.annotation.pointcut; import org.aspectj.lang.reflect.methodsignature; import org.json.jsonarray; import org.json.jsonobject; import org.slf4j.logger; org.slf4j.loggerfactory; org.springframework.beans.factory.annotation.autowired; import org.springframework.core.localvariabletableparameternamedscoverer; import org.springframework.expression.expressionparser; org.springframework.expression.spel.standard.spelexpressionparser; org.springframework.expression.spel.support.standardEvaluationContext; org.springframework.stereotyp.component import; import java.lang.reflect.method; / *** 다단계 캐시 섹션* @author rongdi*/ @aspect @component public class multicacheaspect {private static final logger = loggerfactory.getLogger (multicacheSpect.class); @autowired Private Cachefactory Cachefactory; // 여기서 리스너는 컨테이너를 통해 초기화되며 캐시 스위치는 외부 구성 @EnableCaching 주석 개인 부울 Cacheenable에 따라 제어됩니다. @pointcut ( "@annotation (com.xuanwu.apaas.core.multicache.annotation.cachable)") public void cacheaspect () {} @pointcut ( "@annotation (com.xuanwu.apaas.core.multicache.annotation.cacheevict)")} @around ( "cacheableaspect ()") public 객체 캐시 (proceedingjoinpoint joinpoint) {// 패싯 객체에서 수정 된 메소드의 매개 변수 목록을 가져옵니다 [] args = joinpoint.getargs (); // 결과는 메소드 객체 result = null의 최종 반환 결과입니다. // 캐시가 활성화되지 않은 경우 처리 방법을 직접 호출하여 if (! cacheenable) {try {result = joinpoint.proceed (args); } catch (Throwable e) {logger.error ( "", e); } 반환 결과; } // 프록시 메소드 클래스의 반환 값 유형을 가져옵니다 returnType = ((MethodSignature) joinpoint.getSignature ()). etereturntype (); // 프록시 메소드 메소드 메서드를 가져옵니다 메소드 메서드 = ((메소드 디자이너) joinpoint.getSignature ()). getMethod (); // 프록시 메소드 캐시 가능한 ca = method.getAnnotation (cachable.class)에서 주석을 얻습니다. // el string key = parsekey (ca.key (), method, args)가 구문 분석 한 키 값을 얻습니다. class <?> elementclass = ca.type (); // 주석에서 캐시 이름을 가져옵니다. 문자열 이름 = ca.value (); {// 먼저 ehcache 문자열에서 데이터를 가져옵니다. cachevalue = cachefactory.ehget (name, key); if (stringUtils.isempty (cachevalue)) {// ehcache에 데이터가없는 경우 redis cachevalue = cachefactory.redisget (name, key)에서 데이터를 가져옵니다. if (stringUtils.isempty (cachevalue)) {// ehcache에 데이터가없는 경우 redis cachevalue = cachefactory.redisget (name, key)에서 데이터를 가져옵니다. if (stringUtils.isempty (cachevalue)) {// redis에 데이터가없는 경우 // 비즈니스 메소드를 호출하여 결과 = joinpoint.proceed (args); // 결과를 시리얼링하여 redis cachefactory.redisput (이름, 키, Serialize (결과))에 넣습니다. } else {// 데이터를 redis에서 얻을 수있는 경우 // 캐시에서 얻은 데이터를 사형화하고 if (elementclass == excection.class) {result = deserialize (cachevalue, returnType); } else {result = deserialize (cachevalue, returnType, elementclass); }} // 결과를 시리얼링하여 ehcache cachefactory.ehput (name, key, serialize (result))에 넣습니다. } else {// 캐시에서 얻은 데이터를 제외하고 if (elementClass == exception.class) {result = deserialize (cachevalue, returnType); } else {result = deserialize (cachevalue, returnType, elementclass); }}} catch (Throwable Throwable) {logger.error ( "", Throwable); } 반환 결과; } / ** * 메소드가 호출되기 전에 캐시를 지우고 비즈니스 메소드를 호출 한 다음 비즈니스 메소드 * @param joinpoint * @Throws Throwsable * / @around ( "cacheevict ()") 공개 대상 EvictCache (ProceedingJoinPoint jointpoint) 던지기 가능 {// proxy method.getsignature (getMethod) // 패싯 객체에서 수정 된 메소드의 매개 변수 목록을 가져옵니다 [] args = joinpoint.getArgs (); // 프록시 메소드에서 주석을 가져옵니다. cacheevict ce = method.getAntantation (cacheevict.class); // el string key = parsekey (ce.key (), method, args)에 의해 구문 분석 된 키 값을 얻습니다. // 주석 문자열에서 캐시 이름을 가져옵니다. string name = ce.value (); // 해당 캐시 CacheFactory.Cachedel (이름, 키)을 지우십시오. return jointoppoint.proceed (args); } / ** * 캐시 된 키 * 주석에 정의 된 키 * spel expressions * @return * / private String parsekey (문자열 키, 메소드 메소드, Object [] args) {if (stringUtils.isempty (키)) return null; // 가로 채기 된 메소드의 매개 변수 이름 목록 가져 오기 (스프링 지원 클래스 라이브러리 사용) localVariabletableParameterNamedScoverer u = 새로운 LocalVariabletableArameterNamedScoverer (); 문자열 [] paranamearr = u.getParameterNames (method); // 주요 구문 분석에 spel을 사용하여 표현식 파서 = new spelexpressionparser (); // spel context StandardEvaluationContext context = new StandardEvaluationContext (); // 메소드 매개 변수를 spel 컨텍스트에 넣습니다 (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 (Object obj) {String result = null; try {result = jsonutil.serialize (obj); } catch (예외 e) {result = obj.toString (); } 반환 결과; } // deserialize private 객체 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 (예외 e) {} return result; } // deserialization, 지원 목록 <xxx> 개인 객체 deserialize (String str, class clazz, class elementclass) {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, elementclass); }} catch (예외 e) {} return result; } public void setCacheenable (부울 Cacheenable) {this.cacheenable = cacheenable; }}위의 인터페이스는 캐시 사용 여부를 제어하기 위해 Cacheenable 변수를 사용합니다. SpringBoot에 대한 원활한 액세스를 얻으려면 Native @EnableCaching 주석에 의해 제어되어야합니다. 여기서 스프링 컨테이너로로드 된 리스너를 사용한 다음 청취자에서 @enablecaching 주석으로 수정 된 클래스가 있는지 확인합니다. 그렇다면 스프링 컨테이너에서 다중 성분 객체를 가져온 다음 Cacheenable을 true로 설정하십시오. 이렇게하면 SpringBoot에 완벽하게 액세스 할 수 있습니다. 친구에게 더 우아한 방법이 있는지 궁금합니다. 의사 소통에 오신 것을 환영합니다! 청취자 클래스는 다음과 같습니다
com.xuanwu.apaas.core.multicache.cachefactory import; com.xuanwu.apaas.core.multicache.multicacheaspect import; org.springframework.cache.annotation.enablecaching; org.springframework.context.applicationListener; org.springframework.context.event.contextrefreshedevent; org.springframework.stereotyp.component import; java.util.map import; / ** * 스프링 로딩이 완료된 후 프로젝트에 캐시를 활성화 할 수있는 주석이 있는지 여부를 찾는 데 사용됩니다. @EnableCaching * @author rongdi */ @component public contetrefreshedlistener applicationlistener <contextrepheshedevent> {contextresplicationevent (contextrephedevent evition) {// 두 통화가 발생하지 않도록 컨테이너 (MVC로드는 한 번 트리거됩니다) if (event.getApplicationContext (). getParent () == null) {// @enableCaching Annotation Map <String, objec = event.getApplicationContext ()에 의해 수정 된 모든 클래스를 얻습니다. if (beans! = null &&! beans.isempty ()) {multicachecepper multicache = (multicacheaspect) event.getApplicationContext (). getBean ( "multicacheaspect"); multicache.setCacheenable (true); }}}}}원활한 액세스를 얻으려면 멀티 포인트 ehcache를 배포 할 때 멀티 포인트 ehcache가 Redis 캐시와 어떻게 일치하는지 고려해야합니다. 정상적인 응용 분야에서 Redis는 일반적으로 장기 중앙 집중식 캐시에 적합하며 Ehcache는 단기 로컬 캐시에 적합합니다. 이제 A, B 및 C 서버, A 및 B 배포 비즈니스 서비스 및 C가 Redis 서비스를 배포한다고 가정합니다. 요청이 들어 오면 LVS 또는 NGINX와 같은로드 소프트웨어이든 프론트 엔드 항목은 요청을 특정 서버로 전달합니다. 서버 A로 전달되고 특정 컨텐츠가 수정되었다고 가정 하고이 컨텐츠는 Redis 및 Ehcache 모두에서 사용할 수 있습니다. 현재 서버 A와 서버 C의 ehcache 캐시는 캐시가 유효하지 않은지 삭제되는지 여부를 쉽게 제어 할 수 있습니다. 그러나 현재 서버 B의 ehcache를 제어하는 방법은 무엇입니까? 일반적으로 사용되는 방법은 게시 구독 모드를 사용하는 것입니다. 캐시를 삭제 해야하는 경우 고정 채널에 메시지를 게시합니다. 그런 다음 각 비즈니스 서버는이 채널에 가입합니다. 메시지를받은 후, 당신은 로컬 ehcache 캐시를 삭제하거나 만료하는 것입니다 (만료 된 것이 가장 좋습니다. 그러나 Redis는 현재 열쇠에서 만료 된 작업 만 지원합니다. 키 아래지도에서 멤버의 만료를 조작 할 수있는 방법은 없습니다. 만료를 강요해야한다면, 당신은 그것을 직접 구현할 수 있다면, 삭제를 유발할 수있는 기회는 매우 작아서, 당신은 그것을 더 적게 구현할 수 있습니다. 글을 적게 쓰십시오. 여기에서 편의를 위해 캐시를 직접 삭제합니다). 요약하면, 프로세스는 특정 데이터를 업데이트하고 먼저 Redis에서 해당 캐시를 삭제 한 다음 특정 Redis 채널에 잘못된 캐시가있는 메시지를 게시하는 것입니다. 지역 비즈니스 서비스는이 채널의 메시지를 가입합니다. 비즈니스 서비스 가이 메시지를 받으면 로컬 Ehcache 캐시를 삭제합니다. Redis의 다양한 구성은 다음과 같습니다.
com.fasterxml.jackson.annotation.jsonautodetect 가져 오기; com.fasterxml.jackson.annotation.propertyaccessor import; com.fasterxml.jackson.databind.objectmapper 가져 오기; com.xuanwu.apaas.core.multicache.subscriber.messagesubscriber import; org.springframework.cache.cachemanager import; import org.springframework.context.annotation.bean; org.springframework.context.annotation.configuration; org.springframework.data.redis.cache.rediscachemanager; org.springframework.data.redis.connection.redisconnectionfactory; org.springframework.data.redis.core.redistemplate; import org.springframework.data.redis.core.stringredistemplate; org.springframework.data.redis.listener.patterntopic; import org.springframework.data.redis.listener.redismessagelistenercontainer; org.springframework.data.redis.listener.adapter.messagelisteneradapter; org.springframework.data.redis.serializer.jackson2jsonredisserializer; @configuration public class readisconfig {@bean public cachemanager cachemanager (redistemplate redistemplate) {readiscachemanager rcm = new readiscachemanager (redistemplate); // 캐시 만료 시간 설정 (초) rcm.setDefaultexPiration (600); RCM 반환; } @bean public redistemplate <string, string> redistemplate (readisconnectionFactory factory) {StringRedistemplate 템플릿 = new StringRedistemplate (Factory); Jackson2jsonRedisserializer Jackson2jsonRedisserializer = new Jackson2jsonRedisserializer (Object.Class); ObjectMapper om = new ObjectMapper (); om.setvisibility (propertyAccessor.all, jsonAutoDetect.visibility.any); om.enabledefaulttyping (ObjectMapper.DefaultTyping.Non_Final); jackson2jsonredisserializer.setobjectmapper (om); template.setValueserializer (Jackson2jsonRedisserializer); template.fterProperTiesset (); 리턴 템플릿; } /*** Redis 메시지 리스너 컨테이너* 다른 주제를 듣는 여러 개의 Redis 리스너를 추가 할 수 있습니다. 메시지 청취자와 해당 메시지 구독 프로세서와 메시지 청취자 * 일부 비즈니스 처리를위한 반사 기술을 통해 메시지 구독 프로세서 관련 메소드 호출 * @param connectionFactory * @Param LeargerAdapter */ @bean public redismessAgelistOnerContainer Container (readisconnectionfactory, messageListorAdapter reblistOnerAdapter em). redismessagelistenercontainer 컨테이너 = 새로운 redismessagelistenercontainer (); container.setConnectionFactory (ConnectionFactory); // 채널 컨테이너 구독 .addmessagelistener (LeargerAdapter, new PatternTopic ( "redis.uncache")); //이 컨테이너는 여러 Messagelistener 리턴 컨테이너를 추가 할 수 있습니다. } /** * 메시지 청취자 어댑터, 메시지 프로세서를 바인딩하며 반사 기술을 사용하여 메시지 프로세서의 비즈니스 방법 * @param 수신기 * /@bean messagelisteneradapter LeargeRadapter (messageUbscriber receiver) {//이 장소는 MessagelistenerAdapter를 사용하는 것입니다. Messagelisteneradapter (수신기, "핸들"); }}메시지 게시 수업은 다음과 같습니다.
com.xuanwu.apaas.core.multicache.cachefactory import; import org.apache.commons.lang3.stringutils; import org.slf4j.logger; org.slf4j.loggerfactory; org.springframework.beans.factory.annotation.autowired; org.springframework.stereotyp.component import; @Component Public Class MessageSubScriber {private static final logger = loggerfactory.getLogger (messageSubscriber.class); @autowired Private Cachefactory Cachefactory; / *** redis 구독의 메시지를받은 후, ehcache의 캐시는 무효화됩니다* @param 메시지 형식은 name_key*/ public void handle (string message) {logger.debug ( "redis.ehcache :"+message); if (stringUtils.isempty (메시지)) {return; } string [] strs = message.split ( "#"); 문자열 이름 = strs [0]; 문자열 키 = null; if (strs.length == 2) {key = strs [1]; } cachefactory.ehdel (이름, 키); }}특정 작동 캐시 클래스는 다음과 같습니다.
com.xuanwu.apaas.core.multicache.publisher.messagepublisher import; import net.sf.ehcache.cache; import net.sf.ehcache.cachemanager; import net.sf.ehcache.element; import org.apache.commons.lang3.stringutils; import org.slf4j.logger; org.slf4j.loggerfactory; org.springframework.beans.factory.annotation.autowired; org.springframework.data.redis.redisconnectionFailureException; org.springframework.data.redis.core.hashoperations import; org.springframework.data.redis.core.redistemplate; org.springframework.stereotyp.component import; import java.io.inputstream; / *** 다단계 캐시 섹션* @Author rongdi*/ @Component Public Class Cachefactory {private static final logger = loggerfactory.getLogger (cachefactory.class); @autowired 개인 Redistemplate Redistemplate; @autowired private messagepublisher messagepublisher; 개인 Cachemanager Cachemanager; public cachefactory () {inputStream은 = this.getClass (). getResourceasStream ( "/ehcache.xml"); if (is! = null) {cachemanager = cachemanager.create (is); }} public void cachedel (문자열 이름, 문자열 키) {// redis에 해당하는 캐시를 삭제합니다. // 로컬 ehcache 캐시를 삭제하면 필요하지 않은 가입자가 삭제됩니다. // ehdel (이름, 키); if (cachemanager! = null) {// 가입 된 서비스에 캐시가 잘못된 MessagePublisher.publish (name, key)를 알려주는 메시지를 게시합니다 (이름, key); }} public String ehget (문자열 이름, 문자열 키) {if (cachemanager == null) return null; 캐시 캐시 = CacheManager.getCache (이름); if (cache == null) return null; cache.acquirereadlockonkey (키); try {element ele = cache.get (key); if (ele == null) return null; return (string) ele.getObjectValue (); } 마침내 {cache.releaseReadlockonkey (키); }} public string redisget (문자열 이름, 문자열 키) {hashoperations <string, string, string> Opera = redistemplate.opsforhash (); {return opera.get (이름, 키); } catch (readisconnectionFailureException e) {// 연결에 실패, 오류가 발생하지 않고 logger.error ( "Connect redis error", e); 널 리턴; }} public void ehput (문자열 이름, 문자열 키, 문자열 값) {if (cachemanager == null) return; if (! cachemanager.cacheexists (name)) {cachemanager.addcache (name); } Cache Cache = CacheManager.getCache (이름); // 키에서 쓰기 잠금을 가져 오면 동기화 된 (key.intern ()) {} cache.acquirewritelockonkey (key); try {cache.put (새 요소 (키, 값)); } 마지막으로 {// 쓰기 잠금 캐시를 해제합니다. }} public void redisput (문자열 이름, 문자열 키, 문자열 값) {hashoperations <string, string, string> Opera = redistemplate.opSforhash (); try {operator.put (이름, 키, 값); } catch (readisconnectionFailureException e) {// 연결 실패, 오류가 발생하지 않았고 logger.error ( "Connect redis error", e); }} public void ehdel (문자열 이름, 문자열 키) {if (cachemanager == null) return; if (cachemanager.cacheexists (name)) {// 키가 비어있는 경우 캐시 이름에 따라 직접 삭제합니다 (stringUtils.isempty (key)) {cachemanager.removecache (name); } else {캐시 캐시 = Cachemanager.getCache (이름); CACHE.REMOVE (키); }}} public void redisdel (문자열 이름, 문자열 키) {hashoperations <string, string, string> Opera = redistemplate.opSforhash (); 시도 {// 키가 비어 있으면 if (stringUtils.isempty (key)) {redistemplate.delete (name); } else {Opera.delete (이름, 키); }} catch (RediscOntectionFailureException e) {// 연결 실패, 오류가 발생하지 않았으며 Logger.error ( "Connect Redis Error", e); }}}도구 클래스는 다음과 같습니다
com.fasterxml.jackson.core.type.typereference import; com.fasterxml.jackson.databind.deserializationFeature 가져 오기; com.fasterxml.jackson.databind.javatype 가져 오기; com.fasterxml.jackson.databind.objectmapper 가져 오기; import org.apache.commons.lang3.stringutils; import org.json.jsonarray; import org.json.jsonobject; java.util.*; 공개 클래스 jsonutil {private static objectmapper mapper; static {mapper = new ObjectMapper (); mapper.configure (deserializationfeature.fail_on_unknown_properties, false); } / ** * 객체를 json * * @param obj 객체로 직렬화하여 직렬화 * @return * @throws 예외 * / public static string serialize (object obj) exception {if (obj == null) {throw new ImplegalArgumentException ( "obj가 널이되지 않아야한다"). } return mapper.writeValueAsString (OBJ); . 요소 클래스); return mapper.readValue (jsonst, javatype); } / *** json 문자열을 객체로 사로화* @param src json 문자열을 사로화 할 수 있습니다* @param t 객체의 클래스 유형* @return* @throws 예외* / public static <t> t deserialize (string src, class <t> t는 예외 (src == new) {newleralargumectement trown grow) null이 아닙니다 "); } if ( "{}". Equals (src.trim ())) {return null; } return mappper.readValue (src, t); }}특히 캐시를 사용하려면 @Cachable 및 @Cacheevict 주석에주의를 기울이고 Spring El 표현을 지원하십시오. 또한 여기의 값 속성으로 표시되는 캐시 이름에는 위에서 언급 한 문제가 없습니다. 값을 사용하여 다른 캐시를 분리 할 수 있습니다. 예는 다음과 같습니다
@Cachable (value = "bo", key = "#session.productionCode+''+#session.tenantCode+''+#ObjectCode")@cacheevict (value = "bo", key = "#session.ProductVersionCode+''+#session.tenantCode+''+#ObjectCode").
기본 종속성 패키지를 첨부했습니다
위는이 기사의 모든 내용입니다. 모든 사람의 학습에 도움이되기를 바랍니다. 모든 사람이 wulin.com을 더 지원하기를 바랍니다.