Tomcat 類加載器

開篇

?這是一篇嘗試講解清楚Tomcat的類加載器的文章,估摸著能講清楚六成左右,待后續再理理思路。

?文末有彩蛋,可以直接翻到文章末尾。


Tomcat 類加載器概覽

Tomcat類加載器

說明:

    1. BootstrapClassLoader : 系統類加載器,加載%JAVA_HOME%/lib目錄下的jar
    1. ExtClassLoader : 擴展類加載器,加載%JAVA_HOME%/ext/lib目錄下的jar
    1. AppClassLoader : 普通類加載器,加載CLASSPATH指定目錄下的jar
    1. commonLoader : Tomcat 通用類加載器, 加載的資源可被 Tomcat 和 所有的 Web 應用程序共同獲取
    1. catalinaLoader : Tomcat 類加載器, 加載的資源只能被 Tomcat 獲取(但 所有 WebappClassLoader 不能獲取到 catalinaLoader 加載的類)
    1. sharedLoader : Tomcat 各個Context的父加載器, 這個類是所有 WebappClassLoader 的父類, sharedLoader 所加載的類將被所有的 WebappClassLoader 共享獲取
    1. 這個版本 (Tomcat 8.x.x) 中, 默認情況下 commonLoader = catalinaLoader = sharedLoader
    1. (PS: 為什么這樣設計, 主要這樣這樣設計 ClassLoader 的層級后, WebAppClassLoader 就能直接訪問 tomcat 的公共資源, 若需要tomcat 有些資源不讓 WebappClassLoader 加載, 則直接在 ${catalina.base}/conf/catalina.properties 中的 server.loader 配置一下 加載路徑就可以了)



Tomcat類關系圖

說明:

    1. Common、Catalina、Shared類加載器繼承自URLCLassLoader。
    1. WebappClassLoader繼承WebappClassLoaderBase繼承URLCLassLoader。
  • 3、JVM自帶的ExtClassLoader也是URLClassLoader的子類。

  • 4、JVM自帶的AppClassLoader也是URLClassLoader的子類。


Tomcat 各類ClassLoader初始化

Common&Catalina&Shared ClassLoader

    1. Bootstrap.init()初始化Tomcat的類加載器。
    1. Bootstrap的initClassLoaders()初始化Common、Catalina、Shared的類加載器。
    1. Common ClassLoader的父加載器是AppClassLoader。
    1. Catalina ClassLoader = Common ClassLoader。
    1. Shared ClassLoader = Common ClassLoader。
public final class Bootstrap {

    public static void main(String args[]) {

        if (daemon == null) {
            // Don't set daemon until init() has completed
            Bootstrap bootstrap = new Bootstrap();
            try {
                // 初始化Bootstrap
                bootstrap.init();
            } catch (Throwable t) {
                handleThrowable(t);
                t.printStackTrace();
                return;
            }
            daemon = bootstrap;
        }
    }


    public void init() throws Exception {
        // 初始化Tomcat的類加載器
        initClassLoaders();
    }


    private void initClassLoaders() {
        try {
            // 創建commonLoader并且未指定父節點,默認為
            commonLoader = createClassLoader("common", null);
            if( commonLoader == null ) {
                // no config file, default to this loader - we might be in a 'single' env.
                commonLoader=this.getClass().getClassLoader();
            }
            // 創建CatalinaLoader并且指定parent為commonLoader
            catalinaLoader = createClassLoader("server", commonLoader);
            // 創建SharedLoader并且指定parent為commonLoader
            sharedLoader = createClassLoader("shared", commonLoader);
        } catch (Throwable t) {
            handleThrowable(t);
            log.error("Class loader creation threw exception", t);
            System.exit(1);
        }
    }
}


ClassLoader創建過程

    1. CatalinaProperties的getProperty方法加載配置conf/Catalina/catalina.properties。
    1. catalina.properties的配置文件內容中:
      common.loader="${catalina.base}/lib","${catalina.base}/lib/.jar","${catalina.home}/lib","${catalina.home}/lib/.jar"。
    1. catalina.properties的配置文件內容中:server.loader=,返回common classLoader。
    1. catalina.properties的配置文件內容中:shared.loader=,返回common classLoader。
public final class Bootstrap {

    private ClassLoader createClassLoader(String name, ClassLoader parent)
        throws Exception {
        
        // common.loader配置jar路徑,負責加載${catalina.base}/lib和{catalina.home}/lib
        // server.loader和shared.loader配置路徑為空,所以返回parent即common classloader
        String value = CatalinaProperties.getProperty(name + ".loader");
        if ((value == null) || (value.equals("")))
            return parent;

        value = replace(value);

        List<Repository> repositories = new ArrayList<>();

        String[] repositoryPaths = getPaths(value);

        for (String repository : repositoryPaths) {

            if (repository.endsWith("*.jar")) {
                repository = repository.substring
                    (0, repository.length() - "*.jar".length());
                repositories.add(
                        new Repository(repository, RepositoryType.GLOB));
            } else if (repository.endsWith(".jar")) {
                repositories.add(
                        new Repository(repository, RepositoryType.JAR));
            } else {
                repositories.add(
                        new Repository(repository, RepositoryType.DIR));
            }
        }

        return ClassLoaderFactory.createClassLoader(repositories, parent);
    }
}


catalina.properties配置解析

    1. 解析tomcat目錄下的conf/Catalina/catalina.properties配置文件。
    1. server.loader和shared.loader的值為空
    1. common.loader="${catalina.base}/lib","${catalina.base}/lib/.jar","${catalina.home}/lib","${catalina.home}/lib/.jar"
public class CatalinaProperties {

    private static Properties properties = null;

    static {
        loadProperties();
    }

    public static String getProperty(String name) {
        return properties.getProperty(name);
    }


    private static void loadProperties() {
        InputStream is = null;
        if (is == null) {
            try {

                // conf/Catalina/catalina.properties
                // common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
                // server.loader=
                // shared.loader=
                File home = new File(Bootstrap.getCatalinaBase());
                File conf = new File(home, "conf");
                File propsFile = new File(conf, "catalina.properties");
                is = new FileInputStream(propsFile);
            } catch (Throwable t) {
            }
        }

        if (is != null) {
            try {
                properties = new Properties();
                properties.load(is);
            } catch (Throwable t) {

            } finally {
                try {
                    is.close();
                } catch (IOException ioe) {
                }
            }
        }

        Enumeration<?> enumeration = properties.propertyNames();
        while (enumeration.hasMoreElements()) {
            String name = (String) enumeration.nextElement();
            String value = properties.getProperty(name);
            if (value != null) {
                System.setProperty(name, value);
            }
        }
    }
}


ClassLoaderFactory的ClassLoader工廠

    1. 加載repositories對應的jar目錄
    1. new URLClassLoader(array)創建沒傳parent的ClassLoader,這種情況下父加載器是AppClassLoader。
    1. new URLClassLoader(array, parent)創建以parent作為父加載器的ClassLoader。
public final class ClassLoaderFactory {

    public static ClassLoader createClassLoader(List<Repository> repositories,
                                                final ClassLoader parent)
        throws Exception {

        Set<URL> set = new LinkedHashSet<>();

        if (repositories != null) {
            for (Repository repository : repositories)  {
                if (repository.getType() == RepositoryType.URL) {
                    URL url = buildClassLoaderUrl(repository.getLocation());
                    set.add(url);
                } else if (repository.getType() == RepositoryType.DIR) {
                    File directory = new File(repository.getLocation());
                    URL url = buildClassLoaderUrl(directory);
                    set.add(url);
                } else if (repository.getType() == RepositoryType.JAR) {
                    File file=new File(repository.getLocation());
                    file = file.getCanonicalFile();
                    URL url = buildClassLoaderUrl(file);
                    set.add(url);
                } else if (repository.getType() == RepositoryType.GLOB) {
                    File directory=new File(repository.getLocation());
                    directory = directory.getCanonicalFile();
                    String filenames[] = directory.list();
                    if (filenames == null) {
                        continue;
                    }
                    for (int j = 0; j < filenames.length; j++) {
                        String filename = filenames[j].toLowerCase(Locale.ENGLISH);
                        File file = new File(directory, filenames[j]);
                        file = file.getCanonicalFile();
                        URL url = buildClassLoaderUrl(file);
                        set.add(url);
                    }
                }
            }
        }

        final URL[] array = set.toArray(new URL[set.size()]);
        return AccessController.doPrivileged(
                new PrivilegedAction<URLClassLoader>() {
                    @Override
                    public URLClassLoader run() {
                        if (parent == null)
                            return new URLClassLoader(array);
                        else
                            return new URLClassLoader(array, parent);
                    }
                });
    }
}


URLClassLoader的實現

URLClassLoader

說明:

    1. URLClassLoder是JVM當中的實現方式。
    1. URLClassLoder繼承SecureClassLoader。
    1. SecureClassLoader繼承ClassLoader。
public class URLClassLoader extends SecureClassLoader implements Closeable {

    public URLClassLoader(URL[] urls) {
        super();
        // this is to make the stack depth consistent with 1.1
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkCreateClassLoader();
        }
        this.acc = AccessController.getContext();
        ucp = new URLClassPath(urls, acc);
    }

    public URLClassLoader(URL[] urls, ClassLoader parent) {
        super(parent);
        // this is to make the stack depth consistent with 1.1
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkCreateClassLoader();
        }
        this.acc = AccessController.getContext();
        ucp = new URLClassPath(urls, acc);
    }
}


public class SecureClassLoader extends ClassLoader {

    protected SecureClassLoader() {
        super();
        // this is to make the stack depth consistent with 1.1
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkCreateClassLoader();
        }
        initialized = true;
    }
}


抽象類ClassLoader

    1. ClassLoader()方法使用getSystemClassLoader作為parent。
    1. getSystemClassLoader()返回sun.misc.Launcher.getLauncher().getClassLoader()。
    1. sun.misc.Launcher.getLauncher().getClassLoader()是。AppClassLoader對象
public abstract class ClassLoader {

    private final ClassLoader parent;
    private static ClassLoader scl;

    protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());
    }

    private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;

        if (ParallelLoaders.isRegistered(this.getClass())) {
            parallelLockMap = new ConcurrentHashMap<>();
            package2certs = new ConcurrentHashMap<>();
            domains =
                Collections.synchronizedSet(new HashSet<ProtectionDomain>());
            assertionLock = new Object();
        } else {
            parallelLockMap = null;
            package2certs = new Hashtable<>();
            domains = new HashSet<>();
            assertionLock = this;
        }
    }


    public static ClassLoader getSystemClassLoader() {
        initSystemClassLoader();
        if (scl == null) {
            return null;
        }
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkClassLoaderPermission(scl, Reflection.getCallerClass());
        }
        return scl;
    }

    private static synchronized void initSystemClassLoader() {
        if (!sclSet) {
            sun.misc.Launcher l = sun.misc.Launcher.getLauncher();

            if (l != null) {
                Throwable oops = null;
                scl = l.getClassLoader();
                try {
                    scl = AccessController.doPrivileged(
                        new SystemClassLoaderAction(scl));
                } catch (PrivilegedActionException pae) {
                }
               
            }
            sclSet = true;
        }
    }
}


Launcher

    1. Launcher.AppClassLoader.getAppClassLoader(var1)返回AppClassLoader對象。
public class Launcher {

    private static Launcher launcher = new Launcher();
    private ClassLoader loader;

    public static Launcher getLauncher() {
        return launcher;
    }

    public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
        }

        try {
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
        }
    }
}


Webapp Classloader

整個Class loader的傳遞過程如下,證明Webapp classLoader的parent節點。
StandardEngine通過xml解析獲得了Catalina的class loader。
StandardHost通過xml解析獲得了StandardEngine的class loader。
StandardContext通過getParentClassLoader獲得了StandardHost的class loader。

設置Bootstrap的common、catalina、shared等Loader

Catalina的parent設置

    1. Bootstrap通過反射調用Catalina的setParentClassLoader設置Catalina的parentClassLoader為sharedLoader。
public final class Bootstrap {
    ClassLoader commonLoader = null;
    ClassLoader catalinaLoader = null;
    ClassLoader sharedLoader = null;

    private void initClassLoaders() {
        try {
            commonLoader = createClassLoader("common", null);
            if( commonLoader == null ) {
                // no config file, default to this loader - we might be in a 'single' env.
                commonLoader=this.getClass().getClassLoader();
            }
            catalinaLoader = createClassLoader("server", commonLoader);
            sharedLoader = createClassLoader("shared", commonLoader);
        } catch (Throwable t) {
            handleThrowable(t);
            log.error("Class loader creation threw exception", t);
            System.exit(1);
        }
    }

    public void init() throws Exception {

        initClassLoaders();

        Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
        Object startupInstance = startupClass.getConstructor().newInstance();

        // Set the shared extensions class loader
        String methodName = "setParentClassLoader";
        Class<?> paramTypes[] = new Class[1];
        paramTypes[0] = Class.forName("java.lang.ClassLoader");
        Object paramValues[] = new Object[1];
        paramValues[0] = sharedLoader;
        Method method =
            startupInstance.getClass().getMethod(methodName, paramTypes);
        method.invoke(startupInstance, paramValues);

        catalinaDaemon = startupInstance;
    }
}


StandardEingine的parent設置

    1. StandardEingine的parentClassLoder設置為Catalina當中的parentClassLoader。
    1. Catalina當中的parentClassLoader是sharedLoader。
    1. StandardEingine通過如下方式注入parentClassLoader: digester.addRule("Server/Service/Engine,
      new SetParentClassLoaderRule(parentClassLoader));
public class Catalina {

    protected ClassLoader parentClassLoader =
        Catalina.class.getClassLoader();

    public void setParentClassLoader(ClassLoader parentClassLoader) {
        this.parentClassLoader = parentClassLoader;
    }

    public ClassLoader getParentClassLoader() {
        if (parentClassLoader != null) {
            return (parentClassLoader);
        }
        return ClassLoader.getSystemClassLoader();
    }

    // Engine的解析規則
    protected Digester createStartDigester() {
        digester.addRule("Server/Service/Engine",
                         new SetParentClassLoaderRule(parentClassLoader));
}


StandardHost的parent設置

    1. StandardHost通過StandardEngine的getParentClassLoader獲取ClassLoader。
    1. StandardHost的setParentClassLoader注入StandardEngine的parentClassLoader。
    1. StandardHost的parentClassLoader為sharedLoader。
public class HostRuleSet extends RuleSetBase {
    public void addRuleInstances(Digester digester) {

        digester.addObjectCreate(prefix + "Host",
                                 "org.apache.catalina.core.StandardHost",
                                 "className");
        digester.addSetProperties(prefix + "Host");
        // Host的XML的解析規則
        digester.addRule(prefix + "Host",
                         new CopyParentClassLoaderRule());
    }
}


public class CopyParentClassLoaderRule extends Rule {

    public CopyParentClassLoaderRule() {
    }

    @Override
    public void begin(String namespace, String name, Attributes attributes)
        throws Exception {

        if (digester.getLogger().isDebugEnabled())
            digester.getLogger().debug("Copying parent class loader");
        Container child = (Container) digester.peek(0);
        Object parent = digester.peek(1);
        Method method =
            parent.getClass().getMethod("getParentClassLoader", new Class[0]);
        ClassLoader classLoader =
            (ClassLoader) method.invoke(parent, new Object[0]);
        child.setParentClassLoader(classLoader);

    }
}


StandardContext的parent設置

    1. StandardContext通過parent.getParentClassLoader()返回parentClassLoader。
    1. StandardContext的parent是StandardHost對象。
    1. StandardHost.getParentClassLoader()返回StandardHost的sharedLoader。
public class StandardContext extends ContainerBase
        implements Context, NotificationEmitter {

    public ClassLoader getParentClassLoader() {
        if (parentClassLoader != null)
            return (parentClassLoader);
        if (getPrivileged()) {
            return this.getClass().getClassLoader();
        } else if (parent != null) {
            // parent是StandardHost
            return (parent.getParentClassLoader());
        }
        return (ClassLoader.getSystemClassLoader());
    }
}


類加載過程雙親委派

WebappClassLoader

    1. WebappClassLoader繼承自WebappClassLoaderBase。
    1. 核心實現在于WebappClassLoaderBase類。
public class WebappClassLoader extends WebappClassLoaderBase {

    public WebappClassLoader() {
        super();
    }


    public WebappClassLoader(ClassLoader parent) {
        super(parent);
    }

   @Override
    public WebappClassLoader copyWithoutTransformers() {

        WebappClassLoader result = new WebappClassLoader(getParent());

        super.copyStateWithoutTransformers(result);

        try {
            result.start();
        } catch (LifecycleException e) {
            throw new IllegalStateException(e);
        }

        return result;
    }

    @Override
    protected Object getClassLoadingLock(String className) {
        return this;
    }
}


WebappClassLoaderBase

    1. 調用 findLocaledClass0 從 resourceEntries 中判斷 class 是否已經加載 OK。
    1. 調用 findLoadedClass(內部調用一個 native 方法) 直接查看對應的 WebappClassLoader 是否已經加載過。
    1. 調用 binaryNameToPath 判斷是否 當前 class 是屬于 J2SE 范圍中的, 若是的則直接通過 ExtClassLoader, BootstrapClassLoader 進行加載 (這里是雙親委派)。
    1. 在設置 JVM 權限校驗的情況下, 調用 securityManager 來進行權限的校驗(當前類是否有權限加載這個類, 默認的權限配置文件是 ${catalina.base}/conf/catalina.policy)。
    1. 判斷是否設置了雙親委派機制 或 當前 WebappClassLoader 是否能加載這個 class (通過 filter(name) 來決定), 將最終的值賦值給 delegateLoad。
    1. 根據上一步中的 delegateLoad 來決定是否用 WebappClassloader.parent(也就是 sharedClassLoader) 來進行加載, 若加載成功, 則直接返回。
    1. 上一步若未加載成功, 則調用 WebappClassloader.findClass(name) 來進行加載。
    1. 若上一還是沒有加載成功, 則通過 parent 調用 Class.forName 來進行加載。
    1. 若還沒加載成功的話, 那就直接拋異常。
加載過程
public abstract class WebappClassLoaderBase extends URLClassLoader
        implements Lifecycle, InstrumentableClassLoader, WebappProperties, PermissionCheck {

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return (loadClass(name, false));
    }

    public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

        synchronized (getClassLoadingLock(name)) {

            Class<?> clazz = null;

            // Log access to stopped class loader
            checkStateForClassLoading(name);

            //首先調用findLoaderClass0() 方法檢查WebappClassLoader中是否加載過此類
            // WebappClassLoader 加載過的類都存放在 resourceEntries 緩存中。
            clazz = findLoadedClass0(name);
            if (clazz != null) {
                if (log.isDebugEnabled())
                    log.debug("  Returning class from cache");
                if (resolve)
                    resolveClass(clazz);
                return (clazz);
            }

            // 調用 findLoadedClass(內部調用一個 native 方法) 
            // 直接查看對應的 WebappClassLoader 是否已經加載過
            clazz = findLoadedClass(name);
            if (clazz != null) {
                if (log.isDebugEnabled())
                    log.debug("  Returning class from cache");
                if (resolve)
                    resolveClass(clazz);
                return (clazz);
            }

            // 調用 binaryNameToPath 判斷是否 當前 class 是屬于 J2SE 范圍中的, 
            // 若是的則直接通過 ExtClassLoader, BootstrapClassLoader 進行加載     
            // (這里是雙親委派)

            String resourceName = binaryNameToPath(name, false);

            ClassLoader javaseLoader = getJavaseClassLoader();
            boolean tryLoadingFromJavaseLoader;
            try {
                tryLoadingFromJavaseLoader = (javaseLoader.getResource(resourceName) != null);
            } catch (Throwable t) {
                tryLoadingFromJavaseLoader = true;
            }

            if (tryLoadingFromJavaseLoader) {
                try {
                    clazz = javaseLoader.loadClass(name);
                    if (clazz != null) {
                        if (resolve)
                            resolveClass(clazz);
                        return (clazz);
                    }
                } catch (ClassNotFoundException e) {
                    // Ignore
                }
            }

            // 判斷是否需要委托給父類加載器進行加載,
            // delegate屬性默認為false,那么delegatedLoad的值就取決于filter的返回值了
            // filter中是優先加載tomcat的lib下的class文件
            // filter方法中根據包名來判斷是否需要進行委托加載,
            // 默認情況下會返回false.因此delegatedLoad為false
            boolean delegateLoad = delegate || filter(name, true);

            // 因為delegatedLoad為false,那么此時不會委托父加載器去加載,
            // 這里其實是沒有遵循parent-first的加載機制。
            if (delegateLoad) {
                try {
                    clazz = Class.forName(name, false, parent);
                    if (clazz != null) {
                        if (log.isDebugEnabled())
                            log.debug("  Loading class from parent");
                        if (resolve)
                            resolveClass(clazz);
                        return (clazz);
                    }
                } catch (ClassNotFoundException e) {
                    // Ignore
                }
            }

            // 調用findClass方法在webapp級別進行加載
            try {
                clazz = findClass(name);
                if (clazz != null) {
                    if (log.isDebugEnabled())
                        log.debug("  Loading class from local repository");
                    if (resolve)
                        resolveClass(clazz);
                    return (clazz);
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }

            // 如果還是沒有加載到類,并且不采用委托機制的話,則通過父類加載器去加載
            if (!delegateLoad) {
                try {
                    clazz = Class.forName(name, false, parent);
                    if (clazz != null) {
                        if (log.isDebugEnabled())
                            log.debug("  Loading class from parent");
                        if (resolve)
                            resolveClass(clazz);
                        return (clazz);
                    }
                } catch (ClassNotFoundException e) {
                    // Ignore
                }
            }
        }

        throw new ClassNotFoundException(name);
    }


    protected Class<?> findLoadedClass0(String name) {

        String path = binaryNameToPath(name, true);

        ResourceEntry entry = resourceEntries.get(path);
        if (entry != null) {
            return entry.loadedClass;
        }
        return null;
    }


    protected final Class<?> findLoadedClass(String name) {
        if (!checkName(name))
            return null;
        return findLoadedClass0(name);
    }

    private native final Class<?> findLoadedClass0(String name);

}


參考文章

違反ClassLoader雙親委派機制三部曲第二部——Tomcat類加載機制
Tomcat 源碼分析 WebappClassLoader 分析 (基于8.0.5)


招聘信息

【招賢納士】

歡迎熱愛技術、熱愛生活的你和我成為同事,和貝貝共同成長。

貝貝集團誠招算法、大數據、BI、Java、PHP、android、iOS、測試、運維、DBA等人才,有意可投遞zhi.wang@beibei.com

貝貝集團創建于2011年,旗下擁有貝貝網、貝店、貝貸等平臺,致力于成為全球領先的家庭消費平臺。

貝貝創始團隊來自阿里巴巴,先后獲得IDG資本、高榕資本、今日資本、新天域資本、北極光等數億美金的風險投資。

公司地址:杭州市江干區普盛巷9號東谷創業園(上下班有多趟班車)

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,619評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,155評論 3 425
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,635評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,539評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,255評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,646評論 1 326
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,655評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,838評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,399評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,146評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,338評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,893評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,565評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,983評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,257評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,059評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,296評論 2 376

推薦閱讀更多精彩內容