고급 자바 클래스 튜토리얼: 클래스 재로딩 가이드
게시 됨: 2022-03-11Java 개발 프로젝트에서 일반적인 워크플로는 클래스가 변경될 때마다 서버를 다시 시작해야 하며 아무도 이에 대해 불평하지 않습니다. 이것이 자바 개발에 관한 사실입니다. 우리는 Java를 사용한 첫날부터 그렇게 일했습니다. 그러나 Java 클래스를 다시 로드하는 것이 그렇게 어렵습니까? 그리고 그 문제가 숙련된 Java 개발자가 해결하기에 도전적이고 흥미진진할 수 있습니까? 이 Java 클래스 자습서에서는 문제를 해결하고 즉시 클래스 다시 로드의 모든 이점을 얻고 생산성을 크게 향상시키도록 노력할 것입니다.
Java 클래스 재로딩은 자주 논의되지 않으며 이 프로세스를 탐색하는 문서가 거의 없습니다. 나는 그것을 바꾸러 왔다. 이 Java 클래스 자습서는 이 프로세스에 대한 단계별 설명을 제공하고 이 놀라운 기술을 마스터하는 데 도움이 됩니다. Java 클래스 재로딩을 구현하려면 상당한 주의가 필요하지만 이를 수행하는 방법을 배우면 Java 개발자 및 소프트웨어 설계자로서 큰 리그에 진출할 수 있습니다. 또한 가장 흔한 10가지 Java 실수를 피하는 방법을 이해하는 것도 나쁘지 않을 것입니다.
작업 공간 설정
이 튜토리얼의 모든 소스 코드는 여기 GitHub에 업로드되어 있습니다.
이 자습서를 따르는 동안 코드를 실행하려면 Maven, Git 및 Eclipse 또는 IntelliJ IDEA가 필요합니다.
Eclipse를 사용하는 경우:
-
mvn eclipse:eclipse
명령을 실행하여 Eclipse의 프로젝트 파일을 생성합니다. - 생성된 프로젝트를 로드합니다.
- 출력 경로를
target/classes
로 설정합니다.
IntelliJ를 사용하는 경우:
- 프로젝트의
pom
파일을 가져옵니다. - IntelliJ는 예제를 실행할 때 자동 컴파일되지 않으므로 다음 중 하나를 수행해야 합니다.
- IntelliJ 내에서 예제를 실행한 다음 컴파일할 때마다
Alt+BE
를 눌러야 합니다. -
run_example*.bat
를 사용하여 IntelliJ 외부에서 예제를 실행합니다. IntelliJ의 컴파일러 자동 컴파일을 true로 설정합니다. 그런 다음 Java 파일을 변경할 때마다 IntelliJ가 자동 컴파일합니다.
예 1: Java 클래스 로더를 사용하여 클래스 다시 로드
첫 번째 예는 Java 클래스 로더에 대한 일반적인 이해를 제공합니다. 다음은 소스 코드입니다.
다음 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
는 소스 코드가 GitHub 프로젝트에서도 제공되는 사용자 정의 클래스 로더인 DynamicClassLoader
를 사용하여 로드되며 이에 대해서는 아래에서 자세히 설명합니다.
나머지 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: qj.util.lang.DynamicClassLoader@3941a79c sun.misc.Launcher$AppClassLoader@1f32e575 And different age values: 11 10
여기에서 볼 수 있듯이 User
클래스의 이름은 같지만 실제로는 서로 다른 두 클래스이며 독립적으로 관리 및 조작할 수 있습니다. age 값은 비록 static으로 선언되어 있지만 두 가지 버전으로 존재하며, 각 클래스에 개별적으로 연결되며 독립적으로 변경될 수도 있습니다.
일반 Java 프로그램에서 ClassLoader
는 클래스를 JVM으로 가져오는 포털입니다. 한 클래스가 다른 클래스를 로드해야 하는 경우 로드를 수행하는 것은 ClassLoader
의 작업입니다.
그러나 이 Java 클래스 예제에서는 DynamicClassLoader
라는 사용자 정의 ClassLoader
를 사용하여 두 번째 버전의 User
클래스를 로드합니다. DynamicClassLoader
대신 기본 클래스 로더를 다시 사용하는 경우( StaticInt.class.getClassLoader()
명령으로) 로드된 모든 클래스가 캐시되므로 동일한 User
클래스가 사용됩니다.
DynamicClassLoader
일반 Java 프로그램에는 여러 클래스 로더가 있을 수 있습니다. 기본 클래스인 ClassLoader
를 로드하는 것이 기본 클래스이며 코드에서 원하는 만큼 클래스로더를 만들고 사용할 수 있습니다. 이것이 자바에서 클래스 재로딩의 핵심이다. DynamicClassLoader
는 아마도 이 전체 자습서에서 가장 중요한 부분일 수 있으므로 목표를 달성하기 전에 동적 클래스 로딩이 작동하는 방식을 이해해야 합니다.
ClassLoader
의 기본 동작과 달리 DynamicClassLoader
는 보다 적극적인 전략을 상속합니다. 일반 클래스 로더는 상위 클래스 로더에게 우선 순위를 부여하고 상위 ClassLoader
가 로드할 수 없는 클래스만 로드합니다. 그것은 정상적인 상황에 적합하지만 우리의 경우에는 적합하지 않습니다. 대신 DynamicClassLoader
는 모든 클래스 경로를 살펴보고 부모에 대한 권한을 포기하기 전에 대상 클래스를 해결하려고 합니다.
위의 예에서 DynamicClassLoader
는 "target/classes"
(현재 디렉토리에 있음)라는 하나의 클래스 경로만 사용하여 생성되므로 해당 위치에 있는 모든 클래스를 로드할 수 있습니다. 거기에 없는 모든 클래스의 경우 상위 클래스 로더를 참조해야 합니다. 예를 들어 StaticInt
클래스에서 String
클래스를 로드해야 하고 클래스 로더는 JRE 폴더의 rt.jar
에 액세스할 수 없으므로 상위 클래스 로더의 String
클래스가 사용됩니다.
다음 코드는 DynamicClassLoader
의 상위 클래스인 AggressiveClassLoader
에서 가져온 것이며 이 동작이 정의된 위치를 보여줍니다.
byte[] newClassData = loadNewClass(name); if (newClassData != null) { loadedClasses.add(name); return loadClass(newClassData, name); } else { unavaiClasses.add(name); return parent.loadClass(name); }
DynamicClassLoader
의 다음 속성에 유의하십시오.
- 로드된 클래스는 기본 클래스 로더에 의해 로드된 다른 클래스와 동일한 성능 및 기타 속성을 갖습니다.
-
DynamicClassLoader
는 로드된 모든 클래스 및 개체와 함께 가비지 수집될 수 있습니다.
동일한 클래스의 두 가지 버전을 로드하고 사용할 수 있는 기능을 통해 이제 이전 버전을 버리고 새 버전을 로드하여 교체하는 방법을 생각하고 있습니다. 다음 예에서 우리는 계속해서 그렇게 할 것입니다.
예제 2: 클래스를 지속적으로 다시 로드
이 다음 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); } }
2초마다 이전 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
의 새 인스턴스가 생성될 때마다 최신 클래스 파일을 출력하도록 Eclipse 또는 IntelliJ를 설정한 target/classes
폴더에서 User
클래스를 로드합니다. 모든 이전 DynamicClassLoader
및 이전 User
클래스는 연결이 해제되고 가비지 수집기의 대상이 됩니다.
JVM HotSpot에 익숙하다면 여기에서 클래스 구조도 변경하고 다시 로드할 수 있다는 사실에 주목해야 합니다. playFootball
메서드는 제거되고 playBasketball
메서드가 추가됩니다. 이것은 메서드 내용만 변경하거나 클래스를 다시 로드할 수 없는 HotSpot과 다릅니다.
이제 클래스를 다시 로드할 수 있으므로 많은 클래스를 한 번에 다시 로드할 시간입니다. 다음 예제에서 시도해 보겠습니다.
예 3: 여러 클래스 다시 로드
이 예제의 출력은 예제 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
메서드는 컨텍스트 개체를 유지하며 삭제해야 하는 모든 항목 에 대한 유일한 링크 입니다. 해당 링크를 끊으면 컨텍스트 개체와 컨텍스트 클래스, 서비스 개체 ...가 모두 가비지 수집기의 대상이 됩니다.
일반적으로 클래스가 그렇게 지속적이고 가비지 수집되지 않는 이유에 대한 약간의 설명:
- 일반적으로 모든 클래스를 기본 Java 클래스 로더에 로드합니다.
- 클래스-클래스로더 관계는 양방향 관계이며 클래스 로더는 로드한 모든 클래스도 캐싱합니다.
- 따라서 클래스 로더가 여전히 라이브 스레드에 연결되어 있는 한 모든(로드된 모든 클래스)는 가비지 수집기에 영향을 받지 않습니다.
- 다시 말해서 기본 클래스 로더에 의해 이미 로드된 코드에서 다시 로드하려는 코드를 분리할 수 없다면 새로운 코드 변경 사항은 런타임 중에 적용되지 않습니다.
이 예제를 통해 모든 애플리케이션의 클래스를 다시 로드하는 것이 실제로는 훨씬 쉽다는 것을 알 수 있습니다. 목표는 단지 라이브 스레드에서 사용 중인 동적 클래스 로더로의 연결을 끊을 수 있는 얇은 상태로 유지하는 것입니다. 그러나 일부 객체(및 해당 클래스)가 다시 로드 되지 않고 다시 로드 주기 사이에 재사용되기를 원한다면 어떻게 될까요? 다음 예를 살펴보겠습니다.
예 4: 지속 클래스 공간과 다시 로드된 클래스 공간 분리
여기 소스코드가..
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
에 의해 로드되어서는 안 되며 메모리에는 기본 ClassLoader
에 의해 로드되는 ConnectionPool
클래스가 하나만 있어야 합니다. 이것은 Java에서 클래스 재로딩 아키텍처를 설계할 때 주의해야 하는 이유 중 하나입니다.
DynamicClassLoader
가 실수로 ConnectionPool
클래스를 로드하면 어떻게 될까요? 그러면 지속 공간의 ConnectionPool
객체는 Context
객체로 전달될 수 없습니다. Context
객체는 ConnectionPool
이라는 다른 클래스의 객체를 기대하고 있지만 실제로는 다른 클래스이기 때문입니다!
그러면 DynamicClassLoader
가 ConnectionPool
클래스를 로드하지 못하도록 하려면 어떻게 해야 합니까? DynamicClassLoader
를 사용하는 대신 이 예제에서는 ExceptingClassLoader
라는 하위 클래스를 사용합니다. 이 하위 클래스는 조건 함수를 기반으로 슈퍼 클래스로더에 로드를 전달합니다.
(className) -> className.contains("$Connection")
여기서 ExceptingClassLoader
를 사용하지 않으면 DynamicClassLoader
는 ConnectionPool 클래스가 " target/classes
" 폴더에 있기 때문에 ConnectionPool
클래스를 로드합니다. DynamicClassLoader
에 의해 ConnectionPool
클래스가 선택되는 것을 방지하는 또 다른 방법은 ConnectionPool
클래스를 다른 폴더(아마도 다른 모듈에 있을 수 있음)로 컴파일하고 별도로 컴파일하는 것입니다.
공간 선택 규칙
이제 Java 클래스 로딩 작업이 정말 혼란스러워집니다. 지속 공간에 있어야 하는 클래스와 다시 로드할 수 있는 공간에 있어야 하는 클래스를 어떻게 결정합니까? 규칙은 다음과 같습니다.
- 다시 로드할 수 있는 공간의 클래스는 지속된 공간의 클래스를 참조할 수 있지만 지속된 공간의 클래스는 다시 로드할 수 있는 공간의 클래스를 참조 할 수 없습니다 . 이전 예에서 다시 로드할 수 있는
Context
클래스는 지속형ConnectionPool
클래스를 참조하지만ConnectionPool
에는Context
에 대한 참조가 없습니다. - 클래스는 다른 공간의 클래스를 참조하지 않는 경우 어느 한 공간에 존재할 수 있습니다. 예를 들어
StringUtils
와 같은 모든 정적 메서드가 있는 유틸리티 클래스는 지속 공간에서 한 번 로드되고 다시 로드 가능한 공간에서 별도로 로드될 수 있습니다.
따라서 규칙이 그다지 제한적이지 않다는 것을 알 수 있습니다. 두 공간에서 참조되는 개체가 있는 교차 클래스를 제외하고 다른 모든 클래스는 지속 공간이나 다시 로드 가능한 공간 또는 둘 다에서 자유롭게 사용할 수 있습니다. 물론 재장전 공간에 있는 클래스만 재장전 주기로 재장전되는 것을 즐깁니다.
따라서 클래스 재로딩과 관련된 가장 어려운 문제가 처리됩니다. 다음 예제에서는 이 기술을 간단한 웹 응용 프로그램에 적용하고 모든 스크립팅 언어처럼 Java 클래스를 다시 로드하는 것을 즐깁니다.
예 5: 작은 전화번호부
여기 소스코드가..
이 예제는 일반 웹 애플리케이션의 모습과 매우 유사합니다. AngularJS, SQLite, Maven 및 Jetty Embedded Web Server가 포함된 단일 페이지 응용 프로그램입니다.
다음은 웹 서버 구조에서 다시 로드할 수 있는 공간입니다.
웹 서버는 다시 로드할 수 있도록 다시 로드할 수 있는 공간에 있어야 하는 실제 서블릿에 대한 참조를 보유하지 않습니다. 보유하고 있는 것은 스텁 서블릿으로, 서비스 메소드를 호출할 때마다 실제 컨텍스트에서 실제 서블릿이 실행되도록 해석합니다.
이 예제에서는 웹 서버에 일반 Context와 같은 모든 값을 제공하지만 DynamicClassLoader
에서 다시 로드할 수 있는 실제 컨텍스트 개체에 대한 참조를 내부적으로 보유하는 새 개체 ReloadingWebContext
도 소개합니다. 웹 서버에 스텁 서블릿을 제공하는 것은 이 ReloadingWebContext
입니다.
ReloadingWebContext
는 실제 컨텍스트의 래퍼가 되며:
- "/"에 대한 HTTP GET이 호출될 때 실제 컨텍스트를 다시 로드합니다.
- 웹 서버에 스텁 서블릿을 제공합니다.
- 실제 컨텍스트가 초기화되거나 소멸될 때마다 값을 설정하고 메서드를 호출합니다.
- 컨텍스트를 다시 로드하도록 구성할 수 있고 다시 로드하는 데 사용되는 클래스 로더를 구성할 수 있습니다. 이것은 프로덕션에서 애플리케이션을 실행할 때 도움이 됩니다.
지속 공간과 다시 로드 가능한 공간을 분리하는 방법을 이해하는 것이 매우 중요하기 때문에 다음은 두 공간을 교차하는 두 클래스입니다.
Context
의 개체 public F0<Connection> connF
에 대한 클래스 qj.util.funct.F0
- 함수 개체는 함수가 호출될 때마다 연결을 반환합니다. 이 클래스는
DynamicClassLoader
에서 제외된 qj.util 패키지에 있습니다.
Context
에서 객체 public F0<Connection> connF
에 대한 클래스 java.sql.Connection
- 일반 SQL 연결 개체입니다. 이 클래스는
DynamicClassLoader
의 클래스 경로에 상주하지 않으므로 선택되지 않습니다.
요약
이 Java 클래스 자습서에서는 단일 클래스를 다시 로드하는 방법, 단일 클래스를 계속해서 다시 로드하는 방법, 여러 클래스의 전체 공간을 다시 로드하는 방법, 지속되어야 하는 클래스와 별도로 여러 클래스를 다시 로드하는 방법을 살펴보았습니다. 이러한 도구를 사용하여 안정적인 클래스 재장전을 달성하는 핵심 요소는 매우 깔끔한 디자인을 갖는 것입니다. 그런 다음 클래스와 전체 JVM을 자유롭게 조작할 수 있습니다.
Java 클래스 재로딩을 구현하는 것은 세상에서 가장 쉬운 일이 아닙니다. 그러나 시도해 보고 어느 시점에서 클래스가 즉석에서 로드되는 것을 발견하면 이미 거의 다 온 것입니다. 시스템을 위한 완전히 뛰어난 깔끔한 디자인을 달성하기 전에 해야 할 일이 거의 없습니다.
행운을 빕니다 친구들과 새로 찾은 초능력을 즐기십시오!