类加载器的双亲委派模型_java mock 模拟接口

类加载器的双亲委派模型_java mock 模拟接口JVM类加载器JVM主要有以下几种类加载器:引导类加载器主要加载JVM运行核心类库,位于JRE的lib目录下,如rt.jar中的类。扩展类加载器主要加载JVM中扩展类,位于JRE的ext目录下。应用程序类加载器主要负责加载ClassPath路径下的类,也就是业务类。自定义加载器负责加载用户自定义路径下的类。类加载器关系…

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺

JVM类加载器

JVM主要有以下几种类加载器:

  1. 引导类加载器
    主要加载JVM运行核心类库,位于JRE的lib目录下,如rt.jar中的类。
  2. 扩展类加载器
    主要加载JVM中扩展类,位于JRE的ext目录下。
  3. 应用程序类加载器
    主要负责加载ClassPath路径下的类,也就是业务类。
  4. 自定义加载器
    负责加载用户自定义路径下的类。

类加载器关系

在这里插入图片描述

源码解析

ExtClassLoader和AppClassLoader的创建流程

先看下Launcher的构造方法:

public Launcher() { 
   
        Launcher.ExtClassLoader var1;
        try { 
   
        	//获取扩展类加载器
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) { 
   
            throw new InternalError("Could not create extension class loader", var10);
        }
        
        try { 
   
        	//获取应用类加载器,this.loader就是默认的类加载器:即AppClassLoader
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) { 
   
            throw new InternalError("Could not create application class loader", var9);
        }
        //设置默认classLoader
        Thread.currentThread().setContextClassLoader(this.loader);
        String var2 = System.getProperty("java.security.manager");
    }

ExtClassLoader

看下ExtClassLoader的获取方法getExtClassloader():
可以看到ExtClassLoader是Launcher的一个内部类,继承的是URLClassLoader。

public static Launcher.ExtClassLoader getExtClassLoader() throws IOException { 
   
			//获取要加载的类文件
            final File[] var0 = getExtDirs();

            try { 
   
                return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() { 
   
                    public Launcher.ExtClassLoader run() throws IOException { 
   
                        int var1 = var0.length;

                        for(int var2 = 0; var2 < var1; ++var2) { 
   
                            MetaIndex.registerDirectory(var0[var2]);
                        }
						//new一个ExtClassLoader
                        return new Launcher.ExtClassLoader(var0);
                    }
                });
            } catch (PrivilegedActionException var2) { 
   
                throw (IOException)var2.getException();
            }
        }

查看getExtDirs()方法:可以看到要加载的类文件都是位于ext文件夹下的。

private static File[] getExtDirs() { 
   
            String var0 = System.getProperty("java.ext.dirs");
            File[] var1;
            if (var0 != null) { 
   
                StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
                int var3 = var2.countTokens();
                var1 = new File[var3];

                for(int var4 = 0; var4 < var3; ++var4) { 
   
                    var1[var4] = new File(var2.nextToken());
                }
            } else { 
   
                var1 = new File[0];
            }

            return var1;
        }

继续看ExtClassLoader的构造方法:

  public ExtClassLoader(File[] var1) throws IOException { 
   
            super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
            SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
        }

调用了父类的构造方法:
可以看到ExtClassLoader的parent赋值为null,因为引导类加载器是C++语言写的,没有实际java对象。

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

这样一个ExtClassLoader就创建好了。

AppClassLoader

AppClassLoader同样也是继承了URLClassLoader类
看下getAppClassLoader方法:

public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException { 
   
            final String var1 = System.getProperty("java.class.path");
            final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
            return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() { 
   
                public Launcher.AppClassLoader run() { 
   
                    URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
                    return new Launcher.AppClassLoader(var1x, var0);
                }
            });
        }

可以看到,getAppClassLoader主要加载工程classPath下的类文件。
继续看getAppClassLoader构造方法:

AppClassLoader(URL[] var1, ClassLoader var2) { 
   
            super(var1, var2, Launcher.factory);
            this.ucp.initLookupCache(this);
        }

从一开始的Launcher构造方法中可以看到参数var2就是先初始化的extClassLoader。
同样调用了父类URLClassLoader的构造,将extClassLoader设置为parent,所以appClassLoader的parent是extClassLoader。

由此三个主要类加载器之间的关系弄清楚了,各自要加载的范围也弄清楚。我们再看看自定义类加载器的实现。

自定义类加载器

自定义类加载器要继承ClassLoader方法,只需要重写findClass方法就行了:

package classload;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
/** * @author zhw * @description * @date 2021-07-15 14:36 */
public class MyClassLoader extends ClassLoader{ 
   

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException { 
   
        File file = new File("C:/Users/hiwei/Desktop/hiwei/test/Person.class");
        try{ 
   
            byte[] bytes = getClassBytes(file);
            //defineClass方法可以把二进制流字节组成的文件转换为一个java.lang.Class
            Class<?> c = defineClass(name, bytes, 0, bytes.length);
            return c;
        } catch (Exception e) { 
   
            e.printStackTrace();
        }
        return super.findClass(name);
    }

    private byte[] getClassBytes(File file) throws Exception
    { 
   
        FileInputStream inputStream = new FileInputStream(file);//原始输入流
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len;
        while ((len = inputStream.read(buffer)) != -1 ) { 
   
            baos.write(buffer, 0, len);
        }
        baos.flush();
        return baos.toByteArray();
    }
}

关于自定义类加载器的parent是谁,可以查看:

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

继续看getSystemClassLoader():

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) { 
   
            if (scl != null)
                throw new IllegalStateException("recursive invocation");
            sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
            if (l != null) { 
   
                Throwable oops = null;
                scl = l.getClassLoader();
            
            }
            sclSet = true;
        }
    }

 public ClassLoader getClassLoader() { 
   
        return this.loader;
    }

返回的是this.loader。上面已经知道loader就是AppClassLoader。所以自定义类加载器的默认parent就是AppClassLoader。

双亲委派

在类加载流程中,首先调用的是Launcher.loader.loadClass()方法。

public Launcher() { 
   
        Launcher.ExtClassLoader var1;
        try { 
   
        	//获取扩展类加载器
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) { 
   
            throw new InternalError("Could not create extension class loader", var10);
        }
        
        try { 
   
        	//获取应用类加载器,this.loader就是默认的类加载器:即AppClassLoader
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) { 
   
            throw new InternalError("Could not create application class loader", var9);
        }
        //设置默认classLoader
        Thread.currentThread().setContextClassLoader(this.loader);
        String var2 = System.getProperty("java.security.manager");
    }

loader就是AppClassLoader。所以继续看AppClassLoader.loadClass方法:

public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException { 
   
            int var3 = var1.lastIndexOf(46);
            if (var3 != -1) { 
   
                SecurityManager var4 = System.getSecurityManager();
                if (var4 != null) { 
   
                    var4.checkPackageAccess(var1.substring(0, var3));
                }
            }

            if (this.ucp.knownToNotExist(var1)) { 
   
                Class var5 = this.findLoadedClass(var1);
                if (var5 != null) { 
   
                    if (var2) { 
   
                        this.resolveClass(var5);
                    }

                    return var5;
                } else { 
   
                    throw new ClassNotFoundException(var1);
                }
            } else { 
   
            	//调用父类的loadClass方法
                return super.loadClass(var1, var2);
            }
        }

继续看super.loadClass(var1, var2):双亲委派机制的核心代码来了

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    { 
   
        synchronized (getClassLoadingLock(name)) { 
   
            // First, check if the class has already been loaded
            //先查看自己是否加载过这个类,如果加载过直接返回
            Class<?> c = findLoadedClass(name);
            if (c == null) { 
   
                long t0 = System.nanoTime();
                try { 
   
                	//如果父加载器不为null,则交给父加载器加载。
                    if (parent != null) { 
   
                        c = parent.loadClass(name, false);
                    } else { 
    //如果父加载器为null,则交给引导类加载器加载。
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) { 
   
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
				//如果父加载器未加载到改类,则自己加载
                if (c == null) { 
   
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    //自己加载
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) { 
   
                resolveClass(c);
            }
            return c;
        }
    }

看完上面的代码后,是不是觉得双亲委派机制的实现很简单?
在这里插入图片描述
双亲委派的作用:

  1. 沙箱安全,保证JVM核心代码不被用户自定义类覆盖。
  2. 保证了类加载的唯一性。

如何打破双亲委派?

看双亲委派机制的源码,可以看到主要实现实在loadClass方法中,那么,只需要重写loadClass(String name, boolean resolve)方法即可:

package classload;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
/** * @author zhw * @description * @date 2021-07-15 14:36 */
public class MyClassLoader extends ClassLoader{ 
   

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException { 
   
        File file = new File("C:/Users/hiwei/Desktop/hiwei/test/Person.class");
        try{ 
   
            byte[] bytes = getClassBytes(file);
            //defineClass方法可以把二进制流字节组成的文件转换为一个java.lang.Class
            Class<?> c = defineClass(name, bytes, 0, bytes.length);
            return c;
        } catch (Exception e) { 
   
            e.printStackTrace();
        }
        return super.findClass(name);
    }

    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
    { 
   
        synchronized (getClassLoadingLock(name)) { 
   
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) { 
   
                long t0 = System.nanoTime();
                //去掉双亲委派逻辑
                /*try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader }*/
                //添加自己的逻辑
                //如果是自己要加载的类 不给父加载器加载,其它的仍走双亲委派机制
                if("hiwei.test.Person".equals(name)){ 
   
                    c = findClass(name);
                }else{ 
   
                    c = getParent().loadClass(name);
                }
                
                if (c == null) { 
   
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) { 
   
                resolveClass(c);
            }
            return c;
        }
    }

    private byte[] getClassBytes(File file) throws Exception
    { 
   
        FileInputStream inputStream = new FileInputStream(file);//原始输入流
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len;
        while ((len = inputStream.read(buffer)) != -1 ) { 
   
            baos.write(buffer, 0, len);
        }
        baos.flush();
        return baos.toByteArray();
    }
}

测试类:

package classload;
/** * @author zhw * @description * @date 2021-07-15 15:09 */
public class ClassLoadTest { 
   
    public static void main(String[] args) throws Exception { 
   
        MyClassLoader myClassLoader = new MyClassLoader();
        Class<?> clazz = Class.forName("hiwei.test.Person", true, myClassLoader);
        Object o = clazz.newInstance();
        System.out.println(o.toString());
        System.out.println(clazz.getClassLoader());
    }
}

测试:
目标文件夹和classPath都存在Person.class

  1. 测试一:
    结果:使用自定义加载器加载。
    在这里插入图片描述
  2. 测试二:不覆盖loadClass方法。
    结果:使用AppClassLoader
    在这里插入图片描述

破坏双亲委派的应用

tomcat破环双亲委派

在这里插入图片描述
在tomcat中不同的应用可能依赖同一个jar的不同版本,如果共用一个类加载器,会导致无法进行环境隔离。所以tomcat自定义类加载器,每个应用都有自己的类加载器,负责加载自己应用下的类,打破了双亲委派机制,不在让父加载器先加载。

源码分析

tomcat的Bootstrap.initClassLoaders()方法中会初始化tomcat核心类的类加载器:

	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);
        }
    }

这三个类加载器并未破坏双亲委派模型,这三个都是URLClassLoader的实例。
真正破坏双亲委派模型的是WebappClassLoader类加载器,WebappClassLoader继承了WebappClassLoaderBase,而WebappClassLoaderBase重写了loadClass方法:

@Override
    //todo 此处破坏了双亲委派模型
    public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { 
   

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

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

            // (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;
            }
		//省略,,,,
    }

可以看到,重写的loadClass方法破坏了双亲委派模型。

JDBC破坏双亲委派

原生的JDBC中Driver驱动本身只是一个接口,并没有具体的实现,具体的实现是由不同数据库类型去实现的。例如,MySQL的jar中的Driver类具体实现的。
以以下版本为例:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.25</version>
</dependency>

Driver实现类:

public class Driver extends NonRegisteringDriver implements java.sql.Driver { 
   
    static { 
   
        try { 
   
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) { 
   
            throw new RuntimeException("Can't register driver!");
        }
    }

可以看到,使用了DriverManager类。在DriverManager类中有静态代码块:

	static { 
   
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

继续看loadInitialDrivers()

private static void loadInitialDrivers() { 
   
        String drivers;
        try { 
   
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() { 
   
                public String run() { 
   
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) { 
   
            drivers = null;
        }
        AccessController.doPrivileged(new PrivilegedAction<Void>() { 
   
            public Void run() { 
   

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                try{ 
   
                    while(driversIterator.hasNext()) { 
   
                        driversIterator.next();
                    }
                } catch(Throwable t) { 
   
                // Do nothing
                }
                return null;
            }
        });

        println("DriverManager.initialize: jdbc.drivers = " + drivers);

        if (drivers == null || drivers.equals("")) { 
   
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) { 
   
            try { 
   
                println("DriverManager.Initialize: loading " + aDriver);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) { 
   
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

看下面方法:

 ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
public static <S> ServiceLoader<S> load(Class<S> service) { 
   
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

使用了当前线程的classLoader。

	private ServiceLoader(Class<S> svc, ClassLoader cl) { 
   
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        reload();
    }

回到loadInitialDrivers()方法,继续往下看:

AccessController.doPrivileged(new PrivilegedAction<Void>() { 
   
            public Void run() { 
   

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                //加载Driver.class
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                try{ 
   
                    while(driversIterator.hasNext()) { 
   
                        driversIterator.next();
                    }
                } catch(Throwable t) { 
   
                // Do nothing
                }
                return null;
            }
        });

进入loadedDrivers.iterator():

public Iterator<S> iterator() { 
   
        return new Iterator<S>() { 
   

            Iterator<Map.Entry<String,S>> knownProviders
                = providers.entrySet().iterator();
            public boolean hasNext() { 
   
                if (knownProviders.hasNext())
                    return true;
                return lookupIterator.hasNext();
            }
            public S next() { 
   
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                return lookupIterator.next();
            }
            public void remove() { 
   
                throw new UnsupportedOperationException();
            }
        };
    }

可以看到返回了一个重写了hasNext()和next()方法的匿名Iterator类。

try{ 
   
        while(driversIterator.hasNext()) { 
   
            driversIterator.next();
           }
	 } 

在这里调用的都是重写方法。
由调用关系,最终可以看到下面的方法:

		private boolean hasNextService() { 
   
            if (nextName != null) { 
   
                return true;
            }
            if (configs == null) { 
   
                try { 
   
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                    	//找到Driver.calss
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) { 
   
                    fail(service, "Error locating configuration files", x);
                }
            }
            while ((pending == null) || !pending.hasNext()) { 
   
                if (!configs.hasMoreElements()) { 
   
                    return false;
                }
                pending = parse(service, configs.nextElement());
            }
            nextName = pending.next();
            return true;
        }

        private S nextService() { 
   
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try { 
   
            	//加载Driver.calss
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) { 
   
                fail(service,
                     "Provider " + cn + " not found");
            }
            if (!service.isAssignableFrom(c)) { 
   
                fail(service,
                     "Provider " + cn  + " not a subtype");
            }
            try { 
   
                S p = service.cast(c.newInstance());
                providers.put(cn, p);
                return p;
            } catch (Throwable x) { 
   
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }

可以看到,Driver.class是在hasNextService()中取到,nextService()中加载的:

c = Class.forName(cn, false, loader);

这里的类加载器loader就是上面的

ClassLoader cl = Thread.currentThread().getContextClassLoader();

现在真相大白了,在使用spi机制时,会使用当前线程的类加载器加载”META-INF/services/”下面的Driver.class。
在双亲委派模型下,类的加载是由下至上委托的,jdk无法加载其它文件夹下的类文件。但是在jdbc中,Driver要由供应商实现,所以需要进行加载,在spi使用方法中,使用线程上下文类加载器加载指定路径下的Driver.class文件,解决了这个问题。
JDBC破坏双亲委派的实现是使用父加载器加载指定路径下的class文件。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/169724.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • html2canvas, JsPDF生成pdf

    html2canvas, JsPDF生成pdf创建 pdf js 引入依赖 importVuefro vue importhtml2c html2canvas importJsPDFf jspdf constPDF PDF install function Vue options targetDom 需要打印的 dom 对象 name pdf 的名字 callback 回调函数 Vue prototype create

    2025年7月12日
    3
  • 基于Java开发的testNG接口自动化测试

    基于Java开发的testNG接口自动化测试1.TestNG简介TestNG是一个开源的测试框架与Junit的发行顺序:Junit3->TestNG->Junit4,TestNG的灵感来自于Junit3,在TestNG推出不久后,Junit借鉴了其中很多概念,也推出了差不多四年以来首个发行版本Junit4。所以,TestNG跟JUnit4很像,但它并不是JUnit的扩展,它的创建目的是超越Junit。TestNG具有更强…

    2025年8月13日
    3
  • sqlhelper 下载 使用指南 代码

    sqlhelper 下载 使用指南 代码

    2021年7月27日
    57
  • rj45对接头千兆(百兆以太网接口定义)

    展开全部以太网100Base-T4接口:1TX_D1+TranceiveData+(发送数据32313133353236313431303231363533e4b893e5b19e31333365643662+)2TX_D1-TranceiveData-(发送数据-)3RX_D2+ReceiveData+(接收数据+)4BI_D3+Bi-directionalDat…

    2022年4月16日
    135
  • 91p.wido.ws_tttzzzvipAPP

    91p.wido.ws_tttzzzvipAPP104.27.179.100北美地区IP网段:104.16.0.0-104.31.255.255更新时间:2014年07月19日18:47:41NetRange:104.16.0.0-104.31.255.255CIDR:104.16.0.0/12OriginAS:AS13335NetName:CLOUDFLARENETNetHandle:NET-104-16-0-0-1P…

    2025年6月23日
    2
  • PS套索工具抠图及快捷键[通俗易懂]

    PS套索工具抠图及快捷键[通俗易懂]一、首先打开Photoshop,并打开一张所需的要抠图的文件,并按Ctrl+J复制一层二、套索工具在工具栏的上方,快捷键为L。鼠标点击工具栏选择套索工具,或按快捷键L选择,套索工具有三个子工具菜单,套索,多边形套索和磁性套索1.普通套索工具。这个工具时根据操作者控制鼠标的路径来选取选取的,且精度不易控制,完全靠制作者的手法来控制精度我们只要选择这个工具,然后按着鼠标左键开始跟着鼠标轨迹把选取描绘出来,最后松开鼠标,即可完成2.多边形套索工具。适合选取比较规则的几何图形首先点区图片要扣取的一

    2022年6月22日
    59

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号