-
Notifications
You must be signed in to change notification settings - Fork 0
김승수, 김현수, 이호석 리플렉션
이호석 edited this page Jul 8, 2024
·
2 revisions
- Reflection은 Class, Constructor, Method, Field영역이 존재하고, 클래스의 인스턴스화 없이 해당 클래스의 정보들을 가져올 수 있다.
- 컴파일된 클래스 정보를 활용해 동적인 프로그래밍이 가능한 API를 제공한다.
@Test
@DisplayName("테스트1: 리플렉션을 이용해서 클래스와 메소드의 정보를 정확하게 출력해야 한다.")
public void showClass() {
SoftAssertions s = new SoftAssertions();
Class<Question> clazz = Question.class;
log.debug("Classs Name {}", clazz.getName());
// field name
// getDeclaredFields = 필드 접근제어자 + 패키지 + 타입 + 이름
System.out.println("1. Class.getDeclaredFields() 출력");
Field[] declaredFields = clazz.getDeclaredFields();
for (Field declaredField : declaredFields) {
log.info("declaredField = {}", declaredField);
}
System.out.println("============================================================================================================================\n");
// Field.getName 이름만
System.out.println("2. Field관련 정보 출력");
Field[] fields = clazz.getDeclaredFields();
Arrays.stream(fields)
.map(Field::getName)
.forEach(name -> log.info("FieldName = {}", name));
System.out.println();
// java.lang.reflect.Modifiers 클래스에 정의된 16진수 상수값을 OR연산하여 반환하는 값
Arrays.stream(fields)
.map(Field::getModifiers)
.forEach(mod -> log.info("FieldModifiers = {}", mod));
// constructor name
Constructor<?>[] constructors = clazz.getDeclaredConstructors();
System.out.println("============================================================================================================================\n");
System.out.println("3. 생성자 관련 정보 출력");
Arrays.stream(constructors)
.map(Constructor::getName)
.forEach(name -> log.info("ConstructorName = {}", name));
Arrays.stream(constructors)
.map(Constructor::getModifiers)
.forEach(mod -> log.info("ConstructorModifiers = {}", mod));
Arrays.stream(constructors)
.map(Constructor::getParameterTypes)
.forEach(param -> Arrays.stream(param).forEach(p -> log.info("ParameterType = {}", p)));
System.out.println("============================================================================================================================\n");
// method
System.out.println("4. 메소드 관련 정보 출력");
Method[] methods = clazz.getDeclaredMethods();
Arrays.stream(methods)
.map(Method::getName)
.forEach(name -> log.info("MethodName = {}", name));
Arrays.stream(methods)
.map(Method::getModifiers)
.forEach(mod -> log.info("MethodModifiers = {}", mod));
Arrays.stream(methods)
.map(Method::getParameterTypes)
.forEach(param -> Arrays.stream(param).forEach(p -> log.info("ParameterType = {}", p)));
}
-
Question 클래스의 모든 필드, 생성자, 메소드에 대한 정보를 출력할 수 있습니다.
-
getFields, getConstructors, getMethods는 접근제어자가 public인 것들만 가져옵니다.
-
getDeclaredXxx로 시작하는 메소드는 접근제어자가 private인 것들도 포함하여 가져옵니다.
-
int getModifiers()
메소드는 접근제어자가 어떤게 붙어있는지 알아내기 위해 16진수로 정의된 상수값들을 OR연산하여 값을 반환합니다.- 위 값들은 2진수의 각 자릿수를 나타내기 떄문에, 10진수 값을 다시 2진수로 역변환하여 어떤 접근제어자가 사용되었는지 알아낼 수 있을것으로 예상합니다.
public class Junit3Runner {
@Test
public void runner() throws Exception {
Class clazz = Junit3Test.class;
Method[] methods = clazz.getDeclaredMethods();
Object instance = clazz.getDeclaredConstructor().newInstance();
for (Method method : methods) {
if (method.getName().startsWith("test")) {
method.setAccessible(true); // private 접근제어자를 가진 메소드를 실행할 수 있는 권한을 준다.
method.invoke(instance);
}
}
}
}
-
Class
에서 직접newInstance()
를 하는 방식은 Java 9부터 Deprecated 되었기에Class.getConstructor()
를 통해 생성자를 가져오고, 생성자를 이용해newInstance()
를 해야 합니다. - 다만 주의할 부분은, 메소드 정보만 있다고 해당 메소드를 실행 시킬 수 있는게 아니라, 먼저 메소드를 실행 시킬 클래스의 인스턴스를 생성한 후, Method.invoke()인자에 해당 인스턴스를 같이 넘겨줘야 실행할 수 있습니다.
/**
* @Testable을 붙이면 IDE에서 테스트 가능한 메소드를 추적해 Run 마크를 달아줌
* https://junit.org/junit5/docs/5.2.0/api/org/junit/platform/commons/annotation/Testable.html
*/
public class Junit4Runner {
@Test
public void run() throws Exception {
Class clazz = Junit4Test.class;
Method[] methods = clazz.getDeclaredMethods();
Object instance = clazz.getDeclaredConstructor().newInstance();
for (Method method : methods) {
// if (Objects.nonNull(method.getAnnotation(MyTest.class))) {
if (method.isAnnotationPresent(MyTest.class)) {
method.setAccessible(true); // private 접근제어자를 가진 메소드를 실행할 수 있는 권한을 준다.
method.invoke(instance);
}
}
}
}
- Method에게
isAnnotationPresent(AnnotationType.class)
를 넘겨주면AnnotationType
이라는 애노테이션이 해당 메소드에 붙어있는지 확인하고boolean
값을 반환합니다.- 다른 방법으로는 메소드에 있는 애노테이션 값을 직접 가져와서 비교하는 방식도 있는데, 해당 애노테이션이 없다면
null
을 반환하므로 주의해야 합니다.
- 다른 방법으로는 메소드에 있는 애노테이션 값을 직접 가져와서 비교하는 방식도 있는데, 해당 애노테이션이 없다면
- 추가로 JUnit은 어떻게 @Test 붙여도 인텔리제이에서 실행 마크를 띄워주는지 궁금했는데, @Testable을 붙이면 IDE에서 테스트 가능한 메소드를 추적해 Run 마크를 달아주는 것을 알 수 있었습니다.
@Test
void privateFieldAccess() throws Exception {
// given
Class<Student> studentClass = Student.class;
Student student = studentClass.getConstructor().newInstance();
log.debug(studentClass.getName());
// when
Field name = studentClass.getDeclaredField("name");
Field age = studentClass.getDeclaredField("age");
name.setAccessible(true);
name.set(student, "이름");
age.setAccessible(true);
age.setInt(student, 10);
// then
assertAll(
() -> assertThat(student.getName()).isEqualTo("이름"),
() -> assertThat(student.getAge()).isEqualTo(10)
);
}
-
Student
클래스의 필드는private
입니다. 따라서private
필드에 접근할 수 있는 권한을 얻기 위해Field.setAccessible(true)
를 넘겨주어 내가 해당 필드에 접근할 수 있는 권한을 얻습니다. - 이것 역시 필드를 세팅하려면 인스턴스를 만들어 주어야 합니다.
public class UserTest {
@Test
void userTest() throws Exception {
// given
Class<User> userClass = User.class;
// when
User user = null;
Constructor<?>[] constructors = userClass.getConstructors();
for (Constructor<?> constructor : constructors) {
Class<?>[] parameterTypes = constructor.getParameterTypes();
if (parameterTypes.length == 2 && parameterTypes[0] == String.class
&& parameterTypes[1] == Integer.class) {
user = (User) constructor.newInstance("철수", 10);
}
}
// then
assertThat(user.getName()).isEqualTo("철수");
assertThat(user.getAge()).isEqualTo(10);
}
}
- 기본 생성자가 아니라, 인수가 있는 생성자이므로 생성자의 타입이 무엇인지 검증하고, 해당 타입에 알맞는 생성자 인수를 넘겨주어 생성할 수 있습니다.
public class ElapsedTimeTest {
private static final Logger log = LoggerFactory.getLogger(ElapsedTimeTest.class);
static class TestClass {
public TestClass() {
}
@ElapsedTime
public void test1() {
for (int i = 0; i < 1000; i++) {
System.out.print("");
}
System.out.println("test1 실행");
}
public void test2() {
System.out.println("test2 실행");
}
}
@Test
void elapsedTimeCheck() throws Exception {
Class<TestClass> testClassClass = TestClass.class;
TestClass testClass = testClassClass.getConstructor().newInstance();
Method[] methods = testClassClass.getDeclaredMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(ElapsedTime.class)) {
method.setAccessible(true);
long currentTimeMillis = System.currentTimeMillis();
method.invoke(testClass);
long elapsedTimeMillis = System.currentTimeMillis() - currentTimeMillis;
log.info("{}ms", elapsedTimeMillis);
}
}
}
}
package next;
public class Main {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
-
바이트코드에서 상수 풀 까지 확인하려면 javap를 사용해야 합니다. 따라서 위 Main.java를 컴파일한 Main.class의 위치에서 다음 명령어를 실행하면 됩니다.
-
javap -v Main.class
Classfile /Users/woowatech20/develop/wootacam/java-practice-wootecam/out/production/classes/next/Main.class Last modified 2024. 7. 8.; size 526 bytes SHA-256 checksum c7e78c1fd2b1802ca3ca67a9d5025f210ff186e4294defab95242b0e46ef8fbd Compiled from "Main.java" public class next.Main minor version: 0 major version: 55 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #5 // next/Main super_class: #6 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1 Constant pool: #1 = Methodref #6.#20 // java/lang/Object."<init>":()V #2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream; #3 = String #23 // Hello World! #4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 = Class #26 // next/Main #6 = Class #27 // java/lang/Object #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lnext/Main; #14 = Utf8 main #15 = Utf8 ([Ljava/lang/String;)V #16 = Utf8 args #17 = Utf8 [Ljava/lang/String; #18 = Utf8 SourceFile #19 = Utf8 Main.java #20 = NameAndType #7:#8 // "<init>":()V #21 = Class #28 // java/lang/System #22 = NameAndType #29:#30 // out:Ljava/io/PrintStream; #23 = Utf8 Hello World! #24 = Class #31 // java/io/PrintStream #25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V #26 = Utf8 next/Main #27 = Utf8 java/lang/Object #28 = Utf8 java/lang/System #29 = Utf8 out #30 = Utf8 Ljava/io/PrintStream; #31 = Utf8 java/io/PrintStream #32 = Utf8 println #33 = Utf8 (Ljava/lang/String;)V { public next.Main(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lnext/Main; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String Hello World! 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 6: 0 line 7: 8 LocalVariableTable: Start Length Slot Name Signature 0 9 0 args [Ljava/lang/String; }