tomcat类加载源码分析

在tomcat里某个应用中,每个应用包含1个类加载器WebappClassLoader,该应用的类都通过该类加载器加载,其loadClass方法如下:

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

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

    if (log.isDebugEnabled())
        log.debug("loadClass(" + name + ", " + resolve + ")");
    Class<?> clazz = null;

    // Log access to stopped classloader
    if (!started) {
        try {
            throw new IllegalStateException();
        } catch (IllegalStateException e) {
            log.info(sm.getString("webappClassLoader.stopped", name), e);
        }
    }

    // (0) Check our previously loaded local class cache
    clazz = findLoadedClass0(name);
    if (clazz != null) {
        if (log.isDebugEnabled())
            log.debug("  Returning class from cache");
        if (resolve)
            resolveClass(clazz);
        return (clazz);
    }

    // (0.1) Check our previously loaded class cache
    clazz = findLoadedClass(name);
    if (clazz != null) {
        if (log.isDebugEnabled())
            log.debug("  Returning class from cache");
        if (resolve)
            resolveClass(clazz);
        return (clazz);
    }

    // (0.2) Try loading the class with the system class loader, to prevent
    //       the webapp from overriding J2SE classes
    try {
        clazz = system.loadClass(name);
        if (clazz != null) {
            if (resolve)
                resolveClass(clazz);
            return (clazz);
        }
    } catch (ClassNotFoundException e) {
        // Ignore
    }

    // (0.5) Permission to access this class when using a SecurityManager
    if (securityManager != null) {
        int i = name.lastIndexOf('.');
        if (i >= 0) {
            try {
                securityManager.checkPackageAccess(name.substring(0,i));
            } catch (SecurityException se) {
                String error = "Security Violation, attempt to use " +
                    "Restricted Class: " + name;
                log.info(error, se);
                throw new ClassNotFoundException(error, se);
            }
        }
    }

    boolean delegateLoad = delegate || filter(name);

    // (1) Delegate to our parent if requested
    if (delegateLoad) {
        if (log.isDebugEnabled())
            log.debug("  Delegating to parent classloader1 " + parent);
        ClassLoader loader = parent;
        if (loader == null)
            loader = system;
        try {
            clazz = Class.forName(name, false, loader);
            if (clazz != null) {
                if (log.isDebugEnabled())
                    log.debug("  Loading class from parent");
                if (resolve)
                    resolveClass(clazz);
                return (clazz);
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }
    }

    // (2) Search local repositories
    if (log.isDebugEnabled())
        log.debug("  Searching local repositories");
    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
    }

    // (3) Delegate to parent unconditionally
    if (!delegateLoad) {
        if (log.isDebugEnabled())
            log.debug("  Delegating to parent classloader at end: " + parent);
        ClassLoader loader = parent;
        if (loader == null)
            loader = system;
        try {
            clazz = Class.forName(name, false, loader);
            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);

}

step1:首先是在该WebappClassLoader缓存的类中检查要加载的类有没有缓存过,会调用findLoadedClass0方法:

protected Class<?> findLoadedClass0(String name) {

    ResourceEntry entry = resourceEntries.get(name);
    if (entry != null) {
        return entry.loadedClass;
    }
    return (null);  // FIXME - findLoadedResource()

}

在WebappClassLoader中,缓存的类保存在resourceEntries这个成员里,它是1个map结构,key为类的名字,value为ResourceEntry:

protected HashMap<String, ResourceEntry> resourceEntries = new HashMap<String, ResourceEntry>();

ResourceEntry的成员如下:

成员 类型 说明
lastModified long 上次更新时间,用于热加载功能
binaryContent byte[] 类的字节数组
loadedClass Class<?> 保存该类的类实例
source URL URL source from where the object was loaded.
codeBase URL URL of the codebase from where the object was loaded.
manifest Manifest Manifest (if the resource was loaded from a JAR).
certificates Certificate[] Certificates (if the resource was loaded from a JAR).

以我部署的web.war项目为例,对于/WEB-INF/classes/下的类,如自定义的com.meli.inc.tomcat.web.HelloController类,其ResourceEntry如下:

HelloController.png

对于/WEB-INF/lib/*.jar下的类,如org.springframework.context.i18n.LocaleContext,其ResourceEntry如下:

LocaleContext.png

其source成员的file和path如下:

file:/Users/cangxing/Documents/Study/tomcat/Tomcat/tomcat-7.0.42-sourcecode/webapps/web/WEB-INF/lib/spring-context-4.0.2.RELEASE.jar!/org/springframework/context/i18n/LocaleContext.class

以上ResourceEntry截图中,是在调用defineClass方法之前各个变量的值,loadedClass为null,实际上,在调用defineClass方法后,会把class字节码文件字节流转换为运行时class类对象,即loadedClass会被赋值为Class<?>类型的该类类对象,然后把ResourceEntry其它成员置为null,因为该类已经被加载到了内存中,即loadedClass,其它成员没有用了,置为null将其gc掉。

step2:然后是检查jvm的类加载器中有没有缓存过该类的加载器,会调用父类ClassLoader的findLoadedClass方法:

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

在ClassLoader类中,findLoadedClass0为本地方法。

step3:然后用系统类加载器加载该类,防止J2SE的基础类被覆盖,会调用系统类加载器的loadClass方法。

step4:然后在本地仓库中寻找该类,会调用WebappClassLoader的findClass方法,如下:

@Override
public Class<?> findClass(String name) throws ClassNotFoundException {

    if (log.isDebugEnabled())
        log.debug("    findClass(" + name + ")");

    // Cannot load anything from local repositories if class loader is stopped
    if (!started) {
        throw new ClassNotFoundException(name);
    }

    // (1) Permission to define this class when using a SecurityManager
    if (securityManager != null) {
        int i = name.lastIndexOf('.');
        if (i >= 0) {
            try {
                if (log.isTraceEnabled())
                    log.trace("      securityManager.checkPackageDefinition");
                securityManager.checkPackageDefinition(name.substring(0,i));
            } catch (Exception se) {
                if (log.isTraceEnabled())
                    log.trace("      -->Exception-->ClassNotFoundException", se);
                throw new ClassNotFoundException(name, se);
            }
        }
    }

    // Ask our superclass to locate this class, if possible
    // (throws ClassNotFoundException if it is not found)
    Class<?> clazz = null;
    try {
        if (log.isTraceEnabled())
            log.trace("      findClassInternal(" + name + ")");
        if (hasExternalRepositories && searchExternalFirst) {
            try {
                clazz = super.findClass(name);
            } catch(ClassNotFoundException cnfe) {
                // Ignore - will search internal repositories next
            } catch(AccessControlException ace) {
                log.warn("WebappClassLoader.findClassInternal(" + name
                        + ") security exception: " + ace.getMessage(), ace);
                throw new ClassNotFoundException(name, ace);
            } catch (RuntimeException e) {
                if (log.isTraceEnabled())
                    log.trace("      -->RuntimeException Rethrown", e);
                throw e;
            }
        }
        if ((clazz == null)) {
            try {
                clazz = findClassInternal(name);
            } catch(ClassNotFoundException cnfe) {
                if (!hasExternalRepositories || searchExternalFirst) {
                    throw cnfe;
                }
            } catch(AccessControlException ace) {
                log.warn("WebappClassLoader.findClassInternal(" + name
                        + ") security exception: " + ace.getMessage(), ace);
                throw new ClassNotFoundException(name, ace);
            } catch (RuntimeException e) {
                if (log.isTraceEnabled())
                    log.trace("      -->RuntimeException Rethrown", e);
                throw e;
            }
        }
        if ((clazz == null) && hasExternalRepositories && !searchExternalFirst) {
            try {
                clazz = super.findClass(name);
            } catch(AccessControlException ace) {
                log.warn("WebappClassLoader.findClassInternal(" + name
                        + ") security exception: " + ace.getMessage(), ace);
                throw new ClassNotFoundException(name, ace);
            } catch (RuntimeException e) {
                if (log.isTraceEnabled())
                    log.trace("      -->RuntimeException Rethrown", e);
                throw e;
            }
        }
        if (clazz == null) {
            if (log.isDebugEnabled())
                log.debug("    --> Returning ClassNotFoundException");
            throw new ClassNotFoundException(name);
        }
    } catch (ClassNotFoundException e) {
        if (log.isTraceEnabled())
            log.trace("    --> Passing on ClassNotFoundException");
        throw e;
    }

    // Return the class we have located
    if (log.isTraceEnabled())
        log.debug("      Returning class " + clazz);

    if (log.isTraceEnabled()) {
        ClassLoader cl;
        if (Globals.IS_SECURITY_ENABLED){
            cl = AccessController.doPrivileged(
                new PrivilegedGetClassLoader(clazz));
        } else {
            cl = clazz.getClassLoader();
        }
        log.debug("      Loaded by " + cl.toString());
    }
    return (clazz);

}

不考虑使用SecurityManager的情况,最终会调用findClassInternal方法,如下:

protected Class<?> findClassInternal(String name)
    throws ClassNotFoundException {

    if (!validate(name))
        throw new ClassNotFoundException(name);

    String tempPath = name.replace('.', '/');
    String classPath = tempPath + ".class";

    ResourceEntry entry = null;

    if (securityManager != null) {
        PrivilegedAction<ResourceEntry> dp =
            new PrivilegedFindResourceByName(name, classPath);
        entry = AccessController.doPrivileged(dp);
    } else {
        entry = findResourceInternal(name, classPath);
    }

    if (entry == null)
        throw new ClassNotFoundException(name);

    Class<?> clazz = entry.loadedClass;
    if (clazz != null)
        return clazz;

    synchronized (this) {
        clazz = entry.loadedClass;
        if (clazz != null)
            return clazz;

        if (entry.binaryContent == null)
            throw new ClassNotFoundException(name);

        // Looking up the package
        String packageName = null;
        int pos = name.lastIndexOf('.');
        if (pos != -1)
            packageName = name.substring(0, pos);

        Package pkg = null;

        if (packageName != null) {
            pkg = getPackage(packageName);
            // Define the package (if null)
            if (pkg == null) {
                try {
                    if (entry.manifest == null) {
                        definePackage(packageName, null, null, null, null,
                                null, null, null);
                    } else {
                        definePackage(packageName, entry.manifest,
                                entry.codeBase);
                    }
                } catch (IllegalArgumentException e) {
                    // Ignore: normal error due to dual definition of package
                }
                pkg = getPackage(packageName);
            }
        }

        if (securityManager != null) {

            // Checking sealing
            if (pkg != null) {
                boolean sealCheck = true;
                if (pkg.isSealed()) {
                    sealCheck = pkg.isSealed(entry.codeBase);
                } else {
                    sealCheck = (entry.manifest == null)
                        || !isPackageSealed(packageName, entry.manifest);
                }
                if (!sealCheck)
                    throw new SecurityException
                        ("Sealing violation loading " + name + " : Package "
                         + packageName + " is sealed.");
            }

        }

        try {
            clazz = defineClass(name, entry.binaryContent, 0,
                    entry.binaryContent.length,
                    new CodeSource(entry.codeBase, entry.certificates));
        } catch (UnsupportedClassVersionError ucve) {
            throw new UnsupportedClassVersionError(
                    ucve.getLocalizedMessage() + " " +
                    sm.getString("webappClassLoader.wrongVersion",
                            name));
        }
        /**
         * 加载完某个类后把该类的二进制字节数组等信息置为null,帮助gc,只保留class
         */
        entry.loadedClass = clazz;
        entry.binaryContent = null;
        entry.source = null;
        entry.codeBase = null;
        entry.manifest = null;
        entry.certificates = null;
    }

    return clazz;

}

这里主要包含2步:
step4.1:entry = findResourceInternal(name, classPath);
这一步在WEB-INF/classes/和WEB-INF/lib/*.jar中根据类的name搜索,找到后封装成ResourceEntry返回。

protected ResourceEntry findResourceInternal(String name, String path) {

    if (!started) {
        log.info(sm.getString("webappClassLoader.stopped", name));
        return null;
    }

    if ((name == null) || (path == null))
        return null;

    ResourceEntry entry = resourceEntries.get(name);
    if (entry != null)
        return entry;

    int contentLength = -1;
    InputStream binaryStream = null;
    boolean isClassResource = path.endsWith(".class");

    int jarFilesLength = jarFiles.length;
    int repositoriesLength = repositories.length;

    int i;

    Resource resource = null;

    boolean fileNeedConvert = false;

    for (i = 0; (entry == null) && (i < repositoriesLength); i++) {
        try {

            String fullPath = repositories[i] + path;

            Object lookupResult = resources.lookup(fullPath);
            if (lookupResult instanceof Resource) {
                resource = (Resource) lookupResult;
            }

            // Note : Not getting an exception here means the resource was
            // found

            ResourceAttributes attributes =
                (ResourceAttributes) resources.getAttributes(fullPath);
            contentLength = (int) attributes.getContentLength();
            String canonicalPath = attributes.getCanonicalPath();
            if (canonicalPath != null) {
                // we create the ResourceEntry based on the information returned
                // by the DirContext rather than just using the path to the
                // repository. This allows to have smart DirContext implementations
                // that "virtualize" the docbase (e.g. Eclipse WTP)
                entry = findResourceInternal(new File(canonicalPath), "");
            } else {
                // probably a resource not in the filesystem (e.g. in a
                // packaged war)
                entry = findResourceInternal(files[i], path);
            }
            entry.lastModified = attributes.getLastModified();

            if (resource != null) {


                try {
                    binaryStream = resource.streamContent();
                } catch (IOException e) {
                    return null;
                }

                if (needConvert) {
                    if (path.endsWith(".properties")) {
                        fileNeedConvert = true;
                    }
                }

                // Register the full path for modification checking
                // Note: Only syncing on a 'constant' object is needed
                synchronized (allPermission) {

                    int j;

                    long[] result2 =
                        new long[lastModifiedDates.length + 1];
                    for (j = 0; j < lastModifiedDates.length; j++) {
                        result2[j] = lastModifiedDates[j];
                    }
                    result2[lastModifiedDates.length] = entry.lastModified;
                    lastModifiedDates = result2;

                    String[] result = new String[paths.length + 1];
                    for (j = 0; j < paths.length; j++) {
                        result[j] = paths[j];
                    }
                    result[paths.length] = fullPath;
                    paths = result;

                }

            }

        } catch (NamingException e) {
            // Ignore
        }
    }

    if ((entry == null) && (notFoundResources.containsKey(name)))
        return null;

    JarEntry jarEntry = null;

    synchronized (jarFiles) {

        try {
            if (!openJARs()) {
                return null;
            }
            for (i = 0; (entry == null) && (i < jarFilesLength); i++) {

                jarEntry = jarFiles[i].getJarEntry(path);

                if (jarEntry != null) {

                    entry = new ResourceEntry();
                    try {
                        entry.codeBase = getURI(jarRealFiles[i]);
                        String jarFakeUrl = entry.codeBase.toString();
                        jarFakeUrl = "jar:" + jarFakeUrl + "!/" + path;
                        entry.source = new URL(jarFakeUrl);
                        entry.lastModified = jarRealFiles[i].lastModified();
                    } catch (MalformedURLException e) {
                        return null;
                    }
                    contentLength = (int) jarEntry.getSize();
                    try {
                        entry.manifest = jarFiles[i].getManifest();
                        binaryStream = jarFiles[i].getInputStream(jarEntry);
                    } catch (IOException e) {
                        return null;
                    }

                    // Extract resources contained in JAR to the workdir
                    if (antiJARLocking && !(path.endsWith(".class"))) {
                        byte[] buf = new byte[1024];
                        File resourceFile = new File
                            (loaderDir, jarEntry.getName());
                        if (!resourceFile.exists()) {
                            Enumeration<JarEntry> entries =
                                jarFiles[i].entries();
                            while (entries.hasMoreElements()) {
                                JarEntry jarEntry2 =  entries.nextElement();
                                if (!(jarEntry2.isDirectory())
                                    && (!jarEntry2.getName().endsWith
                                        (".class"))) {
                                    resourceFile = new File
                                        (loaderDir, jarEntry2.getName());
                                    try {
                                        if (!resourceFile.getCanonicalPath().startsWith(
                                                canonicalLoaderDir)) {
                                            throw new IllegalArgumentException(
                                                    sm.getString("webappClassLoader.illegalJarPath",
                                                jarEntry2.getName()));
                                        }
                                    } catch (IOException ioe) {
                                        throw new IllegalArgumentException(
                                                sm.getString("webappClassLoader.validationErrorJarPath",
                                                        jarEntry2.getName()), ioe);
                                    }
                                    File parentFile = resourceFile.getParentFile();
                                    if (!parentFile.mkdirs() && !parentFile.exists()) {
                                        // Ignore the error (like the IOExceptions below)
                                    }
                                    FileOutputStream os = null;
                                    InputStream is = null;
                                    try {
                                        is = jarFiles[i].getInputStream
                                            (jarEntry2);
                                        os = new FileOutputStream
                                            (resourceFile);
                                        while (true) {
                                            int n = is.read(buf);
                                            if (n <= 0) {
                                                break;
                                            }
                                            os.write(buf, 0, n);
                                        }
                                        resourceFile.setLastModified(
                                                jarEntry2.getTime());
                                    } catch (IOException e) {
                                        // Ignore
                                    } finally {
                                        try {
                                            if (is != null) {
                                                is.close();
                                            }
                                        } catch (IOException e) {
                                            // Ignore
                                        }
                                        try {
                                            if (os != null) {
                                                os.close();
                                            }
                                        } catch (IOException e) {
                                            // Ignore
                                        }
                                    }
                                }
                            }
                        }
                    }

                }

            }

            if (entry == null) {
                synchronized (notFoundResources) {
                    notFoundResources.put(name, name);
                }
                return null;
            }

            /* Only cache the binary content if there is some content
             * available and either:
             * a) It is a class file since the binary content is only cached
             *    until the class has been loaded
             *    or
             * b) The file needs conversion to address encoding issues (see
             *    below)
             *
             * In all other cases do not cache the content to prevent
             * excessive memory usage if large resources are present (see
             * https://issues.apache.org/bugzilla/show_bug.cgi?id=53081).
             */
            if (binaryStream != null &&
                    (isClassResource || fileNeedConvert)) {

                byte[] binaryContent = new byte[contentLength];

                int pos = 0;
                try {

                    while (true) {
                        int n = binaryStream.read(binaryContent, pos,
                                                  binaryContent.length - pos);
                        if (n <= 0)
                            break;
                        pos += n;
                    }
                } catch (IOException e) {
                    log.error(sm.getString("webappClassLoader.readError", name), e);
                    return null;
                }
                if (fileNeedConvert) {
                    // Workaround for certain files on platforms that use
                    // EBCDIC encoding, when they are read through FileInputStream.
                    // See commit message of rev.303915 for details
                    // http://svn.apache.org/viewvc?view=revision&revision=303915
                    String str = new String(binaryContent,0,pos);
                    try {
                        binaryContent = str.getBytes(CHARSET_UTF8);
                    } catch (Exception e) {
                        return null;
                    }
                }
                /**
                 * 类的二进制字节数组
                 */
                entry.binaryContent = binaryContent;

                // The certificates are only available after the JarEntry
                // associated input stream has been fully read
                if (jarEntry != null) {
                    entry.certificates = jarEntry.getCertificates();
                }

            }
        } finally {
            if (binaryStream != null) {
                try {
                    binaryStream.close();
                } catch (IOException e) { /* Ignore */}
            }
        }
    }

    // Add the entry in the local resource repository
    synchronized (resourceEntries) {
        // Ensures that all the threads which may be in a race to load
        // a particular class all end up with the same ResourceEntry
        // instance
        ResourceEntry entry2 = resourceEntries.get(name);
        if (entry2 == null) {
            /**
             * 对类进行缓存,下次用到直接返回
             */
            resourceEntries.put(name, entry);
        } else {
            entry = entry2;
        }
    }

    return entry;

}

step4.2:clazz = defineClass(name, entry.binaryContent, 0,
entry.binaryContent.length,
new CodeSource(entry.codeBase, entry.certificates));
4.1中找到了该类并且拿到了该类的字节数组,step4.2调用ClassLoader的defineClass方法,将该类的字节数组转换为jvm能够识别的运行时class对象。

通过上述源码分析,tomcat类加载的规则如下:
(1)首先检查WebappClassLoader加载的类缓存中有没有加载过该类,如果有则返回,否则进入(2);
(2)从jvm的系统类加载器的类缓存中检查有没有加载过该类,如果有则返回,否则进入(3);
(3)通过系统类加载器加载该类,如果能加载到则返回,负责进入(4);
(4)在/WEB-INF/classes/和/WEB-INF/lib/*.jar中寻找该类,如果寻找到该类,把该类对应的class字节码文件转换为字节流,然后调用ClassLoader的defineClass方法将该类字节码文件的字节流转换为运行期class类对象,完成类的加载,否则抛出ClassNotFoundException。

附:WebappClassLoader类的成员:

成员 类型 说明
resources DirContext 该应用的资源对象
resourceEntries HashMap<String, ResourceEntry> 该应用所有缓存的类及对应的ResourceEntry
notFoundResources LinkedHashMap<String, String> 该应用所有找不到的类缓存,当下次加载某个类时如果在notFoundResources中找不到,直接抛出ClassNotFoundException。
delegate boolean 在从该应用的仓库加载类之前是否首先代理给父加载器加载,默认为false,即优先从本应用的类仓库加载该类
lastJarAccessed long Last time a JAR was accessed.
repositories String[] 该应用的类仓库,一般只有/WEB-INF/classes/
repositoryURLs URL[] 该应用所有类所在目录的URL
files File[] 应用自身的类所在文件目录,不包含依赖的jar,一般为/WEB-INF/classes
jarRealFiles File[] 应用依赖的所有jar包的路径
jarPath String jar文件监控路径,一般为/WEB-INF/lib
jarNames String[] jar文件名列表
lastModifiedDates long[] 这个数组的大小与paths一致,对应paths中每个jar或class的更新时间戳
paths String[] 所有类资源的路径,包含/WEB-INF/lib/下的所有jar和/WEB-INF/classes/下的所有class
loaderDir File Path where resources loaded from JARs will be extracted.
canonicalLoaderDir String Path where resources loaded from JARs will be extracted.
loaderPC HashMap<String, PermissionCollection> The PermissionCollection for each CodeSource for a web application context.
parent ClassLoader 父加载器。
system ClassLoader 系统类加载器,即tomcat的应用类加载器,类加载路径为$CATALINA_HOME/bin/下的类
started boolean 组件是否已经启动。
contextName String Name of associated context used with logging and JMX to associate with the right web application. Particularly useful for the clear references messages. Defaults to unknown but if standard Tomcat components are used it will be updated during initialisation from the resources.

resources成员的变量如下:


resources.png

resourceEntries成员如下,它是1个k->v结构,key是类的相对WEB-INF/classes/或WEB-INF/lib/.jar的相对路径文件名,value是ResourceRntry,这个成员维持着该应用已经加载了的类的缓存,下次加载如果能从该缓存中找到该类则直接用缓存的类,同样,在热部署机制中,当扫描到/WEB-INF/classes/和/WEB-INF/lib/.jar有文件的时间戳更新,了则会停止WebappClassLoader并重新启动WebappClassLoader,停止的过程中会清空resourceEntries。

resourceEntries.png

repositories成员如下,这个数组一般只包含1个成员"/WEB-INF/classes/"。


repositories.png

repositoryURLs成员是该应用的类路径,包括/WEB-INF/classes/和/WEB-INF/lib/下的所有jar包。


repositoryURLs.png

files是应用自身的类路径,如下:


files.png

jarRealFiles是该应用依赖的所有jar包的绝对路径,如下:


jarRealFiles.png

jarPath如下:


jarPath.png

jarNames如下:


jarNames.png

paths如下,包含/WEB-INF/classes/下的类路径和WEB-INF/lib/下的jar路径:


paths.png

loaderDir和canonicalLoaderDir如下:


loaderDir.png
canonicalLoaderDir.png

contextName如下:

contextName.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,951评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,606评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,601评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,478评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,565评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,587评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,590评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,337评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,785评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,096评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,273评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,935评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,578评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,199评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,440评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,163评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,133评论 2 352

推荐阅读更多精彩内容