面试官:为什么Mybatis就能直接调用userMapper接口的方法?

面试官:MyBatis批量插入的五种方式,哪种最强? 前言这里我列举了MyBatis和MyBatis-Plus常用的五种批量插入的方式,进行了详细的总结归纳,写的非常详细,整体思路清晰明了,只分享干货。准备工作MyBatis利用For循环批量插入MyBatis的手动批量提交MyBatis以集合方式批量新增(推荐)MyBatis-Plus提供的SaveBatch方法MyBatis-Plus提供的InsertBatchSomeColumn方法(推荐)总结 2、配置yml文件 3、公用的User类 二、MyBatis利用For循环批量插入 1、编写 阅读详情

先上案例代码,这样大家可以更加熟悉是如何使用的,看过Mybatis系列的小伙伴,对这段代码差不多都可以背下来了。

哈哈~,有点夸张吗?不夸张的,就这行代码。

    public class MybatisApplication {
        public static final String URL = "jdbc:mysql://localhost:3306/mblog";
        public static final String USER = "root";
        public static final String PASSWORD = "123456";
    
        public static void main(String[] args) {
            String resource = "mybatis-config.xml";
            InputStream inputStream = null;
            SqlSession sqlSession = null;
            try {
                inputStream = Resources.getResourceAsStream(resource);
                SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
                sqlSession = sqlSessionFactory.openSession();
                //今天主要这行代码
                UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
                System.out.println(userMapper.selectById(1));
    
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                sqlSession.close();
            }
        }

看源码有什么用?

通过源码的学习,我们可以收获Mybatis的核心思想和框架设计,另外还可以收获设计模式的应用。

Mybatis配置文件解析到获取SqlSession,下面我们来分析从SqlSession到userMapper:

     UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

已经知道了这里的sqlSession使用的是默认实现类DefaultSqlSession。所以我们直接进入DefaultSqlSession的getMapper方法。

    //DefaultSqlSession中  
    private final Configuration configuration;
    //type=UserMapper.class
    @Override
    public <T> T getMapper(Class<T> type) {
      return configuration.getMapper(type, this);
    }

这里有三个问题:

问题1:getMapper返回的是个什么对象?

上面可以看出,getMapper方法调用的是Configuration中的getMapper方法。然后我们进入Configuration中

    //Configuration中  
    protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
    ////type=UserMapper.class
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        return mapperRegistry.getMapper(type, sqlSession);
    }

这里也没做什么,继续调用MapperRegistry中的getMapper:

    //MapperRegistry中
    public class MapperRegistry {
      //主要是存放配置信息
      private final Configuration config;
      //MapperProxyFactory 的映射
      private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
    
      //获得 Mapper Proxy 对象
      //type=UserMapper.class,session为当前会话
      public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        //这里是get,那就有add或者put
        final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
        if (mapperProxyFactory == null) {
          throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
        }
       try {
          //创建实例
          return mapperProxyFactory.newInstance(sqlSession);
        } catch (Exception e) {
          throw new BindingException("Error getting mapper instance. Cause: " + e, e);
        }
      }
      
      //解析配置文件的时候就会调用这个方法,
      //type=UserMapper.class
      public <T> void addMapper(Class<T> type) {
        // 判断 type 必须是接口,也就是说 Mapper 接口。
        if (type.isInterface()) {
            //已经添加过,则抛出 BindingException 异常
            if (hasMapper(type)) {
                throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
            }
            boolean loadCompleted = false;
            try {
                //添加到 knownMappers 中
                knownMappers.put(type, new MapperProxyFactory<>(type));
                //创建 MapperAnnotationBuilder 对象,解析 Mapper 的注解配置
                MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
                parser.parse();
                //标记加载完成
                loadCompleted = true;
            } finally {
                //若加载未完成,从 knownMappers 中移除
                if (!loadCompleted) {
                    knownMappers.remove(type);
                }
            }
        }
    }
    }

MapperProxyFactory对象里保存了mapper接口的class对象,就是一个普通的类,没有什么逻辑。

在MapperProxyFactory类中使用了两种设计模式:

  1. 单例模式methodCache(注册式单例模式)。
  2. 工厂模式getMapper()。

继续看MapperProxyFactory中的newInstance方法。

    public class MapperProxyFactory<T> {
      private final Class<T> mapperInterface;
      private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();
    
      public MapperProxyFactory(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
      }
     public T newInstance(SqlSession sqlSession) {
      //创建MapperProxy对象
      final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
      return newInstance(mapperProxy);
    }
    //最终以JDK动态代理创建对象并返回
     protected T newInstance(MapperProxy<T> mapperProxy) {
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
    }
    }

从代码中可以看出,依然是稳稳地基于 JDK Proxy 实现的,而 InvocationHandler 参数是 MapperProxy 对象。

    //UserMapper 的类加载器
    //接口是UserMapper
    //h是mapperProxy对象
    public static Object newProxyInstance(ClassLoader loader,
                                              Class<?>[] interfaces,
                                           InvocationHandler h){
    }

问题2:为什么就可以调用他的方法?

上面调用newInstance方法时候创建了MapperProxy对象,并且是当做newProxyInstance的第三个参数,所以MapperProxy类肯定实现了InvocationHandler。

进入MapperProxy类中:

    //果然实现了InvocationHandler接口
    public class MapperProxy<T> implements InvocationHandler, Serializable {
    
      private static final long serialVersionUID = -6424540398559729838L;
      private final SqlSession sqlSession;
      private final Class<T> mapperInterface;
      private final Map<Method, MapperMethod> methodCache;
    
      public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
        this.sqlSession = sqlSession;
        this.mapperInterface = mapperInterface;
        this.methodCache = methodCache;
      }
      //调用userMapper.selectById()实质上是调用这个invoke方法
      @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
          //如果是Object的方法toString()、hashCode()等方法  
          if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
          } else if (method.isDefault()) {
            //JDK8以后的接口默认实现方法  
            return invokeDefaultMethod(proxy, method, args);
          }
        } catch (Throwable t) {
          throw ExceptionUtil.unwrapThrowable(t);
        }
        //创建MapperMethod对象
        final MapperMethod mapperMethod = cachedMapperMethod(method);
        //下一篇再聊
        return mapperMethod.execute(sqlSession, args);
      }
    }

也就是说,getMapper方法返回的是一个JDK动态代理对象(类型是$Proxy+数字)。这个代理对象会继承Proxy类,实现被代理的接口UserMpper,里面持有了一个MapperProxy类型的触发管理类。

当我们调用UserMpper的方法时候,实质上调用的是MapperProxy的invoke方法。

userMapper=$Proxy6@2355。

为什么要在MapperRegistry中保存一个工厂类?

原来他是用来创建并返回代理类的。这里是代理模式的一个非常经典的应用。

MapperProxy如何实现对接口的代理?

JDK动态代理

我们知道,JDK动态代理有三个核心角色:

  • 被代理类(即就是实现类)
  • 接口
  • 实现了InvocationHanndler的触发管理类,用来生成代理对象。

被代理类必须实现接口,因为要通过接口获取方法,而且代理类也要实现这个接口。

而Mybatis中并没有Mapper接口的实现类,怎么被代理呢?它忽略了实现类,直接对Mapper接口进行代理。

MyBatis动态代理:

在Mybatis中,JDK动态代理为什么不需要实现类呢?

请看下面这张图:

最后返回的userMapper就是MapperProxyFactory的创建的代理对象,然后这个对象中包含了MapperProxy对象,

问题3:到底是怎么根据Mapper.java找到Mapper.xml的?

最后我们调用userMapper.selectUserById(),本质上调用的是MapperProxy的invoke()方法。

请看下面这张图:

如果根据(接口+方法名找到Statement ID ),这个逻辑在InvocationHandler子类(MapperProxy类)中就可以完成了,其实也就没有必要在用实现类了。

总结

本文中主要是讲getMapper方法,该方法实质上是获取一个JDK动态代理对象(类型是Proxy+数字),这个代理类会继承MapperProxy类,实现被代理的接口UserMapper,并且里面持有一个MapperProxy类型的触发管理类。这里我们就拿到代理类了,后面我们就可以使用这个代理对象进行方法调用。

问题涉及到的设计模式:

  1. 代理模式。
  2. 工厂模式。
  3. 单例模式。

整个流程图:

Mybatis CRUD实战:从配置到动态SQL与批量操作详解 ORM(对象关系映射)是Java后端开发中连接应用与数据库的核心技术,它通过将数据库表映射为Java对象,简化了数据持久化操作。其原理在于利用反射和元数据,在运行时自动处理SQL生成、参数绑定和结果集映射,从而提升开发效率并降低手写SQL的出错率。在众多ORM框架中,Mybatis以其半自动化的设计脱颖而出,它精准平衡了SQL的灵活控制与开发便捷性,尤其适用于需要精细优化SQL性能或处理复杂查询的场景。通过XML或注解配置SQL语句,开发者可以完全掌控数据库交互细节,同时利用其动态SQL、缓存机制等特性应对 阅读详情

相关推荐

吃透MyBatis底层原理:从JDBC封装到Spring Boot整合实战

MyBatis本质是对JDBC使用模式的标准化封装,其核心在于将SQL执行抽象为SqlSession工作单元、Mapper动态代理与配置驱动的执行链路。理解其原理需回归JDBC原始痛点——参数绑定脆弱、结果映射冗余、资源管理易错,进而掌握SqlSession生命周期、Executor执行器机制及XML与注解双轨制的工程权衡。技术价值体现在提升SQL可治理性、支持插件化横切(如分页/日志)、实现线程安全的数据库会话隔离;典型应用场景包括Spring Boot中多数据源路由、二级缓存集成Redis、动态SQL精

weixin_30437847的博客 400

MyBatis入门

mybatis入门,mybatis-config.xml基本配置,使用mybatis常见的bug

Java学习博客 1005

MyBatis三级缓存机制深度解析:从原理到实战避坑

缓存是提升数据库访问性能的核心技术之一,其核心原理是在内存中存储热点数据副本,减少对后端数据库的直接请求,从而显著降低系统延迟与负载。在Java持久层框架中,MyBatis通过其内置的多级缓存机制,在数据一致性、性能与资源消耗之间寻求平衡。一级缓存作用于SqlSession会话内部,提供最快的响应;二级缓存扩展至Mapper命名空间,实现跨会话共享;而集成如Redis等分布式缓存框架形成的应用级缓存,则解决了分布式环境下的数据共享难题。理解各级缓存的工作原理、交互顺序及失效策略,对于优化高并发场景下的数据访

weixin_30458043的博客 288

MyBatis核心 - SqlSession如何通过Mapper接口生成Mapper对象

MyBatis核心 - SqlSession如何通过Mapper接口生成Mapper对象

Fearless____的博客 1947

SSM框架深度解析:Spring MVC工作流程、MyBatis缓存与三级缓存实战

在Java企业级开发中,依赖注入(DI)和面向切面编程(AOP)是构建松耦合、可维护应用的核心设计思想。Spring框架通过其IoC容器实现了这些思想,其中三级缓存机制巧妙地解决了单例Bean的循环依赖问题,这是理解Spring Bean生命周期的关键。在Web层,Spring MVC基于DispatcherServlet的前端控制器模式,通过HandlerMapping、HandlerAdapter和ViewResolver等组件协同工作,处理请求分发与视图渲染,其拦截器(Interceptor)为权限校

weixin_34275734的博客 344

第21讲:UserMapper 接口的方法,怎么就能直接调用呢?

大家好,我是田哥 本文是MyBatis源码分析系列文章的第21篇,本文我们继续分析UserMapper接口方法,为什么就能直接调用? 再回上一篇文章的开始的那段代码里: public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { XMLConfigBuilder parser = new XMLConfigBuilder(input

田维常 1523

接口调用什么意思_美团面试:为什么就能直接调用userMapper接口的方法?

字数:2434,阅读耗时:3分40秒。老规矩,先上案例代码,这样大家可以更加熟悉是如何使用的,看过Mybatis系列的小伙伴,对这段代码差不多都可以背下来了。哈哈~,有点夸张吗?不夸张的,就这行代码。 public class MybatisApplication { public static final String URL = "jdbc:mysql://localho...

weixin_39861255的博客 396

MyBatis源码分析之核心流程介绍(下)

  接上一篇我们继续来介绍 1.SqlSession ​ 程序每一次操作数据库,都需要创建一个会话,我们用openSession()方法来创建。接下来我们看看SqlSession创建过程中做了哪些操作 SqlSession sqlSession = factory.openSession(); ​ 通过前面创建的DefaultSqlSessionFactory的openSession方法来创建 @Override public SqlSession openSession() {

波波烤鸭的博客 1216

Java最新面试官:为什么Mybatis就能直接调用userMapper接口的方法?,阿里P7亲自讲解

终极手撕架构师的学习笔记:分布式+微服务+开源框架+性能优化本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录需要这份系统化的资料的朋友,可以点击这里获取终极手撕架构师的学习笔记:分布式+微服务+开源框架+性能优化[外链图片转存中…(img-9TNMQELP-1715448684765)]本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录需要这份系统化的资料的朋友,可以点击这里获取。

2401_84584289的博客 794

2023东华大学OJ机试题22-70

东华大学计算机考研复试机试22 约瑟夫环223、整除的尾数24、回文质数25、汽水瓶26、阶乘最后的非0位27、算菜价28、 水果价格29、求奇数的乘积30、求最晚和最早日期31、素数32、计算e33 数字之和34 繁殖问题35 奇妙的数字36 整除的尾数37 黑色星期五38 树39 约瑟夫环40 最大与最小41 环42 求数列项43 最高频率44 三艘船45 回文数46 特殊四位数47 最大值48 数列149 修理牛棚50 按要求输出序列51 部落人乘法52 序列53 双重回文数54 等差数列55 人见人

m0_49263811的博客 2600

旋转矩阵的左乘与右乘

旋转矩阵左右乘的详细讲解

qq_46619194的博客 3331

面试官问「MyBatis-Plus比原生MyBatis强在哪」,这么答直接拿下offer

在 Java 后端面试中,当面试官问到持久层框架的选型时,「MyBatis-Plus 对比原生 MyBatis 有什么优势」几乎是一个必考题。这个问题看似基础,却能很好地考察你对技术的理解深度:你是只会用 API 的 API 调用工程师,还是真的理解了框架的设计思想,能说出它解决了什么痛点?

小筱在线博客 779

使用DevExpress22.X(Patch)控件库在VisualStudio2022使用C#进行Winform、WPF应用的开发,看这一篇就够了!

写在开头,Dev Express是个十分强大的控件库(下文简称Dev),但碍于其高昂的使用费用,“出于学习目的”,我们一般使用的都是Patch版本(在版权意识日趋加强的当下,不要提那两个字,现在加上那些字,百度都搜不出内容)。

平凡之路 2万+

java中dao层通过mybatis输出的可能为null吗_答完Mybatis这个问题后,面试官居然直接叫我回去等通知……...

背景前段时间在我的技术群里,大家讨论起了为什么UserMapper.java是个接口,没有具体实现类,而我们可以直接调用其方法?关于这个问题,我之前面试过一些人,很多人是这么回答的:1.我领导叫我们使用Mybatis,大家都这么用就这么用了(没想过,反正就这么用)。2.虽然我不知道具体是怎么实现的,但我觉得肯定是……(此处略去若干的漫天猜想),但是也不对啊,难道是……(再次略去若干似懂非懂)。3....

weixin_29867767的博客 183

保姆级教程:用sw_urdf_exporter插件将Solidworks机械臂模型转为ROS可用的URDF

本文提供了一份详细的保姆级教程,指导用户使用sw_urdf_exporter插件将Solidworks机械臂模型转换为ROS可用的URDF文件。从环境准备、插件配置到模型预处理和关键参数设置,再到ROS环境集成测试和性能优化,全面覆盖了机械臂URDF转换的全流程。特别适合机器人工程师和ROS开发者快速实现Solidworks模型到ROS环境的无缝迁移。

weixin_29300931的博客 401

ffmpeg(一) ffmpeg+QT开发环境搭建

(exe 程序编译的时候只需要包含 xxx.dll.a 静态导入库,运行的时候才需要 xxx.dll 文件)这个 .pro 文件的配置,是指导程序编译的时候,可以找到对应的动态库。值得一提的是,在Windows中,静态链接库(Static Link Library)的扩展名也是.lib、.dll.a。其实只要你掌握了FFmpeg,也可以很快上手其他音视频开发库,因为底层原理都是一样的,你最终操作的都是一样的数据,比如MP3、MP4文件。导入库:不包含函数的具体实现代码(函数的具体实现代码存储在dll中)

小道士写程序 8207

答了Mybatis这个问题后,面试官叫我回去等通知……

关注“Java后端技术全栈”回复“面试”获取全套面试资料背景前段时间在我的技术群里,大家讨论起了为什么UserMapper.java是个接口,没有具体实现类,而我们可以直接调用其方法?关...

田维常 472

遥感图像融合实战:5分钟搞懂全色与多光谱图像如何提升分辨率(附Python代码)

本文通过实战案例,详细解析了如何利用全色锐化技术融合全色与多光谱遥感图像,以提升图像分辨率。文章深入浅出地介绍了核心概念、环境配置、经典GS融合算法的Python代码实现,并对比了不同算法的优劣,最后提供了效果评估方法与实战陷阱分析,帮助读者快速掌握遥感图像融合的关键技术。

weixin_29041195的博客 397

【大白话说Java面试题 第132题】【05_Mybatis篇】第2题:MyBatis 动态代理?

Spring AOP 中需要代理类(非接口)时使用 CGLIB,但 MyBatis 的 Mapper 层不需要。等 Object 方法不需要映射到 SQL,直接调用 InvocationHandler 自身的方法即可。与 Spring 集成时,注入的 Mapper 是 JDK 动态代理对象,Spring 通过。:“通过 JDK 动态代理生成 Mapper 接口的实现类。性能方面,虽然 CGLIB 的字节码生成在调用时更快,但 MyBatis 通过。"Spring 注入的 Mapper 是。

Java后端 | AI智能体应用实战派 | 电脑技能行家 364

关于Fluent模拟中的动态接触角UDF实现,详细设计

🏆本文收录于 《全栈Bug调优(实战版)》 专栏,该专栏专注于分享我在真实项目开发中遇到的各类疑难Bug及其深层成因,并系统提供高效、可复现的解决思路和实操方案。无论你是刚入行的新手开发者,还是拥有多年项目经验的资深工程师,本专栏都将为你提供一条系统化、高质量的问题排查与优化路径,助力你加速成长,攻克技术壁垒,迈向技术价值最大化与职业发展的更高峰🚀!

**My Coding Family** 1147

实时多目标跟踪(MOT):SORT与DeepSORT优化

实时多目标跟踪(MOT):SORT与DeepSORT优化​ ,人工智能,计算机视觉,大模型,AI,本文围绕实时多目标跟踪中的 SORT 与 DeepSORT 算法展开,阐述了两者原理、实现及优化。SORT 结合卡尔曼滤波和匈牙利算法,依赖目标检测,实时性好但复杂场景表现欠佳。DeepSORT 在其基础上引入外观特征,采用级联匹配,提升了跟踪精度和鲁棒性。还探讨了两种算法在检测、运动模型、外观特征等方面的优化方向,以及应用场景与选择建议,为相关应用和研究提供参考。

不踩坑,不知道坑有多深;不总结,踩过的坑白踩。 20万+

‍面试官问:Mybatis和Mybatis-Plus执行插入语句后可以返回主键ID吗? ‍我:看我回答...

一、Mybatis执行插入语句后可以返回主键ID吗? 在想写什么内容的时候,正好看到一个基础面试题上有这个问题,就把它记录下来了。 ????‍????面试官:你说Mybatis执行插入语句后可以返回主键ID吗??如果能的话,能否实现一下。 ????‍我:当然是可以的,连JDBC都能做到的事情,Mybatis也能做到的。 开始敲代码… 1.1、Mysql数据库设置ID自增情况 <insert id="insertUser" parameterType="com.crush.mybatisplus.en

北 1719

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

Java工程师为何回归IDEA:Cursor与IDEA在SpringBoot/MyBatis开发中的本质差异

IDE(集成开发环境)是Java工程实践的核心基础设施,其能力边界直接决定开发效率与系统稳定性。在SpringBoot和MyBatis等强约定、深集成的Java生态中,IDE需具备编译期类型推导、字节码级导航、框架生命周期透视及数据库元数据联动等确定性能力——这远超LLM驱动的‘概率性补全’范畴。Cursor虽在自然语言交互与代码生成上表现亮眼,但在SpringBoot自动配置解析、MyBatis Mapper-XML-实体三重绑定验证、运行时SQL参数追踪等关键环节缺乏工程级语义理解;而IDEA凭借十年级

weixin_30847939的博客 395

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

面试官:说说 MyBatis 二级缓存?关联刷新实现?我懵B了

1、MyBatis缓存介绍 Mybatis提供对缓存的支持,但是在没有配置的默认情况下,它只开启一级缓存,二级缓存需要手动开启。 一级缓存只是相对于同一个SqlSession而言。也就是针对于同一事务,多次执行同一Mapper的相同查询方法,第一查询后,MyBatis会将查询结果放入缓存,在中间不涉及相应Mapper的数据更新(Insert,Update和Delete)操作的情况下,后续的查询将会从缓存中获取,而不会查询数据库。 二级缓存是针对于应用级别的缓存,也就是针对不同的SqlSession做.

各种自助共享门店系统定制开发,美团核销服务商 366

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

上一篇: 架构师评价当前软件行业现状及发展前景
下一篇: 面试官:ArrayList和LinkedList在性能方面的差别,能有多大?
Javaesandyou
博客等级 码龄5年 742粉丝 563原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值