기능이 있는 주석
@Oveeride // <= 애노테이션
public void hello(){ ... }
메타데이터로, 데이터를 설명해주는 데이터를 의미한다
기본적으로 주석과 같이 직접적인 영향은 미치지 않는다
클래스, 인터페이스, 생성자, 메서드, 매개변수, 필드, 지역변수
@Custom
public class AnnotaionSample {
@Custom
private int field;
@Custom
private void method(@Custom Object parameter){
@Custom
int localVal = 10;
}
@Custom
private class innser{}
@Custom
private interface innerInter{}
}
자바 8 이후에는 타입에도 애노테이션을 달 수 있다
new @NonNull AnnotaionSample();
자바에서 기본 제공해주는 애노테이션 @Override, @Deprecated, @FunctionalInterface ...
사용자가 직접 만든 애노테이션
public @interface MyAnno { // @interface로 생성이 가능하다
int number = 10;
int num() default 15;
String string();
String[] str_arr();
}
@interface 키워드를 이용하여 생성할 수 있다
상수 또는 애노테이션 내의 요소들을 설정할 수 있다 ( Enum 등등 )
@MyAnno(string = "Hello", str_arr = {"Hello","annotation"})
public void Temp(){
System.out.println(MyAnno.number);
Class<Main> cls = Main.class;
MyAnno anno = cls.getAnnotation(MyAnno.class);
System.out.println(anno.num()); // 15
System.out.println(anno.str_arr()); // Hello, annotaion
System.out.println(anno.string()); // Hello
}
애노테이션 내의 상수를 꺼내쓰는게 가능하다
상수의 경우 직접 클래스명을 통한 접근으로 사용할 수 있고, 나머지는 애노테이션 객체를 생성해야한다
public @interface MyAnno {
class MyClass{ } // 클래스 선언
interface MyInterface{} // 인터페이스 선언
@interface InnerAnno{} // 애노테이션 선언
}
@MyAnno
@MyAnno.InnerAnno // 애노테이션 속 애노테이션
public static void main(String[] args) {
MyAnno.MyClass myClass = new MyAnno.MyClass(); // 애노테이션 안의 객체
MyAnno.MyInterface myInterface = new MyAnno.MyInterface() { ... }; // 애노테이션 안의 인터페이스
}
애노테이션 내부에는 클래스나 인터페이스 혹은 다른 애노테이션이 올 수 있다
애노테이션을 위한 애노테이션
등을 설정할 수 있다
종류에따라 어느 요소를 목표로 생성한 애노테이션인지 명시한다
CLASS, RUNTIME, SOURCE 3가지의 옵션을 선택할 수 있다
RUNTIME이 가장 오래 살아있고 SOURCE가 가장 빨리 사라진다
애노테이션이 붙어있는 코드의 정보를 참조해서 새로운 코드를 생성해낸다
Lombok과 같이 입력한 적 없는 코드를 컴파일 과정에서 생성하는 등, 중간에 끼어들어 작업을 수행한다
컴파일 단계에서 실행되기 때문에 사전에 오류를 점검할 수 있다 ( ex : @Override )
@Override 역시 상속유무를 컴파일단계에서 표시해준다
Processor
클래스에서 제공하는 메서드를 이용하여 특정 애노테이션이 설정된 위치의 코드정보를 읽고 코드를 생성할 수 있다
@Target(ElementType.TYPE) // 클래스, 인터페이스, Enum
@Retention(RetentionPolicy.SOURCE) // 컴파일시에 코드가 추가되므로 SOURCE로 충분
public @interface Magic { ... }
먼저 애노테이션 프로세서가 적용될 애노테이션을 생성한다
이 과정에서 Retension과 Target을 지정해줄 수 있다
Target
의 경우 애노테이션이 설정된 인터페이스를 상속하는 클래스를 만드는 것이 목표기 때문에 TYPE으로 지정했다
Retention
은 클래스파일을 생성하고나면 더이상 존재할 이유가 없어 SOURCE로 지정했다
@SupportedAnnotationTypes({"me.ddings.Magic"}) // 탐색 대상이 될 Annotation의 FQCN
public class MyAnnotationProcessor extends AbstractProcessor {
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported(); // 지원할 소스버전
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Magic.class);
for (Element element : elements){
if(element.getKind() != ElementKind.INTERFACE){
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "지원하지 않는 위치입니다 =>" + element.getSimpleName());
}else{
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "설정된 작업을 수행합니다 => " + element.getSimpleName());
}
}
return true; // 이 프로세서에서 해당 애노테이션에 대한 작업을 마칠건지 선택
}
}
이제 실제로 동작할 Processor
의 구현체를 만들 차례다
기본적으로 애노테이션 프로세서는 특정한 라운드를 돌아가면서 작업을 수행한다
Annotation processing happens in a sequence of rounds.
On each round, a processor may be asked to process a subset of the annotations found on the source and class files produced by a prior round.
매 라운드마다이전 라운드에서 제공된 소스파일이나 클래스파일에서 해당하는 애노테이션이 처리되었는지 확인하는 작업을 수행한다
특정 라운드에서 원하는 애노테이션을 찾게되면 작업을 수행하고 이를 다음 라운드로 넘길지를 선택할 수 있으며, 이 라운드는 roundEnv
로 접근할 수 있다
roundEnv
를 통해 애노테이션이 설정된 element
를 찾게되면 해당 element
가 조건에 부합하는지를 확인한 후에 작업을 수행하면된다
이제 생성하길 원하는 코드를 생각해야한다
public class Console {
public static void main(String[] args) {
// Moja moja = new MagicMoja();
// System.out.println(moja.pulllOut());
}
}
동작하길 원하는 코드는 다음과 같으며, 현재 Moja
위에 @Magic
이 달려있는 상태다
Moja
는 인터페이스고, pullOut()
은 추상메서드이다
따라서 이 두가지 작업을 수행해야한다
MagicMoja
클래스 생성Moja
를 구현해야함pullOut()
구현plugins {
id 'java'
}
group 'me.ddings'
version '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
implementation group: 'com.squareup', name: 'javapoet', version: '1.11.1'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
}
test {
useJUnitPlatform()
}
코드 생성을 위한 javapoet 의존성을 추가했다
이하 작업은 생성될 코드를 명시하기위해 애노테이션 프로세서 클래스의 process() 에 작성한다
// 1. 메서드 정의
MethodSpec methodSpec = MethodSpec.methodBuilder("pullOut") // 메서드 이름
.addModifiers(Modifier.PUBLIC) // 접근제한자
.returns(String.class) // 반환타입
.addStatement("return $S", "Rabbit!!!") // 코드 한 줄이다
.build();
먼저 메서드를 정의한다
위 코드를 통해서 아래 메서드를 정의한다
public String pullOut(){
return "Rabbit!!!";
}
// 2. 클래스 정의
TypeElement typeElement = (TypeElement) element; // 애노테이션이 붙은 element 형변환
ClassName className = ClassName.get(typeElement); // 애노테이션이 붙은 인터페이스 이름
TypeSpec typeSpec = TypeSpec.classBuilder("MagicMoja") // 클래스 이름
.addModifiers(Modifier.PUBLIC) // 접근제한자
.addSuperinterface(className) // 상속할 인터페이스
.addMethod(methodSpec) // 클래스 내부의 메서드
.build();
다음으로 클래스를 정의한 다음 앞서 정의한 메서드를 집어넣는다
위 코드를 통해 아래 클래스를 정의한다
public class MagicMoja implements Moja{
public String pullOut(){
return "Rabbit!!!";
}
}
// 3. 소스파일 생성
Filer filer = processingEnv.getFiler(); // 실제 소스파일로 만들기 위함
try {
JavaFile.builder(className.packageName(), typeSpec) // element와 동일한 패키지경로
.build() // 생성
.writeTo(filer); // 파일로 추가
} catch (IOException e) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "FATAL ERROR : 소스코드 생성중에 문제가 발생했습니다. => " + e);
}
이 과정을 거치면 실제로 소스파일이 생성된다
resources/META-INF/services
밑에 Processor
의 FQCN이름으로 파일을 생성하고 애노테이션 프로세서 구현체의 FQCN을 입력한다
jar {
from {
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
}
}
Gradle 7.x 이상이라면 위와 같이 의존성을 같이 포함하여 jar파일을 생성하도록 옵션을 주고
gradle을 이용하여 의존성을 포함한 jar파일을 생성한다
해당 jar파일을 의존성을 주입받을 프로젝트의 libs 폴더에 집어넣고 ( 없으면 생성하자 )
build.gradle에 다음과 같이 작성한다
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
annotationProcessor files('libs/moja-magic-1.0-SNAPSHOT.jar') // 이 부분
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.mysql:mysql-connector-j'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
annotationProcessor files('libs/moja-magic-1.0-SNAPSHOT.jar')
를 추가하면 된다
이후 gradle을 refresh하면 위 처럼 의존성이 들어온게 보인다
gradle의 comple을 수행하자
내가 원했던 모양대로 클래스가 생성된게 보인다
만들지도않은 MagicMoja 인스턴스를 생성하고 메서드를 실행하는데 성공했다