SprintBoot入土指南

3 篇文章 0 订阅
1 篇文章 0 订阅

SprintBoot入土指南


1. SpringBoot简介

  • SpringBoot的来历:在早起的Spring中,人们只能使用xml来配置,自从jdk5加入注解之后,Spring也开始加入注解。后来注解越来越成为主流,Pivotal干脆就开发了SpringBoot来使开发人员快速配置Spring。
  • SpringBoot优点:
    1. 创建独立的Spring程序;
    2. 嵌入TomCat、Jetty、Undertow,无需部署WAR文件;
    3. 通过maven来根据需要获取starter;
    4. 自动配置Spring;
    5. 没有代码生成,对XML没有要求配置;
    6. 提供生产就绪行功能,如指标、健康检查和外部配置。
  • SpringBoot思想:约定大于配置

2. IDEA搭建SpringBoot环境

  1. 创建SpringBoot项目

  1. 选择jdk版本和项目名

  1. 选择需要使用的类(后期也可以自己加入)

  1. 选择文件位置


2.1 Springboot依赖

  • 打开工程目录下的pom.xml文件,pom.xml文件中就是项目所需的各种库的依赖。

=


2.2 使用自定义配置

  • 除了使用SpringBoot提供的默认配置,我们同样可以使用自定义配置来配置SpringBoot。
  • SpringBoot全局配置文件位置src/main/resources下的名为application的properties或yml文件。我们以appplication.properties为例修改tomcat默认端口位8081。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fEkcJ1Ts-1590271378361)(/Users/liumengran/Library/Application Support/typora-user-images/image-20200519183433061.png)]


3 IoC控制反转

  • Spring最核心的两个概念就是:IoC控制反转和AOP面向切面编程。我们先来介绍IoC。
  • Ioc是什么?IoC是一种区别于通过new创建的对象的通过描述来生成或者获取对象的技术。IoC可以通过XML和注解来进行开发,因为SpringBoot建议使用注解的方式来描述生成对象,所以我们用全注解的方式进行描述Spring IoC。首先我们先明确两个概念:
    • Spring Bean和IoC容器:Spring Bean简称Bean,比如说有学生、老师两个对象,为了描述两者之间的关系,我们需要一个容器。在Spring中我们将需要管理的对象成为Spring Bean,二将管理这些Bean的容器称为IoC容器。

3.1 IoC容器

  • IoC容器是管理Bean的容器,所有的IoC容器都需要实现接口BeanFactory我们首先看一下BeanFactory的源码
public interface BeanFactory {
    String FACTORY_BEAN_PREFIX = "&";

  //多个getBean方法
    Object getBean(String var1) throws BeansException;

    <T> T getBean(String var1, Class<T> var2) throws BeansException;

    Object getBean(String var1, Object... var2) throws BeansException;

    <T> T getBean(Class<T> var1) throws BeansException;

    <T> T getBean(Class<T> var1, Object... var2) throws BeansException;

    <T> ObjectProvider<T> getBeanProvider(Class<T> var1);

    <T> ObjectProvider<T> getBeanProvider(ResolvableType var1);

  //是否包含Bean
    boolean containsBean(String var1);

  //是否单例
    boolean isSingleton(String var1) throws NoSuchBeanDefinitionException;

  //是否原型
    boolean isPrototype(String var1) throws NoSuchBeanDefinitionException;

  //是否类型匹配
    boolean isTypeMatch(String var1, ResolvableType var2) throws NoSuchBeanDefinitionException;

    boolean isTypeMatch(String var1, Class<?> var2) throws NoSuchBeanDefinitionException;

  //获取Bean类型
    @Nullable
    Class<?> getType(String var1) throws NoSuchBeanDefinitionException;

    @Nullable
    Class<?> getType(String var1, boolean var2) throws NoSuchBeanDefinitionException;

  //获取Bean别名
    String[] getAliases(String var1);
}
  • 我们分析下其中几个重要方法:
    1. getBean:从IoC容器中获取Bean方法
    2. isSingleton:是否是单例
    3. isPrototype:是否是多例,如果返回结果为TRUE,则getBean的时候会创建一个新的Bean返回。

3.2 装配Bean

方式一

假设我们有一个User类如下:

public class User {
    
    private Long id;
    private String userName;
    private String note;

    public Long getId() {
        return id;
    }

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

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getNote() {
        return note;
    }

    public void setNote(String note) {
        this.note = note;
    }
}

我们在UserConfig中进行Bean装配

/**
 * @Configuration 代表此类是一个配置文件
 */
@Configuration
public class UserConfig {

    /**
     * @Bean 表明将initUser返回到IoC容器中,
     * 其中name代表名称,如果不写则将initUser作为Bean名称
     * @return
     */
    @Bean(name = "user")
    public User initUser(){
        User user = new User();
        user.getId(Long.valueOf(1));
        user.setUserName("UserName");
        user.setNote("Note");
        
        return user;
    }
}
方式二
  • 通过方式一进行一个个注入@Bean过于麻烦。我们可以使用SpringBoot通过扫描进行Bean装配。使用的注解是@Component
  • 此方法需要将User和UserConfig放入同一个包中

同样用User类和UserConfig类进行演示,但是需要进行一点点改动。装配方法如下:

User类

/**
 * @Component("user") 表明被spring IoC自动扫描装配
 */
@Component("user")
public class User {

    @Value("1")
    private Long id;
    @Value("UserName")
    private String userName;
    @Value("Note")
    private String note;

    public Long getId(Long aLong) {
        return id;
    }

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

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getNote() {
        return note;
    }

    public void setNote(String note) {
        this.note = note;
    }
}

AppConfig类

/**
 * @Configuration 代表此类是一个配置文件
 * @ComponentScan 表明进行扫描,但是只会扫描本类及其当前包及其子包,这也是为什么要把User类放入同一个包中
 */
@ComponentScan
@Configuration
public class UserConfig {
    
}
  • 因为在实际开发中我们为了有更好的目录结构不能将这两个类放入同一个包下,所以我们需要引入@ComponentScan进行扫描自定义的包,首先来分析下@ComponentScan源码看看有哪些配置
package org.springframework.context.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.core.annotation.AliasFor;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {
    //定义扫描的包
    @AliasFor("basePackages")
    String[] value() default {};
    //定义扫描的包
    @AliasFor("value")
    String[] basePackages() default {};

    //定义扫描的类
    Class<?>[] basePackageClasses() default {};

    //Bean name生成器
    Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

    //作用域解析
    Class<? extends ScopeMetadataResolver> scopeResolver() default AnnotationScopeMetadataResolver.class;

    //作用域代理模式
    ScopedProxyMode scopedProxy() default ScopedProxyMode.DEFAULT;

    //资源匹配模式
    String resourcePattern() default "**/*.class";

    //是否启用默认的过滤器
    boolean useDefaultFilters() default true;

    //当满足过滤器的条件时扫描
    ComponentScan.Filter[] includeFilters() default {};

    //当不满足过滤器条件时扫描
    ComponentScan.Filter[] excludeFilters() default {};

    //是否延迟初始化
    boolean lazyInit() default false;

    //定义过滤器
    @Retention(RetentionPolicy.RUNTIME)
    @Target({})
    public @interface Filter {
        //过滤器类型(注解或正则表达式)
        FilterType type() default FilterType.ANNOTATION;

        //定义过滤的类
        @AliasFor("classes")
        Class<?>[] value() default {};

        //定义过滤的类
        @AliasFor("value")
        Class<?>[] classes() default {};
        //匹配方式
        String[] pattern() default {};
    }
}

假设User类在com.example.linxi.model下我们只需要对UserConfig的@ComponentScan进行如下修改就可以进行扫描装配

/**
 * @Configuration 代表此类是一个配置文件
 * @ComponentScan 中的basePackages代表扫描的包
 * @ComponentScan 也支持正则表达式,比如com.example.linxi.*就是扫描com.example.linxi下所有的包
 */
@ComponentScan(basePackages = {"com.example.linxi.model"})
@Configuration
public class UserConfig {

}

3.3 依赖注入

  • 什么是依赖注入?在前面,我们进行了Bean的装配,但是Bean之间的依赖(关系)我们并未进行标明,二确立这种关系的方法就叫做依赖注入。
3.3.1 @Autowired
  • @Autowired 是Spring中最常用的注解之一,她代表着按照类型寻找相应的Bean进行注入。例如:

    1. 首先我们定义几个接口:Person、Bussiness、Animal、Cat、Dog。
    public interface Person {
    
        //调用动物
        public void service();
    
        //设置动物
        public void setAnimal(Animal animal);
    
    }
    
    
    public interface Animal {
        public void use();
    }
    
    
    public class Dog implements Animal{
        @Override
        public void use() {
            System.out.println("狗的使用");
        }
    }
    
    public class Bussiness implements Person{
        @Autowired
        private Animal animal;
    
        @Override
        public void service() {
            this.animal.use();
        }
    
        @Override
        public void setAnimal(Animal animal) {
            this.animal = animal;
        }
    }
    

    上述代码第25行使用了@Autowired进行依赖注入,此时Spring会根据Animal来找到相应的Bean进行注入,注入后我们就可以使用Dog.use()实现相应的功能。

    1. 如果我们增加一个Cat类,那么就会出现一个Animal两个实现类,Spring IoC不知道应该注入哪一个,从而引起错误,方式一是将注入方式改为

      @Autowired
      private Animal dog;
      

      进行注入,但是太笨拙了。所以我们引入一个新的注解@Qualifier解决这个问题。方式如下:

      @Autowired
      @Qualifier("dog")
      private Animal animal;
      

      使用这种方法我们就可以优雅的将dog进行注入。

3.3.2 有参构造方法进行依赖注入

上述我们讨论的都是在无参构造方法的的情况下进行依赖注入,但是一旦存在有参构造方法就无法使用上述方法进行依赖注入。我们修改Bussiness进行演示.

public class Bussiness implements Person{
    
    
    private Animal animal;

    public Bussiness(@Autowired @Qualifier("dog") Animal animal) {
        this.animal = animal;
    }

    @Override
    public void service() {
        this.animal.use();
    }

    @Override
    public void setAnimal(Animal animal) {
        this.animal = animal;
    }
}

可见,我们没有在Animal上进行注入,而是在构造函数中进行注入。


3.4 读取配置文件

  • Spring boot使用配置文件,可以使用application.properties默认配置文件或者自定义配置文件。
  • 下面我们用读取数据库连接信息作为例子;

jdbc.properties文件如下

database.driverName=com.mysql.cj.jdbc.Driver
database.url=jdbc:mysql://localhost:3306/test
database.username=root
database.password=123456

创建实体类读取信息并创建Bean。读取方式有以下几种:

3.4.1 方式一@Value

@value可以在变量处或setter上使用,效果相同

/**
 * @Component 表明SpringBoot将为此类创建Bean
 */
@Component
/**
 * @PropertySource 中value是属性名
 */
@PropertySource(value = {"classpath:jdbc.properties"})
public class DataBaseProperties {

    @Value("${database.driverName}")
    private String driverName = null;
    @Value("${database.url}")
    private String url = null;
    private String username = null;
    private String password = null;

    public String getDriverName() {
        return driverName;
    }

    public void setDriverName(String driverName) {
        this.driverName = driverName;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getUsername() {
        return username;
    }

    @Value("${database.username}")
    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    @Value("${database.password}")
    public void setPassword(String password) {
        this.password = password;
    }
}
3.4.2 方式二@ConfigurationProperties
/**
 * @Component 表明SpringBoot将为此类创建Bean
 */
@Component
/**
 * @PropertySource 中value是属性名
 */
@PropertySource(value = {"classpath:jdbc.properties"})
/**
 * @ConfigurationProperties 中写入database,Springboot会自动假设类中的全限定类名去配置文件中查找
 */
@ConfigurationProperties("database")
public class DataBaseProperties {

    //查找database.driverName
    private String driverName = null;
    private String url = null;
    private String username = null;
    private String password = null;

    public String getDriverName() {
        return driverName;
    }

    public void setDriverName(String driverName) {
        this.driverName = driverName;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

3.5 条件装配

  • 以上文装配mysql相关配置为例,有的时候会存在配置文件有错误导致程序报错抛出异常,而我们想让我们的程序及时读取配置文件出错也不要终止允许。这就引入了条件装配@Conditional
public class DataConfig {

    @Bean(name = "dataSource" ,destroyMethod = "close")
    @Conditional(DatabaseConditional.class)
    public DataSource getDataSource(
            @Value("${database.driverName}") String driverName,
            @Value("${database.url}") String url,
            @Value("${database.username}") String username,
            @Value("${database.password}") String password){

        Properties properties = new Properties();

        properties.setProperty("driver",driverName);
        properties.setProperty("url",url);
        properties.setProperty("username",username);
        properties.setProperty("password",password);

        DataSource dataSource = null;
        try {
            dataSource = BasicDataSourceFactory.createDataSource(properties);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return dataSource;
    }
}

使用方法如下:

public class DatabaseConditional implements Condition {

    /**
     * 数据库装配条件
     * @param conditionContext 条件上下文
     * @param annotatedTypeMetadata 注释类型元数据
     * @return true装配Bean,否则不装配
     */
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        //取出环境变量
        Environment environment = conditionContext.getEnvironment();
        //判断属性文件是否存在对应的数据库配置
        return environment.containsProperty("database.driverName")
        && environment.containsProperty("database.url")
        && environment.containsProperty("database.username")
        && environment.containsProperty("database.password");
    }
}

3.6 Spring EL

  • Spring EL是一种强大,简洁的装配Bean的方式,他可以通过运行期间执行的表达式将值装配到我们的属性或构造函数当中,更可以调用JDK中提供的静态常量,获取外部Properties文件中的的配置

关于Spring EL我们直接用较为直观的例子进行说明。

    //赋值字符串
    @Value("#{'赋值字符串'}")
    private String string = null;
    
    //科学技术法赋值
    @Value("#{6.6E3}")
    private double d;
    
    //浮点数赋值
    @Value("#{3.14}")
    private float pai;
    
    //使用Bean属性赋值
    @Value("#{beanName.str}")
    private String string1 = null;
    
    //使用Bean属性对应的大写进行赋值
    @Value("#{beanName.str?.toUpperCase}")
    private String string2 = null;
    
    //数学运算
    @Value("#{1+2}")
    private int anInt;
    
    //浮点数比较
    @Value("#{beanName.pai == 3.14f}")
    private boolean aBoolean;
    
    //字符串比较运算
    @Value("#{beanName.str eq 'Spring Boot'}")
    private boolean string3;
    
    //字符串连接
    @Value("#{beanName.str + '字符串连接'}")
    private String string4 = null;
    
    //三元运算
    @Value("#{beanName.d > 1000 ? '大于' : '小于'}")
    private String string5 = null;

4 Spring AOP

4.1 约定编程

比如我们有一个useAround方法,在执行这个方法之前我们需要做一些准备,即before方法。在之后如果没有异常需要对结果进行返回。流程图如下:

整个过程就是按照此流程进行。如果传入userAround则调用userAround,否则执行原方法,如果存在异常则抛出异常,否则返回。整个过程我们封装在一个拦截器方法中,我们只需要传入参数即可,执行的过程都已经被约定好了,只能出现几种可控的结果,这种编程方式叫做约定编程。

4.2 AOP是什么

Spring AOP实际上就是一种约定流程的编程,只不过是流程都已经被Spring 约定好了,在Spring中可以多种方式进行配置AOP,我们以@AspectJ为例进行解释。

4.2.1 为什么使用AOP

以数据库事务为例,当我们使用插入的时候,我们需要先进行数据库连接、然后执行SQL语句,成功则提交事务,如果失败需要回滚事务,最后再释放数据库连接资源。流程图如下:

这其中在获取数据库连接、回滚事务、释放连接都需要使用try-catch,过于臃肿,企鹅存在大量重复工作,但是如果我们将整个过程进行封装,只需要用户输入SQL语句,然后使用一个注解将SQL语句植入流程中,将大大简化重复代码量。提升开发效率。

4.2.2 AOP术语和流程
  • 连接点:对应具体拦截对象,因为Spring只支持方法,所以被拦截的对象往往是特定的方法
  • 切点:有的时候切面不仅仅用于单个方法,可以是多个类的不同方法,可以通过增则表达式和指示器的规则进行定义。切点即提供此功能的概念
  • 通知:按照约定流程下的方法,分为前置通知、后置通知、环绕通知、事务返回通知、异常通知
  • 目标对象:被代理对象
  • 引入:引入新的类和器方法、增强现有Bean功能
  • 织入:通过动态代理技术,为原油服务对象生成代理对象,然后将与切点定义匹配的连接点拦截,并按照约定的方法将各类的通知织入约定流程的过程。
  • 切面:一个可以定义切点各类通知和引入的内容,Spring AOP将通过切面的信息增强bean的功能或者将对应的方法织入流程。


4.3 AOP开发
  • AOP开发流程如下:
    1. 确定连接点;
    2. 开发切面;
    3. 定义切点;
    4. 测试AOP。
4.3.1 确定连接点

在AOP编程中,我们需要确定什么地方需要AOP,即确定连接点(Spring中就是什么类中的什么方法),假设我们有一个printUser方法在UserService接口中。

接口和实现类如下

//接口类
public interface UserService {
    public void printUser(User user);
}
//实现类
public class UserServiceImpl implements UserService {

    @Override
    public void printUser(User user) {
        if (user == null){
            throw new RuntimeException("检查用户参数是否为空");
        }

        System.out.println(user.getUserId());
        System.out.println(user.getUserName());
    }
}
4.3.2 开发切面

有了连接点之后我们需要确定切面,通过切面描述AOP的其他信息,用以描述流程的织入。

切面类如下:

//代表此类为切面
@Aspect
public class MyAspect {

    /**
     * 切面中我们一班要定义四个方法:用来分别代表Spring在执行切点之前、之后、返回、异常时分别进行哪些动作
     * 我们定义好后并辅以相应的注解,SpringBoot即可将其自动织入流程中。
     */

    //定义前方我们的流程中的Before方法
  	//正则表达式匹配了对应的方法并确定对应的连接点是否启用切面编程
    /**
       * 注解中正则表达式规则:
       *  execution() 表示执行的时候拦截相应正则对应的方法
       *  * 表示任意返回类型的方法
       *  com.example.linxi.service.serviceImpl.UserServiceImpl 指定目标对象全限定类名
       *  printUser 指定目标方法(连接点)
       *  (..) 任意参数进行匹配
       */
    @Before("execution(*com.example.linxi.service.serviceImpl.UserServiceImpl.printUser(..))")
    public void before(){
        System.out.println("===before===");
    }
    //定义前方我们的流程中的After方法
    @After("execution(*com.example.linxi.service.serviceImpl.UserServiceImpl.printUser(..))")
    public void after(){
        System.out.println("===after===");
    }
    //定义前方我们的流程中的AfterReturning方法
    @AfterReturning("execution(*com.example.linxi.service.serviceImpl.UserServiceImpl.printUser(..))")
    public void afterReturning(){
        System.out.println("===afterReturning===");
    }
    //定义前方我们的流程中的AfterThrowing方法
    @AfterThrowing("execution(*com.example.linxi.service.serviceImpl.UserServiceImpl.printUser(..))")
    public void afterThrowing(){
        System.out.println("===afterThrowing===");
    }
}
4.3.3 定义切点

切点的引入其实是为了解决上述代码中,每一个注解都要匹配一个正则表达式的问题,提高代码整洁度,减小冗余。

切点代码如下:

//代表此类为切面
@Aspect
public class MyAspect {

    /**
     * 切面中我们一班要定义四个方法:用来分别代表Spring在执行切点之前、之后、返回、异常时分别进行哪些动作
     * 我们定义好后并辅以相应的注解,SpringBoot即可将其自动织入流程中。
     */

    /**
     * 注解中正则表达式规则:
     *  execution() 表示执行的时候拦截相应正则对应的方法
     *  * 表示任意返回类型的方法
     *  com.example.linxi.service.serviceImpl.UserServiceImpl 指定目标对象全限定类名
     *  printUser 指定目标方法(连接点)
     *  (..) 任意参数进行匹配
     */
    //定义切点
    @Pointcut("execution(*com.example.linxi.service.serviceImpl.UserServiceImpl.printUser(..) )")
    public void pointCut(){

    }

    //定义前方我们的流程中的Before方法
    //正则表达式匹配了对应的方法并确定对应的连接点是否启用切面编程
    @Before("pointCut()")
    public void before(){
        System.out.println("===before===");
    }
    //定义前方我们的流程中的After方法
    @After("pointCut()")
    public void after(){
        System.out.println("===after===");
    }
    //定义前方我们的流程中的AfterReturning方法
    @AfterReturning("pointCut()")
    public void afterReturning(){
        System.out.println("===afterReturning===");
    }
    //定义前方我们的流程中的AfterThrowing方法
    @AfterThrowing("pointCut()")
    public void afterThrowing(){
        System.out.println("===afterThrowing===");
    }
}
4.3.4 测试AOP

我们定义一个Controller进行测试

//定义控制器
@Controller
//定义请求路径
@RequestMapping("/user")
public class UserController {

    //自动注入
    @Autowired
    private UserService userService = null;
    
    //定义请求路径
    @RequestMapping("/print")
    //转换为JSON
    @ResponseBody
    public User printUser(Long id,String name){
        User user = new User();
        
        user.setUserId(id);
        user.setUserName(name);
        
        //若user==null执行afterThrowing方法
        userService.printUser(user);
        
        return user;
        
    }
}

在主类中进行调用

@SpringBootApplication
public class LinxiApplication {
    
    //定义切面
    @Bean(name = "myAspect")
    public MyAspect myAspect(){
        return new MyAspect();
    }

    public static void main(String[] args) {
        SpringApplication.run(LinxiApplication.class, args);
    }

}

运行后我们可以发现打印出的信息为:

===before===
id=1,name=userName
===after===
===afterReturning===

这也可以证明我们成功的织入了流程。


4.4 环绕通知

什么是环绕通知,我们可以将环绕通知看作是前面的四个通知的结合体。实际上环绕通知时Spring提供的一个可以在代码中手动控制增强方法合适执行的方式。

我们在MyAspect中配置一个环绕通知

@Around("pointCut()")
    public Object around(ProceedingJoinPoint joinPoint){
        Object reValue = null;
        try {
            /**
             * 将方法在此处调用即为前置通知,相当于before()
             */
            
            //回掉目标函数原有方法
            joinPoint.proceed();

            /**
             * 将方法在此处调用即为后置通知,相当于after()
             */
        } catch (Throwable t) {
            /**
             * 将方法在此处调用即为异常通知,相当于afterThrowing(
             */
            throw new RuntimeException(t);
        } finally {
            /**
             * 将方法在此处调用即为最终通知,相当于afterReturning()
             */
        }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

高压锅码农777

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

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

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

打赏作者

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

抵扣说明:

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

余额充值