SpringBoot源码解读与原理分析(三十)AOP模块的生命周期(三)代理对象的底层执行逻辑

文章目录

    • 前言
    • 9.6 代理对象的底层执行逻辑
      • 9.6.1 DemoService#test
      • 9.6.2 获取增强器链
        • 9.6.2.1 前置准备
        • 9.6.2.2 匹配增强器
        • 9.6.2.3 匹配后的处理
        • 9.6.2.4 其他增强器的处理
      • 9.6.3 执行增强器
        • 9.6.3.1 执行proceed方法
        • 9.6.3.2 下标值++
        • 9.6.3.3 执行第一个增强器
        • 9.6.3.4 再次执行```proceed```方法
        • 9.6.3.5 执行第二个增强器
        • 9.6.3.6 执行目标对象方法
        • 9.6.3.7 流程小结
      • 9.6.4 JDK动态代理的执行底层

前言

在第五章 SpringBoot源码解读与原理分析(十六)SpringBoot的AOP支持 中,我们通过一个例子学习了SpringBoot整合AOP的用法,代码如下:

代码清单1// 组件类:DemoService.java
@Service
public class DemoService {public void test() {System.out.println("DemoService.test run ......");}}// 切面类:DemoServiceAspect.java
@Aspect
@Component
public class DemoServiceAspect {@Before("execution(public * com.xiaowd.springboot.aop.test01.*.*(..))")public void beforeTest() {System.out.println("DemoServiceAspect.beforeTest run ......");}}// 主启动类:Test01App.java
@SpringBootApplication
@EnableAspectJAutoProxy
public class Test01App {public static void main(String[] args) {ConfigurableApplicationContext context = SpringApplication.run(Test01App.class, args);context.getBean(DemoService.class).test();}}

执行main方法,控制台打印出以下内容:

DemoServiceAspect.beforeTest run ......
DemoService.test run ......

可见,AOP代理已经生效。本节来研究下当DemoService的test方法执行时,内部执行了哪些重要环节的操作。

9.6 代理对象的底层执行逻辑

9.6.1 DemoService#test

将断点打在DemoService的test方法上,Debug启动后发现程序会运行到CglibAopProxy的内部类DynamicAdvisedInterceptor的intercept方法上。

代码清单2CglibAopProxy.javaprivate static class DynamicAdvisedInterceptor implements MethodInterceptor, Serializable {private final AdvisedSupport advised;public DynamicAdvisedInterceptor(AdvisedSupport advised) {this.advised = advised;}@Override@Nullablepublic Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {Object oldProxy = null;boolean setProxyContext = false;Object target = null;TargetSource targetSource = this.advised.getTargetSource();try {// 如果在@EnableAspectJAutoProxy注解上配置了exposeProxy属性为true// 则会把当前代理对象放入AOP上下文中if (this.advised.exposeProxy) {oldProxy = AopContext.setCurrentProxy(proxy);setProxyContext = true;}// 从TargetSource中提取目标对象target = targetSource.getTarget();Class<?> targetClass = (target != null ? target.getClass() : null);// 根据当前执行的方法,获取要执行的增强器,并以列表返回(链的思想)List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);Object retVal;// 如果没有要执行的增强器,则直接执行目标方法if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);retVal = methodProxy.invoke(target, argsToUse);} else {// 否则,构造增强器链,执行增强器的逻辑retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();}retVal = processReturnType(proxy, target, method, retVal);return retVal;} // finally ......}
}

由 代码清单2 可知,intercept有两个核心步骤:获取增强器链、执行增强器链。

9.6.2 获取增强器链

代码清单3AdvisedSupport.javapublic List<Object> getInterceptorsAndDynamicInterceptionAdvice(Method method, @Nullable Class<?> targetClass) {MethodCacheKey cacheKey = new MethodCacheKey(method);List<Object> cached = this.methodCache.get(cacheKey);if (cached == null) {// 缓存增强器链cached = this.advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice(this, method, targetClass);this.methodCache.put(cacheKey, cached);}return cached;
}

由 代码清单4 可知,获取增强器链的核心逻辑是advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice。同时,获取的增强器链最终会被缓存起来,后续再执行被增强的方法时会直接拿到该增强器链,而无需再次重新解析。

9.6.2.1 前置准备
代码清单5DefaultAdvisorChainFactory.javapublic List<Object> getInterceptorsAndDynamicInterceptionAdvice(Advised config, Method method, @Nullable Class<?> targetClass) {// 初始化一个AdvisorAdapterRegistryAdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance();Advisor[] advisors = config.getAdvisors();List<Object> interceptorList = new ArrayList<>(advisors.length);Class<?> actualClass = (targetClass != null ? targetClass : method.getDeclaringClass());Boolean hasIntroductions = null;// ......
}

由 代码清单5 可知,getInterceptorsAndDynamicInterceptionAdvice首先会初始化一个AdvisorAdapterRegistry对象,意为增强器适配器的注册器*。它的主要作用是将AspectJ类型的增强器转换为方法拦截器MethodInterceptor并返回。

9.6.2.2 匹配增强器
代码清单6DefaultAdvisorChainFactory.javapublic List<Object> getInterceptorsAndDynamicInterceptionAdvice(Advised config, Method method, @Nullable Class<?> targetClass) {// 前置准备 ......for (Advisor advisor : advisors) {if (advisor instanceof PointcutAdvisor) {// 此处获取的是AspectJ形式的通知方法封装PointcutAdvisor pointcutAdvisor = (PointcutAdvisor) advisor;if (config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass)) {// 根据通知方法上的切入点表达式,判断是否可以匹配// 当前要执行的目标对象所属类MethodMatcher mm = pointcutAdvisor.getPointcut().getMethodMatcher();boolean match;// 引介匹配if (mm instanceof IntroductionAwareMethodMatcher) {if (hasIntroductions == null) {hasIntroductions = hasMatchingIntroductions(advisors, actualClass);}match = ((IntroductionAwareMethodMatcher) mm).matches(method, actualClass, hasIntroductions);} else {// 方法匹配match = mm.matches(method, actualClass);}// ......}} // else ......}return interceptorList;
}

由 代码清单6 可知,循环匹配增强器会把取出的增强器集合依次与当前正在调用的目标对象进行匹配,匹配方法借助MethodMatcher进行。

9.6.2.3 匹配后的处理
代码清单7DefaultAdvisorChainFactory.javapublic List<Object> getInterceptorsAndDynamicInterceptionAdvice(Advised config, Method method, @Nullable Class<?> targetClass) {// 前置处理 ......// 匹配增强器 ......// 匹配后的处理if (match) {// 封装MethodInterceptorMethodInterceptor[] interceptors = registry.getInterceptors(advisor);if (mm.isRuntime()) {for (MethodInterceptor interceptor : interceptors) {interceptorList.add(new InterceptorAndDynamicMethodMatcher(interceptor, mm));}} else {interceptorList.addAll(Arrays.asList(interceptors));}}// ......
}

由 代码清单7 可知,匹配到增强器后,开始封装方法拦截器MethodInterceptor。

注意这里有一个MethodMatcher的isRuntime判断,这是判断MethodMatcher是否是动态的。通常情况下,MethodMatcher都是静态匹配器,但如果MethodMatcher被设置为动态匹配器,则每次调用匹配方法时,可以提前获取方法调用的参数值列表。

代码清单8MethodMatcher.javaboolean matches(Method method, Class<?> targetClass);
boolean matches(Method method, Class<?> targetClass, Object... args);

由 代码清单8 可知,静态匹配器只会做基本的方法匹配,而动态匹配器可以提前获取方法调用的参数值列表并进行深度匹配。

匹配完成后,获得一个方法拦截器的List集合。

9.6.2.4 其他增强器的处理
代码清单9DefaultAdvisorChainFactory.javapublic List<Object> getInterceptorsAndDynamicInterceptionAdvice(Advised config, Method method, @Nullable Class<?> targetClass) {// 前置处理 ......for (Advisor advisor : advisors) {if (advisor instanceof PointcutAdvisor) {// 处理AspectJ类型的增强器 ......} else if (advisor instanceof IntroductionAdvisor) {// 处理引介增强器IntroductionAdvisor ia = (IntroductionAdvisor) advisor;if (config.isPreFiltered() || ia.getClassFilter().matches(actualClass)) {Interceptor[] interceptors = registry.getInterceptors(advisor);interceptorList.addAll(Arrays.asList(interceptors));}} else {// 处理其他类型的增强器Interceptor[] interceptors = registry.getInterceptors(advisor);interceptorList.addAll(Arrays.asList(interceptors));}}return interceptorList;
}

由 代码清单9 可知,除了处理AspectJ类型的增强器,还要处理引介增强器以及其他类型的增强器。

经过getInterceptorsAndDynamicInterceptionAdvice方法的处理后,当前目标对象要执行的方法被全部筛选出来,接下来的环节就是构建方法执行器。

9.6.3 执行增强器

回到DynamicAdvisedInterceptor的intercept方法(代码清单2),如果获取到了增强器链,则执行CglibMethodInvocation的proceed方法。

CglibMethodInvocation的proceed方法只是单纯地调用父类ReflectiveMethodInvocation的proceed方法。

代码清单10ReflectiveMethodInvocation.javaprotected final List<?> interceptorsAndDynamicMethodMatchers;
private int currentInterceptorIndex = -1;
public Object proceed() throws Throwable {if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {return invokeJoinpoint();}Object interceptorOrInterceptionAdvice =this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {InterceptorAndDynamicMethodMatcher dm =(InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) {return dm.interceptor.invoke(this);} else {return proceed();}} else {return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);}
}

下面借助Debug来拆解该方法。

9.6.3.1 执行proceed方法

第一次进入proceed方法,首先执行第一个if判断,比对当前已经执行过的增强器在增强器链的下标位置。

此时Debug可以发现currentInterceptorIndex=-1,判断-1≠2-1,因此不进入invokeJoinpoint方法,继续往下执行。

currentInterceptorIndex=-1

9.6.3.2 下标值++

接下来执行以下代码:

Object interceptorOrInterceptionAdvice =this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);

注意this.currentInterceptorIndex变量执行了一次自增操作,自增后currentInterceptorIndex值为0。

9.6.3.3 执行第一个增强器

由上图可知,interceptorsAndDynamicMethodMatchers.get(0)得到的增强器的类型是ExposeInvocationInterceptor.ADVISOR,经过if判断会进入else结构中,执行其invoke方法。

代码清单11ExposeInvocationInterceptor.javapublic Object invoke(MethodInvocation mi) throws Throwable {MethodInvocation oldInvocation = invocation.get();invocation.set(mi);try {return mi.proceed();} finally {invocation.set(oldInvocation);}
}

由 代码清单11 可知,invoke方法保存好MethodInvocation之后,继续向下执行proceed方法。

9.6.3.4 再次执行proceed方法

通过Debug发现,程序回到了 代码清单10 的第一步,不同的是此时currentInterceptorIndex的值不再是-1,而是0。判断0≠2-1,因此也不进入invokeJoinpoint方法,继续向下执行增强器的逻辑。

currentInterceptorIndex=0

9.6.3.5 执行第二个增强器

下一个要执行的增强器是MethodBeforeAdviceInterceptor。

代码清单12MethodBeforeAdviceInterceptor.javapublic Object invoke(MethodInvocation mi) throws Throwable {this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());return mi.proceed();
}
代码清单13AspectJMethodBeforeAdvice.javapublic void before(Method method, Object[] args, @Nullable Object target) throws Throwable {invokeAdviceMethod(getJoinPointMatch(), null, null);
}
代码清单14AbstractAspectJAdvice.javaprotected Object invokeAdviceMethod(@Nullable JoinPointMatch jpMatch, @Nullable Object returnValue, @Nullable Throwable ex)throws Throwable {return invokeAdviceMethodWithGivenArgs(argBinding(getJoinPoint(), jpMatch, returnValue, ex));
}protected Object invokeAdviceMethodWithGivenArgs(Object[] args) throws Throwable {Object[] actualArgs = args;if (this.aspectJAdviceMethod.getParameterCount() == 0) {actualArgs = null;}try {// 反射执行通知方法ReflectionUtils.makeAccessible(this.aspectJAdviceMethod);return this.aspectJAdviceMethod.invoke(this.aspectInstanceFactory.getAspectInstance(), actualArgs);} // catch ......
}

由 代码清单12-14 可知,MethodBeforeAdviceInterceptor会调用advice的before方法,一直调用到invokeAdviceMethodWithGivenArgs方法,最终借助反射调用AspectJ切面类的通知方法

通知方法执行完毕后,继续执行MethodInvocation的proceed方法,又回到了 代码清单10 的第一步。

9.6.3.6 执行目标对象方法

再次回到proceed方法后,此时currentInterceptorIndex的值为1,判断1=2-1,进入invokeJoinpoint方法,该方法会借助反射执行目标对象的目标方法

currentInterceptorIndex=1

9.6.3.7 流程小结

代理对象的底层执行逻辑:利用一个全局索引值决定每次执行的拦截器,当所有拦截器都执行完时,索引值刚好等于size()-1,此时就可以执行真正的目标方法。

9.6.4 JDK动态代理的执行底层

默认情况下,SpringBoot使用Cglib动态代理。而要想在SpringBoot 2.x中激活JDK动态代理,必须在application.properties中显式配置spring.aop.proxy-target-class=false,才会默认禁用Cglib的配置,激活JDK动态代理。

JDK动态代理的核心类是JdkDynamicAopProxy,它的invoke如下:

代码清单15JdkDynamicAopProxy.javapublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {Object oldProxy = null;boolean setProxyContext = false;TargetSource targetSource = this.advised.targetSource;Object target = null;try {// 处理一些不代理的情况if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {// equals方法不代理return equals(args[0]);} else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {// hashCode方法不代理return hashCode();} else if (method.getDeclaringClass() == DecoratingProxy.class) {// 方法来自DecoratingProxy接口的不代理return AopProxyUtils.ultimateTargetClass(this.advised);} else if (!this.advised.opaque && method.getDeclaringClass().isInterface() &&method.getDeclaringClass().isAssignableFrom(Advised.class)) {// 目标对象本身实现了Advised接口的不代理return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);}Object retVal;if (this.advised.exposeProxy) oldProxy = AopContext.setCurrentProxy(proxy);setProxyContext = true;}// Get as late as possible to minimize the time we "own" the target,// in case it comes from a pool.target = targetSource.getTarget();Class<?> targetClass = (target != null ? target.getClass() : null);// 根据当前执行的方法获取要执行的增强器链List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);if (chain.isEmpty()) {// 增强器链为空,直接执行目标方法Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);} else {// 增强器链不为空,执行增强器链MethodInvocation invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);retVal = invocation.proceed();}// ......return retVal;} // finally ......
}

由 代码清单15 可知,JDK动态代理首先会处理一些不代理的情况(如equals方法、hashCode方法等),接着根据当前执行的方法获取要执行的增强器链,如果增强器链为空,直接执行目标方法;如果增强器链不为空,执行增强器链。

可以发现,JDK动态代理的逻辑和CglibAopProxy的实现逻辑是基本一致的,获取增强器链的getInterceptorsAndDynamicInterceptionAdvice方法是同一个,方法执行器也都是ReflectiveMethodInvocation,其proceed也是同一个。

······

本节完,更多内容请查阅分类专栏:SpringBoot源码解读与原理分析

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/262896.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

strings.xml补充知识

复数名词 <plurals name"book"><item name"one">book</item><item name"others">books</item> </plurals>int bookCount 4; Resources res getResources(); String bookCount res.getQuantityString(R.…

2.23数据结构

单向循环链表 创建单向循环链表&#xff0c;创建节点 &#xff0c;头插&#xff0c;按位置插入&#xff0c;输出&#xff0c;尾删&#xff0c;按位置删除功能 //main.c #include "loop_list.h" int main() {loop_p Hcreate_head();insert_head(H,12);insert_head(…

如何查看电脑使用记录?保障个人隐私和安全

查看电脑使用记录是了解电脑活动的一种重要方式&#xff0c;可以帮助用户追踪应用程序的使用情况、登录和关机时间、文件的访问记录等。在本文中&#xff0c;我们将介绍如何查看电脑使用记录的三个方法&#xff0c;以分步骤详细说明如何查看电脑使用记录&#xff0c;帮助用户更…

杂题——1097: 蛇行矩阵

题目描述 蛇形矩阵是由1开始的自然数依次排列成的一个矩阵上三角形。 输入格式 本题有多组数据&#xff0c;每组数据由一个正整数N组成。&#xff08;N不大于100&#xff09; 输出格式 对于每一组数据&#xff0c;输出一个N行的蛇形矩阵。两组输出之间不要额外的空行。矩阵三角…

如何用Docker+jenkins 运行 python 自动化?

1.在 Linux 服务器安装 docker 2.创建 jenkins 容器 3.根据自动化项目依赖包构建 python 镜像(构建自动化 python 环境) 4.运行新的 python 容器&#xff0c;执行 jenkins 从仓库中拉下来的自动化项目 5.执行完成之后删除容器 环境准备 Linux 服务器一台(我的是 CentOS7) …

Jmeter之内置函数__property和__P的区别

1. __property函数 作用 读取 Jmeter 属性 语法格式 ${__property(key,var,default)} 参数讲解 小栗子 ${__property(key)} 读取 key 属性如果找不到 key 属性&#xff0c;则返回 key&#xff08;属性名&#xff09; ${__property(key,,default)} 读取 key 属性如果找不到 k…

PHP实现分离金额和其他内容便于统计计算

得到的结果可以粘贴到excel计算 <?php if($_GET["x"] "cha"){ $tips isset($_POST[tips]) ? $_POST[tips] : ; $pattern /(\d\.\d|\d)/; $result preg_replace($pattern, "\t\${1}\t", $tips); echo "<h2><strong>数…

Python流程控制有知道的吗?

流程控制是编程的核心概念之一&#xff0c;Python也不例外。在Python中&#xff0c;程序的流程控制结构主要包括顺序结构、选择结构和循环结构。这些结构让程序员能够更好地组织代码&#xff0c;使其按照特定的逻辑执行。 1.顺序结构 顺序结构是Python中最简单的流程控制结构&…

ELK介绍以及搭建

基础环境 hostnamectl set-hostname els01 hostnamectl set-hostname els02 hostnamectl set-hostname els03 hostnamectl set-hostname kbased -i s/SELINUXenforcing/SELINUXdisabled/ /etc/selinux/config systemctl stop firewalld & systemctl disable firewalld# 安…

Unity数据持久化之PlayerPrefs

这里写目录标题 PlayerPrefs概述基本方法PlayerPrefs存储位置实践小项目反射知识补充数据管理类的创建反射存储数据----常用成员反射存储数据----List成员反射存储数据----Dictionary成员反射存储数据----自定义类成员反射读取数据----常用成员反射读取数据----List成员反射读取…

Sora-OpenAI 的 Text-to-Video 模型:制作逼真的 60s 视频片段

OpenAI 推出的人工智能功能曾经只存在于科幻小说中。 2022年&#xff0c;Openai 发布了 ChatGPT&#xff0c;展示了先进的语言模型如何实现自然对话。 随后&#xff0c;DALL-E 问世&#xff0c;它利用文字提示生成令人惊叹的合成图像。 现在&#xff0c;他们又推出了 Text-t…

选择适合你的编程语言

引言 在当今瞬息万变的技术领域中&#xff0c;选择一门合适的编程语言对于个人职业发展和技术成长至关重要。每种语言都拥有独特的设计哲学、应用场景和市场需求&#xff0c;因此&#xff0c;在决定投入时间和精力去学习哪种编程语言时&#xff0c;我们需要综合分析多个因素&a…

在 Jupyter Notebook 中查看所使用的 Python 版本和 Python 解释器路径

&#x1f349; CSDN 叶庭云&#xff1a;https://yetingyun.blog.csdn.net/ 我们在做 Python 开发时&#xff0c;有时在我们的服务器上可能安装了多个 Python 版本。 使用 conda info --envs 可以列出所有的 conda 环境。当在 Linux 服务器上使用 which python 命令时&#xff0…

绿盾限制终端网络访问权限会恢复后,别的网站访问正常就是无法访问钉钉网站和下载东西

环境&#xff1a; Win10 专业版 钉钉7.5.5 绿盾7.0 问题描述&#xff1a; 绿盾限制终端网络访问权限会恢复后&#xff0c;别的网站访问正常就是无法访问钉钉网站和下载东西 解决方案&#xff1a; 排查方法 1.重置浏览器或者更换浏览器测试&#xff08;未解决&#xff09…

游戏行业洞察:分布式开源爬虫项目在数据采集与分析中的应用案例介绍

前言 我在领导一个为游戏行业巨头提供数据采集服务的项目中&#xff0c;我们面临着实时数据需求和大规模数据处理的挑战。我们构建了一个基于开源分布式爬虫技术的自动化平台&#xff0c;实现了高效、准确的数据采集。通过自然语言处理技术&#xff0c;我们确保了数据的质量和…

Flutter插件开发指南02: 事件订阅 EventChannel

Flutter插件开发指南02: 事件订阅 EventChannel 视频 https://www.bilibili.com/video/BV1zj411d7k4/ 前言 上一节我们讲了 Channel 通道&#xff0c;但是如果你是卫星定位业务&#xff0c;原生端主动推消息给 Flutter 这时候就要用到 EventChannel 通道了。 本节会写一个 1~…

Maven setting.xml 配置

目的&#xff1a;可以把我们书写的jar包发布到maven私有仓库&#xff0c;简称私仓 1. 打开云效 2.点击 非生产库-snapshot mave release仓库与snapshot仓库区别&#xff1f; 在软件开发中&#xff0c;"Maven release 仓库"和"Maven snapshot 仓库"是两种…

[极客大挑战2019]upload

该题考点&#xff1a;后缀黑名单文件内容过滤php木马的几种书写方法 phtml可以解析php代码&#xff1b;<script language"php">eval($_POST[cmd]);</script> 犯蠢的点儿&#xff1a;利用html、php空格和php.不解析<script language"php"&…

AI文生图网站测评

主要测评文章配图生成效果、绘制logo等效果 测评关键点&#xff1a;生成效果、网站易用度、是否免费 测评prompt&#xff1a;请生成一个文章内容配图&#xff0c;图片比例是3&#xff1a;2&#xff0c;文章主旨是AI既是机遇&#xff0c;也存在挑战和风险&#xff0c;要求图片…

PyTorch概述(二)---MNIST

NIST Special Database3 具体指的是一个更大的特殊数据库3&#xff1b;该数据库的内容为手写数字黑白图片&#xff1b;该数据库由美国人口普查局的雇员手写 NIST Special Database1 特殊数据库1&#xff1b;该数据库的内容为手写数字黑白图片&#xff1b;该数据库的图片由高…