Checkstyle이나 FindBugs 같은 도구들이 어떻게 정적 코드 분석을 수행하는지, 또는 NetBeansEclipse 같은 IDE(Integrated Development Environments)가 어떻게 신속히 코드를 수행하는지 또는 코드에 선언된 필드의 정확한 참조를 어떻게 찾아내는지 생각해 본 적이 있습니까? 많은 경우, IDE는 고유의 API를 이용하여 소스 코드를 구문 분석하고 AST(Abstract Syntax Tree) 또는 "구문 분석 트리"라는 표준 트리 구조를 생성합니다. 그리고 이 트리 구조는 소스 요소를 더 심층적으로 분석하는 데 이용할 수 있습니다.

좋은 소식은 Java Standard Edition 6 릴리스와 함께 자바에 도입된 세 가지 새로운 API의 도움으로 위의 작업은 물론, 훨씬 더 많은 작업을 수행할 수 있게 되었다는 것입니다. 소스 코드 분석을 수행해야 하는 자바 애플리케이션 개발자들이 관심을 가질 만한 이 API들은 자바 컴파일러 API(JSR 199), 플러그형 주석 처리 API(JSR 269), 그리고 컴파일러 트리 API 등입니다.

여기에서는 이 세 API의 기능들을 살펴보고, 입력으로 제공된 일련의 소스 코드 파일에 대해 특정 자바 코딩 규칙을 검증하는 간단한 데모 애플리케이션을 개발해 보겠습니다. 이 유틸리티는 또한 코딩 위반 메시지와 아울러 위반된 소스 코드의 위치를 보여줍니다. Object 클래스의 equals() 메소드를 대체하는 간단한 자바 클래스를 생각해봅시다. 검증할 코딩 규칙은 equals() 메소드를 구현하는 각각의 클래스는 올바른 서명과 함께 hashcode() 메소드도 대체해야 한다는 것입니다. 아래의 TestClass 클래스는 equals() 메소드를 갖고 있음에도 불구하고 hashcode() 메소드를 정의하지 않는다는 것을 알 수 있을 것입니다.


public class TestClass implements Serializable {
 int num;

 @Override
  public boolean equals(Object obj) {
        if (this == obj)
                return true;
        if ((obj == null) || (obj.getClass() != this.getClass()))
                return false;
        TestClass test = (TestClass) obj;
        return num == test.num;
  }
}

그러면, 이 세 개의 API의 도움을 받아 빌드 프로세스의 일환으로 이 클래스를 분석해보겠습니다.


코드로부터 컴파일러 호출: 자바 컴파일러 API

우리 모두는 자바 소스 파일을 클래스 파일로 컴파일할 때 javac 명령줄 도구를 사용합니다. 그렇다면 자바 파일을 컴파일하는 데 왜 API가 필요할까요? 그 해답은 매우 간단합니다. 이름이 의미하듯 이 새로운 표준 API를 이용하면 자신의 자바 애플리케이션으로부터 컴파일러를 호출할 수 있기 때문입니다. 즉, 프로그래밍 방식으로 컴파일러와 상호 작용하여 컴파일을 애플리케이션 레벨 서비스로 만들 수 있기 때문입니다. 아래는 이 API의 일반적인 사용 방식 중 일부입니다.

  • 이 컴파일러 API는, 예를 들어, JSP 페이지에서 생성된 서블릿 소스의 컴파일에 외부 컴파일러를 사용하는 부담을 없앰으로써 애플리케이션 배포에 소요되는 시간을 최소화하는 데 도움이 됩니다.

  • 코드 분석기와 IDE 같은 개발자 도구는 컴파일 시간을 크게 줄여주는 편집기 또는 빌드 도구 내부에서 컴파일러를 호출할 수 있습니다.

자바 컴파일러 클래스는 javax.tools 패키지 내에 패키징됩니다. 이 패키지의 ToolProvider 클래스는 JavaCompiler 인터페이스를 구현하는 일부 클래스의 인스턴스를 반환하는 getSystemJavaCompiler()라는 메소드를 제공합니다. 이 컴파일러 인스턴스는 실제 컴파일을 수행할 컴파일 태스크를 만드는 데 이용될 수 있습니다. 그러면, 컴파일될 자바 소스 파일이 이 컴파일 태스크로 전달됩니다. 이를 위해 이 컴파일러 API는 파일 시스템, 데이터베이스, 메모리 등과 같은 다양한 소스로부터 자바 파일을 불러낼 수 있는 JavaFileManager 라는 파일 관리자 추상화를 제공합니다. 이 예에서는 java.io.File에 기반한 파일 관리자 StandardFileManager를 사용합니다. 이 표준 파일 관리자는 JavaCompiler 인스턴스의 getStandardFileManager() 메소드를 호출하여 얻을 수 있습니다. 위에서 언급한 절차에 관한 코드의 일부는 아래와 같습니다.


//Get an instance of java compiler
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

//Get a new instance of the standard file manager implementation
StandardJavaFileManager fileManager = compiler.
        getStandardFileManager(null, null, null);
        
// Get the list of java file objects, in this case we have only 
// one file, TestClass.java
Iterable<? extends JavaFileObject> compilationUnits1 = 
        fileManager.getJavaFileObjectsFromFiles("TestClass.java");

getStandardFileManager() 메소드에 진단 리스너를 옵션으로 전달하면 치명적이지 않은 문제에 대한 진단 보고서를 생성할 수 있습니다. 우리는 이 도구로부터 진단 정보를 수집하지 않기 때문에 이 코드에서는 null 값을 전달합니다. 이 메소드들에 전달되는 다른 매개변수에 대한 자세한 사항은 자바 6 API를 참고하십시오. StandardJavaFileManagergetJavaFileObjectsfromFiles() 메소드는 제공된 자바 소스 파일에 상응하는 모든 JavaFileObject 인스턴스를 반환합니다.

다음 단계는 자바 컴파일 태스크를 만드는 것입니다. JavaCompilergetTask() 메소드를 이용하면 됩니다. 이 지점까지는 아직 컴파일 태스크가 시작되지 않았습니다. 이 태스크는 CompilationTaskcall() 메소드를 호출하여 트리거할 수 있습니다. 컴파일 태스크의 생성 및 트리거에 대한 코드는 다음과 같습니다.


// Create the compilation task
CompilationTask task = compiler.getTask(null, fileManager, null,
                                        null, null, compilationUnits1);
                                        
// Perform the compilation task.
task.call();

컴파일 오류가 발생하지 않으면 대상 디렉토리에 TestClass.class 파일이 생성됩니다.


주석 처리: 플러그형 주석 처리 API

모두가 알고 있듯이, 자바 SE 5.0은 자바 클래스, 필드, 메소드 등과 같은 요소에 메타데이터 또는 주석을 추가하고 처리하기 위한 지원 기능을 도입했습니다. 일반적으로 주석은 빌드 도구 또는 런타임 환경에 의해 처리되어 애플리케이션 동작 제어, 코드 생성 등과 같은 유용한 태스크를 수행합니다. 자바 5에서는 주석이 첨부된 데이터의 컴파일타임 및 런타임 처리가 가능합니다. 주석 처리기는 컴파일러에 동적으로 플러그인되어 소스 파일을 분석하고 그 안에 있는 주석을 처리할 수 있는 유틸리티입니다. 주석 처리기는 메타데이터 정보를 최대한 활용하여 다음을 일부에 포함하는 수많은 태스크를 수행합니다.

  • 주석은 예를 들어 엔터티 클래스의 경우, persistence.xml 또는 엔터프라이즈 빈의 경우, ejb-jar.xml 등과 같은 배포 설명자를 생성하는 데 사용할 수 있습니다.

  • 주석 처리기는 메타데이터 정보를 이용하여 코드를 생성할 수 있습니다. 예를 들어, 처리기는 올바로 주석이 첨부된 엔터프라이즈 빈의 Home 및 Remote 인터페이스를 생성할 수 있습니다.

  • 주석은 코드 또는 배포 단위의 유효성을 검증하는 데 이용할 수 있습니다.

자바 5.0은 주석을 처리하고 처리된 정보를 모델링할 수 있도록 APT(Annotation Processing Tool) 및 관련된 미러 기반 리플렉션 API(com.sun.mirror.*)를 제공했습니다. APT 도구는 제공된 자바 소스 파일에 있는 주석에 맞는 주석 처리기를 실행합니다. 미러 API는 소스 파일의 컴파일타임, 읽기 전용 뷰를 제공합니다. APT의 주된 단점은 표준화되지 않았다는 것, 즉 APT는 Sun JDK에 한정된 도구라는 점입니다.

자바 SE 6은 사용자 정의 주석 처리기를 만들기 위한 표준화된 지원을 제공하는 플러그형 주석 처리 프레임워크라는 새로운 요소를 도입했습니다. 이것이 '플러그형'이라 불리는 이유는 주석 처리기가 javac에 동적으로 플러그인될 수 있고 자바 소스 파일에 있는 주석에 작용할 수 있기 때문입니다. 이 프레임워크는 두 부분으로 나뉘는데, 하나는 주석 처리기의 선언 및 상호 작용을 위한 API(패키지 javax.annotation.processing), 그리고 다른 하나는 자바 프로그래밍 언어의 모델링을 위한 API(패키지 javax.lang.model)입니다.


사용자 정의 주석 처리기 만들기

아래에서는 사용자 정의 처리기를 만들어 컴파일 태스크에 플러그인하는 방법을 설명합니다. 사용자 정의 주석 처리기는 AbstractProcessor(처리기 인터페이스의 기본 구현)를 확장하여 process() 메소드를 대체합니다.

주석 처리기 클래스는 2개의 클래스 레벨 주석, @SupportedAnnotationTypes@SupportedSourceVersion로 구성됩니다. SupportedSourceVersion 주석은 주석 처리기가 지원하는 최신 소스 버전을 명시합니다. SupportedAnnotationTypes 주석은 이 주석 처리기가 어느 주석에 관심을 두고 있는지 나타냅니다. 예를 들어, @SupportedAnnotationTypes ("javax.persistence.*")는 처리기가 Java Persistence API(JPA) 주석만 처리하면 되는 경우에 사용됩니다. 흥미로운 것은 지원되는 주석 유형이 @SupportedAnnotationTypes("*")로 명시된 경우에는 주석이 없더라도 주석 처리기가 호출된다는 사실입니다. 이로 인해 우리는 트리 API와 함께 모델링 API를 활용하여 일반적인 용도의 소스 코드 처리를 수행할 수 있습니다. 이 API들을 이용하면 수정자, 필드, 메소드 등과 관련된 유용한 정보를 다수 얻을 수 있습니다. 아래는 사용자 정의 주석 처리기의 코드 일부입니다.


@SupportedSourceVersion(SourceVersion.RELEASE_6)
@SupportedAnnotationTypes("*")
public class CodeAnalyzerProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations,
            RoundEnvironment roundEnvironment) {
        for (Element e : roundEnvironment.getRootElements()) {
                System.out.println("Element is "+ e.getSimpleName());
                // Add code here to analyze each root element
        }
        return true;
    }
}

이 주석 처리기는 소스 코드에 어떤 주석이 있고, 어느 처리기가 이용 가능하도록 구성되어 있으며 그 이용 가능한 처리기는 어떤 주석 유형을 처리하는가에 따라 호출됩니다. 주석 처리는 여러 회 발생할 수 있습니다. 예를 들어, 1회차에서는 원래의 입력 자바 소스 파일이 처리되고 2회차에서는 1회차의 처리에서 생성된 파일들이 고려되는 식입니다. 사용자 정의 처리기는 AbstractProcessorprocess()를 대체하게 됩니다. 이 메소드는 두 개의 인수를 갖습니다.

  1. 소스 파일에 있는 TypeElements/annotations의 집합
  2. 주석 처리기의 현재 처리 회차에 관한 정보를 담고 있는 RoundEnvironment

어느 처리기가 자신이 지원하는 주석 유형을 요구하면 process() 메소드는 true를 반환하며, 다른 처리기는 이 주석에 호출되지 않습니다. 그렇지 아니면, process() 메소드가 false 값을 반환하고 그 다음 순서로 이용 가능한 처리기가 호출됩니다.


주석 처리기 연결

이제 사용자 정의 주석 처리기가 준비되었으므로, 컴파일 프로세스 중에 이 처리기를 어떻게 호출할 수 있는지 살펴보겠습니다. 이 처리기는 javac 명령줄 유틸리티를 통해 또는 독립형 자바 클래스를 통해 프로그래밍 방식으로 호출될 수 있습니다. 자바 SE 6의 javac 유틸리티는 플러그인할 사용자 정의 주석 처리기의 전체 이름을 받아들이는 -processor라는 옵션을 제공합니다. 이것의 구문은 다음과 같습니다.


javac -processor demo.codeanalyzer.CodeAnalyzerProcessor TestClass.java

여기에서, CodeAnalyzerProcessor는 주석 처리기이며 TestClass는 처리할 입력 자바 파일입니다. 이 유틸리티는 클래스 경로에서 CodeAnalyzerProcessor를 찾으므로 이 클래스를 클래스 경로(classpath)에 넣는 것이 중요합니다.

아래는 프로그래밍을 통해 이 처리기에 플러그인하기 위해 수정된 코드입니다. CompilationTasksetProcessors() 메소드를 이용하면 컴파일 태스크에 다수의 주석 처리기를 플러그인할 수 있습니다. 이 메소드는 call() 메소드 전에 호출되어야 합니다. 또한 주석 처리기가 컴파일 태스크에 플러그인되어 있다면 주석 처리가 먼저 진행되고, 그 후에야 컴파일 태스크가 진행된다는 점에 유의하십시오. 물론, 이 코드가 컴파일 오류를 일으키면 주석 처리는 이루어지지 않습니다.


CompilationTask task = compiler.getTask(null, fileManager, null,
                                        null, null, compilationUnits1);
                                        
// Create a list to hold annotation processors
LinkedList<AbstractProcessor> processors = new LinkedList<AbstractProcessor>();

// Add an annotation processor to the list
processors.add(new CodeAnalyzerProcessor());

// Set the annotation processor to the compiler task
task.setProcessors(processors);

// Perform the compilation task.
task.call();

위 코드를 실행하면 주석 처리기가 TestClass.java 컴파일 중에 "TestClass"라는 이름을 인쇄합니다.


AST(Abstract Syntax Tree) 액세스: 컴파일러 트리 API

AST(Abstract Syntax Tree)는 소스의 읽기 전용 뷰로서 자바 코드를 노드의 트리로 표현합니다. 여기에서 각 노드는 자바 프로그래밍 언어 구성체 또는 트리를 나타내며 각 노드의 자녀는 이 트리의 의미 있는 구성요소를 나타냅니다. 예를 들어, 자바는 ClassTree로 표시되며 메소드 선언은 MethodTree로, 변수 선언은 VariableTree로, 주석은 AnnotationTree로 표시되는 식입니다.

컴파일러 트리 API는 자바 소스 코드의 AST(Abstract Syntax Tree)에 대한 액세스를 제공하며 이 AST에 연산을 수행할 수 있는 TreeVisitor, TreeScanner, 기타 등등의 유틸리티도 제공합니다. 모든 자녀 트리 노드를 방문하여 필드, 메소드, 주석 및 기타 클래스 요소에 대한 필요 정보를 추출하는 TreeVisitor를 이용하면 소스 콘텐츠에 대한 더 깊은 분석이 가능합니다. 이 트리 방문자들은 방문자 디자인 패턴의 스타일에 구현됩니다. 트리의 승인 메소드에 방문자가 전달되면 그 트리에 가장 적합한 visitXYZ 메소드가 호출됩니다.

자바 컴파일러 트리 API는 TreeVisitor의 세 가지 구현체를 제공하는데, 각각 SimpleTreeVisitor, TreePathScanner, TreeScanner입니다. 데모 애플리케이션에서는 TreePathScanner를 이용하여 자바 소스 파일에 관한 정보를 추출합니다. TreePathScanner는 모든 자녀 트리 노드를 방문하여 부모 노드의 경로 유지를 지원하는 TreeVisitor입니다. 이 트리를 스캔하려면 TreePathScannerscan() 메소드가 호출되어야 합니다. 특정 유형의 노드를 방문하려면 그에 상응하는 visitXYZ 메소드를 대체하면 됩니다. 방문 메소드 내부에서 super.visitXYZ를 호출해서 직계 하위 노드를 방문합니다. 일반적인 방문자 클래스의 코드는 다음과 같습니다.


public class CodeAnalyzerTreeVisitor extends TreePathScanner<Object, Trees>  {
    @Override
    public Object visitClass(ClassTree classTree, Trees trees) {
        ---- some code ----
        return super.visitClass(classTree, trees);
    }
    @Override
    public Object visitMethod(MethodTree methodTree, Trees trees) {
        ---- some code ----
        return super.visitMethod(methodTree, trees);
    }
} 

이 방문 메소드에서는 두 개의 인수를 허락합니다. 하나는 노드를 나타내는 트리(클래스 노드는 ClassTree, 메소드 노드는 MethodTree 등)이고 다른 하나는 Trees 개체입니다. Trees 클래스는 트리에 있는 요소의 경로 정보를 가져오는 유틸리티 메소드를 제공합니다. Trees 개체는 JSR 269와 컴파일러 트리 API 사이의 다리 역할을 한다는 점에 유의해야 합니다. 이 예에서는 TestClass 자체가 유일한 루트 요소입니다.


CodeAnalyzerTreeVisitor visitor = new CodeAnalyzerTreeVisitor();

@Override
public void init(ProcessingEnvironment pe) {
        super.init(pe);
        trees = Trees.instance(pe);
}
for (Element e : roundEnvironment.getRootElements()) {
        TreePath tp = trees.getPath(e);
        // invoke the scanner
        visitor.scan(tp, trees);
}

아래에서는 트리 API를 이용하여 소스 코드 정보를 검색하는 방법을 설명하고 코드 검증에 사용하는 일반적인 모델을 살펴봅니다. visitClass() 메소드는 ClassTrees를 인수로 하여 AST 안에서 class, interface 또는 enum 유형을 방문할 때마다 호출됩니다. 이와 유사하게 visitMethod() 메소드는 MethodTree를 인수로 갖고 있는 모든 메소드에 호출되며, visitVariable()VariableTree를 인수로 갖고 있는 모든 변수에 대해 호출됩니다.


@Override
public Object visitClass(ClassTree classTree, Trees trees) {
         //Storing the details of the visiting class into a model
         JavaClassInfo clazzInfo = new JavaClassInfo();

        // Get the current path of the node     
        TreePath path = getCurrentPath();

        //Get the type element corresponding to the class
        TypeElement e = (TypeElement) trees.getElement(path);

        //Set qualified class name into model
        clazzInfo.setName(e.getQualifiedName().toString());

        //Set extending class info
        clazzInfo.setNameOfSuperClass(e.getSuperclass().toString());

        //Set implementing interface details
        for (TypeMirror mirror : e.getInterfaces()) {
                clazzInfo.addNameOfInterface(mirror.toString());
        }
        return super.visitClass(classTree, trees);
  }

이 코드 부분에서 사용된 JavaClassInfo는 자바 코드에 관한 정보를 저장하기 위한 사용자 정의 모델입니다. 이 코드를 실행한 후에는 완전한 클래스 이름, 수퍼클래스 이름, TestClass에 의해 구현되는 인터페이스 등 클래스에 관련된 정보가 추출되어 나중에 확인할 수 있도록 사용자 정의 모델에 저장됩니다.


소스 위치 설정

지금까지 AST의 다양한 노드에 관한 정보를 입수하고 모델 개체에 클래스, 메소드, 필드 정보를 입력했습니다. 이 정보를 이용하면 소스가 양호한 프로그래밍 관행을 따르고 있는지 규격을 지키고 있는지 등을 확인할 수 있습니다. 이 정보는 Checkstyle 또는 FindBugs 같은 확인 도구에 매우 유용하지만, 여기에는 소스 토큰의 자세한 위치 정보가 필요하고 그것은 규칙을 위반하는 것이므로, 사용자에게 오류 위치 상세 정보를 제공합니다.

컴파일러 트리 API의 일부인 SourcePositions 개체는 컴파일 단위 트리 안에 모든 AST 노드의 위치를 유지합니다. 이 개체는 파일 내에서 ClassTree, MethodTree, FieldTree 등의 시작 위치와 종료 위치에 관한 유용한 정보를 제공합니다. 위치는 CompilationUnit의 시작 위치로부터의 간단한 문자 오프셋으로 정의되며 여기에서 첫 번째 문자는 오프셋 0에 있습니다. 아래의 코드 일부는 컴파일 단위의 시작 위치에서 전달된 Tree 개체의 문자 오프셋 위치를 얻을 수 있는 방법을 보여줍니다.


public static LocationInfo getLocationInfo(Trees trees, 
                                                TreePath path, Tree tree) {
        LocationInfo locationInfo = new LocationInfo();
        SourcePositions sourcePosition = trees.getSourcePositions();
        long startPosition = sourcePosition.
                        getStartPosition(path.getCompilationUnit(), tree);
        locationInfo.setStartOffset((int) startPosition);
        return locationInfo;
}

그러나, 클래스 또는 메소드 자체의 이름을 알려주는 토큰의 위치를 알아야 할 경우에는 이것으로는 충분하지 않습니다. 소스 안에서 실제 토큰의 위치를 찾으려면 소스 파일의 char 컨텐츠 안에서 토큰을 검색하는 것이 하나의 방법입니다. 아래 제시된 바와 같이 먼저 컴파일 단위에 상응하는 JavaFileObject로부터 char 컨텐츠를 얻을 수 있습니다.


//Get the compilation unit tree from the tree path
CompilationUnitTree compileTree = treePath.getCompilationUnit();

//Get the java source file which is being processed
JavaFileObject file = compileTree.getSourceFile();

// Extract the char content of the file into a string
String javaFile = file.getCharContent(true).toString();

//Convert the java file content to a  character buffer
CharBuffer charBuffer = CharBuffer.wrap (javaFile.toCharArray()); 

아래의 코드는 소스 내부에서 클래스 이름 토큰의 위치를 찾아냅니다. 클래스 이름 토큰의 실제 위치를 알아내기 위해 java.util.regex.Pattern 클래스와 java.util.regex.Matcher 클래스가 사용되었습니다. 자바 소스의 컨텐츠는 java.nio.CharBuffer를 사용하는 문자 버퍼로 변환되었습니다. 매처(matcher)는 컴파일 단위 트리 단위 트리 내부의 클래스 트리 시작 위치에서 시작하여 문자 버퍼에서 클래스 이름과 일치하는 것으로 처음 나타난 토큰을 검색합니다.


LocationInfo clazzNameLoc = (LocationInfo) clazzInfo.
                        getLocationInfo();
 int startIndex = clazzNameLoc.getStartOffset();
 int endIndex = -1;
 if (startIndex >= 0) {
   String strToSearch = buffer.subSequence(startIndex, 
   buffer.length()).toString();
   Pattern p = Pattern.compile(clazzName);
   Matcher matcher = p.matcher(strToSearch);
   matcher.find();
   startIndex = matcher.start() + startIndex;
   endIndex = startIndex + clazzName.length();
  } 
 clazzNameLoc.setStartOffset(startIndex);
 clazzNameLoc.setEndOffset(endIndex);
 clazzNameLoc.setLineNumber(compileTree.getLineMap().
              getLineNumber(startIndex));

컴파일러 트리 API의 LineMap 클래스는 CompilationUnitTree 안의 문자 위치와 라인 수를 보여주는 맵을 제공합니다. CompilationUnitTreegetLineMap() 메소드에 시작 오프셋 위치를 전달하여, 토큰의 라인 수를 알아낼 수 있습니다.


규칙과 대조하여 소스 검증하기

이제 AST에서 필요한 정보를 성공적으로 찾아냈으므로, 다음 과제는 현재의 소스가 미리 정의된 코딩 표준 규칙을 만족했는가를 확인하는 것입니다. 코딩 규칙은 XML 파일에 구성되어 있으며 RuleEngine이라는 사용자 정의 클래스를 통해 관리됩니다. 이 클래스는 XML 파일에서 규칙을 가져와 하나씩 차례로 처리합니다. 클래스가 충족하지 못한 규칙이 있으면 ErrorDescription 개체의 목록을 반환합니다. ErrorDescription 개체에는 오류 메시지와, 소스 코드에서 그 오류의 위치 정보가 들어 있습니다.


ClassFile clazzInfo = ClassModelMap.getInstance().
                getClassInfo(className);
for (JavaCodeRule rule : getRules()) {
        // apply rules one by one
        Collection<ErrorDescription> problems = rule.execute(clazzInfo);
        if (problems != null) {
                problemsFound.addAll(problems);
        }
}

각 규칙은 자바 클래스로 구현되며, 검증할 클래스의 모델 정보는 이 클래스로 전달됩니다. 규칙 클래스에는 이 모델 정보를 이용하여 규칙 논리를 검증할 논리가 들어 있습니다. 아래는 샘플 규칙(OverrideEqualsHashCode)의 구현을 보여줍니다. 이 규칙은 equal() 메소드를 대체하는 클래스는 hashcode() 메소드도 반드시 대체할 것을 요구합니다. 여기에서는 클래스의 메소드를 반복 적용하여 equals()hashcode() 협약을 따르는지 확인합니다. TestClass에는 hashcode() 메소드는 없는 반면, equals() 메소드는 있으므로 규칙은 적절한 오류 메시지와 그 오류의 위치 상세 정보가 들어 있는 ErrorDescription 모델을 반환하게 됩니다.


public class OverrideEqualsHashCode extends JavaClassRule {
    @Override
    protected Collection<ErrorDescription> apply(ClassFile clazzInfo) {
        boolean hasEquals = false;
        boolean hasHashCode = false;
        Location errorLoc = null;
        for (Method method : clazzInfo.getMethods()) {
            String methodName = method.getName();
            ArrayList paramList = (ArrayList) method.getParameters();
            if ("equals".equals(methodName) && paramList.size() == 1) {
                if ("java.lang.Object".equals(paramList.get(0))) {
                    hasEquals = true;
                    errorLoc = method.getLocationInfo();
                }
            } else if ("hashCode".equals(methodName) &&

                method.getParameters().size() == 0) {
                hasHashCode = true;
            }
        }
        if (hasEquals) {
            if (hasHashCode) {
                return null;
            } else {
                StringBuffer errrMsg = new StringBuffer();
                errrMsg.append(CodeAnalyzerUtil.
                                getSimpleNameFromQualifiedName(clazzInfo.getName()));
                errrMsg.append(" : The class that overrides 
                                        equals() should ");
                errrMsg.append("override hashcode()");
                Collection<ErrorDescription> errorList = new 
                                                ArrayList<ErrorDescription>();
                errorList.add(setErrorDetails(errrMsg.toString(), 
                                                        errorLoc));
                return errorList;
            }
        }
        return null;
    }
}

샘플 실행

자료 섹션에서는 이 데모 애플리케이션의 바이너리 파일을 다운로드할 수 있습니다. 이 파일을 로컬 디렉토리에 저장하십시오. 명령줄에 아래의 명령을 입력하여 애플리케이션을 실행합니다.


java -classpath <JAVA_HOME>\lib\tools.jar;.; demo.codeanalyzer.main.Main <comma separated list of source files to be verified>

요약

여기에서는 새로운 자바 6 API를 활용하여 자바 코드에서 컴파일러를 호출하고, 플러그형 주석 처리기와 트리 방문자를 이용하여 소스 코드의 구문을 분석하는 방법을 설명했습니다. IDE 전용 구문 분석 논리 대신 표준 자바 API를 사용함으로써 다양한 도구와 환경에 코드를 재사용하는 것이 가능해졌습니다. 여기에서는 세 개의 컴파일러 관련 API를 아주 간략히 살펴보았지만, 이 API들을 자세히 알아보면 이 밖에도 유용한 기능들이 매우 많습니다.


자료



이 글의 영문 원본은
Source Code Analysis Using Java 6 APIs
에서 보실 수 있습니다.

"Java SE" 카테고리의 다른 글

2008/05/21 10:59 2008/05/21 10:59

TRACKBACK :: http://blog.sdnkorea.com/blog/trackback/578

댓글을 달아 주세요

[로그인][오픈아이디란?]

◀ Prev 1  ... 73 74 75 76 77 78 79 80 81  ... 624  Next ▶