深入分析 Spring 基于注解的 AOP 实现原理
一、AOP 的基本使用
AOP 的使用分为三步走:
- 将业务逻辑组件和切面类都加入到容器中:告诉 Spring 哪个是切面类;
@Aspect
- 在切入类上的每一个通知方法上标注通知注解:告诉 Spring 何时何地运行(切入点表达式)
@Pointcut
、@Before
~~~ - 在配置类上开启基于注解的
AOP
模式;@EnableAspectJAutoProxy
使用 aop
相关的注解必须先导入依赖:
<dependency><groupId>org.springframework</groupId><artifactId>spring-aspects</artifactId><version>5.1.2.RELEASE</version>
</dependency>
下面以一个计算器的例子来介绍 AOP 的基本使用:
1、待增强类
这是一个简单的计算器类,为了能够演示异常,所以创建了一个有除法的方法。
public class MathCalculator {/*** 除法** @param i 被除数* @param j 除数* @return 返回运算结果*/public int div(int i, int j) {return i / j;}
}
2、增强类
我们想通过 AOP 实现记录除法运行的日志信息,所以新建一个 Log 类。
@Aspect
public class LogAspect {/*** 抽取出来的切入点表达式*/@Pointcut("execution(* top.wsuo.aop.MathCalculator.*(..))")public void pointCut() {}/*** 前置通知** @param joinPoint 连接点*/@Before("pointCut()")public void logStart(JoinPoint joinPoint) {System.out.println(joinPoint.getSignature().getName() + "运行...参数列表是:{" + Arrays.toString(joinPoint.getArgs()) + "}");}/*** 后置通知** @param joinPoint 连接点*/@After("pointCut()")public void logEnd(JoinPoint joinPoint) {System.out.println(joinPoint.getSignature().getName() + "结束...");}/*** 返回通知** @param joinPoint 连接点* @param result 执行结果*/@AfterReturning(value = "pointCut()", returning = "result")public void logReturn(JoinPoint joinPoint, Object result) {System.out.println(joinPoint.getSignature().getName() + "正常返回...运行结果:{" + result + "}");}/*** 异常通知** @param joinPoint 连接点* @param exception 异常信息*/@AfterThrowing(value = "pointCut()", throwing = "exception")public void logException(JoinPoint joinPoint, Exception exception) {System.out.println(joinPoint.getSignature().getName() + "出现异常...异常信息:{" + exception.getMessage() + "}");}
}
3、配置类
最后在配置类上开启注解版 AOP,同时注册组件到容器中。
@Configuration
// Spring 中有很多 EnableXXX 代表开启某一项功能: 取代了配置
@EnableAspectJAutoProxy
public class MainConfigOfAOP {@Beanpublic MathCalculator mathCalculator() {return new MathCalculator();}@Beanpublic LogAspect logAspect() {return new LogAspect();}
}
4、测试
测试及运行结果。
@Test
public void test12() {ApplicationContext context = new AnnotationConfigApplicationContext(MainConfigOfAOP.class);MathCalculator calculator = context.getBean(MathCalculator.class);System.out.println(calculator.div(4, 2));
}
二、注解 AOP 的实现原理
1、@EnableAspectJAutoProxy
整个 AOP 要想起作用,必须加上 @EnableAspectJAutoProxy
注解,这个注解的作用是什么呢?
点进去该注解:
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {
看到了要导入一个 AspectJAutoProxyRegistrar
类组件,它继承自一个接口 ImportBeanDefinitionRegistrar
,这个接口我们之前讲过,他是添加自定义组件的接口,在这里:https://blog.csdn.net/weixin_43941364/article/details/107243459。
这说明 @EnableAspectJAutoProxy
注解的作用就是给容器中添加组件, 追踪 AspectJAutoProxyRegistrar
类的方法,发现有这么一段代码:
这段代码的作用就是先看一下容器中有没有
public static final String AUTO_PROXY_CREATOR_BEAN_NAME ="org.springframework.aop.config.internalAutoProxyCreator";
internalAutoProxyCreator
这个类,同时我们看到在调用上述方法的时候,传入了一个类型:
该类型是 AspectJAwareAdvisorAutoProxyCreator
实体类,看一下该类的继承结构。
2、AspectJAwareAdvisorAutoProxyCreator
可以看到该类实现了一个接口,就是 BeanPostProcessor
接口,他是一个 后置处理器 。这个接口是 Bean 生命周期相关的接口。
所以我们要重点分析一下该类的执行顺序,接下来 打断点调试 之前举的计算器的例子。
3、容器的创建流程
从容器启动开始分析:
@Test
public void test12() {ApplicationContext context = new AnnotationConfigApplicationContext(MainConfigOfAOP.class);MathCalculator calculator = context.getBean(MathCalculator.class);System.out.println(calculator.div(4, 2));
}
首先传入配置类,创建 IOC 容器;然后注册配置类,调用 refresh
方法刷新容器;
3.1、注册后置处理器
使用 registerBeanPostProcessors(beanFactory)
注册 Bean 的后置处理器,来拦截 Bean 的创建
先获取 IOC 容器中已经定义了的需要创建对象的所有
BeanPostProcessor
给容器中加别的
BeanPostProcessor
优先注册实现了
PriorityOrdered
接口的BeanPostProcessor
再注册实现了
Ordered
接口的BeanPostProcessor
之后注册没实现
Ordered
接口的BeanPostProcessor
最后
registerBeanPostProcessors
执行,注册BeanPostProcessor
,实际上就是创建BeanPostProcessor
对象,保存在容器中。创建
org.springframework.aop.config.internalAutoProxyCreator
的BeanPostProcessor
,它的类型是AnnotationAwareAspectJAutoProxyCreator
。首先创建 Bean 的实例
instanceWrapper = createBeanInstance(beanName, mbd, args)
然后给属性赋值
populateBean(beanName, mbd, instanceWrapper)
最后初始化 Bean
exposedObject = initializeBean(beanName, exposedObject, mbd)
invokeAwareMethods(beanName, bean)
初始化 Aware 接口的方法回调;applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName)
执行后置处理器的postProcessBeforeInitialization
方法;invokeInitMethods(beanName, wrappedBean, mbd)
执行自定义初始化方法;applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName)
执行后置处理器的postProcessAfterInitialization
方法;
到此为止
AnnotationAwareAspectJAutoProxyCreator
类型的 BeanPostProcessor 创建成功;
创建完
BeanPostProcessor
对象之后,注册到beanFactory
中registerBeanPostProcessors(beanFactory, internalPostProcessors)注册方法的实现:for (BeanPostProcessor postProcessor : postProcessors) {beanFactory.addBeanPostProcessor(postProcessor); }
到此为止 AnnotationAwareAspectJAutoProxyCreator
就算是创建成功了,而它作为一个后置处理器,肯定有作用,下面分析一下他作为后置处理器做了什么事情。
注意 AnnotationAwareAspectJAutoProxyCreator
是 InstantiationAwareBeanPostProcessor
类型的后置处理器。
3.2、初始化剩下的单实例 Bean
finishBeanFactoryInitialization(beanFactory)
完成 BeanFactory 的初始化工作
- 遍历获取容器中所有的 Bean,依次创建对象:
getBean、doGetBean、getSingleton
; - 创建 Bean(业务逻辑组件和切面组件);
先从缓存中获取当前 Bean,如果能获取到,说明 Bean 是之前创建过的,直接使用,否则再创建;
先从缓存中检查有没有这个 Bean // Eagerly check singleton cache for manually registered singletons. Object sharedInstance = getSingleton(beanName);如果 他等于 null,才会继续执行下面的方法 sharedInstance = getSingleton(beanName, () -> {
只要创建好的 Bean 都会被缓存起来,这也是 Spring 保证单实例 Bean 的实现原理。
createBean
创建 Bean:AnnotationAwareAspectJAutoProxyCreator
会在任何 Bean 创建完成之前先尝试返回 Bean 的实例,其实就是拦截;BeanPostProcessor 是在对象创建Bean完成初始化前后调用的,而 InstantiationAwareBeanPostProcessor 是在创建Bean实例之前先尝试用后置处理器返回对象的。
Object bean = resolveBeforeInstantiation(beanName, mbdToUse)
,这句话的意思是希望后置处理器返回一个代理对象,如果能返回代理对象就使用,如果不能就继续;这个方法的实现就是拿到所有后置处理器,如果是 InstantiationAwareBeanPostProcessor,就执行 postProcessBeforeInstantiation 方法 bean = applyBeanPostProcessorsBeforeInstantiation(targetType, beanName);if (bean != null) {bean = applyBeanPostProcessorsAfterInitialization(bean, beanName); }
Object beanInstance = doCreateBean(beanName, mbdToUse, args)
,真正的去创建一个 Bean,和之前3.6
的流程是一样的。
所以
AnnotationAwareAspectJAutoProxyCreator
会在任何 Bean 创建完成之前先尝试返回 Bean 的实例,因为他实现了InstantiationAwareBeanPostProcessor
接口,这个接口有两个方法,一个是postProcessBeforeInstantiation
,另一个是postProcessAfterInstantiation
,这两个方法是在 Bean 创建完成前后执行的,而BeanPostProcessor
接口的两个方法是在创建完成并且初始化前后调用的。
在每一个 Bean 创建之前调用
postProcessBeforeInstantiation
方法,在这一步找出需要增强的 Bean;判断当前 Bean 是否在
advisedBeans
中(它保存了所有需要增强的 Bean )判断当前 Bean 是否是基础类型
isInfrastructureClass
或者是切面。判断是否该跳过
shouldSkip
:源码如下@Override protected boolean shouldSkip(Class<?> beanClass, String beanName) {// TODO: Consider optimization by caching the list of the aspect namesList<Advisor> candidateAdvisors = findCandidateAdvisors();for (Advisor advisor : candidateAdvisors) {if (advisor instanceof AspectJPointcutAdvisor &&((AspectJPointcutAdvisor) advisor).getAspectName().equals(beanName)) {return true;}}return super.shouldSkip(beanClass, beanName); }
首先获取所有候选的增强器,增强器就是切面里面的通知方法;
0 = {InstantiationModelAwarePointcutAdvisorImpl@2180} "InstantiationModelAwarePointcutAdvisor: expression [pointCut()]; advice method [public void top.wsuo.aop.LogAspect.logStart(org.aspectj.lang.JoinPoint)]; perClauseKind=SINGLETON" 1 = {InstantiationModelAwarePointcutAdvisorImpl@2181} "InstantiationModelAwarePointcutAdvisor: expression [pointCut()]; advice method [public void top.wsuo.aop.LogAspect.logEnd(org.aspectj.lang.JoinPoint)]; perClauseKind=SINGLETON" 2 = {InstantiationModelAwarePointcutAdvisorImpl@2182} "InstantiationModelAwarePointcutAdvisor: expression [pointCut()]; advice method [public void top.wsuo.aop.LogAspect.logReturn(org.aspectj.lang.JoinPoint,java.lang.Object)]; perClauseKind=SINGLETON" 3 = {InstantiationModelAwarePointcutAdvisorImpl@2183} "InstantiationModelAwarePointcutAdvisor: expression [pointCut()]; advice method [public void top.wsuo.aop.LogAspect.logException(org.aspectj.lang.JoinPoint,java.lang.Exception)]; perClauseKind=SINGLETON"
可以看到这就是我们的那几个通知方法。
只不过他把这些通知方法包装成为了一个List<Advisor> candidateAdvisors
集合,每一个封装的通知方法的增强器是InstantiationModelAwarePointcutAdvisor
。
这段代码的逻辑就是判断每一个增强器是否是AspectJPointcutAdvisor
类型的,如果是返回 true ,如果不是就返回 false ;
在 Bean 创建之后调用
postProcessAfterInitialization
方法,在这一步增强需要增强的 Bean:@Override public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {if (bean != null) {Object cacheKey = getCacheKey(bean.getClass(), beanName);if (!this.earlyProxyReferences.contains(cacheKey)) {return wrapIfNecessary(bean, beanName, cacheKey);}}return bean; }
在
wrapIfNecessary
方法中,获取当前 Bean 的所有增强器(通知方法),判断是否需要包装(增强)。// Create proxy if we have advice. Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
@Nullable protected Object[] getAdvicesAndAdvisorsForBean(Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) {List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);if (advisors.isEmpty()) {return DO_NOT_PROXY;}return advisors.toArray(); }
那么他是 怎么找的增强器 呢 ?我们继续查看方法调用。
protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {List<Advisor> candidateAdvisors = findCandidateAdvisors();List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);extendAdvisors(eligibleAdvisors);if (!eligibleAdvisors.isEmpty()) {eligibleAdvisors = sortAdvisors(eligibleAdvisors);}return eligibleAdvisors; }
这一步是找到候选的所有增强器,即哪些通知方法是需要切入当前 Bean 方法的。
然后下面的方法
findAdvisorsThatCanApply
是获取到能在当前 Bean 使用的增强器,它使用了canApply
方法判断。public static boolean canApply(Advisor advisor, Class<?> targetClass, boolean hasIntroductions) {if (advisor instanceof IntroductionAdvisor) {return ((IntroductionAdvisor) advisor).getClassFilter().matches(targetClass);}else if (advisor instanceof PointcutAdvisor) {PointcutAdvisor pca = (PointcutAdvisor) advisor;return canApply(pca.getPointcut(), targetClass, hasIntroductions);}else {// It doesn't have a pointcut so we assume it applies.return true;} }
最后是给这些增强器排序。
- 保存当前 Bean 到
advisedBeans
中; - 如果当前 Bean 需要增强,创建当前 Bean 的代理对象;
获取所有的增强器(通知方法)
保存到
proxyFactory
中;proxyFactory.getProxy(getProxyClassLoader());
创建代理对象,Spring 自动决定使用哪一种动态代理
@Override public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(Class<?> targetClass = config.getTargetClass();if (targetClass == null) {throw new AopConfigException("TargetSource cannot determine target class: " +"Either an interface or a target is required for proxy creation.");}if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {return new JdkDynamicAopProxy(config);}return new ObjenesisCglibAopProxy(config);}else {return new JdkDynamicAopProxy(config);} }
可以看到有两种自动代理,分别是
JdkDynamicAopProxy
和ObjenesisCglibAopProxy
。此时创建完成之后,代理对象为 top.wsuo.aop.MathCalculator,通过 Spring 增强的类型。
4.所以最后
wrapIfNecessary(bean, beanName, cacheKey)
方法就是返回了当前组件使用的cglib
增强了的代理对象。5.以后容器中获取到的就是这个组件的 代理对象 ,执行目标方法的时候,代理对象就会执行通知方法的流程。
3.3、执行目标方法
我们在测试方法上面打断点,看看除法运行的时候都有啥:
观察到此时的对象已经是 cglib 代理之后的对象了,这个对象中保存了详细信息,比如所有的增强器和目标对象。
下面进入到
org.springframework.aop.framework.CglibAopProxy.DynamicAdvisedInterceptor
的intercept
方法中。本来是想执行目标的,但是代理之后就要先被拦截一下。
然后根据
ProxyFactory
对象获取将要执行的目标拦截器链;List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass)
拦截器链是如何获取的?
主要是在
getInterceptorsAndDynamicInterceptionAdvice
方法中。- 首先创建一个集合保存所有的拦截器,默认有 5 个
List<Object> interceptorList = new ArrayList<>(advisors.length);
这 5 个包括一个默认的
org.springframework.aop.interceptor.ExposeInvocationInterceptor.ADVISOR
和 4 个增强器。 - 遍历所有的增强器,将其转为
Interceptor
。for (Advisor advisor : advisors)registry.getInterceptors(advisor)
- 将增强器转为
List<MethodInterceptor>
:如果本来就是
MethodInterceptor
,则直接加到集合中;如果不是,则使用
AdvisorAdapter
适配器转为MethodInterceptor
。怎么转的呢,其实这里就是强转然后包装了一下,源码如下。
@Override public MethodInterceptor getInterceptor(Advisor advisor) {AfterReturningAdvice advice = (AfterReturningAdvice) advisor.getAdvice();return new AfterReturningAdviceInterceptor(advice); }
可以看到这就是 最终通知 。
转化完成返回
MethodInterceptor
数组。
- 所以 拦截器链 就是每一个通知方法又被包装成为方法拦截器,利用
MethodInterceptor
的机制控制执行顺序。
- 首先创建一个集合保存所有的拦截器,默认有 5 个
如果没有拦截器链,直接执行目标方法
retVal = methodProxy.invoke(target, argsToUse);
如果有拦截器链,把需要执行的目标对象,目标方法,拦截器链等信息传入创建一个
CglibMethodInvocation
对象,并调用它的proceed
方法。// We need to create a method invocation... retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
拦截器链的触发过程,触发方法就是
proceed
,所以只需要分析一下这个方法即可。- 如果没有拦截器或者是最后一个拦截器就执行目标方法
if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {return invokeJoinpoint(); }
- 如果有拦截器就链式的获取每一个拦截器,拦截器执行
invoke
方法,每一个拦截器等待下一个拦截器执行完成返回以后再来执行。这里的返回值是还是拦截器,传入的是这个拦截器本身,每次调用都会减少一个长度,并且改变当前的拦截器,所以执行顺序是栈式的结构。 return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
首先执行到
interceptorOrInterceptionAdvice
的实现类ExposeInvocationInterceptor
,就是方法本身;
跟进去执行的是org.springframework.aop.interceptor.ExposeInvocationInterceptor
的invoke
方法,该方法的实现如下:@Override public Object invoke(MethodInvocation mi) throws Throwable {MethodInvocation oldInvocation = invocation.get();invocation.set(mi);try {return mi.proceed();}finally {invocation.set(oldInvocation);} }
这一块代码的核心业务是放在
finally
中的,所以肯定会执行,下面接着跟进去proceed
方法:这个时候再次来到 proceed 方法,此时的下标变为 0,执行到
AspectJAfterThrowingAdvice
,即异常通知;
跟进去执行org.springframework.aop.aspectj.AspectJAfterThrowingAdvice
的invoke
方法,该方法的实现如下:@Override public Object invoke(MethodInvocation mi) throws Throwable {try {return mi.proceed();}catch (Throwable ex) {if (shouldInvokeOnThrowing(ex)) {invokeAdviceMethod(getJoinPointMatch(), null, ex);}throw ex;} }
注意到这里有异常的捕捉,所以异常发生时是在这里处理的,没有异常则不会执行,继续跟进
proceed
方法。
这个时候再次来到 proceed 方法,此时的下标变为 1,执行到
AfterReturningAdviceInterceptor
,即返回(最终)通知;
跟进去执行org.springframework.aop.framework.adapter.AfterReturningAdviceInterceptor
的invoke
方法,该方法的实现如下:@Override public Object invoke(MethodInvocation mi) throws Throwable {Object retVal = mi.proceed();this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis());return retVal; }
就是先执行其他的,然后执行返回通知的内容,继续跟进
proceed
方法。
这个时候再次来到 proceed 方法,此时的下标变为 2,执行到
AspectJAfterAdvice
,即后置通知;
跟进去执行org.springframework.aop.aspectj.AspectJAfterAdvice
的invoke
方法,该方法的实现如下:@Override public Object invoke(MethodInvocation mi) throws Throwable {try {return mi.proceed();}finally {invokeAdviceMethod(getJoinPointMatch(), null, null);} }
就是先执行后面的前置通知,然后执行后置通知的内容,继续跟进
proceed
方法。
这个时候再次来到 proceed 方法,此时的下标变为 3,执行到
MethodBeforeAdviceInterceptor
,即前置通知;
跟进去执行org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor
的invoke
方法,该方法的实现如下:@Override public Object invoke(MethodInvocation mi) throws Throwable {this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());return mi.proceed(); }
这块代码是先执行自己的业务,再往下传递,我们继续跟进
proceed
方法:
这个时候再次来到 proceed 方法,此时的下标变为 4,还是执行MethodBeforeAdviceInterceptor
但是现在已经开始回溯了,因为方法都已经入栈了,此时执行 前置通知 中的方法,控制台输出如下:
然后执行 后置通知 :
执行 返回通知 :
最后所有的通知执行完毕,由于没有异常产生,所以没有执行异常通知:
- 如果没有拦截器或者是最后一个拦截器就执行目标方法
深入分析 Spring 基于注解的 AOP 实现原理相关推荐
- Spring —— 基于注解的Aop在同一类下产生嵌套时切面不生效问题产生原因及解决
一.背景介绍 由于程序中大量方法需要监控执行耗时,因此写了基于注解的Aop类来减少重复代码,主要作用是通过环绕通知在方法执行前后进行耗时计算,最后输出到日志/监控. 相关代码如下: // 注解 @Re ...
- Spring基于注解的AOP配置
pom.xml <?xml version="1.0" encoding="UTF-8"?> <project xmlns="htt ...
- 【Spring AOP】基于注解的 AOP 编程
Spring AOP 基于注解的 AOP 编程的开发 开发步骤 切入点复用 切换动态代理的创建方式(JDK.Cglib) AOP 开发中的一个坑(业务方法互相调用) AOP 知识总结 更多内容请查看笔 ...
- 基于注解的 AOP 配置
基于注解的 AOP 配置 bean.xml <?xml version="1.0" encoding="UTF-8"?> <beans xml ...
- 从源码分析 Spring 基于注解的事务
从源码分析 Spring 基于注解的事务 在spring引入基于注解的事务(@Transactional)之前,我们一般都是如下这样进行拦截事务的配置: <!-- 拦截器方式配置事务 --> ...
- Spring基于注解的方式二
Spring基于注解二 上一次介绍了很多的关于spring的基本的注解,这篇文章描述一下关于Spring注解的基本的原理,从简单的例子入手 @Configuration @Import({Color. ...
- Spring基于注解的方式一
Spring基于注解的方式一 Spring注解简介 之前的时候我们学习的Spring都是基于Spring配置文件的形式来编写,现在很多的情况下使用SpringBoot的时候是基于注解的形式,这里我们首 ...
- (spring-第4回【IoC基础篇】)spring基于注解的配置
(spring-第4回[IoC基础篇])spring基于注解的配置 基于XML的bean属性配置:bean的定义信息与bean的实现类是分离的. 基于注解的配置:bean的定义信息是通过在bean实现 ...
- Spring基于注解的自动装配
Spring基于注解的自动装配 基于XML的自动装配是在配置文件的bean里设置autowire属性,有byType,byName的方式.而基于注解的自动装配同样是这样只不过我们直接在成员变量上直接标 ...
- Spring基于注解TestContext 测试框架使用详解
原创整理不易,转载请注明出处:Spring基于注解TestContext 测试框架使用详解 代码下载地址:http://www.zuidaima.com/share/1775574182939648. ...
最新文章
- 魔法引用函数magic_quotes_gpc和magic_quotes_runtime的区别和用法
- oracle ebs po_header_all含税单价,Oracle EBS-追踪PO全过程
- java 对象 序列化 文件中_如何将一个java对象序列化到文件里
- 【PyTorch 】静态图与动态图机制
- Python中的无序集合(set)
- 清平乐·风鬟雨鬓 [清] 纳兰性德
- 计算机发展史和数字电路
- 吴裕雄 Bootstrap 前端框架开发——Bootstrap 辅助类:在元素获取焦点时显示(如:键盘操作的用户)...
- mysql 更新并查询结果_数据库_基础知识_MySQL_UpdateSelect(根据查询出来的结果批量更新)...
- Atitit 2017年的技术趋势与未来的大技术趋势 1. 2017年的技术趋势	2 1.1. Web not native	2 1.2. 更加移动优先 ,,more spa	3 1.3. Ar
- 嵌入式C语言(入门必看)
- VL2 异步复位的串联T触发器
- 羊皮卷之二:我要用全身心的爱来迎接今天
- 电阻接地再串联一个电容,电阻和电容并联
- Android 11.0 蓝牙去掉传输文件的功能
- C++ 小游戏 视频及资料集(四)
- pycharm定义空的二维数组_数组与面向对象
- [OpenCV实战]24 使用OpenCV进行曝光融合
- Win7下基于Anaconda安装TensorFlow
- shared_ptr与make_shared的用法
热门文章
- python xlsxwriter生成图片保存_Python xlsxwriter库 图表Demo
- 从腾讯云迁移到腾讯云,开心消消乐的云端迁移战事
- 微信开发者工具小技巧——快速打开微信程序API文档。
- python 递归 和 动态规划 DP算法两种方法求解 最长回文子串问题
- html——点击a标签打开新的标签页
- 输入输出工具技术(ITTO)要背吗?——软考高项笔记8
- Java编程降序排序代码,Java选择排序(升序跟降序)
- Autojs实现图片转字符串(简易ocr预备步骤)
- 五一期间完成了某市交警系统的一个系统升级迁移项目
- 医护人员计算机专业培训内容,电子病历-住院医生工作站的前期培训