lombok은 어떻게 동작할까
- lombok
- @Getter, @Setter, @Builder 등의 애노테이션과 애노테이션 프로세서를 제공하여 표준적으로 작성해야 할 코드를 개발자 대신 생성해주는 라이브러리
- lombok 사용하기
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.8</version> <scope>provided</scope> </dependency>
- 동작 원리
- 컴파일 시점에 애노테이션 프로세스를 사용하여 소스코드의 AST(Abstract Syntax Tree)를 조작한다.
- https://javaparser.org/inspecting-an-ast/
- 애노테이션 프로세서는 컴파일 단계에서 끼어들어서 특정한 Annotation이 붙어있는 소스코드를 참조하여 다른 소스를 만들어 내는 것을 말한다.
- 컴파일 시점에 애노테이션 프로세스를 사용하여 소스코드의 AST(Abstract Syntax Tree)를 조작한다.
- 논란 거리
- 공개된 API가 아닌 컴파일러 내부 클래스를 사용하여 기존 소스 코드를 조작한다.
- 특히 이클립스의 경우 java agent를 사용하여 컴파일러 클래스까지 조작하여 사용한다.
- 해당 클래스들 역시 공개된 API가 아니다보니 버전 호환성 문제가 생길 수 있다.
- 그럼에도 불구하고 편리성 때문에 널리 쓰이고 있으며 대안이 몇가지 있지만 롬복의 모든 기능과 편의성을 대체하지 못하는 현실
- AutoValue (https://github.com/google/auto/blob/master/value/userguide/index.md)
- Immutables (https://immutables.github.io)
- 참고
- https://docs.oracle.com/javase/8/docs/api/javax/annotation/processing/Processor.html
- https://projectlombok.org/contributing/lombok-execution-path
- https://stackoverflow.com/questions/36563807/can-i-add-a-method-to-a-class-from-a-compile-time-annotation
- http://jnb.ociweb.com/jnb/jnbJan2010.html#controversy
- https://www.oracle.com/technetwork/articles/grid/java-5-features-083037.html
Annotation Processor 1
- Processor Interface
- https://docs.oracle.com/en/java/javase/11/docs/api/java.compiler/javax/annotation/processing/Processor.html
- 여러 라운드(rounds)에 거쳐 소스 및 컴파일 된 코드를 처리할 수 있다.
- 예제
- Moja project
- Moja.class
@Magic public interface Moja { public void pullOut(); }
- Magic Annotation 활용
- Moja.class
- MagicMoja project
- Magic Annotation
@Retention(RetentionPolicy.SOURCE) @Target(ElementType.TYPE) // interface, class, Enum public @interface Magic { }
- Target에 TYPE을 하면 interface, class, enum에 사용할 수 있다.
- 구체적이지 않기 때문에 Annotation Processor를 생성하여 interface에 붙는 경우 실패하도록 할 수 있다.
- MaginProcess.class
@AutoService(Processor.class) public class MagicProcessor extends AbstractProcessor{ @Override public Set<String> getSupportedAnnotationTypes() { // TODO Auto-generated method stub return Set.of(Magic.class.getName()); } @Override public SourceVersion getSupportedSourceVersion() { // TODO Auto-generated method stub return SourceVersion.latestSupported(); } @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { // TODO Auto-generated method stub Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Magic.class); for(Element element: elements) { Name elementName = element.getSimpleName(); if(element.getKind() != ElementKind.INTERFACE) { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Magic anntation can not be used on" + elementName); } else { processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Processing " + elementName); } } return true; } }
- Process 인터페이스를 구현해도 되지만, 자바에서 제공하는 AbstractProcessor 추상클래스를 구현해도 된다.
- Process 에서 구현해야하는 여러 메서드들을 구현해주고 있다.
- getSupportAnnotationTypes() override
- 이 프로세서가 처리할 애노테이션들을 지정
- Element란?
- 패키지, 클래스, 메서드 등 소스코드의 구성요소를 Element라고 부른다.
- 각 Element 들이 프로세스를 할 때 참조할 수 있다.
- getSupportedSourceVersion() override
- 몇 버전의 소스코드를 지원하는지 설정.
- boolean processor(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) override
- 애노테이션 프로세서는 라운드 라는 개념으로 처리를 한다.
- 여러 라운드에 거쳐 처리를 한다.
- 각 라운드마다 프로세서에게 특정 애노테이션을 가지고 있는 엘리먼트를 찾으면 처리를 요청한다.
- 처리된 결과가 다음 라운드에게 전달될 수 있다.
- 만약 여기서 true를 리턴하면, 애노테이션 프로세서가 처리를 한것이다.
- true를 리턴하면, 다른 프로세서가 이를 처리하지 않는다.
- Round란?
- 애노테이션 프로세서는 라운드라는 개념이 존재한다.
- 여러 라운드에 걸쳐 처리를 한다.
- 각 라운드 마다 애노테이션 프로세서가 처리할 엘리먼트를 찾으면, 이를 처리하고 그 결과를 다음 프로세서에게 넘길 수도 있다.
- 애노테이션 프로세서는 라운드 라는 개념으로 처리를 한다.
- @AutoService
- 자동적으로 MAINFEST파일을 생성해준다.
- 컴파일 시점에 애노테이션 프로세서를 사용하여 META-INF/services/javax.annotation.processor.Processor 파일 자동으로 생성해 준다.
- Packaging
- mvn clean install
- 생성된 jar파일을 Moja project의 pom.xml에 dependency 추가하여 사용
<dependency> <groupId>com.study</groupId> <artifactId>MagicMoja</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>
- Magic Annotation
- Moja project
- 유틸리티
- AutoService: 서비스 프로바이더 레지스트리 생성기
- https://github.com/google/auto/tree/master/service ```
com.google.auto.service auto-service 1.0-rc6 ```
- AutoService: 서비스 프로바이더 레지스트리 생성기
- Service Provider
- https://itnext.io/java-service-provider-interface-understanding-it-via-code-30e1dd45a091
- 참고
- http://hannesdorfmann.com/annotation-processing/annotationprocessing101
- http://notatube.blogspot.com/2010/12/project-lombok-creating-custom.html
- https://medium.com/@jintin/annotation-processing-in-java-3621cb05343a
- https://medium.com/@iammert/annotation-processing-dont-repeat-yourself-generate-your-code-8425e60c6657
- https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javac.html#processing
Annotation Processor 2부
- 예제
- @Magic annotation을 붙이면 Moja의 구현체인 MagicMoja클래스를 생성
- MagicProcessor.class
@Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { // TODO Auto-generated method stub Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Magic.class); for(Element element: elements) { Name elementName = element.getSimpleName(); if(element.getKind() != ElementKind.INTERFACE) { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Magic anntation can not be used on" + elementName); } else { processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Processing " + elementName); } TypeElement typeElement = (TypeElement)element; ClassName className = ClassName.get(typeElement); // 원하는 메소드를 생성 MethodSpec pullOut = MethodSpec.methodBuilder("pullOut") .addModifiers(Modifier.PUBLIC) .returns(String.class) .addStatement("return $S", "Rabbit!") .build(); // 원하는 클래스 생성, pullOut 메소드를 넘겨준다 TypeSpec MagicMoja = TypeSpec.classBuilder("MagicMoja") .addModifiers(Modifier.PUBLIC) .addMethod(pullOut) .addSuperinterface(className) .build(); Filer filer = processingEnv.getFiler(); try { JavaFile.builder(className.packageName(), MagicMoja) .build().writeTo(filer); } catch(IOException e) { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Fatal error: " + e.getMessage()); } } return true; }
- ClassName
- Element를 TypeElement로 변환한뒤, JavaPoet을 사용하여 ClassName 타입의 객체로 변환한다.
- 이 타입의 객체는 클래스 정보들을 참조할 수 있다.
- MethodSpec
- 우리가 생성할 클래스의 메소드 스팩을 구현한다.
- methodBuilder(메소드명): 구현할 메소드의 이름을 정의한다.
- addModifireds(접근지시자를): 메소드의 접근 지시자를 정의한다.
- returns: 해당 메소드에서 리턴하는 타입을 정의한다.
- addStatement: 메소드 내부의 스테이트먼트를 정의한다.
- TypeSpec
- 우리가 생성한 클래스의 스팩을 구현한다.
- classBuilder(클래스명): 구현할 클래스 명을 정의한다. (이때 풀패키지 경로가아닌 심플 네임만 지정해준다.)
- addSuperInterface(인터페이스): 우리가 생성할 클래스가 구현할 인터페이스에 대한 정보를 정의한다.
- addModifiers(접근지시자를): 클래스의 접근 지시자를 정의한다.
- addMethod(메소드스팩): 클래스에 추가할 메소드 스팩을 정의한다. (위에서 정의한 pullOut 메소드를 추가한다.)
- build 를 통해 클래스 스팩 구현을 마친다.
- Filer
- 소스코드, 클래스 코드 및 리소스를 생성할 수 있는 인터페이스이다.
- JavaPoet를 사용한다면 보다 쉽게 생성이 가능하다.
- ClassName
- Main 함수
Moja moja = new MagjcMoja(); System.out.println(moja.pullOut());
- MagicMoja를 생성하여 가져올 수 있다.
- Filer 인터페이스
- 소스 코드, 클래스 코드 및 리소스를 생성할 수 있는 인터페이스
- 유틸리티
- Javapoet: 소스 코드 생성 유틸리티
- 간단한 API를 제공하여 Class, Method 등을 생성할 수 있다. ```
com.squareup javapoet 1.12.1 ```
- Javapoet: 소스 코드 생성 유틸리티
정리
- 애노테이션 프로세서 사용 예
- 롬복
- AutoService: java.util.ServiceLoader용 파일 생성 유틸리티
- @Override
- https://stackoverflow.com/questions/18189980/how-do-annotations-like-overridework-internally-in-java/18202623
- Dagger 2: 컴파일 타임 DI 제공
- 안드로이드 라이브러리
- ButterKinfe: @BindView (뷰 아이디와 애노테이션 붙인 필드 바인딩)
- DeepLinkDispatch: 특정 URI 링크를 Activity로 연결할 때 사용
- 애노테이션 프로세서 장점
- 런타임 비용이 제로
- 애노테이션 프로세서 단점
- 기존 클래스 코드를 변경할 때는 약간의 hack이 필요하다.