类加载器的双亲委派模型_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文件。

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

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

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • Django模糊查询「建议收藏」

    Django模糊查询「建议收藏」本文介绍了Django中的单子段和多字段联合的模糊查询

  • datagrip 激活码_在线激活

    (datagrip 激活码)JetBrains旗下有多款编译器工具(如:IntelliJ、WebStorm、PyCharm等)在各编程领域几乎都占据了垄断地位。建立在开源IntelliJ平台之上,过去15年以来,JetBrains一直在不断发展和完善这个平台。这个平台可以针对您的开发工作流进行微调并且能够提供…

  • DeepLearning之LSTM模型输入参数:time_step, input_size, batch_size的理解[通俗易懂]

    DeepLearning之LSTM模型输入参数:time_step, input_size, batch_size的理解[通俗易懂]1.LSTM模型输入参数理解(LongShort-TermMemory)lstm是RNN模型的一种变种模式,增加了输入门,遗忘门,输出门。LSTM也是在时间序列预测中的常用模型。小白我也是从这个模型入门来开始机器学习的坑。LSTM的基本概念与各个门的解释已经有博文写的非常详细:推荐博文:【译】理解LSTM(通俗易懂版)这篇文章写的非常详细,生动,概念解释的非常清楚。我也是从这个…

  • acm博弈论经典模型_博弈论分析

    acm博弈论经典模型_博弈论分析基本覆盖了比赛中常用到的博弈论知识点,之前整理的,最近要开始系统的看博弈论,先找出来复习一下。

    2022年10月10日
  • latex 希腊字母加粗_mathtype公式取消加粗

    latex 希腊字母加粗_mathtype公式取消加粗在编辑公式时,当使用\mathbf{\sigma}时,\mathbf{}不起作用?【解决方案】方案一、用\usepackage{amsmath}\boldsymbol{\sigma}\mathbf只对公式中的普通字母ABC…abcdef等起作用。方案二、更好的方法是使用\usepackage{bm}\bm{}来加粗。…

    2022年10月13日
  • STM32F103+RFID-RC522模块 实现简单读卡写卡demo「建议收藏」

    目录前言代码下载:功能介绍:接线STM32STM32F1开发指南(精英版)-库函数版本_V1.2STM32中文参考手册RFID-RC522RFID射频模块电路原理图使用图+效果图一、先用手机软件NFCWriter读取空卡看看内容1、打开软件和NFC(ps:我的手机是小米10)2、将空卡贴于手机背部,弹出提示发现新卡,点击“好的”3、上面的新卡片左滑到新卡片1,单击这个卡片4、进入卡片信息详细页面钥匙扣卡M1空白卡二、编译、烧写程序三、将钥匙扣卡发在模块上,打开串口,开始测试核心代码main.crc522.

发表回复

您的电子邮箱地址不会被公开。

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