当前位置: 首页 > article >正文

【Spring源码核心篇-03】精通spring的aop的底层原理和源码实现

Spring源码核心篇整体栏目


内容链接地址
【一】Spring的bean的生命周期https://zhenghuisheng.blog.csdn.net/article/details/143441012
【二】深入理解spring的依赖注入和属性填充https://zhenghuisheng.blog.csdn.net/article/details/143854482
【三】精通spring的aop的底层原理和源码实现https://zhenghuisheng.blog.csdn.net/article/details/144012934

深入理解spring的依赖注入和属性填充

  • 一,精通spring的aop的底层原理
    • 1,proxyFactory获取代理对象
    • 2,invoke获取代理对象的某个方法
      • 2.1,pointcutAdvisor匹配
      • 2.2,IntroductionAdvisor匹配
    • 3,proceed责任链调用
    • 4,spring中aop后置处理器
    • 5,@AspectJ注解获取advisor
    • 6,实现Advisor类获取advisor
    • 7,匹配advisor,创建动态代理
    • 8,aop总结

如需转载,请附上链接:https://blog.csdn.net/zhenghuishengq/article/details/144012934

一,精通spring的aop的底层原理

前面两篇分析了spring的bean的生命周期,以及属性填充的底层实现,接下来这篇主要讲解的是Spring的另一个核心特性 AOP ,在了解aop之前,需要先了解动态代理的具体实现cglib和jdk动态代理。

1,proxyFactory获取代理对象

在spring源码中,实现动态代理的方式主要是通过这个proxyFactory工厂实现,如下面这段代码,先创建一个动态代理的工厂,然后从工厂中获取到对应的target

ProxyFactory proxyFactory = new ProxyFactory();
UserService proxy = (UserService)proxyFactory.getProxy();

直接进入这个getProxy中可以发现会先创建一个Aop的动态代理,再通过调用对应的代理方法拿到具体的实现类

public Object getProxy() {
	return createAopProxy().getProxy();
}

首先先看这个 createAopProxy 方法,判断是创建cglib动态代理还是创建jdk的动态代理

  • config指的就是上面的userService对应的 proxy 对象,因此第一步就是对proxy 对象属性进行判断
  • 随后拿到target代理类,判断是否接口,以及判断proxy是否是jdk代理,否则则使用jdk动态代理
  • 如果不是接口或者不是使用的jdk动态代理,那么就是使用cglib代理,cglib底层也是通过实现父类实现

在这里插入图片描述

这个 getProxy 方法的AopProxy接口中,主要有两种方式实现动态代理,一种是基于cglib的 cglibAopProxy ,一种是基于jdk动态代理的 JdkDynamicAopProxy

在这里插入图片描述

使用jdk动态代理的实现如下,直接诶通过调用这个 newProxyInstance 实现,从而拿到代理对象

@Override
public Object getProxy(@Nullable ClassLoader classLoader) {
	Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
	findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
	return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
}

使用这个cglib的动态代理如下,从而拿到代理对象

@Override
public Object getProxy(@Nullable ClassLoader classLoader) {
	createProxyClassAndInstance(enhancer, callbacks)
}

2,invoke获取代理对象的某个方法

在上面已经通过proxyFactory获取到了一个代理对象,在有了对象之后,就需要获取到需要代理的方法,如在jdk动态代理中,主要是通过这个invoke方法来获取代理对象的方法

  • 首先不会执行代理对象的equal方法,hashcode方法
  • 若执行的class对象是DecoratingProxy 则不会对其应用切面进行方法的增强
  • 如果目标对象实现的Advised接口,则不会对其应用切面进行方法的增强。 直接执行方法

在这里插入图片描述

接下来继续往这个方法下面走,会有一个提前暴露的操作,如果设置为true,那么就将这个代理存入到一个ThreadLocal里面,后续只需要去这个ThreadLocal中获取即可,如事务失效的情况,就可以在这个ThreadLocal中获取

if (this.advised.exposeProxy) {
	//把我们的代理对象暴露到线程变量中
	oldProxy = AopContext.setCurrentProxy(proxy);
	setProxyContext = true;
}

继续看这个类中的方法,会调用一个 getInterceptorsAndDynamicInterceptionAdvice 方法,把aop的advisor 全部转化为拦截器, 通过责任链模式依次调用,通过该类筛选出符合的 advisor

List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

通过筛选出来的数组进行值判断,对有代理和没有代理,会有两种形式去获取方法

  • 如果筛选的链路为空,代表不需要代理,那么就会直接的通过反射的方式进行获取对象和方法
  • 如果筛选后发现链路不为空,那么就需要将代理类,目标类,目标方法,参数,class以及链路传入到 ReflectiveMethodInvocation 方法进行调用,然后返回一个重要的方法MethodInvocation对象,最终调用这个 proceed 方法进行责任链方法的执行

在这里插入图片描述

2.1,pointcutAdvisor匹配

接下来回到上面的那个 getInterceptorsAndDynamicInterceptionAdvice 筛选责任链的方法,在spring中,每一个advice都会被封装成一个advisor,每一个advisor会包括一个advice和一个pointcut切入点。

在这里插入图片描述

因此在这个筛选方法中,会去遍历全部的advisor,pointcut主要是作为匹配使用,因此需要将这个advisor先转换成这个pointcutAdvisor,然后继续执行的流程如下,通过以下流程确定对应的类和方法。可能会匹配到多个链路,因此最终返回一个一个list数组,包含 MethodInterceptor 类型的对象

  • pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass) 先通过这个方法进行类的匹配
  • MethodMatchers.matches(mm, method, actualClass, hasIntroductions) 再通过这个方法进行方法匹配

2.2,IntroductionAdvisor匹配

上面这种切入点匹配直接是通过类方法进行比较进行匹配,而下面的这种 IntroductionAdvisor 方式是通过一些适配器匹配的方式进行匹配

在这里插入图片描述

其核心是下面这个getInterceptors方法,其匹配的流程如下,先判断是否是他的MethodInterceptor实现类 ,然后判断是否 AdvisorAdapter 适配器匹配模式

@Override
public MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdviceTypeException {
	List<MethodInterceptor> interceptors = new ArrayList<>(3);
	Advice advice = advisor.getAdvice();
	if (advice instanceof MethodInterceptor) {
		interceptors.add((MethodInterceptor) 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[0]);
}

核心就是看这个适配器匹配模式,在构造这个 DefaultAdvisorAdapterRegistry 类时,会有三个具体的适配器模式,分别是 MethodBeforeAdviceAdapterAfterReturningAdviceAdapterThrowsAdviceAdapter ,为什么没有around这种模式,其实很简单,这三种模式的总集合就是around模式

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

随便查看一个 MethodBeforeAdviceAdapter 的实现如下,最左会返回一个 MethodInterceptor 对象,说明肯定是可以匹配成功的

在这里插入图片描述

3,proceed责任链调用

在spring的aop中,其底层主要是通过责任链模式实现,其底层主要是通过调用这个 proceed 方法实现,在上面匹配方法时,就已经进行了这个方法的调用

invocation.proceed();

在这里插入图片描述

直接进入这个 ReflectiveMethodInvocation 类中的proceed方法中,假设有三个调用链,那么第一步会先记录调用链的个数,随后每调用一个会加1,知道累加的个数等于调用链的个数时则会直接停止

在这里插入图片描述

接下来看这段执行完毕的方法,当执行的责任链路次数达到链路的个数时,那么就会执行这个 invokeJoinpoint 方法

if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1){
	return invokeJoinpoint();
}

接下来进入这个 invokeJoinpoint 方法,发现只有一行代码,就是调用这个 invokeJoinpointUsingReflection

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

最后查看这个 invokeJoinpointUsingReflection 方法,发现最终就是执行上面的invoke方法,这里就是执行代理对象的那个方法

在这里插入图片描述

也就是说再匹配完所有的链路之后,才会最终的执行这个代理对象所对应的方法

4,spring中aop后置处理器

接下来又得回到spring的生命周期中,直接看这个 initializeBean 初始化方法,里面会有一个 applyBeanPostProcessorsAfterInitialization 调用bean的后置处理器的方法,主要是解析aop的

在这里插入图片描述

在这个 applyBeanPostProcessorsAfterInitialization 方法中,最主要的就是这个 postProcessAfterInitialization 核心方法

@Override
public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)
		throws BeansException {

	Object result = existingBean;
	//获取我们容器中的所有的bean的后置处理器
	for (BeanPostProcessor processor : getBeanPostProcessors()) {
		Object current = processor.postProcessAfterInitialization(result, beanName);
		//若只有有一个返回null 那么直接返回原始的
		if (current == null) {
			return result;
		}
		result = current;
	}
	return result;
}

最后进入这个 postProcessAfterInitialization 核心方法,并且进入里面的实现类 AbstractAutoProxyCreator

在这里插入图片描述

进入该类后看到的方法就是 postProcessAfterInitialization ,最后进入这个重要的 wrapIfNecessary 方法

@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) throws BeansException {
	if (bean != null) {
		//获取缓存key
		Object cacheKey = getCacheKey(bean.getClass(), beanName);
		// 之前循环依赖创建的动态代理 如果是现在的bean 就不再创建,,并且移除
		if (this.earlyProxyReferences.remove(cacheKey) != bean) {
			// 该方法将会返回动态代理实例
			return wrapIfNecessary(bean, beanName, cacheKey);
		}
	}
	return bean;
}

wrapIfNecessary方法的详细流程如下:

  • 已经被处理过,就是自己实现创建动态代理逻辑,那么可以直接返回
  • 不需要增加的bean,也直接返回
  • 是不是基础的bean,是不是需要跳过的,重复判断
  • 最后就是来到了重点,根据当前bean找到匹配的advisor,最后加入到缓存中

在这里插入图片描述

在spring中创建真正的代理对象的方法如下,首先创建一个 ProxyFactory 代理工厂,然后判断是cglib的动态代理还是cglib的动态代理,然后构建Advisors数组,随后将数组加入到代理工厂中,最后调用这个 getProxy 方法获取真正的代理对象,这里的getProxy方法,又回到了最上面使用cglib还是jdk动态代理的方法

protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
		@Nullable Object[] specificInterceptors, TargetSource targetSource) {

	if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
		AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
	}
	//创建一个代理对象工厂
	ProxyFactory proxyFactory = new ProxyFactory();
	proxyFactory.copyFrom(this);

	//为proxyFactory设置创建jdk代理还是cglib代理
	// 如果设置了 <aop:aspectj-autoproxy proxy-target-class="true"/>不会进if,说明强制使用cglib
	if (!proxyFactory.isProxyTargetClass()) {
		// 内部设置的 ,   配置类就会设置这个属性
		if (shouldProxyTargetClass(beanClass, beanName)) {
			proxyFactory.setProxyTargetClass(true);
		}
		else {
			// 检查有没有接口
			evaluateProxyInterfaces(beanClass, proxyFactory);
		}
	}

	//把我们的specificInterceptors数组中的Advisor转化为数组形式的
	Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
	//为我们的代理工厂加入通知器,
	proxyFactory.addAdvisors(advisors);
	//设置targetSource对象
	proxyFactory.setTargetSource(targetSource);
	customizeProxyFactory(proxyFactory);

	proxyFactory.setFrozen(this.freezeProxy);
	// 代表之前是否筛选advise.
	// 因为继承了AbstractAdvisorAutoProxyCreator , 并且之前调用了findEligibleAdvisors进行筛选, 所以是true
	if (advisorsPreFiltered()) {
		proxyFactory.setPreFiltered(true);
	}
	//真正的创建代理对象
	return proxyFactory.getProxy(getProxyClassLoader());
}

再来看一下这个 getAdvicesAndAdvisorsForBean 方法,如何根据bean找到匹配的advisor,进入这个 AbstractAdvisorAutoProxyCreator 实现类,最后查看这个方法里面的 findEligibleAdvisors 找advisor的方法

List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);

找到合适的Advisor的具体实现如下,先拿到接口方式的aop,再判断通知能否作用到类上,最后进行一个排序

protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
	// 拿到接口方式的AOP (这次是从缓存中拿了)
	List<Advisor> candidateAdvisors = findCandidateAdvisors();
	//判断我们的通知能不能作用到当前的类上(切点是否命中当前Bean)
	List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
	extendAdvisors(eligibleAdvisors);
	//对我们的advisor进行排序
	if (!eligibleAdvisors.isEmpty()) {
		eligibleAdvisors = sortAdvisors(eligibleAdvisors);
	}
	return eligibleAdvisors;
}

接下来就是查看这个 AnnotationAwareAspectJAutoProxyCreator 类中获取advisor的方法,可以发现有两种方式去找这个advisor对象,一种是直接通过xml配置或者实现原生Aop的Advisor的接口,另一种是加了@AspectJ注解的类

在这里插入图片描述

5,@AspectJ注解获取advisor

在spring中通过@AspectJ使用aop时,@Before对应的就是advice注解,execution内部那一串就是pointcut

@Before("execution(* com.example.service..*.*(..))")

再来查看一下这个 @Aspect 这个注解是如何获取到封装的Advisor对象的,接下来直接看这个 buildAspectJAdvisors 方法,其内部核心步骤实现如下,就是遍历IOC容器中的全部bean,判断bean上面是否带有这个@AspectJ注解,找到这个注解之后,再去对应的类中解析一些 @Before,@After等注解,会将这些注解全部的封装成对应的Advisor

在这里插入图片描述

接下来在这个方法中,查看下面这段代码,真正的去获取我们的通知对象

List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory);

接下来进入这个 getAdvisors 方法,看@AspectJ是如何去解析这些切面通知的

public List<Advisor> getAdvisors(MetadataAwareAspectInstanceFactory aspectInstanceFactory) {
	//获取我们的标记为Aspect的类
	Class<?> aspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass();
	//获取我们的切面类的名称
	String aspectName = aspectInstanceFactory.getAspectMetadata().getAspectName();
	//校验我们的切面类
	validate(aspectClass);

	//我们使用的是包装模式来包装我们的MetadataAwareAspectInstanceFactory 构建为MetadataAwareAspectInstanceFactory
	MetadataAwareAspectInstanceFactory lazySingletonAspectInstanceFactory =
			new LazySingletonAspectInstanceFactoryDecorator(aspectInstanceFactory);

	List<Advisor> advisors = new ArrayList<>();
	//获取到切面类中的所有方法,但是该方法不会解析标注了@PointCut注解的方法
	for (Method method : getAdvisorMethods(aspectClass)) {
		//挨个去解析我们切面中的方法
		Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, advisors.size(), aspectName);
		if (advisor != null) {
			advisors.add(advisor);
		}
	}
}    

再查看这个 getAdvisorMethods 方法,首先会过滤掉没有pointcut的方法,随后进行一个sort的排序

private List<Method> getAdvisorMethods(Class<?> aspectClass) {
	final List<Method> methods = new ArrayList<>();
	ReflectionUtils.doWithMethods(aspectClass, method -> {
		// Exclude pointcuts
		if (AnnotationUtils.getAnnotation(method, Pointcut.class) == null) {
			methods.add(method);
		}
	});
	methods.sort(METHOD_COMPARATOR);
	return methods;
}

ReflectiveAspectJAdvisorFactory 类的静态代码块中,有一段排序的接口,其先执行的的优先级如下:Around —> Before —> After —> AfterReturning ,如果有多个相同的切面,如有两个After的切面,那么会根据方法名字符串的ascII码进行比较,谁小谁先执行

static {
	Comparator<Method> adviceKindComparator = new ConvertingComparator<>(
			new InstanceComparator<>(
					Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class),
			(Converter<Method, Annotation>) method -> {
				AspectJAnnotation<?> annotation =
					AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(method);
				return (annotation != null ? annotation.getAnnotation() : null);
			});
	Comparator<Method> methodNameComparator = new ConvertingComparator<>(Method::getName);
	METHOD_COMPARATOR = adviceKindComparator.thenComparing(methodNameComparator);
}

在查看getAdvisors 方法中的 getAdvisor 方法,上面的getAdvisorMethods方法是过滤掉没有pointcut匹配的方法,那么这个方法就是用于解析这些方法,封装成对应的advisor

在这里插入图片描述

6,实现Advisor类获取advisor

在整个aop的动态代理思路中,有一步核心就是找到全部的advisor,这里讲解的第一种方式就是通过实现一些advisor类来获取。如举个例子,直接使用aop的原生注解的实现类来实现aop的动态增强

public class AopAdvisorTest implements MethodBeforeAdvice {
	@Override
	public void before(Method method, Object[] args, Object target) throws Throwable {
		if (method.getName().equals("test")){
			System.out.println("test方法前置增加");
		}
	}
}

那么来通过源码来查看底层是如何实现的,直接进入这个 getAdvisor 方法,内部的核心方法就是调用了 InstantiationModelAwarePointcutAdvisorImpl 方法去封装成Advisor对象

在这里插入图片描述

通过外部传进来的参数生成一个Advisor对象

在这里插入图片描述

在这个对象的核心方法就是 getAdvice 方法,后序会调用这个 instantiateAdvice 方法,后面再次调用getadvice方法

在这里插入图片描述

在这个方法中,首先会获取切面类,然后获取方法上面的注解

在这里插入图片描述

后面会将获取到的注解进行匹配,通过不同的类型进行对应的封装,最后生成一个advice对象返回,5个注解对应5个advice

在这里插入图片描述

以before来看,内部就会提供一个before的实现方法,不同的advice内部会有对应的advice方法

在这里插入图片描述

advice不会以单独的方式存在,在 DefaultAdvisorAdapterRegistry 的适配器类中,最终通过这种责任链的调用模式,会将这些单独存在advice封装成一个个的advisor

在这里插入图片描述

7,匹配advisor,创建动态代理

接下来还是得回到4里面的 findEligibleAdvisors 方法,在5和6中主要都是在 findCandidateAdvisors 执行这个方法,就是如何在程序启动之后去获取这些advisor,主要有两种方法:一种是直接通过xml配置或者实现原生Aop的Advisor的接口,另一种是加了@AspectJ注解的类 ,这两种方法在找到对应的bean实例之后,最后都会封装成对应的advisor

protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
	// 拿到接口方式的AOP (这次是从缓存中拿了)
	List<Advisor> candidateAdvisors = findCandidateAdvisors();
	//判断我们的通知能不能作用到当前的类上(切点是否命中当前Bean)
	List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
	extendAdvisors(eligibleAdvisors);
	//对我们的advisor进行排序
	if (!eligibleAdvisors.isEmpty()) {
		eligibleAdvisors = sortAdvisors(eligibleAdvisors);
	}
	return eligibleAdvisors;
}

接下来就是查看这个 findAdvisorsThatCanApply 方法,里面的核心方法就是 findAdvisorsThatCanApply 方法,从候选的通知器中找到当前bean关联的advisors对象

在这里插入图片描述

findAdvisorsThatCanApply 方法核心如下,

在这里插入图片描述

接下来查看第一个canApply方法,又到了熟悉的环节,上面也讲过,起流程如下:

  • 先判断是否IntroductionAdvisor的实现类,是的话则根据类进行过滤判断
  • 再判断是否是PointcutAdvisor的实现类,是的话再次调用这个canApply方法再次进行过滤

在这里插入图片描述

平常时开发用的最多的也是实现pointcut的实现类,因此主要看这个里面 canApply 中的过滤方法

在这里插入图片描述

其核心代码如下,首对class类进行过滤,匹配出代理类class对象,找到全部符合的对象之后,再去遍历这些class类对象,对里面的方法进行匹配,先通过反射获取类中的所有方法,在判断类上面是否有@AspectJ注解,方法上面的pointcut是否匹配等,如果都匹配的话,那么就会认为这了类上面的这个方法需要进行动态代理,后续就会去创建一个动态代理对象

//创建一个集合用于保存targetClass 的class对象
Set<Class<?>> classes = new LinkedHashSet<>();
//判断当前class是不是代理的class对象
if (!Proxy.isProxyClass(targetClass)) {
	//加入到集合中去
	classes.add(ClassUtils.getUserClass(targetClass));
}
//获取到targetClass所实现的接口的class对象,然后加入到集合中
classes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetClass));
//循环所有的class对象
for (Class<?> clazz : classes) {
	//通过class获取到所有的方法
	Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);
	//循环我们的方法
	for (Method method : methods) {
		//通过methodMatcher.matches来匹配我们的方法
		if (introductionAwareMethodMatcher != null ?
				// 通过切点表达式进行匹配 AspectJ方式
				introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions) :
				// 通过方法匹配器进行匹配 内置aop接口方式
				methodMatcher.matches(method, targetClass)) {
			// 只要有1个方法匹配上了就创建代理
			return true;
		}
	}
}

8,aop总结

  • aop主要是用于切面,在原来的代码逻辑上,通过无侵入的方式实现代码的动态增强,内部主要通过代理方式cglib代理和jdk动态代理实现。
  • aop主要是在bean初始化之后,通过bean的后置处理器实现,会将需要进行aop的方法找出全部封装成advisor,生成advisor的方式主要有两种,一种是直接实现advice接口,另一种是通过@AspectJ的方式实现。
  • 每一个advisor包含一个advice和一个pointcut,advice主要用于增强使用,pointcut主要由于后续的寻找bean的匹配。
  • 当把全部的advisor全部找到之后,会进行一个匹配的操作,将全部的获取到的advisor和进行aop的bean进行匹配,先通过class类和对应的method进行匹配,匹配成功之后再通过pointcut切入点进行匹配,当全部匹配成功之后,那么就会创建一个动态代理。

http://www.kler.cn/a/408709.html

相关文章:

  • Android 设备使用 Wireshark 工具进行网络抓包
  • 计算机前沿技术-人工智能算法-大语言模型-最新研究进展-2024-11-05
  • 可视化建模与UML《协作图实验报告》
  • 2023年9月GESPC++一级真题解析
  • 怎么只提取视频中的声音?从视频中提取纯音频技巧
  • vue 目录结构
  • c++(入门)
  • 群核科技首次公开“双核技术引擎”,发布多模态CAD大模型
  • 从零开始:使用 Spring Boot 开发图书管理系统
  • pip 与当前python环境版本不匹配, pyenv, pipenv, conda
  • 速盾:海外服务器使用CDN加速有什么优势?
  • [Python3学习笔记-基础语法] Python3 基础语法
  • Excel如何批量导入图片
  • UE5中T_noise 纹理的概述
  • 前端把dom页面转为pdf文件下载和弹窗预览
  • C语言蓝桥杯组题目
  • transformer.js(一):这个前端大模型运行框架的可运行环境、使用方式、代码示例以及适合与不适合的场景
  • C#里怎么样使用多线程读取多文件?
  • 深度学习实战图像缺陷修复
  • 二分查找的几种寻找情况
  • 逻辑像素与物理像素——canvas缩放后绘图区域的长宽究竟是多少
  • draggable的el-dialog实现对话框标题可以选择
  • 一篇保姆式centos/ubuntu安装docker
  • 内网渗透横向移动1
  • 鸿蒙开发——根据背景图片来构建特定颜色的蒙版
  • mac安装appuim