캠핑과 개발


JNI를 사용하여 c 함수를 호출할 때의 실제 라이브러리 파일을 로딩하기 위하여 다음과 같이한다.

  1. 환경 변수 PATH에 포함된 디렉토리에 dll 파일을 카피해 둔다.
  2. java 실행 시 -Djava.library.path=c:\library\path 와 같이 dll 파일이 있는 디렉토리를 설정한다.

그런데 위 방법들이 마땅치 않을 수 있다. 웹 어플리케이션의 경우 필요한 파일 모두를 해당 웹 어플리케이션 디렉토리 밑에 모두 모아두고 싶은 경우이다. 만약 1번의 방법을 사용한다면 해당 dll만 웹하고는 전혀 관계없는 C:\windows 같은 시스템 폴더에 두어야 한다. 혹은 dll이 있는 디렉토리 이름을 환경변수 PATH에 포함시켜야 한다. 역시 깔끔하지 않다. 2번 째 방법을 적용하려면 WAS의 실행 스크립트를 수정하여야 한다. 특정 웹 어플리케이션을 설치하기 위하여 WAS 자체를 건드리는 것은 바람직하지 않다.

실행 시에 System 속성 "java.library.path"를 설정하고, 그 변경된 것이 적용되면 좋겠는데, 실행전에 설정하지 않으면 적용되지 않는다.

java.lang.ClassLoader의 코드를 보면 "java.library.path"에 설정된 값을 초기에 한번 String[] usr_paths에 담아두고 그것을 계속 사용한다. reflection을 사용하여 usr_paths에 원하는 경로를 추가하게 하면 되겠다. 하지만 이런 방법은 바람직하진 않고, 또 어떤 위험성이 있는지도 추측하기 힘든 꼼수다.

아래 코드를 달긴 하지만, 어떤 위험성이 있는지는 아직 파악되지 않았다.
그리고 ClassLoader의 private 속성 usr_paths의 이름이 각 JVM마다 다 똑같다는 것도 확인 안되었고.
무엇 보다도 치명적인 것은 private으로 은닉화시켜 놓은 것은 언제든지 변경될 수 있고, 유지보수가 어렵다는 것이다.
꼼수다운 단점이다.

 public static void loadLibrary(String libraryPath, String libraryName) throws LibraryLoadingException {
     
  // 라이브러리 패스를 추가
     addLibrarayPath(libraryPath);
  // 라이브러리를 로딩    
     System.loadLibrary(libraryName);
     
    }

 private static void addLibrarayPath(String libraryPath) throws LibraryLoadingException {
  
  if(libraryPath==null) { return; }
  if(libraryPath.equals("")) { return; }
  
     Field usrPathsField;
  // ClassLoader의 String[] usr_paths 필드를 구하고
  // usr_paths의 값은 시스템 속성 java.library.path의 값으로 초기에 한번만 설정된다.
  try {
   usrPathsField = getField(ClassLoader.class, "usr_paths");
  } catch (ReflectionFailException e) {
   throw new LibraryLoadingException("library loading failed.", e);
  }

  if(usrPathsField==null) {
   throw new LibraryLoadingException("library loading failed. invalid attribute usr_paths");
  }

  // private으로 선언된 것을 접근 가능하게 바꿔주고.
     usrPathsField.setAccessible(true);
     
     String[] userPaths;
  try {
   // 실제 설정된 값을 구하고
   userPaths = (String[])usrPathsField.get(null);
  } catch (IllegalArgumentException e) {
   throw new LibraryLoadingException("library loading failed.", e);
  } catch (IllegalAccessException e) {
   throw new LibraryLoadingException("library loading failed.", e);
  }
     
  // 만약 추가할 패스가 이미 있다면 더 할일이 없다. 나간다.
     for(int i=0; i<userPaths.length; i++) {
      if(libraryPath.equalsIgnoreCase(userPaths[i])) {
       return;
      }
     }

  // 새로운 패스를 추가하고     
     String[] newUserPaths = new String[userPaths.length+1];
     for(int i=0; i<userPaths.length; i++) {
      newUserPaths[i] = userPaths[i];
     }
     newUserPaths[newUserPaths.length-1] = libraryPath;
     
  // 추가된 패스가 포함된 새로운 값을 설정한다.
     try {
   usrPathsField.set(null, newUserPaths);
  } catch (IllegalArgumentException e) {
   throw new LibraryLoadingException("library loading failed.", e);
  } catch (IllegalAccessException e) {
   throw new LibraryLoadingException("library loading failed.", e);
  }
     
 }


 public static Field getField(Class clazz, String fieldName) throws ReflectionFailException {
  
  if(clazz==null) { return null; }
  if(fieldName==null) { return null; }
  
  Field field = null;
  
  Exception rootException = null;
  try {
   field = clazz.getDeclaredField(fieldName);
   return field;
  } catch (SecurityException e) {
   rootException = e;
  } catch (NoSuchFieldException e) {
   rootException = e;
  }
  
  Class superClass = clazz.getSuperclass();
  while(superClass!=null) {
   try {
    field = superClass.getDeclaredField(fieldName);
    return field;
   } catch (SecurityException e) {
   } catch (NoSuchFieldException e) {
   }
   superClass = superClass.getSuperclass();
  }
  
  throw new ReflectionFailException("getting field "+fieldName+" failed.", rootException);
 }

 


2009/05/11 추가사항

이외에 다른 방법이 있다. System.loadLibrary()는 시스템 속성 "java.library.path"의 값을 사용하기 전에 메소드를 호출한 클래스의 클래스로더의 findLibrary(String libraryName)을 호출한다. 해당 라이브러리의 경로를 반환하는 메소드 이다. 만약 요 메소드의 반환값을 적절히 해주면 해결될 것이다. 가장 합벅적인(?)인 방법이다. 그러나 클래스 로드를 따로 개발해야 한다는 부담이 있다. 더군다나 native 라이브러리를 호출하는 코드를 건드리지 못한다면(다른 곳에서 가져온 라이브러리일 경우) 요 방법은 적용하지 못한다. 실제로 테스트해본 결과 로딩은 되긴 한다. 그러나 라이브러리간의 의존성에서 실패한다. a.dll이 있고 b.dll이 있고, a.dll이 b.dll을 사용하는 경우이다. java 코드에서는 a.dll을 잘 로딩했는데 a.dll이 b.dll을 로딩하지 못하였다. "Can't find dependent libraries" 메시지의 java.lang.UnsatisfiedLinkError가 발생한다. 이러한 것이 내가 작성한 코드의 버그인지 다른 원인인지도 모르는 상태에서 덮어 버렸다.

혹시나 해서 비슷한 코드를 찾아 보았다. theyard라는 프로젝트에 JNILibraryLoader라는 클래스가 있다.(http://code.google.com/p/theyard/source/detail?r=168). 가져다 사용해 보니 훌륭하게 동작한다. 내부를 들여다 보니 System.load(String fileName)을 사용하고 있다. 요메소드가 System.loadLibrary()와 무슨 차이가 있는지는 파악하진 않았다. System.load()가 실패하면 loadLibrary()를 호출한다. 그리고 더 마음에 드는 것은 OS별 라이브러리 파일의 확장자를 알아서 처리해 주고, 라이브러리의 버전까지도 명시할 수 있다. 실제 수정한 코드를 첨부한다. JniLibraryLoader.java 사용방법은 다음과 같다.
JniLibraryLoadder.load("c:/some/path", "myLib");


System.loadLibrary()메소드가 시스템 속성 "java.library.path"의 값을 그때 마다 읽지 않고 한번만 읽고는 그 값을 사용하게 한 것은 무언가 이유가 있을 것 같다. 뭔지는 잘 모르겠는데, 이렇게 처리하는 것이 그 무언지 모르는 위험성을 안고 가는것 같아 찜찜하다.

[출처] http://aploit.egloos.com/4937154

'DEVELOPMENT > Java' 카테고리의 다른 글

Commons-lang, Commons-io 사용 샘플  (0) 2011.06.01
JAVA Tip  (0) 2011.04.13
Velocity의 기본 문법  (0) 2011.02.11
Spring - Quartz를 사용하여 스케쥴러 구현하기  (0) 2011.02.08
EHCache를 이용한 캐시 구현  (0) 2011.01.05