02 Spring-DI

1 依赖注入

1.1 什么是依赖注入

依赖注入(dependency injection)的意思为,给予调用方它所需要的事物。“依赖”是指可被方法调用的事物。依赖注入形式下,调用方不再直接指使用“依赖”,取而代之是“注入”。"注入”是指将“依赖”传递给调用方的过程。
要想搞清楚这个“依赖注入”就必须理解什么是“依赖”,依赖就是指一个业务功能执行期间所需要的前提,比如:做米饭以米为前提,则做米饭功能依赖为米,汽车需要汽油才能跑起来,则汽车跑依赖于汽油,再比如光头强砍树需要用到电锯,则光头强砍树依赖于电锯。
注入是指被依赖的前提对象获得方式,如果是自己去创建就是“主动获得”,如果是被动得到,就是“注入”得到,比如光头强砍树依赖的电锯是李老板给的,这就是李老板将“电锯”注入给光头强,光头强去砍树。
在这里插入图片描述

1.2 @Bean注入

Spring IOC容器提供了依赖注入功能,可以将一个组件依赖的对象,在使用之前注入到合适位置。比如,如下关系模拟了光头强砍树时候依赖电锯:
在这里插入图片描述

Spring IOC解决依赖注入,在配置类Config中为Bean组件初始化方法增加参数,Spring IOC容器会在初始化时候自动根据类型注入Bean组件对象,解决“依赖注入”问题:
在这里插入图片描述

1.2.1 案例:

创建新的spring项目:

  1. 创建Saw
package cn.tedu.demo;

import java.io.Serializable;

public class Saw implements Serializable{
	
	private String name = "寒冰锯";
	
	@Override
	public String toString() {
		// TODO Auto-generated method stub
		return name;
	}

}
  1. 创建Worker
package cn.tedu.demo;

import java.io.Serializable;

public class Worker implements Serializable{
	
	private String name = "光头强";
	
	public Saw saw;
	
	public void work() {
		System.out.println(name + "使用" + saw + "砍树");
	}

}
  1. 创建配置文件
package cn.tedu.context;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import cn.tedu.demo.Saw;
import cn.tedu.demo.Worker;

@Configuration
public class config {
	
	@Bean
	public Saw saw() {
		return new Saw();
	}
	
	@Bean
	/*
	 * spring会自动根据变量类型匹配bean组件类型,如果匹配到就将bean组件注入到方法参数中
	 */
	public Worker worker(Saw s) {
		Worker worker = new Worker();
		worker.saw = s;
		return worker;
	}
}
  1. 测试案例
package cn.tedu.test;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import cn.tedu.context.config;
import cn.tedu.demo.Worker;

public class TestCase {

	AnnotationConfigApplicationContext ctx;

	@Before
	public void init() {
		ctx = new AnnotationConfigApplicationContext(config.class);
	}

	@After
	public void destroy() {
		ctx.close();
	}

	
	@Test
	public void testWorker() {
		Worker worker = ctx.getBean("worker",Worker.class);
		worker.work();
	}
}
  1. 运行结果
光头强使用寒冰锯砍树

1.2.2 Spring IOC组件注入时候组件自动匹配规则:

  1. 首先按照注入参数类型查找相对应类型的Bean组件,如果没有直接报错
  2. 如果在Spring容器中能够匹配上唯一类型的Bean组件,如果没有直接报错
  3. 如果按照类型匹配到俩个Bean组件,则在查找组件ID和变量名是否匹配,如果匹配则注入成功
  4. 如果组件类型和组件ID都不能很好匹配则报错
    在这里插入图片描述

1.2.3 注入失败案例:

  1. 更新Config.java
package cn.tedu.context;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import cn.tedu.demo.Saw;
import cn.tedu.demo.Worker;

@Configuration
public class config {
	
	@Bean
	public Saw saw() {
		return new Saw();
	}
	
	@Bean
	public Saw saw2() {
		return new Saw();
	}
	
	@Bean
	/*
	 * spring会自动根据变量类型匹配bean组件类型,如果匹配倒就将bean组件注入倒方法参数中
	 */
	public Worker worker(Saw s) {
		Worker worker = new Worker();
		worker.saw = s;
		return worker;
	}
}
  1. 重新运行测试结果
No qualifying bean of type 'cn.tedu.demo.Saw' available: expected single matching bean but found 2: saw,saw2

利用名字匹配案例:

package cn.tedu.context;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import cn.tedu.demo.Saw;
import cn.tedu.demo.Worker;

@Configuration
public class config {
	
	@Bean
	public Saw saw1() {
		return new Saw();
	}
	
	@Bean
	public Saw saw2() {
		return new Saw();
	}
	
	@Bean
	/*
	 * spring会自动根据变量类型匹配bean组件类型,如果匹配倒就将bean组件注入倒方法参数中
	 */
	public Worker worker(Saw saw1) {
		Worker worker = new Worker();
		worker.saw = saw;
		return worker;
	}
}
  1. 重新运行测试结果
光头强使用寒冰锯砍树

2 @Autowired

Spring提供的组件扫描功能,在扫描时候也可以完成依赖注入,这样可以减少编码提高编程效率。
如何使用@Autowired:
在这里插入图片描述

2.1 案例:

2.1.1 创建项目

  1. 创建一个新的Spring项目
  2. 电锯类Saw
    在这里插入图片描述

2.1.2 创建Worker类

package cn.tedu.demo;

import java.io.Serializable;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;


@Component
public class Worker implements Serializable{
	
	private String name = "光头强";
	
	@Autowired
	public Saw saw;
	
	public void work() {
		System.out.println(name + "使用" + saw + "砍树");
	}

}

2.1.3 创建Saw类

package cn.tedu.demo;

import java.io.Serializable;

import org.springframework.stereotype.Component;

@Component
public class Saw implements Serializable{
	
	private String name = "寒冰锯";
	
	@Override
	public String toString() {
		// TODO Auto-generated method stub
		return name;
	}

}

2.1.4 创建配置类

package cn.tedu.context;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

import cn.tedu.demo.Saw;
import cn.tedu.demo.Worker;

@Configuration
@ComponentScan(basePackages = "cn.tedu.demo")
public class config {

}

2.1.5 测试案例

package cn.tedu.test;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import cn.tedu.context.config;
import cn.tedu.demo.Worker;

public class TestCase {

	AnnotationConfigApplicationContext ctx;

	@Before
	public void init() {
		ctx = new AnnotationConfigApplicationContext(config.class);
	}

	@After
	public void destroy() {
		ctx.close();
	}

	
	@Test
	public void testWorker() {
		Worker worker = ctx.getBean("worker",Worker.class);
		worker.work();
	}
}

2.1.6 测试结果运行

光头强使用寒冰锯砍树

3 set方法注入

刚刚学过@Autowired可以实现对象属性注入,@Autowired也可以标注在set方法上实现set方法注入。set方法也称为Bean属性访问方法,所以set方法注入也称为Bean属性注入。
Spring提供set方法注入的目的是给程序员以更多可以选择的注入方式,程序员可以根据实际情况下选择一种方式注入对象。
使用set方法注入原理:
在这里插入图片描述
案例:

  1. 更新Worker
package cn.tedu.demo;

import java.io.Serializable;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;


@Component
public class Worker implements Serializable{
	
	private String name = "光头强";
	public Saw saw;
	@Autowired
	public void setSaw(Saw saw) {
		this.saw = saw;
		System.out.println("setSaw()");
	}
	
	public void work() {
		System.out.println(name + "使用" + saw + "砍树");
	}

}
  1. 重新测试
setSaw()
光头强使用寒冰锯砍树

测试结果中出现setSaw则说明确实是执行了setSaw方法注入的,在实际工作中,可以在此过程中实现一些其他逻辑,而不是直接注入输出,可以使用此方法。

4 IOC/DI解耦

4.1 利用接口解耦

IOC/DI和接口配合可以实现软件组件“解耦”。
要想理解“解耦”,就需要理解什么是耦合性,我们将一个组件依赖另外一个组件的现象称为耦合。组件连接紧密的称为“紧耦合”,反之松散连接关系称为“松耦合”。比如将手机和电池制造为一个整体,则手机和电池是紧密连接关系,是紧耦合;将手机和电池做成可拆卸组合的零件,则手机和电池是松散连接关系,是松耦合。松耦关系带来的好处是显而易见的,可以使组件之间可以重构,重组,重新搭配,就像手机和电池如果是松耦合关系,就可以在电池没有电的时候进行更换,生活中,松耦合关系比比皆是。
将紧密耦合关系改变为松散耦合关系称为“解耦”。
软件组件之间也存在耦合关系,将紧耦合转化为松耦合使软件之间能够更灵活的组合是很有必要的,上述的案例中“工人”只能依赖“电锯”砍树就是一种紧耦合关系,这种方式造成工人无法更换工具砍树。解决办法是利用接口进行抽象设计,这样组件依赖与接口就“解耦”了。

  1. 抽象设计工具接口,作为电锯和斧子的父类型
  2. 电锯和斧子实现工具接口
  3. 工人依赖于工具接口,工人不依赖于具体的工具
  4. 利用IOC管理组件,为工人注入适当的工具
    在这里插入图片描述
    利用Spring IOC容易提供的DI可以将工具对象注入给工人对象,继而解决对象之间的依赖关系,并且由于依赖接口,所以利用Spring IOC就可以控制组件的组合关系,实现松耦合。

案例:

  1. 创建一个新Spring项目
  2. 声明工具接口
package cn.tedu.demo;
/*
 *	代表抽象的工具类型 
 */
public interface Tool {

}
  1. 声明工人类
package cn.tedu.demo;

import java.io.Serializable;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class Worker implements Serializable{
	
	private String name = "光头强";
	
	@Autowired
	private Tool tool;
	
	public void work() {
		System.out.println(name + "使用" + tool + "砍树");
	}

}
  1. 声明电锯类
package cn.tedu.demo;

import java.io.Serializable;

import org.springframework.stereotype.Component;

//@Component
public class Saw implements Tool{
	
	private String name = "寒冰锯";
	
	@Override
	public String toString() {
		// TODO Auto-generated method stub
		return name;
	}

}
  1. 声明斧子类
package cn.tedu.demo;

import org.springframework.stereotype.Component;

@Component
public class Axe implements Tool{
	
	private String name = "开天斧";
	
	@Override
	public String toString() {
		// TODO Auto-generated method stub
		return name;
	}

}
  1. 配置类
package cn.tedu.context;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

@Configuration
@ComponentScan(basePackages = "cn.tedu.demo")
public class Config {

}
  1. 测试类
package cn.tedu.text;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import cn.tedu.context.Config;
import cn.tedu.demo.Worker;

public class TestCase {
	
	AnnotationConfigApplicationContext ctx;
	
	@Before
	public void init() {
		ctx = new AnnotationConfigApplicationContext(Config.class);
	}
	
	@After
	public void destroy() {
		ctx.close();
	}
	
	@Test
	public void testWorker() {
		Worker worker = ctx.getBean("worker",Worker.class);
		worker.work();
	}

}
  1. 测试结果
光头强使用开天斧砍树

删除斧子类上的@Component注解,在电锯类上增加@Component注解,再次测试:

光头强使用寒冰锯砍树

说明只需要简单更改配置就可以轻松解决注解之间的耦合关系

4.2 @Autowired注入规则

上述案例中,如果将斧子类和电锯类都加上@Component注解将出现运行错误,原因是违反类@Autowired注解的注解注规则,@Autowired注入规则与@Bean组件注入规则类似:

  1. 首先按照注入参数类型查找相应类型的Bean组件,如果没有直接报错误
  2. 如果在Spring容器中能够匹配上唯一类型的Bean组件,则进行注入成功
  3. 如果按照类型匹配到俩个bean组件,则在查找组件ID和变量名是否匹配,如果匹配则注入成功
  4. 如果组件类型的组件ID都不能很好匹配则报错
    • 如何解决斧子类和电锯类都加上@Component注解将出现运行错误?
      1. 利用组件自定义的ID,当组件的ID设置为too时,@Autowired注解就会根据ID匹配成功
      1. 利用注解@Qualifier(“BeanID”)指定注入Bean组件的ID

案例:将斧子类和电锯类都加上@Component注解

  1. 在斧子类上设置自定义BeanID
package cn.tedu.demo;

import org.springframework.stereotype.Component;

@Component("tool")
public class Axe implements Tool{
	
	private String name = "开天斧";
	
	@Override
	public String toString() {
		// TODO Auto-generated method stub
		return name;
	}

}

测试结果:

光头强使用开天斧砍树
  1. 重构Worker类,设定@Qualifier(“saw”)
package cn.tedu.text;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import cn.tedu.context.Config;
import cn.tedu.demo.Worker;

public class TestCase {
	
	AnnotationConfigApplicationContext ctx;
	
	@Before
	public void init() {
		ctx = new AnnotationConfigApplicationContext(Config.class);
	}
	
	@After
	public void destroy() {
		ctx.close();
	}
	
	@Test
	public void testWorker() {
		Worker worker = ctx.getBean("worker",Worker.class);
		worker.work();
	}

}

测试结果:

光头强使用寒冰锯砍树

以上方式都可以解决同时注入冲突问题,日常使用中根据实际情况使用即可。

4.3 同时使用@Bean和@Component

@Bean组件和@Component注解可以同时使用,都可以在spring容器中创建Bean组件。运行结果没有差异,并且注入时候俩种方式创建的组件也可以相互注入,@Bean声明组件可以注入到@Component声明的组件,反之也可以。

案例:@Bean声明的组件注入到@Component声明的组件

  1. 利用@Bean在配置类Config中声明组件
package cn.tedu.context;



import java.util.Date;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

@Configuration
@ComponentScan(basePackages = "cn.tedu.demo")
public class Config {
	
	@Bean
	public Date currentDate() {
		return new Date();
	}

}
  1. 利用@Component声明组件,注入Date类型组件
package cn.tedu.demo;

import java.util.Date;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class Employee {
	
	private String name = "Tom";
	
	@Autowired
	private Date date;
	
	@Override
	public String toString() {
		// TODO Auto-generated method stub
		return "Employee [name = "+ name + ",date =" + date + "]";
	}

}
  1. 测试案例
	@Test
	public void testEmployee() {
		/*
		 *	测试@Bean声明的组件注入到@Component声明的组件 
		 */
		Employee employee = ctx.getBean("employee",Employee.class);
		System.out.println(employee);
	}

测试结果:输出当前系统时间,表示Date注入成功

Employee [name = Tom,date =Mon Jan 03 22:46:10 CST 2022]

案例:@Component声明的组件注入到@Bean声明的组件

  1. 声明Bean类型
package cn.tedu.demo;

public class Dept {
	
	private String name = "教研部";
	
	private Employee manager;
	
	public void serManager(Employee manager) {
		
		this.manager = manager;
		
	}

	
	@Override
	public String toString() {
		return "Dept [name=" + name + ", manager=" + manager + "]";
	}
	
}
  1. 在配置类Config中利用@Bean声明Bean组件
	@Bean
	public Dept dept(Employee employee) {
		Dept dept = new Dept();
		dept.serManager(employee);
		return dept;
	}
  1. 测试案例
	@Test
	public void testDept() {
		/*
		 * 测试@Componet 声明的Bean组件注入到@Bean声明的组件中
		 */
		Dept dept = ctx.getBean("dept",Dept.class);
		System.out.println(dept);
	}
  1. 测试结果
Dept [name=教研部, manager=Employee [name = Tom,date =Mon Jan 03 22:59:11 CST 2022]]

5 使用Properties

5.1 Druid连接池

Druid连接池是阿里巴巴提供开源数据库连接池(https://github.com/alibaba/druid/wiki/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98),是目前被广泛使用的数据库连接池,官方网站是https://github.com/alibaba/druid。
连接池本身也是对象,可以利用Spring IOC容器进行管理,利用Spring IOC管理Druid连接池的步骤是:

  1. 利用Maven导入Druid连接池API和MySql数据库驱动API
  2. 将连接池作为JavaBean在Config.java中配置
  3. 测试连接池是否可用

案例:

  1. 导入连接池
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid</artifactId>
			<version>1.2.8</version>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>8.0.27</version>
		</dependency>
  1. 配置Config.java
	@Bean(initMethod = "init",destroyMethod = "close")
	public DataSource dataSource() {
		DruidDataSource ds = new DruidDataSource();
		ds.setDriverClassName("com.mysql.jdbc.Driver");
		ds.setUrl("jdbc:mysql://localhost:3306/mysql?characterEncoding=utf8&useSSL=false&rewriteBatchedStatements=true");
		ds.setUsername("root");
		ds.setPassword("root");
		ds.setMaxActive(10);
		ds.setInitialSize(2);
		return ds;
	}
  1. 测试
	@Test
	public void testDataSource() {
		/*
		 *	测试数据是否可以成功连接数据库 
		 */
		String sql = "select 'Hello World!'";
		DataSource ds = ctx.getBean("dataSource",DataSource.class);
		
		try (Connection conn = ds.getConnection()){
			
			java.sql.Statement st = conn.createStatement();
			ResultSet rs = st.executeQuery(sql);
			while (rs.next()) {
				System.out.println(rs.getString(1));
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

测试结果:

信息: {dataSource-1} inited
Hello World!
一月 08, 2022 8:01:50 上午 com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl info
信息: {dataSource-1} closing ...
一月 08, 2022 8:01:50 上午 com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl info
信息: {dataSource-1} closed

5.2 @PropertiesSource和Environment

程序参数经常保存在Properties文件中,Spring提供了@PropertiesSource注解用于读取Properties文件,读取后保存到Environment对象中,可以在程序中注入Environment对象,就可以获得配置文件中的信息了。
在这里插入图片描述

案例:

  1. 在resource文件夹中编写jdbc.properties文件
db.driver=com.mysql.jdbc.Driver
db.url=jdbc:mysql://localhost:3306/mysql?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
db.username=root
db.password=root
db.maxActive=10
db.initialSize=2
  1. 配置Config.java
	@Bean(initMethod = "init",destroyMethod = "close")
	public DataSource dataSource2() {
		DruidDataSource ds = new DruidDataSource();
		ds.setDriverClassName(env.getProperty("jdbc.driver"));
		ds.setUrl(env.getProperty("db.url"));
		ds.setUsername(env.getProperty("db.username"));
		ds.setPassword(env.getProperty("db.password"));
		ds.setMaxActive(env.getProperty("db.maxActive",Integer.class));
		ds.setInitialSize(env.getProperty("db.initialSize",Integer.class));
		return ds;
	}
  1. 测试案例
	@Test
	public void testDataSource2() {
		/*
		 *	测试@PropertiesSource 读取Properties文件
		 */
		String sql = "select 'Hello World!!'";
		DataSource ds = ctx.getBean("dataSource2",DataSource.class);
		try (Connection conn = ds.getConnection()){
			java.sql.Statement st = conn.createStatement();
			ResultSet rs = st.executeQuery(sql);
			while (rs.next()) {
				System.out.println(rs.getString(1));
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

测试结果:

Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.
一月 08, 2022 8:39:43 上午 com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl info
信息: {dataSource-1} inited
一月 08, 2022 8:39:43 上午 com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl info
信息: {dataSource-2} inited
Hello World!!
一月 08, 2022 8:39:43 上午 com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl info
信息: {dataSource-1} closing ...
一月 08, 2022 8:39:43 上午 com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl info
信息: {dataSource-1} closed
一月 08, 2022 8:39:43 上午 com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl info
信息: {dataSource-2} closing ...
一月 08, 2022 8:39:43 上午 com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl info
信息: {dataSource-2} closed

5.3 @Value

利用@Value可以读取当前系统环境Environment中的信息,注入到变量中,这个方式更加灵活方便。
在这里插入图片描述
${}是Spring提供的表达式,这里可以得到配置文件中的属性信息。
案例:

	@Bean(initMethod = "init",destroyMethod = "close")
	public DataSource dataSource3(		
			@Value("${db.driver}") String driver,
			@Value("${db.url}") String url,
			@Value("${db.username}") String username,
			@Value("${db.password}") String password,
			@Value("${db.maxActive}")int maxActive,
			@Value("${db.initialSize}") int initialSize) {
	DruidDataSource ds = new DruidDataSource();
	ds.setDriverClassName(driver);
	ds.setUrl(url);
	ds.setUsername(username);
	ds.setPassword(password);
	ds.setMaxActive(maxActive);
	ds.setInitialSize(initialSize);
	return ds;	
	}

测试案例:

Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.
一月 08, 2022 9:01:08 上午 com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl info
信息: {dataSource-1} inited
一月 08, 2022 9:01:08 上午 com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl info
信息: {dataSource-2} inited
Hello World!!
一月 08, 2022 9:01:08 上午 com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl info
信息: {dataSource-2} closing ...
一月 08, 2022 9:01:08 上午 com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl info
信息: {dataSource-2} closed
一月 08, 2022 9:01:08 上午 com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl info
信息: {dataSource-1} closing ...
一月 08, 2022 9:01:08 上午 com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl info
信息: {dataSource-1} closed
一月 08, 2022 9:01:08 上午 com.alibaba.druid.support.logging.JakartaCommonsLoggingImpl info
信息: {dataSource-0} closing ...
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值