Spring IoC componet-scan 节点解析详解
前言
我们在了解 Spring 容器的扩展功能 (ApplicationContext) 之前,先介绍下 context:componet-scan
标签的解析过程,其作用很大是注解能生效的关键所在。
正文
我们此次直接从 BeanDefinitionParseDelegate#parseCustomElement()
开始往下分析,不知道前面流程的可以看一下 [Spring XML Bean 定义的加载和注册](https://leisurexi.github.io/category/2020/04/14/Spring IoC/Spring XML Bean 定义的加载和注册.html) 这篇文章,之前的流程都有解析。
BeanDefinitionParseDelegate#parseCustomElement()
public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
// 获取自定义标签的命名空间
String namespaceUri = getNamespaceURI(ele);
if (namespaceUri == null) {
return null;
}
// 获取自定义标签的处理器,这里解析的是context:component-scan,所以获取的是ContextNamespaceHandler
NamespaceHandler handler =
this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
if (handler == null) {
error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
return null;
}
// 进行标签的解析,见下文详解
return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}
ComponentScanBeanDefinitionParser#parse
public BeanDefinition parse(Element element, ParserContext parserContext) {
// 获取属性basePacket的值
String basePackage = element.getAttribute(BASE_PACKAGE_ATTRIBUTE);
// 解析占位符,例如${basePackage}
basePackage = parserContext.getReaderContext().getEnvironment().resolvePlaceholders(basePackage);
// 解析base-package(允许通过 ",; \t\n" 中的任一符号填写多个),例如: com.leisurexi.one;com.leisurexi.two
String[] basePackages = StringUtils.tokenizeToStringArray(basePackage,
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
// Actually scan for bean definitions and register them.
// 构建和配置ClassPathBeanDefinitionScanner,见下文详解
ClassPathBeanDefinitionScanner scanner = configureScanner(parserContext, element);
// 使用scanner在指定的包路径进行扫描,返回注册后的BeanDefinition,见下文详解
Set<BeanDefinitionHolder> beanDefinitions = scanner.doScan(basePackages);
// 注册组件(包括一些内部注解的后置处理器),见下文详解
registerComponents(parserContext.getReaderContext(), beanDefinitions, element);
return null;
}
ComponetScanBeanDefinitionParser#configureScanner
protected ClassPathBeanDefinitionScanner configureScanner(ParserContext parserContext, Element element) {
// 是否使用默认的过滤器,默认为true
boolean useDefaultFilters = true;
// 如果设置了use-default-filters属性,则使用设置的值
if (element.hasAttribute(USE_DEFAULT_FILTERS_ATTRIBUTE)) {
useDefaultFilters = Boolean.parseBoolean(element.getAttribute(USE_DEFAULT_FILTERS_ATTRIBUTE));
}
// Delegate bean definition registration to scanner class.
// 构建ClassPathBeanDefinitionScanner,将bean定义注册委托给scanner类,见下文详解
ClassPathBeanDefinitionScanner scanner = createScanner(parserContext.getReaderContext(), useDefaultFilters);
scanner.setBeanDefinitionDefaults(parserContext.getDelegate().getBeanDefinitionDefaults());
scanner.setAutowireCandidatePatterns(parserContext.getDelegate().getAutowireCandidatePatterns());
// 解析resource-pattern属性
if (element.hasAttribute(RESOURCE_PATTERN_ATTRIBUTE)) {
scanner.setResourcePattern(element.getAttribute(RESOURCE_PATTERN_ATTRIBUTE));
}
try {
// 解析name-generator属性
parseBeanNameGenerator(element, scanner);
}
catch (Exception ex) {
parserContext.getReaderContext().error(ex.getMessage(), parserContext.extractSource(element), ex.getCause());
}
try {
// 解析scope-resolver、scoped-proxy属性
parseScope(element, scanner);
}
catch (Exception ex) {
parserContext.getReaderContext().error(ex.getMessage(), parserContext.extractSource(element), ex.getCause());
}
// 解析类型过滤器,见下文详解
parseTypeFilters(element, scanner, parserContext);
return scanner;
}
ComponentScanBeanDefinitionParser#createScanner
protected ClassPathBeanDefinitionScanner createScanner(XmlReaderContext readerContext, boolean useDefaultFilters) {
return new ClassPathBeanDefinitionScanner(readerContext.getRegistry(), useDefaultFilters,readerContext.getEnvironment(), readerContext.getResourceLoader());
}
// ClassPathBeanDefinitionScanner.java
public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters,Environment environment, @Nullable ResourceLoader resourceLoader) {
Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
this.registry = registry;
// 如果使用默认的过滤器,注册默认的过滤器,见下文详解
if (useDefaultFilters) {
registerDefaultFilters();
}
setEnvironment(environment);
setResourceLoader(resourceLoader);
}
ClassPathScanningCandidateComponentProvider#registerDefaultFilters
protected void registerDefaultFilters() {
// 将注解@Component添加到includeFilters中,这将隐式的注册所有@Componet的派生注解,例如@Repository、@Service、@Controller
this.includeFilters.add(new AnnotationTypeFilter(Component.class));
ClassLoader cl = ClassPathScanningCandidateComponentProvider.class.getClassLoader();
try {
// 如果当前环境中有@ManagedBean,添加到includeFilters中,否则会catch未找到类的异常并忽略
this.includeFilters.add(new AnnotationTypeFilter(((Class<? extends Annotation>) ClassUtils.forName("javax.annotation.ManagedBean", cl)), false));
logger.trace("JSR-250 'javax.annotation.ManagedBean' found and supported for component scanning");
}
catch (ClassNotFoundException ex) {
// JSR-250 1.1 API (as included in Java EE 6) not available - simply skip.
}
try {
// 如果当前环境中有@Named,添加到includeFilters中,否则会catch未找到类的异常并忽略
this.includeFilters.add(new AnnotationTypeFilter(((Class<? extends Annotation>) ClassUtils.forName("javax.inject.Named", cl)), false));
logger.trace("JSR-330 'javax.inject.Named' annotation found and supported for component scanning");
}
catch (ClassNotFoundException ex) {
// JSR-330 API not available - simply skip.
}
}
ClassPathBeanDefinitionScanner#doScan
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
for (String basePackage : basePackages) {
// 寻找包路径下符合要求的bean定义(最常见的就是使用@Component标注的类)
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
for (BeanDefinition candidate : candidates) {
// 解析@Scope注解,没有标注默认为singleton作用域
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
candidate.setScope(scopeMetadata.getScopeName());
// 生成bean的名称,见下文详解
String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
if (candidate instanceof AbstractBeanDefinition) {
// beanDefinition的后置处理,就是设置beanDefinition的一些默认属性,如autowireMode、initMethod等,并且设置AutowireCandidate,
// 一般为true代表可以自动装配到其他bean中
postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
}
if (candidate instanceof AnnotatedBeanDefinition) {
// 处理公共注解,见下文详解
AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
}
// 如果beanDefinition通过检查(大多时候是检查是不是第一次注册),则将definitionHolder添加进注册中心中,如果有同名的bean在这里会直接抛出异常
if (checkCandidate(beanName, candidate)) {
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
definitionHolder =
AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
beanDefinitions.add(definitionHolder);
registerBeanDefinition(definitionHolder, this.registry);
}
}
}
return beanDefinitions;
}
ClassPathScanningCandidateComponentProvider#findCandidateComponents
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
if (this.componentsIndex != null && indexSupportsIncludeFilters()) {
return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
}
else {
// 扫描符合条件的组件
return scanCandidateComponents(basePackage);
}
}
private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
Set<BeanDefinition> candidates = new LinkedHashSet<>();
try {
// 将要扫描的包的路径拼成完整的,例如:com.leisurexi.ioc.context 会被拼成 classpath*:com/leisurexi/ioc/context/**/*.class
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
resolveBasePackage(basePackage) + '/' + this.resourcePattern;
// 获取路径下的所有类的资源
Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
for (Resource resource : resources) {
// 如果资源是可读的
if (resource.isReadable()) {
try {
// 使用metadataReader读取资源,MetadataReader是专门用来访问元数据的类(包括: 类元数据ClassMetadata、注解元数据AnnotationMetadata等)
MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
// 判断该类是不是候选的组件
if (isCandidateComponent(metadataReader)) {
ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
sbd.setResource(resource);
sbd.setSource(resource);
// 判断是否是候选组件,默认条件是class不是接口并且不依赖于内部类
if (isCandidateComponent(sbd)) {
candidates.add(sbd);
}
}
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException("Failed to read candidate component class: " + resource, ex);
}
}
}
}
catch (IOException ex) {
throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
}
return candidates;
}
上面代码中判断该类是不是候选的组件的方法 isCandidateComponent(metadataReader)
一般情况下 是判断该类是否标注了上文中 ClassPathScanningCandidateComponentProvider#registerDefaultFilters()
中的注解 @Compont
、@ManagedBean
、@Named
;@Componet
注解是肯定会存在与当前环境中的,所以标注了 @Compont
或者其派生的注解如:@Repository
、@Service
、@Controller
都会被当做候选组件。
AnnotationBeanNameGenerator#generateBeanName
public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
// 如果BeanDefinition是AnnotatedBeanDefinition类型
if (definition instanceof AnnotatedBeanDefinition) {
// 获取自定义的beanName,如 @Component注解的value值
String beanName = determineBeanNameFromAnnotation((AnnotatedBeanDefinition) definition);
// 如果不为空,并且不是空字符串
if (StringUtils.hasText(beanName)) {
// Explicit bean name found.
// 返回明确的beanName
return beanName;
}
}
// Fallback: generate a unique default bean name.
// 没有手动指定beanName,默认生成一个,是当前类名的首字母小写,如User类的beanName是user
return buildDefaultBeanName(definition, registry);
}
AnnotationConfigUtils#processCommonDefinitionAnnotations
static void processCommonDefinitionAnnotations(AnnotatedBeanDefinition abd, AnnotatedTypeMetadata metadata) {
// 如果bean标注了@Lazy注解,进行解析
AnnotationAttributes lazy = attributesFor(metadata, Lazy.class);
if (lazy != null) {
abd.setLazyInit(lazy.getBoolean("value"));
}
else if (abd.getMetadata() != metadata) {
lazy = attributesFor(abd.getMetadata(), Lazy.class);
if (lazy != null) {
abd.setLazyInit(lazy.getBoolean("value"));
}
}
// 解析@Primary注解,自动装配时当出现多个Bean都匹配时,标注了@Primary注解的Bean将作为首选者
if (metadata.isAnnotated(Primary.class.getName())) {
abd.setPrimary(true);
}
// 解析@DependsOn注解,表明当前bean需要依赖的bean,会保证依赖的bean会在当前bean前实例化
AnnotationAttributes dependsOn = attributesFor(metadata, DependsOn.class);
if (dependsOn != null) {
abd.setDependsOn(dependsOn.getStringArray("value"));
}
// 解析@Role注解,用于标识bean的分类,实际用的比较少
AnnotationAttributes role = attributesFor(metadata, Role.class);
if (role != null) {
abd.setRole(role.getNumber("value").intValue());
}
// 解析@Description注解,bean的描述,实际用的比较少
AnnotationAttributes description = attributesFor(metadata, Description.class);
if (description != null) {
abd.setDescription(description.getString("value"));
}
}
ComponentScanBeanDefinitionParser#registerComponents
protected void registerComponents(XmlReaderContext readerContext, Set<BeanDefinitionHolder> beanDefinitions, Element element) {
Object source = readerContext.extractSource(element);
CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), source);
// 将扫描的所有BeanDefinition添加到compositeDef中
for (BeanDefinitionHolder beanDefHolder : beanDefinitions) {
compositeDef.addNestedComponent(new BeanComponentDefinition(beanDefHolder));
}
// Register annotation config processors, if necessary.
boolean annotationConfig = true;
// 获取annotation-config属性,默认为true
if (element.hasAttribute(ANNOTATION_CONFIG_ATTRIBUTE)) {
annotationConfig = Boolean.parseBoolean(element.getAttribute(ANNOTATION_CONFIG_ATTRIBUTE));
}
if (annotationConfig) {
// 向注册中心中注册用于注解的后置处理器,如AutowireAnnotationProcessor、CommonAnnotationProcessor
Set<BeanDefinitionHolder> processorDefinitions =
AnnotationConfigUtils.registerAnnotationConfigProcessors(readerContext.getRegistry(), source);
// 将注册的注解后置处理器的BeanDefinition添加到compositeDef的nestedComponents属性中
for (BeanDefinitionHolder processorDefinition : processorDefinitions) {
compositeDef.addNestedComponent(new BeanComponentDefinition(processorDefinition));
}
}
// 触发注册事件,默认实现为EmptyReaderEventListener(空实现,没有具体操作)
readerContext.fireComponentRegistered(compositeDef);
}
AnnotationConfigUtils#registerAnnotationConfigProcessors
public static Set<BeanDefinitionHolder> registerAnnotationConfigProcessors(
BeanDefinitionRegistry registry, @Nullable Object source) {
// 获取beanFactory
DefaultListableBeanFactory beanFactory = unwrapDefaultListableBeanFactory(registry);
if (beanFactory != null) {
if (!(beanFactory.getDependencyComparator() instanceof AnnotationAwareOrderComparator)) {
// 设置dependencyComparator属性
beanFactory.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE);
}
if (!(beanFactory.getAutowireCandidateResolver() instanceof ContextAnnotationAutowireCandidateResolver)) {
// 设置autowireCandidateResolver属性(设置自动注入候选对象的解析器,用于判断BeanDefinition是否为候选对象)
beanFactory.setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver());
}
}
Set<BeanDefinitionHolder> beanDefs = new LinkedHashSet<>(8);
// 注册用于处理@Configuration注解的后置处理器
if (!registry.containsBeanDefinition(CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)) {
// 创建一个ConfigurationClassPostProcessor的BeanDefinition
RootBeanDefinition def = new RootBeanDefinition(ConfigurationClassPostProcessor.class);
def.setSource(source);
// 将def添加进注册中心
beanDefs.add(registerPostProcessor(registry, def, CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME));
}
// 注册用于处理@Autowired、@Value、@Inject注解的后置处理器
if (!registry.containsBeanDefinition(AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)) {
RootBeanDefinition def = new RootBeanDefinition(AutowiredAnnotationBeanPostProcessor.class);
def.setSource(source);
beanDefs.add(registerPostProcessor(registry, def, AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
}
// Check for JSR-250 support, and if present add the CommonAnnotationBeanPostProcessor.
// 注册用于处理@Resource、@PostConstructor、@PostDestroy注解的后置处理器
if (jsr250Present && !registry.containsBeanDefinition(COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)) {
RootBeanDefinition def = new RootBeanDefinition(CommonAnnotationBeanPostProcessor.class);
def.setSource(source);
beanDefs.add(registerPostProcessor(registry, def, COMMON_ANNOTATION_PROCESSOR_BEAN_NAME));
}
// Check for JPA support, and if present add the PersistenceAnnotationBeanPostProcessor.
// 注册用于处理JPA注解的后置处理器,如@PersistenceContext、@PersistenceUnit
if (jpaPresent && !registry.containsBeanDefinition(PERSISTENCE_ANNOTATION_PROCESSOR_BEAN_NAME)) {
RootBeanDefinition def = new RootBeanDefinition();
try {
def.setBeanClass(ClassUtils.forName(PERSISTENCE_ANNOTATION_PROCESSOR_CLASS_NAME,
AnnotationConfigUtils.class.getClassLoader()));
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException(
"Cannot load optional framework class: " + PERSISTENCE_ANNOTATION_PROCESSOR_CLASS_NAME, ex);
}
def.setSource(source);
beanDefs.add(registerPostProcessor(registry, def, PERSISTENCE_ANNOTATION_PROCESSOR_BEAN_NAME));
}
// 注册用于处理@EventListener注解的后置处理器
if (!registry.containsBeanDefinition(EVENT_LISTENER_PROCESSOR_BEAN_NAME)) {
RootBeanDefinition def = new RootBeanDefinition(EventListenerMethodProcessor.class);
def.setSource(source);
beanDefs.add(registerPostProcessor(registry, def, EVENT_LISTENER_PROCESSOR_BEAN_NAME));
}
// 注册内部管理用于生产ApplicationListener对象的EventListenerFactory对象
if (!registry.containsBeanDefinition(EVENT_LISTENER_FACTORY_BEAN_NAME)) {
RootBeanDefinition def = new RootBeanDefinition(DefaultEventListenerFactory.class);
def.setSource(source);
beanDefs.add(registerPostProcessor(registry, def, EVENT_LISTENER_FACTORY_BEAN_NAME));
}
return beanDefs;
}
总结
本文主要介绍了 Spring 解析 context:component-scan
标签的解析流程,我们可以重新整理一下思路:
- 首先添加默认的过滤器,也就是判定哪些类会被作为组件来注册;比如默认过滤器添加后,会将标注了
@Component
注解以及它的派生注解当做组件注册。 - 找到指定包路径下所有符合条件的类并封装成
BeanDefinition
;解析@Scope
注解,没有默认为singleton
;生成bean
的名称,首先会用@Component
的value
属性,如果为空或空字符串则默认使用类名,并把首字母转成小写;然后对一些注解的解析,如@Lazy
、@Primary
、@DependsOn
等;最后检查注册中心中是否包含当前beanName
,如果没有直接添加进注册中心,否则如果不是同一个类会抛出异常。比如com.leisurexi.a.User
和com.leisurexi.b.User
同时添加@Component
注解并且不手动指定不同的beanName
在启动时就会抛出异常。 - 最后注册中心注册一些注解的后置处理器,如下:
- 处理
@Configuration
注解的ConfigurationClassPostProcessor
。 - 处理
@Autowired
、@Value
、@Inject
注解的AutowiredAnnotationBeanPostProcessor
。 - 处理
@Resource
、@PostConstructor
、@PostDestroy
注解的CommonAnnotationBeanPostProcessor
。 - 处理JPA注解的
PersistenceAnnotationBeanPostProcessor
。 - 处理
@EventListener
注解的EventListenerMethodProcessor
。
- 处理
最后,我模仿 Spring 写了一个精简版,代码会持续更新。地址:https://github.com/leisurexi/tiny-spring。访问新博客地址,观看效果更佳 https://leisurexi.github.io/
Spring IoC componet-scan 节点解析详解的更多相关文章
- spring在IoC容器中装配Bean详解
1.Spring配置概述 1.1.概述 Spring容器从xml配置.java注解.spring注解中读取bean配置信息,形成bean定义注册表: 根据bean定义注册表实例化bean: 将bean ...
- (转)java之Spring(IOC)注解装配Bean详解
java之Spring(IOC)注解装配Bean详解 在这里我们要详细说明一下利用Annotation-注解来装配Bean. 因为如果你学会了注解,你就再也不愿意去手动配置xml文件了,下面就看看 ...
- 2017.2.13 开涛shiro教程-第十二章-与Spring集成(一)配置文件详解
原博客地址:http://jinnianshilongnian.iteye.com/blog/2018398 根据下载的pdf学习. 第十二章-与Spring集成(一)配置文件详解 1.pom.xml ...
- Spring源码之九finishRefresh详解
Spring源码之九finishRefresh详解 公众号搜索[程序员田同学],专职程序员兼业余写手,生活不止于写代码 Spring IoC 的核心内容要收尾了,本文将对最后一个方法 finishRe ...
- javascript 节点属性详解
javascript 节点属性详解 根据 DOM,html 文档中的每个成分都是一个节点 DOM 是这样规定的:整个文档是一个文档节点每个 html 标签是一个元素节点包含在于 html 元素中的文本 ...
- 【Spring】——声明式事务配置详解
项目中用到了spring的事务: @Transactional(rollbackFor = Exception.class, transactionManager = "zebraTrans ...
- Spring Boot的每个模块包详解
Spring Boot的每个模块包详解,具体如下: 1.spring-boot-starter 这是Spring Boot的核心启动器,包含了自动配置.日志和YAML. 2.spring-boot-s ...
- Spring Boot源码中模块详解
Spring Boot源码中模块详解 一.源码 spring boot2.1版本源码地址:https://github.com/spring-projects/spring-boot/tree/2.1 ...
- idea spring+springmvc+mybatis环境配置整合详解
idea spring+springmvc+mybatis环境配置整合详解 1.配置整合前所需准备的环境: 1.1:jdk1.8 1.2:idea2017.1.5 1.3:Maven 3.5.2 2. ...
随机推荐
- 0x01-Linux常用文件处理命令
0x01-Linux常用文件处理命令 摘要 文件可以说是占据了Linux系统半壁江山,那么,我们理所应当要认识文件,且还要懂得如何创建.查看文件(touch.cat命令).既然是使用Linux,当然是 ...
- Tunnel Warfare 线段树 区间合并|最大最小值
B - Tunnel WarfareHDU - 1540 这个有两种方法,一个是区间和并,这个我个人感觉异常恶心 第二种方法就是找最大最小值 kuangbin——线段树专题 H - Tunnel Wa ...
- 【Hadoop离线基础总结】MapReduce自定义InputFormat和OutputFormat案例
MapReduce自定义InputFormat和OutputFormat案例 自定义InputFormat 合并小文件 需求 无论hdfs还是mapreduce,存放小文件会占用元数据信息,白白浪费内 ...
- Unity ugui拖动控件(地图模式与物件模式)
拖动在游戏中使用频繁,例如将装备拖动到指定的快捷栏,或者大地图中拖动以查看局部信息等. Unity的EventSystems中可以直接继承几个接口来实现拖动功能,如下: namespace Unity ...
- Docker知识点整理
目录 1. Docker简介 1.1 Docker是什么 1.2 在隔离的容器中运行软件 1.3 分发容器 2. Docker镜像 2.1 Docker镜像简介 2.2 Docker镜像常见操作 2. ...
- 一看就懂的Ubuntu系统下samba服务器安装配置教程
文章目录 前言 环境搭建 安装 配置 Examples 1 创建共享(任何人都可以访问) 2 单用户权限(需要密码访问) 添加samba用户 配置参数 3 支持游客访问(单用户拥有管理员权限) 前言 ...
- Codeforces 909E(Coprocessor,双队列维护)
题意:给出n个待处理的事件(0 ~n-1),再给出了n个标(0表示只能在主处理器中处理这个事件,1表示只能在副处理器中处理这个事件),处理器每次能处理多个任务.每个事件有关联,如果一个任务要在副处理器 ...
- python监听、操作键盘鼠标库pynput详细教程
§ 0.0.0 前言 监听.操作鼠标.键盘是实现自动化的捷径,比如我实现自动化签到用到了模拟键盘操作. pynput是监听.操控鼠标和键盘的跨平台第三方python库. 你可以通过pip insnal ...
- Python单元测试框架:unittest(一)
Python单元测试框架unittest使用方法讲解 主要介绍了Python单元测试框架unittest使用方法讲解,本文讲解了unittest概述.命令行接口.测试案例自动搜索.创建测试代码.构建测 ...
- Python单元测试框架:unittest(二)
一.直接使用TestCase 注意所有测试方法都需要以test开头.代码如下: import unittest class Test1(unittest.TestCase): @classmethod ...