Spring学习

一、Spring概述

● Spring的核心概念IOC(控制反转)和AOP(面向切面编程)

二、IOC

2.1、IOC概念

2.2、Bean对象的三种创建方式

2.2.1、XML配置文件

1、将applicationContent.xml文件导入src包下resource中
        ?xml version="1.0" encoding="UTF-8"?>

<bean id="..." class="...">  
    <!-- collaborators and configuration for this bean go here -->
</bean>

<bean id="..." class="...">
    <!-- collaborators and configuration for this bean go here -->
</bean>

<!-- more bean definitions go here -->

2、applicationContent.xml文件中属性解析

● :定义Spring中资源,将需要交给Spring控制的资源用标签定义。

        id:bean对象的唯一标识,类比于new对象中的变量名。

        class:交个Spring控制资源的全路径类名(包名+类名),类比与new对象中的 “new 对象名()”这个操作。

        name:别名,和id的含义类似,但是可以配置多个不同的值,不同值之间用逗号隔开。

        scope:配置bean对象是否是为单例(默认为单例),singleton--单例、prototype--多例

        lazy-init:配置bean对象的创建时机,默认bean是在ioc容器创建时创建,可设置为true,在使用时创建。

        init-method:指定初始化方法,bean对象初始化过程中会自动调用该方法。

        destory-method:指定销毁方法,bean对象销毁过程中会自动调用该方法。

        生成bean对象是用到的属性

3、DI(Dependency injection)

● DI是将容器创建的对象赋值给bean对象属性的过程。
● 属性值注入方式一:属性注入(通过无参构造函数+setter方法注入)

        property:注入标签(set注入,需要在使用注入资源的类中声明属性的set方法)

        name:标识属性名

        普通值注入:value

        Bean对象注入: ref

        数组注入:
        xml <array> <value>xxx</alue> </array> 
        List注入:

 <list>
     <value>xxx</alue>
 </list> 

        map注入:
        xml <map> <entry key = "" ,value = "">xxx</entry> </map> 
        set注入:
        xml <set> <value>xxx</alue> </set> 
        Null注入:
        xml <null/> 
        properties注入:
        xml <props> <prop key = ""> xxx </prop> </props> 
● 属性值注入方式二:通过有参的构造函数注入。
        必须存在有参构造函数

<constructor-arg>:有参构造函数注入(需要在bean对应的类中声明对应构造方法)
	name/index/type:定位要为注入的属性
  	name:通过参数名称定位到要注入的参数
    index:通过下标索引定位要注入的参数
    type:通过参数类型定位要注入的参数(参数类型要保持唯一)
  value:普通属性注入
  ref:bean对象注入

● 属性值注入方式三:扩展方式
        1、P命名空间注入:可以直接注入属性值(用于简化“属性注入”方式书写)
        2、C命名空间注入:通过构造器注入(用于简化“有参构造器注入”方式书写)
        xml文件头文件需要引入对应的约束才能生效
4、标签下其他重要标签
● import:在当前配置文件中导入其他配置文件
○ resource:加载的配置文件名

2.2.2、注解开发

1、在applicationContent.xml文件中配置对应的属性

<!--开启注解扫描,只有配置这个,注解才能生效!-->
<context:annotation-config/>
<!--指定要扫描的包-->
<context:component-scan base-package="com.pojo"/>

2、配置Bean

● @Component --- 标识这个类被Spring所管理。
○ Controller --- 对应的Web层
○ Service --- 对应的是服务层
○ Repositoty --- 对应的数据层

  • 如果没有指定具体的id,默认的id是类名(首字母小写)

● @Scope:定义在类上,控制单例或者多例。

● @PostConstruct:定义在方法上,指定该方法为初始化方法。

● @PreDestory :定义在方法上,指定该方法为销毁方法。

3、Bean中属性注入
● @Value:普通数据注入
● @Autowired:引用类型注入
○ 查找方法:先通过Type查找,类型不唯一,通过变量名进行匹配。类型和变量名都不唯一,查看是否有一个被@Primary修饰,或者结合@Qualifier使用。
● @PropertySource:定义在类上方,用于加载properties文件中属性值。

2.2.3、java方式

1、@Configuration的使用
● 从Spring3.0,@Configuration用于定义配置类,可替换xml配置文件,被注解的类内部包含有一个或多个被@Bean注解的方法,这些方法会被AnnotationConfigApplicationContest或AnnotationConfigWebApplicationContext类进行扫描,并用于构建bean定义,初始化Spring容器。

● @Configuration注解的配置类有如下要求:

  • @Configuration不可以是final类型;
  • @Configuration不可以是匿名类;
  • 嵌套的configuration必须是静态类。

2、@Configuration如何加载Spring

● @Configuration配置spring并启动spring容器

// @Configuration标注在类上,相当于把该类作为Spring的xml配置文件中的<beans>,作用为:配置Spring容器(应用上下文)
@Configuration
public class TestConfiguratin{
	public TestConfiguration(){
    	System.out.println("TestConfiguration容器启动初始化......")
    }
}
相当于:.xml 文件中的这个配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context" 

</beans>
// 测试类
public class TestMain {
    public static void main(String[] args) {

        // @Configuration注解的spring容器加载方式,用AnnotationConfigApplicationContext替换ClassPathXmlApplicationContext
        ApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class);

        // 如果加载spring-context.xml文件:
        // ApplicationContext context = new
        // ClassPathXmlApplicationContext("spring-context.xml");
    }

        // 输出结果
        "TestConfiguration容器启动初始化......"
● @Configuration启动容器+@Bean注册Bean

"@Bean标注在方法上(返回某个实例的方法),等价于spring的xml配置文件中的<bean>,作用为:注册bean对象"
// Bean类
public class TestBean {

    private String username;

    public void sayHello() {
        System.out.println("TestBean sayHello...");
    }

    public String toString() {
        return "username:" + this.username;
    }
    public void start() {
        System.out.println("TestBean 初始化。。。");
    }
    public void cleanUp() {
        System.out.println("TestBean 销毁。。。");
    }
}
// 配置类
@Configuration
public class TestConfiguration {
    public TestConfiguration() {
        System.out.println("TestConfiguration容器启动初始化。。。");
    }
    // @Bean注解注册bean,同时可以指定初始化和销毁方法
    // @Scope 指定Bean的范围,是单例还是多例
    // @Bean(name="testBean",initMethod="start",destroyMethod="cleanUp")
    @Bean
    @Scope("prototype")
    public TestBean testBean() {
        return new TestBean();
    }
}

注:
        (1)、@Bean注解在返回实例的方法上,如果未通过@Bean指定bean的名称,则默认与标注的方法名相同;
        (2)、@Bean注解默认作用域为单例singleton作用域,可通过@Scope(“prototype”)设置为原型作用域;
        (3)、既然@Bean的作用是注册bean对象,那么完全可以使用@Component、@Controller、@Service、@Ripository等注解注册bean,当然需要配置@ComponentScan注解进行自动扫描。
● @Configuration启动容器+@Component注册Bean
        "在配置类上添加对应的包扫描范围,不用定义方法,通过注解同样可以定义Bean"

// Bean类
@Component    
public class TestBean {

    private String username;

    public void sayHello() {
        System.out.println("TestBean sayHello...");
    }

    public String toString() {
        return "username:" + this.username;
    }
    public void start() {
        System.out.println("TestBean 初始化。。。");
    }
    public void cleanUp() {
        System.out.println("TestBean 销毁。。。");
    }
}
// 配置类
@Configuration
public class TestConfiguration {
    public TestConfiguration() {
        System.out.println("TestConfiguration容器启动初始化。。。");
    }
    // @Bean注解注册bean,同时可以指定初始化和销毁方法
    // @Scope 指定Bean的范围,是单例还是多例
    // @Bean(name="testBean",initMethod="start",destroyMethod="cleanUp")
  /*  @Bean
    @Scope("prototype")
    public TestBean testBean() {
        return new TestBean();
    } */
}

        注: 效果同@Configuration启动容器+@Bean注册Bean是一致的。
3、@Configuration如何组合多个配置类
● 在@Configuration中引入Spring的xml配置文件

@Configuration
@ImportResource("classpath:applicationContext-configuration.xml")
public class WebConfig {
}

● 在@Configuration中引入其他注释配置

@Configuration
@ImportResource("classpath:applicationContext-configuration.xml")
@Import(TestConfiguration.class)
public class WebConfig {
}

● @configuration嵌套(嵌套的Configuration必须是静态类)

@Configuration
@ComponentScan(basePackages = "全路径类名")
public class TestConfiguration {
    public TestConfiguration() {
        System.out.println("TestConfiguration容器启动初始化。。。");
    }
    
    @Configuration
    static class DatabaseConfig {
        @Bean
        DataSource dataSource() {
            return new DataSource();
        }
    }
}

三、AOP

3.1、AOP概念

● AOP:面向切面编程,可以理解为由一点及面,在程序中只需修改一个地方,就可以影响很多地方,这些很多地方在一起组成一个面,故称之为面向切面编程。
● OOP:面向对象编程,可以理解为由点及点,在程序中只需修改一个地方,只可影响修改的哪个地方,故称之为面向对象编程。

3.2、AOP应用的三种方式

3.2.1、使用spring接口【springAPI接口实现】

● 定义我们需要的类继承SpringAOP相应的类
○ MethodBeforeAdvice -- 前置通知
○ AfterAdvice -- 后置通知
○ AfterReturningAdvice -- 返回值通知
○ 略。。。。

import org.springframework.aop.MethodBeforeAdvice;

import java.lang.reflect.Method;

public class Log implements MethodBeforeAdvice {
    //method:要执行的目标对象的方法
    //args:参数
    //target:目标对象
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println(target.getClass().getName()+method.getName());
    }
}

● 在applicationContent.xml文件中配置开启AOP

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beanss
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--注册bean-->
    <bean id="log" class="com.log.Log"/>

    <!--配置aop-->
    <aop:config>
        <!--切入点:expression:表达式,execution(要执行的位置)-->
        <aop:pointcut id="point" expression="execution(* com.service.UserServiceImp.*(..))"/>
        <!--执行环绕-->
        <aop:advisor advice-ref="log" pointcut-ref="point"/>
        <aop:advisor advice-ref="afterlog" pointcut-ref="point"/>
    </aop:config>
</beans>

这种方式不需要配置切面

3.2.2、自定类来实现AOP

● 自定义功能增强类,用于实现特定功能。

● 在applicationContent.xml文件中进行对应AOP的配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--注册bean-->
    <bean id="log" class="com.log.Log"/>
    <bean id="diy" class="com.diy.DiyPointcut">
    </bean>
     
    <aop:config>
        <!--自定义切面-->
        <aop:aspect ref="diy"> // ref="diy" 引用在Spring中定义好的Bean
            <!--切入点-->
            <aop:pointcut id="point" expression="execution(* com.service.UserServiceImp.*(..))"/>
            // 定义通知方式
            <aop:before method="before" pointcut-ref="point"/>  // 在diy中定义好的方法
            <aop:after method="after" pointcut-ref="point"/> 
        </aop:aspect>
    </aop:config>

</beans>

3.2.3、注解方式实现AOP

● 在applicationContent.xml文件中进行对应AOP的配置,开启注解支持

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">
    
    <bean id="ann" class="com.diy.Annotation"></bean>
    // 开启注解支持
    <aop:aspectj-autoproxy/>
    <!--注册bean-->
    <bean id="userservice" class="com.service.UserServiceImp"></bean>
    
</beans>

● 自定义功能增强类,用于实现特定功能。
○ @Aspect --- 标注这类是一个切面
○ @Before("execution(* com.service.UserServiceImp.(..))") --- 标注这个方法通知方式以及切入点
○ @Pointcut("execution(
 com.service.UserServiceImp.*(..))") --- 配置切点
○ 略。。。。。。

3.3、AOP原理(代理模式)

3.3.1、为什么要用代理模式?

● 我们可以使用代理对象来替代真实对象的访问,这个就能在不修改目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。
● 代理模式的主要作用是扩展目标对象的功能,比如说在目标对象的某个方法执行前后可以增加一些自定的操作。

3.3.2、静态代理

● 静态代理:我们对目标对象的每个方法的增强都是手动完成的,从JVM层面上来说,静态代理在编译时就以及将接口、实现类、代理类这些都变成了一个个实际的class文件。
● 静态代理实现步骤:
○ 定义一个接口及其实现类
○ 创建一个代理类同样是实现这个接口。
○ 将目标对象注入代理类,然后在代理类的对应方法调用目标类中的方法。
● 代码实现

// 定义一个租房接口
public interfare Rent{
 void rentHous();
}

// 定义租房实现(目标对象)
public class RentImpl implements Rent{
	 @Override
    public void rendHouse() {
        System.out.println("出租房子");
    }
}

// 定义代理类,实现同一个接口
public class ProxyRent implements Rent{
    private Rent rent;
    //定义有参构造
    public ProxyRent(Rent rent) {
        this.rent = rent;
    }

 @Override
    public void rendHouse() {
        System.out.println("操作前");// 自定义操作
       rent.rentHoues(); // 调用Rent接口的方法
    }
}

// 定义Test类
public class Test {
	public static void main(String[] args) {
    ProxyRent proxy = new ProxyRent(new RentImpl());
    proxy.rendHouse();;
	}
}

● 静态代理的优点:
○ 可以做到在符合开闭原则的情况下对目标对象进行功能扩展。
● 静态代理缺点:
○ 得为每一个服务都创建代理类,工作量大且不易管理,耦合性强。

3.3.3、JDK动态代理机制

● JDK动态代理:只能代理实现了接口得类
● 动态代理: Java 动态代理机制中 InvocationHandler 接口和 Proxy 类是核心。
● Proxy类中使用频率最高的方法是:newProxyInstance( ),这个方法主要用来生成一个代理对象。

public static Object newProxyInstance(ClassLoader loader,
                                     Class<?> interfaces,
                                     InvocationHandler h)
         throws IllegalArgumentException
    {
       ......
    }

参数解析
        1、loader : 类加载器,用于加载代理对象。
        2、interfaces:被代理类实现的一些接口。
        3、h:实现了 InvocationHandler接口的对象。
● 要实现动态代理的话,还必须要实现InvocationHandler来自定义处理逻辑,当我们的动态代理对象调用一个方法时,这个方法的调用就会被转发到实现InvocationHandler接口类的invoker方法来调用。

public interface InvocationHandler {

    /**
     * 当你使用代理对象调用方法的时候实际会调用到这个方法
     */
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

invoker() 方法参数解析:

        1、proxy:动态生成的代理类
        2、method:与代理类对象调用的方法相对应
        3、args:当前method方法的参数
● 通过Proxy类的newProxyInstance()创建的代理对象在调用方法的时候,实际会调用到实现InvocationHanler接口的类的invoke()方法,可以在invoke()方法中自定义处理逻辑。
● JDK 动态代理类使用步骤:
        1、定义一个接口及其实现类;
        2、自定义 InvocationHandler 并重写invoke方法,在 invoke 方法中我们会调用原生方法(被代理类的方法)并自定义一些处理逻辑;
        3、通过 Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) 方法创建代理对象;
● 代码实现

//定义一个接口
public interface Rent {
    void RentHouse();
}

// 定义这个接口的实现类
public class RentImpl implements Rent {
    @Override
    public void RentHouse() {
        System.out.println("我就想租房子");
    }
}

// 自定义类实现InvocationHandler接口并重写invoke方法
public class JDKInvocationHandler implements InvocationHandler {
    private Object target;

    public JDKInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        seeHouse();
        Object result = method.invoke(target, args);
        return result;
    }

    public void seeHouse(){
        System.out.println("看房子");
    }
}

// 通过 Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) 方法创建代理对象;
public class Test {
    public static void main(String[] args) {
        Rent rent = new Rent1();
        JDKInvocationHandler j = new JDKInvocationHandler(rent);

        Rent proxy = (Rent) Proxy.newProxyInstance(rent.getClass().getClassLoader(), rent.getClass().getInterfaces(), j);
        proxy.RentHouse();
    }
}

3.3.4、CGLIB 动态代理机制

● CGLIB动态代理:CGLIB 通过继承方式实现代理。
● CGLIB:CGLIB(Code Generation Library)是一个基于ASM的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。
● 动态代理 : 在 CGLIB 动态代理机制中 MethodInterceptor 接口和 Enhancer 类是核心。
● MethodInterceptor 接口

public interface MethodInterceptor
extends Callback{
    // 拦截被代理类中的方法
    public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args,
                               MethodProxy proxy) throws Throwable;
}

// intercept方法参数解析
1 obj:被代理的对象(需要增强的对象)
2 method:被拦截的方法(需要增强的方法)
3 args:方法入参
4 proxy:用于调用原始方法

● 可以通过Enhancer类来动态获取代理类,当代理类调用方法的时候,实际调用的是MethodInterceptor中的intercept方法.
● CGLIB 动态代理类使用步骤
        1 定义一个类;
        2 自定义 MethodInterceptor 并重写 intercept 方法,intercept 用于拦截增强被代理类的方法,和 JDK 动态代理中的 invoke 方法类似;
        3 通过 Enhancer 类的 create()创建代理类;

● 代码演示
// 定义一个类
public class Rent {
    public void RentHouse() {
        System.out.println("唉,就是玩");
    }
}

// 自定义类实现MethodInterceptor接口并重写 intercept 方法.
public class DebugMethodInterceptor implements MethodInterceptor {

    /**
     *
     * @param o           代理对象(增强的对象)
     * @param method      被拦截的方法(需要增强的方法)
     * @param objects     方法入参
     * @param methodProxy 用于调用原始方法
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("before method" + method.getName());
        Object object = methodProxy.invokeSuper(o, objects);
        return object;
    }
}

//测试
public class Test {
    public static void main(String[] args) {
        CglibProxyFactory cglibProxyFactory = new CglibProxyFactory();
        RentImpl proxy = (RentImpl) cglibProxyFactory.getProxy(RentImpl.class);
        proxy.RentHouse();

    }
// 通过Enhancer 类的 create()创建代理类;
    static class CglibProxyFactory{
        public   Object getProxy(Class<?> clazz){
            Enhancer enhancer = new Enhancer();
            enhancer.setClassLoader(clazz.getClassLoader());
            enhancer.setSuperclass(clazz);
            enhancer.setCallback(new DebugMethodInterceptor());
            return enhancer.create();
        }
    }
}

四、Spring源码分析

4.1、IOC及Bean的生命周期

4.1.1、【流程图】

4.1.2、容器启动阶段

● 首先会通过某种途径加载配置信息,大部分情况下,容器需要依赖Spring自定义的某些工具类(BeanDefinitionReader)对加载的配置信息进行解析和分析,并将解析后的信息编组为对应的BeanDefinition,最后把这些保存了Bean定义信息的BeanDefinition,注册到相应的Be按DifinitonRegistry,这样容器启动工作就完成了。

4.1.3、干预容器启动

● Spring提供了一种叫做BeanFactoryPostProcesson的扩展机制,允许在容器实例化对象之前,对注册到容器的BeanDefinition所保存信息做相应的修改。这就相当于在容器实现的第一阶段最后加入一道工序,让我们对最终的BeanDefinition做的一些额外的操作,比如修改其中Bean定义的某些属性,为beand定义增加其他信息等。

4.1.4、Bean实例化阶段

● 【流程图】

● 实现流程
        a. Spring 启动,查找并加载需要被 Spring 管理的 Bean,并实例化 Bean。
        b. 利用依赖注入完成 Bean 中所有属性值的配置注入。
        c. 如果 Bean 实现了 BeanNameAware 接口,则 Spring 调用 Bean 的 setBeanName() 方法传入当前 Bean 的 id 值。
        d. 如果 Bean 实现了 BeanFactoryAware 接口,则 Spring 调用 setBeanFactory() 方法传入当前工厂实例的引用。
        e. 如果 Bean 实现了 ApplicationContextAware 接口,则 Spring 调用 setApplicationContext() 方法传入当前 ApplicationContext 实例的引用。
        f. 如果 Bean 实现了 BeanPostProcessor 接口,则 Spring 调用该接口的预初始化方法 postProcessBeforeInitialzation() 对 Bean 进行加工操作,此处非常重要,Spring 的 AOP 就是利用它实现的。
        g. 如果 Bean 实现了 InitializingBean 接口,则 Spring 将调用 afterPropertiesSet() 方法。
        h. 如果在配置文件中通过 init-method 属性指定了初始化方法,则调用该初始化方法。
        i. 如果 BeanPostProcessor 和 Bean 关联,则 Spring 将调用该接口的初始化方法 postProcessAfterInitialization()。此时,Bean 已经可以被应用系统使用了。
        j. 如果在 中指定了该 Bean 的作用域为 singleton,则将该 Bean 放入 Spring IoC 的缓存池中,触发 Spring 对该 Bean 的生命周期管理;如果在 中指定了该 Bean 的作用域为 prototype,则将该 Bean 交给调用者,调用者管理该 Bean 的生命周期,Spring 不再管理该 Bean。
        k. 如果 Bean 实现了 DisposableBean 接口,则 Spring 会调用 destory() 方法销毁 Bean;如果在配置文件中通过 destory-method 属性指定了 Bean 的销毁方法,则 Spring 将调用该方法对 Bean 进行销毁。

4.2、循环依赖

4.2.1、循环依赖定义

● java中的循环依赖分为两种,一种是构造器的循环依赖,另一种是属性的循环依赖。
○ 构造器的循环依赖:这种循环依赖没有什么解决办法,因为JVM虚拟机在对类进行实例化的时候,需要实例化构造器的参数,而由于循环引用这个参数无法提前实例化,故只能抛出异常。
○ 属性的循环依赖:在构造器执行的时候为完成属性的注入,而在调用方法的时候已经完成了注入。

4.2.2、解决循环依赖

● 【流程图】

● Spring 单例 Bean 的创建 中介绍介绍了使用三级缓存。
○ singletonObjects: 一级缓存,存储单例对象,Bean 已经实例化,初始化完成。
○ earlySingletonObjects: 二级缓存,存储 singletonObject,这个 Bean 实例化了,还没有初始化。
○ singletonFactories: 三级缓存,存储 singletonFactory。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值