В Проекти за разработка на Java , типичният работен процес включва рестартиране на сървъра с всяка промяна на класа и никой не се оплаква от това. Това е факт за разработката на Java. Ние работим така от първия ни ден с Java. Но дали презареждането на Java клас е толкова трудно за постигане? И може ли този проблем да бъде едновременно предизвикателен и вълнуващ за решаване опитни разработчици на Java ? В този урок за Java клас ще се опитам да се справя с проблема, да ви помогна да се възползвате от всички предимства на презареждането на клас в движение и да повишите вашата производителност неимоверно.
Презареждането на Java клас не се обсъжда често и има много малко документация, изследваща този процес. Тук съм, за да променя това. Този урок за уроци по Java ще предостави стъпка по стъпка обяснение на този процес и ще ви помогне да овладеете тази невероятна техника. Имайте предвид, че внедряването на презареждане на Java клас изисква много внимание, но научаването как да го направите ще ви постави в големите лиги, както като разработчик на Java, така и като софтуерен архитект. Също така няма да навреди да се разбере как да избегнем 10-те най-често срещани грешки в Java .
Целият изходен код за този урок се качва на GitHub тук .
За да стартирате кода, докато следвате този урок, ще ви трябва Мейвън , Отивам и или Затъмнение или IntelliJ IDEA .
mvn eclipse:eclipse
за генериране на проектни файлове на Eclipse.target/classes
.pom
файл.Alt+B E
run_example*.bat
. Задайте автоматично компилиране на компилатора на IntelliJ на вярно. След това, всеки път, когато промените който и да е java файл, IntelliJ ще го компилира автоматично.Първият пример ще ви даде общо разбиране за Java loader товара. Ето изходния код.
Като се има предвид следното User
определение на класа:
public static class User { public static int age = 10; }
Можем да направим следното:
public static void main(String[] args) { Class userClass1 = User.class; Class userClass2 = new DynamicClassLoader('target/classes') .load('qj.blog.classreloading.example1.StaticInt$User'); ...
В този пример на урок ще има две User
класове, заредени в паметта. userClass1
ще се зареди от зареждащия по подразбиране клас на JVM и userClass2
използвайки DynamicClassLoader
, потребителски зареждащ клас, чийто изходен код също е предоставен в проекта GitHub и който ще опиша подробно по-долу.
Ето и останалата част от main
метод:
out.println('Seems to be the same class:'); out.println(userClass1.getName()); out.println(userClass2.getName()); out.println(); out.println('But why there are 2 different class loaders:'); out.println(userClass1.getClassLoader()); out.println(userClass2.getClassLoader()); out.println(); User.age = 11; out.println('And different age values:'); out.println((int) ReflectUtil.getStaticFieldValue('age', userClass1)); out.println((int) ReflectUtil.getStaticFieldValue('age', userClass2)); }
И изходът:
Seems to be the same class: qj.blog.classreloading.example1.StaticInt$User qj.blog.classreloading.example1.StaticInt$User But why there are 2 different class loaders: [email protected] [email protected] And different age values: 11 10
Както можете да видите тук, въпреки че User
класовете имат едно и също име, те всъщност са два различни класа и те могат да бъдат управлявани и манипулирани независимо. Стойността на възрастта, макар и декларирана като статична, съществува в две версии, прикрепена отделно към всеки клас и може да бъде променяна и независимо.
В нормална Java програма, ClassLoader
е порталът, въвеждащ класове в JVM. Когато един клас изисква друг клас да бъде зареден, задачата ClassLoader
е да извърши зареждането.
Бутонът на Twitter не се показва като стилизиран
В този пример за Java клас обаче потребителският ClassLoader
с име DynamicClassLoader
се използва за зареждане на втората версия на User
клас. Ако вместо DynamicClassLoader
, трябваше отново да използваме зареждащия клас по подразбиране (с командата StaticInt.class.getClassLoader()
), тогава същото User
клас ще се използва, тъй като всички заредени класове се кешират.
DynamicClassLoader
В нормална програма на Java може да има няколко зареждащи класа. Този, който зарежда вашия основен клас, ClassLoader
, е този по подразбиране и от вашия код можете да създавате и използвате толкова много зареждащи класа, колкото искате. Това е ключът към презареждането на класа в Java. DynamicClassLoader
е може би най-важната част от целия урок, така че трябва да разберем как работи динамичното зареждане на класа, преди да можем да постигнем целта си.
За разлика от поведението по подразбиране на ClassLoader
, нашите DynamicClassLoader
наследява по-агресивна стратегия. Нормален loadloader ще даде на родителя си ClassLoader
приоритетните и само класове зареждане, които родителят му не може да зареди. Това е подходящо за нормални обстоятелства, но не и в нашия случай. Вместо това, DynamicClassLoader
ще се опита да прегледа всички пътища на неговия клас и да разреши целевия клас, преди той да се откаже от правото на своя родител.
В нашия пример по-горе, DynamicClassLoader
се създава само с един път на класа: 'target/classes'
(в текущата ни директория), така че е в състояние да зареди всички класове, които се намират на това място. За всички класове, които не са там, той ще трябва да се позовава на родителския loadloader. Например трябва да заредим String
клас в нашия StaticInt
клас, а нашият клас за зареждане няма достъп до rt.jar
в нашата папка JRE, така че String
клас на зареждащия клас родител ще бъде използван.
Следният код е от AggressiveClassLoader
, родителският клас на DynamicClassLoader
и показва къде е дефинирано това поведение.
byte[] newClassData = loadNewClass(name); if (newClassData != null) { loadedClasses.add(name); return loadClass(newClassData, name); } else { unavaiClasses.add(name); return parent.loadClass(name); }
Обърнете внимание на следните свойства на DynamicClassLoader
:
DynamicClassLoader
може да се събира боклук заедно с всичките му заредени класове и обекти.С възможността да заредим и използваме две версии от един и същи клас, сега мислим да изхвърлим старата версия и да заредим новата, за да я заменим. В следващия пример ще правим точно това ... непрекъснато.
Този следващ пример за Java ще ви покаже, че JRE може да зарежда и презарежда класове завинаги, като старите класове се изхвърлят и се събират боклуци, и чисто нов клас се зарежда от твърдия диск и се използва. Ето изходния код.
Ето основния цикъл:
public static void main(String[] args) { for (;;) { Class userClass = new DynamicClassLoader('target/classes') .load('qj.blog.classreloading.example2.ReloadingContinuously$User'); ReflectUtil.invokeStatic('hobby', userClass); ThreadUtil.sleep(2000); } }
На всеки две секунди, старата User
клас ще бъде изхвърлен, ще бъде зареден нов и неговият метод hobby
призовани.
Тук е User
определение на класа:
как да проверите дали имате изтичане на памет
@SuppressWarnings('UnusedDeclaration') public static class User { public static void hobby() { playFootball(); // will comment during runtime // playBasketball(); // will uncomment during runtime } // will comment during runtime public static void playFootball() { System.out.println('Play Football'); } // will uncomment during runtime // public static void playBasketball() { // System.out.println('Play Basketball'); // } }
Когато стартирате това приложение, трябва да се опитате да коментирате и да коментирате кода, посочен в User
клас. Ще видите, че винаги ще се използва най-новата дефиниция.
Ето някои примерни резултати:
... Play Football Play Football Play Football Play Basketball Play Basketball Play Basketball
Всеки път, когато се появи нов екземпляр на DynamicClassLoader
е създаден, той ще зареди User
клас от target/classes
папка, където сме настроили Eclipse или IntelliJ да извеждат най-новия клас файл. Всички стари DynamicClassLoader
и стари User
класовете ще бъдат прекратени и ще бъдат подложени на сметосъбирача.
Ако сте запознати с JVM HotSpot, тук трябва да се отбележи, че структурата на класовете също може да бъде променена и презаредена: playFootball
методът трябва да бъде премахнат и playBasketball
добавен метод. Това е различно от HotSpot, което позволява да се променя само съдържанието на метода или класът не може да се презареди.
Сега, когато сме в състояние да презаредим клас, е време да опитаме да презаредим много класове наведнъж. Нека изпробваме в следващия пример.
Резултатът от този пример ще бъде същият с пример 2, но ще покаже как да се приложи това поведение в по-подобна на приложение структура с обекти на контекст, услуга и модел. Изходният код на този пример е доста голям, така че тук съм показал само части от него. Пълният изходен код е тук .
пенсионни общности с нестопанска цел
Ето това е main
метод:
public static void main(String[] args) { for (;;) { Object context = createContext(); invokeHobbyService(context); ThreadUtil.sleep(2000); } }
И методът createContext
:
private static Object createContext() { Class contextClass = new DynamicClassLoader('target/classes') .load('qj.blog.classreloading.example3.ContextReloading$Context'); Object context = newInstance(contextClass); invoke('init', context); return context; }
Методът invokeHobbyService
:
private static void invokeHobbyService(Object context) { Object hobbyService = getFieldValue('hobbyService', context); invoke('hobby', hobbyService); }
И тук е Context
клас:
public static class Context { public HobbyService hobbyService = new HobbyService(); public void init() { // Init your services here hobbyService.user = new User(); } }
И HobbyService
клас:
public static class HobbyService { public User user; public void hobby() { user.hobby(); } }
Context
клас в този пример е много по-сложен от User
клас в предишните примери: има връзки към други класове и има init
метод, който да бъде извикан всеки, когато е създаден. По принцип е много подобен на контекстните класове на приложението в реалния свят (който проследява модулите на приложението и прави инжектиране на зависимост). Така че може да презаредите това Context
клас заедно с всички свързани класове е чудесна стъпка към прилагането на тази техника в реалния живот.
С нарастването на броя на класовете и обектите, нашата стъпка за „пускане на стари версии“ също ще се усложни. Това е и най-голямата причина, поради която презареждането на класа е толкова трудно. За евентуално отпадане на стари версии ще трябва да се уверим, че след като бъде създаден новият контекст, всичко препратките към старите класове и обекти отпадат. Как да се справим елегантно с това?
main
метод тук ще има задържане на обекта контекст и това е единствената връзка към всички неща, които трябва да отпаднат. Ако прекъснем тази връзка, обектът на контекста и класът на контекста и обектът на услугата ... ще бъдат подложени на събирача на боклук.
Малко обяснение защо нормално класовете са толкова упорити и не събират боклука:
С този пример виждаме, че презареждането на всички класове на приложения всъщност е доста лесно. Целта е просто да се запази тънка връзка с възможност за пускане от активната нишка към използвания динамичен зареждащ клас. Но какво, ако искаме някои обекти (и техните класове) да не да се презареди и да се използва повторно между циклите на презареждане? Нека разгледаме следващия пример.
main
метод:
public static void main(String[] args) { ConnectionPool pool = new ConnectionPool(); for (;;) { Object context = createContext(pool); invokeService(context); ThreadUtil.sleep(2000); } }
Така че можете да видите, че трикът тук е зареждането на ConnectionPool
клас и да го създаде екземпляр извън цикъла на презареждане, като го запази в постоянното пространство и предаде препратката към Context
обекти
createContext
методът също е малко по-различен:
private static Object createContext(ConnectionPool pool) { ExceptingClassLoader classLoader = new ExceptingClassLoader( (className) -> className.contains('.crossing.'), 'target/classes'); Class contextClass = classLoader.load('qj.blog.classreloading.example4.reloadable.Context'); Object context = newInstance(contextClass); setFieldValue(pool, 'pool', context); invoke('init', context); return context; }
Отсега нататък ще наричаме обектите и класовете, които се презареждат с всеки цикъл, „презареждаемото пространство“, а други - обектите и класовете, които не са рециклирани и не се подновяват по време на циклите на презареждане - „постоянно пространство“. Ще трябва да сме много ясни кои обекти или класове остават в кое пространство, като по този начин очертаваме разделителна линия между тези две пространства.
Както се вижда от снимката, не само Context
обект и UserService
обект, отнасящ се до ConnectionPool
обект, но Context
и UserService
класовете също се отнасят до ConnectionPool
клас. Това е много опасна ситуация, която често води до объркване и провал. ConnectionPool
класът не трябва да се зарежда от нашия DynamicClassLoader
, трябва да има само един ConnectionPool
клас в паметта, който е този, зареден по подразбиране ClassLoader
. Това е един пример за това защо е толкова важно да бъдете внимателни при проектирането на архитектура за презареждане на клас в Java.
Ами ако нашите DynamicClassLoader
случайно зарежда ConnectionPool
клас? Тогава ConnectionPool
обект от постоянното пространство не може да бъде предаден на Context
обект, защото Context
object очаква обект от различен клас, който също е наречен ConnectionPool
, но всъщност е различен клас!
как да проектирам API
И така, как да предотвратим нашите DynamicClassLoader
от зареждането на ConnectionPool
клас? Вместо да използва DynamicClassLoader
, този пример използва подклас от него, наречен: ExceptingClassLoader
, който ще предаде зареждането на super classloader въз основа на функция за състояние:
(className) -> className.contains('$Connection')
Ако не използваме ExceptingClassLoader
тук, тогава DynamicClassLoader
би заредил ConnectionPool
клас, защото този клас се намира в „target/classes
“ папка. Друг начин за предотвратяване на ConnectionPool
клас, вдигнат от нашия DynamicClassLoader
е да се компилира ConnectionPool
клас в различна папка, може би в различен модул, и тя ще бъде компилирана отделно.
Сега заданието за зареждане на Java клас става наистина объркващо. Как да определим кои класове трябва да са в постоянното пространство и кои класове в презареждаемото пространство? Ето правилата:
Context
клас се позовава на постоянния ConnectionPool
клас, но ConnectionPool
няма препратка към Context
StringUtils
може да се зареди веднъж в постоянното пространство и да се зареди отделно в презареждаемото пространство.Така че можете да видите, че правилата не са много ограничителни. С изключение на пресичащите класове, които имат обекти, посочени в двете пространства, всички останали класове могат да се използват свободно или в постоянното пространство, или в презареждаемото пространство, или и в двата. Разбира се, само класовете в презареждаемото пространство ще се радват да бъдат презареждани с цикли за презареждане.
Така че е решен най-трудният проблем с презареждането на класа. В следващия пример ще се опитаме да приложим тази техника към просто уеб приложение и ще се насладим на презареждане на класове Java точно като всеки скриптов език.
настройка на производителността в sql server 2012 стъпка по стъпка
Този пример ще бъде много подобен на това как трябва да изглежда нормално уеб приложение. Това е приложение с една страница с AngularJS, SQLite, Maven и Вграден уеб сървър на Jetty .
Ето мястото за презареждане в структурата на уеб сървъра:
Уеб сървърът няма да съдържа препратки към реалните сървлети, които трябва да останат в презареждаемото пространство, за да бъдат презаредени. Това, което държи, са сърблети за заглушаване, които при всяко извикване на неговия метод на обслужване ще разрешат действителния сървлет в действителния контекст да се изпълни.
Този пример също въвежда нов обект ReloadingWebContext
, който предоставя на уеб сървъра всички стойности като нормален контекст, но вътрешно съдържа препратки към действителен контекстен обект, който може да бъде презареден от DynamicClassLoader
. Това е това ReloadingWebContext
които предоставят сърблети за заглушаване на уеб сървъра.
ReloadingWebContext
ще бъде обвивката на действителния контекст и:
Тъй като е много важно да разберем как изолираме постоянното пространство и презареждаемото пространство, ето двата класа, които се пресичат между двете пространства:
Клас qj.util.funct.F0
за обект public F0 connF
в Context
DynamicClassLoader
. Клас java.sql.Connection
за обект public F0 connF
в Context
DynamicClassLoader
клас, така че няма да бъде взет.В този урок за Java класове видяхме как да презаредим един клас, да презаредим непрекъснато един клас, да презаредим цяло пространство от множество класове и да презаредим множество класове отделно от класовете, които трябва да се запазят. С тези инструменти ключовият фактор за постигане на надеждно презареждане на класа е да има супер изчистен дизайн. След това можете свободно да манипулирате класовете си и цялата JVM.
Внедряването на презареждане на Java клас не е най-лесното нещо в света. Но ако опитате и в даден момент откриете, че класовете ви се зареждат в движение, значи вече сте почти там. Ще остане много малко да се направи, преди да можете да постигнете напълно превъзходен изчистен дизайн на вашата система.
Успех мои приятели и се насладете на вашата новооткрита суперсила!