Java 手写 Spring框架 IOC 和 AOP---SpringIoC高级应用面试知识点,源码

源码(码云):https://gitee.com/yin_zhipeng/implement-_spring_of_myself.git

一、手写Spring

篇幅限制,我将其放在这篇文章中:https://blog.csdn.net/grd_java/article/details/122625811

二、Spring IoC高级应用面试常问知识点复习

通过手写Spring,我们已经对spring思想有了基本了解,接下来进行高级应用讲解,在此之前,我们依然对一些基本概念做些回顾,同样是面试官经常问到的一些东西
IoC配置bean实例的3种方式
  1. 纯xml,bean信息全部定义在xml文件中,我们可以通过如下方式,指定读取xml
//JavaSE:
AppliccationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
AppliccationContext applicationContext = new FileSystemXmlApplicationContext("beans.xml");
//Java Web 可以使用监听器加载xml
ContextLoaderListener
  1. 纯注解,所有bean都用注解定义,加载方法如下
//JavaSE SpringConfig.class注解配置类的class对象
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
//Java Web 监听器去加载注解配置类
ContextLoaderListener
  1. xml+注解,部分bean配置在xml中,部分bean使用注解定义(加载方法,是xml和注解混合)
BeanFactory和ApplicationContext的区别
  1. 前面我们手写spring,使用BeanFactory生产bean,那么spring怎么做呢?
  2. 这里有一张spring的BeanFactory继承关系图,发现BeanFactory是IoC容器的顶层接口,而ApplicationContext是子接口,所以ApplicationContext的功能更强(国际化支持,资源访问例如xml,java配置类),我们常用ApplicationContext
    在这里插入图片描述
  3. 可以发现,spring层次划分的很多,功能分工明确,没必要把所有功能都划分到BeanFactory
  4. BeanFactory就像一个领导,它不可能把所有细节的事都做了,它可以掌控大局,但完成整套工作,需要很多人才相互配合
测试环境说明
  1. 我们将手写spring的例子复制一份,将其改造为使用真正的Spring
    在这里插入图片描述

1. springIoC基本概念和使用

使用spring就得引入基本模块依赖,context,包含core、beans、aop和expression
  1. 引入相关依赖
    在这里插入图片描述

1.1 纯xml模式

1.1.1 JavaEE版
xml我们一直自己定义,虽然遵循spring规范,但是是我们人为模仿,我们需要引入spring的约束
  1. 给xml文件改名为applicationContext.xml(想不想改都行,只不过大家都叫这名字),然后添加xml约束
    在这里插入图片描述
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">
  1. 使用spring IoC获取bean实例
    在这里插入图片描述
1.1.2 web使用监听器版
1. 先将我们工程改造为web工程
  1. 引入tomcat依赖和servlet依赖,让maven打包方式为war
    在这里插入图片描述
    在这里插入图片描述
  2. 编写Servlet,启动tomcat,然后访问8080端口Servlet
    在这里插入图片描述
2. 引入spring-web模块依赖,编写web.xml
  1. 引入spring-web依赖
    在这里插入图片描述
  2. 编写web.xml之前,我们知道我们通过ContextLoaderListener这个监听器来创建bean,那么它其实有一些参数,比如我们可以指定它加载哪个xml配置文件,如下:
    在这里插入图片描述
  3. 接下来我们编写web.xml(自己创建相关目录和文件即可),指定监听器,并且指定属性,告诉监听器加载那个配置文件
    在这里插入图片描述
<!DOCTYPE web-app PUBLIC
        "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
    <display-name>Archetype Creadted Web Application</display-name>
    <!--ContextLoaderListener的一个属性,指定容器的配置文件位置-->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:applicationContext.xml</param-value>
    </context-param>

    <!--使用监听器启动SpringIoC容器-->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

</web-app>
3. 改造Servlet 通过监听器获取bean,然后测试效果

在这里插入图片描述
在这里插入图片描述

1.1.3 Bean标签属性
1.1.3.1 Bean的常用创建方式
创建实例的不同方式:先前都使用无参构造来创建,下面给出不同方式
  1. 通过静态方法获取bean实例,一般配合工厂设计模式
    在这里插入图片描述
    在这里插入图片描述
<bean id="connectionUtils" class="spring.factory.CreateBeanFactory" factory-method="getInstanceStaticConnectionUtils"></bean>
  1. 通过非静态方法,一般配合工厂设计模式
    在这里插入图片描述
    在这里插入图片描述
1.1.3.2 Bean的作用范围以及生命周期
Bean的作用范围可变,spring框架管理Bean对象的创建,默认使用单例模式创建,它支持配置的方式改变作用范围
Scope(🍅:不常用/🏆:常用)Description
🏆singleton单例,使用单例模式创建的实例,整个IoC容器中就一个,默认方式
🏆prototype原型(多例)模式,使用原型设计模式创建的实例,每次想要bean,都会拷贝给你一个新的bean
🍅requestbean实例会在客户端每次进行Http请求时创建,在WebApplicationContext环境下使用,范围在一个请求中
🍅sessionbean会在客户端与服务器的每一次会话中创建一个bean,不同会话创建不同对象,同一会话始终使用同一对象,范围在一个会话中
🍅application范围在ServletContext的声明周期中,只有在web的Spring application上下文中有效
🍅websocket范围在WebSocket,只在web-aware Spring ApplicationContext的上下文中有效
我们指定类ConnectionUtils为单例模式,启动tomcat后,查看两次bean是不是一个
  1. 指定bean的范围为单例(不指定scope属性,默认就是单例,所以这一步不配置也可以)
    在这里插入图片描述
  2. 测试
    在这里插入图片描述
我们指定类ConnectionUtils为原型模式,启动tomcat后,查看两次bean是不是一个
  1. 指定为原型模式,然后测试
    在这里插入图片描述
单例模式和原型模式bean的生命周期(其它类型的bean压根就用不到,就不说了)
  1. 单例singleton,生命周期和IoC容器相同
  1. 对象出生:当创建容器时,对象被创建
  2. 对象活动范围:只要容器还在,对象就会一直活这
  3. 对象死亡:容器销毁时,对象被销毁
  1. 多例prototype,使用时,IoC容器创建,多会销毁,取决于java垃圾回收器多会回收它
  1. 对象出生:使用对象时,创建新的对象实例
  2. 对象活动范围:只要对象在使用,就一直活着
  3. 对象死亡:被java垃圾回收器回收(没有引用指向,或者满足垃圾回收算法的条件,比如长时间不使用)
1.1.3.3 Bean标签属性
property(🍅:不常用/🏆:常用)Description
🏆id属性用于给bean提供一个唯一标识,一个< beans>标签内部,标识必须唯一
🏆class属性用于创建Bean对象的全限定类名
🍅name属性用于给Bean提供一个或多个名称,多个名称用空格分隔
🍅factory-bean属性用于指定创建当前bean对象的工厂bean的唯一标识,指定此属性后,class属性失效
🍅factory-method属性指定创建当前bean对象的工厂方法,配合factory-bean属性使用时,class属性失效,配合class属性使用时,方法必须是static
🍅scope属性用于指定bean对象的作用范围,默认singleton单例模式,常用的还有prototype原型模式
🏆init-method属性用于指定bean对象的初始化方法,此方法会在bean对象装配后调用,必须是一个无参方法
🍅destory-method属性用于指定bean对象的消耗方法,此方法会在bean对象销毁前执行,只能为scope是singleton时起作用
init-method和destory-method属性,是两个生命周期属性,可以在bean创建前和销毁前执行
  1. 在ConnectionUtils类中定义两个无参方法
    在这里插入图片描述
  2. 配置xml
    在这里插入图片描述
  3. 测试,销毁方法,需要调用IoC容器的close方法来关闭,只有部分子类有,也不常用就不测试了
    在这里插入图片描述

1.2 纯xml模式的DI依赖注入

依赖注入分类
  1. 两种常用注入方式
  1. 构造函数注入:利用带参构造函数实现对类成员的数据赋值
  2. set方法注入:通过类成员的set方法实现数据的注入(使用最多)
  1. 按照注入的数据类型
  1. 基本类型和String:注入的数据类型是基本类型或者是字符串类型的数据
  2. 其他Bean类型:注入的数据类型是对象类型,称为其他Bean的原因是,这个对象是要求出现在IoC容器中的,那么针对当前Bean来说,就是其他Bean了。
  3. 复杂类型(集合类型):注入的数据类型时Array,List,Set,Map,Properties中的一种类型
我们先前使用的都是set注入的方式,注入的数据类型是其他Bean类型,如果是基本类型或String,将ref换成value,里面输入值,例如value = “100.3”,很简单不过多介绍了

在这里插入图片描述

1.2.1 构造函数注入
  1. 为ConnectionUtils类添加几个参数,提供构造方法
    在这里插入图片描述
  2. xml注入
    在这里插入图片描述
1.2.2 复杂(集合)类型
  1. 为ConnectionUtils添加复杂类型变量个set方法
    在这里插入图片描述
  2. xml注入(同样普通值用value,引用类型用ref,map的类型统一是entry,entry中的key和value如果是引用类型,加ref前缀即可,例如key-ref)
    在这里插入图片描述

1.3 xml+注解模式

xml+注解,spring IoC容器启动依然从加载xml开始,下面介绍xml中标签对应的注解
  1. 自己写的类,用注解
  2. 第三方jar中的bean配置到xml中
xml(🍅:不常用/🏆:常用)Annontation
🏆< bean id = “connectionUtils”>标签@Component(“connectionUtils”),加在类上,id属性是value(默认一个参数就是给它赋值),如果不配置,id默认为类名(第一个单词首字母小写,其它首字母大写),另外针对分层开发,还提供了3种别名:@Controller@Service@Repository,分别用于控制层,服务层,持久层的bean定义,4个注解功能用法完全一样,只是为了区分清晰
🏆DI 依赖注入@Autowired,用于属性(变量)上,采用策略为,按照类型注入,但是如果一个类型有多个bean值时,会不知道选哪个
🏆DI 依赖注入,告诉Spring具体装配哪个bean@Qualifier(“bean的id”),配合@Autowired使用,告诉Spring具体装配哪个bean实例
🍅Java原生注解@Resource,和@Autowired相同,java原生注解,但是在JDK11移除了,可以同时指定id和类型,和@Autowired一样,如果写在字段上,不用提供set方法,也可直接写在setter方法上
🍅< bean >标签的scope属性Scope(“prototype”),加在类上,默认单例
🍅< bean >标签的init-method属性PostConstruct(),加在方法上,标识该方法为bean初始化后调用方法
🍅< bean >标签的destory-method属性PreDestory(),加在方法上,标识该方法为bean销毁前调用方法
1.3.1 使用xml+注解模式改造工程
1. 第三方jar配置在xml,其它都用注解
  1. xml中配置第三方jar
    在这里插入图片描述
    在这里插入图片描述
  2. 使用注解配置ConnectionUtils
    在这里插入图片描述
  3. 使用注解配置TransactionManager
    在这里插入图片描述
  4. 使用注解配置proxyFactory
    在这里插入图片描述
  5. 使用注解配置dao层
    在这里插入图片描述
  6. 使用注解配置service
    在这里插入图片描述
2. web.xml配置使用新的xml文件

在这里插入图片描述

3. 开启注解驱动

在这里插入图片描述

<?xml version="1.0" encoding="UTF-8" ?>
<!--beans 根标签,里面有若干个bean子标签-->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:comtext="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd
">

    <!--开启注解扫描,base-package指定扫描的包路径-->
    <comtext:component-scan base-package="com.yzpnb.advanced_application"></comtext:component-scan>
    <comtext:component-scan base-package="spring"></comtext:component-scan>
    <!--第三方bean配置到xml中-->
    <bean id="datasource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"></property>
        <property name="url" value="jdbc:mysql://localhost:3306/bank"></property>
        <property name="username" value="root"></property>
        <property name="password" value="123456"></property>
    </bean>
</beans>
4. 测试

在这里插入图片描述

1.3.2 引入外部文件配置
通常,我们将数据库配置,都放在单独一个配置文件中

在这里插入图片描述

然后xml中引入外部文件使用

在这里插入图片描述

1.4 纯注解模式

纯注解模式,完全不需要xml了,和使用springboot时基本相同,基本注解如下
Description(🍅:不常用/🏆:常用)Annontation
🏆标识当前类是一个配置类@Configuration
🏆替代context:compoent-scan,开启注解驱动@ComponentScan
🏆引入外部配置文件@PropertySource
🏆重点🏆引入其他配置类,如果配置类有多个,可以用这个注解,统一引入到一个配置类中@Import
🏆对变量赋值,可以直接赋值,或者使用${}读取资源配置文件中信息@Vlaue
🏆将方法返回对象加入SpringIoC容器中@Bean
那么spring毕竟不是springboot,没有启动类,我们将这些注解全部配置到配置类上
  1. 创建一个SpringConfiguration配置类,用@Configuration注解标识
    在这里插入图片描述
  2. 使用@ComponentScan注解,替代context:compoent-scan,开启注解驱动
    在这里插入图片描述
  3. 使用@PropertySource注解引入外部资源文件
    在这里插入图片描述
  4. 使用@Vlaue‘注解’注入配置文件中的值,使用@Bean()注解,配置第三方jar的bean实例
    在这里插入图片描述
没有启动类,我们怎么加载配置类,然后使用我们的配置呢
  1. javaEE版
    在这里插入图片描述
  2. web版:纯注解已经不需要xml了,web程序需要监听器,做如下更改,监听器配置保留,并指定监听器使用注解配置类,而且指定xml配置文件的配置需要改变,改为指定配置类
    在这里插入图片描述
  3. 测试
    在这里插入图片描述

2. spring 高级特性

2.1 lazy-Init延迟加载

一种可以在一定程度上提高容器启动和运转性能的机制,对于不常用的bean设置懒加载,偶尔使用时再加载,没必要一开始这个bean就占用资源
Bean的延迟加载(延迟创建),就是懒加载,按需加载,第一次向容器索取bean的时候,才会实例化
  1. ApplicationContext容器的默认行为是启动服务器时将所有singleton bean提前进行实例化,意味着实例化作为初始化过程的一部分,ApplicationContext实例会创建并配置所有的singleton bean。
  2. 如果不指定lazy-Init参数,则默认为false,不进行懒加载
<!--不指定或者指定lazy-Init为false时,表示不开启懒加载,spring容器启动时,立即进行实例化-->
<bean id="testBean" class = "com.yzpnb.testBean" />
等价于
<bean id="testBean" class = "com.yzpnb.testBean" lazy-init="false"/>

<!--开启懒加载,只有需要的时候才进行实例化-->
<bean id="testBean" class = "com.yzpnb.testBean" lazy-init="true"/>
  1. 什么时候是需要一个bean的时候,当我们getBean的时候,或者bean1引用了开启懒加载的bean2,bean1实例化时,bean2也会实例化
统一给所有bean都设置懒加载
  1. < beans>标签上使用default-lazy-init属性,来控制< bean>标签的懒加载
<!--容器层次,控制懒加载初始化-->
<beans default-lazy-init = "true">
	<!--经管没指定lazy-init="true",它也开启了懒加载,因为<beans>标签统一设置了-->
	<bean id="testBean" class = "com.yzpnb.testBean" />
</beans>
通过注解@lazy
@Component("connectionUtils")
@Lazy
public class ConnectionUtils {
}
做个简单测试,其实它已经初始化了,只不过放在了一个map中(缓存),需要的时候,从缓存中搞出来,后面看源码会讲

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

通过注解呢?
  1. 加注解
    在这里插入图片描述
  2. 测试:因为我们配置类中,有其它bean依赖于connectionUtils,你可以自己创建一个单独的bean测试,这里我只告诉方法,后面会重点讲源码
    在这里插入图片描述

2.2 FactoryBean和BeanFactory

BeanFactory我们前面已经提到了,是IoC容器的顶级接口,定义了容器的一些基础行为,负责生产和管理Bean的一个工厂,具体使用它下面的子接口类型,例如ApplicationContext
FactoryBean:工厂bean,spring中有普通bean和工厂bean两种,FactoryBean可以生成某一类型的Bean实例返回给我们,也就是说,我们可以借助它自定义Bean的创建过程
前面我们介绍了创建bean的三种方式,构造方法,静态方法,实例方法,FactoryBean类似于静态方法和实例方法,但是FactoryBean是常用的方法,静态和实例方法,用的并不多
一般用来创建复杂对象,就是xml和注解没办法创建的对象
  1. 新建一个类Company
    在这里插入图片描述
  2. 创建一个生产此对象的工厂,继承实现FactoryBean
    在这里插入图片描述
如何使用呢,依然需要通过注解或者xml指定使用FactoryBean来生产复杂对象
  1. xml
    在这里插入图片描述
    在这里插入图片描述
  2. Annontation,就是简单的配置类中配置了
    在这里插入图片描述
    在这里插入图片描述

3. Spring Bean的生命周期

Created with Raphaël 2.3.0 第一步:实例化Bean 第二步:设置属性值 第三步:调用BeanNameAware的setBeanName方法 第四步:调用BeanFactoryAware的setBeanFactory方法 第五步:调用ApplicationContextAware的setApplicationContext方法 第六步:调用BeanPostProcessor的预初始化方法 第七步:调用InitializingBean的afterPropertiesSet方法 第八步:调用定制的初始方法init-method 第九步:调用BeanPostProcessor的后初始化方法
  1. 第九步,有两种可能,如果是prototype(原型模式)的bean,就直接交给调用者
  2. 如果是singleton(单例模式)的bean,就放入spring缓存池中准备就绪的bean
Created with Raphaël 2.3.0 Spring缓存池中准备就绪的bean,当Bean销毁时 调用destory-method属性配置的销毁方法(没有先后顺序) 调用DisposableBean的destory方法(没有先后顺序)
文字描述一下
  1. 根据配置情况调用Bean构造方法或工厂方法实例化Bean
  2. 利用依赖注入完成Bean中所有属性值的配置注入
  3. 如果Bean实现了BeanNameAware接口,则Spring调用Bean的setBeanName()方法传入当前Bean的id值
  4. 如果Bean实现了BeanFactoryAware接口,则Spring调用setBeanFactory()方法传入当前工厂实例的引用
  5. 如果Bean实现了ApplicationContextAware接口,则Spring调用setApplicationContext()方法传入当前ApplicationContext实例的引用。
  6. 如果BeanPostProcessor和Bean关联,则Spring将调用该接口的预初始化方法postProcessBeforeInitialzation()对Bean进行加工操作,此处非常重要,Spring的AOP就是利用它实现的
  7. 如果Bean实现了InitializingBean接口,则Spring将调用afterPropertiesSet()方法.
  8. 如果在配置文件中通过init-method属性指定了初始化方法,则调用该初始化方法。
  9. 如果BeanPostProcessor和Bean关联,则Spring将调用该接口的初始化方法postProcessAfterInitialization()。此时,Bean已经可以被应用系统使用了。
  10. 如果在< bean>中指定了该Bean的生命周期管理;如果在< bean>中指定了该Bean的作用范围为scope=“prototype”,则将实例交给调用者,剩下的不归Spring管
  11. 如果Bean实现了DisposableBean接口,则Spring会调用destory()方法将Spring中的Bean销毁;如果在配置文件中通过destory-method属性指定了Bean的销毁方法,则Spring将调用该方法对Bean进行销毁
注意,虽然Spring为Bean提供了细致全面的生命周期过程,通过实现特定的接口或< bean>的属性设置,都可以对Bean生命周期进行管理,虽然可以随意配置< bean>的属性,但是不建议过多使用Bean实现接口,因为这样会导致代码和Spring的高耦合

3.1 spring bean的生命周期测试

接下来我们写一个测试类,测试一下生命周期
  1. 第一、二、三步:创建Result类,即可测试生命周期第一步,而设置一些属性注入,即可测试生命周期第二步,调用BeanNameAware的setBeanName()为第三步,,所以我们实现BeanNameAware,并实现setBeanName方法
    在这里插入图片描述

  2. 第四步:调用BeanFactoryAware的setBeanFactory方法
    在这里插入图片描述

  3. 第五步:调用ApplicationContextAware的setApplicationContext方法
    在这里插入图片描述

  4. 第六步:调用BeanPostProcessor的预初始化方法:这个是后置处理器,这里先略过,下面就会讲

  5. 第七步:调用InitializingBean的afterPropertiesSet方法
    在这里插入图片描述

  6. 第八步:调用定制的初始方法init-method,可以用xml配置
    在这里插入图片描述

  7. 第九步:调用BeanPostProcessor的后初始化方法

  8. Bena销毁前

  1. 如果是单例模式,调用DisposableBean的destory方法,调用destory-method属性配置的销毁方法
    在这里插入图片描述
  1. 测试
    在这里插入图片描述
    在这里插入图片描述

3.2 实例化之前干的事

其实IoC容器,在第一步之前还干了一些事
  1. 实例化之前会读取xml,涉及到了一个对象BeanDefinition(Spring容器启动过程中,会将Bean解析成Spring内部的BeanDefinition结构)
    在这里插入图片描述
  2. 其实就是将Bean的定义信息存储到BeanDefinition相应的属性中,类名、scope、属性、构造函数参数列表、依赖的bean、是否单例、是否懒加载等等
  3. 后面对Bean的操作,就是直接对BeanDefinition进行,例如拿到这个对象,根据类名,构造函数等利用反射,创建实例
当然,在Bean生命周期前,是有工厂已经存在的,那么针对工厂的后置处理器BeanFactoryPostProcessor,也会进行工作,具体看下面

4. 后置处理器

spring 提供两种后置处理bean的扩展接口,BeanPostProcessor(针对bean对象的后置处理器)和BeanFactoryPostProcessor(针对bean工厂的后置处理器)

3.1 BeanPostProcessor

针对Bean基本的处理,可以针对某个具体的bean,看下面源码
public interface BeanPostProcessor {
	//初始化方法前执行(初始化方法,类似于init-method所指定方法)
	@Nullable
	default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
		return bean;
	}
	//初始化方法后执行
	@Nullable
	default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
		return bean;
	}

}
编写后置处理器
  1. 创建类,然后实现BeanPostProcessor,让其特定监听lazyResult这个Bean
    在这里插入图片描述

3.2 BeanFactoryPostProcessor

在BeanFactory初始化前后可以使用BeanFactoryPostProcessor进行后置处理,针对整个Bean工厂进行处理,典型应用是PropertyPlaceholderConfigurer。看下源码,不过多介绍了
源码如下,是个函数式接口,可以使用lambda表达式直接用,只有一个方法,方法参数是ConfigurableListableBeanFactory
@FunctionalInterface
public interface BeanFactoryPostProcessor {
	void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException;

}
ConfigurableListableBeanFactory ,提供了getBeanDefinition方法,可以根据此方法,找到我们定义bean的BeanDefinition对象,然后我们可以对定义的属性进行修改

在这里插入图片描述

三、Spring IoC源码

由于篇幅限制,我将其放在这篇文章:https://blog.csdn.net/grd_java/article/details/122716151

四、Spring AOP 高级应用和源码

由于篇幅原因,我将其放在这篇文章:https://blog.csdn.net/grd_java/article/details/122858872
  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

殷丿grd_志鹏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值