Spring AOP 中的代理对象是怎么创建出来的?

news2024/11/16 5:34:02

文章目录

    • 1. AOP 用法
    • 2. 原理分析
      • 2.1 doCreateBean
      • 2.2 postProcessAfterInitialization
      • 2.3 getAdvicesAndAdvisorsForBean
        • 2.3.1 findCandidateAdvisors
        • 2.3.2 findAdvisorsThatCanApply
        • 2.3.3 extendAdvisors
      • 2.4 createProxy

今天和小伙伴们聊一聊 Spring AOP 中的代理对象是怎么创建出来的,透过这个过程再去熟悉一下 Bean 的创建过程。

最近几篇文章都是和小伙伴们聊 Spring 容器的一些使用细节,结合这些细节再看一看源码,等到这些功能都看完之后,我会再做一个汇总,到时候小伙伴们对 Spring 容器的整个创建过程就会有一个比较完整的了解了。

1. AOP 用法

先来一个简单的案例,小伙伴们先回顾一下 AOP,假设我有如下类:

@Service
public class UserService {

    public void hello() {
        System.out.println("hello javaboy");
    }
}

然后我写一个切面,拦截 UserService 中的方法:

@Component
@Aspect
@EnableAspectJAutoProxy
public class LogAspect {

    @Before("execution(* org.javaboy.bean.aop.UserService.*(..))")
    public void before(JoinPoint jp) {
        String name = jp.getSignature().getName();
        System.out.println(name+" 方法开始执行了...");
    }
}

最后,我们看一下从 Spring 容器中获取到的 UserService 对象:

ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("aop.xml");
UserService us = ctx.getBean(UserService.class);
System.out.println("us.getClass() = " + us.getClass());

打印结果如下:

可以看到,获取到的 UserService 是一个代理对象。

其他各种类型的通知我这里就不说了,不熟悉的小伙伴可以在公众号【江南一点雨】后台回复 ssm,有松哥录制的免费入门视频。

2. 原理分析

那么注入到 Spring 容器中的 UserService,为什么在获取的时候变成了一个代理对象,而不是原本的 UserService 了呢?

整体上来说,我们可以将 Spring Bean 的生命周期分为四个阶段,分别是:

  1. 实例化。
  2. 属性赋值。
  3. 初始化。
  4. 销毁。

如下图:

首先实例化就是通过反射,先把 Bean 的实例创建出来;接下来属性赋值就是给创建出来的 Bean 的各个属性赋值;接下来的初始化就是给 Bean 应用上各种需要的后置处理器;最后则是销毁。

2.1 doCreateBean

AOP 代理对象的创建是在初始化这个过程中完成的,所以今天我们就从初始化这里开始看起。

AbstractAutowireCapableBeanFactory#doCreateBean:

protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
		throws BeanCreationException {
    //...
	try {
		populateBean(beanName, mbd, instanceWrapper);
		exposedObject = initializeBean(beanName, exposedObject, mbd);
	}
	//...
	return exposedObject;
}

小伙伴们看到,这里有一个 initializeBean 方法,在这个方法中会对 Bean 执行各种后置处理器:

protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {
	invokeAwareMethods(beanName, bean);
	Object wrappedBean = bean;
	if (mbd == null || !mbd.isSynthetic()) {
		wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
	}
	try {
		invokeInitMethods(beanName, wrappedBean, mbd);
	}
	catch (Throwable ex) {
		throw new BeanCreationException(
				(mbd != null ? mbd.getResourceDescription() : null), beanName, ex.getMessage(), ex);
	}
	if (mbd == null || !mbd.isSynthetic()) {
		wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
	}
	return wrappedBean;
}

这里一共是执行了四个方法,也都是非常常见的 Bean 初始化方法:

  1. invokeAwareMethods:执行 Aware 接口下的 Bean。
  2. applyBeanPostProcessorsBeforeInitialization:执行 BeanPostProcessor 中的前置方法。
  3. invokeInitMethods:执行 Bean 的初始化方法 init。
  4. applyBeanPostProcessorsAfterInitialization:执行 BeanPostProcessor 中的后置方法。

1、3 这两个方法很明显跟 AOP 关系不大,我们自己平时创建的 AOP 对象基本上都是在 applyBeanPostProcessorsAfterInitialization 中进行处理的,我们来看下这个方法:

@Override
public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)
		throws BeansException {
	Object result = existingBean;
	for (BeanPostProcessor processor : getBeanPostProcessors()) {
		Object current = processor.postProcessAfterInitialization(result, beanName);
		if (current == null) {
			return result;
		}
		result = current;
	}
	return result;
}

小伙伴们看到,这里就是遍历各种 BeanPostProcessor,并执行其 postProcessAfterInitialization 方法,将执行结果赋值给 result 并返回。

2.2 postProcessAfterInitialization

BeanPostProcessor 有一个实现类 AbstractAutoProxyCreator,在 AbstractAutoProxyCreator 的 postProcessAfterInitialization 方法中,进行了 AOP 的处理:

@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
	if (bean != null) {
		Object cacheKey = getCacheKey(bean.getClass(), beanName);
		if (this.earlyProxyReferences.remove(cacheKey) != bean) {
			return wrapIfNecessary(bean, beanName, cacheKey);
		}
	}
	return bean;
}
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
	if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
		return bean;
	}
	if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
		return bean;
	}
	if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
		this.advisedBeans.put(cacheKey, Boolean.FALSE);
		return bean;
	}
	// Create proxy if we have advice.
	Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
	if (specificInterceptors != DO_NOT_PROXY) {
		this.advisedBeans.put(cacheKey, Boolean.TRUE);
		Object proxy = createProxy(
				bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
		this.proxyTypes.put(cacheKey, proxy.getClass());
		return proxy;
	}
	this.advisedBeans.put(cacheKey, Boolean.FALSE);
	return bean;
}

可以看到,首先会尝试去缓存中获取代理对象,如果缓存中没有的话,则会调用 wrapIfNecessary 方法进行 AOP 的创建。

正常来说,普通 AOP 的创建,前面三个 if 的条件都是不满足的。第一个 if 是说 beanName 是否是一个 targetSource,显然我们这里不是;第二个 if 是说这个 Bean 是不是不需代理(结合上篇文章一起理解),我们这里显然是需要代理的;第三个 if 的作用我们也在上篇文章中和小伙伴们介绍过,这里就不再赘述了。

关于第二个 if 我多说一句,如果这里进来的是一个切面的 Bean,例如第一小节中的 LogAspect,这种 Bean 显然是不需要代理的,所以会在第二个方法中直接返回,如果是其他普通的 Bean,则第二个 if 并不会进来。

所在在 wrapIfNecessary 中,最重要的方法实际上就是两个:getAdvicesAndAdvisorsForBean 和 createProxy,前者用来找出来所有跟当前类匹配的切面,后者则用来创建代理对象。

2.3 getAdvicesAndAdvisorsForBean

这个方法,说白了,就是查找各种 Advice(通知/增强) 和 Advisor(切面)。来看下到底怎么找的:

AbstractAdvisorAutoProxyCreator#getAdvicesAndAdvisorsForBean:

@Override
@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();
}

从这里可看到,这个方法主要就是调用 findEligibleAdvisors 去获取到所有的切面,继续:

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;
}

这里一共有三个主要方法:

  • findCandidateAdvisors:这个方法是查询到所有候选的 Advisor,说白了,就是把项目启动时注册到 Spring 容器中所有切面都找到,由于一个 Aspect 中可能存在多个 Advice,每个 Advice 最终都能封装为一个 Advisor,所以在具体查找过程中,找到 Aspect Bean 之后,还需要遍历 Bean 中的方法。
  • findAdvisorsThatCanApply:这个方法主要是从上个方法找到的所有切面中,根据切点过滤出来能够应用到当前 Bean 的切面。
  • extendAdvisors:这个是添加一个 DefaultPointcutAdvisor 切面进来,这个切面使用的 Advice 是 ExposeInvocationInterceptor,ExposeInvocationInterceptor 的作用是用于暴露 MethodInvocation 对象到 ThreadLocal 中,如果其他地方需要使用当前的 MethodInvocation 对象,直接通过调用 currentInvocation 方法取出即可。

接下来我们就来看一下这三个方法的具体实现。

2.3.1 findCandidateAdvisors

AnnotationAwareAspectJAutoProxyCreator#findCandidateAdvisors

@Override
protected List<Advisor> findCandidateAdvisors() {
	List<Advisor> advisors = super.findCandidateAdvisors();
	if (this.aspectJAdvisorsBuilder != null) {
		advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
	}
	return advisors;
}

这个方法的关键在于通过 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 ArrayList<>();
				aspectNames = new ArrayList<>();
				String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
						this.beanFactory, Object.class, true, false);
				for (String beanName : beanNames) {
					if (!isEligibleBean(beanName)) {
						continue;
					}
					// We must be careful not to instantiate beans eagerly as in this case they
					// would be cached by the Spring container but would not have been weaved.
					Class<?> beanType = this.beanFactory.getType(beanName, false);
					if (beanType == null) {
						continue;
					}
					if (this.advisorFactory.isAspect(beanType)) {
						aspectNames.add(beanName);
						AspectMetadata amd = new AspectMetadata(beanType, beanName);
						if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) {
							MetadataAwareAspectInstanceFactory factory =
									new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);
							List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory);
							if (this.beanFactory.isSingleton(beanName)) {
								this.advisorsCache.put(beanName, classAdvisors);
							}
							else {
								this.aspectFactoryCache.put(beanName, factory);
							}
							advisors.addAll(classAdvisors);
						}
						else {
							// Per target or per this.
							if (this.beanFactory.isSingleton(beanName)) {
								throw new IllegalArgumentException("Bean with name '" + beanName +
										"' is a singleton, but aspect instantiation model is not singleton");
							}
							MetadataAwareAspectInstanceFactory factory =
									new PrototypeAspectInstanceFactory(this.beanFactory, beanName);
							this.aspectFactoryCache.put(beanName, factory);
							advisors.addAll(this.advisorFactory.getAdvisors(factory));
						}
					}
				}
				this.aspectBeanNames = aspectNames;
				return advisors;
			}
		}
	}
	if (aspectNames.isEmpty()) {
		return Collections.emptyList();
	}
	List<Advisor> advisors = new ArrayList<>();
	for (String aspectName : aspectNames) {
		List<Advisor> cachedAdvisors = this.advisorsCache.get(aspectName);
		if (cachedAdvisors != null) {
			advisors.addAll(cachedAdvisors);
		}
		else {
			MetadataAwareAspectInstanceFactory factory = this.aspectFactoryCache.get(aspectName);
			advisors.addAll(this.advisorFactory.getAdvisors(factory));
		}
	}
	return advisors;
}

这个方法第一次进来的时候,aspectNames 变量是没有值的,所以会先进入到 if 分支中,给 aspectNames 和 aspectBeanNames 两个变量赋值。

具体过程就是首先调用 BeanFactoryUtils.beanNamesForTypeIncludingAncestors 方法(不熟悉该方法的小伙伴参考 Spring 中的父子容器是咋回事?一文),去当前容器以及当前容器的父容器中,查找到所有的 beanName,将返回的数组赋值给 beanNames 变量,然后对 beanNames 进行遍历。

遍历时,首先调用 isEligibleBean 方法,这个方法是检查给定名称的 Bean 是否符合自动代理的条件的,这个细节我们就不看了,因为一般情况下,我们项目中的 AOP 都是自动代理的。

接下来根据 beanName,找到对应的 bean 类型 beanType,然后调用 advisorFactory.isAspect 方法去判断这个 beanType 是否是一个 Aspect,具体的判断过程上篇文章讲过了,小伙伴们可以参考。

如果当前 beanName 对应的 Bean 是一个 Aspect,那么就把 beanName 添加到 aspectNames 集合中,并且把 beanName 和 beanType 封装为一个 AspectMetadata 对象。

接下来会去判断 kind 是否为 SINGLETON,这个默认都是 SINGLETON,所以这里会进入到分支中,进来之后,会调用 this.advisorFactory.getAdvisors 方法去 Aspect 中找到各种通知和切点并封装成 Advisor 对象返回,由于一个切面中可能定义多个通知,所以最终返回的 Advisor 是一个集合,最后把找到的 Advisor 集合存入到 advisorsCache 缓存中。

后面方法的逻辑就很好懂了,从 advisorsCache 中找到某一个 aspect 对应的所有 Advisor,并将之存入到 advisors 集合中,然后返回集合。

这样,我们就找到了所有的 Advisor。

额外多说一句,在上篇文章中,松哥最后留了一个彩蛋,当时说普通的 Bean 会走到 shouldSkip 方法中,这个 shouldSkip 方法最终就会走到 buildAspectJAdvisors 中来,所以上篇文章后来就没和大家分析 buildAspectJAdvisors 方法了。其实,如果在 shouldSkip 方法中就调用了 buildAspectJAdvisors,那么就完成了 Advisor 的收集了,后续每次直接获取就可以了。

2.3.2 findAdvisorsThatCanApply

接下来 findAdvisorsThatCanApply 方法主要是从众多的 Advisor 中,找到能匹配上当前 Bean 的 Advisor,小伙伴们知道,每一个 Advisor 都包含一个切点 Pointcut,不同的切点意味着不同的拦截规则,所以现在需要进行匹配,检查当前类需要和哪个 Advisor 匹配:

protected List<Advisor> findAdvisorsThatCanApply(
		List<Advisor> candidateAdvisors, Class<?> beanClass, String beanName) {
	ProxyCreationContext.setCurrentProxiedBeanName(beanName);
	try {
		return AopUtils.findAdvisorsThatCanApply(candidateAdvisors, beanClass);
	}
	finally {
		ProxyCreationContext.setCurrentProxiedBeanName(null);
	}
}

这里实际上就是调用了静态方法 AopUtils.findAdvisorsThatCanApply 去查找匹配的 Advisor:

public static List<Advisor> findAdvisorsThatCanApply(List<Advisor> candidateAdvisors, Class<?> clazz) {
	if (candidateAdvisors.isEmpty()) {
		return candidateAdvisors;
	}
	List<Advisor> eligibleAdvisors = new ArrayList<>();
	for (Advisor candidate : candidateAdvisors) {
		if (candidate instanceof IntroductionAdvisor && canApply(candidate, clazz)) {
			eligibleAdvisors.add(candidate);
		}
	}
	boolean hasIntroductions = !eligibleAdvisors.isEmpty();
	for (Advisor candidate : candidateAdvisors) {
		if (candidate instanceof IntroductionAdvisor) {
			// already processed
			continue;
		}
		if (canApply(candidate, clazz, hasIntroductions)) {
			eligibleAdvisors.add(candidate);
		}
	}
	return eligibleAdvisors;
}

这个方法中首先会去判断 Advisor 的类型是否是 IntroductionAdvisor 类型,IntroductionAdvisor 类型的 Advisor 只能在类级别进行拦截,灵活度不如 PointcutAdvisor,所以我们一般都不是 IntroductionAdvisor,因此这里最终会走入到最后一个分支中:

public static boolean canApply(Advisor advisor, Class<?> targetClass, boolean hasIntroductions) {
	if (advisor instanceof IntroductionAdvisor ia) {
		return ia.getClassFilter().matches(targetClass);
	}
	else if (advisor instanceof PointcutAdvisor pca) {
		return canApply(pca.getPointcut(), targetClass, hasIntroductions);
	}
	else {
		// It doesn't have a pointcut so we assume it applies.
		return true;
	}
}

从这里小伙伴们就能看到,IntroductionAdvisor 类型的 Advisor 只需要调用 ClassFilter 过滤一下就行了,ClassFilter 松哥在前面的文章中已经介绍过了(玩一玩编程式 AOP),小伙伴们看这里的匹配逻辑也是非常 easy!而 PointcutAdvisor 类型的 Advisor 则会继续调用 canApply 方法进行判断:

public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) {
	if (!pc.getClassFilter().matches(targetClass)) {
		return false;
	}
	MethodMatcher methodMatcher = pc.getMethodMatcher();
	if (methodMatcher == MethodMatcher.TRUE) {
		// No need to iterate the methods if we're matching any method anyway...
		return true;
	}
	IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null;
	if (methodMatcher instanceof IntroductionAwareMethodMatcher iamm) {
		introductionAwareMethodMatcher = iamm;
	}
	Set<Class<?>> classes = new LinkedHashSet<>();
	if (!Proxy.isProxyClass(targetClass)) {
		classes.add(ClassUtils.getUserClass(targetClass));
	}
	classes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetClass));
	for (Class<?> clazz : classes) {
		Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);
		for (Method method : methods) {
			if (introductionAwareMethodMatcher != null ?
					introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions) :
					methodMatcher.matches(method, targetClass)) {
				return true;
			}
		}
	}
	return false;
}

小伙伴们看一下,这里就是先按照类去匹配,匹配通过则继续按照方法去匹配,方法匹配器要是设置的 true,那就直接返回 true 就行了,否则就加载当前类,也就是 targetClass,然后遍历 targetClass 中的所有方法,最后调用 introductionAwareMethodMatcher.matches 方法去判断方法是否和切点契合。

就这样,我们就从所有的 Advisor 中找到了所有和当前类匹配的 Advisor 了。

2.3.3 extendAdvisors

这个是添加一个 DefaultPointcutAdvisor 切面进来,这个切面使用的 Advice 是 ExposeInvocationInterceptor,ExposeInvocationInterceptor 的作用是用于暴露 MethodInvocation 对象到 ThreadLocal 中,如果其他地方需要使用当前的 MethodInvocation 对象,直接通过调用 currentInvocation 方法取出即可。

这个方法的逻辑比较简单,我就不贴出来了,小伙伴们可以自行查看。

2.4 createProxy

看完了 getAdvicesAndAdvisorsForBean 方法,我们已经找到了适合我们的 Advisor,接下来继续看 createProxy 方法,这个方法用来创建一个代理对象:

protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
		@Nullable Object[] specificInterceptors, TargetSource targetSource) {
	return buildProxy(beanClass, beanName, specificInterceptors, targetSource, false);
}
private Object buildProxy(Class<?> beanClass, @Nullable String beanName,
		@Nullable Object[] specificInterceptors, TargetSource targetSource, boolean classOnly) {
	if (this.beanFactory instanceof ConfigurableListableBeanFactory clbf) {
		AutoProxyUtils.exposeTargetClass(clbf, beanName, beanClass);
	}
	ProxyFactory proxyFactory = new ProxyFactory();
	proxyFactory.copyFrom(this);
	if (proxyFactory.isProxyTargetClass()) {
		// Explicit handling of JDK proxy targets and lambdas (for introduction advice scenarios)
		if (Proxy.isProxyClass(beanClass) || ClassUtils.isLambdaClass(beanClass)) {
			// Must allow for introductions; can't just set interfaces to the proxy's interfaces only.
			for (Class<?> ifc : beanClass.getInterfaces()) {
				proxyFactory.addInterface(ifc);
			}
		}
	}
	else {
		// No proxyTargetClass flag enforced, let's apply our default checks...
		if (shouldProxyTargetClass(beanClass, beanName)) {
			proxyFactory.setProxyTargetClass(true);
		}
		else {
			evaluateProxyInterfaces(beanClass, proxyFactory);
		}
	}
	Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
	proxyFactory.addAdvisors(advisors);
	proxyFactory.setTargetSource(targetSource);
	customizeProxyFactory(proxyFactory);
	proxyFactory.setFrozen(this.freezeProxy);
	if (advisorsPreFiltered()) {
		proxyFactory.setPreFiltered(true);
	}
	// Use original ClassLoader if bean class not locally loaded in overriding class loader
	ClassLoader classLoader = getProxyClassLoader();
	if (classLoader instanceof SmartClassLoader smartClassLoader && classLoader != beanClass.getClassLoader()) {
		classLoader = smartClassLoader.getOriginalClassLoader();
	}
	return (classOnly ? proxyFactory.getProxyClass(classLoader) : proxyFactory.getProxy(classLoader));
}

这段代码不知道小伙伴们看了是否觉得眼熟,这就是前面发的另类 AOP,编程式 AOP!一文中的内容,所以这块源码大家自己看看就好了,我就不啰嗦了。

好啦,经过上面这一顿操作,代理对象就创建出来了~本文是一个大致的逻辑,还有一些特别细的小细节没和小伙伴们梳理,咱们有面有空松哥继续整文章和大家介绍

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

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

相关文章

将number输入框改写成只能输入数字的输入框

去掉 type”number” 末尾的箭头 场景代码 场景 在很多场景下&#xff0c;输入框是会被要求限制只能输入数字&#xff0c;不能输入文字或者符号的。通常我们会使用input事件 正则表达式 去实现这个功能&#xff0c;但今天提供另一种css的方法&#xff0c;将 type”number” 的…

面试总结-Redis篇章(九)——Redis主从复制、主从数据同步原理

Redis其他面试问题 主从复制单节点Redis的并发能力是有上限的&#xff0c;要进一步提高Redis的并发能力&#xff0c;就需要搭建主从集群&#xff0c;实现读写分离主节点主要进行客户端的写操作&#xff0c;从节点进行客户端的读操作&#xff0c;因为Redis一直都是读多写少&…

iTOP-RK3568开发板Windows 安装 RKTool 驱动

在烧写镜像之前首先需要安装 RKTool 驱动。 RKTool 驱动在网盘资料“iTOP-3568 开发板\01_【iTOP-RK3568 开发板】基础资料 \02_iTOP-RK3568 开发板烧写工具及驱动”路径下。 驱动如下图所示&#xff1a; 解压缩后&#xff0c;进入文件夹&#xff0c;如下图所示&#xff1a;…

品牌活动 | 阿里云云原生技术实践营:大模型+CloudOS,实现企业智能化

近日&#xff0c;由阿里云举办的“云原生技术实践营-应用和容器实践专场”在广州顺利开展。行云创新CEO马洪喜作为受邀嘉宾之一&#xff0c;参加了本次活动&#xff0c;分享了主题为“API大语言模型&#xff0c;以非侵入式实现企业业务智能化变革”的演讲&#xff0c;向参会者展…

【华秋干货铺】一文轻松搞定PCB叠层和阻抗设计

为了减少在高速信号传输过程中的反射现象&#xff0c;必须在信号源、接收端以及传输线上保持阻抗的匹配。单端信号线的具体阻抗取决于它的线宽尺寸以及与参考平面之间的相对位置。特定阻抗要求的差分对间的线宽/线距则取决于选择的PCB叠层结构。 由于最小线宽和最小线距是取决…

WRF替换GVF数据

WRF替换GVF数据 GVF(Green Vegetation Fraction) 是决定WRF模拟局地/区域气候研究的关键参数。研究表明GVF对模式模拟温度、湿度和潜热误差校正贡献率分别为62%&#xff0c;87%和92%。因此本文提供对WRF中的GVF进行替换的具体方法。 1.GVF获取   目前还没有现成的GVF产品可以…

Java代码连接RabbitMQ服务器

目录 1.添加依赖 2.生产者代码 3.消费者代码 4.效果 1.发送消息 2.消费消息 5.注意 1.添加依赖 <dependency><groupId>com.rabbitmq</groupId><artifactId>amqp-client</artifactId><version>5.12.0</version></dependenc…

12、springboot自定义banner

springboot自定义banner ▲ 控制Banner信息是否显示及在哪里显示&#xff1a; 在application.properties这个配置文件里面进行以下的属性设置&#xff1a;spring.main.banner-mode 属性进行设置&#xff0c;该属性支持如下3个属性值&#xff1a;- console&#xff1a;在Spring…

JavaWeb+jsp+Tomcat的教务查询系统

点击以下链接获取源码&#xff1a; https://download.csdn.net/download/qq_64505944/88134601?spm1001.2014.3001.5503 jsp/tomcat7.05/MySQL5.7或8版本/ssm框架/spring/ Web框架&#xff1a;SpringBoot/ORM框架&#xff1a;Mybatis/安全框架&#xff1a;Shiro/分页插件&am…

指针进阶详解---C语言

❤博主CSDN:啊苏要学习 ▶专栏分类&#xff1a;C语言◀ C语言的学习&#xff0c;是为我们今后学习其它语言打好基础&#xff0c;C生万物&#xff01; 开始我们的C语言之旅吧&#xff01;✈ 目录 前言&#xff1a; 一.字符指针 二.指针数组 三.数组指针 四.数组、指针参数 …

html富文本编辑器

接了个单子&#xff0c;需要添加一个文章模块&#xff0c;一看用到的技术这么老&#xff0c;人傻了&#xff0c;纯html css js 。 在普通页面中 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"…

word xls有用小技巧

不少office、代码编辑等软件&#xff0c;很简单高效小技巧。Word xlsx 某一行或列不动&#xff1a; 视图》冻结窗格》冻结首行 eclispe 全局搜索 CtrlH 制定变量、名称搜索 鼠标左键点中CtrlAltG

Promethus(普罗米修斯)监控系统

一、普罗米修斯概述 Prometheus(由go语言(golang)开发)是一套开源的监控&报警&时间序列数据库的组合。适合监控docker容器。因为kubernetes(俗称k8s)的流行带动了prometheus的发展。 Overview | Prometheus 二、时间序列数据 1、什么是序列数据 时间序列数据(TimeSer…

Docker安装es以及ik分词器

1、拉取镜像 docker pull elasticsearch:7.10.12、下载对应版本的ik分词、并将它们解压到ik文件夹下&#xff0c;如图 https://github.com/medcl/elasticsearch-analysis-ik/releases 3、在服务器上创建文件夹 mkdir /usr/elklog/elk/es mkdir /usr/elklog/elk/es/data mkdi…

Android 架构模式如何选择

作者&#xff1a;vivo 互联网客户端团队-Xu Jie Android架构模式飞速演进&#xff0c;目前已经有MVC、MVP、MVVM、MVI。到底哪一个才是自己业务场景最需要的&#xff0c;不深入理解的话是无法进行选择的。这篇文章就针对这些架构模式逐一解读。重点会介绍Compose为什么要结合MV…

脑电信号处理与特征提取——6.运用机器学习技术和脑电进行大脑解码(涂毅恒)

目录 六、运用机器学习技术和脑电进行大脑解码 6.1 前言 6.2 基于脑电数据的机器学习基础分析 6.3 基于脑电数据的机器学习进阶分析 6.4 代码解读 六、运用机器学习技术和脑电进行大脑解码 6.1 前言 6.2 基于脑电数据的机器学习基础分析 6.3 基于脑电数据的机器学习进阶分…

反射简述

什么是反射反射在java中起到什么样的作用获取class对象的三种方式反射的优缺点图 什么是反射 JAVA反射机制是在运行状态中&#xff0c;对于任意一个类&#xff0c;都能够知道这个类的所有属性和方法&#xff1b;对于任意一个对象&#xff0c;都能够调用它的任意一个方法和属性&…

【yolov8+人/车流量统计】

文章目录 前言修改点PreprocessInference 总结 前言 之前写个yolov8的一个试用版&#xff0c;【深度学习】Yolov8追踪从0到1, 这要是做计数啥的,简单的一批&#xff0c;一套工程化的代码&#xff0c;给自己挖了个坑&#xff0c;说要实现一个基于yolov8的人/车流量统计. 现在要…

如何学习ARM嵌入式开发?

ARM和单片机还是有许多区别的&#xff0c;可以说比单片机的应用更为复杂吧&#xff0c;往往在单片机里只需要对一个寄存器赋值就可以的初始化&#xff0c;在ARM下就要调用库函数了。甚至每个引脚其功能都多了许多&#xff0c;相应的配置也会更为麻烦&#xff0c;但如果做多了AR…

回归分析简明教程【Regression Analysis】

为了理解回归背后的动机&#xff0c;让我们考虑以下简单的例子。 下面的散点图显示了2001年至2012年美国大学毕业生的数量。 现在根据现有的数据&#xff0c;如果有人问你2018年有多少名大学毕业生获得硕士学位呢&#xff1f; 可以看出&#xff0c;具有硕士学位的大学毕业生数量…