В этой статье вводит краткое обсуждение принципа впрыскивания зависимости весенних контейнеров МОК и делится им с вами следующим образом:
Основная задача инициализации контейнера МОК - установить карту данных о боиндефинировании в контейнере МОК. Я не видел контейнер IOC, вводящий отношения с зависимостью от бобов.
Предполагая, что текущий контейнер МОК загружает пользовательскую информацию о бобах, инъекция зависимостей в основном происходит на двух этапах.
При нормальных обстоятельствах это запускается, когда пользователь запрашивает боб из контейнера IOC в первый раз.
Тем не менее, мы можем контролировать атрибут Lazy-INIT в информации о BeAndDefinition, чтобы контейнер предварительно установил бобы, то есть процесс инъекции зависимости определенных бобов завершается в процессе инициализации.
1. Инъекция зависимости, вызванная GetBean
В базовом интерфейсе IOC интерфейс BeanFactory есть определение интерфейса для GetBean. Реализация этого интерфейса - это то, где происходит инъекция зависимости триггера. Чтобы дополнительно понять процесс инъекции зависимостей, мы начинаем с базового класса AbstractBeanfactory от defletablebeanfactory, чтобы взглянуть на реализацию GetBean
// Вот реализация интерфейса BeanFactory, такого как метод интерфейса getBean // Эти методы интерфейса GetBean в конечном итоге реализованы путем вызова Dogetbean @Override Public Object getBean (String name) Throws Beansexception {return Dogetbean (имя, ноль, ноль, false); } @Override public <t> t getBean (string name, class <t> обязательный тип) бросает BeanSexception {return Dogetbean (имя, обязательное значение, null, false); } @Override public Object getBean (String name, Object ... Args) Throws Beansexception {return Dogetbean (имя, null, args, false); } public <t> t getBean (string name, class <t> обязательный, объект ... args) бросает BeanSexception {return Dogetbean (имя, обязательное значение, args, false); } // Здесь фактически получается фасоль, то есть, где инъекция зависимости запускается @suppresswarnings ("uncecked") защищен <t> t Dogetbean (конечное имя строки, окончательный класс <T> обязанный, конечный объект [] args, boolean typecheckonly) throwsexception {final String beanmedame = nectorebeanname); Объектный боб; // с нетерпением проверьте синглтонский кэш на предмет зарегистрированных вручную синглтонов. // с нетерпением проверьте синглтонский кэш на предмет зарегистрированных вручную синглтонов. // с нетерпением проверьте синглтонский кэш для ручной регистрации Singletons // Сначала извлечь бобы из кеша и обработайте те синглтонские бобы, которые были созданы. Не создавайте объект sharedinstance = getsingleton (beanname); if (sharedInstance! = null && args == null) {if (logger.isdebugenabled ()) {if (issingletoncurryincreation (beanname)) {logger.debug («Возвращение с нетерпением езды на кэшированный экземпляр синглтона Bean ' + beanname +», который пока не является полностью инициализированным, - это то, что еще не инициализируется, - это то, что еще не инициализируется, - это то, что еще не инициализируется, - это то, что еще не инициализируется, - это то, что еще не инициализируется циркулярный экземпляр Systleton'); } else {logger.debug ("возвращающий кэшированный экземпляр синглтонского бона '" + beanname + "'"); }} // getObjectforbeanInstance здесь завершает соответствующую обработку заводских бобов для получения соответствующей обработки заводских бобов для получения результатов производства заводской бобы. Разница между BeanFactory и Factory Bean была упомянута ранее. Этот процесс будет подробно проанализирован позже. = getObjectForbeanInstance (sharedinstance, имя, beanname, null); } else {// Не удалось, если мы уже создаем этот экземпляр Bean: // Мы предполагаем в круговой ссылке. if (isprototypurlyNincreation (beanname)) {бросить новый BeancurryIncreationException (BeanName); } // // Проверьте, существует ли Beaindefinition в контейнере IOC. Если его не существует на текущей фабрике, следуйте по материнской цепочке Beanfactory и посмотрите вверх BeanFactory ParentBeanFactory = getParentBeanFactory (); if (parentbeanfactory! = null &&! Содержит Beandefinition (beanname)) {// не найдено -> Проверьте родитель. String nametolookup = OriginalBeanName (name); if (args! = null) {// делегирование родителям с явными Args. return (t) parentbeanfactory.getbean (nametolookup, args); } else {// no args -> делегировать в стандартный метод GetBean. return parentbeanfactory.getbean (nametolookup, readytype); }} if (! typecheckonly) {markbeanascreated (beanname); } try {// Get Beandefinition final RootBeandefinition mbd = getmergedlocalbeandefinition (beanname); Checkmergedbeandefinition (MBD, Beanname, Args); // гарантировать инициализацию бобов, от которой зависит текущий боб. // рекурсивно получить все бобы, которые текущий боб зависит от (если есть) string [] deventson = mbd.getDependson (); if (иждинсон! = null) {for (string dep: dependson) {if (isdependious (beanname, dep)) {throw new beancreationexception (mbd.getresourcedescription (), beanname, "циркуляр зависит от" между '" + beanname +"' и '" + dep +" "); } RegisterDependentBean (dep, Beanname); GetBean (DEP); }} // Создать экземпляр Singleton Bean, вызывая метод CreateBean if (mbd.issingleton ()) {sharedInstance = getsingleton (beanname, new objectfactory <bogate> () {@override public object getObject () throwsexcept Явно удалите экземпляр из Синглтона. bean = getObjectforbeanInstance (sharedinstance, имя, beanname, mbd); } // Это место для создания прототипа бобов еще if (mbd.isprototype ()) {// Это прототип -> Создать новый экземпляр. Объект PrototypeInstance = null; try {перед прототипированием (Beanname); PrototypeInstance = CreateBean (Beanname, MBD, ARGS); } наконец {AfterPrototypeCreation (Beanname); } bean = getObjectForbeanInstance (PrototypeInstance, имя, Beanname, MBD); } else {string scopename = mbd.getScope (); Окончательная область применения = this.scopes.get (scopename); if (scope == null) {бросить new allodalstateexception ("без применения, не зарегистрированной для имени сферы" " + scopename +" '"); } try {object scopedInstance = scope.get (beanname, new objectfactory <object> () {@Override public Object getObject () Throws BeanSexception {перед прототипированием (Beanname); try {return createbean (beanname, mbd, args); bean = getObjectforbeanInstance (ScopedInstance, имя, Beanname, MBD); } catch (allogalStateException ex) {Throw New BeancreationException (Beanname, «Scope '» + Scopename + "' не является активным для текущего потока; рассмотрите" + "определение прокси -сервера для этого боба, если вы собираетесь ссылаться на него из синглтона", ex); }}} catch (beansexception ex) {cleanupafterbeancreationfailure (beanname); бросить бывш; }} // Проверка, если требуется тип, соответствует типу фактического экземпляра боба. // Проверка типа выполняется на созданном бобах здесь. Если нет проблем, вновь созданный фасоль возвращается. Этот фасоль уже представляет собой бон, содержащий зависимость, если (обязательно имен! } catch (typemismatchexception ex) {if (logger.isdebugenabled ()) {logger.debug («Не удалось преобразовать компонент '" + name + "' в требуемый тип '" + classutils.getqualifiedname (requiretype) + "' ', ex); } бросить новый BeannotofRequiredTyPeexception (имя, обязательное значение, bean.getClass ()); }} return (t) bean; }Инъекция зависимости запускается здесь. Инъекция зависимости происходит, когда были установлены данные о бондфониции в контейнере. Хотя мы можем описать контейнер МОК самым простым способом, то есть рассматривать его как хэшмап, мы можем только сказать, что этот хэшмап является самой основной структурой данных контейнера, а не весь контейнер МОК.
Этот процесс впрыска зависимостей будет подробно объяснен ниже. На рисунке 1.1 показан общий процесс инъекции зависимости.
Рисунок 1.1 Процесс впрыска зависимостей
GetBean - отправная точка впрыска зависимостей. После этого будет вызвано CreateBean in AbstractautowirecapableBeanFactory для производства требуемых бобов, а также инициализация бобов также обрабатывается, например, реализация определения атрибута init-method в BeaNdefinition, пост-процессор бобов и т. Д.
@Override защищенный объект CreateBean (String Beanname, RootBeanDefinition MBD, Object [] args) Throws BeancreationException {if (logger.isdebugenabled ()) {logger.debug («Создание экземпляра Bean '» + beanname + "'"); } Rootbeandefinition mbdtouse = mbd; // Убедитесь, что класс бобов фактически разрешен на этом этапе, и // клонировать определение бобов в случае динамически разрешенного класса //, который не может быть сохранен в общем определении объединенных бобов. // Здесь мы определяем, можно ли создавать бобы, можно создать, можно ли загружать этот класс через класс загрузчика класса <?> ResolvedClass = ResulebeanClass (MBD, Beanname); if (ResolvedClass! = null &&! mbd.hasbeanclass () && mbd.getbeanclassname ()! = null) {mbdtouse = new Rootbeandefinition (mbd); mbdtouse.setbeanclass (ResolvedClass); } // Подготовка метода переопределения. try {mbdtouse.preparemethodoverrides (); } catch (beandefinitionvalidationException ex) {Throw New BeandefinitionStoreException (mbdtouse.getResourceDescription (), Beanname, «Проверка переопределения метода не удалась», Ex); } try {// Дайте BeanpostProcessors возможность вернуть прокси вместо экземпляра целевого боба. // Если боб настроил постпроцессор, то возвращаемый прокси -объект Bean = resolvebeForeInstantiation (Beanname, Mbdtouse); if (bean! = null) {return bean; }} catch (throwable ex) {throw new beancreationException (mbdtouse.getresourcedescription (), beanname, «Beanpostprocessor до того, как экстремация бобов не удалась», Ex); } try {Object beaninStance = docReateebean (Beanname, mbdtouse, args); if (logger.isdebugenabled ()) {logger.debug («Закончено создавать экземпляр бобов» « + beanname +" '""); } return beaninStance; } catch (beancreationException ex) {// уже обнаруженное исключение с правильным контекстом создания бобов уже ... бросить ex; } catch (неявно -приподнятаясинглетонексация ex) {// antallalstateException, которое должно быть передано в соответствии с дефолтом по умолчанию ... } catch (Throwable ex) {Throw New BeancreationException (mbdtouse.getResourceDescription (), Beanname, «Неожиданное исключение во время создания бобов», Ex); }} // рядом с DocReate, чтобы увидеть, как создаются бобы, защищенный объект DocReateBean (Final String Beanname, Final Rootbeandefinition MBD, Final Object [] args) {// создание магистратуры. // используется для удержания созданного бобового объекта BeanWrapper ancesswrapper = null; // Если это синглтон, сначала очистите бобы с одним и тем же именем в кэше if (mbd.issingleton ()) {exancewrapper = this.factorybeaninstancecache.remove (beanname); } // Это место для создания бобов, и это делается CreateBeanInstance if (exancewrapper == null) {// Создание нового экземпляра, основанного на указанном фасоли, использующем соответствующую стратегию, такой как: фабричный метод, автоматический инъекцию конструктора, простая инициализация exantywrapper = createbeaninstance (beanname, mbd, args); } final Object bean = (encoswrapper! = null? exancewrapper.getWrappedInstance (): null); Class <?> Beantype = (EncanceWrapper! = Null? Exancewrapper.getWrappedClass (): null); // Позволяйте пост-обработкам изменять определение объединенного бобов. Synchronized (mbd.postprocessinglock) {if (! mbd.postprocasted) {ApplymergedbeandefinitionPostProcessors (MBD, Beantype, Beanname); mbd.postprocased = true; }} // с нетерпением Qache Singletons, чтобы иметь возможность разрешить круговые ссылки // даже при запусках интерфейсами жизненного цикла, такими как BeanFactoryAware. // Необходимо ли разоблачить заранее: Singleton & Разрешить циклические зависимости и текущие бобы создается, обнаруживает циклические зависимости логические рано Ranoysingletonexposture = (mbd.issingleton () && this.allycircularReferences && issingletoncurryNcreation (beanname)); if (ransysingletonexposure) {if (logger.isdebugenabled ()) {logger.debug («с нетерпением Qaching Bean '" + beanname + "', чтобы разрешить разрешение потенциальных круговых ссылок"); } // Чтобы избежать зависимостей позднего цикла, объект, который создает экземпляр, может быть добавлен на завод до завершения инициализации бобов. Addsingletonfactory (beanname, new objectfactory <object> () {@override public Object getObject () Throws Beansexception {// Полагаться на ссылки на бобы снова, в основном используя SmartInstantialiationaware BeanpostProcessor, // AOP, который мы знаем здесь динамически, втягивает консультации в Bean. getearlybeanReference (beanname, mbd, bean); } // Инициализируйте экземпляр боба. // Это инициализация бобов, и здесь часто происходит инъекция зависимости. Этот ExposedObject сожалеет о возвращении в качестве боба после обработки инициализации. Object exposedObject = bean; Попробуйте {// поместить бобы и введите каждое значение атрибута. Среди них могут быть атрибуты, которые зависят от других бобов, фасоль зависимости будет инициализироваться рекурсивно заполненным (Beanname, MBD, ExtanceWrapper); if (exposedObject! = null) {// вызов метода инициализации, такой как init-method eSposedObject = initiazebean (beanname, exposedObject, mbd); }} catch (throwable ex) {if (ex exanceof beancreationexception && beanname.equals (((beancreationexception) ex) .getbeanname ())) {throw (beancreationexception) ex; } else {бросить новое beancreationException (mbd.getresourcedescription (), beanname, «Инициализация ошибки бобов», Ex); }} if (RanachsingleTonexPosture) {Object ranssingletonReference = getsingleton (beanname, false); // RanarsingletonReference не является пустым, только если круговая зависимость обнаруживается, если (RanachsingletonReference! = null) {if (exposedObject == bean) {// Если eSposedObject не изменяется в методе инициализации, это не усиливается exposedObject = ranssingletonReference; } else if (! this.AllowRawInectionDespiteWrapping && hasDependentBean (beanname)) {String [] getencebeans = getDependentBeans (beanname); SET <String> ActualDependentBeans = new LinkedHashSet <String> (upendentBeans.length); Для (строка зависит от: зависимых beans) {// зависимость обнаружения, если (! removesingletonifcreatedpecheCeceCeckonly (в зависимости)) {ActualDependentBeans.Add (Зависимый bean); }} // Поскольку бобы, от которых он зависит, после создания бобов должен был быть создан, фактические зависимые бобы не пусты, что означает, что бобы, от которых он зависит после создания текущего боба, не были созданы, то есть, существует циркулярная зависимость, если (! ActualDependenceBeans.isempty ()) {бросает новую BeancurrayincreationExceptexceptexceptexceptexceptexceptexceple (BeanNameMemanmeMeMemanmeMemanmeMemanseM.SemanseM.Sempty () {BeancurnlyCreationException (! был введен в другие бобы [« + stringUtils.collectiontocommadelimitedstring (ActualDependentBeans) +»] в своей необработанной версии как часть круговой ссылки, но в конечном итоге это было « +». Например, флаг «AlloweAgerinit» выключился. »); }}}}} // Зарегистрировать фасоль как одноразовый. try {// register bean на основе Scope RegisterDisposableBeanifNecessary (Beanname, Bean, MBD); } catch (beandefinitionValidationException ex) {бросить новое BeancreationException (mbd.getresourcedescription (), Beanname, «Инвалидные подписи разрушения», Ex); } вернуть eSposedObject; }Инъекция зависимости фактически включает в себя два основных процесса
Из вышесказанного мы видим, что методы, которые особенно тесно связаны с инъекцией зависимостей, включают
CreateBeanInstance
Генерировать Java -объекты, содержащиеся в бобах
Заполнение.
Обработка процесса обработки свойств различных объектов бобов (то есть процесс обработки зависимостей)
Давайте сначала посмотрим на исходный код CreateBeanInstance
/** * Создайте новый экземпляр для указанного боба, используя соответствующую стратегию экземпляра: * Фабричный метод, создание конструктора или простой экземпляр. * @param beanname Имя боба * @param mbd определение бобов для бобов * @param args явные аргументы, которые можно использовать для конструктора или заводского метода вызов * @return A BeanWrapper для нового экземпляра */ Защищенный BeanWrapper CreateBeanStance (String BeanNam Решено в этот момент. // Утвердите, что класс экземпляра Бин, который вы хотите создать, может быть созданным Class <?> Beanclass = ResulbeanClass (MBD, Beanname); if (beanclass! = null &&! modifier.ispublic (beanclass.getmodifiers ()) &&! mbd.isnonpublicaccessallowed ()) {throw new beancreationexception (mbd.getresourcedescription (), beanname, «Bean Class не public и не Public vioplic не разрешают:” + bean-reate (); } Поставщик <?> EntachPlier = mbd.getInstancesUpplier (); if (экземпляры! } // Если метод завода не является пустым, используйте стратегию заводского метода, чтобы создать магистр бобов if (mbd.getfactorymetorymethodname ()! = Null) {return amentaitiateusingFactoryMethod (BeanName, MBD, Args); } // ярлык при воссоздании одного и того же бобов ... логический разрешение = false; логический AutoWireneCessary = false; if (args == null) {synchronized (mbd.constructorargumentlock) {// класс имеет несколько конструкторов, каждый конструктор имеет разные параметры, поэтому перед вызовом вам необходимо заблокировать конструктор или соответствующий метод завода в соответствии с параметрами, если (mbd. -rosolvustructorfactorymetorymetorymothod! AutowIreneCessary = mbd.constructorargumentsResolved; }}} // Если он был проанализирован, используйте метод проанализированного конструктора, не блокируя снова, если (разрешение) {if (AutowIrenecessary) {// Конструктор автоматически впрыскивает AutowIreconstructor (Beanname, MBD, NULL, NULL); } else {// Конструктор с использованием конструктора по умолчанию для конструктора Instantiatebean (Beanname, MBD); }} // необходимо определить конструктор ... // создание компонента с использованием конструктора конструктора <?> [] Ctors = degineConstructorsFrombeanPostProcessors (Beanclass, Beanname); if (ctors! = null || mbd.getresolvedautowiRemode () == rootbeandefinition.autowire_constructor || mbd.hasconstructorargumentValues () ||! objectutils.isempty (args)) {вернуть autowireconstructor (beanname, mbd, ctors, args); } // Нет специальной обработки: просто используйте конструктор NO-Arg. // создание бобов с помощью его конструктора по умолчанию; } /*** создание заданного боба, используя его конструктор по умолчанию. * @param beanname Имя боба * @param mbd определение бобов для бобов * @return A BeanWrapper для нового экземпляра */// Самый распространенный инстантский защищенный бабочек Стратегия экземпляра по умолчанию - // cglibsubclassingInstantiationStrategy, то есть экземпляры боба с использованием CGlib. try {Object beaninStance; окончательный BeanFactory Parent = This; if (System.getSecurityManager() != null) { beanInstance = AccessController.doPrivileged(new PrivilegedAction<Object>() { @Override public Object run() { return getInstantiationStrategy().instantiate(mbd, beanName, parent); } }, getAccessControlContext()); } else {beaninStance = GetInstantiationStrategy (). instantiate (mbd, beanname, parent); } BeanWrapper bw = new BeanWrapperimpl (beaninStance); initbeanWrapper (BW); вернуть BW; } catch (Throwable ex) {Throw New BeancreationException (mbd.getresourcedescription (), Beanname, «Инстанция ошибки бобов», Ex); }}Cglib используется здесь для создания экземпляров бобов. Cglib - это библиотека классов для генераторов Bytecode, которая предоставляет ряд API для обеспечения функции генерации и преобразования Java Bytecode.
В Spring AOP Cglib также используется для улучшения байт -кода Java. В контейнерах МОК, чтобы понять, как использовать Cglib для генерации объектов бобов, вам нужно посмотреть на класс SimpleInstantiationStrategy. Это класс по умолчанию, используемый пружиной для генерации объектов бобов. Он предоставляет два метода экземпляра объектов бобов.
Public Class SimpleInstantiationStrategy реализует InstantiationStrategy {@Override Public Object Instantiate (rootbeandefinition BD, String Beanname, BeanFactory владелец) {// Не переопределяйте класс с помощью CGlib, если нет переопределения. if (bd.getmethodoverrides (). isempty ()) {// Здесь вы получаете указанный конструктор или метод завода для создания экземпляра Beanconstructor <?> ContructortOrouse; синхронизированный (bd.constructorargumentlock) {constructortouse = (Constructor <?>) Bd.ResolvedConstructorOrfactoryMethod; if (constructortouse == null) {final class <?> clazz = bd.getbeanclass (); if (clazz.isInterface ()) {бросить новый BeanInstantiationException (clazz, «Указанный класс - это интерфейс»); } try {if (System.getSecurityManager ()! = null) {constructortouse = accessController.doprivileged (new privielegedExceptionAction <constructor <? >> () {@Override public Constructor <?> Run () бросает исключение {return clazz.getDeclustructor ((] null); } else {constructortouse = clazz.getDeclaredConstructor ((class []) null); } bd.ReSolvedConstructororFactoryMethod = contructOrtortouse; } catch (Throwable ex) {бросить новый BeaninStantiationException (clazz, «не найден конструктор по умолчанию», Ex); }}} // экземпляры через Banuatils. Инстанция этого шагатура экстремирует бобов через конструктор. В Beanuatils вы можете увидеть конкретный вызов ctor.newinstance (args) return beanatils.instantiateclass (constructortortouse); } else {// создание объекта возвращает instantiateWithMethoDinection (bd, beanname, владелец); }}}Обработка зависимостей между бобами
Вход в обработку зависимостей - это метод заполнения, упомянутый выше. Поскольку существует слишком много аспектов, я не буду публиковать код здесь. Краткое введение в процесс обработки зависимостей: в методе населения,
Во -первых, установите значение свойства в Beandefinition, а затем запустите процесс впрыска зависимостей.
Во -первых, инъекция аутоирования может быть обработана, имени или байтипа, а затем атрибуты вводится.
Тогда вам нужно проанализировать ссылку на бобов. После разбора управляющего, управляющего, управления, ManageMap и т. Д. Вы подготовили условия для инъекции зависимостей. Именно здесь вы действительно установите объект бобов на другое свойство бобов, от которого он зависит, и обработанные свойства различны.
Инъекция зависимости происходит в SetPropertyValues BeanWrapper, но конкретное завершение осуществляется в BeanWrapper Subclass Beanwrapperimpl. Он завершит инъекцию стоимости имущества бобов, включая инъекцию массива, инъекцию классов сбора, таких как список, и инъекцию классов, не являющихся коллекциями.
После серии инъекций процесс инъекции зависимости различных свойств бобов завершен.
Во время процесса создания бобов и инъекции зависимости объектов необходимо рекурсивно заполнить инъекцию зависимостей на основе информации в борьбе.
Из предыдущих процессов рекурсии мы видим, что все эти рекурсии переносимы с GetBean.
Рекурсия заключается в том, чтобы найти необходимые бобы и создать рекурсивный призыв к бобам в контекстной системе;
Другая рекурсия заключается в том, чтобы вызвать метод GetBean в контейнере во время инъекции зависимостей, чтобы получить текущий фасоль бобов, а также запустить создание и инъекцию фасоли зависимостей.
При выполнении инъекции зависимостей на свойствах бобов процесс анализа также является рекурсивным процессом. Таким образом, в соответствии с зависимостью, создание и инъекция бобов завершаются слоем за слоем до тех пор, пока не будет завершено создание текущего боба. При создании этого бобов верхнего уровня и завершением его инъекции зависимости атрибутов это означает, что впрыск всей цепи зависимостей, связанной с текущими бобами, завершено.
После того, как создание бобов и впрыска зависимости завершено, в контейнере МОК устанавливается ряд бобов, связанных зависимостями. Этот фасоль больше не простой объект Java. После установки зависимости между сериями бобов и бобами ее можно использовать очень удобно для приложений верхнего уровня с помощью соответствующих методов интерфейса МОК.
2. Атрибут Lazy-Init и предварительный инстанс
В предыдущем методе обновления мы видим, что FinishedBeanfactoryInialization вызывается для обработки бобов, настроенных с Lazy-INIT.
Фактически, в этом методе инкапсулируется обработка атрибута Lazy-Init, и фактическая обработка выполняется в предпринимательском методе базового контейнера Default-ListableBeanfactory. Этот метод завершает предварительную инсставной синглтонской бобы, и это предварительное завершение завершено умно делегировано в контейнер для реализации. Если требуется предварительная инсталляция, то GetBean используется здесь для запуска введения зависимости. По сравнению с нормальными триггерами впрыска зависимостей, время запуска и случай только разные. Здесь инъекция зависимостей происходит во время процесса обновления контейнера, то есть во время процесса инициализации контейнера МОК, в отличие от обычной инъекции зависимости, когда контейнер запрашивает бобы в первый раз после того, как контейнер МОК инициализируется через GetBean.
Выше всего содержание этой статьи. Я надеюсь, что это будет полезно для каждого обучения, и я надеюсь, что все будут поддерживать Wulin.com больше.