2020-10-7 바이트코드 강의 정리(2) 리플렉션

리플렉션

클래스타입을 접근하는 방법

public static void main(String[] args) throws ClassNotFoundException {
  Class<Book> bookClass = Book.class;

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

  Class<?> aClass1 = Class.forName("me.jimmy.reflection.Book");
}
  • Class<Book> Book = Book.class와 같이 클래스타입을 이용해서 접근.

  • 인스턴스의 getClass()를 이용
  • class.forName을 통해 full package경로를 이용
  • 리플렉션을 활용하면 클래스의 다양한 정보들을 접근할 수 있다.
public class Book {
  private String name = "name1";
  private static String author = "Ji";
  private static final String message = "hello world";
  public String d= "d";
  protected String e = "E";
  public Book() {
  }
  public Book(String name, String d, String e) {
    this.name = name;
    this.d = d;
    this.e = e;
  }
  private void print() {
    System.out.println("printf method");
  }
  public void g() {
    System.out.println("g");
  }
  public int h() {
    return 100;
  }
}

public class BookApp {
  public static void main(String[] args) throws ClassNotFoundException {
    Class<Book> bookClass = Book.class;
    Arrays.stream(bookClass.getFields()).forEach(System.out::println);
    System.out.println("---");
    Arrays.stream(bookClass.getDeclaredFields()).forEach(System.out::println);

    // other example
    Arrays.stream(bookClass.getDeclaredFields()).forEach(f -> {
      int modifiers = f.getModifiers();
      System.out.println(f);
      System.out.println(Modifier.isPrivate(modifiers));
      System.out.println(Modifier.isStatic(modifiers));
    });
  }
}

// result
public java.lang.String me.jimmy.reflection.Book.d
---
private java.lang.String me.jimmy.reflection.Book.name
private static java.lang.String me.jimmy.reflection.Book.author
private static final java.lang.String me.jimmy.reflection.Book.message
public java.lang.String me.jimmy.reflection.Book.d
protected java.lang.String me.jimmy.reflection.Book.e
  • 메세지를 보면 d밖에 안나온다. getFields 메서드는 public밖에 리턴을 안한다.
  • getDeclaredFields()를 하게되면 선언된 모든 필드들을 가져올 수 있다.
  • Fields Class의 getModifier()를 이용하여 modifier값을 가져온다. 이를 활용하여 Modifier Class에 정의된 isPrivate, isProtected, isFinal, isPublic 등등 확인할 수 있다.
    • modifier int value에 대해서는 Modifier Class에 설명이 잘되있다.

어노테이션

  • 어노테이션은 기본적으로 주석과 같다. 바이트코드에는 읽어오지 않기때문에, 바이트코드에 남기고 싶으면 @Retention을 Runtime으로 변경해야한다.
  • 어노테이션은 값들을 가질 수 있다. Primitive Type, Boxed, List
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD})
@Inherited
public @interface MyAnnotation {
    String name() default "jimmy";
    int number() default 100;
    String value();
}

@MyAnnotation("value test")
public class Book {
}

public class MyBook extends Book implements MyInterface {
}

public class BookApp {
  public static void main(String[] args) throws ClassNotFoundException {
    Arrays.stream(MyBook.class.getAnnotations()).forEach(System.out::println);
  }
}
  • 어노테이션 내부에 value라는 필드는 값을 할당할때, value를 생략해도 된다. 하지만 여러개의 속성을 동시에 할당할때는 생략불가, 값을 1개만 받을때는 value만 쓰자.
  • default값을 설정하지 않으면 어노테이션을 추가할때마다 값을 할당해줘야한다.
  • @MyAnnotation을 Book에서 사용하고 있기때문에, MyBook이 Book을 상속하고 있음에도 annotation이 출력되지 않는다.
    • MyAnnotation에 @Inherited를 추가하면 상속받은 클래스에서도 가져올 수 있다.
    • @me.jimmy.reflection.MyAnnotation(name=”jimmy”, number=100, value=”value”)
public class BookApp {
  public static void main(String[] args) throws ClassNotFoundException {
    Arrays.stream(MyBook.class.getAnnotations()).forEach(f -> {
      if (f instanceof MyAnnotation) {
        MyAnnotation myAnnotation = (MyAnnotation)f;
        System.out.println(myAnnotation.value());
        System.out.println(myAnnotation.name());
        System.out.println(myAnnotation.number());
      }
    });
  }
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
@Inherited
public @interface MyAnnotation {
    String name() default "jimmy";
    int number() default 100;
    String value() default "hello value";
}
  • 리플렉션을 활용하면 어노테이션에 붙어있는 값들도 참조할 수 있다.

클래스정보 수정

  • Class.newInstance() deprecated됬고 생성자를 통해 인스턴스를 만들어야한다.
  • Constructor.newInstacne(params);
  • 필드값 접근하고 수정하기 : 필드값에 접근해야되서 인스턴스 필요
    • Field.get(object)
    • Field.set(object, value)
    • Static 필드는 obj가 없어도 되서, null을 넘겨도 된다.
  • 메서드 실행 : Method.invoke(object, params);
public class Book {
  public static String A = "A";
  private String B = "B";
  public Book() {}
  public Book(String b) {
    B = b;
  }
  private void c() {
    System.out.println("c");
  }
  public int sum(int left, int right) {
    return left + right;
  }
}

public class BookApp {
  public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
    Class<?> bookClass =  Class.forName("me.jimmy.reflection.Book");
    Constructor<?> constructor = bookClass.getConstructor(String.class);
    Book book = (Book)constructor.newInstance("constructor test");
    System.out.println(book);

    // Field Test
    Field a = Book.class.getDeclaredField("A");
    System.out.println(a.get(null));
    a.set(null, "AAA");
    System.out.println(a.get(null));
    Field b = Book.class.getDeclaredField("B");
    b.setAccessible(true);
    System.out.println(b.get(book));
    b.set(book, "private");
    System.out.println(b.get(book));

    // Method Test
    Method c = bookClass.getDeclaredMethod("c");
    c.setAccessible(true);
    c.invoke(book);

    Method d = bookClass.getDeclaredMethod("sum", int.class, int.class);
    int result = (int)d.invoke(book, 1, 2);
    System.out.println(result);
  }
}
// result
me.jimmy.reflection.Book@515f550a
A
AAA
constructor test
private
  • Class객체를 이용하면 선언된 필드들을 조회할 수 있다. 기본적으로 getField는 public에 대해서만 가져오기 때문에, getDeclaredField를 사용하면 protected, private 접근제어자도 가져올 수 있다.
  • A필드의 경우 static이기때문에 인스턴스가 없어도 된다. a.set(null, “AAA”)에서 첫번째 인자가 인스턴스이기때문에 null을 해도 상관없다.
  • B필드의 경우 private이기때문에 setAccessible(true)로 변경해줘야한다. 안하면 에러난다.
  • Method c의 경우 paramter가 없어서 그냥 가져올 수 있었지만, Method d에서 sum이라는 메서드에는 int left, int right라는 parameter를 받는다. 따라서 가져올때, parameter 타입도 함께 명시해야한다.
    • Integer.class, int.class 모두 구별한다.

나만의 DI

  • @Inject를 이용해서 컨테이너 서비스 만들어보기
// 의존성 주입을 테스트하기 위해 임의로 만든 어노테이션
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {
}

public class ContainerService {
  public static <T> T getObject(Class<T> classType) {
    T instance = createInstance(classType);
    Arrays.stream(classType.getDeclaredFields()).forEach(f -> {
      if (f.getAnnotation(Inject.class) != null) {
        Object fieldInstance = createInstance(f.getType());
        f.setAccessible(true); // 필드의 접근제어자가 public이 아닐 수 있어서.
        try {
          f.set(instance, fieldInstance);
        } catch (IllegalAccessException e) {
          throw new RuntimeException(e);
        }
      }
    });
    return instance;
  }
  private static <T> T createInstance(Class<T> classType) {
    try {
      return classType.getConstructor(null).newInstance();
    } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
      throw new RuntimeException(e);
    }
  }
}

public class ContainerServiceTest {
  @Test
  public void getObject_BookRepository() {
    BookRepository bookRepository = ContainerService.getObject(BookRepository.class);
    assertNotNull(bookRepository);
  }

  @Test
  public void getObject_BookService() {
    BookService bookService = ContainerService.getObject(BookService.class);
    assertNotNull(bookService);
    assertNotNull(bookService.bookRepository);
  }
}

  • 리플렉션을 활용해서 Class<T>, getConstructor를 활용하여 인스턴스를 생성할 수 있다.
  • T인스턴스를 가져오고, 선언된 필드중에 특정어노테이션이 있으면, instance에 value를 set해준다. 스프링에서 DI를 하는 방식이 이같은 방식이다.
  • mvn install을 하게되면, 만든 파일들이 jar로 패키징되고, 로컬메이븐 저장소에 저장된다. 다른프로젝트를 생성해서 의존성을 추가할 수 있다.

테스트해보기

  • 새로운 프로젝트를 만들고, 위에서 패키징한 jar파일을 pom.xml에 의존성을 추가한다.

  • public class AccountRepository {
      public void save() {
        System.out.println("repository injected");
      }
    }
    public class AccountService {
    
      @Inject
      AccountRepository accountRepository;
    
      public void join() {
        System.out.println("service join");
        accountRepository.save();
      }
    }
    
    public class App {
      public static void main( String[] args ) {
        AccountService accountService = ContainerService.getObject(AccountService.class);
        accountService.join();
      }
    }
    
    // result
    service join
    repository injected
    
  • 새로만든 프로젝트에서 이전에 만든 .jar파일을 만들었기때문에, @Inject라는 어노테이션을 인식할 수 있다.

  • ContainerService.getObject메서드를 호출해서, AccountService에 정의된 AccountRepository를 주입받고 사용할 수 있다.

주의점

  • 지나친 사용은 성능이슈를 야기. 필요한 경우만 쓰자.
  • 컴파일 타임에 확인되지 않고, 런타임시에는 문제를 만들 수 있다.
  • 접근지시자를 무시한다.

사용처

  • 스프링 : 의존성주입, MVC뷰에서 넘어온 데이터 객체를 바인딩할때
  • 하이버네이트 : @Entity 클래스에 setter가 없으면 리플렉션을 사용
Written on October 7, 2020