spring3.x第三章 IOC容器概述

3.1 IOC概述

  IOC(Inverse of Control)是Spring容器的内核,AOP、声明式事务等功能在此基础上开花结果。

3.1.1 通过实例理解IOC的概念

  电影<墨攻>中,刘德华扮演的墨者革离达到都城下,城上问道:“来者何人?”,刘德华回答:“墨者革离”。

public class MoAttack{
    public void cityGateAsk(){
        //1.演员直接侵入剧本
        LiuDeHua ldh = new LiuDeHua()
        ldh.responseAsk("墨者革离");
    }
}

  在剧本1处,作为具体角色扮演者的刘德华直接侵入到剧本中,使剧本和演员直接耦合在一起。
  一个明智的编剧在剧情创作时应围绕故事的角色进行,而不是应该考虑角色具体的扮演者,这样就可以自由的选择任何合适的演员,而非绑定到刘德华一人身上,通过以上分析,我们直到需要为该剧本主人公革离定义一个接口:

public class MoAttack{
    public void cityGateAsk(){
        //1.引入革离角色接口
        GeLi geli = new LiuDeHua();
        //2.通过接口开展剧情
        geli.responseAsk("墨者革离");
    }
}

  在1处引入了剧本的角色——革离,剧本的情节通过角色展开,而在拍摄时角色由演员饰演,如2处。但是,MoAttack同时依赖于GeLi接口和LiuDeHua类,并没有达到我们所期望的剧本仅依赖角色的目的。但是角色最终必须通过具体的演员才能完成拍摄,如何让LiuDeHua和剧本无关而又完成GeLi的具体动作呢?当然是在影片真正投拍时,让导演将剧本、角色、饰演者装配起来。
  通过引入了导演,使剧本和具体饰演者解耦了。对应到软件中,导演像是一个装配器,安排演员表演具体的角色。
  IOC包括两个内容:
* 控制
* 反转。

  那到底是什么东西的”控制”被”反转”了呢?对应到前面的例子,”控制”是指选择GeLi角色扮演者的控制权;”反转”是指这种控制权从<墨攻>剧本中移除,转交到导演的手中。对于软件来说,即是某一接口具体实现类的选择控制权从调用类中移除,转交给第三方决定。
  因为IOC不够开门见山,因此提出了DI(依赖注入:Dependency Injection)的概念用以代替IOC,即让调用类对某一接口实现类的依赖关系由第三方(容器或协作类)注入,以移除调用类对某一接口实现类的依赖。

3.1.2 IOC的类型

  从注入方法上看,主要分为三种类型:构造函数注入、属性注入和接口注入。Spring支持构造函数注入和属性注入。
  构造函数注入

public class MoAttack{
    private GeLi geli;
    public MoAttack(GeLi geli){
        this.geli = geli;
    }
    public void CityGateAsk(){
        geli.responseAsk("墨者革离");
    }
}
//接着是Director;通过构造函数注入革离扮演者
public class Director{
    public void direct(){
        Geli ldh = new LiDeHua();
        MoAttack m = new MoAttack(ldh);
        m.CityGateAsk();
    }
}

  属性注入
  有时,导演会发现,有的情节不需要革离的出现,这种情况下通过构造函数注入并不妥当(读者:可以通过构造器),这时可以考虑使用属性注入,更加的灵活:

public class MoAttack{
    private GeLi geli;
    public void SetGeli(GeLi geli){
        this.geli = geli;
    }
    public void CityGateAsk(){
        geli.responseAsk("墨者革离");
    }
}
//接着是Director;通过构造函数注入革离扮演者
public class Director{
    public void direct(){
        Geli ldh = new LiDeHua();
        MoAttack m = new MoAttack();
        m.setGeLi(ldh);
        m.CityGateAsk();
    }
}
3.1.3 通过容器完成依赖关系

  虽然MoAttack和LiuDeHua实现了解耦,MoAttack无须关注角色实现类的实例化工作,但这些工作在代码中依然存在,只是转移到Director类中而已。假设,在选择某个剧后,希望通过一个“海选“或者第三中介机构来选择导演、演员。那剧本、导演、演员都实现了解耦。
  所谓”海选“和第三方中介机构在程序领域即是一个第三方的容器,它帮助完成类的初始化与装配工作,让开发者从这些底层实现类的实例化、依赖关系装配等工作中脱离出来,专注于业务逻辑开发工作。Spring就是这样的一个容器,它通过配置文件或注解描述类和类之间的依赖关系,自动完成类的初始化和依赖注入的工作。下面是对以上实例进行配置的配置文件片段:

<!-- 实现类实例化 -->
<bean id = "geli" class="LiuDeHua"/>
<!-- 通过geli-ref建立依赖关系 -->
<bean id = "moAttack" class="com.baobaotao.ioc.MoAttack"
    p:geli-ref="geli"/>

  通过new XmlBeanFactory("Beans.xml")等方式即可启动容器,自动实例化Bean并完成依赖关系的装配。主要通过反射。

3.2 相关Java基础知识

  Java语言允许通过程序化的方式间接对Class的对象实例操作,Class文件由类装载器装载后,在JVM中将形成一份描述Class结构的元信息对象,通过该元信息对象可以获知Class的结构信息:如构造函数、属性和方法等。Java允许用户借由这个Class相关的元信息对象间接调用Class对象的功能。

package com.baobaotao.reflect;

public class Car {

    public void setMaxSpeed(int maxSpeed) {
        this.maxSpeed = maxSpeed;
    }

    private String brand;
    private String color;
    private int maxSpeed;
    //省略Get、Set方法

    public Car(){

    }

    public Car(String brand, String color, int maxSpeed){
    this.brand = brand;
    this.color = color;
    this.maxSpeed = maxSpeed;
    }

    public void introduce(){
    System.out.println("brand:" + brand + ",color:" + color + ",maxSpeed:" + maxSpeed);
    }

}
package com.baobaotao.reflect;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class ReflectTest {
    public static Car initByDefaultConst() throws Exception{
    //1.通过类装载器获取Car类对象
    ClassLoader loader = Thread.currentThread().getContextClassLoader();
    Class clazz = loader.loadClass("com.baobaotao.reflect.Car");

    //2.获取类的默认构造器对象并通过它实例化Car
    Constructor cons = clazz.getDeclaredConstructor((Class[])null);
    Car car = (Car) cons.newInstance();

    //3.通过反射方法设置属性
    Method setBrand = clazz.getMethod("setBrand", String.class);
    setBrand.invoke(car, "GT-R");
    Method setColor = clazz.getMethod("setColor", String.class);
    setColor.invoke(car, "红色");
    Method setMaxSpeed = clazz.getMethod("setMaxSpeed", int.class);
    setMaxSpeed.invoke(car, 200);
        return car;
    }

    public static void main(String[] args) throws Exception{
    Car car = initByDefaultConst();
    car.introduce();
    }
}
3.2.2 类装载器ClassLoader

  类装载器工作机制
  类装载器就是寻找类的字节码文件并构造出类在JVM内部表示的对象组件。在Java中,类装载器把一个类装入JVM中,要经过一下步骤:
  1.装载:查找和导入Class文件。
  2.链接:执行校验、准备和解析步骤,其中解析步骤是可以选择的:
    a)校验:检查载入Class文件数据的;
    b)准备:给类的静态变量分配存储空间;
    c)解析:将符号引用转成直接引用;
  3.初始化:对类的静态变量、静态代码块执行初始化工作;
  类装载工作由ClassLoader及其子类负责,ClassLoader是一个重要的Java运行时系统组件,它负责在运行时查找和装入Class字节码文件。JVM在运行时会产生三个ClassLoader:跟装载器、ExtClassLoader(扩展类装载器)和AppClassLoader(系统类装载器)。其中,根装载器不是ClassLoader的子类,它使用C++编写,因此我们在Java看不到它,根装载器负责装载JRE的核心类库,如JRE目标下的rt.jar、charsets.jar等。ExtClassLoader和AppClassLoader都是ClassLoader的子类。其中ExtClassLoader负责装载JRE扩展目录ext中的JAR类包;AppClassLoader负责装载Classpath路径下的类包。
  这三个类装载器之间存在父子层级关系,即根装载器是ExtClassLoader的父装载器,ExtClassLoader是AppClassLoader的父装载器。默认情况下,使用AppClassLoader装载应用程序的类。可以做一个实验:

package com.baobaotao.reflect;
public class ClassLoaderTest {
    public static void main(String[] args) {

    ClassLoader loader = Thread.currentThread().getContextClassLoader();
    System.out.println("current loader:" + loader);
    System.out.println("parrent loader:" + loader.getParent());
    System.out.println("grandparrent loader:" + loader.getParent().getParent());
    //output:
    //current loader:sun.misc.Launcher$AppClassLoader@2a139a55
    //parrent loader:sun.misc.Launcher$ExtClassLoader@7852e922
    //grandparrent loader:null
    }

}

  JVM装载类时使用”全盘负责委托机制”,”全盘负责”是指当一个ClassLoader装载一个类的时候,除非显示地使用另一个ClassLoader,该类所依赖及引用的类也由这个ClassLoader载入;”委托机制”是指先委托父装载器寻找目标类,只有在找不到的情况下才从自己的类路径中查找并装载目标类。这一点是从安全角度考虑的,试想如果有人编写了一个恶意的基础类(如java.lang.String)并装在道JVM中将会引起可怕的后果。但由于有了”全盘负责委托机制”,java.lang.String永远是由根装载器来装载,这样就避免了上述事件的发生。
  ClassLoader重要方法
  ClassLoiader是一个抽象类,位于java.lang包下:

3.2.3 Java反射机制
3.3 资源访问利器
3.3.1 资源抽象接口

  JDK所提供的访问资源的类并不能很好地满足各种底层资源的访问需求,比如缺少从类路径或者Web容器的上下文中获取资源的操作类。因此,Spring设计了一个Resource接口,它为应用提供了更强的访问底层资源的能力。
FileSystemResource以文件系统绝对路径的方式进行访问;
ClassPathResource以类路径的方式进行访问;
ServletContextResource以相对与Web应用根目录的方式进行访问

package com.baobaotao.resource;

import java.io.IOException;
import java.io.InputStream;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;

public class FileSourceExample {
    public static void main(String[] args) throws IOException {
        String filePathString = "C:/WiFi_Log.txt";
        Resource res1 = new FileSystemResource(filePathString);
        InputStream ins1 = res1.getInputStream();
        System.out.println("res1:" +res1.getFilename());
        //output:res1:WiFi_Log.txt
    }
}

  采用特殊编码进行读取资源

package com.baobaotao.resource;

import java.io.IOException;
import java.io.InputStream;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.util.FileCopyUtils;

public class FileSourceExample {

    public static void main(String[] args) throws IOException {
        String filePathString = "C:/WiFi_Log.txt";
        Resource res1 = new FileSystemResource(filePathString);
        EncodedResource encRes = new EncodedResource(res1);
        String contentString = FileCopyUtils.copyToString(encRes.getReader());
        System.out.println(contentString);
    }
}
3.3.2 资源加载

  为了访问不同类型的资源,必须使用相应的Resource实现类。是否可以仅通过资源地址的特殊标识就可以加载相应的资源呢?Spring提供了一个强大的加载资源的机制,不但能通过”classpath:”、”file:”等资源地址前缀识别不同的资源类型,还支持Ant风格带通配符的资源地址。
  资源地址表达式
  1. classpath:从类路径中加载资源,classpath:和classpath:/是等价的,都是相对于类的根路径。
  2. file:从文件系统目录中加载资源,可采用绝对或相对路径
  3. http://:从Web服务器中装载资源
  4. ftp://:从FTP服务器中装载资源
  5. 没有前缀:根据ApplicationContext具体实现类采用对应类型的Resource
  其中”classpath*:”前缀。假设一个名为baobaotao的应用分为3个模块,每个模块都对应一个配置文件,分别是module1.xml,module2.xml,module3.xml,都放到com.baobaotao目录下。使用”classpath*:com/baobaotao/module*.xml”将可以加载这三个模块的配置文件,而使用”classpath:com/baobaotao/module*.xml”时只会加载一个模块的配置文件。
  Ant风格资源地址支持3种匹配符:
   ?:匹配文件名中的一个字符;
   *:匹配文件名中任意个字符;
   **:匹配多层路径
  资源加载器

package com.baobaotao.resource;

import java.io.IOException;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;

public class PatternResolverTest {
    public static void main(String[] args) throws IOException {

        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        Resource resources[] = resolver.getResources("classpath*:com/baobaotao/**/*.xml");
        for (Resource resource : resources) {
            System.out.println(resource.getDescription());
        }
    }
}
3.4 BeanFactory和ApplicationContext

  Spring通过一个配置文件描述Bean以Bean之间的依赖关系,利用Java语言的反射功能实例化Bean并建立Bean之间的依赖关系。Spring的IOC容器还提供了Bean实例缓存、声明周期管理、Bean实例代理、事件发布、资源装载等高级服务。
  Bean工厂(com.springframework.beans.factory.BeanFactory)是Spring最核心的接口,提供了高级IOC配置,管理不同类型的Java对象。
  应用上下文(com.springframework.context.ApplicationContext)建立在BeanFactory基础之上,提供了更多面向应用的功能,提供了国际化支持和框架事件体系,更易于创建实际应用。我们一般称BeanFactory为IOC容器,而称ApplicationContext为应用上下文。但有时为了行文方便,也将ApplicationContext称为Spring容器。
  BeanFactory是Spring框架的基础设施,面向Spring本身;ApplicationContext面向使用Spring框架的开发者,几乎所有的应用场合都直接使用ApplicationContext而非底层的BeanFactory。

3.4.1 BeanFactory介绍
  BeanFactory的体系结构

  Spring为BeanFactory提供了多种实现,最常用的是XmlBeanFactory。
  初始化BeanFactory
  下面,我们使用Spring配置文件为Car提供配置信息,然后通过BeanFactory装载配置文件,启动Spring IOC容器。Spring配置文件如下所示:

<?xml version="1.0" encoding="UTF-8" ?>
<!-- 引用Spring的多个Schema空间的格式定义文件 -->
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <bean
        id="car1"
        class="com.baobaotao.Car"
        p:brand="GT-R中国红"
        p:color="红色"
        p:maxSpeed="200"/>

</beans>

  通过XmlBeanFactory实现类启动SpringIOC容器:

package com.baobaotao.beanfactory;

import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;

import com.baobaotao.Car;

public class BeanFactoryTest {
    public static void main(String[] args){
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        Resource res = resolver.getResource("classpath:com/baobaotao/beanfactory/beans.xml");
        BeanFactory bFactory = new XmlBeanFactory(res);
        System.out.println("init BeanFactory.");
        Car car = bFactory.getBean("car1", Car.class);
        System.out.println("car bean is resdy for use");
    }
}

  XmlBeanFactory通过Resource装载Spring配置信息并启动IOC容器,然后就可以通过BeanFactory#getBean(beanName)方法从IOC容器中获取Bean了。通过BeanFactory启动IOC容器时,并不会初始化配置文件中定义的Bean,初始化动作发生在第一个调用时。对于单实例的Bean来说,BeanFactory会缓存Bean实例,所以第二次使用getBean获取Bean时直接从IOC容器的缓存中获取Bean实例。使用HashMap实现的缓存器。
  在初始化BeanFactory时,必须为其提供一种日志框架,我们使用Log4J,即在类路径下提供Log4J配置文件,这样启动Spring容器才不会报错。

3.4.2 ApplicationContext介绍

  如果说BeanFactory是Spring的心脏,那么ApplicationContext就是完整的身躯了。ApplicationContext由BeanFactory派生而来,提供了更多面向实际应用的功能。通过配置的方式实现。

  ApplicationContext类体系结构

  主要实现类是ClassPathXmlApplicationContext和FileSystemXmlApplicationContext,前者默认从类路径加载配置文件,后者默认从文件系统中加载配置文件。
  在获取ApplicationContext实例后,就可以像BeanFactory一样调用getBean(beanName)返回Bean了。ApplicationContext的初始化和BeanFactory由一个重大的区别:BeanFactory在初始化容器时,并未实例化Bean,直到第一次访问某个Bean时才实例目标Bean;而ApplicationContext则在初始化应用上下文时就实例化所有单实例的Bean。淫才ApplicationContext的初始化时间会比BeanFactory稍长一些,不过稍后的调用则没有”第一次惩罚”的问题。
  Spring3.0支持基于类注解的配置方式,主要功能来自于Spring的一个名为JavaConfig的子项目,目前JavaConfig已经升级为Spring核心框架的一部分。一个标注@Configuration注解的POJO即可提供Spring所需的Bean配置信息。

@Configuration //表示是一个配置信息提供类
public class Beans{
    @Bean(name="car")
    public Car buildCar(){
        Car car = new Car();
        car.setBrand("GT-R");
        car.setMaxSpeed(498);
        return car;
    }
}

  相对于XML配置,类注解的配置方式更加灵活。
  Spring为基于注解类的配置提供了专门的ApplicationContext实现类AnnotationConfigApplicationContext。使用AnnotationConfigApplicationContext启动Spring容器:

ApplicationContext ctx = new AnnotationConfigApplicationContext(Beans.class);
        Car car = ctx.getBean("car", Car.class);
  WebApplicationContext类体系结构

  WebApplicationContext是专门为Web应用准备的,从相对于Web根目录的路径中装载配置文件完成初始化工作。扩展了ApplicationContext。使Spring的Web应用上下文和Web容器的上下文实现互访。

  WebApplicationContext类初始化

  WebApplicationContext的初始化范式和BeanFactory、ApplicationContext有所区别,因为WebApplicationContext需要ServletContext实例(与Web应用的上下互访),也就是说它必须在拥有Web容器的前提下才能完成启动工作。有过Web开发经验的读者都知道可以在web.xml中配置自启动的Servlet或定义Web容器监听器(ServletContextListener),借助这两者中的任何一个,就可以完成启动Spring Web应用上下文的工作。
  下面使用Web容器监听器引导:

    <!-- 指定配置文件 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/baobaotao-dao.xml,/WEB-INF/baobaotao-service.xml</param-value>
    </context-param>
    <!-- 声明Web容器监听器 -->
    <listener>
        <listener-class>
            org.springframework.web.context.ContextLoaderListener
        </listener-class>
    </listener>

  下面使用自启动的Servlet引导:

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/baobaotao-dao.xml,/WEB-INF/baobaotao-service.xml</param-value>
    </context-param>
    <!-- 声明自启动的Servlet -->
    <servlet>
        <servlet-name>springContextLoaderServlet</servlet-name>
        <servlet-class>
            org.springframework.web.context.ContextLoaderServlet
        </servlet-class>
        <!-- 启动顺序 -->
        <load-on-startup>2</load-on-startup>
    </servlet>

  由于WebApplicationContext需要使用日志功能,用户可以将log4J的配置文件放置到类路径WEB-INF/classes,这时Log4J引擎即可顺利启动。如果Log4J配置文件放置在其他位置,用户还必须在web.xml指定Log4J配置文件位置。Spring为启用Log4J引擎提供了两种类食欲启动WebApplicationContext的实现类:Log4jConfigServlet和Log4jConfigListener。

<!-- 指定Log4J配置文件位置 -->
<context-param>
    <param-name>log4jConfigLocation</param-name>
    <param-value>/WEB-INF/log4j.properties</param-value>
</context-param>
<!-- 指定Log4J配置文件位置 -->
<servlet>
    <servlet-name>log4jConfigServlet</servlet-name>
    <servlet-class>org.springframework.web.util.Log4jConfigServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
</servlet>

  log4j启动顺序在springContextLoaderServlet之前启动。如果使用监听,则需要在之前。
  如果使用标注@Configuration的Java类提供配置信息,则web.xml的配置需要按如下方式配置:

<!-- 通过指定context参数,让Spring使用AnnotationConfigWebApplicationContext而
    非XmlWebApplicationContext启动容器 -->
    <context-param>
        <param-name>contextClass</param-name>
        <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
    </context-param>

    <!-- 指定标注了@Configuration的配置类,可以使用逗号或空格隔开 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>com.baobaotao.AppConfig1, com.baobaotao.AppConfig2</param-value>
    </context-param>

    <!-- 监听器将根据上面配置使用 -->
    <listener>
        <listener-class>
            org.springframework.web.context.ContextLoaderListener
        </listener-class>
    </listener>

  ContextLoaderListener如果发现配置了contextClass上下文参数,就会使用参数所指定的WebApplicationContext实现类(即AnnotationConfigWebApplicationContext)初始化容器,该实现类会根据contextConfigLocation上下文参数指定的@Configuration的配置类所提供的Spring配置信息初始化容器。

3.4.3 父子容器

  通过HierarachicalBeanFactory接口,Spring的IOC容器可以建立父子层级关联的容器体系,自容器可以访问父容器中的Bean,但父容器不能访问自容器的Bean。在容器内,Bean的id必须是唯一的,但自容器可以拥有一个和父容器id相同的Bean。
  比如,展现层Bean位于一个子容器中,而业务层和持久层的Bean位于父容器中。这样展现层Bean就可以引用业务层和持久层的Bean,而业务层和持久层的Bean则看不到展现层的Bean。

3.5 Bean的生命周期

  Web容器中的Servlet拥有明确的声明周期,Spring容器中的Bean也拥有相似的生命周期。

3.5.1 BeanFactory中Bean的生命周期
  生命周期图解

  * Bean自身的方法:如调用Bean构造函数实例化Bean,调用Setter设置Bean的属性值以及通过的init-method和destroy-method所指定的方法;
  * Bean级生命周期接口方法,如BeanNameAware、BeanFactoryAware、InitializingBean和DisposableBean,这些接口方法由Bean类直接实现;
  * 容器级生命周期接口方法:BeanPostProcessor。

  关于Bean生命周期接口的探讨

  通过实现Spring的Bean生命周期接口对Bean进行额外控制,就让Bean和Spring框架绑定在一起。这和Spring一直推崇的”不对应用程序类作出任何限制”的理念是相博的。
  可以通过的init-method和destroy-method,进行配置进行与Spring框架接口解耦。而BeanPostProcessor接口像插件一样注册到Spring容器中,为容器提供额外功能。

3.5.2 ApplicationContext中Bean的生命周期

  与BeanFactory中的周期类似,但是增加了一个接口,在应用上下文在装载配置文件之后初始化Bean实例之前调用BeanFactoryPostProcessor对配置信息进行加工处理。另外不同之处:ApplicationContext利用Java反射识别配置文件的定义,而BeanFactory需要代码手工调用进行注册。

3.6 小结

  深入分析了IOC的概念,控制反转中的”控制”指接口实现类的选择控制权;而”反转”是指这种选择控制权从调用类转移到外部第三方类或容器的手中。
  BeanFactory、ApplicationContext和WebApplicationContext是Spring框架三个最核心的接口。Resource是一个资源访问接口,实现了和具体资源的解耦,ResourceLoader采用了策略模式,通过不同的前缀自动选择合适的底层资源实现类。
  Spring为Bean提供了周全的声明周期过程,推荐使用配置方式,与Spring进行解耦,进行对Bean生周期的控制。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值