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,一经查实,立即删除!

相关文章

为什么随机性是信息

用位思考 (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…

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

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

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…

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…

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…

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

普里姆从不同顶点出发绘制大流行时期社区的风险群图&#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 …

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

荷兰牛栏 荷兰售价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…

结对项目-小学生四则运算系统网页版项目报告

结对作业搭档&#xff1a;童宇欣 本篇博客结构一览&#xff1a; 1&#xff09;.前言(包括仓库地址等项目信息) 2&#xff09;.开始前PSP展示 3&#xff09;.结对编程对接口的设计 4&#xff09;.计算模块接口的设计与实现过程 5&#xff09;.计算模块接口部分的性能改进 6&…

袁中的第三次作业

第一题&#xff1a; 输出月份英文名 设计思路: 1:看题目&#xff1a;主函数与函数声明&#xff0c;知道它要你干什么2&#xff1a;理解与分析&#xff1a;在main中&#xff0c;给你一个月份数字n&#xff0c;要求你通过调用函数char *getmonth&#xff0c;来判断&#xff1a;若…

Python从菜鸟到高手(1):初识Python

1 Python简介 1.1 什么是Python Python是一种面向对象的解释型计算机程序设计语言&#xff0c;由荷兰人吉多范罗苏姆&#xff08;Guido van Rossum&#xff09;于1989年发明&#xff0c;第一个公开发行版发行于1991年。目前Python的最新发行版是Python3.6。 Python是纯粹的自由…

如何成为数据科学家_成为数据科学家需要了解什么

如何成为数据科学家Data science is one of the new, emerging fields that has the power to extract useful trends and insights from both structured and unstructured data. It is an interdisciplinary field that uses scientific research, algorithms, and graphs to…

阿里云对数据可靠性保障的一些思考

背景互联网时代的数据重要性不言而喻&#xff0c;任何数据的丢失都会给企事业单位、政府机关等造成无法计算和无法弥补的损失&#xff0c;尤其随着云计算和大数据时代的到来&#xff0c;数据中心的规模日益增大&#xff0c;环境更加复杂&#xff0c;云上客户群体越来越庞大&…

linux实验二

南京信息工程大学实验报告 实验名称 linux 常用命令练习 实验日期 2018-4-4 得分指导教师 系 计软院 专业 软嵌 年级 2015 级 班次 &#xff08;1&#xff09; 姓名王江远 学号20151398006 一、实验目的 1. 掌握 linux 系统中 shell 的基础知识 2. 掌握 linux 系统中文件系统的…

个人项目api接口_5个免费有趣的API,可用于学习个人项目等

个人项目api接口Public APIs are awesome!公共API很棒&#xff01; There are over 50 pieces covering APIs on just the Towards Data Science publication, so I won’t go into too lengthy of an introduction. APIs basically let you interact with some tool or servi…

咕泡-模板方法 template method 设计模式笔记

2019独角兽企业重金招聘Python工程师标准>>> 模板方法模式&#xff08;Template Method&#xff09; 定义一个操作中的算法的骨架&#xff0c;而将一些步骤延迟到子类中Template Method 使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤Template Me…

如何评价强gis与弱gis_什么是gis的简化解释

如何评价强gis与弱gisTL;DR — A Geographic Information System is an information system that specializes in the storage, retrieval and display of location data.TL; DR — 地理信息系统 是专门从事位置数据的存储&#xff0c;检索和显示的信息系统。 The standard de…

Scrum冲刺-Ⅳ

第四次冲刺任务 团队分工 成员&#xff1a;刘鹏芝&#xff0c;罗樟&#xff0c;王小莉&#xff0c;沈兴艳&#xff0c;徐棒&#xff0c;彭康明&#xff0c;胡广键 产品用户&#xff1a;王小莉 需求规约&#xff1a;彭康明&#xff0c;罗樟 UML&#xff1a;刘鹏芝&#xff0c;沈…