    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}