【Spring源码:AOP二】基于JDK动态代理和Cglib创建代理对象的原理分析

上一篇,我们最后通过一个例子讲解了resolveBeforeInstantiation()中,通过InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation返回一个代理对象,下面,我们继续分析这个方法返回的是null的情况,那么它就会往下执行doCreateBean()

	@Override
	protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
			throws BeanCreationException {
        
        // 省略部分代码...           

		//  1、代理对象的创建
        // 一般是实现了InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation()
		Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
        if (bean != null) {
			// 如果bean不为null ,则直接返回,这种情况通常创建了代理对象。后面的doCreateBean不再执行
			return bean;
		}

		// 2、普通bean创建
		// 包括检查是否进行了类加载(没有则进行类加载),bean对象实例创建,bean对象实例的属性赋值,init-method的调用,BeanPostProcessor的调用
		Object beanInstance = doCreateBean(beanName, mbdToUse, args);

	}

假如,我们有一个A bean需要创建,并且设置了一个AOP切面:

@Component
public class A {

	public void test(){
		System.out.println("----a---");
	}
}

@Aspect
@Component
public class Aop {

	@Pointcut("execution(* com.example.demo.aop2..*.*(..))")
	private void pointcut(){}

	@After("pointcut()")
	public void advice(){
		System.out.println("-----------后置增强---------");
	}
}

@Configuration
@ComponentScan("com.example.demo.aop2")
@EnableAspectJAutoProxy
public class AppConfig {
}

public class Test {

	public static void main(String[] args) {
		AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
		A a = ac.getBean(A.class);
		a.f();
	}
}

通过打断点调试,我们发现在创建beanName=a的时候,执行到:Object bean = resolveBeforeInstantiation(beanName, mbdToUse) 时,返回的bean=null,所以会往下执行:

Object beanInstance = doCreateBean(beanName, mbdToUse, args);

这个方法是重点,它主要作用是检查是否进行了类加载(没有则进行类加载)、bean对象的创建、bean对象的属性赋值,init-method方法的调用,BeanPostProcessor的调用等,大致:

实例化(前后)-->属性填充-->初始化(前后) (这里暂且不讨论循环依赖的情况)。

【重点】

AbstractAutowireCapableBeanFactory#doCreateBean()

	protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
			throws BeanCreationException {

		// Instantiate the bean.
		BeanWrapper instanceWrapper = null;
		if (mbd.isSingleton()) {
			instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
		}
		if (instanceWrapper == null) {
			// 1、推断构造方法并且实例化bean对象
			instanceWrapper = createBeanInstance(beanName, mbd, args);
		}
        // 获取原始对象
		final Object bean = instanceWrapper.getWrappedInstance();
		Class<?> beanType = instanceWrapper.getWrappedClass();
		if (beanType != NullBean.class) {
			mbd.resolvedTargetType = beanType;
		}

		// Allow post-processors to modify the merged bean definition.
		// 2、做一些合并bean定义的逻辑
		synchronized (mbd.postProcessingLock) {
			if (!mbd.postProcessed) {
				try {
					applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
				} catch (Throwable ex) {
					throw new BeanCreationException(mbd.getResourceDescription(), beanName,
							"Post-processing of merged bean definition failed", ex);
				}
				mbd.postProcessed = true;
			}
		}

		// Eagerly cache singletons to be able to resolve circular references
		// even when triggered by lifecycle interfaces like BeanFactoryAware.
		// 判断属于单例、并且允许循环依赖、在当前正在创建的beanName集合中
		boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
				isSingletonCurrentlyInCreation(beanName));
		if (earlySingletonExposure) {
			if (logger.isDebugEnabled()) {
				logger.debug("Eagerly caching bean '" + beanName +
						"' to allow for resolving potential circular references");
			}

			// 重点:3、将当前正在创建的bean的单例工厂放入到SingletonFactories集合中(三级缓存)
			// 第二个参数,ObjectFactory属于函数式接口,只有getObject()一个方法,所以支持lambda表达式
			addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
		}

		// Initialize the bean instance.
		Object exposedObject = bean;
		try {

			// 4、填充属性--->循环依赖
			// populateBean中 会执行InstantiationAwareBeanPostProcessor中的postProcessAfterInstantiation,
			// 这是在对象实例化后,还没有进行属性填充的时候会调用的方法。如果此时该方法返回false,
			// 并且mbd.getDependencyCheck() 不需要check,则不会进行属性填充,否则继续走下面的逻辑

			// 【注意】提前动态代理(循环依赖),其实是在依赖注入的时候,也就是在populateBean属性填充方法内完成的
			populateBean(beanName, mbd, instanceWrapper);

			// 5、执行initializeBean,初始化Bean
			// 【注意】非提前生成代理对象(非循环依赖)是在属性填充populateBean完成之后,执行了initializeBean方法的里面进行的动态代理
			exposedObject = initializeBean(beanName, exposedObject, mbd);

		}
    。。。。
}

上面注释可以清楚知道,实例化-->属性填充-->初始化,下面我们重点关注初始化方法initializeBean()

	// 初始化(前后),AOP代理对象
	protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {

		// 执行invokeAwareMethods 处理部分Aware接口的方法
		if (System.getSecurityManager() != null) {
			AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
				invokeAwareMethods(beanName, bean);
				return null;
			}, getAccessControlContext());
		} else {
			invokeAwareMethods(beanName, bean);
		}

		Object wrappedBean = bean;
		if (mbd == null || !mbd.isSynthetic()) {
			//初始化之前的回调:对该bean调用所有后置处理器的postProcessBeforeInitialization方法
			wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
		}

		try {
			//初始化方法
			invokeInitMethods(beanName, wrappedBean, mbd);
		} catch (Throwable ex) {
			throw new BeanCreationException(
					(mbd != null ? mbd.getResourceDescription() : null),
					beanName, "Invocation of init method failed", ex);
		}
		if (mbd == null || !mbd.isSynthetic()) {
			//【aop重点】初始化后:代理对象的生成就在这个方法中生成
			// 非提前生成代理对象(非循环依赖)是在属性填充populateBean完成之后,执行了initializeBean方法的时候进行的动态代理
			wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
		}

		return wrappedBean;
	}

看注释基本都清楚,这个方法就是:

  • 初始化前:对该bean调用所有后置处理器的postProcessBeforeInitialization方法
  • 初始化
  • 初始化后:代理对象的生成就在这个方法中生成

这里我们重点关注初始化后,调用applyBeanPostProcessorsAfterInitialization()方法,主要是遍历所有的bean后置处理器,然后分别执行相应的后置处理方法postProcessAfterInitialization()

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

		Object result = existingBean;
		// 遍历所有的bean后置处理器
		for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) {
			// 其中有一个子类AbstractAutoProxyCreator,调用它的postProcessAfterInitialization就会产生aop代理对象
			// 所以非提前生成的代理对象 是在属性填充populateBean完成之后,执行了initializeBean方法的时候进行的动态代理
			Object current = beanProcessor.postProcessAfterInitialization(result, beanName);
			if (current == null) {
				return result;
			}
			result = current;
		}
		return result;
	}

我们上面有分析,其中,有一个AbstractAutoProxyCreator,就属于BeanPostProcessor接口的子类,调用它的postProcessAfterInitialization就会产生aop代理对象。

下面,我们跟进断点调试验证:

原始对象A@2508并非代理对象

当执行完applyBeanPostProcessorsAfterInitialization()方法后,发现返回了一个代理对象,并且该代理对象是通过cglib生成的(因为我们的A类没有实现任何接口,在Spring中默认为基于JDK的动态代理,但在SpringBoot2.x中默认是Cglib)

因此,我们知道代理对象就是在bean初始化后进行创建的,具体AbstractAutoProxyCreator#postProcessAfterInitialization() 如下:

	@Override
	public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) throws BeansException {
		if (bean != null) {
			Object cacheKey = getCacheKey(bean.getClass(), beanName);
			if (!this.earlyProxyReferences.contains(cacheKey)) {
				return wrapIfNecessary(bean, beanName, cacheKey);
			}
		}
		return bean;
	}

上述方法:根据bean类型和名字创建缓存key,判断earlyProxyReferences提前动态代理的集合当中存不存在这个缓存key,若存在则说明已经进行过动态代理了,则不再进行动态代理,而本例子中,很明显是没有执行提前动态代理的,所以会执行wrapIfNecessary方法进行构建动态代理对象

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

创建代理对象:createProxy

createProxy的源码如下:代理对象可以基于JDK的动态代理或者基于Cglib来创建,在Spring中默认为基于JDK的动态代理(在SpringBoot2.x中默认是Cglib,具体可看《SpringBoot2.x中为何默认使用Cglib》),如果ProxyConfig配置类的proxy-target-class属性为true或者目标类没有实现接口,则使用Cglib来创建代理对象。

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

		// 每个bean对象都new一个单例的ProxyFactory来创建代理对象,因为每个bean需要的辅助方法不一样,
		// 然后将该ProxyFactory对象引用作为构造函数参数创建对应的代理对象
		ProxyFactory proxyFactory = new ProxyFactory();
		proxyFactory.copyFrom(this);

		// // 检查是否配置了<aop:config />节点的proxy-target-class属性为true-->cglib
		if (!proxyFactory.isProxyTargetClass()) {
			if (shouldProxyTargetClass(beanClass, beanName)) {
				proxyFactory.setProxyTargetClass(true);
			} else {
				evaluateProxyInterfaces(beanClass, proxyFactory);
			}
		}


		Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);

		// 为该代理工厂添加辅助功能包装器Advisors,结合Advisors来生成代理对象的方法拦截器
		proxyFactory.addAdvisors(advisors);

		// 目标类
		proxyFactory.setTargetSource(targetSource);
		customizeProxyFactory(proxyFactory);

		proxyFactory.setFrozen(this.freezeProxy);
		if (advisorsPreFiltered()) {
			proxyFactory.setPreFiltered(true);
		}

		// 为目标类创建代理对象,如果配置了aop:config的proxy-target-class为true,则使用CGLIB
		// 否则如果目标类为接口则使用JDK代理,否则使用CGLIB
		return proxyFactory.getProxy(getProxyClassLoader());
	}

上面源码注释已经写得很清楚,下面进入proxyFactory.getProxy方法,由于ProxyFactory继承ProxyCreatorSupport类,所以进入该方法后可知,真正的实现在ProxyCreatorSupport类中,调用的方法是ProxyCreatorSupport类中的方法,而ProxyFactory只是做了一层封装,

    public Object getProxy(ClassLoader classLoader) {
        // 下面的createAopProxy()方法是ProxyCreatorSupport类中的方法
        return createAopProxy().getProxy(classLoader);
    }

重点来了,下面将浮现jdk和cglib代理的真正面目

createAopProxy()方法返回的是AopProxy接口类型,它有两个实现类,分别是:

  1. CglibAopProxy(通过cglib方式生成代理对象)
  2. JdkDynamicAopProxy(通过JDK动态代理方式生成对象)

AopProxy的作用是用于生成代理对象,稍后将会分析这两种不同的实现方式。

那么,CglibAopProxy或者JdkDynamicAopProxy又是如何生成的呢?进入createAopProxy方法,该方法就是获取AopProxy的地方,由方法可知,这里使用了AopProxyFactory来创建AopProxy,而AopProxyFactory使用的是DefaultAopProxyFactory默认的代理工厂类。

public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {

	@Override
	public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
		// 选择jdk动态代理 或 cglib 代理
		if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
			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.");
			}
			// 如果targetClass是接口类,那么使用JDK来生成代理对象,返回JdkDynamicAopProxy类型的对象
			if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
				return new JdkDynamicAopProxy(config);
			}
			// 否则,返回ObjenesisCglibAopProxy类型的对象,它是使用cglib的方式生成代理对象
			return new ObjenesisCglibAopProxy(config);
		} else {
			return new JdkDynamicAopProxy(config);
		}
	}

扩展:Cglib代理,原理是生成目标类的子类(即这个子类对象就是代理对象),它是在内存中构件一个子类对象,从而实现对目标对象的功能拓展。不管有没有实现接口都可以使用Cglib代理, 而不是只有在无接口的情况下才能使用。具体可以查看《【Java必备】Java代理模式(静态代理、JDK/Cglib动态代理)》

到这里,我们已经基本清楚AopProxy对象是如何生成的了,接下来我们介绍CglibAopProxy和JdkDynamicAopProxy又是如何生成代理对象的?

JDK动态代理

基于接口实现,通过实现目标类所包含的接口的方法来实现代理,即返回的代理对象为接口的实现类。由于是基于JDK的动态代理实现,即实现了JDK提供的InvocationHandler接口,故在运行时在invoke方法拦截目标类对应被代理的接口的所有方法的执行:获取当前执行的方法对应的方法拦截器链,然后通过反射执行该方法时,在方法执行前后执行对应的方法拦截器

JdkDynamicAopProxy#getProxy

	@Override
	public Object getProxy(@Nullable ClassLoader classLoader) {
		if (logger.isDebugEnabled()) {
			logger.debug("Creating JDK dynamic proxy: target source is " + this.advised.getTargetSource());
		}
		Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
		findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
		return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
	}

JdkDynamicAopProxy#getProxy

// JDK的动态代理是基于接口的,故只能代理接口中定义的方法。
// 该类需要通过代理工厂,具体为继承了AdvisedSupport的代理工厂来创建,而不是直接创建,
// 因为AdvisedSupport提供了AOP的相关配置信息,如Advisors列表等。
final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable {

    // 重写InvocationHandler接口的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;
		Object target = null;

		try {
			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);
			} else if (!this.advised.opaque && method.getDeclaringClass().isInterface() &&
					method.getDeclaringClass().isAssignableFrom(Advised.class)) {
				// Service invocations on ProxyConfig with the proxy config...
				return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);
			}

			Object retVal;

			if (this.advised.exposeProxy) {
				// Make invocation available if necessary.
				oldProxy = AopContext.setCurrentProxy(proxy);
				setProxyContext = true;
			}

			// Get as late as possible to minimize the time we "own" the target,
			// in case it comes from a pool.
			target = targetSource.getTarget();
			Class<?> targetClass = (target != null ? target.getClass() : null);

			// 获取该方法对应的方法拦截器列表
			// 实现:通过该方法所在类对应的Advisors,获取该方法的辅助功能Advices列表,即方法拦截器列表。这里的实现为懒加载,
			// 即当方法第一次调用的时候才创建该方法拦截器列表,然后使用一个ConcurrentHashMap缓存起来,之后的方法调用直接使用。

			// 其中advised就是该方法的所在bean对应的ProxyFactory对象引用,通过ProxyFactory来创建AopProxy,即当前类对象实例。
			// Get the interception chain for this method.
			List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

			// Check whether we have any advice. If we don't, we can fallback on direct
			// reflective invocation of the target, and avoid creating a MethodInvocation.
			// 当前执行的方法不包括方法拦截器,即不需要额外的辅助功能,则可以直接执行
			if (chain.isEmpty()) {
				// We can skip creating a MethodInvocation: just invoke the target directly
				// Note that the final invoker must be an InvokerInterceptor so we know it does
				// nothing but a reflective operation on the target, and no hot swapping or fancy proxying.
				Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
				retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
			} else {
				// 如果当前方法包括方法拦截器,即在执行时需要其他额外的辅助功能,则创建ReflectiveMethodInvocation
				// We need to create a method invocation...
				invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
				// Proceed to the joinpoint through the interceptor 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())) {
				// Special case: it returned "this" and the return type of the method
				// is type-compatible. Note that we can't help if the target sets
				// a reference to itself in another returned object.
				retVal = proxy;
			} 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);
			}
		}
	}
。。。。

CGLIB代理

创建目标类的子类来实现代理,代理拦截的只能是目标类的public和protected方法,核心方法为getProxy(),即创建代理对象,在这里织入了辅助功能。

注意:基于类的代理,具体实现为通过创建目标类的子类来实现代理,即代理对象对应的类为目标类的子类。 所以目标类的需要被代理的方法不能为final,因为子类无法重写final的方法;同时被代理的方法需要是public或者protected,不能是static,private或者包可见,即不加可见修饰符。如在事务中,@Transactional注解不能对private,static,final,包可见的方法添加事务功能,只能为public方法。这个代理对象也需要通过代理工厂来创建,具体为继承了AdvisedSupport的代理工厂来创建,而不是直接创建。


class CglibAopProxy implements AopProxy, Serializable {

	// 创建代理对象
	@Override
	public Object getProxy(@Nullable ClassLoader classLoader) {
		if (logger.isDebugEnabled()) {
			logger.debug("Creating CGLIB proxy: target source is " + this.advised.getTargetSource());
		}

		try {
			//获取目标对象
			Class<?> rootClass = this.advised.getTargetClass();
			Assert.state(rootClass != null, "Target class must be available for creating a CGLIB proxy");

			Class<?> proxySuperClass = rootClass;
			if (ClassUtils.isCglibProxyClass(rootClass)) {
				proxySuperClass = rootClass.getSuperclass();
				Class<?>[] additionalInterfaces = rootClass.getInterfaces();
				for (Class<?> additionalInterface : additionalInterfaces) {
					this.advised.addInterface(additionalInterface);
				}
			}

			// Validate the class, writing log messages as necessary.
			validateClassIfNecessary(proxySuperClass, classLoader);

			// 创建目标类的子类来实现代理,即织入辅助功能
			// 创建并配置Enhancer,它是cglib主要的操作类,用于代理对象的生成
			// Configure CGLIB Enhancer...
			Enhancer enhancer = createEnhancer();
			if (classLoader != null) {
				enhancer.setClassLoader(classLoader);
				if (classLoader instanceof SmartClassLoader &&
						((SmartClassLoader) classLoader).isClassReloadable(proxySuperClass)) {
					enhancer.setUseCache(false);
				}
			}
			// 配置enhancer对象,比如 代理接口,父类,回调方法等
			enhancer.setSuperclass(proxySuperClass);
			enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
			enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
			enhancer.setStrategy(new ClassLoaderAwareUndeclaredThrowableStrategy(classLoader));

			Callback[] callbacks = getCallbacks(rootClass);
			Class<?>[] types = new Class<?>[callbacks.length];
			for (int x = 0; x < types.length; x++) {
				types[x] = callbacks[x].getClass();
			}
			// fixedInterceptorMap only populated at this point, after getCallbacks call above
			enhancer.setCallbackFilter(new ProxyCallbackFilter(
					this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset));
			enhancer.setCallbackTypes(types);

			// 通过enhancer来生成代理对象  
			// Generate the proxy class and create a proxy instance.
			return createProxyClassAndInstance(enhancer, callbacks);
		} 
        。。。。
	}

到此,目标对象的代理对象已经生成,并且已经设置好了拦截器(通知),当代理对象调用目标方法时,就会触发这些拦截器。

参考

史上最强Tomcat8性能优化

阿里巴巴为什么能抗住90秒100亿?--服务端高并发分布式架构演进之路

B2B电商平台--ChinaPay银联电子支付功能

学会Zookeeper分布式锁,让面试官对你刮目相看

SpringCloud电商秒杀微服务-Redisson分布式锁方案

查看更多好文,进入公众号--撩我--往期精彩

一只 有深度 有灵魂 的公众号0.0

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值