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 static org.opengion.fukurou.system.HybsConst.CR; // 6.1.0.0 (2014/12/26) refactoring 019import org.opengion.fukurou.system.OgRuntimeException ; // 6.4.2.0 (2016/01/29) 020 021import java.io.File; 022import java.io.StringWriter; 023import java.net.MalformedURLException; 024import java.net.URL; 025import java.net.URLClassLoader; 026import java.util.Arrays; 027import java.util.Map; 028import java.util.WeakHashMap; 029import java.util.Collections; // 6.4.3.1 (2016/02/12) refactoring 030import java.security.AccessController; // 6.1.0.0 (2014/12/26) findBugs 031import java.security.PrivilegedAction; // 6.1.0.0 (2014/12/26) findBugs 032import java.lang.reflect.InvocationTargetException; // Ver7.0.0.0 033 034import javax.tools.JavaCompiler; 035import javax.tools.StandardJavaFileManager; 036import javax.tools.ToolProvider; 037import javax.tools.JavaCompiler.CompilationTask; 038 039/** 040 * AutoCompile機能、HotDeploy機能を実現するためのクラスローダーです。 041 * 042 * AutoCompile機能は、クラスの動的コンパイルを行います。 043 * AutoCompile機能を有効にするには、コンストラクタで与えられるHybsLoaderConfigオブジェクトで、 044 * AutoCompileフラグをtrueにしておく必要があります。 045 * 046 * HotDeploy機能は、クラスの動的ロードを行います。 047 * HotDeploy機能を有効にするには、コンストラクタで与えられるHybsLoaderConfigオブジェクトで、 048 * HotDeployフラグをtrueにしておく必要があります。 049 * 050 * (1)クラスの動的コンパイル 051 * {@link #loadClass(String)}メソッドが呼ばれた場合に、ソースディレクトより、対象となるソースファイルを 052 * 検索し、クラスのコンパイルを行います。 053 * コンパイルが行われる条件は、「クラスファイルが存在しない」または「クラスファイルのタイムスタンプがソースファイルより古い」です。 054 * 055 * コンパイルを行うには、JDKに含まれるtools.jarが存在している必要があります。 056 * tools.jarが見つからない場合、エラーとなります。 057 * 058 * また、コンパイルのタスクのクラス(オブジェクト)は、JVMのシステムクラスローダー上のクラスに存在しています。 059 * このため、サーブレットコンテナで、通常読み込まれるWEB-INF/classes,WEB-INF/lib以下のクラスファイルも、 060 * そのままでは参照することができません。 061 * これらのクラスを参照する場合は、HybsLoaderConfigオブジェクトに対してクラスパスを設定しておく必要があります。 062 * 063 * (2)クラスロード 064 * クラスの動的ロードは、クラスローダーの入れ替えによって実現しています。 065 * HotDeploy機能を有効にした場合、読み込むクラス単位にURLClassLoaderを生成しています。 066 * クラスロードを行う際に、URLClassLoaderを新しく生成することで、クラスの再ロードを行っています。 067 * つまり、HotDeployにより読み込まれるそれぞれのクラスは、お互いに独立した(平行な位置に存在する)関係に 068 * なります。 069 * このため、あるHotDeployによりロードされたクラスAから、同じくHotDeployによりロードされたクラスBを直接参照 070 * することができません。 071 * この場合は、クラスBのインターフェースを静的なクラスローダー(クラスAから参照できる位置)に配置することで、クラスB 072 * のオブジェクトにアクセスすることができます。 073 * 074 * @og.rev 5.1.1.0 (2009/12/01) 新規作成 075 * @og.group 業務ロジック 076 * 077 * @version 5.0 078 * @author Hiroki Nakamura 079 * @since JDK1.6, 080 */ 081public class HybsLoader { 082 083 // HotDeploy機能を使用しない場合のURLClossLoaderのキャッシュキー 084 private static final String CONST_LOADER_KEY = "CONST_LOADER_KEY"; 085 086 private static final JavaCompiler COMPILER = ToolProvider.getSystemJavaCompiler(); 087 private static final StandardJavaFileManager FILE_MANAGER = COMPILER.getStandardFileManager(null, null, null); 088 089 /** 6.4.3.1 (2016/02/12) Collections.synchronizedMap で同期処理を行います。 */ 090 private final Map<String, HybsURLClassLoader> loaderMap = Collections.synchronizedMap( new WeakHashMap<>() ); 091 /** 6.4.3.1 (2016/02/12) Collections.synchronizedMap で同期処理を行います。 */ 092 private final Map<String, String> clsNameMap = Collections.synchronizedMap( new WeakHashMap<>() ); 093 private final String srcDir ; 094 private final String classDir ; 095 private final boolean isHotDeploy ; 096 private final boolean isAutoCompile ; 097 private final String classPath ; 098 099 /** 100 * HybsLoaderOptionを使用してHybsLoaderオブジェクトを生成します。 101 * 102 * @param option HybsLoaderを構築するための設定情報 103 */ 104 public HybsLoader( final HybsLoaderConfig option ) { 105 srcDir = option.getSrcDir() ; 106 classDir = option.getClassDir() ; 107 isHotDeploy = option.isHotDeploy() ; 108 isAutoCompile = option.isAutoCompile(); 109 classPath = option.getClassPath() ; 110 } 111 112 /** 113 * 指定されたクラス名のクラスをロードします。 114 * クラス名については、クラス自身の名称のみを指定することができます。 115 * (パッケージ名を含めた完全な形のクラス名を指定することもできます) 116 * 117 * @param clsNm クラス名 118 * 119 * @return クラス 120 */ 121 public Class<?> load( final String clsNm ) { 122 final String clsName = getQualifiedName( clsNm ); 123 if( isAutoCompile ) { 124 compileClass( clsName ); 125 } 126 final Class<?> cls = loadClass( clsName ); 127 128 return cls; 129 } 130 131 /** 132 * 指定されたクラス名のクラスをロードし、デフォルトコンストラクターを使用して 133 * インスタンスを生成します。 134 * 135 * @og.rev 5.1.8.0 (2010/07/01) Exceptionのエラーメッセージの修正(状態の出力) 136 * @og.rev 6.8.2.3 (2017/11/10) java9対応(cls.newInstance() → cls.getDeclaredConstructor().newInstance()) 137 * 138 * @param clsName クラス名(Qualified Name) 139 * 140 * @return インスタンス 141 */ 142 public Object newInstance( final String clsName ) { 143 final Class<?> cls = load( clsName ); 144 Object obj = null; 145 try { 146 obj = cls.getDeclaredConstructor().newInstance(); // Ver7.0.0.0 147 } 148 catch( final InstantiationException | InvocationTargetException | NoSuchMethodException ex ) { // 6.8.2.3 (2017/11/10) 149 final String errMsg = "インスタンスの生成に失敗しました。[" + clsName + "]" ; 150 throw new OgRuntimeException( errMsg , ex ); 151 } 152 catch( final IllegalAccessException ex ) { 153 final String errMsg = "アクセスが拒否されました。[" + clsName + "]" ; 154 throw new OgRuntimeException( errMsg , ex ); 155 } 156 return obj; 157 } 158 159 /** 160 * クラス名より完全クラス名を検索します。 161 * 162 * @og.rev 5.2.1.0 (2010/10/01) クラスファイルが存在する場合は、エラーにしない。 163 * @og.rev 6.4.3.1 (2016/02/12) Collections.synchronizedMap に置き換え。 164 * 165 * @param clsNm クラス名 166 * 167 * @return 完全クラス名 168 */ 169 private String getQualifiedName( final String clsNm ) { 170 String clsName = null; 171 if( clsNm.indexOf( '.' ) >= 0 ) { 172 clsName = clsNm; 173 } 174 else { 175 clsName = clsNameMap.get( clsNm ); 176 if( clsName == null ) { 177 clsName = findFile( "", clsNm ); 178 } 179 if( clsName == null ) { 180 clsName = findFileByCls( "", clsNm ); 181 } 182 clsNameMap.put( clsNm, clsName ); 183 184 if( clsName == null ) { 185 throw new OgRuntimeException( "ソースファイルが存在しません。ファイル=[" + clsNm + "]" ); 186 } 187 } 188 return clsName; 189 } 190 191 /** 192 * クラス名に対応するJavaファイルを再帰的に検索します。 193 * 194 * @param path 既定パス 195 * @param nm クラス名 196 * 197 * @return 完全クラス名 198 */ 199 private String findFile( final String path, final String nm ) { 200 final String tmpSrcPath = srcDir + path; 201 final File[] files = new File( tmpSrcPath ).listFiles(); 202 if( files != null && files.length > 0 ) { 203 for( int i=0; i<files.length; i++ ) { 204 if( files[i].isDirectory() ) { 205 final String rtn = findFile( path + files[i].getName() + File.separator, nm ); 206 if( rtn != null && rtn.length() > 0 ) { 207 return rtn; 208 } 209 } 210 else if( ( nm + ".java" ).equals( files[i].getName() ) ) { 211 return path.replace( File.separatorChar, '.' ) + nm; 212 } 213 } 214 } 215 return null; 216 } 217 218 /** 219 * クラス名に対応するJavaファイルを再帰的に検索します。 220 * 221 * @og.rev 5.2.1.0 (2010/10/01) クラスファイルが存在する場合は、エラーにしない。 222 * 223 * @param path 既定パス 224 * @param nm クラス名 225 * 226 * @return 完全クラス名 227 */ 228 private String findFileByCls( final String path, final String nm ) { 229 final String tmpSrcPath = classDir + path; 230 final File[] files = new File( tmpSrcPath ).listFiles(); 231 if( files != null && files.length > 0 ) { 232 for( int i=0; i<files.length; i++ ) { 233 if( files[i].isDirectory() ) { 234 final String rtn = findFile( path + files[i].getName() + File.separator, nm ); 235 if( rtn != null && rtn.length() > 0 ) { 236 return rtn; 237 } 238 } 239 else if( ( nm + ".class" ).equals( files[i].getName() ) ) { 240 return path.replace( File.separatorChar, '.' ) + nm; 241 } 242 } 243 } 244 return null; 245 } 246 247 /** 248 * クラスをコンパイルします。 249 * 250 * @og.rev 5.1.8.0 (2010/07/01) ソースファイルのエンコードは、UTF-8にする。 251 * @og.rev 5.2.1.0 (2010/10/01) クラスファイルが存在する場合は、エラーにしない。 252 * 253 * @param clsNm クラス名 254 */ 255 private void compileClass( final String clsNm ) { 256 if( COMPILER == null ) { 257 throw new OgRuntimeException( "コンパイラクラスが定義されていません。tools.jarが存在しない可能性があります" ); 258 } 259 260 final String srcFqn = srcDir + clsNm.replace( ".", File.separator ) + ".java"; 261 final File srcFile = new File( srcFqn ); 262 final String classFqn = classDir + clsNm.replace( ".", File.separator ) + ".class"; 263 final File clsFile = new File ( classFqn ); 264 265 // クラスファイルが存在する場合は、エラーにしない。 266 if( !srcFile.exists() ) { 267 if( clsFile.exists() ) { 268 return; 269 } 270 throw new OgRuntimeException( "ソースファイルが存在しません。ファイル=[" + srcFqn + "]" ); 271 } 272 273 if( clsFile.exists() && srcFile.lastModified() <= clsFile.lastModified() ) { 274 return; 275 } 276 277 // 6.0.0.1 (2014/04/25) These nested if statements could be combined 278 if( !clsFile.getParentFile().exists() && !clsFile.getParentFile().mkdirs() ) { 279 throw new OgRuntimeException( "ディレクトリが作成できませんでした。ファイル=[" + clsFile + "]" ); 280 } 281 282 final StringWriter sw = new StringWriter(); 283 final File[] sourceFiles = { new File( srcFqn ) }; 284 // 5.1.8.0 (2010/07/01) ソースファイルのエンコードは、UTF-8にする。 285 final String[] cpOpts = new String[]{ "-d", classDir, "-classpath", classPath, "-encoding", "UTF-8" }; 286 287 final CompilationTask task = COMPILER.getTask(sw, FILE_MANAGER, null, Arrays 288 .asList(cpOpts), null, FILE_MANAGER 289 .getJavaFileObjects(sourceFiles)); 290 291 boolean isOk = false; 292 // lockしておかないと、java.lang.IllegalStateExceptionが発生することがある 293 synchronized( this ) { 294 isOk = task.call(); 295 } 296 if( !isOk ) { 297 throw new OgRuntimeException( "コンパイルに失敗しました。" + CR + sw.toString() ); 298 } 299 } 300 301 /** 302 * クラスをロードします。 303 * 304 * @og.rev 6.4.3.1 (2016/02/12) Collections.synchronizedMap に置き換え。 305 * 306 * @param clsNm クラス名 307 * 308 * @return ロードしたクラスオブジェクト 309 */ 310 private Class<?> loadClass( final String clsNm ) { 311 312 final String classFqn = classDir + clsNm.replace( ".", File.separator ) + ".class"; 313 final File clsFile = new File( classFqn ); 314 if( !clsFile.exists() ) { 315 throw new OgRuntimeException( "クラスファイルが存在しません。ファイル=[" + classFqn + "]" ); 316 } 317 final long lastModifyTime = clsFile.lastModified(); // 6.0.2.5 (2014/10/31) refactoring 318 319 HybsURLClassLoader loader = null; 320 // 6.4.1.1 (2016/01/16) PMD refactoring. Avoid declaring a variable if it is unreferenced before a possible exit point. 321 final String key = isHotDeploy ? clsNm : CONST_LOADER_KEY; 322 loader = loaderMap.get( key ); 323 if( loader == null || lastModifyTime > loader.getCreationTime() ) { // 6.0.2.5 (2014/10/31) refactoring 324 try { 325 // 6.3.9.1 (2015/11/27) In J2EE, getClassLoader() might not work as expected. Use Thread.currentThread().getContextClassLoader() instead.(PMD) 326 loader = new HybsURLClassLoader( new URL[] { new File( classDir ).toURI().toURL() }, Thread.currentThread().getContextClassLoader() ); 327 } 328 catch( final MalformedURLException ex ) { 329 throw new OgRuntimeException( "クラスロードのURL変換に失敗しました。ファイル=[" + classFqn + "]", ex ); 330 } 331 loaderMap.put( key, loader ); 332 } 333 334 Class<?> cls; 335 try { 336 cls = loader.loadClass( clsNm ); 337 } 338 catch( final ClassNotFoundException ex ) { 339 final String errMsg = "クラスが存在しません。ファイル=[" + classFqn + "]" ; 340 throw new OgRuntimeException( errMsg , ex ); 341 } 342 return cls; 343 } 344 345 /** 346 * このオブジェクトの内部表現を、文字列にして返します。 347 * 348 * @og.rev 6.1.0.0 (2014/12/26) refactoring 349 * 350 * @return オブジェクトの内部表現 351 * @og.rtnNotNull 352 */ 353 @Override 354 public String toString() { 355 return "srcDir=" + srcDir + " , classDir=" + classDir ; 356 } 357 358 /** 359 * URLClassLoaderを拡張し、クラスローダーの生成時間を管理できるようにしています。 360 */ 361 private static final class HybsURLClassLoader { // 6.3.9.1 (2015/11/27) final を追加 362 private final URLClassLoader loader; 363 private final long creationTime; 364 365 /** 366 * URL配列 を引数に取るコンストラクタ 367 * 368 * @param urls URL配列 369 */ 370 HybsURLClassLoader( final URL[] urls ) { 371 this( urls, null ); 372 } 373 374 /** 375 * URL配列と、クラスローダーを引数に取るコンストラクタ 376 * 377 * @param urls URL配列 378 * @param clsLd クラスローダー 379 */ 380 HybsURLClassLoader( final URL[] urls, final ClassLoader clsLd ) { 381 // 6.1.0.0 (2014/12/26) findBugs: Bug type DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED (click for details) 382 // new org.opengion.fukurou.util.HybsLoader$HybsURLClassLoader(URL[], ClassLoader) は、doPrivileged ブロックの中でクラスローダ java.net.URLClassLoader を作成するべきです。 383 loader = AccessController.doPrivileged( 384 new PrivilegedAction<URLClassLoader>() { 385 /** 386 * 特権を有効にして実行する PrivilegedAction<T> の run() メソッドです。 387 * 388 * このメソッドは、特権を有効にしたあとに AccessController.doPrivileged によって呼び出されます。 389 * 390 * @return URLClassLoaderオブジェクト 391 * @og.rtnNotNull 392 */ 393 public URLClassLoader run() { 394 return new URLClassLoader( urls, clsLd ); 395 } 396 } 397 ); 398 creationTime = System.currentTimeMillis(); 399 } 400 401 /** 402 * クラスをロードします。 403 * 404 * @param clsName クラス名の文字列 405 * @return Classオブジェクト 406 */ 407 /* default */ Class<?> loadClass( final String clsName ) throws ClassNotFoundException { 408 return loader.loadClass( clsName ); 409 } 410 411 /** 412 * 作成時間を返します。 413 * 414 * @return 作成時間 415 */ 416 /* default */ long getCreationTime() { 417 return creationTime; 418 } 419 } 420}