Spring真的是程序员的春天吗?

我是一个从汽车行业转行IT的项目经理,我是Edward,如想了解更多,请关注我的公众号【转行项目经理的逆袭之路】。学了两天Spring框架,没有感觉到任何便利,反而是一堆条条框框的注解把原本随心所欲的程序逻辑变得不可控了,不测试一下你甚至不知道既定的操作到底完成了没有。

1. 什么是框架

框架在项目中的表现就是一系列的jar包,例如Thymeleaf就是一个框架。

每种框架都会解决某种特定的问题,可能是开发效率的问题,或运行效率的问题,或代码管理维护的问题等等。

项目中使用框架就相当于得到了一个“毛坯房”,使用了框架之后,开发人员只需要关心后续的“装修”即可。

绝大部分的框架都有特定的使用方式,在使用时,必须遵循框架的使用规则!

每个框架都可能是若干个开发人员甚至开发团队多年的工作积累的作品,对于初学者来说,不要过于钻牛角尖,尝试理解框架的底层实现原理!

简单的说:使用框架,可以让编程变得更加简单!在学习框架时,主要学习框架的正确使用方式!

2. 依赖关系

假设在项目中需要开发一个用户注册的功能!在项目中可能存在:

public class UserRegServlet {
    private UserDao userDao = new UserDao();
    
    public void doPost() {
        userDao.reg(); // 调用userDao对象实现存储用户数据
    }
}
public class UserDao {
    public void reg() {
        // 通过JDBC技术将用户数据存储到数据库中
    }
}

在以上代码中,UserRegServlet就是依赖UserDao的!

3. 耦合度

如果某个类过于依赖于另外一个类,通常称之为了“耦合度较高”,是不利于代码的管理和维护的,简单的说,如果UserRegServlet依赖于UserDao,在未来的某一天,UserDao已经不能满足项目的需求了(可能是因为代码有Bug,或者使用的技术比较落后等),如果需要把UserDao替换掉,替换难度大就会影响项目的管理和维护,为了解决这样的问题采取的解决方案就称之为“解耦”,使得依赖关系不那么明确,甚至就是不明确!

就以上UserRegServlet依赖UserDao的问题,如果要解耦,可以先创建一个接口:

public interface IUserDao {
    void reg();
}

然后,使得UserDao是实现了以上接口的:

public class UserDao implements IUserDao {
    public void reg() {
        // 具体的实现了reg()方法应该实现的功能
    }
}

经过以上调整以后,如果在UserRegServlet中需要使用到UserDao,以前的代码是这样的:

private UserDao userDao = new UserDao();

现在就可以改为:

private IUserDao userDao = new UserDao();

以上代码就相当于:

private List<String> strings = new ArrayList<>();

改成这样以后,在同一个项目中,无论多少个Servlet组件需要使用到UserDao,都可以使用以上“声明为接口,创建实现类的对象”的语法风格,如果以后UserDao需要被替换掉,也只需要替换“赋值”的代码,声明部分是不需要替换的!例如需要把UserDao替换为UserMybatisDao时,原来的代码是:

private IUserDao userDao = new UserDao();

新的代码就可以是:

public class UserMybatisDao implements IUserDao {
    public void reg() {
        // 使用更好的方式实现reg()应该实现的功能
    }
}

在后续的使用中,就可以是:

private IUserDao userDao = new UserMybatisDao();

也就是说,在UserDao换成了UserMybatisDao时,在各个Servlet中,都只需要调整等于号右侧的内容,而不再需要修改等于号左侧的部分!

当然,关于以上代码的右侧部分,还可以使用“工厂设计模式”作进一步的处理:

public class UserDaoFactory {
    // 返回接口类型的对象
    public static IUserDao newInstance() {
        return new UserDao(); // 也可以返回UserMybatisDao的对象
    }
}

当有了工厂后,此前的代码就可以进一步调整为:

private IUserDao userDao = UserDaoFactory.newInstance();

可以发现,以上代码中不再出现任何一个实现类的名字了,无论是哪个Servlet组件需要访问数据库,都声明为以上代码即可,以后,如果实现类需要被替换,也只需要替换工厂方法的返回值即可!

在实际项目开发时,项目中的组件的依赖更加复杂,为每个组件都创建对应的接口及工厂是非常麻烦的,而Spring框架就很好的解决了这个问题,可以简单的将Spring理解为一个“万能工厂”,当使用了Spring框架后,就不必自行开发工厂了!

4. Spring框架简介

Spring框架的主要作用:解决了创建对象和管理对象的问题。

5. 通过Spring创建对象

创建Maven Project,在创建过程中,勾选Create a simple projectGroup Id填为cn.teduArtifact Id填为spring01,其它项保持默认即可。

使用Spring框架时,必须在项目的pom.xml中添加spring-context的依赖:

<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.5.RELEASE</version>
</dependency>

首先,在项目中,创建cn.tedu.spring包,并在这个包下创建BeanFactory类:

package cn.tedu.spring;

public class BeanFactory {

}

当前,代码放在哪个包中并不重要,应该养成习惯,每个类都应该放在某个包中,不要让任何类不放在任何包中!

以上类的名称也不重要,是自定义的!

如果希望由Spring来创建并管理某个类的对象,必须在以上类中添加方法,关于这个方法:

  • 应该使用public权限;
  • 返回值类型就是需要Spring创建并管理的类的对象的类型;
  • 方法名称可以自定义;
  • 参数列表暂时为空;
  • 在方法体中,自行编写创建返回值对象的代码。

假设需要Spring来创建Date类型的对象,则在类中添加方法:

public Date aaa() {
    // 规范,规则
}

Spring框架要求:创建对象的方法必须添加@Bean注解,并且,这样的方法必须在配置类中!任何一个类添加了@Configuration注解都可以作为配置类!

package cn.tedu.spring;

@Configuration
public class BeanFactory {
	
	@Bean
	public Date aaa() {
		return new Date();
	}

}

完成后,应该使用一个可以运行的类,或通过单元测试来检验“是否可以通过Spring容器获取对象”。本次先创建一个可以运行的类:

package cn.tedu.spring;

public class SpringTests {

	public static void main(String[] args) {
		// 1. 加载配置类,得到Spring容器
		AnnotationConfigApplicationContext ac
			= new AnnotationConfigApplicationContext(BeanFactory.class);
		
		// 2. 从Spring容器中获取所需要的对象
		Date date = (Date) ac.getBean("aaa"); // getBean()方法的参数就是创建对象的方法的名称
		
		// 3. 测试获取到的对象
		System.out.println(date);
		
		// 4. 关闭
		ac.close();
	}

}

6. 关于@Bean注解

当方法的声明之前添加了@Bean注解,就表示这个方法是需要由Spring框架所调用,并且,由Spring框架管理该方法返回的对象的!默认情况下,该方法的名称就是后续获取对象时,调用getBean()方法的参数!

由于添加了@Bean注解的方法是被Spring框架调用的,不需要自行编写代码来调用这个方法,所以,Spring的建议是“使用合理的属性名称作为方法名,并不需要使用动词或动词为前缀的方法名”,简单的说,如果方法是为了获取Date类型的对象的,该方法的名称应该是date,而不是getDate(),则后续调用getBean()时,参数就是date这个名称!

当然,如果不遵循Spring的建议,还可以在@Bean注解中配置注解参数来指定Bean的名称,例如:

@Bean("date")
public Date getDate() {
    return new Date();
}

则后续就根据注解参数来获取对象:

Date date = (Date) ac.getBean("date");

其关系如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-98tspAXI-1590056665138)(01.png)]

其实,在开发项目时,真的不必关心这些问题,也就是说,例如是一个获取Date对象的方法,其名称到底是date还是getDate都是正确的!毕竟这个方法最终就是由Spring框架来调用,开发人员不会自行调用该方法!

7. Spring管理对象的作用域

由Spring管理的对象,默认情况下,是单例的!所以,其作用域就非常久!

在Spring管理对象的情况下,讨论对象的作用域,其本质就是讨论其是否单例!

在创建对象的方法之前,添加@Scope注解,并配置注解参数为prototype,就可以使得该对象不是单例的:

@Scope("prototype")
@Bean
public User user() {
    return new User();
}

由Spring管理的对象,如果是单例模式的,默认情况下,是饿汉式的!在创建对象的方法之前,添加@Lazy注解,就可以调整为懒汉式的:

@Bean
@Lazy
public User user() {
    return new User();
}

一般,在开发项目时,极少调整对象的作用域!

8. 当天小结:

  • Spring的主要作用:创建对象,管理对象;
  • 如果某个方法是用于给Spring框架创建对象的,这个方法就必须添加@Bean注解;
  • 所有添加了@Bean注解的方法,其所在的类应该添加@Configuration注解,凡添加了@Configuration注解的类称之为配置类
  • 默认情况下,由Spring管理的对象是单例的,使用@Scope注解可以将Spring管理的对象调整为“非单例”的;
  • 默认情况下,由Spring管理的单例的对象是是“饿汉式”的,使用@Lazy可以将它们改为“懒汉式”的。

附1:设计模式之单例模式

单例模式的特点:在同一时期,某个类的对象一定最多只有1个!也许会尝试多次的获取对象,但是,获取到的一定是同一个对象!

假设项目中有King类:

public class King {
}

很显然,目前它并不是单例的,因为,可以:

King k1 = new King();
King k2 = new King();
King k3 = new King();

以上代码就创建了3个King类型的对象!如果要实现单例,首先,就必须限制构造方法的访问,例如:

public class King {
    private King() {
    }
}

每个类中都可以有若干个构造方法,如果某个类没有显式的声明任何构造方法,编译器就会自动添加1个公有的、无参数的构造方法!如果类中已经声明任何构造方法,则编译器不会自动添加构造方法!

由于将构造方法声明为私有的,则原有的King k1 = new King();这类代码就不能用于创建对象了!

限制构造方法的访问,其目的是“不允许随意创建对象”,并不是“不允许创建对象”,在King类的内部,还是可以创建对象的,可以添加方法,返回内部创建的对象:

public class King {
    private King king = new King();
    
    private King() {
    }
    
    public King getInstance() {
        return king;
    }
}

所以,当需要King类型的对象时,可以通过getInstance()方法来获取!

但是,以上代码是不可行的!因为,如果要调用getInstance()方法,必须先获取King的对象,而获取King对象的唯一方式就是调用getInstance()方法!为了解决这个问题,必须在getInstance()方法的声明之前添加static修饰符,最终,就可以通过类名.方法名()的语法格式来调用方法了!同时,由于“被static修饰的成员,不可以访问其它未被static修饰的成员”,所以,全局属性king也必须被static修饰:

public class King {
    private static King king = new King();
    
    private King() {
    }
    
    public static King getInstance() {
        return king;
    }
}

至此,基本的单例模式的代码就设计完成了!

以上代码是“饿汉式”的单例模式,另外,还有“懒汉式”的单例模式!

基本的懒汉式单例模式的代码是:

public class King {
    private static King king = null;
    
    private King() {
    }
    
    public static King getInstance() {
        if (king == null) {
            king = new King();
        }
        return king;
    }
}

注意:以上代码是多线程不安全的!

在开发领域中,只要数据的产生、变化不是开发人员预期的,就称之为“不安全”,也就是“数据安全问题”。

为了保障线程安全,应该为以上创建对象的代码片断“加锁”,例如:

public class King {
    private static King king = null;
    
    private King() {
    }
    
    public static King getInstance() {
        synchronized ("hello") {
            if (king == null) {
                king = new King();
            }
        }
        return king;
    }
}

当然,无论是哪个线程在什么时候执行以上代码,都必须先“锁住”代码片断后才能开始执行,是没有必要的,“锁”的性能消耗是浪费的,所以,可以进一步调整为:

public class King {
    private static King king = null;
    
    private King() {
    }
    
    public static King getInstance() {
        if (king == null) { // 判断有没有必要锁定接下来的代码
            synchronized ("java") {
                if (king == null) { // 判断有没有必要创建对象
                    king = new King();
                }
            }
        }
        return king;
    }
}

至此,懒汉式的单例模式就完成了!

9. 由Spring管理的对象的生命周期

如果需要管理Bean的生命周期,可以在对应的类中自定义生命周期的初始化方法和销毁方法,关于这2个方法的声明:

  • 应该使用public权限;
  • 使用void表示返回值类型;
  • 方法名称可以自定义;
  • 参数列表为空。

例如:

package cn.tedu.spring;

public class User {
	
	public User() {
		System.out.println("User.User()");
	}
	
	public void init() {
		System.out.println("User.init()");
	}
	
	public void destroy() {
		System.out.println("User.destroy()");
	}

}

在配置Spring管理对象的@Bean注解中,配置注解参数,以指定以上2个方法分别是初始化方法和销毁方法:

package cn.tedu.spring;

@Configuration
public class BeanFactory {

	@Bean(initMethod = "init", destroyMethod = "destroy")
	public User user() {
		return new User();
	}
	
}

最终,可以看到:

  • 初始化方法会在构造方法之后执行,且只执行1次;
  • 销毁方法会在Spring容器被销毁之前执行,且只执行1次。

10. 使用组件扫描使得Spring管理类的对象

首先,自定义某个类(类名、包名均没有要求),在类的声明之前添加@ComponentScan注解,该注解用于配置组件扫描,注解的参数是String类型的,表示“被扫描的根包”:

package cn.tedu.spring;

import org.springframework.context.annotation.ComponentScan;

@ComponentScan("cn.tedu.spring")
public class SpringConfig {

}

在组件扫描的包下创建类,该类的声明之前需要添加@Component注解,以表示这个类是一个“组件类”,后续,当Spring扫描时,会自动创建所有组件类的对象:

package cn.tedu.spring;

import org.springframework.stereotype.Component;

@Component
public class User {

}

当完成以后配置后,后续,程序执行时,只要加载了SpringConfig类,由于类之前配置了组件扫描,Spring框架就会扫描对应的包下所有的类,并逐一检查是否为“组件类”,如果是,则创建对象,如果不是,则不创建!

使用@ComponentScan时,配置的是需要扫描的“根包”,假设需要扫描的是cn.tedu.spring,在配置时,配置为cn.tedu甚至配置为cn都是可用的,但是,强烈不推荐使用过于简单的设置,避免出现扫描范围过多而导致的浪费资源!

另外,在@ComponentScan注解的源代码中:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {

	@AliasFor("basePackages")
	String[] value() default {};

	@AliasFor("value")
	String[] basePackages() default {};
    
}

可以看出,配置的值可以是String[],也就是可以指定多个包名。

在使用这种做法时,必须保证被Spring管理的对象所归属的类存在无参数构造方法!

在使用这种做法时,Spring创建对象后,默认会使用以下原则作为Bean的名称:

  • 如果类名的第1个字母是大写的,第2个字母是小写的(不关心其它字母的大小写),则会把类名的第1个字母改为小写,其它不变,作为Bean的名称,例如类名是User时,Bean的名称就是user,类名是UserDao时,Bean的名称就是userDao
  • 如果不满足以上条件,则类名就是Bean的名称。

如果希望使用自定义的名称作为Bean的名称,可以在@Component注解中配置参数,例如:

package cn.tedu.spring;

import org.springframework.stereotype.Component;

@Component("uuu")
public class User {

}

则后续调用getBean()方法时,就必须使用"uuu"作为参数来获取对象!

在Spring框架的作用范围内,除了@Component以外,另外还有3个注解,可以起到完全等效的效果:

  • @Controller:通常添加在控制器类的声明之前;
  • @Service:通常添加在业务类的声明之前;
  • @Repository:通常添加在**持久层的类(负责数据的持久化管理)**的声明之前。

也就是说,这4种注解作用、用法完全相同,只是语义不同。

目前,已经介绍了2种使得Spring框架管理类的对象的做法:

  • 自定义方法返回某个对象,并在方法的声明之前添加@Bean注解;
  • 将类放在组件扫描的包或其子孙包中,并在类的声明之前添加@Component/@Controller/@Service/@Repository注解。

以上的第1种做法是万能的,适用于任何条件,但是,在设计代码时相对麻烦,管理起来相对不便利;而第2种做法就更加简单、直观,却只适用于自定义的类。

所以,只要是自行编写的类,都应该采取第2种做法,如果需要Spring管理其它类(JDK中的,或某框架中的)的对象,只能使用第1种做法!

11. 使用Spring读取.properties文件

假设在项目的src/main/resources下存在jdbc.properties文件,其内容是:

url=jdbc:mysql://localhost:3306/db_name
driver=com.mysql.jdbc.Driver

然后,在项目中,自定义某个类,在这个类中,声明对应数量的属性,这些属性的值将会是以上配置信息的值!

public class JdbcProperties {
    private String url;
    private String driver;
    // 生成以上2个属性的Getters & Setters
}

当需要读取以上jdbc.properties配置文件时,需要在以上类的声明之前添加@PropertySource注解,并配置需要读取的文件的位置:

// 以下注解的参数是配置文件的名称
@PropertySource("jdbc.properties")
public class JdbcProperties {
    private String url;
    private String driver;
    // 生成以上2个属性的Getters & Setters
}

接下来,就可以把读取到的值赋值给类中的2个属性,可以通过@Value注解来实现:

// 以下注解的参数是配置文件的名称
@PropertySource("jdbc.properties")
public class JdbcProperties {
    @Value("${url}") // 在注解参数的大括号的值,是jdbc.properties配置中等于号左侧的名称
    private String url;
    @Value("${driver}")
    private String driver;
    // 生成以上2个属性的Getters & Setters
}

最后,整个的读取过程是由Spring框架来完成的,所以,以上JdbcProperties类还应该被Spring框架所管理,可以采取组件扫描的做法,则创建SpringConfig类,用于指定组件扫描的包:

// 以下注解参数配置的就是组件扫描的包,同时,请保证JdbcProperties类是在这个包或其子孙包中的
@ComponentScan("cn.tedu.spring")
public class SpringConfig {
}

然后,在JdbcProperties类的声明之前,补充添加@Component注解,使得Spring框架扫描到这个类时,能明确的知道“这个类是组件类”,从而创建该类的对象:

@Component
// 以下注解的参数是配置文件的名称
@PropertySource("jdbc.properties")
public class JdbcProperties {
    @Value("${url}") // 在注解参数的大括号的值,是jdbc.properties配置中等于号左侧的名称
    private String url;
    @Value("${driver}")
    private String driver;
    // 生成以上2个属性的Getters & Setters
}

全部完成后,可以自定义某个类,用于测试运行:

package cn.tedu.spring;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class SpringTests {
	
	public static void main(String[] args) {
		// 1. 加载配置类,得到Spring容器
		AnnotationConfigApplicationContext ac 
			= new AnnotationConfigApplicationContext(SpringConfig.class);

		// 2. 从Spring容器中获取对象
		JdbcProperties jdbcProperties
			= (JdbcProperties) ac.getBean("jdbcProperties");
		
		// 3. 测试
		System.out.println(jdbcProperties.getUrl());
		System.out.println(jdbcProperties.getDriver());

		// 4. 关闭
		ac.close();
	}

}

注意:在类似于jdbc.properties这样的配置文件中,如果某个属性的名称是username,且最终项目是在Windows操作系统的平台上运行时,读取到的值将是“当前登录Windows系统的系统用户名称”,而不是jdbc.properties文件中配置的属性值!所以,一般推荐在编写jdbc.properties这类配置文件时,各属性之前最好都添加一些特有的前缀,使得属性名一定不与某些关键名称发生冲突,例如:

project.jdbc.url=jdbc:mysql://localhost:3399/db_name
project.jdbc.driver=com.mysql.jdbc.Driver
project.jdbc.username=root
project.jdbc.password=1234

并且,在使用@Value注解时,也配置为以上各个等于号左侧的完整名称:

@Component
@PropertySource("jdbc.properties")
public class JdbcProperties {

	@Value("${project.jdbc.url}")
	private String url;
	@Value("${project.jdbc.driver}")
	private String driver;
	@Value("${project.jdbc.username}")
	private String username;
	@Value("${project.jdbc.password}")
	private String password;
    
    // Getters & Setters
    
}

最后,使用Spring框架时,如果属性的值是由Spring框架进行赋值的,Spring框架会自动的处理数据类型的转换,所以,在声明属性时,声明为所期望的类型即可,例如,在配置文件中存在:

project.jdbc.initialSize=5
project.jdbc.maxTotal=20

这2个属性分别表示“初始化连接数”和“最大连接数”,应该是数值类型的,在类中声明属性时,就可以使用intInteger类型:

@Value("${project.jdbc.initialSize}")
private int initialSize;
@Value("${project.jdbc.maxTotal}")
private int maxTotal;

当然,必须保证类型的转换是可以成功的,例如数字5既可以转换为String,又可以是intInteger,所以,声明以上initialSize时,这几个数据类型都是可用的,根据使用需求进行选取即可!

另外,还有另一种做法读取**.properties**类型的文件,就是使用@Autowired注解为Environment类型的属性自动赋值:

@Component
@PropertySource("jdbc.properties")
public class JdbcProperties {
	
	@Autowired
	private Environment environment;

	public Environment getEnvironment() {
		return environment;
	}

	public void setEnvironment(Environment environment) {
		this.environment = environment;
	}

}

最终,测试运行:

package cn.tedu.spring;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class SpringTests {
	
	public static void main(String[] args) {
		// 1. 加载配置类,得到Spring容器
		AnnotationConfigApplicationContext ac 
			= new AnnotationConfigApplicationContext(SpringConfig.class);

		// 2. 从Spring容器中获取对象
		JdbcProperties jdbcProperties
			= (JdbcProperties) ac.getBean("jdbcProperties");
		
		// 3. 测试
		System.out.println(jdbcProperties.getEnvironment().getProperty("project.jdbc.url"));
		System.out.println(jdbcProperties.getEnvironment().getProperty("project.jdbc.driver"));
		System.out.println(jdbcProperties.getEnvironment().getProperty("project.jdbc.username"));
		System.out.println(jdbcProperties.getEnvironment().getProperty("project.jdbc.password"));
		System.out.println(jdbcProperties.getEnvironment().getProperty("project.jdbc.initialSize"));
		System.out.println(jdbcProperties.getEnvironment().getProperty("project.jdbc.maxTotal"));

		// 4. 关闭
		ac.close();
	}

}

可以看到,使用这种做法时,Spring框架会把读取到的所有配置信息都封装到了Environment类型的对象中,当需要获取某个配置值时,调用Environment对象的getProperty()方法再获取,同时,getProperty()方法返回的是String类型的数据,如果希望的数据类型不是String,则需要开发人员自行转换类型!

一般,还是推荐使用@Value注解逐一读取各配置值,使用起来更加灵活一些!

抽象类与接口的区别

1. 共同点

都可以包含抽象方法;

2. 区别
  • 抽象类是一种“类”,是使用class作为关键字来声明的;而接口是另一种数据,是使用interface作为关键字来声明的;
  • 抽象类中可以有各种权限不同、修饰符不同的属性,也可以包含普通方法、抽象方法,或者完全没有普通方法,或者完全没有抽象方法;而接口中的所有成员都是public的,所有属性都是staticfinal的,在JDK 1.8之前,所有的方法都是抽象的;
  • 普通的类与抽象类的关系是“继承”的关系,当普通的类继承了抽象类后,就有义务重写抽象类中的抽象方法,在Java语句中,类之间的继承是1对1的关系;普通的类与接口的关系是”实现“的关系,当普通的类实现了接口后,也有义务重写接口中的所有抽象方法,类与接口的实现关系是1对多的,即1个类可以同时实现若干个接口;接口与接口之间也可以存在继承关系,且是1对多的关系,即某1个接口可以同时继承若干个接口;
3. 使用心得 / 装

类,是描述”类别“的;接口,是描述形为模式、行为特征、规范、标准的!

类与类之间是is a的关系;类与接口之间是has a的关系。

public class Person { public String name; }
public class Student extends Person {}
public class Teacher extends Person {}

public class Animal { }
public class Cat extends Animal {}

public interface 学习 { void 学习(某参数); }
public interface 授课 {}
public interface 驾驶 { void 驾驶(某参数); }
public class Person implements 学习, 授课, 驾驶 {}

Person 张三 = new Person();
Person 李四 = new Person();

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值