001/* 002 * Copyright (c) 2009 The openGion Project. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 013 * either express or implied. See the License for the specific language 014 * governing permissions and limitations under the License. 015 */ 016package org.opengion.fukurou.util; 017 018import java.io.File; 019import java.io.StringWriter; 020import java.net.MalformedURLException; 021import java.net.URL; 022import java.net.URLClassLoader; 023import java.util.Arrays; 024import java.util.Map; 025import java.util.WeakHashMap; 026 027import javax.tools.JavaCompiler; 028import javax.tools.StandardJavaFileManager; 029import javax.tools.ToolProvider; 030import javax.tools.JavaCompiler.CompilationTask; 031 032/** 033 * AutoCompile機能、HotDeploy機能を実現するためのクラスローダーです。 034 * 035 * AutoCompile機能は、クラスの動的コンパイルを行います。 036 * AutoCompile機能を有効にするには、コンストラクタで与えられるHybsLoaderConfigオブジェクトで、 037 * AutoCompileフラグをtrueにしておく必要があります。 038 * 039 * HotDeploy機能は、クラスの動的ロードを行います。 040 * HotDeploy機能を有効にするには、コンストラクタで与えられるHybsLoaderConfigオブジェクトで、 041 * HotDeployフラグをtrueにしておく必要があります。 042 * 043 * (1)クラスの動的コンパイル 044 * {@link #loadClass(String)}メソッドが呼ばれた場合に、ソースディレクトより、対象となるソースファイルを 045 * 検索し、クラスのコンパイルを行います。 046 * コンパイルが行われる条件は、「クラスファイルが存在しない」または「クラスファイルのタイムスタンプがソースファイルより古い」です。 047 * 048 * コンパイルを行うには、JDKに含まれるtools.jarが存在している必要があります。 049 * tools.jarが見つからない場合、エラーとなります。 050 * 051 * また、コンパイルのタスクのクラス(オブジェクト)は、JVMのシステムクラスローダー上のクラスに存在しています。 052 * このため、サーブレットコンテナで、通常読み込まれるWEB-INF/classes,WEB-INF/lib以下のクラスファイルも、 053 * そのままでは参照することができません。 054 * これらのクラスを参照する場合は、HybsLoaderConfigオブジェクトに対してクラスパスを設定しておく必要があります。 055 * 056 * (2)クラスロード 057 * クラスの動的ロードは、クラスローダーの入れ替えによって実現しています。 058 * HotDeploy機能を有効にした場合、読み込むクラス単位にURLClassLoaderを生成しています。 059 * クラスロードを行う際に、URLClassLoaderを新しく生成することで、クラスの再ロードを行っています。 060 * つまり、HotDeployにより読み込まれるそれぞれのクラスは、お互いに独立した(平行な位置に存在する)関係に 061 * なります。 062 * このため、あるHotDeployによりロードされたクラスAから、同じくHotDeployによりロードされたクラスBを直接参照 063 * することができません。 064 * この場合は、クラスBのインターフェースを静的なクラスローダー(クラスAから参照できる位置)に配置することで、クラスB 065 * のオブジェクトにアクセスすることができます。 066 * 067 * @og.rev 5.1.1.0 (2009/12/01) 新規作成 068 * @og.group 業務ロジック 069 * 070 * @version 5.0 071 * @author Hiroki Nakamura 072 * @since JDK1.6, 073 */ 074public class HybsLoader { 075 private static final String CR = System.getProperty("line.separator"); 076 077 // HotDeploy機能を使用しない場合のURLClossLoaderのキャッシュキー 078 private static final String CONST_LOADER_KEY = "CONST_LOADER_KEY"; 079 080 private static final JavaCompiler COMPILER = ToolProvider.getSystemJavaCompiler(); 081 private static final StandardJavaFileManager FILE_MANAGER = COMPILER.getStandardFileManager(null, null, null); 082 083 private final Map<String, HybsURLClassLoader> loaderMap = new WeakHashMap<String, HybsURLClassLoader>(); 084 private final Map<String, String> clsNameMap = new WeakHashMap<String, String>(); 085 private final String srcDir; 086 private final String classDir; 087 private final boolean isHotDeploy; 088 private final boolean isAutoCompile; 089 private final String classPath; 090 091 /** 092 * HybsLoaderOptionを使用してHybsLoaderオブジェクトを生成します。 093 * 094 * @param option HybsLoaderを構築するための設定情報 095 */ 096 public HybsLoader( final HybsLoaderConfig option ) { 097 srcDir = option.getSrcDir(); 098 classDir = option.getClassDir(); 099 isHotDeploy = option.isHotDeploy(); 100 isAutoCompile = option.isAutoCompile(); 101 classPath = option.getClassPath(); 102 } 103 104 /** 105 * 指定されたクラス名のクラスをロードします。 106 * クラス名については、クラス自身の名称のみを指定することができます。 107 * (パッケージ名を含めた完全な形のクラス名を指定することもできます) 108 * 109 * @param clsNm クラス名 110 * 111 * @return クラス 112 */ 113 public Class<?> load( final String clsNm ) { 114 String clsName = getQualifiedName( clsNm ); 115 if( isAutoCompile ) { 116 compileClass( clsName ); 117 } 118 Class<?> cls = loadClass( clsName ); 119 120 return cls; 121 } 122 123 /** 124 * 指定されたクラス名のクラスをロードし、デフォルトコンストラクターを使用して 125 * インスタンスを生成します。 126 * 127 * @og.rev 5.1.8.0 (2010/07/01) Exceptionのエラーメッセージの修正(状態の出力) 128 * 129 * @param clsName クラス名(Qualified Name) 130 * 131 * @return インスタンス 132 */ 133 public Object newInstance( final String clsName ) { 134 Class<?> cls = load( clsName ); 135 Object obj = null; 136 try { 137 obj = cls.newInstance(); 138 } 139 catch( InstantiationException ex ) { 140 String errMsg = "インスタンスの生成に失敗しました。[" + clsName + "]" ; 141 throw new RuntimeException( errMsg , ex ); 142 } 143 catch( IllegalAccessException ex ) { 144 String errMsg = "アクセスが拒否されました。[" + clsName + "]" ; 145 throw new RuntimeException( errMsg , ex ); 146 } 147 return obj; 148 } 149 150 /** 151 * クラス名より完全クラス名を検索します。 152 * 153 * @og.rev 5.2.1.0 (2010/10/01) クラスファイルが存在する場合は、エラーにしない。 154 * 155 * @param clsNm クラス名 156 * 157 * @return 完全クラス名 158 */ 159 private String getQualifiedName( final String clsNm ) { 160 String clsName = null; 161 if( clsNm.indexOf( '.' ) >= 0 ) { 162 clsName = clsNm; 163 } 164 else { 165 synchronized( clsNameMap ) { 166 clsName = clsNameMap.get( clsNm ); 167 if( clsName == null ) { 168 clsName = findFile( "", clsNm ); 169 } 170 if( clsName == null ) { 171 clsName = findFileByCls( "", clsNm ); 172 } 173 clsNameMap.put( clsNm, clsName ); 174 } 175 if( clsName == null ) { 176 throw new RuntimeException( "ソースファイルが存在しません。ファイル=[" + clsNm + "]" ); 177 } 178 } 179 return clsName; 180 } 181 182 /** 183 * クラス名に対応するJavaファイルを再帰的に検索します。 184 * 185 * @param path 既定パス 186 * @param nm クラス名 187 * 188 * @return 完全クラス名 189 */ 190 private String findFile( final String path, final String nm ) { 191 String tmpSrcPath = srcDir + path; 192 File[] files = new File( tmpSrcPath ).listFiles(); 193 if( files != null && files.length > 0 ) { 194 for( int i=0; i<files.length; i++ ) { 195 if ( files[i].isDirectory() ) { 196 String rtn = findFile( path + files[i].getName() + File.separator, nm ); 197 if( rtn != null && rtn.length() > 0 ) { 198 return rtn; 199 } 200 } 201 else if( ( nm + ".java" ).equals( files[i].getName() ) ) { 202 return path.replace( File.separatorChar, '.' ) + nm; 203 } 204 } 205 } 206 return null; 207 } 208 209 /** 210 * クラス名に対応するJavaファイルを再帰的に検索します。 211 * 212 * @og.rev 5.2.1.0 (2010/10/01) クラスファイルが存在する場合は、エラーにしない。 213 * 214 * @param path 既定パス 215 * @param nm クラス名 216 * 217 * @return 完全クラス名 218 */ 219 private String findFileByCls( final String path, final String nm ) { 220 String tmpSrcPath = classDir + path; 221 File[] files = new File( tmpSrcPath ).listFiles(); 222 if( files != null && files.length > 0 ) { 223 for( int i=0; i<files.length; i++ ) { 224 if ( files[i].isDirectory() ) { 225 String rtn = findFile( path + files[i].getName() + File.separator, nm ); 226 if( rtn != null && rtn.length() > 0 ) { 227 return rtn; 228 } 229 } 230 else if( ( nm + ".class" ).equals( files[i].getName() ) ) { 231 return path.replace( File.separatorChar, '.' ) + nm; 232 } 233 } 234 } 235 return null; 236 } 237 238 /** 239 * クラスをコンパイルします。 240 * 241 * @og.rev 5.1.8.0 (2010/07/01) ソースファイルのエンコードは、UTF-8にする。 242 * @og.rev 5.2.1.0 (2010/10/01) クラスファイルが存在する場合は、エラーにしない。 243 * 244 * @param clsNm クラス名 245 */ 246 private void compileClass( final String clsNm ) { 247 if( COMPILER == null ) { 248 throw new RuntimeException( "コンパイラクラスが定義されていません。tools.jarが存在しない可能性があります" ); 249 } 250 251 String srcFqn = srcDir + clsNm.replace( ".", File.separator ) + ".java"; 252 File srcFile = new File( srcFqn ); 253 String classFqn = classDir + clsNm.replace( ".", File.separator ) + ".class"; 254 File clsFile = new File ( classFqn ); 255 256 // クラスファイルが存在する場合は、エラーにしない。 257 if( !srcFile.exists() ) { 258 if( clsFile.exists() ) { 259 return; 260 } 261 throw new RuntimeException( "ソースファイルが存在しません。ファイル=[" + srcFqn + "]" ); 262 } 263 264 if( clsFile.exists() && srcFile.lastModified() <= clsFile.lastModified() ) { 265 return; 266 } 267 268 // 6.0.0.1 (2014/04/25) These nested if statements could be combined 269 if( !clsFile.getParentFile().exists() && !clsFile.getParentFile().mkdirs() ) { 270 throw new RuntimeException( "ディレクトリが作成できませんでした。ファイル=[" + clsFile + "]" ); 271 } 272 273 StringWriter sw = new StringWriter(); 274 File[] sourceFiles = { new File( srcFqn ) }; 275 // 5.1.8.0 (2010/07/01) ソースファイルのエンコードは、UTF-8にする。 276 String[] cpOpts = new String[]{ "-d", classDir, "-classpath", classPath, "-encoding", "UTF-8" }; 277 278 CompilationTask task = COMPILER.getTask(sw, FILE_MANAGER, null, Arrays 279 .asList(cpOpts), null, FILE_MANAGER 280 .getJavaFileObjects(sourceFiles)); 281 282 boolean isOk = false; 283 // lockしておかないと、java.lang.IllegalStateExceptionが発生することがある 284 synchronized( this ) { 285 isOk = task.call(); 286 } 287 if( !isOk ) { 288 throw new RuntimeException( "コンパイルに失敗しました。" + CR + sw.toString() ); 289 } 290 } 291 292 /** 293 * クラスをロードします。 294 * 295 * @param clsNm クラス名 296 * 297 * @return ロードしたクラスオブジェクト 298 */ 299 private Class<?> loadClass( final String clsNm ) { 300 String key = isHotDeploy ? clsNm : CONST_LOADER_KEY; 301 302 String classFqn = classDir + clsNm.replace( ".", File.separator ) + ".class"; 303 File clsFile = new File( classFqn ); 304 if( !clsFile.exists() ) { 305 throw new RuntimeException( "クラスファイルが存在しません。ファイル=[" + classFqn + "]" ); 306 } 307 long lastClassModifyTime = clsFile.lastModified(); 308 309 HybsURLClassLoader loader = null; 310 synchronized( loaderMap ) { 311 loader = loaderMap.get( key ); 312 if( loader == null || lastClassModifyTime > loader.getCreationTime() ) { 313 try { 314 loader = new HybsURLClassLoader( new URL[] { new File( classDir ).toURI().toURL() }, this.getClass().getClassLoader() ); 315 } 316 catch( MalformedURLException ex ) { 317 throw new RuntimeException( "クラスロードのURL変換に失敗しました。ファイル=[" + classFqn + "]", ex ); 318 } 319 loaderMap.put( key, loader ); 320 } 321 } 322 323 Class<?> cls; 324 try { 325 cls = loader.loadClass( clsNm ); 326 } 327 catch( ClassNotFoundException ex ) { 328 String errMsg = "クラスが存在しません。ファイル=[" + classFqn + "]" ; 329 throw new RuntimeException( errMsg , ex ); 330 } 331 return cls; 332 } 333 334 /** 335 * URLClassLoaderを拡張し、クラスローダーの生成時間を管理できるようにしています。 336 */ 337 private static class HybsURLClassLoader { 338 final URLClassLoader loader; 339 final long creationTime; 340 341 HybsURLClassLoader( final URL[] urls, final ClassLoader clsLd ) { 342 loader = new URLClassLoader( urls, clsLd ); 343 creationTime = System.currentTimeMillis(); 344 } 345 346 HybsURLClassLoader( final URL[] urls ) { 347 this( urls, null ); 348 } 349 350 Class<?> loadClass( final String clsName ) throws ClassNotFoundException { 351 return loader.loadClass( clsName ); 352 } 353 354 long getCreationTime() { 355 return creationTime; 356 } 357 } 358}