前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >源码分析 | 基于jdbc实现一个Demo版的Mybatis

源码分析 | 基于jdbc实现一个Demo版的Mybatis

作者头像
小傅哥
发布2020-07-14 15:21:23
3450
发布2020-07-14 15:21:23
举报
文章被收录于专栏:CodeGuide | 程序员编码指南

作者 | 付政委

博客 | https://bugstack.cn

微信公众号:bugstack虫洞栈 | 博客:https://bugstack.cn 沉淀、分享、成长,专注于原创专题案例,以最易学习编程的方式分享知识,让自己和他人都能有所收获。目前已完成的专题有;Netty4.x实战专题案例、用Java实现JVM、基于JavaAgent的全链路监控、手写RPC框架、架构设计专题案例、源码分析等。 你用剑?、我用刀?,好的代码都很烧?,望你不吝出招?!

一、前言介绍

在前面一篇分析了 mybatis 源码,从它为什么之后接口但是没有实现类就能执行数据库操作为入口,整个源码核心流程完全解释了一遍。对于一个3年以上的程序员来说,新知识的学习过程应该是从最开始 helloworld 到熟练使用 api 完成业务功能。下一步为了深入了解就需要阅读部分核心源码,从而在出问题后可以快速定位,迅速排查。从而减少线上事故的持续时长,提升个人影响力。但!这不是学习终点,因为无论是任何一个框架的源码,如果只是看那么就很难学习到它的实用技术。纸上得来终觉浅,唯有实战和操练。

那么,本章节我们去简单实现一个基于jdbc的demo版本Mybatis,从而更加清楚这样框架的设计。与此同时这份思想会让你可以在其他场景使用,比如给ES查询写一个EsBatis。实现了心情也好了;

微信公众号:bugstack虫洞栈 & DemoMybatis

二、案例工程

扩展上一篇源码分析工程;itstack-demo-mybatis,增加 like 包,模仿 Mybatis 工程。完整规程下载,关注公众号:bugstack虫洞栈 | 回复:源码分析

代码语言:javascript
复制
 1itstack-demo-mybatis
 2└── src
 3    ├── main
 4    │   ├── java
 5    │   │   └── org.itstack.demo
 6    │   │       ├── dao
 7    │   │       │    ├── ISchool.java        
 8    │   │       │    └── IUserDao.java   
 9    │   │       ├── like
10    │   │       │    ├── Configuration.java
11    │   │       │    ├── DefaultSqlSession.java
12    │   │       │    ├── DefaultSqlSessionFactory.java
13    │   │       │    ├── Resources.java
14    │   │       │    ├── SqlSession.java
15    │   │       │    ├── SqlSessionFactory.java
16    │   │       │    ├── SqlSessionFactoryBuilder.java   
17    │   │       │    └── SqlSessionFactoryBuilder.java   
18    │   │       └── interfaces     
19    │   │             ├── School.java 
20    │   │            └── User.java
21    │   ├── resources    
22    │   │   ├── mapper
23    │   │   │   ├── School_Mapper.xml
24    │   │   │   └── User_Mapper.xml
25    │   │   ├── props    
26    │   │   │   └── jdbc.properties
27    │   │   ├── spring
28    │   │   │   ├── mybatis-config-datasource.xml
29    │   │   │   └── spring-config-datasource.xml
30    │   │   ├── logback.xml
31    │   │   ├── mybatis-config.xml
32    │   │   └── spring-config.xml
33    │   └── webapp
34    │       └── WEB-INF
35    └── test
36         └── java
37             └── org.itstack.demo.test
38                 ├── ApiLikeTest.java
39                 ├── MybatisApiTest.java
40                 └── SpringApiTest.java

三、环境配置

  1. JDK1.8
  2. IDEA 2019.3.1
  3. dom4j 1.6.1

四、代码讲述

关于整个 Demo 版本,并不是把所有 Mybatis 全部实现一遍,而是拨丝抽茧将最核心的内容展示给你,从使用上你会感受一模一样,但是实现类已经全部被替换,核心类包括;

  • Configuration
  • DefaultSqlSession
  • DefaultSqlSessionFactory
  • Resources
  • SqlSession
  • SqlSessionFactory
  • SqlSessionFactoryBuilder
  • XNode

1. 先测试下整个DemoJdbc框架

ApiLikeTest.test_queryUserInfoById()

代码语言:javascript
复制
 1@Test
 2public void test_queryUserInfoById() {
 3    String resource = "spring/mybatis-config-datasource.xml";
 4    Reader reader;
 5    try {
 6        reader = Resources.getResourceAsReader(resource);
 7        SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
 8        SqlSession session = sqlMapper.openSession();
 9
10        try {
11            User user = session.selectOne("org.itstack.demo.dao.IUserDao.queryUserInfoById", 1L);
12            System.out.println(JSON.toJSONString(user));
13        } finally {
14            session.close();
15            reader.close();
16        }
17    } catch (Exception e) {
18        e.printStackTrace();
19    }
20}

一切顺利结果如下(新人往往会遇到各种问题);

代码语言:javascript
复制
1{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000}
2
3Process finished with exit code 0

可能乍一看这测试类完全和 MybatisApiTest.java 测试的代码一模一样呀,也看不出区别。其实他们的引入的包是不一样;

MybatisApiTest.java 里面引入的包

代码语言:javascript
复制
1import org.apache.ibatis.io.Resources;
2import org.apache.ibatis.session.SqlSession;
3import org.apache.ibatis.session.SqlSessionFactory;
4import org.apache.ibatis.session.SqlSessionFactoryBuilder;

ApiLikeTest.java 里面引入的包

代码语言:javascript
复制
1import org.itstack.demo.like.Resources;
2import org.itstack.demo.like.SqlSession;
3import org.itstack.demo.like.SqlSessionFactory;
4import org.itstack.demo.like.SqlSessionFactoryBuilder;

好!接下来我们开始分析这部分核心代码。

2. 加载XML配置文件

这里我们采用 mybatis 的配置文件结构进行解析,在不破坏原有结构的情况下,最大可能的贴近源码。mybatis 单独使用的使用的时候使用了两个配置文件;数据源配置、Mapper 映射配置,如下;

mybatis-config-datasource.xml & 数据源配置

代码语言:javascript
复制
 1<?xml version="1.0" encoding="UTF-8"?>
 2<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
 3        "http://mybatis.org/dtd/mybatis-3-config.dtd">
 4
 5<configuration>
 6    <environments default="development">
 7        <environment id="development">
 8            <transactionManager type="JDBC"/>
 9            <dataSource type="POOLED">
10                <property name="driver" value="com.mysql.jdbc.Driver"/>
11                <property name="url" value="jdbc:mysql://127.0.0.1:3306/itstack?useUnicode=true"/>
12                <property name="username" value="root"/>
13                <property name="password" value="123456"/>
14            </dataSource>
15        </environment>
16    </environments>
17
18    <mappers>
19        <mapper resource="mapper/User_Mapper.xml"/>
20        <mapper resource="mapper/School_Mapper.xml"/>
21    </mappers>
22
23</configuration>

User_Mapper.xml & Mapper 映射配置

代码语言:javascript
复制
 1<?xml version="1.0" encoding="UTF-8"?>
 2<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 3<mapper namespace="org.itstack.demo.dao.IUserDao">
 4
 5    <select id="queryUserInfoById" parameterType="java.lang.Long" resultType="org.itstack.demo.po.User">
 6        SELECT id, name, age, createTime, updateTime
 7        FROM user
 8        where id = #{id}
 9    </select>
10
11    <select id="queryUserList" parameterType="org.itstack.demo.po.User" resultType="org.itstack.demo.po.User">
12        SELECT id, name, age, createTime, updateTime
13        FROM user
14        where age = #{age}
15    </select>
16
17</mapper>

这里的加载过程与 mybaits 不同,我们采用 dom4j 方式。在案例中会看到最开始获取资源,如下;

ApiLikeTest.test_queryUserInfoById() & 部分截取

代码语言:javascript
复制
1String resource = "spring/mybatis-config-datasource.xml";
2    Reader reader;
3    try {
4        reader = Resources.getResourceAsReader(resource);
5    ...

从上可以看到这是通过配置文件地址获取到了读取流的过程,从而为后面解析做基础。首先我们先看 Resources 类,整个是我们的资源类。

Resources.java & 资源类

代码语言:javascript
复制
 1/**
 2 * 公众号 | bugstack虫洞栈
 3 * 博 客 | https://bugstack.cn
 4 * Create by 小傅哥 @2020
 5 */
 6public class Resources {
 7
 8    public static Reader getResourceAsReader(String resource) throws IOException {
 9        return new InputStreamReader(getResourceAsStream(resource));
10    }
11
12    private static InputStream getResourceAsStream(String resource) throws IOException {
13        ClassLoader[] classLoaders = getClassLoaders();
14        for (ClassLoader classLoader : classLoaders) {
15            InputStream inputStream = classLoader.getResourceAsStream(resource);
16            if (null != inputStream) {
17                return inputStream;
18            }
19        }
20        throw new IOException("Could not find resource " + resource);
21    }
22
23    private static ClassLoader[] getClassLoaders() {
24        return new ClassLoader[]{
25                ClassLoader.getSystemClassLoader(),
26                Thread.currentThread().getContextClassLoader()};
27    }
28
29}

这段代码方法的入口是getResourceAsReader,直到往下以此做了;

  1. 获取 ClassLoader 集合,最大限度搜索配置文件
  2. 通过 classLoader.getResourceAsStream 读取配置资源,找到后立即返回,否则抛出异常

3. 解析XML配置文件

配置文件加载后开始进行解析操作,这里我们也仿照 mybatis 但进行简化,如下;

代码语言:javascript
复制
1SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);

SqlSessionFactoryBuilder.build() & 入口构建类

代码语言:javascript
复制
 1public DefaultSqlSessionFactory build(Reader reader) {
 2    SAXReader saxReader = new SAXReader();
 3    try {
 4        Document document = saxReader.read(new InputSource(reader));
 5        Configuration configuration = parseConfiguration(document.getRootElement());
 6        return new DefaultSqlSessionFactory(configuration);
 7    } catch (DocumentException e) {
 8        e.printStackTrace();
 9    }
10    return null;
11}
  • 通过读取流创建 xml 解析的 Document 类
  • parseConfiguration 进行解析 xml 文件,并将结果设置到配置类中,包括;连接池、数据源、mapper关系

SqlSessionFactoryBuilder.parseConfiguration() & 解析过程

代码语言:javascript
复制
1private Configuration parseConfiguration(Element root) {
2    Configuration configuration = new Configuration();
3    configuration.setDataSource(dataSource(root.selectNodes("//dataSource")));
4    configuration.setConnection(connection(configuration.dataSource));
5    configuration.setMapperElement(mapperElement(root.selectNodes("mappers")));
6    return configuration;
7}
  • 在前面的 xml 内容中可以看到,我们需要解析出数据库连接池信息 datasource,还有数据库语句映射关系 mappers

SqlSessionFactoryBuilder.dataSource() & 解析出数据源

代码语言:javascript
复制
 1private Map<String, String> dataSource(List<Element> list) {
 2    Map<String, String> dataSource = new HashMap<>(4);
 3    Element element = list.get(0);
 4    List content = element.content();
 5    for (Object o : content) {
 6        Element e = (Element) o;
 7        String name = e.attributeValue("name");
 8        String value = e.attributeValue("value");
 9        dataSource.put(name, value);
10    }
11    return dataSource;
12}
  • 这个过程比较简单,只需要将数据源信息获取即可

SqlSessionFactoryBuilder.connection() & 获取数据库连接

代码语言:javascript
复制
1private Connection connection(Map<String, String> dataSource) {
2    try {
3        Class.forName(dataSource.get("driver"));
4        return DriverManager.getConnection(dataSource.get("url"), dataSource.get("username"), dataSource.get("password"));
5    } catch (ClassNotFoundException | SQLException e) {
6        e.printStackTrace();
7    }
8    return null;
9}
  • 这个就是jdbc最原始的代码,获取了数据库连接池

SqlSessionFactoryBuilder.mapperElement() & 解析SQL语句

代码语言:javascript
复制
 1private Map<String, XNode> mapperElement(List<Element> list) {
 2    Map<String, XNode> map = new HashMap<>();
 3    Element element = list.get(0);
 4    List content = element.content();
 5    for (Object o : content) {
 6        Element e = (Element) o;
 7        String resource = e.attributeValue("resource");
 8        try {
 9            Reader reader = Resources.getResourceAsReader(resource);
10            SAXReader saxReader = new SAXReader();
11            Document document = saxReader.read(new InputSource(reader));
12            Element root = document.getRootElement();
13            //命名空间
14            String namespace = root.attributeValue("namespace");
15            // SELECT
16            List<Element> selectNodes = root.selectNodes("select");
17            for (Element node : selectNodes) {
18                String id = node.attributeValue("id");
19                String parameterType = node.attributeValue("parameterType");
20                String resultType = node.attributeValue("resultType");
21                String sql = node.getText();
22                // ? 匹配
23                Map<Integer, String> parameter = new HashMap<>();
24                Pattern pattern = Pattern.compile("(#\\{(.*?)})");
25                Matcher matcher = pattern.matcher(sql);
26                for (int i = 1; matcher.find(); i++) {
27                    String g1 = matcher.group(1);
28                    String g2 = matcher.group(2);
29                    parameter.put(i, g2);
30                    sql = sql.replace(g1, "?");
31                }
32                XNode xNode = new XNode();
33                xNode.setNamespace(namespace);
34                xNode.setId(id);
35                xNode.setParameterType(parameterType);
36                xNode.setResultType(resultType);
37                xNode.setSql(sql);
38                xNode.setParameter(parameter);
39
40                map.put(namespace + "." + id, xNode);
41            }
42        } catch (Exception ex) {
43            ex.printStackTrace();
44        }
45    }
46    return map;
47}
  • 这个过程首先包括是解析所有的sql语句,目前为了测试只解析 select 相关
  • 所有的 sql 语句为了确认唯一,都是使用;namespace + select中的id进行拼接,作为 key,之后与sql一起存放到 map 中。
  • 在 mybaits 的 sql 语句配置中,都有占位符,用于传参。where id = #{id} 所以我们需要将占位符设置为问号,另外需要将占位符的顺序信息与名称存放到 map 结构,方便后续设置查询时候的入参。

4. 创建DefaultSqlSessionFactory

最后将初始化后的配置类 Configuration,作为参数进行创建 DefaultSqlSessionFactory,如下;

代码语言:javascript
复制
 1public DefaultSqlSessionFactory build(Reader reader) {
 2    SAXReader saxReader = new SAXReader();
 3    try {
 4        Document document = saxReader.read(new InputSource(reader));
 5        Configuration configuration = parseConfiguration(document.getRootElement());
 6        return new DefaultSqlSessionFactory(configuration);
 7    } catch (DocumentException e) {
 8        e.printStackTrace();
 9    }
10    return null;
11}

DefaultSqlSessionFactory.java & SqlSessionFactory的实现类

代码语言:javascript
复制
 1public class DefaultSqlSessionFactory implements SqlSessionFactory {
 2
 3    private final Configuration configuration;
 4
 5    public DefaultSqlSessionFactory(Configuration configuration) {
 6        this.configuration = configuration;
 7    }
 8
 9    @Override
10    public SqlSession openSession() {
11        return new DefaultSqlSession(configuration.connection, configuration.mapperElement);
12    }
13
14}
  • 这个过程比较简单,构造函数只提供了配置类入参
  • 实现 SqlSessionFactory 的 openSession(),用于创建 DefaultSqlSession,也就可以执行 sql 操作

5. 开启SqlSession

代码语言:javascript
复制
1SqlSession session = sqlMapper.openSession();

上面这一步就是创建了DefaultSqlSession,比较简单。如下;

代码语言:javascript
复制
1@Override
2public SqlSession openSession() {
3    return new DefaultSqlSession(configuration.connection, configuration.mapperElement);
4}

6. 执行SQL语句

代码语言:javascript
复制
1User user = session.selectOne("org.itstack.demo.dao.IUserDao.queryUserInfoById", 1L);

在 DefaultSqlSession 中通过实现 SqlSession,提供数据库语句查询和关闭连接池,如下;

SqlSession.java & 定义

代码语言:javascript
复制
 1public interface SqlSession {
 2
 3    <T> T selectOne(String statement);
 4
 5    <T> T selectOne(String statement, Object parameter);
 6
 7    <T> List<T> selectList(String statement);
 8
 9    <T> List<T> selectList(String statement, Object parameter);
10
11    void close();
12}

接下来看具体的执行过程,session.selectOne

DefaultSqlSession.selectOne() & 执行查询

代码语言:javascript
复制
 1public <T> T selectOne(String statement, Object parameter) {
 2    XNode xNode = mapperElement.get(statement);
 3    Map<Integer, String> parameterMap = xNode.getParameter();
 4    try {
 5        PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
 6        buildParameter(preparedStatement, parameter, parameterMap);
 7        ResultSet resultSet = preparedStatement.executeQuery();
 8        List<T> objects = resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
 9        return objects.get(0);
10    } catch (Exception e) {
11        e.printStackTrace();
12    }
13    return null;
14}
  • selectOne 就objects.get(0);,selectList 就全部返回
  • 通过 statement 获取最初解析 xml 时候的存储的 select 标签信息; 1<select id="queryUserInfoById" parameterType="java.lang.Long" resultType="org.itstack.demo.po.User"> 2 SELECT id, name, age, createTime, updateTime 3 FROM user 4 where id = #{id} 5</select>
  • 获取 sql 语句后交给 jdbc 的 PreparedStatement 类进行执行
  • 这里还需要设置入参,我们将入参设置进行抽取,如下; 1private void buildParameter(PreparedStatement preparedStatement, Object parameter, Map<Integer, String> parameterMap) throws SQLException, IllegalAccessException {int size = parameterMap.size(); 2// 单个参数 3if (parameter instanceof Long) { 4 for (int i = 1; i &lt;= size; i++) { 5 preparedStatement.setLong(i, Long.parseLong(parameter.toString())); 6 } 7 return; 8} 9 10if (parameter instanceof Integer) { 11 for (int i = 1; i &lt;= size; i++) { 12 preparedStatement.setInt(i, Integer.parseInt(parameter.toString())); 13 } 14 return; 15} 16 17if (parameter instanceof String) { 18 for (int i = 1; i &lt;= size; i++) { 19 preparedStatement.setString(i, parameter.toString()); 20 } 21 return; 22} 23 24Map&lt;String, Object&gt; fieldMap = new HashMap&lt;&gt;(); 25// 对象参数 26Field[] declaredFields = parameter.getClass().getDeclaredFields(); 27for (Field field : declaredFields) { 28 String name = field.getName(); 29 field.setAccessible(true); 30 Object obj = field.get(parameter); 31 field.setAccessible(false); 32 fieldMap.put(name, obj); 33} 34 35for (int i = 1; i &lt;= size; i++) { 36 String parameterDefine = parameterMap.get(i); 37 Object obj = fieldMap.get(parameterDefine); 38 39 if (obj instanceof Short) { 40 preparedStatement.setShort(i, Short.parseShort(obj.toString())); 41 continue; 42 } 43 44 if (obj instanceof Integer) { 45 preparedStatement.setInt(i, Integer.parseInt(obj.toString())); 46 continue; 47 } 48 49 if (obj instanceof Long) { 50 preparedStatement.setLong(i, Long.parseLong(obj.toString())); 51 continue; 52 } 53 54 if (obj instanceof String) { 55 preparedStatement.setString(i, obj.toString()); 56 continue; 57 } 58 59 if (obj instanceof Date) { 60 preparedStatement.setDate(i, (java.sql.Date) obj); 61 } 62 63} 64}
    • 单个参数比较简单直接设置值即可,Long、Integer、String …
    • 如果是一个类对象,需要通过获取 Field 属性,与参数 Map 进行匹配设置
  • 设置参数后执行查询 preparedStatement.executeQuery()
  • 接下来需要将查询结果转换为我们的类(主要是反射类的操作),resultSet2Obj(resultSet, Class.forName(xNode.getResultType())); 1private <T> List<T> resultSet2Obj(ResultSet resultSet, Class<?> clazz) { 2 List<T> list = new ArrayList<>(); 3 try { 4 ResultSetMetaData metaData = resultSet.getMetaData(); 5 int columnCount = metaData.getColumnCount(); 6 // 每次遍历行值 7 while (resultSet.next()) { 8 T obj = (T) clazz.newInstance(); 9 for (int i = 1; i <= columnCount; i++) { 10 Object value = resultSet.getObject(i); 11 String columnName = metaData.getColumnName(i); 12 String setMethod = "set" + columnName.substring(0, 1).toUpperCase() + columnName.substring(1); 13 Method method; 14 if (value instanceof Timestamp) { 15 method = clazz.getMethod(setMethod, Date.class); 16 } else { 17 method = clazz.getMethod(setMethod, value.getClass()); 18 } 19 method.invoke(obj, value); 20 } 21 list.add(obj); 22 } 23 } catch (Exception e) { 24 e.printStackTrace(); 25 } 26 return list; 27}
    • 主要通过反射生成我们的类对象,这个类的类型定义在 sql 标签上
    • 时间类型需要判断后处理,Timestamp,与 java 不是一个类型

7. Sql查询补充说明

sql 查询有入参、有不需要入参、有查询一个、有查询集合,只需要合理包装即可,例如下面的查询集合,入参是对象类型;

ApiLikeTest.test_queryUserList()

代码语言:javascript
复制
 1@Test
 2public void test_queryUserList() {
 3    String resource = "spring/mybatis-config-datasource.xml";
 4    Reader reader;
 5    try {
 6        reader = Resources.getResourceAsReader(resource);
 7        SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
 8        SqlSession session = sqlMapper.openSession();
 9
10        try {
11            User req = new User();
12            req.setAge(18);
13            List<User> userList = session.selectList("org.itstack.demo.dao.IUserDao.queryUserList", req);
14            System.out.println(JSON.toJSONString(userList));
15        } finally {
16            session.close();
17            reader.close();
18        }
19    } catch (Exception e) {
20        e.printStackTrace();
21    }
22
23}

测试结果:

代码语言:javascript
复制
1[{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000},{"age":18,"createTime":1576944000000,"id":2,"name":"豆豆","updateTime":1576944000000}]
2
3Process finished with exit code 0

五、综上总结

  • 学习完 Mybaits 核心源码,再实现一下核心过程,那么就会很清晰这个过程是怎么个流程,也就不会觉得自己知识栈有漏洞
  • 只有深入的学习才能将这样的技术赋能于其他开发上,例如给ES增加这样查询包,让ES更加容易操作。其实还可以有很多创造
  • 知识往往是综合的使用,将各个知识点综合起来使用,才能更加熟练。不要总看不做,否则全套的流程不能在自己脑子流程下什么印象
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-01-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 bugstack虫洞栈 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、前言介绍
  • 二、案例工程
  • 三、环境配置
  • 四、代码讲述
    • 1. 先测试下整个DemoJdbc框架
      • 2. 加载XML配置文件
        • 3. 解析XML配置文件
          • 4. 创建DefaultSqlSessionFactory
            • 5. 开启SqlSession
              • 6. 执行SQL语句
                • 7. Sql查询补充说明
                • 五、综上总结
                相关产品与服务
                应用性能监控
                应用性能监控(Application Performance Management,APM)是一款应用性能管理平台,基于实时多语言应用探针全量采集技术,为您提供分布式性能分析和故障自检能力。APM 协助您在复杂的业务系统里快速定位性能问题,降低 MTTR(平均故障恢复时间),实时了解并追踪应用性能,提升用户体验。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档