Spring知识点

Spring的生态

  • spring是spring全家桶的基石

  • 扩展性

扩展性

  • 接口

  • 空的方法实现(模板方法)

  • postProcessor(增强器)---beanFactoryPostProcessor ---beanPostProcessor 两个接口

onRefresh()这个方法在springboot里面做了一个很重要的扩展---内嵌tomcat

AOP术语

连接点

  • 类里那些方法可以被增强,这些方法称为连接点

切入点

  • 实际被增强的方法

通知

前置通知 before

  • 在方法之前执行

后置通知 after

  • 相当于finally里的东西,无论是否有异常都会执行

环绕通知 around

  • 在方法执行前后都会执行

异常通知 afterThrowing

  • 有异常的时候会执行

最终通知 afterReturning

  • 返回通知,当有异常不执行

切面

  • 是一个动作,把通知应用到切入点的过程

切入点表达式

  • *是权限修饰符

  • 返回类型可以不用写

  • add(..)中的..表示方法中的参数

 

AspectJ

  • spring4和springboot1.xx.xx的正常和异常的Aop执行顺序

  • spring5和springboot2.xx.xx的正常和异常的Aop执行顺序

try {
    @AfterReturning
}catch (Exception e){
    @AfterThrowing
}finally {
    @After
}
@Configuration
//ture表示使用cglib代理 默认false表示用jdk代理
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AopConfig {
    
}
//增强的类
@Aspect
@Component
@Order(1)//设置多个增强类的优先级,值越小优先级越高
public class UserProxy {
    @Pointcut(value = "execution(* com.springbootTest.aopanno.UserA.add(..))")
    public void pointcut() {
    }
​
    @Before(value = "pointcut()")
    public void before(JoinPoint joinPoint) {
        System.out.println("在方法之前执行......");
        System.out.println("目标方法名为:" + joinPoint.getSignature().getName());
        System.out.println("目标方法所属类的简单类名:" + joinPoint.getSignature().getDeclaringType().getSimpleName());
        System.out.println("目标方法所属类的类名:" + joinPoint.getSignature().getDeclaringTypeName());
        System.out.println("目标方法声明类型:" + Modifier.toString(joinPoint.getSignature().getModifiers()));
        //获取传入目标方法的参数
        Object[] args = joinPoint.getArgs();
        for (int i = 0; i < args.length; i++) {
            System.out.println("第" + (i + 1) + "个参数为:" + args[i]);
        }
        System.out.println("被代理的对象:" + joinPoint.getTarget());
        System.out.println("代理对象自己:" + joinPoint.getThis());
​
    }
​
    //有没有异常都执行
    @After(value = "pointcut()")
    public void after() {
        System.out.println("在方法之后执行......");
    }
​
    @Around(value = "pointcut()")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("环绕之前执行......");
        joinPoint.proceed();
        System.out.println("环绕之后执行......");
    }
​
    @AfterThrowing(value = "pointcut()")
    public void afterThrowing() {
        System.out.println("在方法抛出异常执行......");
    }
​
    //当有异常不执行
    @AfterReturning(value = "pointcut()")
    public void afterReturning() {
        System.out.println("在方法返回值之后执行......");
    }
}

实例化和初始化

  • 实例化:在堆中申请内存空间,属性都是默认值

  • 初始化:给对象的属性进行赋值操作或者初始化方法的调用

循环依赖

  • 尽可能不要使用构造方法注入,这样会很难解决循环依赖,因为两个对象都没有new出来,二者谁都不会创建

  • 只有单例的bean才会放入三级缓存中来解决循环依赖的问题,而非单例的bean,每次从容器中获取的都是一个新的对象,都会重新创建,所以非单例的bean是没有缓存的,不会将其放入三级缓存中。

DefaultSingletonBeanRegistry

ObjectFactory----->是函数式接口
  • 一级缓存:存放成品对象

  • 二级缓存:半成品对象

  • 三级缓存: lambda表达式

为什么一级缓存容量大,二三级缓存容量少

  • 因为二三级缓存会删的,当一个对象完成之后就把对象删掉,算个临时存储的空间

如果只有一个缓存,能否解决循环依赖的问题

  • 不能,如果成品对象和半成品对象放到一起,那么从缓存中取出对象的时候可能获取到半成品对象,半成品对象不能直接暴露给外界使用的。

如果只有两个缓存,能否解决循环依赖的问题

  • 二级缓存可以解决部分循环依赖问题,但是循环依赖过程中包含了代理对象的创建,那么必须要使用三级缓存了。

为什么使用三级缓存可以解决这个问题

  • 在容器中不能创建两个同名的不同对象

  • 当需要创建代理对象的时候,原始对象一定被创建出来了,容器中一定包含原始对象

  • 如果原始对象有了,那么代理对象创建出来之后,对外暴露的是原始对象还是代理对象? 当需要对外暴露对象的时候,必须要确定一件事,到底要暴露原始对象还是代理对象,如果暴露代理对象的话就要将原始对象覆盖调。

  • 你怎么知道什么时候需要对外暴露呢?怎么确定何时暴露呢? 当需要被引用的时候,来判断当前对象是否需要被代理,所以,引入lambda表达式的时候,getEarlyBeanReference方法里面就来确定对外暴露的是原始对象还是代理对象在jdk1.8之前用的是匿名内部类

执行顺序

构造器产生的循环依赖问题是无解的

Bean的生命周期

  1. Bean容器找到Spring配置文件中Bean的定义;

  2. Bean容器利用java 反射机制实例化Bean;

  3. Bean容器为实例化的Bean设置属性值;

  4. 如果Bean实现了BeanNameAware接口,则执行setBeanName方法;

  5. 如果Bean实现了BeanClassLoaderAware接口,则执行setBeanClassLoader方法;

  6. 如果Bean实现了BeanFactoryAware接口,则执行setBeanFactory方法;

  7. 如果Bean实现了EnvironmentAware接口,则执行setEnvironment方法

  8. 如果Bean实现了ResourceLoaderAware接口,则执行setResourceLoader方法

  9. 如果Bean实现了ApplicationEventPublisherAware接口,则执行setApplicationEventPublisher方法

  10. 如果Bean实现了ApplicationContextAware接口,则执行setApplicationContext方法;

  11. 如果加载了BeanPostProcessor相关实现类,则执行postProcessBeforeInitialization方法;

  12. 如果Bean定义初始化方法(PostConstruct注解、配置init-method、实现了InitializingBean接口),则执行定义的初始化方法;

  13. 如果加载了BeanPostProcessor相关实现类,则执行postProcessAfterInitialization方法;

  14. 当要销毁这个Bean时,如果自定义了销毁方法(PreDestroy注解、配置destroy-method、实现了DisposableBean接口),则执行定义的销毁方法。

简单描述一下Bean的生命周期(源码版)

Spring容器帮我们去管理对象,从对象的生产和销毁环节都是由容器控制的,其中主要包括

实例化和初始化的两个关键环节,当然在整个过程中会存在一些扩展点:

  • 实例化bean对象,通过反射的方法来创建对象,在源码中有一个方法叫createBeanInstance的方法专门来生成对象的

  • 当bean对象创建完成之后,对象的属性值都是默认值,所以要给bean属性填充,通过populateBean方法来完成对象属性的填充,这里会出现循环依赖的问题。

  • 向bean对象中设置容器属性的时候,会调用invokeAwareMethods方法来将容器对象设置到bean对象中。

  • 调用BeanPostProcessor中的前置处理方法来对bean对象进行扩展工作,像ApplicationContextPostProcessor、EmbeddValueResolver

  • 如果这个bean实现了InitializingBean接口,就调用afterPropertiesSet方法,之后执行init-method方法。

  • 调用BeanPostProcessor的后置处理方法,完成对Bean对象的后置处理工作,AOP就是在此实现的,实现的接口的名字叫做AbstractAutoProxyCreator

  • 获得完整的bean对象,通过getBean方法来获取使用

  • 当对象使用完成之后,在容器关闭的时候,会销毁对象,首先会判断是否实现了DispableBean接口,然后去调用destroyMethod方法。

BeanDefinition

xml文件加载过程

protected abstract void loadBeanDefinitions(DefaultListableBeanFactory var1) throws BeansException, IOException;
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
        Assert.notNull(encodedResource, "EncodedResource must not be null");
        if (this.logger.isInfoEnabled()) {
            this.logger.info("Loading XML bean definitions from " + encodedResource.getResource());
        }
​
        Set<EncodedResource> currentResources = (Set)this.resourcesCurrentlyBeingLoaded.get();
        if (currentResources == null) {
            currentResources = new HashSet(4);
            this.resourcesCurrentlyBeingLoaded.set(currentResources);
        }
​
        if (!((Set)currentResources).add(encodedResource)) {
            throw new BeanDefinitionStoreException("Detected cyclic loading of " + encodedResource + " - check your import definitions!");
        } else {
            int var5;
            try {
                InputStream inputStream = encodedResource.getResource().getInputStream();
​
                try {
                    InputSource inputSource = new InputSource(inputStream);
                    if (encodedResource.getEncoding() != null) {
                        inputSource.setEncoding(encodedResource.getEncoding());
                    }
​
                    var5 = this.doLoadBeanDefinitions(inputSource, encodedResource.getResource());
                } finally {
                    inputStream.close();
                }
            } catch (IOException var15) {
                throw new BeanDefinitionStoreException("IOException parsing XML document from " + encodedResource.getResource(), var15);
            } finally {
                ((Set)currentResources).remove(encodedResource);
                if (((Set)currentResources).isEmpty()) {
                    this.resourcesCurrentlyBeingLoaded.remove();
                }
​
            }
​
            return var5;
        }
    }

BeanFactoryPostProcessor

  • 对beanFactory对象进行扩展实现

  • beanFactory是程序访问的一个入口

  • 将BD对象的一些占位符替换成真正的值

对象的创建

BeanPostProcessor

  • @AotoWired不能注入BeanPostProcessor类和BeanFactoryPostProcessor类,因为注入过程是在Bean的BeanPostProcessor中执行的

  • 对bean对象进行扩展实现

  • AOP在生成动态代理对象的时候就是扩展

  • 针对的操作对象不同

private void invokeAwareMethods(String beanName, Object bean) {
    //你的bean是否实现了Aware接口
    if (bean instanceof Aware) {
        //如果bean是BeanNameAware实例
        if (bean instanceof BeanNameAware) {
            ((BeanNameAware)bean).setBeanName(beanName);
        }
        //如果bean是BeanClassLoaderAware实例
        if (bean instanceof BeanClassLoaderAware) {
            //获取次工厂的类加载器以加载Bean类(即使无法使用系统ClassLoader,也只             能为空)
            ClassLoader bcl = this.getBeanClassLoader();
            if (bcl != null) {
                ((BeanClassLoaderAware)bean).setBeanClassLoader(bcl);
            }
        }
        //如果bean是BeanFactoryAware实例  
        if (bean instanceof BeanFactoryAware) {
            ((BeanFactoryAware)bean).setBeanFactory(this);
        }
    }
​
}

refresh

@Override
public void refresh() throws BeansException, IllegalStateException {
   // 来个锁,不然 refresh() 还没结束,你又来个启动或销毁容器的操作,那不就乱套了嘛
   synchronized (this.startupShutdownMonitor) {
​
      // 准备工作,记录下容器的启动时间、标记“已启动”状态、处理配置文件中的占位符
      prepareRefresh();
​
      // 这步比较关键,这步完成后,配置文件就会解析成一个个 Bean 定义,注册到 BeanFactory 中,
      // 当然,这里说的 Bean 还没有初始化,只是配置信息都提取出来了,
      // 注册也只是将这些信息都保存到了注册中心(说到底核心是一个 beanName-> beanDefinition 的 map)
      ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
​
      // 设置 BeanFactory 的类加载器,添加几个 BeanPostProcessor,手动注册几个特殊的 bean
      // 这块待会会展开说
      prepareBeanFactory(beanFactory);
​
      try {
         // 【这里需要知道 BeanFactoryPostProcessor 这个知识点,Bean 如果实现了此接口,
         // 那么在容器初始化以后,Spring 会负责调用里面的 postProcessBeanFactory 方法。】
​
         // 这里是提供给子类的扩展点,到这里的时候,所有的 Bean 都加载、注册完成了,但是都还没有初始化
         // 具体的子类可以在这步的时候添加一些特殊的 BeanFactoryPostProcessor 的实现类或做点什么事
         postProcessBeanFactory(beanFactory);
         // 调用 BeanFactoryPostProcessor 各个实现类的 postProcessBeanFactory(factory) 方法
         invokeBeanFactoryPostProcessors(beanFactory);
​
         // 注册 BeanPostProcessor 的实现类,注意看和 BeanFactoryPostProcessor 的区别
         // 此接口两个方法: postProcessBeforeInitialization 和 postProcessAfterInitialization
         // 两个方法分别在 Bean 初始化之前和初始化之后得到执行。注意,到这里 Bean 还没初始化
         registerBeanPostProcessors(beanFactory);
​
         // 初始化当前 ApplicationContext 的 MessageSource,国际化这里就不展开说了,不然没完没了了
         initMessageSource();
​
         // 初始化当前 ApplicationContext 的事件广播器,这里也不展开了
         initApplicationEventMulticaster();
​
         // 从方法名就可以知道,典型的模板方法(钩子方法),
         // 具体的子类可以在这里初始化一些特殊的 Bean(在初始化 singleton beans 之前)
         onRefresh();
​
         // 注册事件监听器,监听器需要实现 ApplicationListener 接口。这也不是我们的重点,过
         registerListeners();
​
         // 重点,重点,重点
         // 初始化所有的 singleton beans
         //(lazy-init 的除外)
         finishBeanFactoryInitialization(beanFactory);
​
         // 最后,广播事件,ApplicationContext 初始化完成
         finishRefresh();
      }
​
      catch (BeansException ex) {
         if (logger.isWarnEnabled()) {
            logger.warn("Exception encountered during context initialization - " +
                  "cancelling refresh attempt: " + ex);
         }
​
         // Destroy already created singletons to avoid dangling resources.
         // 销毁已经初始化的 singleton 的 Beans,以免有些 bean 会一直占用资源
         destroyBeans();
​
         // Reset 'active' flag.
         cancelRefresh(ex);
​
         // 把异常往外抛
         throw ex;
      }
​
      finally {
         // Reset common introspection caches in Spring's core, since we
         // might not ever need metadata for singleton beans anymore...
         resetCommonCaches();
      }
   }
}

BeanFactory和FactoryBean

  • FactoryBean 可以让我们自定义Bean的创建过程 ,可以说为IOC容器中Bean的实现提供了更加灵活的方式,FactoryBean在IOC容器的基础上给Bean的实现加上了一个简单工厂模式和装饰模式,我们可以在getObject()方法中灵活配置。

  • getBean("&factoryBean")获取的并不是factoryBean对象,而是调用factoryBean.getObject()方法返回的对象。

ApplicationContext和BeanFactory区别

BeanFactory是访问spring容器的根接口,里面只是提供了某些基本方法的约束和规范,为了满足更多的需求,ApplicationContext实现了BeanFactory接口,并在此接口之上做了某些扩展功能,提供了更多丰富的api调用。而一般情况下我们使用的是ApplicationContext更多。

AOP底层实现原理

动态代理,aop是ioc的一个扩展功能,先有ioc,再有aop,aop只是在ioc的整个流程中新增的一个扩展点而已:BeanPostProcessor

总:aop的概念,应用场景,动态代理

分:bean的创建过程中有一个步骤可以对bean进行扩展实现,而aop本身就是一个扩展功能,在beanPostProcessor的后置处理方法中进行扩展实现。

1、代理对象的创建过程(通知Advice,切面,切点)

2、通过JDK或者cglib的方式来生成代理对象

3、在执行方法调用的时候,会调用到生成的字节码文件中,直接会找到DynamicAdvisoredInterceptor类中的intercept方法,从此方法开始执行

4、根据之前定义好的通知来生成拦截器链

5、从拦截器链中依次获取每一个通知开始进行执行,在执行过程中,为了方便找到下一个通知是哪个,会有一个InvocationInterceptor的对象,找的时候是从-1的位置一次开始查找并且执行的。

Spring的事务是如何回滚的

spring的事务是如何实现的

总:spring的事务是由aop实现的,首先要生成具体的代理对象,然后按照aop的整套流程来执行具体的操作逻辑,正常情况下要通过通知来完成核心功能,但是事务并不是通过通知来实现的,而是通过一个TransactionInterceptor来实现的,然后调用invoke来实现具体的逻辑

分:1、先做准备工作,解析各个方法上事务相关的属性,根据具体的属性来判断是否开启新事物

2、当需要开启的时候,获取数据库连接,关闭自动提交功能,开启事务

3、执行具体的sql逻辑操作

4、在操作过程中如果执行失败了,那么会通过completeTransactionAfterThrowing来完成事务的回滚操作,回滚的具体逻辑是通过doRollBack方法实现的,实现的时候也是要先获取连接对象,通过连接对象来回滚,底层是调用的jdbc的rollBack方法来进行回滚的。

5、如果执行过程中,没有任何意外情况的发生,那么会通过commitTransactionAfterReturning来完成事务的提交操作,提交的具体逻辑是通过doCommit方法来实现的,实现的时候也是先获取连接,通过连接对象来提交,底层调用的是jdbxc的commit方法来进行提交的。

6、当事务执行完毕之后,需要清除相关的事务信息cleanupTransactionInfo

Spring的事务传播

传播特性  7种

Required,Requires_new,nested,Support,Not_Support,Never,Mandatory

@Transactional(propagation=Propagation.REQUIRED) 如果有事务, 那么加入事务, 没有的话新建一个(默认情况下)

@Transactional(propagation=Propagation.NOT_SUPPORTED) 容器不为这个方法开启事务

@Transactional(propagation=Propagation.REQUIRES_NEW) 不管是否存在事务,都创建一个新的事务,原来的挂起,新的执行完毕,继续执行老的事务

@Transactional(propagation=Propagation.MANDATORY) 必须在一个已有的事务中执行,否则抛出异常

@Transactional(propagation=Propagation.NEVER) 必须在一个没有的事务中执行,否则抛出异常(与Propagation.MANDATORY相反)

@Transactional(propagation=Propagation.SUPPORTS) 如果其他bean调用这个方法,在其他bean中声明事务,那就用事务.如果其他bean没有声明事务,那就不用事务.

PROPAGATION_NESTED

如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。

某一个事务嵌套另一个事务怎么办?

A方法调用B方法,AB方法都有事务,并且传播特性不同,那么A如果有异常B怎么办,B如果有异常A怎么办?

总:事务的传播特性指的是不同方法的嵌套调用过程中,事务应该如何进行处理,是用同一个事务还是不同的事务,当出现异常的时候会回滚还是提交,两个方法之间的相互影响,在日常工作中,使用的比较多的是Required,Requires_new,nested

分:1、先说事务的不同分类,可以分为三类:支持当前事务,不支持当前事务,嵌套事务

2、如果外层方法是Required,内层方法是Required,Requires_new,nested

3、如果外层方法是Requires_new,内层方法是Required,Requires_new,nested

4、如果外层方法是nested,内层方法是Required,Requires_new,nested

1、判断内外方法是否是同一个事务:

是,统一在外层方法中处理

不是,内层方法有可能影响到外层方法,但是外层方法是不会影响内层方法的

 

@AutoWired源码

  • 1.属性方式注入 2.普通非静态方法的注入 3.构造方法的注入

@AutoWired注入分为两步,第一步是找到注入点,然后放入缓存,第二步是从缓存获取注入点进行注入,spring中的依赖注入@AutoWired也是通过AutowiredAnnotationBeanPostProcessor这个bean后置处理器实现的,这个后置处理器中提供了两个方法,spring分别是在实例化后初始化前调用,这两个方法是postProcessMergedBeanDefinition和postProcessProperties,其中postProcessMergedBeanDefinition是找到bena的注入点,而postProcessProperties是进行注入,先来看下如何找到注入点的源码,注入点的源码是在doCreateBean中实现的

不加@AutoWired进行注入

@Component
public class CBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        GenericBeanDefinition c = (GenericBeanDefinition) beanFactory.getBeanDefinition("c");
        c.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_NAME);
        System.out.println("我们拿到了c的定义" + c.getBeanClassName());
    }
}

Spring用到了那些设计模式

适配器模式

  • 首先,将所有的适配器放到一个集合中,当要使用适配器的时候,遍历集合找出相对应的适配器,将该适配器返回给用户,用户执行适配器中的方法传入需要适配的类型,返回需要的类型

  • 每一个通知都对应一个拦截器,Spring需要将具体的通知封装成拦截器,返回给容器,这里对advice的转换就用到了适配器模式

  • 想一下,你定义的advice通知方法,最终是不是要通过拦截器去执行的

public interface AdvisorAdapter {
	// 判断通知类型是否匹配
	boolean supportsAdvice(Advice advice);
	// 获取对应的拦截器
	MethodInterceptor getInterceptor(Advisor advisor);

}


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);
	}
}
  • 在Spring MVC中,DispatcherServlet作为用户,HandlerAdapter作为期望接口,具体的适配器实现类用于对目标类进行适配,Controller作为需要适配的类

  • 当Spring容器启动后,会将所有定义好的适配器对象存放在一个List集合中,当一个请求来临时,DispatcherServlet会通过handler的类型找到对应适配器,并将该适配器对象返回给用户,然后就可以统一通过适配器的hanle()方法来调用Controller中的用于处理请求的方法。

public interface HandlerAdapter {  

    boolean supports(Object handler);

    ModelAndView handle(HttpServletRequest request, HttpServletResponse 

    response, Object handler) throws Exception;  
}

单例模式

  • spring的bean模式都是单例的 singleton

简单工厂

  • spring中的BeanFactory就是简单工厂模式的体现,传入一个唯一的标识来获取bean对象

工厂方法

  • 通常由应用程序直接使用new创建新的对象,为了将对象的创建和使用相分离,采用工厂模式,即应用程序将对象的创建及初始化职责交给工厂对象。

  • 实现了FactoryBean接口的bean是一类叫做factory的bean。其特点是,spring会在使用getBean()调 用获得该bean时,会自动调用该bean的getObject()方法,所以返回的不是factory这个bean,而是这个 bean.getOjbect()方法的返回值。

动态代理

  • 切面在应用运行的时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象创建动态的创建一个代理 对象。SpringAOP就是以这种方式织入切面的。

织入:把切面应用到目标对象并创建新的代理对象的过程。

模板方法

  • spring启动流程有个onRefresh()方法是空方法,留给子类来重写,在springboot中来内嵌tomcat。

装饰器模式

  • Spring中用到的包装器模式在类名上有两种表现:一种是类名中含有Wrapper,另一种是类名中含有 Decorator。

观察者模式

  • spring的事件驱动模型使用的是 观察者模式,listener的实现

策略模式

  • 策略模式作为一种软件设计模式,指对象有某个行为,但是在不同的场景中,该行为有不同的实现算法,可以替代代码中大量的 if-else。

  • 比如我们生活中的场景:买东西结账可以使用微信支付、支付宝支付或者银行卡支付,这些交易方式就是不同的策略。

  • 在《阿里巴巴Java开发手册》中有提到当超过 3 层的 if-else 的逻辑判断代码可以使用策略模式来实现。

什么时候@Transactional失效

  • 因为Spring的事务是基于代理来实现的,所以加上@Transactional注解的方法只有被代理类调用的时候事务才会生效,不是代理类调用的话不会生效

  • 如果某个方法是private的,那么@Transactional会失效,因为底层Cglib是依靠生成子类来代理对象的,对于private修饰的方法,子类不能够去继承的,也就无法生效

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值