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 * @og.rev 6.9.7.0 (2018/05/14) 中間変数を用意せず、直接返します。 118 * 119 * @param clsNm クラス名 120 * @return クラス 121 */ 122 public Class<?> load( final String clsNm ) { 123 final String clsName = getQualifiedName( clsNm ); 124 if( isAutoCompile ) { 125 compileClass( clsName ); 126 } 127// final Class<?> cls = loadClass( clsName ); 128 129// return cls; 130 return loadClass( clsName ); // 6.9.7.0 (2018/05/14) 131 } 132 133 /** 134 * 指定されたクラス名のクラスをロードし、デフォルトコンストラクターを使用して 135 * インスタンスを生成します。 136 * 137 * @og.rev 5.1.8.0 (2010/07/01) Exceptionのエラーメッセージの修正(状態の出力) 138 * @og.rev 6.8.2.3 (2017/11/10) java9対応(cls.newInstance() → cls.getDeclaredConstructor().newInstance()) 139 * 140 * @param clsName クラス名(Qualified Name) 141 * 142 * @return インスタンス 143 */ 144 public Object newInstance( final String clsName ) { 145 final Class<?> cls = load( clsName ); 146 Object obj = null; 147 try { 148 obj = cls.getDeclaredConstructor().newInstance(); // Ver7.0.0.0 149 } 150 catch( final InstantiationException | InvocationTargetException | NoSuchMethodException ex ) { // 6.8.2.3 (2017/11/10) 151 final String errMsg = "インスタンスの生成に失敗しました。[" + clsName + "]" ; 152 throw new OgRuntimeException( errMsg , ex ); 153 } 154 catch( final IllegalAccessException ex ) { 155 final String errMsg = "アクセスが拒否されました。[" + clsName + "]" ; 156 throw new OgRuntimeException( errMsg , ex ); 157 } 158 return obj; 159 } 160 161 /** 162 * クラス名より完全クラス名を検索します。 163 * 164 * @og.rev 5.2.1.0 (2010/10/01) クラスファイルが存在する場合は、エラーにしない。 165 * @og.rev 6.4.3.1 (2016/02/12) Collections.synchronizedMap に置き換え。 166 * 167 * @param clsNm クラス名 168 * 169 * @return 完全クラス名 170 */ 171 private String getQualifiedName( final String clsNm ) { 172 String clsName = null; 173 if( clsNm.indexOf( '.' ) >= 0 ) { 174 clsName = clsNm; 175 } 176 else { 177 clsName = clsNameMap.get( clsNm ); 178 if( clsName == null ) { 179 clsName = findFile( "", clsNm ); 180 } 181 if( clsName == null ) { 182 clsName = findFileByCls( "", clsNm ); 183 } 184 clsNameMap.put( clsNm, clsName ); 185 186 if( clsName == null ) { 187 throw new OgRuntimeException( "ソースファイルが存在しません。ファイル=[" + clsNm + "]" ); 188 } 189 } 190 return clsName; 191 } 192 193 /** 194 * クラス名に対応するJavaファイルを再帰的に検索します。 195 * 196 * @param path 既定パス 197 * @param nm クラス名 198 * 199 * @return 完全クラス名 200 */ 201 private String findFile( final String path, final String nm ) { 202 final String tmpSrcPath = srcDir + path; 203 final File[] files = new File( tmpSrcPath ).listFiles(); 204 if( files != null && files.length > 0 ) { 205 for( int i=0; i<files.length; i++ ) { 206 if( files[i].isDirectory() ) { 207 final String rtn = findFile( path + files[i].getName() + File.separator, nm ); 208 if( rtn != null && rtn.length() > 0 ) { 209 return rtn; 210 } 211 } 212 else if( ( nm + ".java" ).equals( files[i].getName() ) ) { 213 return path.replace( File.separatorChar, '.' ) + nm; 214 } 215 } 216 } 217 return null; 218 } 219 220 /** 221 * クラス名に対応するJavaファイルを再帰的に検索します。 222 * 223 * @og.rev 5.2.1.0 (2010/10/01) クラスファイルが存在する場合は、エラーにしない。 224 * 225 * @param path 既定パス 226 * @param nm クラス名 227 * 228 * @return 完全クラス名 229 */ 230 private String findFileByCls( final String path, final String nm ) { 231 final String tmpSrcPath = classDir + path; 232 final File[] files = new File( tmpSrcPath ).listFiles(); 233 if( files != null && files.length > 0 ) { 234 for( int i=0; i<files.length; i++ ) { 235 if( files[i].isDirectory() ) { 236 final String rtn = findFile( path + files[i].getName() + File.separator, nm ); 237 if( rtn != null && rtn.length() > 0 ) { 238 return rtn; 239 } 240 } 241 else if( ( nm + ".class" ).equals( files[i].getName() ) ) { 242 return path.replace( File.separatorChar, '.' ) + nm; 243 } 244 } 245 } 246 return null; 247 } 248 249 /** 250 * クラスをコンパイルします。 251 * 252 * @og.rev 5.1.8.0 (2010/07/01) ソースファイルのエンコードは、UTF-8にする。 253 * @og.rev 5.2.1.0 (2010/10/01) クラスファイルが存在する場合は、エラーにしない。 254 * 255 * @param clsNm クラス名 256 */ 257 private void compileClass( final String clsNm ) { 258 if( COMPILER == null ) { 259 throw new OgRuntimeException( "コンパイラクラスが定義されていません。tools.jarが存在しない可能性があります" ); 260 } 261 262 final String srcFqn = srcDir + clsNm.replace( ".", File.separator ) + ".java"; 263 final File srcFile = new File( srcFqn ); 264 final String classFqn = classDir + clsNm.replace( ".", File.separator ) + ".class"; 265 final File clsFile = new File ( classFqn ); 266 267 // クラスファイルが存在する場合は、エラーにしない。 268 if( !srcFile.exists() ) { 269 if( clsFile.exists() ) { 270 return; 271 } 272 throw new OgRuntimeException( "ソースファイルが存在しません。ファイル=[" + srcFqn + "]" ); 273 } 274 275 if( clsFile.exists() && srcFile.lastModified() <= clsFile.lastModified() ) { 276 return; 277 } 278 279 // 6.0.0.1 (2014/04/25) These nested if statements could be combined 280 if( !clsFile.getParentFile().exists() && !clsFile.getParentFile().mkdirs() ) { 281 throw new OgRuntimeException( "ディレクトリが作成できませんでした。ファイル=[" + clsFile + "]" ); 282 } 283 284 final StringWriter sw = new StringWriter(); 285 final File[] sourceFiles = { new File( srcFqn ) }; 286 // 5.1.8.0 (2010/07/01) ソースファイルのエンコードは、UTF-8にする。 287 final String[] cpOpts = new String[]{ "-d", classDir, "-classpath", classPath, "-encoding", "UTF-8" }; 288 289 final CompilationTask task = COMPILER.getTask(sw, FILE_MANAGER, null, Arrays 290 .asList(cpOpts), null, FILE_MANAGER 291 .getJavaFileObjects(sourceFiles)); 292 293 boolean isOk = false; 294 // lockしておかないと、java.lang.IllegalStateExceptionが発生することがある 295 synchronized( this ) { 296 isOk = task.call(); 297 } 298 if( !isOk ) { 299 throw new OgRuntimeException( "コンパイルに失敗しました。" + CR + sw.toString() ); 300 } 301 } 302 303 /** 304 * クラスをロードします。 305 * 306 * @og.rev 6.4.3.1 (2016/02/12) Collections.synchronizedMap に置き換え。 307 * 308 * @param clsNm クラス名 309 * 310 * @return ロードしたクラスオブジェクト 311 */ 312 private Class<?> loadClass( final String clsNm ) { 313 314 final String classFqn = classDir + clsNm.replace( ".", File.separator ) + ".class"; 315 final File clsFile = new File( classFqn ); 316 if( !clsFile.exists() ) { 317 throw new OgRuntimeException( "クラスファイルが存在しません。ファイル=[" + classFqn + "]" ); 318 } 319 final long lastModifyTime = clsFile.lastModified(); // 6.0.2.5 (2014/10/31) refactoring 320 321 HybsURLClassLoader loader = null; 322 // 6.4.1.1 (2016/01/16) PMD refactoring. Avoid declaring a variable if it is unreferenced before a possible exit point. 323 final String key = isHotDeploy ? clsNm : CONST_LOADER_KEY; 324 loader = loaderMap.get( key ); 325 if( loader == null || lastModifyTime > loader.getCreationTime() ) { // 6.0.2.5 (2014/10/31) refactoring 326 try { 327 // 6.3.9.1 (2015/11/27) In J2EE, getClassLoader() might not work as expected. Use Thread.currentThread().getContextClassLoader() instead.(PMD) 328 loader = new HybsURLClassLoader( new URL[] { new File( classDir ).toURI().toURL() }, Thread.currentThread().getContextClassLoader() ); 329 } 330 catch( final MalformedURLException ex ) { 331 throw new OgRuntimeException( "クラスロードのURL変換に失敗しました。ファイル=[" + classFqn + "]", ex ); 332 } 333 loaderMap.put( key, loader ); 334 } 335 336 Class<?> cls; 337 try { 338 cls = loader.loadClass( clsNm ); 339 } 340 catch( final ClassNotFoundException ex ) { 341 final String errMsg = "クラスが存在しません。ファイル=[" + classFqn + "]" ; 342 throw new OgRuntimeException( errMsg , ex ); 343 } 344 return cls; 345 } 346 347 /** 348 * このオブジェクトの内部表現を、文字列にして返します。 349 * 350 * @og.rev 6.1.0.0 (2014/12/26) refactoring 351 * 352 * @return オブジェクトの内部表現 353 * @og.rtnNotNull 354 */ 355 @Override 356 public String toString() { 357 return "srcDir=" + srcDir + " , classDir=" + classDir ; 358 } 359 360 /** 361 * URLClassLoaderを拡張し、クラスローダーの生成時間を管理できるようにしています。 362 */ 363 private static final class HybsURLClassLoader { // 6.3.9.1 (2015/11/27) final を追加 364 private final URLClassLoader loader; 365 private final long creationTime; 366 367 /** 368 * URL配列 を引数に取るコンストラクタ 369 * 370 * @param urls URL配列 371 */ 372 HybsURLClassLoader( final URL[] urls ) { 373 this( urls, null ); 374 } 375 376 /** 377 * URL配列と、クラスローダーを引数に取るコンストラクタ 378 * 379 * @param urls URL配列 380 * @param clsLd クラスローダー 381 */ 382 HybsURLClassLoader( final URL[] urls, final ClassLoader clsLd ) { 383 // 6.1.0.0 (2014/12/26) findBugs: Bug type DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED (click for details) 384 // new org.opengion.fukurou.util.HybsLoader$HybsURLClassLoader(URL[], ClassLoader) は、doPrivileged ブロックの中でクラスローダ java.net.URLClassLoader を作成するべきです。 385 loader = AccessController.doPrivileged( 386 new PrivilegedAction<URLClassLoader>() { 387 /** 388 * 特権を有効にして実行する PrivilegedAction<T> の run() メソッドです。 389 * 390 * このメソッドは、特権を有効にしたあとに AccessController.doPrivileged によって呼び出されます。 391 * 392 * @return URLClassLoaderオブジェクト 393 * @og.rtnNotNull 394 */ 395 public URLClassLoader run() { 396 return new URLClassLoader( urls, clsLd ); 397 } 398 } 399 ); 400 creationTime = System.currentTimeMillis(); 401 } 402 403 /** 404 * クラスをロードします。 405 * 406 * @param clsName クラス名の文字列 407 * @return Classオブジェクト 408 */ 409 /* default */ Class<?> loadClass( final String clsName ) throws ClassNotFoundException { 410 return loader.loadClass( clsName ); 411 } 412 413 /** 414 * 作成時間を返します。 415 * 416 * @return 作成時間 417 */ 418 /* default */ long getCreationTime() { 419 return creationTime; 420 } 421 } 422}