JavaRush /Java блогу /Random-KY /Качан болсо да сериалдаштыруу. 1-бөлүк
articles
Деңгээл

Качан болсо да сериалдаштыруу. 1-бөлүк

Группада жарыяланган
Бир караганда, сериалдаштыруу анча деле маанилүү эместей сезилет. Чынында, эмне жөнөкөй болушу мүмкүн? Интерфейсти ишке ашыруу үчүн класс жарыялады java.io.Serializable- жана бул. Сиз классты көйгөйсүз сериялай аласыз. Качан болсо да сериалдаштыруу.  1-1-бөлүкТеориялык жактан алганда, бул чындык. Практикада бир топ кылдаттыктар бар. Алар аткарууга, сериядан чыгарууга, класстын коопсуздугуна байланыштуу. Жана дагы көптөгөн аспектилери менен. Мындай кылдаттыктар талкууланат. Бул макаланы төмөнкү бөлүктөргө бөлүүгө болот:
  • Механизмдердин кылдаттыктары
  • Бул эмне үчүн керек?Externalizable
  • Performance
  • бирок экинчи жагынан
  • Маалымат коопсуздугу
  • Объектти сериялаштырууSingleton
Биринчи бөлүккө өтөбүз -

Механизмдердин кылдаттыктары

Биринчиден, тез суроо. Объектти сериялаштыруунун канча жолу бар? Практика көрсөткөндөй, иштеп чыгуучулардын 90% дан ашыгы бул суроого болжол менен бирдей жооп беришет (формулировкага чейин) - бир гана жол бар. Ошол эле учурда алардын экөөсү бар. Экинчисин баары эле эстей бербейт, анын өзгөчөлүктөрү жөнүндө түшүнүктүү бир нерсе айтпайлы. Демек, бул ыкмалар кандай? Биринчисин баары эстейт. Бул буга чейин айтылган ишке ашыруу java.io.Serializableжана эч кандай күч-аракетти талап кылbyte. Экинчи ыкма ошондой эле интерфейстин ишке ашырылышы, бирок башкасы: java.io.Externalizable. Айырмаланып java.io.Serializable, ал ишке ашырылышы керек болгон эки ыкманы камтыйт - writeExternal(ObjectOutput)жана readExternal(ObjectInput). Бул ыкмалар сериялаштыруу/сериялаштыруу логикасын камтыйт. Комментарий.Төмөндө мен кээде стандарттуу ишке ашыруу менен сериялаштырууга жана кеңейтилген ишке ашырууга Serializableкайрылам . ExternalizableБашкакомментарий. readObjectМен атайылап азыр аныктоо жана сыяктуу стандарттуу сериялаштыруу башкаруу параметрлерине тийбейм writeObject, анткени Мен бул ыкмалар бир аз туура эмес деп ойлойм. Бул ыкмалар интерфейсте аныкталбаган Serializableжана чындыгында, чектөөлөрдү айланып иштөө жана стандарттуу сериалдаштырууну ийкемдүү кылуу үчүн негиз болуп саналат. ExternalizableИйкемдүүлүктү камсыз кылган ыкмалар аларга башынан эле орнотулган . Дагы бир суроо берели. стандарттык сериялаштыруу чындыгында кантип иштейт java.io.Serializable? Жана ал Reflection API аркылуу иштейт. Ошол. класс талаалардын жыйындысы катары талданат, алардын ар бири чыгуу агымына жазылган. Бул операция аткаруу жагынан оптималдуу эмес экени анык деп ойлойм. Так канча экенин кийин билебиз. Жогоруда айтылган эки сериялаштыруу ыкмаларынын ортосунда дагы бир негизги айырма бар. Тактап айтканда, сериядан чыгаруу механизминде. Колдонулганда, Serializableсериядан чыгаруу төмөнкүдөй болот: эстутум an object үчүн бөлүнөт, андан кийин анын талаалары агымдагы маанилер менен толтурулат. Объекттин конструктору чакырылbyte. Бул жерде бул жагдайды өзүнчө карап чыгышыбыз керек. Макул, биздин класс серияланса болот. Анан анын ата-энеси? Толугу менен милдеттүү! Андан тышкары, эгер сиз классты мурастап алсаңыз Object- ата-эне, албетте, сериялаштырылbyte. Биз талаалар жөнүндө эч нерсе билбесек да Object, алар биздин ата-энелер класстарыбызда болушу мүмкүн. Алар эмне болот? Алар сериалдаштыруу агымына кирбейт. Сериялаштырууда алар кандай баалуулуктарды алышат? Бул мисалды карап көрөлү:
package ru.skipy.tests.io;

import java.io.*;

/**
 * ParentDeserializationTest
 *
 * @author Eugene Matyushkin aka Skipy
 * @since 05.08.2010
 */
public class ParentDeserializationTest {

    public static void main(String[] args){
        try {
            System.out.println("Creating...");
            Child c = new Child(1);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            c.field = 10;
            System.out.println("Serializing...");
            oos.writeObject(c);
            oos.flush();
            baos.flush();
            oos.close();
            baos.close();
            ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bais);
            System.out.println("Deserializing...");
            Child c1 = (Child)ois.readObject();
            System.out.println("c1.i="+c1.getI());
            System.out.println("c1.field="+c1.getField());
        } catch (IOException ex){
            ex.printStackTrace();
        } catch (ClassNotFoundException ex){
            ex.printStackTrace();
        }
    }

    public static class Parent {
        protected int field;
        protected Parent(){
            field = 5;
            System.out.println("Parent::Constructor");
        }
        public int getField() {
            return field;
        }
    }

    public static class Child extends Parent implements Serializable{
        protected int i;
        public Child(int i){
            this.i = i;
            System.out.println("Child::Constructor");
        }
        public int getI() {
            return i;
        }
    }
}
Бул ачык-айкын - бизде сериялаштырылбаган ата-эне класс жана сериялаштырылуучу бала класс бар. Жана бул эмне болот:
Creating...
Parent::Constructor
Child::Constructor
Serializing...
Deserializing...
Parent::Constructor
c1.i=1
c1.field=5
Башкача айтканда, сериядан чыгаруу учурунда ата-энелик NON-сериялаштырылбаган класстын параметрлери жок конструктор деп аталат . Ал эми мындай конструктор жок болсо, сериядан чыгаруу учурунда ката пайда болот. Биз сериядан чыгарып жаткан бала an objectинин конструктору, жогоруда айтылгандай, чакырылbyte. Стандарттык механизмдер колдонулганда ушундай болот Serializable. Аны колдонууда Externalizableабал башкача. Биринчиден, параметрлери жок конструктор чакырылат, андан кийин түзүлгөн an objectте readExternal методу чакырылат, ал иш жүзүндө анын бардык маалыматтарын окуйт. Демек, Externalizable интерфейсин ишке ашырган ар кандай класста параметрлери жок коомдук конструктор болушу керек! Мындан тышкары, мындай класстын бардык урпактары да интерфейсти ишке ашыруу үчүн карала тургандыктан Externalizable, аларда параметрсиз конструктор да болушу керек! Андан ары кетели. сыяктуу талаа өзгөрткүчү бар transient. Бул талааны сериялаштырбоо керек дегенди билдирет. Бирок, өзүңүз түшүнгөндөй, бул нускама стандарттуу сериялоо механизмине гана таасир этет. Колдонулганда, Externalizableбул талааны сериялаштыруу, ошондой эле аны алып салуу үчүн эч ким убара болбойт. Эгерде талаа убактылуу деп жарыя кылынса, анда an object сериядан чыгарылганда, ал демейки маанини алат. Дагы бир кылдат жагдай. Стандарттык сериялаштыруу менен, өзгөрткүчү бар талаалар staticсериялаштырылbyte. Демек, сериядан ажыратылгандан кийин бул талаа өз маанисин өзгөртпөйт. Албетте, ишке ашыруу учурунда, Externalizableэч ким бул талааны сериялаштырууга жана сериядан чыгарууга убара болбойт, бирок мен муну жасабоону сунуштайм, анткени бул тымызын каталарга алып келиши мүмкүн. Модификатору бар талаалар finalкадимкидей катарлаштырылган. Бир гана кошпогондо - Externalizable колдонууда аларды сериядан чыгарууга болбойт. Анткени final-поляалар конструктордо инициализацияланышы керек жана андан кийин бул талаанын маанисин readExternalде өзгөртүү мүмкүн болбой калат. Демек, сиз -талаасы бар an objectти сериялаштыруу керек болсо final, стандарттуу сериялаштыруу гана колдонушуңуз керек болот. Көптөр билбеген дагы бир жагдай. Стандарттык сериялаштыруу класста талаалардын жарыялоо тартибин эске алат. Кандай болгон күндө да, бул мурунку versionларда болгон; Oracle ишке ашыруунун JVM 1.6 versionсында тартип мындан ары маанилүү эмес, талаанын түрү жана аталышы маанилүү. Методдордун курамы, талаалар жалпысынан ошол эле бойдон калышы мүмкүн экендигине карабастан, стандарттык механизмге таасир этиши мүмкүн. Мунун алдын алуу үчүн төмөнкү механизм бар. Интерфейсти ишке ашырган ар бир класска Serializableкомпиляция этабында дагы бир талаа кошулат -private static final long serialVersionUID. Бул талаа серияланган класстын уникалдуу version идентификаторун камтыйт. Ал класстын мазмуну боюнча эсептелет - талаалар, алардын жарыялоо тартиби, ыкмалары, алардын жарыялоо тартиби. Демек, класстын ар кандай өзгөрүшү менен бул талаа анын маанисин өзгөртөт. Бул талаа класс серияланганда агымга жазылат. Айтмакчы, бул, балким, мага белгилүү болгон бирден-бир учур static-талаа серияланган. Сериялаштыруу учурунда бул талаанын мааниси виртуалдык машинадагы класс менен салыштырылат. Эгерде баалуулуктар дал келбесе, анда мындай өзгөчөлүк ташталат:
java.io.InvalidClassException: test.ser2.ChildExt;
    local class incompatible: stream classdesc serialVersionUID = 8218484765288926197,
                                   local class serialVersionUID = 1465687698753363969
Бирок, бул текшерүүнү кыйгап өтпөстөн, алдап кетүүнүн жолу бар. Бул класс талааларынын жыйындысы жана алардын тартиби мурунтан эле аныкталган болсо, пайдалуу болушу мүмкүн, бирок класстын ыкмалары өзгөрүшү мүмкүн. Бул учурда, сериалдаштыруу коркунучта эмес, бирок стандарттык механизм өзгөртүлгөн класстын byte codeун колдонуу менен маалыматтарды сериядан чыгарууга жол бербейт. Бирок, мен айткандай, алдап кетиши мүмкүн. Тактап айтканда, класстагы талааны кол менен аныктаңыз private static final long serialVersionUID. Негизи, бул талаанын мааниси таптакыр эч нерсе болушу мүмкүн. Кээ бир адамдар аны code өзгөртүлгөн күнгө барабар коюуну каалашат. Кээ бирлери 1 л колдонушат. Стандарттык маанини алуу үчүн (ичке эсептелген), SDK камтылган сериялык программаны колдоно аласыз. Ушундай жол менен аныкталгандан кийин, талаанын мааниси белгиленет, демек, сериядан чыгарууга ар дайым уруксат берилет. Мындан тышкары, 5.0 versionсында documentацияда болжол менен төмөндөгүлөр пайда болду: бардык сериялаштырылуучу класстар бул талааны ачык жарыялоосу сунушталат, анткени демейки эсептөө класс түзүмүнүн деталдарына өтө сезгич, ал компилятордун ишке ашырылышына жараша өзгөрүшү мүмкүн, Ошентип, күтүлбөгөн InvalidClassExceptionкесепеттерге алып келет. Бул талаа деп жарыялоо жакшы private, анткени ал жарыяланган класска гана тиешелүү. Модификатор спецификацияда көрсөтүлгөн эмес да. Эми ушул жагын карап көрөлү. Бизде бул класс структурасы бар дейли:
public class A{
    public int iPublic;
    protected int iProtected;
    int iPackage;
    private int iPrivate;
}

public class B extends A implements Serializable{}
Башкача айтканда, бизде серияланбаган ата-энеден мураска алынган класс бар. Бул классты сериялаштыруу мүмкүнбү жана бул үчүн эмне керек? Ата-эне класстын өзгөрмөлөрү эмне болот? Жооп бул. Ооба, Bсиз класстын үлгүсүн сериялай аласыз. Бул үчүн эмне керек? Бирок класста Aпараметрлери жок конструктор болушу керек, publicже protected. Андан кийин, сериядан чыгаруу учурунда, бардык класс өзгөрмөлөрү Aушул конструктордун жардамы менен инициализацияланат. Класс өзгөрмөлөрү Bсерияланган маалымат агымынын маанилери менен инициализацияланат. BТеориялык жактан алганда, класста мен башында айтып өткөн ыкмаларды аныктоого болот - readObjectжана writeObject, - анын башында класс өзгөрмөлөрүн , андан кийин (де-) сериялаштыруу Bаркылуу жеткorктүү өзгөрмөлөрдү сериялаштыруу (де-) in.defaultReadObject/out.defaultWriteObjectкласстан A(биздин учурда булар iPublic, iProtectedжана iPackage, эгерде Bал бир пакетте болсо A). Бирок, менин оюмча, бул үчүн кеңейтилген сериализацияны колдонуу жакшы. Мен токтолгум келген кийинки жагдай - бул бир нече an objectтерди сериялаштыруу. Бизде төмөнкү класс структурасы бар дейли:
public class A implements Serializable{
    private C c;
    private B b;
    public void setC(C c) {this.c = c;}
    public void setB(B b) {this.b = b;}
    public C getC() {return c;}
    public B getB() {return b;}
}
public class B implements Serializable{
    private C c;
    public void setC(C c) {this.c = c;}
    public C getC() {return c;}
}
public class C implements Serializable{
    private A a;
    private B b;
    public void setA(A a) {this.a = a;}
    public void setB(B b) {this.b = b;}
    public B getB() {return b;}
    public A getA() {return a;}
}
Качан болсо да сериалдаштыруу.  1-2-бөлүкЭгер класстын бир мисалын сериялаштырсаңыз эмне болот A? Ал класстын экземплярын сүйрөп барат , ал өз кезегинде бардыгы башталган инстанцияга шилтемеси бар Bинстанцияны бойлой сүйрөйт . Туура эмес айлана жана чексиз рекурсия? Бактыга жараша, жок. Төмөнкү сыноо codeун карап көрөлү: CA
// initiaizing
A a = new A();
B b = new B();
C c = new C();
// setting references
a.setB(b);
a.setC(c);
b.setC(c);
c.setA(a);
c.setB(b);
// serializing
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(a);
oos.writeObject(b);
oos.writeObject(c);
oos.flush();
oos.close();
// deserializing
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
A a1 = (A)ois.readObject();
B b1 = (B)ois.readObject();
C c1 = (C)ois.readObject();
// testing
System.out.println("a==a1: "+(a==a1));
System.out.println("b==b1: "+(b==b1));
System.out.println("c==c1: "+(c==c1));
System.out.println("a1.getB()==b1: "+(a1.getB()==b1));
System.out.println("a1.getC()==c1: "+(a1.getC()==c1));
System.out.println("b1.getC()==c1: "+(b1.getC()==c1));
System.out.println("c1.getA()==a1: "+(c1.getA()==a1));
System.out.println("c1.getB()==b1: "+(c1.getB()==b1));
Биз эмне кылып жатабыз? Биз класстардын үлгүсүн түзөбүз Aжана Bаларга Cбири-бирине шилтемелерди берип, анан алардын ар бирин сериялаштырабыз. Андан кийин биз аларды сериядан чыгарып, бир катар текшерүүлөрдү жүргүзөбүз. Натыйжада эмне болот:
a==a1: false
b==b1: false
c==c1: false
a1.getB()==b1: true
a1.getC()==c1: true
b1.getC()==c1: true
c1.getA()==a1: true
c1.getB()==b1: true
Анда бул сыноодон эмнеге үйрөнө аласың? Алгачкы. Сериялаштыруудан кийинки an object шилтемелери ага чейинки шилтемелерден айырмаланат. Башкача айтканда, сериялаштыруу/сериялаштыруу учурунда an object көчүрүлгөн. Бул ыкма кээде an objectтерди клондоо үчүн колдонулат. Экинчи корутунду кыйла маанилүү. Кайчылаш шилтемелери бар бир нече an objectтерди сериялаштыруу/сериядан чыгарууда, ал шилтемелер сериядан ажыратылгандан кийин күчүндө калат. Башкача айтканда, сериялаштырууга чейин алар бир an objectти көрсөтсө, сериядан ажыраткандан кийин алар дагы бир an objectти көрсөтөт. Муну ырастоо үчүн дагы бир кичинекей тест:
B b = new B();
C c = new C();
b.setC(c);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(b);
oos.writeObject(c);
oos.writeObject(c);
oos.writeObject(c);
oos.flush();
oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
B b1 = (B)ois.readObject();
C c1 = (C)ois.readObject();
C c2 = (C)ois.readObject();
C c3 = (C)ois.readObject();
System.out.println("b1.getC()==c1: "+(b1.getC()==c1));
System.out.println("c1==c2: "+(c1==c2));
System.out.println("c1==c3: "+(c1==c3));
Класс an objectисинде Bкласс an objectисине шилтеме бар C. Сериялаштырылганда, bал класстын инстанциясы менен катар серияланат С, андан кийин ошол эле c инстанциясы үч жолу серияланат. Сериялаштыруудан кийин эмне болот?
b1.getC()==c1: true
c1==c2: true
c1==c3: true
Көрүнүп тургандай, сериядан ажыратылган төрт an object тең бир an objectти билдирет - ага шилтемелер бирдей. Так сериализацияга чейинкидей. Дагы бир кызыктуу жагдай - биз бир эле учурда ишке ашырсак эмне болот Externalizableжана Serializable? Бул суроодогудай - пил менен кит - ким кимди жеңет? Жеңет Externalizable. Сериялаштыруу механизми адегенде анын бар-жоктугун текшерет, андан кийин гана анын бар-жоктугун текшерет.Ошондуктан, ишке ашырган SerializableВ классы SerializableА классынан мураска алса Externalizable, В классынын талаалары сериялаштырылbyte. Акыркы пункт - мурас. ишке ашырган класстан мурастоодо Serializable, эч кандай кошумча аракеттердин кереги жок. Сериялаштыруу балдар классына да жайылтылат. -ды ишке ашырган класстан мураска алууда Externalizable, сиз ата-эне класстын readExternal жана writeExternal ыкмаларын жокко чыгарышыңыз керек. Болбосо, балдар классынын талаалары серияланbyte. Бул учурда, сиз ата-эне ыкмаларын чакырууну унутпашыңыз керек, антпесе ата-эне талаалар серияланbyte. * * * Биз, балким, майда-чүйдөсүнө чейин бүтүргөн. Бирок, биз козгой элек глобалдык мүнөздөгү бир маселе бар. Тактап айтканда -

Эмне үчүн сизге Externalizable керек?

Эмне үчүн бизге өнүккөн сериялаштыруу керек? Жооп жөнөкөй. Биринчиден, ал көбүрөөк ийкемдүүлүктү берет. Экинчиден, ал көп учурда серияланган маалыматтардын көлөмү жагынан олуттуу кирешелерди камсыз кыла алат. Үчүнчүдөн, аткаруу сыяктуу аспект бар, ал жөнүндө төмөндө сөз болот . Ийкемдүүлүк менен баары түшүнүктүү көрүнөт. Чынында эле, биз сериялаштыруу жана сериялаштыруу процесстерин каалагандай башкара алабыз, бул бизди класстагы ар кандай өзгөрүүлөрдөн көз карандысыз кылат (мен жогоруда айткандай, класстагы өзгөрүүлөр сериядан чыгарууга чоң таасирин тийгизиши мүмкүн). Ошондуктан, мен көлөмдөгү пайда жөнүндө бир нече сөз айткым келет. Бизде төмөнкү класс бар дейли:
public class DateAndTime{

  private short year;
  private byte month;
  private byte day;
  private byte hours;
  private byte minutes;
  private byte seconds;

}
Калганы маанилүү эмес. Талаалар int түрүнөн жасалышы мүмкүн, бирок бул мисалдын таасирин гана күчөтөт. Чындыгында талаалар intаткаруунун себептеринен улам терorши мүмкүн. Кандай болгон күндө да кеп ачык. Класс датаны жана убакытты билдирет. Бизге биринчи кезекте сериялаштыруу жагынан кызык. Балким, эң оңой нерсе бул жөнөкөй убакыт белгисин сактоо. Бул узун типте, б.а. серияланганда ал 8 byte алат. Мындан тышкары, бул ыкма компоненттерди бир мааниге жана кайра айландыруу ыкмаларын талап кылат, б.а. - өндүрүмдүүлүктү жоготуу. Бул ыкманын артыкчылыгы - 64 битке туура келе турган толугу менен жинди дата. Бул коопсуздуктун чоң маржасы, көбүнчө иш жүзүндө кереги жок. Жогоруда берилген класс 2 + 5*1 = 7 byte алат. Класс жана 6 талаа үчүн кошумча чыгымдар. Бул маалыматтарды кысуу үчүн кандайдыр бир жол барбы? Так. Секунд жана мүнөттөр 0-59 диапазонунда, б.а. аларды көрсөтүү үчүн 8 биттин ордуна 6 бит жетиштүү. Саат – 0-23 (5 бит), күн – 0-30 (5 бит), айлар – 0-11 (4 бит). Бардыгы болуп, жылды эске албаганда бардыгы - 26 бит. int өлчөмүнө дагы 6 бит калды. Теориялык жактан алганда, кээ бир учурларда бул бир жыл үчүн жетиштүү болушу мүмкүн. Болбосо, башка byte кошуу маалымат талаасынын көлөмүн 14 битке чейин көбөйтөт, бул 0-16383 диапазонун берет. Бул реалдуу колдонмолордо жетиштүү. Жалпысынан биз керектүү маалыматты сактоо үчүн зарыл болгон маалыматтардын көлөмүн 5 byteка чейин кыскарттык. 4 чейин жок болсо. Кемчorги мурунку учурда эле - пакеттелген датаны сактаса, анда конversion ыкмалары керек. Бирок мен муну мындай кылгым келет: аны өзүнчө талааларда сактап, пакеттелген түрдө сериялаштыруу. Бул жерде колдонуунун мааниси бар Externalizable:
// data is packed into 5 bytes:
//  3         2         1
// 10987654321098765432109876543210
// hhhhhmmmmmmssssssdddddMMMMyyyyyy yyyyyyyy
public void writeExternal(ObjectOutput out){
    int packed = 0;
    packed += ((int)hours) << 27;
    packed += ((int)minutes) << 21;
    packed += ((int)seconds) << 15;
    packed += ((int)day) << 10;
    packed += ((int)month) << 6;
    packed += (((int)year) >> 8) & 0x3F;
    out.writeInt(packed);
    out.writeByte((byte)year);
}

public void readExternal(ObjectInput in){
    int packed = in.readInt();
    year = in.readByte() & 0xFF;
    year += (packed & 0x3F) << 8;
    month = (packed >> 6) & 0x0F;
    day = (packed >> 10) & 0x1F;
    seconds = (packed >> 15) & 0x3F;
    minutes = (packed >> 21) & 0x3F;
    hours = (packed >> 27);
}
Чындыгында, баары ушунда. Сериялаштыруудан кийин биз класска кошумча чыгымдарды, эки талааны (6 ордуна) жана 5 byte маалыматтарды алабыз. Кайсы бир кыйла жакшыраак. Андан ары таңгак атайын китепканаларга калтырылышы мүмкүн. Келтирилген мисал абдан жөнөкөй. Анын негизги максаты - өнүккөн сериализацияны кантип колдонсо болорун көрсөтүү. Менин оюмча, серияланган маалыматтардын көлөмүндөгү мүмкүн болгон пайда негизги артыкчылыктан алыс болсо да. Негизги артыкчылыгы, ийкемдүүлүктөн тышкары... (кийинки бөлүмгө жылмакай өтүңүз...) Булакка шилтеме: Сериялаштыруу
Комментарийлер
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION