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