Spring 的设计思想、创建和使用、Bean 作用域和生命周期

Spring 设计思想

Spring 是什么?

Spring 是包含了众多工具方法的 IoC 容器

什么是 IoC?

控制反转(英语:Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。

技术描述

往往一个类中都需要获取与其合作的类的对象(也就是它所依赖的对象)的引用。如果这个获取过程要靠自身实现,那么这将导致代码高度耦合并且难以维护和调试。

比如,Class A中用到了Class B的对象b,一般情况下,需要在A的代码中显式地用 new 建立 B 的对象。

采用依赖注入技术之后,A 的代码只需要定义一个 private 的B对象,不需要直接 new 来获得这个对象,而是通过相关的容器控制程序来将B对象在外部new出来并通过构造方法或其他方法注入到A类里的引用中。而具体获取的方法、对象被获取时的状态由配置文件(如XML)来指定。

为什么说在 A 里面 new B 的对象的耦合度会比在外面 new B 对象然后注入到 A 里面要高呢?

  • 比如 B 的构造方法发生改变,new B 需要传入其他类型的参数了,那么 A 类里面的 new B 就要跟着修改,耦合度就比较高了,把 new B 放到外面,A 和 B 之间的耦合度就没这么高了。

反转在哪?

  • 依赖对象的获得被反转了

依赖注入和依赖查找

这是实现控制反转的两种主要方式,两者的区别在于,前者是被动的接收对象,在类A的实例创建过程中即创建了依赖的B对象,通过类型或名称来判断将不同的对象注入到不同的属性中,而后者是主动索取相应类型的对象,获得依赖对象的时间也可以在代码中自由控制。

依赖注入有如下实现方式:

  • 基于接口。实现特定接口以供外部容器注入所依赖类型的对象。
  • 基于 set 方法。实现特定属性的public set方法,来让外部容器调用传入所依赖类型的对象。
  • 基于构造函数。实现特定参数的构造函数,在新建对象时传入所依赖类型的对象。
  • 基于注解。基于Java的注解功能,在私有变量前加“@Autowired”等注解,不需要显式的定义以上三种代码,便可以让外部容器传入对应的对象。该方案相当于定义了public的set方法,但是因为没有真正的set方法,从而不会为了实现依赖注入导致暴露了不该暴露的接口(因为set方法只想让容器访问来注入而并不希望其他依赖此类的对象访问)。

依赖查找更加主动,在需要的时候通过调用框架提供的方法来获取对象,获取时需要提供相关的配置文件路径、key等信息来确定获取对象的状态

Spring 是一个 IoC 容器,说的是对象的创建和销毁的权利都交给 Spring 来管理了,它本身又具备存储对象和获取对象的能力

Spring 创建和使用

创建 Spring 项目

创建 Spring 项目和创建 Servlet 项目类似,分为以下 3 步:

  1. 创建一个 Maven 项目

  2. 添加 Spring 框架支持(spring-context、spring-beans)

    直接去中央仓库找就行了,也可以在这里复制:

    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.26</version>
        </dependency>
    
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-beans -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>5.3.26</version>
        </dependency>
    </dependencies>
    
  3. 添加启动类

    就是创建一个带有 main 方法的类。之前学习 Servlet 没有写过 main 方法,是因为 main 方法是 Tomcat 实现的,这里的 Spring 就不一样了。

注册 Bean 对象

Bean 对象就是普通的 Java 对象。

  1. 定义一个 Bean

    public class User {
        public void hi() {
            System.out.println("Hello");
        }
    }
    
  2. 将 Bean 注册到 Spring(并非真正存储,而是告诉 Spring,此 Bean 需要托管给 Spring)

    首先在 resources 里面创建 xml 文件,名字随意,如果是 IDEA 专业版,在右键 resources->new 会看到 XML Configuration File-> Spring Config 选项,直接选择就可以生成。如果是社区版,可以复制以下代码:

    <?xml version="1.0" encoding="UTF-8"?>
    <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 http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    </beans>
    

    接下来,要将 User 对象注册到 Spring 中,具体操作是在 <beans> 中添加如下配置:

    <bean id="user" class="User"></bean>
    

    idclass 属性分别指定了该 Bean 的标识符和实现类。如果实现类写在包里面,那么这里还要带上包名路径。

    这个 <bean> 元素,就是告诉 Spring 容器创建一个名为 “user” 的Bean,该Bean的实现类是 “User” 类。其他部分的应用程序或配置可以通过这个唯一的标识符 “user” 来引用和使用这个Bean。

    注意:id 不可重复,而 class 可以重复,也就是说,同一个类,可以在 Spring 中注册两次,只要 id 不同即可。一个 id 就代表一个对象。

获取并使用 Bean 对象

  1. 得到 Spring 上下文对象,因为对象都交给 Spring 管理了,所以获取对象要从 Spring 中获取,那么就要先得到 Spring 上下文
  2. 通过 Spring 上下文,获取某一个指定的 Bean 对象
  3. 使用 Bean 对象

例:

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class App {
    public static void main(String[] args) {
        // 1.得到 Spring 上下文对象
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
        // 2.从 Spring 中获取 bean 对象
        //User u = new User(); 传统方式,在 spring 中不推荐使用
        User user = (User) context.getBean("user");
        // 3.使用 bean
        user.hi();
    }
}
// 输出:Hello

获取 bean 的其他方法

通过类对象

User user = context.getBean(User.class);

通过此方法获取,有一个弊端,如果同一个类型的 Bean 在 xml 中注册了两次或多次,那么 Spring 就不知道你要获取的是哪个,从而报错。

根据 String 和 Class 获取 bean

User user = context.getBean("user", User.class);

这种方法就避免了上一个方法的弊端,而且不需要强制类型转换。

常用方法总结

方法说明
Object getBean(String name)返回指定 bean 的一个实例,该实例可以是共享的,也可以是独立的
T getBean(Class<T> requiredType)返回唯一匹配给定对象类型的 bean 实例(如果有的话)
T getBean(String name, Class<T> requiredType)返回指定 bean 的一个实例,该实例可以是共享的,也可以是独立的

获取上下文对象的其他方法

BeanFactory context = new XmlBeanFactory(new ClassPathResource("spring-config.xml"));

这种方法不建议使用。

ApplicationContext 和 BeanFactory 的区别是什么

共同点:都是用来获取 Spring 上下文对象

不同点:

  1. 继承关系和功能:ApplicationContext 是 BeanFactory 的子类,BeanFactory 只具备最基本的访问容器的能力,而 ApplicationContext 还提供了对国际化支持,资源访问支持,以及事件传播等方面的支持
  2. 性能和加载方式:BeanFactory 按需加载,当需要使用 bean 的时候再加载,ApplicationContext 一次性加载所有的 bean

Spring 更方便地存储和读取对象

配置文件

在 Spring 配置文件中设置 Bean 扫描根路径,在该路径下的使用了注解的类会被存储到 Spring 中。

<?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:content="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    <content:component-scan base-package="com.cero"></content:component-scan>
</beans>

就是在 <beans> 里面添加 <content:component-scan base-package="com.cero"></content:component-scan> 这一行,其中的 base-package=“” 自己设置。

使用注解

使用注解 将 Bean 对象更简单地存储到 Spring

注解类型有两种

  1. 类注解:@Controller @Service @Repository @Component @Configuration

    @Controller 控制器,用来验证前端传递的参数

    @Service 服务层,服务调用的编排和汇总

    @Repository 仓库,直接操作数据库

    @Component 组件,通用化的工具类

    @Configuration 配置,项目的所有配置

  2. 方法注解:@Bean

使用类注解

package com.cero.service;

import org.springframework.stereotype.Service;

@Service
public class UserService {
    public void doService() {
        System.out.println("Do user service");
    }
}
import com.cero.service.UserService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class App {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
        // 使用注解的方式,bean 的 id 是类名的小驼峰
        UserService userService = context.getBean("userService", UserService.class);
        userService.doService();
    }
}

虽然我们没有在配置文件中显式写出 id,但是 Spring 存储的时候会自动把 id 命名成小驼峰。

这些注解的功能是一样的,但为什么要有这么多注解?

  • 为了主程序员看到类注解之后,就能直接了解当前类的用途比如:
    • @Controller 表示的业务逻辑层
    • @Service 服务层
    • @Repository 持久层
    • @Configuration配置层

程序的工程分层,设备流程如下:

img

五大类注解之间的关系

  • @Controller @Service @Repository @Configuration 都是基于 @Component,它们的作用都是将 Bean 存储到 Spring 中

这一点通过看源代码可以发现

使用注解方式时 Bean 的 id

  • 默认情况下,就是原类名的首字母小写,如:StudentController->studentController
  • 特例:当首字母和第二个字母都是大写的情况下,那么 Bean 的名称为原类名,如:SController->SController

使用方法注解

拥有方法注解的方法,返回的对象会被存储到 Spring 中。

注意

  • 方法注解 @Bean 一定要和类注解配合使用。因为 Spring 是先扫描哪些类要存储,然后再去扫描这个类里面的方法。
  • 方法注解 @Bean 的命名规则和类注解不一样,它的 bean 对象的 id 就是方法名。
  • 使用了方法注解的方法必须是无参的,因为 Spring 初始化存储时,无法提供相应的参数。

例子 :

先定义一个普通的 User 类

package com.cero.model;

public class User {
    private int id;
    private String name;
    private int age;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

定义一个 UserBeans 类,其中的 user 方法会返回一个 User 对象,该对象会被存到 Spring:

package com.cero.controller;

import com.cero.model.User;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

@Component
public class UserBeans {
    @Bean
    public User getUser() {
        User user = new User();
        user.setId(1);
        user.setName("张三");
        user.setAge(18);
        return user;
    }
}

获取存储在 Spring 里的 User 对象:

import com.cero.model.User;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class App {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");

        User user = context.getBean("getUser", User.class);
        System.out.println(user);
    }
}
// 输出:User{id=1, name='张三', age=18}

@Bean 重命名

思考方法注解的命名规则,如果有两个同名的方法返回了不同的对象怎么办,这两个对象怎么区分呢?

如下代码,有两个同名方法,它们返回的对象都将被注册到 Spring 中

package com.cero.controller;

import com.cero.model.User;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

@Component
public class UserBeans {
    @Bean
    public User getUser() {
        User user = new User();
        user.setId(1);
        user.setName("张三");
        user.setAge(18);
        return user;
    }
}

@Component
class UserBeans2 {
    @Bean
    public User getUser() {
        User user = new User();
        user.setId(1);
        user.setName("李四");
        user.setAge(80);
        return user;
    }
}

获取对象并打印:

import com.cero.model.User;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class App {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");

        User user = context.getBean("getUser", User.class);
        System.out.println(user);
    }
}
// 输出:User{id=1, name='李四', age=80}

结果并没有报错,而是打印了李四,说明两个对象都存了,只是李四把张三给覆盖了。

而我要想分别获得这两个方法返回的对象怎么办?

答案是使用 @Bean 重命名

给注解提供 name 参数,具体有 3 种写法:

  1. @Bean(name = "user1")
  2. 也可以省略 name :@Bean("user1")
  3. 使用 {} 来命名多个名字: @Bean(name = {"user1", "user2"})
  4. 使用{} 的 name 也可以省略 @Bean({"user1", "user2"})
package com.cero.controller;

import com.cero.model.User;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

@Component
public class UserBeans {
    @Bean(name = "user1")
    public User getUser() {
        User user = new User();
        user.setId(1);
        user.setName("张三");
        user.setAge(18);
        return user;
    }
}

@Component
class UserBeans2 {
    @Bean("user2")
    public User getUser() {
        User user = new User();
        user.setId(1);
        user.setName("李四");
        user.setAge(80);
        return user;
    }
}

这样它们返回的对象就不再使用默认命名规则了,而是使用由程序员指定的名字。

import com.cero.model.User;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class App {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");

        User user1 = context.getBean("user1", User.class);
        User user2 = context.getBean("user2", User.class);
        System.out.println(user1);
        System.out.println(user2);
    }
}
/* 输出:
User{id=1, name='张三', age=18}
User{id=1, name='李四', age=80}
*/

获取 Bean 对象

获取 Bean 对象也叫做对象装配,对象注入,是把对象取出来放到某个类中

对象装配(对象注入)的实现方法有以下 3 种:

  1. 属性注入
  2. 构造方法注入
  3. Setter 注入

属性注入

这是最简单的对象注入方式,和平常的定义属性类似,这里以我们之前存在 Spring 中的 UserService 为例,

在定义了 private UserService userService; 这个属性之后,在上面加上 @Autowired 注解。然后我们就可以直接调用 UserService 里的方法了。

属性注入的时候,Spring 会优先按照类型在容器中进行查找,如果找不到相同类型的 bean 对象,就会抛异常,如果找到相同类的多个 bean 对象,就再按照属性名查找,找到就赋值到该属性上,找不到就抛异常。

package com.cero.controller;

import com.cero.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class UserController {
    // 从 Spring 中读取 UserService
    // 属性注入
    @Autowired
    private UserService userService;

    public void hi() {
        userService.doService();
    }
}
import com.cero.controller.UserController;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class App {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");

        // 获取 UserController,并调用其中的 hi 方法
        UserController userController = context.getBean("userController", UserController.class);
        userController.hi();
    }
}
// 输出:Do user service

一个细节问题,我的 main 方法可以使用属性注入的方式获取 UserController 对象吗?

import com.cero.controller.UserController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class App {
    @Autowired
    private static UserController userController;

    public static void main(String[] args) {
        userController.hi();
    }
}
/* 输出:
Exception in thread "main" java.lang.NullPointerException
	at App.main(App.java:11)
*/

抛出了空指针异常,说明没有注入成功。这是因为,main 方法是静态方法,它的执行时间是比 Spring 装配的时机要早的,所以它并不能通过属性注入的方式获取 Bean 对象。

优点

  • 属性注入最大的优点就是实现简单,使用简单

缺点

  • 功能性问题:无法注入一个 final 修饰的属性,因为 Java 规定了 final 修饰的变量,要么在定义时直接赋值,要么使用构造方法赋值。这里的属性注入无法满足这两个条件,所以无法注入。
  • 通用性问题:只能适应于 IoC 容器
  • 设计原则问题:更容易违背单一设计原则。因为使用简单,所以滥用的风险更大

Setter 注入

给属性添加 Setter 方法,然后在 Setter 方法上添加 @Autowired 注解

package com.cero.controller;

import com.cero.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class UserController {
    // 从 Spring 中读取 UserService
    // setter 注入
    private UserService userService;

    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    public void hi() {
        userService.doService();
    }
}

优点:

  • 符合单一设计原则。一个 Setter 只针对一个对象

缺点:

  • 不能注入 final 修饰的对象。原因和属性注入一样。
  • 注入的对象可能被改变。因为 Setter 可能被调用多次。

构造方法注入

——这是目前官方推荐的写法

在构造方法的上添加 @Autowired

package com.cero.controller;

import com.cero.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class UserController {
    // 从 Spring 中读取 UserService
    // 构造方法注入
    private UserService userService;

    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }

    public void hi() {
        userService.doService();
    }
}

注意:

  • 当这个类只有一个构造方法的时候,@Autowired 可以省略
  • 构造方法的参数必须是 Spring 容器中有的
  • 一个类,最多只能在一个构造方法上添加 @Autowired

优点:

  • 可以注入 final 修饰的对象
  • 注入的对象不会被修改
  • 依赖对象在使用前会被完全初始化。
  • 通用性更好,因为构造方法是 Java(JDK)支持,所以更换任何的框架,它都是适用的。

缺点:

  • 容易违背单一设计原则,因为一个构造方法可以传入多个参数。

@Resource

它的用法和 @Autowired 是一样的。

@Resource@Autowired 的区别:

  1. @Resource 来自于 JDK,@Autowired 来自 Spring

  2. @Resource 支持属性注入和 Setter 注入,但是不支持构造方法注入

  3. @Resource 支持 name 参数

    @Resource(name = "user2")
    private User user;
    

    上述代码读取 Spring 中名称为 “user2” 的对象,赋值给 user

@Autowired 不支持传入 name 参数,但是它可以配合 @Qualifier 注解来达到相同的效果,上述代码等价于:

@Autowired
@Qualifier(value = "user2")
private User user;

Bean 作用域和生命周期

认识作用域

程序中限定变量的可用范围叫做作用域。

Bean 作用域是指 Bean 在 Spring 整个框架中的某种行为模式。比如 singleton单例作用域,就表示 Bean 在整个 Spring 中只有一份,全局共享。

Spring 容器在初始化一个 Bean 实例时,同时会指定该实例的作用域。Spring 有 6 种作用域,最后四种是基于 Spring MVC 生效的:

  1. singleton:单例作用域
  2. prototype:原型作用域(多例作用域)
  3. request:请求作用域
  4. session:会话作用域
  5. application:全局作用域
  6. websocket:HTTP WebSocket 作用域

singleton

  • 描述:该作用域下的 Bean 在 IoC 容器中只存在一个实例,获取 Bean(即通过 applicationContext.getBean等方法获取)及装配 Bean(即通过 @Autowired 注入)都是同一个对象
  • 场景:通常无状态的 Bean 使用该作用域。无状态即 Bean 对象的属性状态不需要更新。比如 A 和 B 都用到了这个 Bean,因为 A 和 B 共享这一个 Bean 对象,如果 A 把这个 Bean 对象内的属性修改了,那么就会影响到 B,造成不便。
  • 注:Spring 默认选择该作用域

prototype

  • 描述:每次对该作用域下的 Bean 的请求都会创建新的实例:获取 Bean 和装配 Bean 都是新的对象实例。
  • 场景:通常有状态的 Bean 使用该作用域

request

  • 描述:每次 http 请求都会创建新的 Bean 实例,类似于 prototype
  • 场景:一次 http 的请求和响应的共享 Bean
  • 注:限定在 SpringMVC 中使用

session

  • 描述:每次 http 会话都会创建新的 Bean 实例
  • 场景:用户会话的共享 Bean,比如:记录一个用户的登录信息
  • 注:限定在 SpringMVC 中使用

application(不常用)

  • 描述:在一个 http servlet Context 中,定义一个 Bean 实例
  • 场景:Web 应用的上下文信息,比如:记录一个应用的共享信息
  • 注:限定在 SpringMVC 中使用

websocket(不常用)

  • 描述:在一个 WebSocket 的生命周期中,定义一个 Bean 实例
  • 注:限定在 Spring WebSocket 中使用

设置作用域

使用 @Scope 标签用来声明 Bean 的作用域

@Scope 标签既可以修饰方法也可以修饰类,@Scope 有两种设置方式:

  1. 直接设置值:@Scope("prototype")
  2. 使用类似枚举的方式设置:@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)

例:

package com.cero.service;

import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Controller;

@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Controller
public class UserService {
    public void doService() {
        System.out.println("Do user service");
    }
}

Spring 执行流程

  1. 启动容器

    ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
    

    加载配置文件(类加载路径下的 spring-config.xml)

  2. Bean 的初始化(根据配置文件中的 bean,以及 base-package 路径下有类注解的类进行初始化)

    <content:component-scan base-package="com.cero"></content:component-scan>
    <bean id="userConfiguration" class="com.cero.config.UserConfiguration"></bean>
    
  3. 注册 Bean 对象到容器中

  4. 使用 Bean

  5. Spring 销毁

Bean 生命周期

在 Spring 中,Bean 的生命周期指的是 Bean 实例从创建到销毁的整个过程。Spring 容器负责管理 Bean 的生命周期,包括实例化、属性赋值、初始化、销毁等过程。

  1. 实例化 Bean(为 Bean 分配内存空间)
  2. 设置属性,Spring通过反射机制给Bean的属性赋值
  3. Bean 初始化,如果Bean配置了初始化方法,Spring就会调用它。初始化方法是在Bean创建并赋值之后调用,可以在这个方法里面写一些业务处理代码或者做一些初始化的工作。
    • 执行各种通知
    • 执行初始化前置方法(xml 里面定义 init-method | @PostConstruct)
    • 执行初始化方法
    • 执行初始化的后置方法
  4. 使用 Bean
  5. 销毁 Bean(xml destroy-method | @PreDestroy),如果Bean配置了销毁方法,Spring会在所有Bean都已经使用完毕,且IOC容器关闭之前调用它,可以在销毁方法里面做一些资源释放的工作,比如关闭连接、清理缓存等。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

世真

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

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

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

打赏作者

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

抵扣说明:

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

余额充值