    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//                      throw new RuntimeException( "インスタンスの生成に失敗しました" );
    141                        String errMsg = "インスタンスの生成に失敗しました。["  + clsName + "]" ;
    142                        throw new RuntimeException( errMsg , ex );
    143                }
    144                catch( IllegalAccessException ex ) {
    145//                      throw new RuntimeException( "アクセスが拒否されました" );
    146                        String errMsg = "アクセスが拒否されました。["  + clsName + "]" ;
    147                        throw new RuntimeException( errMsg , ex );
    148                }
    149                return obj;
    150        }
    151
    152        /**
    153         * クラス名より完全クラス名を検索します。
    154         *
    155         * @og.rev 5.2.1.0 (2010/10/01) クラスファイルが存在する場合は、エラーにしない。
    156         *
    157         * @param clsNm クラス名
    158         *
    159         * @return 完全クラス名
    160         */
    161        private String getQualifiedName( final String clsNm ) {
    162                String clsName = null;
    163                if( clsNm.indexOf( '.' ) >= 0 ) {
    164                        clsName = clsNm;
    165                }
    166                else {
    167                        synchronized( clsNameMap ) {
    168                                clsName = clsNameMap.get( clsNm );
    169                                if( clsName == null ) {
    170                                        clsName = findFile( "", clsNm );
    171                                }
    172                                if( clsName == null ) {
    173                                        clsName = findFileByCls( "", clsNm );
    174                                }
    175                                clsNameMap.put( clsNm, clsName );
    176                        }
    177                        if( clsName == null ) {
    178                                throw new RuntimeException( "ソースファイルが存在しません。ファイル=[" + clsNm + "]" );
    179                        }
    180                }
    181                return clsName;
    182        }
    183
    184        /**
    185         * クラス名に対応するJavaファイルを再帰的に検索します。
    186         *
    187         * @param path 既定パス
    188         * @param nm クラス名
    189         *
    190         * @return 完全クラス名
    191         */
    192        private String findFile( final String path, final String nm ) {
    193                String tmpSrcPath = srcDir + path;
    194                File[] files = (new File( tmpSrcPath )).listFiles();
    195                if( files != null && files.length > 0 ) {
    196                        for( int i=0; i<files.length; i++ ) {
    197                                if ( files[i].isDirectory() ) {
    198                                        String rtn = findFile( path + files[i].getName() + File.separator, nm );
    199                                        if( rtn != null && rtn.length() > 0 ) {
    200                                                return rtn;
    201                                        }
    202                                }
    203                                else if( ( nm + ".java" ).equals( files[i].getName() ) ) {
    204                                        return path.replace( File.separatorChar, '.' ) + nm;
    205                                }
    206                        }
    207                }
    208                return null;
    209        }
    210
    211        /**
    212         * クラス名に対応するJavaファイルを再帰的に検索します。
    213         *
    214         * @og.rev 5.2.1.0 (2010/10/01) クラスファイルが存在する場合は、エラーにしない。
    215         *
    216         * @param path 既定パス
    217         * @param nm クラス名
    218         *
    219         * @return 完全クラス名
    220         */
    221        private String findFileByCls( final String path, final String nm ) {
    222                String tmpSrcPath = classDir + path;
    223                File[] files = (new File( tmpSrcPath )).listFiles();
    224                if( files != null && files.length > 0 ) {
    225                        for( int i=0; i<files.length; i++ ) {
    226                                if ( files[i].isDirectory() ) {
    227                                        String rtn = findFile( path + files[i].getName() + File.separator, nm );
    228                                        if( rtn != null && rtn.length() > 0 ) {
    229                                                return rtn;
    230                                        }
    231                                }
    232                                else if( ( nm + ".class" ).equals( files[i].getName() ) ) {
    233                                        return path.replace( File.separatorChar, '.' ) + nm;
    234                                }
    235                        }
    236                }
    237                return null;
    238        }
    239
    240        /**
    241         * クラスをコンパイルします。
    242         *
    243         * @og.rev 5.1.8.0 (2010/07/01) ソースファイルのエンコードは、UTF-8にする。
    244         * @og.rev 5.2.1.0 (2010/10/01) クラスファイルが存在する場合は、エラーにしない。
    245         *
    246         * @param clsNm クラス名
    247         */
    248        private void compileClass( final String clsNm ) {
    249                if( COMPILER == null ) {
    250                        throw new RuntimeException( "コンパイラクラスが定義されていません。tools.jarが存在しない可能性があります" );
    251                }
    252
    253                String srcFqn = srcDir + clsNm.replace( ".", File.separator ) + ".java";
    254                File srcFile = new File( srcFqn );
    255                String classFqn =  classDir + clsNm.replace( ".", File.separator ) + ".class";
    256                File clsFile = new File ( classFqn );
    257
    258                // クラスファイルが存在する場合は、エラーにしない。
    259                if( !srcFile.exists() ) {
    260                        if( clsFile.exists() ) {
    261                                return;
    262                        }
    263                        throw new RuntimeException( "ソースファイルが存在しません。ファイル=[" + srcFqn + "]" );
    264                }
    265
    266                if( clsFile.exists() && srcFile.lastModified() <= clsFile.lastModified() ) {
    267                        return;
    268                }
    269
    270                if( !clsFile.getParentFile().exists() ) {
    271                        if( !clsFile.getParentFile().mkdirs() ) {
    272                                throw new RuntimeException( "ディレクトリが作成できませんでした。ファイル=[" + clsFile + "]" );
    273                        }
    274                }
    275
    276                StringWriter sw = new StringWriter();
    277                File[] sourceFiles = { new File( srcFqn ) };
    278                // 5.1.8.0 (2010/07/01) ソースファイルのエンコードは、UTF-8にする。
    279//              String[] cpOpts = new String[]{ "-d", classDir, "-classpath", classPath };
    280                String[] cpOpts = new String[]{ "-d", classDir, "-classpath", classPath, "-encoding", "UTF-8" };
    281
    282                CompilationTask task = COMPILER.getTask(sw, FILE_MANAGER, null, Arrays
    283                                .asList(cpOpts), null, FILE_MANAGER
    284                                .getJavaFileObjects(sourceFiles));
    285
    286                boolean isOk = false;
    287                // lockしておかないと、java.lang.IllegalStateExceptionが発生することがある
    288                synchronized( this ) {
    289                        isOk = task.call();
    290                }
    291                if( !isOk ) {
    292                        throw new RuntimeException( "コンパイルに失敗しました。" + CR + sw.toString() );
    293                }
    294        }
    295
    296        /**
    297         * クラスをロードします。
    298         *
    299         * @param       clsNm クラス名
    300         *
    301         * @return      ロードしたクラスオブジェクト
    302         */
    303        private Class<?> loadClass( final String clsNm ) {
    304                String key = isHotDeploy ? clsNm : CONST_LOADER_KEY;
    305
    306                String classFqn =  classDir + clsNm.replace( ".", File.separator ) + ".class";
    307                File clsFile = new File( classFqn );
    308                if( !clsFile.exists() ) {
    309                        throw new RuntimeException( "クラスファイルが存在しません。ファイル=[" + classFqn + "]" );
    310                }
    311                long lastClassModifyTime = clsFile.lastModified();
    312
    313                HybsURLClassLoader loader = null;
    314                synchronized( loaderMap ) {
    315                        loader = loaderMap.get( key );
    316                        if( loader == null || lastClassModifyTime > loader.getCreationTime() ) {
    317                                try {
    318                                        loader = new HybsURLClassLoader( new URL[] { ( new File( classDir ).toURI().toURL() ) }, this.getClass().getClassLoader() );
    319                                }
    320                                catch( MalformedURLException ex ) {
    321                                        throw new RuntimeException( "クラスロードのURL変換に失敗しました。ファイル=[" + classFqn + "]", ex );
    322                                }
    323                                loaderMap.put( key, loader );
    324                        }
    325                }
    326
    327                Class<?> cls;
    328                try {
    329                        cls = loader.loadClass( clsNm );
    330                }
    331                catch( ClassNotFoundException ex ) {
    332//                      throw new RuntimeException( "クラスが存在しません。ファイル=[" + classFqn + "]" );
    333                        String errMsg = "クラスが存在しません。ファイル=[" + classFqn + "]"  ;
    334                        throw new RuntimeException( errMsg , ex );
    335                }
    336                return cls;
    337        }
    338
    339        /**
    340         * URLClassLoaderを拡張し、クラスローダーの生成時間を管理できるようにしています。
    341         */
    342        private static class HybsURLClassLoader {
    343                final URLClassLoader loader;
    344                final long creationTime;
    345
    346                HybsURLClassLoader( final URL[] urls, final ClassLoader clsLd ) {
    347                        loader = new URLClassLoader( urls, clsLd );
    348                        creationTime = System.currentTimeMillis();
    349                }
    350
    351                HybsURLClassLoader( final URL[] urls ) {
    352                        this( urls, null );
    353                }
    354
    355                Class<?> loadClass( final String clsName ) throws ClassNotFoundException {
    356                        return loader.loadClass( clsName );
    357                }
    358
    359                long getCreationTime() {
    360                        return creationTime;
    361                }
    362        }
    363}