Spring Aop之Advisor解析

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

       在上文Spring Aop之Target Source详解中,我们讲解了Spring是如何通过封装Target Source来达到对最终获取的目标bean进行封装的目的。其中我们讲解到,Spring Aop对目标bean进行代理是通过AnnotationAwareAspectJAutoProxyCreator.postProcessAfterInitialization()进行的,Spring Aop的代理主要分为三个步骤:获取所有的Advisor,过滤可应用到当前bean的Adivsor和使用Advisor为当前bean生成代理对象。本文主要对这三步中的第一步获取所有的Advisor进行讲解。

1. 骨架方法

       首先我们看看postProcessAfterInitialization()方法的实现:

public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) throws BeansException {if (bean != null) {// 获取当前bean的key:如果beanName不为空,则以beanName为key,如果为FactoryBean类型,// 前面还会添加&符号,如果beanName为空,则以当前bean对应的class为keyObject cacheKey = getCacheKey(bean.getClass(), beanName);// 判断当前bean是否正在被代理,如果正在被代理则不进行封装if (!this.earlyProxyReferences.contains(cacheKey)) {// 对当前bean进行封装return wrapIfNecessary(bean, beanName, cacheKey);}}return bean;
}

       从上述代码可以看出,对目标bean的封装是主要是通过wrapIfNecessary()方法进行的,该方法就是Spring对目标bean进行代理的骨架方法。如下是该方法的实现:

protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {// 判断当前bean是否在TargetSource缓存中存在,如果存在,则直接返回当前bean。这里进行如此判断的// 原因是在上文中,我们讲解了如何通过自己声明的TargetSource进行目标bean的封装,在封装之后其实// 就已经对封装之后的bean进行了代理,并且添加到了targetSourcedBeans缓存中。因而这里判断得到// 当前缓存中已经存在当前bean,则说明该bean已经被代理过,这样就可以直接返回当前bean。if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {return bean;}// 这里advisedBeans缓存了已经进行了代理的bean,如果缓存中存在,则可以直接返回if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {return bean;}// 这里isInfrastructureClass()用于判断当前bean是否为Spring系统自带的bean,自带的bean是// 不用进行代理的;shouldSkip()则用于判断当前bean是否应该被略过if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {// 对当前bean进行缓存this.advisedBeans.put(cacheKey, Boolean.FALSE);return bean;}// 获取当前bean的Advices和AdvisorsObject[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);if (specificInterceptors != DO_NOT_PROXY) {// 对当前bean的代理状态进行缓存this.advisedBeans.put(cacheKey, Boolean.TRUE);// 根据获取到的Advices和Advisors为当前bean生成代理对象Object proxy = createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));// 缓存生成的代理bean的类型,并且返回生成的代理beanthis.proxyTypes.put(cacheKey, proxy.getClass());return proxy;}this.advisedBeans.put(cacheKey, Boolean.FALSE);return bean;
}

       在上述骨架方法中,Spring主要进行了三件事:

  • 判断当前bean是否已经生成过代理对象,或者是否是应该被略过的对象,是则直接返回,否则进行下一步;
  • 获取当前bean的Advisors和Advices,如果当前bean不需要代理,则返回DO_NOT_PROXY;
  • 通过生成的Advisors和Advices为目标bean生成代理对象。

       关于上述骨架方法,这里需要说明两个点:

  • shouldSkip()方法中对当前bean判断是否应该略过时,其主要做了两件事:a. 为当前bean生成需要代理的Advisors;b. 判断生成的Advisor是否为AspectJPointcutAdvisor类型。因而实际上判断略过的过程就是判断是否为AspectJPointcutAdvisor,判断这个类的原因在于Spring Aop的切面和切点的生成也可以通过在xml文件中使用<aop:config/>标签进行。这个标签最终解析得到的Adivsor类型就是``AspectJPointcutAdvisor类型的,因为其在解析aop:config/的时候就已经生成了Advisor,因而这里需要对这种类型的Advisor进行略过。这里aop:config/`也是一种自定义标签,关于其解析过程,读者可以参照本人前面的博文自行阅读器源码;
  • getAdvicesAndAdvisorsForBean()方法就其名称而言是获取Advisors和Advices,但实际上其返回值是一个Advisor的数组。Spring Aop在为目标bean获取需要进行代理的切面逻辑的时候最终得到的是Advisor,这里Advice表示的是每个切面逻辑中使用@Before@After@Around等需要织入的代理方法。因为每个代理方法都表示一个Advice,并且每个代理方法最终都会生成一个Advisor,因而Advice和Advisor就本质而言其实没有太大的区别。Advice表示需要织入的切面逻辑,而Advisor则表示将切面逻辑进行封装之后的织入者。

2. 切面的生成

       虽然在shouldSkip()方法中会为当前bean生成Advisor,但是在getAdvicesAndAdvisorsForBean()中也还是会获取一次,只不过在第一次生成的时候会将得到的Advisor都进行缓存,因而第二次获取时可以直接从缓存中获取。我们这里还是以getAdvicesAndAdvisorsForBean()方法为准来进行讲解。如下是该方法的源码:

protected Object[] getAdvicesAndAdvisorsForBean(Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) {// 为目标bean生成AdvisorList<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);if (advisors.isEmpty()) {return DO_NOT_PROXY;}return advisors.toArray();
}

       我们继续看findEligibleAdvisors()方法:

protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {// 将当前系统中所有的切面类的切面逻辑进行封装,从而得到目标AdvisorList<Advisor> candidateAdvisors = findCandidateAdvisors();// 对获取到的所有Advisor进行判断,看其切面定义是否可以应用到当前bean,从而得到最终需要应用的AdvisorList<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);// 提供的hook方法,用于对目标Advisor进行扩展extendAdvisors(eligibleAdvisors);if (!eligibleAdvisors.isEmpty()) {// 对需要代理的Advisor按照一定的规则进行排序eligibleAdvisors = sortAdvisors(eligibleAdvisors);}return eligibleAdvisors;
}

       在上述方法中,Spring Aop首先获取到了系统中所有的切面逻辑,并将其封装为了Advisor对象,然后通过遍历Advisor判断哪些Advisor是可以应用到当前bean的,最后将需要织入的Advisor返回。这里我们看看findCandidateAdvisors()的源码:

protected List<Advisor> findCandidateAdvisors() {// 找到系统中实现了Advisor接口的beanList<Advisor> advisors = super.findCandidateAdvisors();if (this.aspectJAdvisorsBuilder != null) {// 找到系统中使用@Aspect标注的bean,并且找到该bean中使用@Before,@After等标注的方法,// 将这些方法封装为一个个Advisoradvisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());}return advisors;
}

       可以看到,findCandidateAdvisors()主要是通过两种方式获取切面逻辑,一种是在系统中找到实现了Advisor接口的所有类,另一种是在找到系统中使用@Aspect标注的类,并将其切面逻辑封装为Advisor,这两种Advisor都有可能是我们需要进行织入的切面逻辑。这里super.findCandidateAdvisors()方法最终调用的是BeanFactoryAdvisorRetrievalHelper.findAdvisorBeans()方法,我们首先看看该方法的实现:

public List<Advisor> findAdvisorBeans() {String[] advisorNames = null;synchronized (this) {advisorNames = this.cachedAdvisorBeanNames;if (advisorNames == null) {// 获取当前BeanFactory中所有实现了Advisor接口的bean的名称advisorNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, Advisor.class, true, false);this.cachedAdvisorBeanNames = advisorNames;}}if (advisorNames.length == 0) {return new LinkedList<>();}// 对获取到的实现Advisor接口的bean的名称进行遍历List<Advisor> advisors = new LinkedList<>();for (String name : advisorNames) {// isEligibleBean()是提供的一个hook方法,用于子类对Advisor进行过滤,这里默认返回值都是trueif (isEligibleBean(name)) {// 如果当前bean还在创建过程中,则略过,其创建完成之后会为其判断是否需要织入切面逻辑if (this.beanFactory.isCurrentlyInCreation(name)) {if (logger.isDebugEnabled()) {logger.debug("Skipping currently created advisor '" + name + "'");}} else {try {// 将当前bean添加到结果中advisors.add(this.beanFactory.getBean(name, Advisor.class));} catch (BeanCreationException ex) {// 对获取过程中产生的异常进行封装Throwable rootCause = ex.getMostSpecificCause();if (rootCause instanceof BeanCurrentlyInCreationException) {BeanCreationException bce = (BeanCreationException) rootCause;String bceBeanName = bce.getBeanName();if (bceBeanName != null && this.beanFactory.isCurrentlyInCreation(bceBeanName)) {if (logger.isDebugEnabled()) {logger.debug("Skipping advisor '" + name + "' with dependency on currently created bean: " + ex.getMessage());}continue;}}throw ex;}}}}return advisors;
}

       这里findAdvisorBeans()方法逻辑其实非常简单,其主要是在BeanFactory中找打实现了Advisor接口的类,然后通过hook方法判断子类是否需要对Advisor进行过滤,最后将过滤之后的Advisor返回。

       接下来我们看看BeanFactoryAspectJAdvisorsBuilder.buildAspectJAdvisors()的实现:

public List<Advisor> buildAspectJAdvisors() {List<String> aspectNames = this.aspectBeanNames;if (aspectNames == null) {synchronized (this) {aspectNames = this.aspectBeanNames;if (aspectNames == null) {List<Advisor> advisors = new LinkedList<>();aspectNames = new LinkedList<>();// 获取当前BeanFactory中所有的beanString[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, Object.class, true, false);// 对获取到的所有bean进行循环遍历for (String beanName : beanNames) {// 判断当前bean是否为子类定制的需要过滤的beanif (!isEligibleBean(beanName)) {continue;}// 获取当前遍历的bean的Class类型Class<?> beanType = this.beanFactory.getType(beanName);if (beanType == null) {continue;}// 判断当前bean是否使用了@Aspect注解进行标注if (this.advisorFactory.isAspect(beanType)) {aspectNames.add(beanName);// 对于使用了@Aspect注解标注的bean,将其封装为一个AspectMetadata类型。// 这里在封装的过程中会解析@Aspect注解上的参数指定的切面类型,如perthis// 和pertarget等。这些被解析的注解都会被封装到其perClausePointcut属性中AspectMetadata amd = new AspectMetadata(beanType, beanName);// 判断@Aspect注解中标注的是否为singleton类型,默认的切面类都是singleton// 类型if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) {// 将BeanFactory和当前bean封装为MetadataAwareAspect-// InstanceFactory对象,这里会再次将@Aspect注解中的参数都封装// 为一个AspectMetadata,并且保存在该factory中MetadataAwareAspectInstanceFactory factory =new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);// 通过封装的bean获取其Advice,如@Before,@After等等,并且将这些// Advice都解析并且封装为一个个的AdvisorList<Advisor> classAdvisors this.advisorFactory.getAdvisors(factory);// 如果切面类是singleton类型,则将解析得到的Advisor进行缓存,// 否则将当前的factory进行缓存,以便再次获取时可以通过factory直接获取if (this.beanFactory.isSingleton(beanName)) {this.advisorsCache.put(beanName, classAdvisors);} else {this.aspectFactoryCache.put(beanName, factory);}advisors.addAll(classAdvisors);} else {// 如果@Aspect注解标注的是perthis和pertarget类型,说明当前切面// 不可能是单例的,因而这里判断其如果是单例的则抛出异常if (this.beanFactory.isSingleton(beanName)) {throw new IllegalArgumentException("Bean with name '" + beanName + "' is a singleton, but aspect "+ "instantiation model is not singleton");}// 将当前BeanFactory和切面bean封装为一个多例类型的FactoryMetadataAwareAspectInstanceFactory factory =new PrototypeAspectInstanceFactory(this.beanFactory, beanName);// 对当前bean和factory进行缓存this.aspectFactoryCache.put(beanName, factory);advisors.addAll(this.advisorFactory.getAdvisors(factory));}}}this.aspectBeanNames = aspectNames;return advisors;}}}if (aspectNames.isEmpty()) {return Collections.emptyList();}// 通过所有的aspectNames在缓存中获取切面对应的Advisor,这里如果是单例的,则直接从advisorsCache// 获取,如果是多例类型的,则通过MetadataAwareAspectInstanceFactory立即生成一个List<Advisor> advisors = new LinkedList<>();for (String aspectName : aspectNames) {List<Advisor> cachedAdvisors = this.advisorsCache.get(aspectName);// 如果是单例的Advisor bean,则直接添加到返回值列表中if (cachedAdvisors != null) {advisors.addAll(cachedAdvisors);} else {// 如果是多例的Advisor bean,则通过MetadataAwareAspectInstanceFactory生成MetadataAwareAspectInstanceFactory factory = this.aspectFactoryCache.get(aspectName);advisors.addAll(this.advisorFactory.getAdvisors(factory));}}return advisors;
}

       对于通过@Aspect注解获取切面逻辑的方法,这里的逻辑也比较简单,Spring首先会过滤得到BeanFactory中所有标注有@Aspect的类,然后对该注解参数进行解析,判断其环绕的目标bean是单例的还是多例的。如果是单例的,则直接缓存到advisorsCache中;如果是多例的,则将生成Advisor的factory进行缓存,以便每次获取时都通过factory获取一个新的Advisor。上述方法中主要是对@Aspect注解进行了解析,我们前面讲过,Spring Aop的Advisor对应的是Advice,而每个Advice都是对应的一个@Before或者@After等标注方法的切面逻辑,这里对这些切面逻辑的解析过程就在上述的advisorFactory.getAdvisors(factory)方法调用中。这里我们看看该方法的实现:

public List<Advisor> getAdvisors(MetadataAwareAspectInstanceFactory aspectInstanceFactory) {// 获取当前切面类的Class类型Class<?> aspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass();// 获取当前切面bean的名称String aspectName = aspectInstanceFactory.getAspectMetadata().getAspectName();// 对当前切面bean进行校验,主要是判断其切点是否为perflow或者是percflowbelow,Spring暂时不支持// 这两种类型的切点validate(aspectClass);// 将当前aspectInstanceFactory进行封装,这里LazySingletonAspectInstanceFactoryDecorator// 使用装饰器模式,主要是对获取到的切面实例进行了缓存,保证每次获取到的都是同一个切面实例MetadataAwareAspectInstanceFactory lazySingletonAspectInstanceFactory =new LazySingletonAspectInstanceFactoryDecorator(aspectInstanceFactory);List<Advisor> advisors = new LinkedList<>();// 这里getAdvisorMethods()会获取所有的没有使用@Pointcut注解标注的方法,然后对其进行遍历for (Method method : getAdvisorMethods(aspectClass)) {// 判断当前方法是否标注有@Before,@After或@Around等注解,如果标注了,则将其封装为一个AdvisorAdvisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, advisors.size(), aspectName);if (advisor != null) {advisors.add(advisor);}}// 这里的isLazilyInstantiated()方法判断的是当前bean是否应该被延迟初始化,其主要是判断当前// 切面类是否为perthis,pertarget或pertypewithiin等声明的切面。因为这些类型所环绕的目标bean// 都是多例的,因而需要在运行时动态判断目标bean是否需要环绕当前的切面逻辑if (!advisors.isEmpty() && lazySingletonAspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) {// 如果Advisor不为空,并且是需要延迟初始化的bean,则在第0位位置添加一个同步增强器,// 该同步增强器实际上就是一个BeforeAspect的AdvisorAdvisor instantiationAdvisor = new SyntheticInstantiationAdvisor(lazySingletonAspectInstanceFactory);advisors.add(0, instantiationAdvisor);}// 判断属性上是否包含有@DeclareParents注解标注的需要新添加的属性,如果有,则将其封装为一个Advisorfor (Field field : aspectClass.getDeclaredFields()) {Advisor advisor = getDeclareParentsAdvisor(field);if (advisor != null) {advisors.add(advisor);}}return advisors;
}

       在上述getAdvisors()方法中,Spring会遍历当前切面类所有的方法,包括父类和父接口的方法,找到其中没有使用@Pointcut注解标注的方法,然后对找到的方法进行遍历,将其封装为一个Advisor。这里我们继续看封装为Advisor的方法:

public Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrderInAspect, String aspectName) {// 校验当前切面类是否使用了perflow或者percflowbelow标识的切点,Spring暂不支持这两种切点validate(aspectInstanceFactory.getAspectMetadata().getAspectClass());// 获取当前方法中@Before,@After或者@Around等标注的注解,并且获取该注解的值,将其// 封装为一个AspectJExpressionPointcut对象AspectJExpressionPointcut expressionPointcut = getPointcut(candidateAdviceMethod, aspectInstanceFactory.getAspectMetadata().getAspectClass());if (expressionPointcut == null) {return null;}// 将获取到的切点,切点方法等信息封装为一个Advisor对象,也就是说当前Advisor包含有所有// 当前切面进行环绕所需要的信息return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod, this, aspectInstanceFactory, declarationOrderInAspect, aspectName);
}

       到这里Spring才将@Before,@After或@Around标注的方法封装为了一个Advisor对象。需要说明的是,这里封装成的Advisor对象只是一个半成品。所谓的半成品指的是此时其并没有对切点表达式进行解析,其还只是使用一个字符串保存在AspectJExpressionPointcut对象中,只有在真正使用当前Advice逻辑进行目标bean的环绕的时候才会对其进行解析。

3. 小结

       本文主要讲解了Spring是如何获取所有的Advisor的,即首先获取BeanFactory中所有实现了Advisor接口的bean,然后获取BeanFactory中所有标注了@Aspect注解的bean,解析该bean中的所有的切面逻辑,并且封装为一个个Advisor,这两种方式得到的Advisor都有可能是最终会应用到目标bean上的切面逻辑。需要注意的是,这里获取到的Advisor并没有对切点表达式进行解析,实际的解析过程是在判断当前bean是否可以应用到目标bean时进行的。这也是一个小小的优化,因为解析切点表达式的过程是一个比较复杂的过程。

转载于:https://my.oschina.net/zhangxufeng/blog/1929863

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

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

相关文章

react事件处理函数中绑定this的bind()函数

问题引入 import React, { Component } from react; import {Text,View } from react-native;export default class App extends Component<Props> {constructor(props){super(props)this.state{times:0}this.timePlusthis.timePlus.bind(this);}timePlus(){let timethis…

301. 删除无效的括号

301. 删除无效的括号 给你一个由若干括号和字母组成的字符串 s &#xff0c;删除最小数量的无效括号&#xff0c;使得输入的字符串有效。 返回所有可能的结果。答案可以按 任意顺序 返回。 示例 1&#xff1a; 输入&#xff1a;s “()())()” 输出&#xff1a;["(())…

为什么随机性是信息

用位思考 (Thinking in terms of Bits) Imagine you want to send outcomes of 3 coin flips to your friends house. Your friend knows that you want to send him those messages but all he can do is get the answer of Yes/No questions arranged by him. Lets assume th…

Chrome无法播放m3u8格式的直播视频流的问题解决

出国&#xff0c;然后安装这个插件即可&#xff1a;Native HLS Playback https://chrome.google.com/webstore/detail/native-hls-playback/emnphkkblegpebimobpbekeedfgemhof?hlzh-CN转载于:https://www.cnblogs.com/EasonJim/p/8737001.html

大数据相关从业_如何在组织中以数据从业者的身份闪耀

大数据相关从业Build bridges, keep the maths under your hat and focus on serving.架起桥梁&#xff0c;将数学放在脑海中&#xff0c;并专注于服务。 通过协作而不是通过孤立的孤岛来交付出色的数据工作。 (Deliver great data work through collaboration not through co…

暑假周总结六

本周开始了做网站的商品展示和商品查询的功能&#xff0c;基本功能已完成了。平均每天花4到5个小时进行学习和编码 这周学习了lucene分词器&#xff0c;但是虽然学了一些这些方面的东西&#xff0c;但是查询的时候效果还是不行&#xff0c;还是继续学习 一些更好处理关键字的方…

Django进阶之中间件

中间件简介 在http请求 到达视图函数之前 和视图函数return之后&#xff0c;django会根据自己的规则在合适的时机执行中间件中相应的方法。 中间件的执行流程 1、执行完所有的request方法 到达视图函数。 2、执行中间件的其他方法 2、经过所有response方法 返回客户端。 注意…

汉诺塔递归算法进阶_进阶python 1递归

汉诺塔递归算法进阶When something is specified in terms of itself, it is called recursion. The recursion gives us a new idea of how to solve a kind of problem and this gives us insights into the nature of computation. Basically, many of computational artifa…

500. 键盘行

500. 键盘行 给你一个字符串数组 words &#xff0c;只返回可以使用在 美式键盘 同一行的字母打印出来的单词。键盘如下图所示。 美式键盘 中&#xff1a; 第一行由字符 “qwertyuiop” 组成。 第二行由字符 “asdfghjkl” 组成。 第三行由字符 “zxcvbnm” 组成。 示例 1&a…

windows 停止nginx

1、查找进程 tasklist | findstr nginx2、杀死进程 taskkill /pid 6508 /F3、一次杀死多个进程taskkill /pid 6508 /pid 16048 /f转载于:https://blog.51cto.com/dressame/2161759

SpringBoot返回json和xml

有些情况接口需要返回的是xml数据&#xff0c;在springboot中并不需要每次都转换一下数据格式&#xff0c;只需做一些微调整即可。 新建一个springboot项目&#xff0c;加入依赖jackson-dataformat-xml&#xff0c;pom文件代码如下&#xff1a; <?xml version"1.0&quo…

575. 分糖果

575. 分糖果 给定一个偶数长度的数组&#xff0c;其中不同的数字代表着不同种类的糖果&#xff0c;每一个数字代表一个糖果。你需要把这些糖果平均分给一个弟弟和一个妹妹。返回妹妹可以获得的最大糖果的种类数。 示例 1:输入: candies [1,1,2,2,3,3] 输出: 3 解析: 一共有三…

如何开启并配置CITRIX Xenserver的SNMP服务

以下博文转载至虚拟人生Citrix Xenserver使用标准的NET-SNMP协议&#xff0c;关于NET-SNMP请参考www.net-snmp.org. Xenserver并没有自己的MIB库.Xenserver默认是禁止SNMP服务且并没有开启SNMP服务使用的端口,通过以下方式开启并配置SNMP服务&#xff1a;1.编辑Xenserver的/etc…

orange 数据分析_使用Orange GUI的放置结果数据分析

orange 数据分析Objective : Analysing of several factors influencing the recruitment of students and extracting information through plots.目的&#xff1a;分析影响学生招生和通过情节提取信息的几个因素。 Description : The following analysis presents the diffe…

C++(1)引用

引用 引用 为对象起另外一个名字&#xff0c;通过将声明符写成 &d&#xff0c;其中d是声明的变量名。一旦初始化完成&#xff0c;引用将和起初始值绑定在一起&#xff0c;无法再绑定到另一个对象&#xff0c;因此引用必须初始化。 引用就是别名&#xff0c;初始化以后&am…

普里姆从不同顶点出发_来自三个不同聚类分析的三个不同教训数据科学的顶点...

普里姆从不同顶点出发绘制大流行时期社区的风险群图&#xff1a;以布宜诺斯艾利斯为例 (Map Risk Clusters of Neighbourhoods in the time of Pandemic: a case of Buenos Aires) 介绍 (Introduction) Every year is unique and particular. But, 2020 brought the world the …

一步一步图文介绍SpriteKit使用TexturePacker导出的纹理集Altas

1、为什么要使用纹理集&#xff1f; 游戏是一种很耗费资源的应用&#xff0c;特别是在移动设备中的游戏&#xff0c;性能优化是非常重要的 纹理集是将多张小图合成一张大图&#xff0c;使用纹理集有以下优点&#xff1a; 1、减少内存占用&#xff0c;减少磁盘占用&#xff1b; …

BZOJ.1007.[HNOI2008]水平可见直线(凸壳 单调栈)

题目链接 可以看出我们是要维护一个下凸壳。 先对斜率从小到大排序。斜率最大、最小的直线是一定会保留的&#xff0c;因为这是凸壳最边上的两段。 维护一个单调栈&#xff0c;栈中为当前可见直线(按照斜率排序)。 当加入一条直线l时&#xff0c;可以发现 如果l与栈顶直线l的交…

荷兰牛栏 荷兰售价_荷兰的公路货运是如何发展的

荷兰牛栏 荷兰售价I spent hours daily driving on one of the busiest motorways in the Netherlands when commuting was still a norm. When I first came across with the goods vehicle data on CBS website, it immediately attracted my attention: it could answer tho…

Vim 行号的显示与隐藏

2019独角兽企业重金招聘Python工程师标准>>> Vim 行号的显示与隐藏 一、当前文档的显示与隐藏 1 打开一个文档 [rootpcname ~]# vim demo.txt This is the main Apache HTTP server configuration file. It contains the configuration directives that give the s…