Spring AOP源码解析-拦截器链的执行过程

2023-11-18

一.简介

在前面的两篇文章中,分别介绍了 Spring AOP 是如何为目标 bean 筛选合适的通知器,以及如何创建代理对象的过程。现在得到了 bean 的代理对象,且通知也以合适的方式插在了目标方法的前后。接下来要做的事情,就是执行通知逻辑了。通知可能在目标方法前执行,也可能在目标方法后执行。具体的执行时机,取决于用户的配置。当目标方法被多个通知匹配到时,Spring 通过引入拦截器链来保证每个通知的正常执行。在本文中,我们将会通过源码了解到 Spring 是如何支持 expose-proxy 属性的,以及通知与拦截器之间的关系,拦截器链的执行过程等。

二.背景知识

有时候目标对象内部的自我调用将无法实施切面中的增强:

public interface UserService{
	public void a();
	public void a();
}

public class UserServiceImpl implements UserService{
	@Transactional(propagation = Propagation.REQUIRED)
	public void a(){
		this.b();
	}
	@Transactional(propagation = Propagation.REQUIRED_NEW)
	public void b(){
		System.out.println("b has been called");
	}
}

a的事务会生效,b中不会有事务,因为a中调用b属于内部调用,没有通过代理,所以不会有事务产生(原因:SpringAOP对于最外层的函数只拦截public方法,不拦截protected和private方法,另外不会对最外层的public方法内部调用的其他方法也进行拦截,即只停留于代理对象所调用的方法。)。

为了解决这个问题,我们可以这样做:<aop:aspectj-autoproxy expose-proxy=“true”> ,设置expose-proxy属性为true,将代理暴露出来,使用AopContext.currentProxy()获取当前代理,将this.b()改为((UserService)AopContext.currentProxy()).b()。

三.源码解析

本章所分析的源码来自 JdkDynamicAopProxy。

3.1 JDK 动态代理逻辑分析

对于 JDK 动态代理,代理逻辑封装在 InvocationHandler 接口实现类的 invoke 方法中。JdkDynamicAopProxy 实现了 InvocationHandler 接口,下面我们就来分析一下 JdkDynamicAopProxy 的 invoke 方法。如下:

	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		MethodInvocation invocation;
		Object oldProxy = null;
		boolean setProxyContext = false;
 
		//获取原对象信息
		TargetSource targetSource = this.advised.targetSource;
		Class<?> targetClass = null;
		Object target = null;
 
		try {
			//如果接口中定义了equals或hashCode方法,则进行专门处理
			if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {
				// The target does not implement the equals(Object) method itself.
				return equals(args[0]);
			}
			else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
				// The target does not implement the hashCode() method itself.
				return hashCode();
			}
			else if (method.getDeclaringClass() == DecoratingProxy.class) {
				// There is only getDecoratedClass() declared -> dispatch to proxy config.
				return AopProxyUtils.ultimateTargetClass(this.advised);
			}
			//opaque属性,表示是否禁止将代理对象转换为Advised对象,默认是false
			//如果调用的方法来自Advised接口
			else if (!this.advised.opaque && method.getDeclaringClass().isInterface() &&
					method.getDeclaringClass().isAssignableFrom(Advised.class)) {
				//通过反射Method.invoke,调用advised(传入的ProxyFactory实例,该类实现了Advised接口)对应的方法
				return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);
			}
 
			Object retVal;
			//为了解决目标对象内部的自我调用无法实施切面中的增强,需要暴露代理对象
			if (this.advised.exposeProxy) {
				oldProxy = AopContext.setCurrentProxy(proxy);
				setProxyContext = true;
			}
 
			// 获取目标对象信息
			target = targetSource.getTarget();
			if (target != null) {
				targetClass = target.getClass();
			}
 
			// 获取当前方法的拦截器链
			List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
 
			// 如果没有任何拦截器,则调用直接对原目标对象调用方法
			if (chain.isEmpty()) {
				Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
				retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
			}
			else {
				// 将拦截器链封装到ReflectiveMethodInvocation,方便使用其proceed进行链式调用拦截器
				invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
				// 执行拦截器链
				retVal = invocation.proceed();
			}
 
			// Massage return value if necessary.
			Class<?> returnType = method.getReturnType();
			if (retVal != null && retVal == target &&
					returnType != Object.class && returnType.isInstance(proxy) &&
					!RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
				// 特殊情况:为了防止方法返回"return this",返回原目标对象,会将返回值替换为代理对象
				retVal = proxy;
			}
			//如果返回类型是基本类型兵器,但是返回结果为null,抛出异常
			else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) {
				throw new AopInvocationException(
						"Null return value from advice does not match primitive return type for: " + method);
			}
			return retVal;
		}
		finally {
			if (target != null && !targetSource.isStatic()) {
				// Must have come from TargetSource.
				targetSource.releaseTarget(target);
			}
			if (setProxyContext) {
				// Restore old proxy.
				AopContext.setCurrentProxy(oldProxy);
			}
		}
	}

下面来总结一下invoke方法的执行流程:

  1. 检测expose-proxy是否为true,若为true,则暴露代理对象
  2. 获取适合当前方法的拦截器
  3. 如果拦截器链为空,则直接通过反射执行目标方法
  4. 若拦截器链不为空,则创建方法调用ReflectiveMethodInvocation对象
  5. 调用ReflectiveMethodInvocation对象的proceed方法启动拦截器链
  6. 处理返回值(如果返回“this”,即原目标对象,则会替换为返回代理对象;如果返回结果为null,但返回类型为基本数据类型(int、char等)则抛出异常),并返回返回值。

3.2 获取所有的拦截器链

3.2.1 前置拦截器链

拦截器链是指对于目标方法的调用进行拦截的一种工具,下面以前置拦截器为例,如下:

public class MethodBeforeAdviceInterceptor implements MethodInterceptor, Serializable {
    
    /** 前置通知 */
    private MethodBeforeAdvice advice;

    public MethodBeforeAdviceInterceptor(MethodBeforeAdvice advice) {
        Assert.notNull(advice, "Advice must not be null");
        this.advice = advice;
    }

    @Override
    public Object invoke(MethodInvocation mi) throws Throwable {
        // 执行前置通知逻辑
        this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());
        // 通过 MethodInvocation 调用下一个拦截器,若所有拦截器均执行完,则调用目标方法
        return mi.proceed();
    }
}

3.2.3 获取拦截器链

如源码所示,前置通知在目标方法执行前被执行。下来看看如何获取拦截器:

	public List<Object> getInterceptorsAndDynamicInterceptionAdvice(Method method, Class<?> targetClass) {
		//缓存的cacheKey,先尝试从缓存获取,不存在再从ProxyFactory中解析
		MethodCacheKey cacheKey = new MethodCacheKey(method);
		List<Object> cached = this.methodCache.get(cacheKey);
		if (cached == null) {
			//通过实例方法和ProxyFactory中保存的信息,解析出匹配的增强
			cached = this.advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice(
					this, method, targetClass);
			this.methodCache.put(cacheKey, cached);
		}
		return cached;
	}
	public List<Object> getInterceptorsAndDynamicInterceptionAdvice(
			Advised config, Method method, Class<?> targetClass) {
 
		List<Object> interceptorList = new ArrayList<Object>(config.getAdvisors().length);
		Class<?> actualClass = (targetClass != null ? targetClass : method.getDeclaringClass());
		//方法是否匹配引介增强
		boolean hasIntroductions = hasMatchingIntroductions(config, actualClass);
		AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance();
		
		//遍历增强器
		for (Advisor advisor : config.getAdvisors()) {
			//PointcutAdvisor类型的增强器
			//通过@Aspect加入的增强器类型为InstantiationModelAwarePointcutAdvisorImpl,实现了PointcutAdvisor
			if (advisor instanceof PointcutAdvisor) {
				// Add it conditionally.
				PointcutAdvisor pointcutAdvisor = (PointcutAdvisor) advisor;
				if (config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass)) {
					MethodInterceptor[] interceptors = registry.getInterceptors(advisor);
					MethodMatcher mm = pointcutAdvisor.getPointcut().getMethodMatcher();
					if (MethodMatchers.matches(mm, method, actualClass, hasIntroductions)) {
						if (mm.isRuntime()) {
							// Creating a new object instance in the getInterceptors() method
							// isn't a problem as we normally cache created chains.
							for (MethodInterceptor interceptor : interceptors) {
								interceptorList.add(new InterceptorAndDynamicMethodMatcher(interceptor, mm));
							}
						}
						else {
							interceptorList.addAll(Arrays.asList(interceptors));
						}
					}
				}
			}
			//对于引介增强的处理
			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;
	}

public MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdviceTypeException {
		List<MethodInterceptor> interceptors = new ArrayList<MethodInterceptor>(3);
		//获取Advice
		Advice advice = advisor.getAdvice();
		//如果Advice实例同时已经实现MethodInterceptor接口,则直接使用
		if (advice instanceof MethodInterceptor) {
			interceptors.add((MethodInterceptor) advice);
		}
		//需要使用适配器来转换Advice接口
		for (AdvisorAdapter adapter : this.adapters) {
			if (adapter.supportsAdvice(advice)) {
				interceptors.add(adapter.getInterceptor(advisor));
			}
		}
		if (interceptors.isEmpty()) {
			throw new UnknownAdviceTypeException(advisor.getAdvice());
		}
		return interceptors.toArray(new MethodInterceptor[interceptors.size()]);
	}

以上就是获取拦截器的过程,这里简单总结一下以上源码的执行过程,如下:

  1. 从缓存中获取当前方法的拦截器链
  2. 若缓存未命中,则调用getInterceptorsAndDynamicInterceptionAdvice获取拦截器链
  3. 遍历通知器列表
  4. 对于PointcutAdvisor类型的通知器,这里要调用通知器所持有的的切点(Pointcut)对类和方法进行匹配,匹配成功应向当前方法织入通知逻辑
  5. 调用getInterceptors方法对MethodInteceptor类型的通知进行转换(如果Advice增强已经实现了MethodInterceptor,则不需要转换,可以直接使用。例如:@After注解标注的增强方法会被表示为AspectJAfterAdvice,该类同时实现了Advice和MethodInterceptor接口,也就是已经在类中规定好了拦截的逻辑。Advice实现了没有同时实现MethodInterceptor,所以需要使用内置的适配器将Advice增强转换为MethodInterceptor拦截器)。
  6. 返回拦截器数组,并在随后存入缓存中

3.2.3 内置的Advice适配器

在前面的getInterceptors方法中将Advice转换为MethodInterceptor的工作是交给this.adapters来完成的,该变量定义如下:

List<MethodInterceptor> interceptors = new ArrayList<MethodInterceptor>(3);

并且在DefaultAdvisorAdapterRegistry的构造函数中,对该变量进行了初始化填充:

	public DefaultAdvisorAdapterRegistry() {
		registerAdvisorAdapter(new MethodBeforeAdviceAdapter());
		registerAdvisorAdapter(new AfterReturningAdviceAdapter());
		registerAdvisorAdapter(new ThrowsAdviceAdapter());
	}

可以看到,Spring实现会加入3个默认的是Advice适配器:MethodBeforeAdviceAdapter、AfterReturningAdviceAdapter、ThrowsAdviceAdapter。

这三个适配器通过support方法,验证是否是否支持传入的Advice对象,如果支持,会将Advice实例封装为对应的MethodInterceptor实例:

class MethodBeforeAdviceAdapter implements AdvisorAdapter, Serializable {
 
	@Override
	public boolean supportsAdvice(Advice advice) {
		return (advice instanceof MethodBeforeAdvice);
	}
 
	@Override
	public MethodInterceptor getInterceptor(Advisor advisor) {
		MethodBeforeAdvice advice = (MethodBeforeAdvice) advisor.getAdvice();
		return new MethodBeforeAdviceInterceptor(advice);
	}
 
}

3.3 启动拦截器链

Spring会在获取到方法匹配的拦截器后,将代理对象、目标对象、调用方法、参数、拦截器等信息封装ReflectiveMethodInvocation中:

	protected ReflectiveMethodInvocation(
			Object proxy, Object target, Method method, Object[] arguments,
			Class<?> targetClass, List<Object> interceptorsAndDynamicMethodMatchers) {
 
		this.proxy = proxy;
		this.target = target;
		this.targetClass = targetClass;
		this.method = BridgeMethodResolver.findBridgedMethod(method);
		this.arguments = AopProxyUtils.adaptArgumentsIfNecessary(method, arguments);
		this.interceptorsAndDynamicMethodMatchers = interceptorsAndDynamicMethodMatchers;
	}

然后调用ReflectiveMethodInvocation的proceed方法:

	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;
			if (dm.methodMatcher.matches(this.method, this.targetClass, this.arguments)) {
				return dm.interceptor.invoke(this);
			}
			else {
				//不匹配则不执行拦截器
				return proceed();
			}
		}
		else {
			// 普通拦截器。比如:MethodBeforeAdviceInterceptor、AfterReturningAdviceInterceptor、ThrowsAdviceInterceptor
			return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
		}
	}

通过currentInterceptorIndex记录当前拦截器的索引,每调用一个拦截器就+1,再次调用proceed方法时就会获取下一个拦截器调用。

3.3.1 前置拦截器和后置拦截器的执行流程

在之前一节中,我们以前置拦截器为例,向大家展示拦截器的样子,现在又向大家展示下后置拦截器:

public class AspectJAfterAdvice extends AbstractAspectJAdvice
        implements MethodInterceptor, AfterAdvice, Serializable {

    public AspectJAfterAdvice(
            Method aspectJBeforeAdviceMethod, AspectJExpressionPointcut pointcut, AspectInstanceFactory aif) {

        super(aspectJBeforeAdviceMethod, pointcut, aif);
    }


    @Override
    public Object invoke(MethodInvocation mi) throws Throwable {
        try {
            // 调用 proceed
            return mi.proceed();
        }
        finally {
            // 调用后置通知逻辑
            invokeAdviceMethod(getJoinPointMatch(), null, null);
        }
    }

    //...
}

和前置拦截器不同的是,由于后置通知需要在目标方法返回后执行,所以 AspectJAfterAdvice 先调用 mi.proceed() 执行下一个拦截器逻辑,等下一个拦截器返回后,再执行后置通知逻辑。

这里假设目标方法 method 在执行前,需要执行一个前置通知和一个后置通知。下面我们看一下由三个拦截器组成的拦截器链是如何执行的,如下:

3.3.2 执行目标方法

protected Object invokeJoinpoint() throws Throwable {
    return AopUtils.invokeJoinpointUsingReflection(this.target, this.method, this.arguments);
}

public abstract class AopUtils {
    public static Object invokeJoinpointUsingReflection(Object target, Method method, Object[] args)
            throws Throwable {

        try {
            ReflectionUtils.makeAccessible(method);
            // 通过反射执行目标方法
            return method.invoke(target, args);
        }
        catch (InvocationTargetException ex) {...}
        catch (IllegalArgumentException ex) {...}
        catch (IllegalAccessException ex) {...}
    }
}

四.补充知识

在之前的筛选合适通知器的一文中,介绍了extendAdvisors,其中有一个点没有详细说明,现在重新进行解析:

protected void extendAdvisors(List<Advisor> candidateAdvisors) {
    AspectJProxyUtils.makeAdvisorChainAspectJCapableIfNecessary(candidateAdvisors);
}

public static boolean makeAdvisorChainAspectJCapableIfNecessary(List<Advisor> advisors) {
    if (!advisors.isEmpty()) {
        // 省略部分代码

        if (foundAspectJAdvice && !advisors.contains(ExposeInvocationInterceptor.ADVISOR)) {
            // 向通知器列表中添加 ExposeInvocationInterceptor.ADVISOR
            advisors.add(0, ExposeInvocationInterceptor.ADVISOR);
            return true;
        }
    }
    return false;
}

如上,extendAdvisors 所调用的方法会向通知器列表首部添加 ExposeInvocationInterceptor.ADVISOR。现在我们再来看看 ExposeInvocationInterceptor 的源码,如下:

public class ExposeInvocationInterceptor implements MethodInterceptor, PriorityOrdered, Serializable {

    public static final ExposeInvocationInterceptor INSTANCE = new ExposeInvocationInterceptor();

    // 创建 DefaultPointcutAdvisor 匿名对象
    public static final Advisor ADVISOR = new DefaultPointcutAdvisor(INSTANCE) {
        @Override
        public String toString() {
            return ExposeInvocationInterceptor.class.getName() +".ADVISOR";
        }
    };

    private static final ThreadLocal<MethodInvocation> invocation =
            new NamedThreadLocal<MethodInvocation>("Current AOP method invocation");

    public static MethodInvocation currentInvocation() throws IllegalStateException {
        MethodInvocation mi = invocation.get();
        if (mi == null)
            throw new IllegalStateException(
                    "No MethodInvocation found: Check that an AOP invocation is in progress, and that the " +
                    "ExposeInvocationInterceptor is upfront in the interceptor chain. Specifically, note that " +
                    "advices with order HIGHEST_PRECEDENCE will execute before ExposeInvocationInterceptor!");
        return mi;
    }

    // 私有构造方法
    private ExposeInvocationInterceptor() {
    }

    @Override
    public Object invoke(MethodInvocation mi) throws Throwable {
        MethodInvocation oldInvocation = invocation.get();
        // 将 mi 设置到 ThreadLocal 中
        invocation.set(mi);
        try {
            // 调用下一个拦截器
            return mi.proceed();
        }
        finally {
            invocation.set(oldInvocation);
        }
    }

    //...
}

如上,ExposeInvocationInterceptor.ADVISOR 经过 registry.getInterceptors 方法(前面已分析过)处理后,即可得到 ExposeInvocationInterceptor。ExposeInvocationInterceptor 的作用是用于暴露 MethodInvocation 对象到 ThreadLocal 中,其名字也体现出了这一点。如果其他地方需要当前的 MethodInvocation 对象,直接通过调用 currentInvocation 方法取出。

 

参考文章:

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Spring AOP源码解析-拦截器链的执行过程 的相关文章

随机推荐

  • ORACLE数据库怎样查看当前的SID

    方法一 echo ORACLE SID 方法二 select from V database
  • 【自用】深度学习工作站安装ubuntu 18.04 LTS系统

    工作站配置 自己组装的 主板 华硕Z790P PCIE插槽间距大 可以装双显卡 CPU i5 13600KF 内存 32 G 显卡 GTX 2080 Ti 魔改版 22G 存储 1T SSD 8T机械硬盘 系统 ubuntu 18 04 L
  • storm教程(三):用Java开发storm

    1 操作模式 开始之前 有必要了解一下Storm的操作模式 有下面两种方式 本地模式 在本地模式下 Storm拓扑结构运行在本地计算机的单一JVM进程上 这个模式用于开发 测试以及调试 因为这是观察所有组件如何协同工作的最简单方法 在这种模
  • IOCTL函数用法详解

    ioctl是设备驱动程序中对设备的I O通道进行管理的函数 所谓对I O通道进行管理 就是对设备的一些特性进行控制 例如串口的传输波特率 马达的转速等等 它的调用个数如下 int ioctl int fd ind cmd 其中fd是用户程序
  • 使用深度优先搜索查找图中的路径(java)

    package depthfirstpaths import edu princeton cs algs4 Graph import edu princeton cs algs4 Stack public class DepthFirstP
  • php mysql替换数据库中出现过的所有域名实现办法 (原)

    一般新的项目上线或域名必须要更改的时候 有些数据库存的图片或者文件地址带域名的要全部改 恰巧呢 数据库表超级多 恰巧呢 又刚做了接盘侠 啥也不知道 就给你连接数据库让你改 头大不头大 我这个接盘侠上任第一天就遇上了这问题 当拿到将近1G的数
  • Thinkphp 3.2 模型View 里面使用时间戳,在模板中输出时间戳

    很简单 只需要一个代码就能搞定 time 我是这样运用的 这样子做能保证一直更新 Css文件 保证整个布局的及时更新 跟 有效性
  • Android智能下拉刷新框架-SmartRefreshLayout

    框架 下拉刷新控件还能框架化 智能又怎么回事 二话不多少先上Demo效果图 咱们再来探个究竟 Github 传送门注意 本文仅仅是博客文章 主要用于项目介绍和宣传 由于发布时间关系 部分内容已经过期 详细使用文档请跳转 github Dem
  • 大数据毕设 基于python的疫情爬虫分析可视化系统

    文章目录 0 前言 1 课题背景 2 实现效果 3 Flask框架 4 Echarts 5 爬虫 0 前言 这两年开始毕业设计和毕业答辩的要求和难度不断提升 传统的毕设题目缺少创新和亮点 往往达不到毕业答辩的要求 这两年不断有学弟学妹告诉学
  • uniapp表单密码校验:判断两次密码输入是否一致

    uniapp表单密码校验 无需使用自定义validator进行校验 使用uniapp文档内自带的this u test object value password 即可
  • ORA-01536: 超出表空间 'YYPART' 的空间限额

    ORA 01536 超出表空间 YYPART 的空间限额 author skatetime 2008 08 01 现象 研发提示空间不够用 日志显示 ORA 01536 超出表空间 YYPART 的空间限额 解决 alter user sk
  • Redis之十大类型(三)(上)

    redis是k v键值对进行存储 这里的数据类型是value的数据类型 key的类型都是字符串 keys 当前库的所有key exists key 判断某个key是否存在 type key 查看你的key是什么类型 del key 删除指定
  • 华为OD机试 - 去除多余空格(Java)

    题目描述 去除文本多余空格 但不去除配对单引号之间的多余空格 给出关键词的起始和结束下标 去除多余空格后刷新关键词的起始和结束下标 条件约束 1 不考虑关键词起始和结束位置为空格的场景 2 单词的的开始和结束下标保证涵盖一个完整的单词 即一
  • Vue自定义指令及使用

    一 什么是指令 学习 vue 的时候肯定会接触指令 那么什么是指令呢 在 vue 中提供了一些对于页面和数据更为方便的输出 这些操作就叫做指令 以 v xxx 表示 比如 html 页面中的属性 div div div gt 比如在 ang
  • HC-05两个蓝牙配对经验(绝对好使)!!!!!

    特别注意 一 串口调试助手发送 命令时 波特率最好是38400 其他的可能不好使 具体自己可以尝试一下 二 发送命令的时候一定要加入换行 否则不好使 步骤 首先 断电按下蓝牙上的按键进入AT模式 这时候灯会慢闪 大约两秒一次 1 串口调试助
  • python中__str__()函的用法

    python中 str 函数的用法 定义一个 str 的用法 class Person def init self name age self name name self age age def str self return My na
  • 无线WiFi网络的密码破解攻防及原理详解

    大家应该都有过这样的经历 就是感觉自己家的无线网怎么感觉好像变慢了 是不是有人蹭我家网 还有的时候咱们出门也想试图蹭一下别人家的网 这里 蹭网 的前提是要破解对方的 无线密码 那么这个 无线密码 到底是否安全呢 其技术原理是如何的呢 我们又
  • 相应通道无电压但ADC的值却在大幅变化且不等于0的可能原因

    今天分享一个自己的粗心引起的现象 就是之前在做ADC时候 采用单通道 规则组 和软件触发 发现ADC采集的值一直在变化 而且我都没有输入相应的电压 按理来说 ADC输出的值应该为0 10 存在偏差 但是其值却不等于0并且一直不断的变化 于是
  • 基于免疫优化算法的物流配送中心选址规划研究(Matlab实现)

    目录 1 概述 2 物流配送中心选址规划研究 3 Matlab代码 4 结果 1 概述 影响物流配送中心选址的因素有很多 精确选址优化问题亟待解决 通过充分考虑货物的配送时间 将免疫算法加入其中 介绍了物流配送选址模型的构建以及免疫算法实现
  • Spring AOP源码解析-拦截器链的执行过程

    一 简介 在前面的两篇文章中 分别介绍了 Spring AOP 是如何为目标 bean 筛选合适的通知器 以及如何创建代理对象的过程 现在得到了 bean 的代理对象 且通知也以合适的方式插在了目标方法的前后 接下来要做的事情 就是执行通知