프로그래밍 언어/Java

[Java] 리플렉션 API : 클래스, 필드, 메서드 정보 조회

코딩하는흑구 2020. 1. 9. 23:07

리플렉션은 Class<?> 타입으로 시작합니다.

도큐먼트 : https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html

 

Class (Java Platform SE 8 )

Determines if the specified Class object represents a primitive type. There are nine predefined Class objects to represent the eight primitive types and void. These are created by the Java Virtual Machine, and have the same names as the primitive types tha

docs.oracle.com

 

위의 도큐먼트 사이트에 접속해서 보시면 아래의 내용을 모두 확인하실 수 있을 것입니다.

 

리플렉션이란..?

자바 소스코드를 작성하고 컴파일러에 의해 .java 파일이 .class 파일로 컴파일이 됩니다. 이후 JVM이 .class 파일을 읽어들일때 클래스로더 3인방(부트스트랩, 확장, 어플리케이션)이 메모리에 클래스 로딩하게 됩니다. 이때 .class 파일은 자바 바이트코드로 이루어져 있는데요. JVM이 메모리에 올려논 클래스 정보는 대부분 .class에 적힌 바이트코드대로 메모리에 올리게 됩니다. 

리플렉션은 이 부분을 다루는 아주 강력한 api로 .class 파일에 적힌 자바 바이트코드대로 메모리에 올렸다가 리플렉션 코드에 의해 세세한 부분까지도 접근이 가능하게 합니다. 

활용에 따라 타입, 변수명, 파라미터 타입, 수 등등 아주 상세한 클래스 명세정보를 다룰 수 있는 기술입니다.

 

예제 코드

- Book 클래스

package org.example;

public class Book {
    private static String B = "BOOK";

    private static final String C = "BOOK";

    private String a = "a";

    public String d = "d";

    protected String e = "e";

    public Book(){

    }

    public Book(String a, String d, String e) {
        this.a = a;
        this.d = d;
        this.e = e;
    }

    private void f(){
        System.out.println("F");
    }

    public void g(){
        System.out.println("g");
    }

    public int h(){
        return 100;
    }

}

 

- MyBook 클래스

package org.example;

public class MyBook extends Book implements MyInterface {
}

- MyInterface 인터페이스

package org.example;

public interface MyInterface {
}

 

 

 

클래스의 정보 조회

위에서 리플렉션 api의 시작은 Class<?> 라고 하였습니다. 이러한 타입의 클래스를 얻는 방법은 3가지가 있습니다.

 

1. 직접 선언

Class<Book> bookClass = Book.class;

2. 인스턴스를 이용한 getClass() 메소드

Book book = new Book();
Class<? extends Book> bookClassFromInstance = book.getClass();

3. Class.forName() 메소드

try {
	//주로 JDBC 예제에서 많이 봄
	Class<?> bookClassFromPackageString = Class.forName("org.example.Book");
} catch (ClassNotFoundException e) {
	e.printStackTrace();
}

 

 

Class<?> 타입을 이용한 클래스 정보 조회

필드 정보 조회

- (Field[]) getFields() : 클래스에 선언된 필드들 반환(public 접근지시자만)

- (Field) getField(String name) : name에 해당하는 필드를 반환 (없으면 NoSuchFieldException)

- (Field[]) getDeclaredFields() : 클래스에 선언된 모든 필드들 반환(private 까지도 포함)

- (Field) getDeclaredField(String name) : name에 해당하는 필드를 반환(없으면 NoSuchFieldException)

 

Field[] fields = bookClass.getFields(); //클래스가 가진 모든 필드들 반환

// public java.lang.String org.example.Book.d 밖에 안보임. 왜?? -> getFields는 public만
Arrays.stream(fields).forEach(System.out::println); 

// 모두다 가져오고싶다면..
fields = bookClass.getDeclaredFields();

 

필드 값 접근

Arrays.stream(fields).forEach(f -> {
	try {
    	System.out.printf("%s, %s",f, f.get(book));
	} catch (IllegalAccessException e) {
		e.printStackTrace();
	}
});

- 모든 Field들을 가져와서 get() 메소드에 book 인스턴스(new Book())를 인자로 던져주면 값을 반환함.

- 근데 에러가 발생할것임 :

java.lang.IllegalAccessException: Class org.example.App can not access a member of class org.example.Book with modifiers "private static", "private", "private static final" 

f.setAccessible(true); -> 이걸하면 접근지시자 무시하고 접근할 수 있음.
- 최종 동작코드

Arrays.stream(fields).forEach(f -> {
    try {
        f.setAccessible(true);
        System.out.printf("%s, %s",f, f.get(book));
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
});

이제 Stream API를 이용한 forEach문에서 각 필드에 접근할 때 접근지시자가 private인 경우에도 접근을 가능하도록(accessible) 설정을 하여 에러없이 모든 값을 출력가능하게 할 것임.

 

메소드 접근

Method[] methods = bookClass.getMethods();
Arrays.stream(methods).forEach(System.out::println); //Object 상속 메소드 포함.

생성자 접근

Constructor[] constructors = bookClass.getConstructors();
Arrays.stream(constructors).forEach(System.out::println);

부모클래스 접근

Class<? super MyBook> superClass = MyBook.class.getSuperclass();
System.out.println(superClass);

인터페이스 접근

Class<?>[] implementsInterface = MyBook.class.getInterfaces();
Arrays.stream(implementsInterface).forEach(System.out::println);

 

Modifier(제어자)를 이용한 필드 / 메소드의 제어자 확인 (static, abastract, private, public 등)

[필드]

1. 클래스에 선언한 필드들을 가져온다. 이때 모든 필드에 대해 접근하려할 때, getFields() 메소드로 가져오면 public만 가져오므로 주의한다.

Field[] bookClassFields = Book.class.getDeclaredFields();

2. Stream API를 이용하여 forEach를 돌것인데 이 때 f.getModifiers()라는 메소드로 int값 modifiers를 얻어내어 Modifiers 클래스의 static 메소드에 이 값을 인자로 넘겨준다. 이름은 대부분 명시적으로 작성되어 있다.

Arrays.stream(bookClassFields).forEach(f -> {
    int modifiers = f.getModifiers();
    System.out.printf("[%s]\n", f);
    System.out.println("abstract : "+Modifier.isAbstract(modifiers));
    System.out.println("public : "+Modifier.isPublic(modifiers));
    System.out.println("private : "+Modifier.isPrivate(modifiers));
    System.out.println("final : "+Modifier.isFinal(modifiers));
    System.out.println("static : "+Modifier.isStatic(modifiers));
});

 

 

[메소드]

1. 클래스에 선언한 메서드들을 가져온다. 마찬가지로 getMethods()로 가져오면 public만 가져오므로 주의한다.

Method[] bookClassMethods = Book.class.getDeclaredMethods();

2. 필드에서와 마찬가지로 여러가지 정보를 확인 가능하다. 아래의 예제에서는

 - 리턴타입

 - 선언되어있는 어노테이션 배열

 - 예외 처리 배열

 - 파라미터 타입 배열

 - 파라미터 수

순서대로 되어있다.

Arrays.stream(bookClassMethods).forEach(m -> {
    System.out.printf("[%s]\n", m.getName());
    System.out.println(m.getReturnType());
    System.out.println(m.getDeclaredAnnotations()); // 얘네들은
    System.out.println(m.getExceptionTypes()); // Class<?>[] 배열 반환해서
    System.out.println(m.getParameterTypes()); // forEach 돌려서 확인해야함.
    System.out.println(m.getParameterCount());
});

Modifier 도큐먼트 : https://docs.oracle.com/javase/7/docs/api/java/lang/reflect/Modifier.html