Spring 是如何整合 Mybatis 注册 Mapper 接口的?

完整的项目代码见 https://codechina.csdn.net/dreaming_coder/mybatis-spring

1. 引言

以前使用 Spring 整合 Mybatis 时,应该都用过下面的方式将 Mapper 接口加入到 Spring 中:

<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
  <property name="mapperInterface" value="org.mybatis.spring.sample.mapper.UserMapper" />
  <property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>

可以看到,类型并不是 XXXMapper,而是 MapperFactoryBean,你知道为什么这么配吗?

我们新建一个 Maven 项目来看看:

在这里插入图片描述

【pom.xml】

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.ice</groupId>
    <artifactId>demo</artifactId>
    <version>1.0</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.3.8</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.7</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.25</version>
        </dependency>
    </dependencies>
</project>

【UserMapper.java】

package com.ice.mapper;

import org.apache.ibatis.annotations.Select;

public interface UserMapper {

    @Select("select 'user'")
    String selectById();
}

【UserService.java】

package com.ice.service;

import com.ice.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;


@Component
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public void test() {
        System.out.println(userMapper.selectById());
    }
}

【IceApplication.java】

import com.ice.service.UserService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;


@ComponentScan("com.ice")
public class IceApplication {
    public static void main(String[] args) {
        // 启动 Spring
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        applicationContext.register(IceApplication.class);
        applicationContext.refresh();

        // 获取 bean
        UserService userService = applicationContext.getBean("userService", UserService.class);
        userService.test();
    }
}

现在执行 main 方法可以正常执行吗?答案是不行!

容器中并没有名为 userService 的 bean, 因为 UserMapper 是接口,无法实例化,更别提创建 bean 了!那么它就无法注入到 userService 的属性中,自然,名为 userService 的 bean 并没有创建成功(注意对象和 bean 的区别)。

2. UserMapper 的依赖注入

显然,有两种方式可以选择:

  • 写一个实现类注入
  • 代理对象

当然是代理对象方便点,因为可以用动态代理节省代码量

那么, 这个代理对象是由谁来产生的, spring V.S. mybatis?答案是 mybatis,不然为啥离开 spring 它也能使用呢?

3. 创建代理对象 bean

创建 bean 有两种方式:

  • 声明式,@Bean,@Component
  • 编程式,BeanDefination

声明式的应该用的很多,这里看看编程式的。

例如我们有个类 User:

public class User {
    
}

然后,在 main 方法里这样写:

public static void main(String[] args) {
    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
    applicationContext.register(IceApplication.class);

    AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
    beanDefinition.setBeanClass(User.class);

    applicationContext.refresh();

    System.out.println(applicationContext.getBean("user", User.class));
}

可以运行成功吗?No!

在这里插入图片描述

因为光定义一个 bean 还不行,还要注册到容器中。

public static void main(String[] args) {
    // 启动 Spring
    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
    applicationContext.register(IceApplication.class);

    AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
    beanDefinition.setBeanClass(User.class);
    // 注册到容器中,给 bean 设置 name 为 user
    applicationContext.registerBeanDefinition("user", beanDefinition); 

    applicationContext.refresh();

    System.out.println(applicationContext.getBean("user", User.class));
}

在这里插入图片描述

不过,还是不能用这种方法直接注册 UserMapper:

public static void main(String[] args) {
    // 启动 Spring
    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
    applicationContext.register(IceApplication.class);

    AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
    beanDefinition.setBeanClass(UserMapper.class);
    applicationContext.registerBeanDefinition("userMapper",beanDefinition);

    applicationContext.refresh();

    System.out.println(applicationContext.getBean("userMapper", UserMapper.class));
}

在这里插入图片描述

毕竟 UserMapper 是接口,没有构造函数,所以我们可以通过 FactoryBean 来创建代理对象。

先看看 FactoryBean 怎么用的:

【IceFactoryBean.java】

package org.mybatis.spring;


import com.ice.service.User;
import org.springframework.beans.factory.FactoryBean;

public class IceFactoryBean implements FactoryBean {
    @Override
    public Object getObject() throws Exception {
        User user = new User();
        return user;
    }

    @Override
    public Class<?> getObjectType() {
        return User.class;
    }
}

【IceApplication.java】

import org.mybatis.spring.IceFactoryBean;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;


@ComponentScan("com.ice")
public class IceApplication {
    public static void main(String[] args) {
        // 启动 Spring
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        applicationContext.register(IceApplication.class);

        AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
        beanDefinition.setBeanClass(IceFactoryBean.class);
        applicationContext.registerBeanDefinition("user",beanDefinition);

        applicationContext.refresh();

        System.out.println(applicationContext.getBean("user"));
        System.out.println(applicationContext.getBean("&user"));
    }
}

在这里插入图片描述

因为我们只能指定一个名字,就是 FactoryBean 创建的 bean 的名字 user,FactoryBean 其本身也是一个 bean,名字默认是 &user

所以我们可以在 IceFactoryBean 的 getObject() 方法中返回 UserMapper 的代理对象。

【IceFactoryBean.java】

package org.mybatis.spring;


import com.ice.mapper.UserMapper;
import org.springframework.beans.factory.FactoryBean;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;


public class IceFactoryBean implements FactoryBean {
    @Override
    public Object getObject() throws Exception {
        Object o = Proxy.newProxyInstance(IceFactoryBean.class.getClassLoader(), new Class[]{UserMapper.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                return null;
            }
        });
        return o;
    }

    @Override
    public Class<?> getObjectType() {
        return UserMapper.class;
    }
}

此时再执行一开始的 main 方法:

public static void main(String[] args) {
    // 启动 Spring
    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
    applicationContext.register(IceApplication.class);

    AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
    beanDefinition.setBeanClass(IceFactoryBean.class);
    applicationContext.registerBeanDefinition("userMapper",beanDefinition);

    applicationContext.refresh();

    // 获取 bean
    UserService userService = applicationContext.getBean("userService", UserService.class);
    userService.test();
}

可以发现不报错了:

在这里插入图片描述

为什么是 null 呢?因为执行 test() 方法会调用 selectById() 方法,会通过 invoke() 方法执行,而 invoke() 方法返回的是 null,所以结果为 null。

此时还有一个问题,IceFactoryBean 不可能仅用于 UserMapper 接口,那太奢侈了,如果我还有 OrderMapper,MemberMapper 呢?

我们修改一下 IceFactoryBean

package org.mybatis.spring;


import org.springframework.beans.factory.FactoryBean;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;


public class IceFactoryBean implements FactoryBean {

    private Class mapperClass;

    public IceFactoryBean(Class mapperClass) {
        this.mapperClass = mapperClass;
    }

    @Override
    public Object getObject() throws Exception {
        Object o = Proxy.newProxyInstance(IceFactoryBean.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println(method.getName() + "--------" + mapperClass);
                return null;
            }
        });
        return o;
    }

    @Override
    public Class<?> getObjectType() {
        return mapperClass;
    }
}

现在问题,是怎么用构造方法给属性赋值。

Spring 提供了传参的方法:

beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(UserMapper.class);

现在我们修改的 main 方法如下:

import com.ice.mapper.MemberMapper;
import com.ice.mapper.OrderMapper;
import com.ice.mapper.UserMapper;
import com.ice.service.UserService;
import org.mybatis.spring.IceFactoryBean;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;


@ComponentScan("com.ice")
public class IceApplication {
    public static void main(String[] args) {
        // 启动 Spring
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        applicationContext.register(IceApplication.class);

        AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
        beanDefinition.setBeanClass(IceFactoryBean.class);
        beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(UserMapper.class);
        applicationContext.registerBeanDefinition("userMapper",beanDefinition);

        AbstractBeanDefinition beanDefinition1 = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
        beanDefinition1.setBeanClass(IceFactoryBean.class);
        beanDefinition1.getConstructorArgumentValues().addGenericArgumentValue(OrderMapper.class);
        applicationContext.registerBeanDefinition("orderMapper",beanDefinition1);

        AbstractBeanDefinition beanDefinition2 = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
        beanDefinition2.setBeanClass(IceFactoryBean.class);
        beanDefinition2.getConstructorArgumentValues().addGenericArgumentValue(MemberMapper.class);
        applicationContext.registerBeanDefinition("memberMapper",beanDefinition2);

        applicationContext.refresh();

        // 获取 bean
        UserService userService = applicationContext.getBean("userService", UserService.class);
        userService.test();
    }
}

输出结果如下:

在这里插入图片描述

这种方式在 mybatis 中有体现:

在这里插入图片描述

但是,每增加一个 XXXMapper 接口,都要在 main 方法增加一段代码,太麻烦了

4. ImportBeanDefinitionRegistrar 改写

Spring 提供了 ImportBeanDefinitionRegistrar 接口,方便我们注册 bean,我们利用它来改写之前在 main 中繁琐的代码:

【IceImportBeanDefinitionRegistrar.java】

package org.mybatis.spring;

import com.ice.mapper.MemberMapper;
import com.ice.mapper.OrderMapper;
import com.ice.mapper.UserMapper;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;


public class IceImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
        AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
        beanDefinition.setBeanClass(IceFactoryBean.class);
        beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(UserMapper.class);
        registry.registerBeanDefinition("userMapper", beanDefinition);

        AbstractBeanDefinition beanDefinition1 = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
        beanDefinition1.setBeanClass(IceFactoryBean.class);
        beanDefinition1.getConstructorArgumentValues().addGenericArgumentValue(OrderMapper.class);
        registry.registerBeanDefinition("orderMapper", beanDefinition1);

        AbstractBeanDefinition beanDefinition2 = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
        beanDefinition2.setBeanClass(IceFactoryBean.class);
        beanDefinition2.getConstructorArgumentValues().addGenericArgumentValue(MemberMapper.class);
        registry.registerBeanDefinition("memberMapper", beanDefinition2);
    }
}

启动类改动如下:

import com.ice.service.UserService;
import org.mybatis.spring.IceImportBeanDefinitionRegistrar;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;


@ComponentScan("com.ice")
@Import(IceImportBeanDefinitionRegistrar.class)
public class IceApplication {
    public static void main(String[] args) {
        // 启动 Spring
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        applicationContext.register(IceApplication.class);
        applicationContext.refresh();

        // 获取 bean
        UserService userService = applicationContext.getBean("userService", UserService.class);
        userService.test();
    }
}

执行结果:·

在这里插入图片描述

5. 扫描改写

前面的方法虽然启动的时候清爽了,但是还是有些繁琐,因为 ImportBeanDefinitionRegistrar 也应该是中间件的内容,不能耦合业务类,所以应该用扫描来注册。

【IceMapperScanner.java】

package org.mybatis.spring;


import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;

import java.util.Set;

public class IceMapperScanner extends ClassPathBeanDefinitionScanner {

    public IceMapperScanner(BeanDefinitionRegistry registry) {
        super(registry);
    }

    @Override
    protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
        Set<BeanDefinitionHolder> beanDefinitionHolders = super.doScan(basePackages);
        for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders) {
            BeanDefinition beanDefinition = beanDefinitionHolder.getBeanDefinition();
            beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanDefinition.getBeanClassName());
            beanDefinition.setBeanClassName(IceFactoryBean.class.getName());
        }
        return beanDefinitionHolders;
    }

    @Override
    // 我们只关心接口,所以重写时要覆盖Spring的默认规则
    protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
        return beanDefinition.getMetadata().isInterface();
    }
}

【IceImportBeanDefinitionRegistrar.java】需要做点改动:

package org.mybatis.spring;

import com.ice.mapper.MemberMapper;
import com.ice.mapper.OrderMapper;
import com.ice.mapper.UserMapper;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.TypeFilter;

import java.io.IOException;


public class IceImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {

        String path = "com.ice.mapper";
        IceMapperScanner iceMapperScanner = new IceMapperScanner(registry);
        // 这里是因为 Spring 默认会排除一些文件,我们希望所有的都被扫描
        iceMapperScanner.addIncludeFilter(new TypeFilter() {
            @Override
            public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
                return true;
            }
        });
        iceMapperScanner.scan(path);
    }
}

此时我们再添加一个 XXXMapper 的情况下,输出结果为:

在这里插入图片描述

目前,还有几个问题:

  • 扫描路径是写死的
  • XXXMapper 的代理对象目前你是我们自己生成的,我们需要 mybatis 生成的代理对象

Mybatis 是怎么生成代理对象的呢?

sqlSession.getMapper(XXMapper.class);

下面我们改写使其产生 Mybatis 生成的代理对象:

【IceFactoryBean.java】

package org.mybatis.spring;


import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.annotation.Autowired;


public class IceFactoryBean implements FactoryBean {

    private Class mapperClass;

    private SqlSession sqlSession;

    @Autowired
    public void setSqlSession(SqlSessionFactory sqlSessionFactory) {
        sqlSessionFactory.getConfiguration().addMapper(mapperClass);
        this.sqlSession = sqlSessionFactory.openSession();
    }

    public IceFactoryBean(Class mapperClass) {
        this.mapperClass = mapperClass;
    }

    @Override
    public Object getObject() throws Exception {
        Object mapper = sqlSession.getMapper(mapperClass);
        return mapper;
    }

    @Override
    public Class<?> getObjectType() {
        return mapperClass;
    }
}

然后需要配置 bean

@Bean
public SqlSessionFactory sqlSessionFactory() throws IOException {
    InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    return sqlSessionFactory;
}

这样,我们再执行时,就会调用真的 SQL 查数据库了,将刚刚 test() 方法中的语句结果 print 可以看到:

在这里插入图片描述

下面解决扫描路径的问题,因为现在路径还是写死的。

我们定义一个注解:

【IceScan.java】

package org.mybatis.spring;

import org.springframework.context.annotation.Import;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(IceImportBeanDefinitionRegistrar.class)
public @interface IceScan {
    String value();
}

然后在启动类基于该注解指定扫描路径:

@ComponentScan("com.ice")
@IceScan("com.ice.mapper")
public class IceApplication {
    // ...
}

注意,@Import 的位置移动到注解上了,这是因为 Spring 启动会扫描配置类的注解,这样 IceScan 接口的值就被扫描到了,同时 Spring 还会继续看注解上是否有 @Import 注解,如果有,则会执行注册 bean 的方法,此时,IceScan 注解的值就传进去了。

此时,IceImportBeanDefinitionRegistrar 类应该改成下面这样:

【IceImportBeanDefinitionRegistrar.java】

package org.mybatis.spring;

import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.TypeFilter;

import java.io.IOException;
import java.util.Map;


public class IceImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {

        Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(IceScan.class.getName());
        String path = (String) annotationAttributes.get("value");

        IceMapperScanner iceMapperScanner = new IceMapperScanner(registry);
        // 这里是因为 Spring 默认会排除一些文件,我们希望所有的都被扫描
        iceMapperScanner.addIncludeFilter(new TypeFilter() {
            @Override
            public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
                return true;
            }
        });
        iceMapperScanner.scan(path);
    }
}

此时仍然可以运行:

在这里插入图片描述

6. 替换所有 Spring 注解

作为中间件,不太适合用一些其他包的注解,那如何进行 set 注入呢?

首先,删除 IceFactoryBean 中 的 @Autowired 注解,然后,进行下面这样的修改:

public class IceMapperScanner extends ClassPathBeanDefinitionScanner {

    // ...
    @Override
    protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
        Set<BeanDefinitionHolder> beanDefinitionHolders = super.doScan(basePackages);
        for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders) {
            GenericBeanDefinition beanDefinition = (GenericBeanDefinition) beanDefinitionHolder.getBeanDefinition();
            beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanDefinition.getBeanClassName());
            beanDefinition.setBeanClassName(IceFactoryBean.class.getName());
            // 配置注入模型
            beanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
        }
        return beanDefinitionHolders;
    }

    // ...
}
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值