文档章节

Tomcat7源码分析(二)类加载体系

 宸明
发布于 2018/03/13 11:19
字数 3196
阅读 128
收藏 0

一、总体分析

    主流的Java Web服务器,如Tomcat、Jetty、WebLogic、WebSphere等都实现了自己定义的类加载器(一般都不止一个)。因为一个功能健全的Web服务器,需要解决如下的几个问题:

  1. 部署在同一个服务器上的两个Web应用程序使用的Java 类库可以实现相互隔离,这是最基本的要求.两个不同应用程序可能会依赖同一个第三方类库的不同版本的,不能要求一个类库在一个服务器中只有一份,服务器应当保证两个应用程序的类库可以互相独立使用
  2. 部署在同一个服务器上的两个Web应用程序所使用的Java类库可以互相共享,这个需求也很常见,如果Java类库不能共享使用,虚拟机的方法区很容易出现过度膨胀的风险
  3. 服务器需要尽可能保证自身安全不受部署的Web应用程序影响.目前有许多主流的Java Web服务器都使用Java语言开发,因此服务器本身也有类库依赖的问题,一般来说,基于安全的考虑,服务器所使用的类库应该与应用程序使用的类库互相独立
  4. 支持JSP的服务器,大部分都需要支持HotSwap功能(热交换功能)

    本文基于Tomcat7.0.69的Java源码,对其类加载体系进行分析。

    由于上述的种种问题,在部署Web应用的时候如果只使用一个单独的ClassPath是无法满足需求的,所以各种Web服务器都不约而同的提供了多个ClassPath路径供用户存在第三方类库,这些路径一般都以lib,classes命名,被放置到不同路径的类库,具备不同的访问范围和服务对象.tomcat服务器划分用户类库结构和类加载描述如下,然后用一张图片来展示Tomcat的类加载体系:各个类加载器之间不是继承关系,而是一种委派关系。

这里结合之前对双亲委派模式的类加载过程的描述,对上图所示类加载体系进行介绍:
ClassLoader:Java提供的类加载器抽象类,用户自定义的类加载器需要继承实现
commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;

WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见;

通过下面类关系图以及逻辑关系图,同时对比上文内容梳理这些类加载器之间的关系。

1、类关系图

classloader-3

从图中看到了Common,Catalina,Shared类加载器是URLClassLoader类的一个实例,只是它们的类加载路径不一样,在tomcat/conf/catalina.properties配置文件中配置(common.loader,server.loader,shared.loader).WebAppClassLoader继承自WebAppClassLoaderBase,基本所有逻辑都在WebAppClassLoaderBase为中实现了,可以看出tomcat的所有类加载器都是以URLClassLoader为基础进行扩展。

2、逻辑关系图

classloader-4

上面说到Common,Catalina,Shared类加载器是URLClassLoader类的一个实例,在默认的配置中,它们其实都是同一个对象,即commonLoader,结合初始化时的代码(只保留关键代码):

 private void initClassLoaders() {
        commonLoader = createClassLoader("common", null);  // commonLoader的加载路径为common.loader
        if( commonLoader == null ) {
            commonLoader=this.getClass().getClassLoader();
        }
        catalinaLoader = createClassLoader("server", commonLoader); // 加载路径为server.loader,默认为空,父类加载器为commonLoader
        sharedLoader = createClassLoader("shared", commonLoader); // 加载路径为shared.loader,默认为空,父类加载器为commonLoader
    }
 private ClassLoader createClassLoader(String name, ClassLoader parent) throws Exception {
        String value = CatalinaProperties.getProperty(name + ".loader");
        if ((value == null) || (value.equals("")))
            return parent;      // catalinaLoader与sharedLoader的加载路径均为空,所以直接返回commonLoader对象,默认3者为同一个对象
    }

 

    在上面的代码初始化时很明确是指出了,catalina与shared类加载器的父类加载器为common类加载器,而初始化commonClassLoader时父类加载器设置为null,最终会调到createClassLoader静态方法:

 public static ClassLoader createClassLoader(List<Repository> repositories,
                                                final ClassLoader parent)
        throws Exception {
        .....
        return AccessController.doPrivileged(
                new PrivilegedAction<URLClassLoader>() {
                    @Override
                    public URLClassLoader run() {
                        if (parent == null)
                            return new URLClassLoader(array);  //该构造方法默认获取系统类加载器为父类加载器,即AppClassLoader
                        else
                            return new URLClassLoader(array, parent);
                    }
                });

    }

    在createClassLoader中指定参数parent==null时,最终会以系统类加载器(AppClassLoader)作为父类加载器,这解释了为什么commonClassLoader的父类加载器是AppClassLoader.

一个web应用对应着一个StandardContext实例,每个web应用都拥有独立web应用类加载器(WebClassLoader),这个类加载器在StandardContext.startInternal()中被构造了出来:

 if (getLoader() == null) {
            WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
            webappLoader.setDelegate(getDelegate());
            setLoader(webappLoader);
        }

    这里getParentClassLoader()会获取父容器StandarHost.parentClassLoader对象属性,而这个对象属性是在Catalina$SetParentClassLoaderRule.begin()初始化,初始化的值其实就是Catalina.parentClassLoader对象属性,再来跟踪一下Catalina.parentClassLoader,在Bootstrap.init()时通过反射调用了Catalina.setParentClassLoader(),将Bootstrap.sharedLoader属性设置为Catalina.parentClassLoader,所以WebClassLoader的父类加载器是Shared ClassLoader.

3、类加载逻辑

    Tomcat的类加载机制是违反了双亲委托原则的,对于一些未加载的非基础类(Object,String等),各个web应用自己的类加载器(WebAppClassLoader)会优先加载,加载不到时再交给commonClassLoader走双亲委托。具体的加载逻辑位于WebAppClassLoaderBase.loadClass()方法中,代码篇幅长,这里以文字描述加载一个类过程:

  1. 先在本地缓存中查找是否已经加载过该类(对于一些已经加载了的类,会被缓存在resourceEntries这个数据结构中),如果已经加载即返回,否则 继续下一步。
  2. 让系统类加载器(AppClassLoader)尝试加载该类,主要是为了防止一些基础类会被web中的类覆盖,如果加载到即返回,返回继续。
  3. 前两步均没加载到目标类,那么web应用的类加载器将自行加载,如果加载到则返回,否则继续下一步。
  4. 最后还是加载不到的话,则委托父类加载器(Common ClassLoader)去加载。

第3第4两个步骤的顺序已经违反了双亲委托机制,除了tomcat之外,JDBC,JNDI,Thread.currentThread().setContextClassLoader();等很多地方都一样是违反了双亲委托。

 

二、源码分析

commonLoader、catalinaLoader和sharedLoader在Tomcat容器初始化的一开始,即调用Bootstrap的init方法时创建。catalinaLoader会被设置为Tomcat主线程的线程上下文类加载器,并且使用catalinaLoader加载Tomcat容器自身容器下的class。Bootstrap的init方法的部分代码见代码清单1。

代码清单1 Bootstrap的init方法的部分实现

 

[java] view plain copy

  1. /** 
  2.  * Initialize daemon. 
  3.  */  
  4. public void init()  
  5.     throws Exception  
  6. {  
  7.   
  8.     // Set Catalina path  
  9.     setCatalinaHome();  
  10.     setCatalinaBase();  
  11.   
  12.     initClassLoaders();  
  13.   
  14.     Thread.currentThread().setContextClassLoader(catalinaLoader);  
  15.   
  16.     SecurityClassLoad.securityClassLoad(catalinaLoader);  
  17.     // 省略后边的代码  

 

 代码清单1中,我们首先关注initClassLoaders方法的实现,见代码清单2.initClassLoaders方法用来初始化commonLoader、catalinaLoader、sharedLoader。

代码清单2 initClassLoaders方法的实现

 

[java] view plain copy

  1. private void initClassLoaders() {  
  2.     try {  
  3.         commonLoader = createClassLoader("common", null);  
  4.         if( commonLoader == null ) {  
  5.             // no config file, default to this loader - we might be in a 'single' env.  
  6.             commonLoader=this.getClass().getClassLoader();  
  7.         }  
  8.         catalinaLoader = createClassLoader("server", commonLoader);  
  9.         sharedLoader = createClassLoader("shared", commonLoader);  
  10.     } catch (Throwable t) {  
  11.         log.error("Class loader creation threw exception", t);  
  12.         System.exit(1);  
  13.     }  
  14. }  

 

 从代码清单2中看到创建类加载器是通过调用createClassLoader方法实现的,createClassLoader的实现见代码清单3.

代码清单3 createClassLoader方法的实现

 

[java] view plain copy

  1. private ClassLoader createClassLoader(String name, ClassLoader parent)  
  2.     throws Exception {  
  3.   
  4.     String value = CatalinaProperties.getProperty(name + ".loader");  
  5.     if ((value == null) || (value.equals("")))  
  6.         return parent;  
  7.   
  8.     ArrayList<String> repositoryLocations = new ArrayList<String>();  
  9.     ArrayList<Integer> repositoryTypes = new ArrayList<Integer>();  
  10.     int i;  
  11.   
  12.     StringTokenizer tokenizer = new StringTokenizer(value, ",");  
  13.     while (tokenizer.hasMoreElements()) {  
  14.         String repository = tokenizer.nextToken();  
  15.   
  16.         // Local repository  
  17.         boolean replace = false;  
  18.         String before = repository;  
  19.         while ((i=repository.indexOf(CATALINA_HOME_TOKEN))>=0) {  
  20.             replace=true;  
  21.             if (i>0) {  
  22.             repository = repository.substring(0,i) + getCatalinaHome()   
  23.                 + repository.substring(i+CATALINA_HOME_TOKEN.length());  
  24.             } else {  
  25.                 repository = getCatalinaHome()   
  26.                     + repository.substring(CATALINA_HOME_TOKEN.length());  
  27.             }  
  28.         }  
  29.         while ((i=repository.indexOf(CATALINA_BASE_TOKEN))>=0) {  
  30.             replace=true;  
  31.             if (i>0) {  
  32.             repository = repository.substring(0,i) + getCatalinaBase()   
  33.                 + repository.substring(i+CATALINA_BASE_TOKEN.length());  
  34.             } else {  
  35.                 repository = getCatalinaBase()   
  36.                     + repository.substring(CATALINA_BASE_TOKEN.length());  
  37.             }  
  38.         }  
  39.         if (replace && log.isDebugEnabled())  
  40.             log.debug("Expanded " + before + " to " + repository);  
  41.   
  42.         // Check for a JAR URL repository  
  43.         try {  
  44.             new URL(repository);  
  45.             repositoryLocations.add(repository);  
  46.             repositoryTypes.add(ClassLoaderFactory.IS_URL);  
  47.             continue;  
  48.         } catch (MalformedURLException e) {  
  49.             // Ignore  
  50.         }  
  51.   
  52.         if (repository.endsWith("*.jar")) {  
  53.             repository = repository.substring  
  54.                 (0, repository.length() - "*.jar".length());  
  55.             repositoryLocations.add(repository);  
  56.             repositoryTypes.add(ClassLoaderFactory.IS_GLOB);  
  57.         } else if (repository.endsWith(".jar")) {  
  58.             repositoryLocations.add(repository);  
  59.             repositoryTypes.add(ClassLoaderFactory.IS_JAR);  
  60.         } else {  
  61.             repositoryLocations.add(repository);  
  62.             repositoryTypes.add(ClassLoaderFactory.IS_DIR);  
  63.         }  
  64.     }  
  65.   
  66.     String[] locations = repositoryLocations.toArray(new String[0]);  
  67.     Integer[] types = repositoryTypes.toArray(new Integer[0]);  
  68.   
  69.     ClassLoader classLoader = ClassLoaderFactory.createClassLoader  
  70.         (locations, types, parent);  
  71.   
  72.     // Retrieving MBean server  
  73.     MBeanServer mBeanServer = null;  
  74.     if (MBeanServerFactory.findMBeanServer(null).size() > 0) {  
  75.         mBeanServer = MBeanServerFactory.findMBeanServer(null).get(0);  
  76.     } else {  
  77.         mBeanServer = ManagementFactory.getPlatformMBeanServer();  
  78.     }  
  79.   
  80.     // Register the server classloader  
  81.     ObjectName objectName =  
  82.         new ObjectName("Catalina:type=ServerClassLoader,name=" + name);  
  83.     mBeanServer.registerMBean(classLoader, objectName);  
  84.   
  85.     return classLoader;  
  86.   
  87. }  

createClassLoader方法的执行步骤如下:

 

  1. 获取各个类加载器相应的资源配置文件(分别为common.loader、server.loader、shared.loader),从中获取类资源路径的配置信息;
  2. 解析类资源路径下的各个资源位置和类型,也包括对jar资源的检查;
  3. 调用ClassLoaderFactory.createClassLoader(locations, types, parent)方法创建ClassLoader;
  4. 将ClassLoader注册到JMX服务中,有个JMX的内容可以参照《Tomcat7.0源码分析——生命周期管理 》一文中的相关介绍。

 

我们回头看看代码清单1中的SecurityClassLoad.securityClassLoad(catalinaLoader)的实现,见代码清单4.这说明加载Tomcat容器本身的类资源的确是使用catalinaLoader来完成的。

代码清单4 securityClassLoad的实现

 

[java] view plain copy

  1. public static void securityClassLoad(ClassLoader loader)  
  2.     throws Exception {  
  3.   
  4.     if( System.getSecurityManager() == null ){  
  5.         return;  
  6.     }  
  7.       
  8.     loadCorePackage(loader);  
  9.     loadLoaderPackage(loader);  
  10.     loadSessionPackage(loader);  
  11.     loadUtilPackage(loader);  
  12.     loadJavaxPackage(loader);  
  13.     loadCoyotePackage(loader);          
  14.     loadTomcatPackage(loader);  
  15. }  

 

securityClassLoad方法主要加载Tomcat容器所需的class,包括:

  • Tomcat核心class,即org.apache.catalina.core路径下的class;
  • org.apache.catalina.loader.WebappClassLoader$PrivilegedFindResourceByName;
  • Tomcat有关session的class,即org.apache.catalina.session路径下的class;
  • Tomcat工具类的class,即org.apache.catalina.util路径下的class;
  • javax.servlet.http.Cookie;
  • Tomcat处理请求的class,即org.apache.catalina.connector路径下的class;
  • Tomcat其它工具类的class,也是org.apache.catalina.util路径下的class;

我们以加载Tomcat核心class的loadCorePackage方法为例,其实现见代码清单5所示。

代码清单5 loadCorePackage的实现

 

[java] view plain copy

  1. private final static void loadCorePackage(ClassLoader loader)  
  2.     throws Exception {  
  3.     String basePackage = "org.apache.catalina.";  
  4.     loader.loadClass  
  5.         (basePackage +  
  6.          "core.ApplicationContextFacade$1");  
  7.     loader.loadClass  
  8.         (basePackage +  
  9.          "core.ApplicationDispatcher$PrivilegedForward");  
  10.     loader.loadClass  
  11.         (basePackage +  
  12.          "core.ApplicationDispatcher$PrivilegedInclude");  
  13.     loader.loadClass  
  14.         (basePackage +  
  15.         "core.AsyncContextImpl");  
  16.     loader.loadClass  
  17.         (basePackage +  
  18.         "core.AsyncContextImpl$AsyncState");  
  19.     loader.loadClass  
  20.         (basePackage +  
  21.         "core.AsyncContextImpl$DebugException");  
  22.     loader.loadClass  
  23.         (basePackage +  
  24.         "core.AsyncContextImpl$1");  
  25.     loader.loadClass  
  26.         (basePackage +  
  27.         "core.AsyncContextImpl$2");  
  28.     loader.loadClass  
  29.         (basePackage +  
  30.         "core.AsyncListenerWrapper");  
  31.     loader.loadClass  
  32.         (basePackage +  
  33.          "core.ContainerBase$PrivilegedAddChild");  
  34.     loader.loadClass  
  35.         (basePackage +  
  36.          "core.DefaultInstanceManager$1");  
  37.     loader.loadClass  
  38.         (basePackage +  
  39.          "core.DefaultInstanceManager$2");  
  40.     loader.loadClass  
  41.         (basePackage +  
  42.          "core.DefaultInstanceManager$3");  
  43.     loader.loadClass  
  44.         (basePackage +  
  45.          "core.DefaultInstanceManager$4");  
  46.     loader.loadClass  
  47.         (basePackage +  
  48.          "core.DefaultInstanceManager$5");  
  49.     loader.loadClass  
  50.         (basePackage +  
  51.          "core.ApplicationHttpRequest$AttributeNamesEnumerator");  
  52. }  

 

 至此,有关commonLoader、catalinaLoader和sharedLoader三个类加载器的初始化以及使用catalinaLoader加载Tomcat容器自身类资源的内容已经介绍完了,但是我们还没有看到WebappClassLoader。启动StandardContext的时候会创建WebappLoader,根据《Tomcat7.0源码分析——生命周期管理 》一文的内容,我们知道启动StandardContext时会最终调用其startInternal方法,其实现见代码清单6.

代码清单6 StandardContext的startInternal方法

 

[java] view plain copy

  1. /** 
  2.  * Start this component and implement the requirements 
  3.  * of {@link LifecycleBase#startInternal()}. 
  4.  * 
  5.  * @exception LifecycleException if this component detects a fatal error 
  6.  *  that prevents this component from being used 
  7.  */  
  8. @Override  
  9. protected synchronized void startInternal() throws LifecycleException {  
  10.   
  11.     // 省略前边的代码   
  12.   
  13.     if (getLoader() == null) {  
  14.         WebappLoader webappLoader = new WebappLoader(getParentClassLoader());  
  15.         webappLoader.setDelegate(getDelegate());  
  16.         setLoader(webappLoader);  
  17.     }  
  18.    // 省略中间的代码   
  19.    // Start our subordinate components, if any  
  20.    if ((loader != null) && (loader instanceof Lifecycle))  
  21.         ((Lifecycle) loader).start();   
  22.    // 省略后边的代码   
  23. }  

 

 从代码清单6看到首先创建WebappLoader实例,然后调用WebappLoader的start方法,start又调用了startInternal方法,WebappLoader的startInternal的实现见代码清单7.

代码清单7 WebappLoader的startInternal实现

 

[java] view plain copy

  1. /** 
  2.  * Start associated {@link ClassLoader} and implement the requirements 
  3.  * of {@link LifecycleBase#startInternal()}. 
  4.  * 
  5.  * @exception LifecycleException if this component detects a fatal error 
  6.  *  that prevents this component from being used 
  7.  */  
  8. @Override  
  9. protected void startInternal() throws LifecycleException {  
  10.       
  11.     // Register a stream handler factory for the JNDI protocol  
  12.     URLStreamHandlerFactory streamHandlerFactory =  
  13.         new DirContextURLStreamHandlerFactory();  
  14.     if (first) {  
  15.         first = false;  
  16.         try {  
  17.             URL.setURLStreamHandlerFactory(streamHandlerFactory);  
  18.         } catch (Exception e) {  
  19.             // Log and continue anyway, this is not critical  
  20.             log.error("Error registering jndi stream handler", e);  
  21.         } catch (Throwable t) {  
  22.             // This is likely a dual registration  
  23.             log.info("Dual registration of jndi stream handler: "   
  24.                      + t.getMessage());  
  25.         }  
  26.     }  
  27.   
  28.     // Construct a class loader based on our current repositories list  
  29.     try {  
  30.   
  31.         classLoader = createClassLoader();  
  32.         classLoader.setResources(container.getResources());  
  33.         classLoader.setDelegate(this.delegate);  
  34.         classLoader.setSearchExternalFirst(searchExternalFirst);  
  35.         if (container instanceof StandardContext) {  
  36.             classLoader.setAntiJARLocking(  
  37.                     ((StandardContext) container).getAntiJARLocking());  
  38.             classLoader.setClearReferencesStatic(  
  39.                     ((StandardContext) container).getClearReferencesStatic());  
  40.             classLoader.setClearReferencesStopThreads(  
  41.                     ((StandardContext) container).getClearReferencesStopThreads());  
  42.             classLoader.setClearReferencesStopTimerThreads(  
  43.                     ((StandardContext) container).getClearReferencesStopTimerThreads());  
  44.             classLoader.setClearReferencesThreadLocals(  
  45.                     ((StandardContext) container).getClearReferencesThreadLocals());  
  46.         }  
  47.   
  48.         for (int i = 0; i < repositories.length; i++) {  
  49.             classLoader.addRepository(repositories[i]);  
  50.         }  

 

 我们看到代码清单7中通过调用createClassLoader来创建类加载器,并且设置其资源路径为当前Webapp下某个context的类资源。最后我们看看createClassLoader的实现,见代码清单8.

代码清单8 createClassLoader的实现

 

[java] view plain copy

  1. /** 
  2.  * Create associated classLoader. 
  3.  */  
  4. private WebappClassLoader createClassLoader()  
  5.     throws Exception {  
  6.   
  7.     //loaderClass即字符串org.apache.catalina.loader.WebappClassLoader  
  8.     Class<?> clazz = Class.forName(loaderClass);  
  9.     WebappClassLoader classLoader = null;  
  10.   
  11.     if (parentClassLoader == null) {  
  12.         parentClassLoader = container.getParentClassLoader();  
  13.     }  
  14.     Class<?>[] argTypes = { ClassLoader.class };  
  15.     Object[] args = { parentClassLoader };  
  16.     Constructor<?> constr = clazz.getConstructor(argTypes);  
  17.     classLoader = (WebappClassLoader) constr.newInstance(args);  
  18.   
  19.     return classLoader;  
  20.   
  21. }  

 

 这里loaderClass的值是字符串org.apache.catalina.loader.WebappClassLoader,通过反射来实例化WebappClassLoader。由于每个Webapp下的类资源由不同的WebappClassLoader负责加载,因此Webapp下各个Context的类资源是独立的。至此,整个Tomcat的类加载体系构建完毕。

此外每个jsp为了实现热替换,会有专门的类加载器负责加载。

参考文档:http://blog.csdn.net/beliefer/article/details/50995516

《深入理解Java虚拟机》

http://blog.csdn.net/czmacd/article/details/54017027

© 著作权归作者所有

粉丝 20
博文 53
码字总数 75396
作品 0
杭州
程序员
私信 提问
Spring源码解析系列之IOC容器(一)

前言 实际上我所有的博客都是原来对原来印象笔记里笔记内容的加工,关于Spring源码自己已经解析了很多遍,但是时间长总是忘记,写一篇博客权当加强记忆,也算再次学习下大师们的设计思想,思...

后厂村老司机
2018/06/02
0
0
Tomcat7.0源码分析——类加载体系

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/beliefer/article/details/50995516 前言Tomcat遵循J2EE规范,实现了Web容器。很多有关web的书籍和文章都离不...

泰山不老生
2016/03/28
0
0
Tomcat7 自动加载类及检测文件变动原理

在一般的web应用开发里通常会使用开发工具(如Eclipse、IntelJ)集成tomcat,这样可以将web工程项目直接发布到tomcat中,然后一键启动。经常遇到的一种情况是直接修改一个类的源文件,此时开...

JinHengyu
2017/11/16
1K
1
【问题】Tomcat中 执行tomcat7.exe报错

The system cannot find the Registry key for service 'tomcat7' 问题: 在启动解压后的tomcat7,点击bin下面的tomcat7.exe失败了,报错“The system cannot find the Registry key for ser......

xiaoxiaomo
2015/01/21
264
0
让Tomcat7识别War包的Class-Path(MANIFEST.MF)属性动态加载类路径

一、背景 几个Web项目需要打包到一个发布包中,问题是War包都各自包含了自己的WEB-INF/lib,其中不少依赖Jar有重复。所以希望把War包的所有的lib都放到外部的一个公共目录减小总工程大小。但...

NoahX
2013/11/01
6.6K
0

没有更多内容

加载失败,请刷新页面

加载更多

java通过ServerSocket与Socket实现通信

首先说一下ServerSocket与Socket. 1.ServerSocket ServerSocket是用来监听客户端Socket连接的类,如果没有连接会一直处于等待状态. ServetSocket有三个构造方法: (1) ServerSocket(int port);...

Blueeeeeee
今天
6
0
用 Sphinx 搭建博客时,如何自定义插件?

之前有不少同学看过我的个人博客(http://python-online.cn),也根据我写的教程完成了自己个人站点的搭建。 点此:使用 Python 30分钟 教你快速搭建一个博客 为防有的同学不清楚 Sphinx ,这...

王炳明
昨天
5
0
黑客之道-40本书籍助你快速入门黑客技术免费下载

场景 黑客是一个中文词语,皆源自英文hacker,随着灰鸽子的出现,灰鸽子成为了很多假借黑客名义控制他人电脑的黑客技术,于是出现了“骇客”与"黑客"分家。2012年电影频道节目中心出品的电影...

badaoliumang
昨天
15
0
很遗憾,没有一篇文章能讲清楚线程的生命周期!

(手机横屏看源码更方便) 注:java源码分析部分如无特殊说明均基于 java8 版本。 简介 大家都知道线程是有生命周期,但是彤哥可以认真负责地告诉你网上几乎没有一篇文章讲得是完全正确的。 ...

彤哥读源码
昨天
15
0
jquery--DOM操作基础

本文转载于:专业的前端网站➭jquery--DOM操作基础 元素的访问 元素属性操作 获取:attr(name);$("#my").attr("src"); 设置:attr(name,value);$("#myImg").attr("src","images/1.jpg"); ......

前端老手
昨天
7
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部