JavaRush /Java Blog /Random-KO /반사 API. 반사. 자바의 어두운 면
Oleksandr Klymenko
레벨 13
Харків

반사 API. 반사. 자바의 어두운 면

Random-KO 그룹에 게시되었습니다
안녕하세요, 젊은 파다완님. 이 기사에서는 Java 프로그래머가 겉보기에 절망적인 상황에서만 사용하는 힘인 Force에 대해 설명하겠습니다. 따라서 Java의 어두운 측면은 -Reflection API
반사 API.  반사.  자바의 어두운 면 - 1
Java의 리플렉션은 Java Reflection API를 사용하여 수행됩니다. 이 반성은 무엇입니까? 인터넷에서도 인기 있는 짧고 정확한 정의가 있습니다. 반사 (후기 라틴어 반사 - 돌아가기)는 프로그램 실행 중에 프로그램에 대한 데이터를 연구하는 메커니즘입니다. Reflection을 사용하면 필드, 메서드 및 클래스 생성자에 대한 정보를 검사할 수 있습니다. 리플렉션 메커니즘 자체를 사용하면 컴파일 중에 누락되었지만 프로그램 실행 중에 나타나는 유형을 처리할 수 있습니다. 오류 보고를 위한 논리적으로 일관된 모델의 존재와 반영을 통해 올바른 동적 코드를 생성할 수 있습니다. 즉, Java에서 리플렉션이 어떻게 작동하는지 이해하면 수많은 놀라운 기회가 열립니다. 문자 그대로 클래스와 해당 구성 요소를 저글링할 수 있습니다.
반사 API.  반사.  자바의 어두운 면 - 2
다음은 리플렉션이 허용하는 기본 목록입니다.
  • 객체의 클래스를 알아내거나 결정합니다.
  • 클래스 수정자, 필드, 메서드, 상수, 생성자 및 슈퍼클래스에 대한 정보를 가져옵니다.
  • 구현된 인터페이스/인터페이스에 어떤 메서드가 속하는지 알아보세요.
  • 클래스의 인스턴스를 생성하면 프로그램이 실행될 때까지 클래스 이름을 알 수 없습니다.
  • 이름으로 개체 필드의 값을 가져오고 설정합니다.
  • 이름으로 개체의 메서드를 호출합니다.
리플렉션은 거의 모든 최신 Java 기술에서 사용됩니다. 플랫폼으로서의 Java가 반성 없이 그렇게 엄청난 채택을 달성할 수 있었는지 상상하기 어렵습니다. 아마도 나는 할 수 없었을 것입니다. 반사에 대한 일반적인 이론적 아이디어에 익숙해졌습니다. 이제 실제 적용에 대해 알아 보겠습니다! Reflection API의 모든 메소드를 연구하는 것이 아니라 실제로 실제로 접하게 되는 메소드만 연구할 것입니다. 리플렉션 메커니즘에는 클래스 작업이 포함되므로 간단한 클래스를 사용합니다 MyClass.
public class MyClass {
   private int number;
   private String name = "default";
//    public MyClass(int number, String name) {
//        this.number = number;
//        this.name = name;
//    }
   public int getNumber() {
       return number;
   }
   public void setNumber(int number) {
       this.number = number;
   }
   public void setName(String name) {
       this.name = name;
   }
   private void printData(){
       System.out.println(number + name);
   }
}
보시다시피 이것은 가장 일반적인 클래스입니다. 매개변수가 있는 생성자는 이유 때문에 주석 처리되어 있습니다. 이에 대해서는 나중에 다시 설명하겠습니다. 수업 내용을 자세히 살펴보면 getter아마도 name. 필드 자체는 name액세스 한정자로 표시되어 있으므로 private클래스 자체 외부에서는 해당 필드에 액세스할 수 없으며 =>해당 값을 가져올 수 없습니다. "그럼 뭐가 문제야? - 당신이 말했잖아요. “ getter액세스 한정자를 추가하거나 변경하세요.” 그리고 당신 말이 맞을 것입니다. 그러나 그것이 컴파일된 aar 라이브러리에 있거나 편집 액세스가 없는 다른 닫힌 모듈에 있다면 어떨까요 MyClass? 실제로 이런 일은 매우 자주 발생합니다. 그리고 일부 부주의한 프로그래머는 단순히 getter. 성찰에 대해 기억할 시간입니다! 클래스 private필드 로 이동해 보겠습니다 . nameMyClass
public static void main(String[] args) {
   MyClass myClass = new MyClass();
   int number = myClass.getNumber();
   String name = null; //no getter =(
   System.out.println(number + name);//output 0null
   try {
       Field field = myClass.getClass().getDeclaredField("name");
       field.setAccessible(true);
       name = (String) field.get(myClass);
   } catch (NoSuchFieldException | IllegalAccessException e) {
       e.printStackTrace();
   }
   System.out.println(number + name);//output 0default
}
지금 여기서 무슨 일이 일어났는지 알아봅시다. Java에는 훌륭한 클래스가 있습니다 Class. 실행 가능한 Java 애플리케이션의 클래스와 인터페이스를 나타냅니다. Class와 사이의 연결 에 대해서는 다루지 않겠습니다 ClassLoader. 이것은 기사의 주제가 아닙니다. 다음으로 이 클래스의 필드를 가져오려면 메서드를 호출해야 합니다 getFields(). 이 메서드는 클래스의 사용 가능한 모든 필드를 반환합니다. 우리의 필드는 이기 때문에 이것은 우리에게 적합하지 않습니다. private그래서 우리는 메소드를 사용합니다 getDeclaredFields(). 이 메소드는 또한 클래스 필드의 배열을 반환하지만 이제는 private및 가 모두 반환됩니다 protected. 우리 상황에서는 관심 있는 분야의 이름을 알고 있으며 원하는 분야의 이름이 getDeclaredField(String)어디에 있는지 메소드를 사용할 수 있습니다.String메모: getFields()getDeclaredFields()상위 클래스의 필드를 반환하지 마세요 ! Field 좋습니다. 에 대한 링크가 포함된 객체를 받았습니다 name. 왜냐하면 필드가 публичным(공개)이 아니었으므로 해당 필드를 사용하려면 액세스 권한이 부여되어야 합니다. 이 방법을 setAccessible(true)사용하면 작업을 계속할 수 있습니다. 이제 이 분야는 name완전히 우리의 통제하에 있습니다! 클래스의 인스턴스인 객체 get(Object)를 호출하여 그 값을 얻을 수 있습니다 . 이를 캐스팅 하고 변수에 할당합니다 . 갑자기 'a'가 없는 경우 메소드를 사용하여 name 필드에 새 값을 설정할 수 있습니다 . FieldObjectMyClassStringnamesetterset
field.set(myClass, (String) "new value");
축하해요! 당신은 방금 반성의 기본 메커니즘을 마스터했으며 해당 private필드에 접근할 수 있었습니다! 처리되는 예외의 블록 try/catch과 유형에 주의하세요. IDE 자체는 필수 존재를 나타내지만 이름을 통해 여기에 있는 이유를 분명하게 알 수 있습니다. 계속하세요! 눈치채셨겠지만, 우리는 MyClass이미 클래스 데이터에 대한 정보를 표시하는 메소드를 가지고 있습니다:
private void printData(){
       System.out.println(number + name);
   }
하지만 이 프로그래머는 여기에도 유산을 남겼습니다. 이 메소드는 접근 한정자 아래에 private있으며, 매번 출력 코드를 직접 작성해야 했습니다. 순서가 맞지 않습니다. 우리의 생각은 어디에 있습니까?... 다음 함수를 작성해 보겠습니다.
public static void printData(Object myClass){
   try {
       Method method = myClass.getClass().getDeclaredMethod("printData");
       method.setAccessible(true);
       method.invoke(myClass);
   } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
       e.printStackTrace();
   }
}
여기서 절차는 필드를 가져오는 것과 거의 동일합니다. 원하는 메서드를 이름으로 가져오고 이에 대한 액세스 권한을 부여합니다. Method그리고 우리가 사용하는 개체를 호출하려면 클래스의 인스턴스도 있어야 invoke(Оbject, Args)합니다 . - 메소드 인수 - 우리 인수는 없습니다. 이제 정보를 표시하는 기능을 사용합니다 . ОbjectMyClass ArgsprintData
public static void main(String[] args) {
   MyClass myClass = new MyClass();
   int number = myClass.getNumber();
   String name = null; //?
   printData(myClass); // outout 0default
   try {
       Field field = myClass.getClass().getDeclaredField("name");
       field.setAccessible(true);
       field.set(myClass, (String) "new value");
       name = (String) field.get(myClass);
   } catch (NoSuchFieldException | IllegalAccessException e) {
       e.printStackTrace();
   }
   printData(myClass);// output 0new value
}
만세, 이제 클래스의 비공개 메서드에 액세스할 수 있습니다. 하지만 메서드에 여전히 인수가 있고 주석 처리된 생성자가 있는 이유는 무엇입니까? 모든 것에는 시간이 있습니다. 처음의 정의에서 리플렉션을 사용하면 runtime(프로그램이 실행되는 동안) 모드에서 클래스의 인스턴스를 생성할 수 있다는 것이 분명해졌습니다! 해당 클래스의 정규화된 이름으로 클래스의 개체를 만들 수 있습니다. 정규화된 클래스 이름은 에 경로가 지정된 클래스 이름입니다 package.
반사 API.  반사.  자바의 어두운 면 - 3
내 계층 구조에서 package전체 이름은 MyClass" "입니다 reflection.MyClass. 간단한 방법으로 클래스 이름을 찾을 수도 있습니다(클래스 이름을 문자열로 반환함).
MyClass.class.getName()
리플렉션을 사용하여 클래스의 인스턴스를 만들어 보겠습니다.
public static void main(String[] args) {
   MyClass myClass = null;
   try {
       Class clazz = Class.forName(MyClass.class.getName());
       myClass = (MyClass) clazz.newInstance();
   } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
       e.printStackTrace();
   }
   System.out.println(myClass);//output created object reflection.MyClass@60e53b93
}
Java 애플리케이션이 시작될 때 모든 클래스가 JVM에 로드되는 것은 아닙니다. 코드가 클래스를 참조하지 않는 경우 MyClassJVM에 클래스를 로드하는 책임을 맡은 사람, 즉 ClassLoader클래스를 JVM에 로드하지 않습니다. 따라서 우리는 ClassLoader클래스에 대한 설명을 유형의 변수 형식으로 로드하고 수신하도록 강제해야 합니다 Class. 이 작업을 위해 설명이 필요한 클래스의 이름이 있는 forName(String)메소드 가 있습니다 . String을 수신하면 Сlass메서드 호출은 동일한 설명에 따라 생성되는 을 newInstance()반환합니다 . Object이 물건을 우리 수업에 가져오는 일이 남아 있습니다 MyClass. 시원한! 힘들었지만 이해되길 바랍니다. 이제 문자 그대로 한 줄에서 클래스의 인스턴스를 만들 수 있습니다! 불행하게도 설명된 메서드는 기본 생성자(매개변수 없이)에서만 작동합니다. 인수가 있는 메소드와 매개변수가 있는 생성자를 호출하는 방법은 무엇입니까? 이제 생성자의 주석 처리를 제거할 차례입니다. 예상대로 newInstance()기본 생성자를 찾지 못하고 더 이상 작동하지 않습니다. 클래스 인스턴스 생성을 다시 작성해 보겠습니다.
public static void main(String[] args) {
   MyClass myClass = null;
   try {
       Class clazz = Class.forName(MyClass.class.getName());
       Class[] params = {int.class, String.class};
       myClass = (MyClass) clazz.getConstructor(params).newInstance(1, "default2");
   } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
       e.printStackTrace();
   }
   System.out.println(myClass);//output created object reflection.MyClass@60e53b93
}
클래스 생성자를 얻으려면 클래스 설명에서 메서드를 호출 getConstructors()하고 생성자 매개변수를 얻으려면 다음을 호출합니다 getParameterTypes().
Constructor[] constructors = clazz.getConstructors();
for (Constructor constructor : constructors) {
   Class[] paramTypes = constructor.getParameterTypes();
   for (Class paramType : paramTypes) {
       System.out.print(paramType.getName() + " ");
   }
   System.out.println();
}
이런 방식으로 우리는 모든 생성자와 그에 대한 모든 매개변수를 얻습니다. 내 예에는 이미 알려진 특정 매개변수를 사용하여 특정 생성자를 호출하는 경우가 있습니다. newInstance그리고 이 생성자를 호출하기 위해 이러한 매개변수의 값을 지정하는 메소드를 사용합니다 . invoke메소드 호출에서도 마찬가지입니다 . 질문이 생깁니다: 생성자의 반사적 호출이 어디에 유용할 수 있습니까? 처음에 언급했듯이 최신 Java 기술은 Reflection API 없이는 할 수 없습니다. 예를 들어 메서드 및 생성자의 반영과 결합된 주석이 Android 개발에서 널리 사용되는 Dagger 라이브러리를 형성하는 DI(종속성 주입)가 있습니다. 이 기사를 읽고 나면 Reflection API의 메커니즘을 이해했다고 확신할 수 있습니다. 리플렉션을 자바의 어두운 면이라고 부르는 것은 괜한 일이 아닙니다. 이는 OOP 패러다임을 완전히 깨뜨립니다. Java에서 캡슐화는 일부 프로그램 구성 요소에 대한 액세스를 숨기고 다른 구성 요소에 대한 액세스를 제한하는 역할을 합니다. private 수정자를 사용한다는 것은 이 필드에 대한 액세스가 이 필드가 존재하는 클래스 내에서만 가능하다는 것을 의미하며, 이를 기반으로 프로그램의 추가 아키텍처를 구축합니다. 이 기사에서는 리플렉션을 사용하여 어디든 갈 수 있는 방법을 살펴보았습니다. 아키텍처 솔루션 형태의 좋은 예는 생성적 디자인 패턴입니다 Singleton. 주요 아이디어는 프로그램의 전체 작업을 통해 이 템플릿을 구현하는 클래스에 복사본이 하나만 있어야 한다는 것입니다. 이는 생성자의 기본 액세스 한정자를 private으로 설정하여 수행됩니다. 그리고 어떤 프로그래머가 자신의 생각을 가지고 그러한 클래스를 만든다면 그것은 매우 나쁠 것입니다. 그런데 최근 직원으로부터 들었던 매우 흥미로운 질문이 있습니다. 템플릿을 구현하는 클래스에 Singleton상속인이 있을 수 있습니까? 이 경우 반사조차도 무력할 수 있습니까? 기사에 대한 피드백과 답변을 댓글로 작성하고, 질문도 남겨주세요! Reflection API의 진정한 힘은 런타임 주석과 결합하여 나타납니다. 이에 대해서는 Java의 어두운 면에 대한 향후 기사에서 다루게 될 것입니다. 관심을 가져주셔서 감사합니다!
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION