Mybatis源码分析(二)Mybatis-config.xml的初始化

news2024/11/23 21:20:19

目录

  • 一 环境搭建
  • 二 配置文件初始化
    • 2.1 ClassLoader
    • 2.2 获取配置文件

官网:mybatis – MyBatis 3 | 简介
参考书籍:《通用源码阅读指导书:MyBatis源码详解》 易哥
参考文章:

  • 一看你就懂,超详细java中的ClassLoader详解
  • AppClassLoader/ExtClassLoader/BootstrapClassLoader
  • Mybatis源码解析

上一篇文章我们介绍了Mybatis与SpringBoot的整合,我们可以掌握Mybatis的基本用法,到这我们需要来了解一条Sql的执行的基本处理过程

一 环境搭建

  • 依赖
      <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.26</version>
        </dependency>


        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.9</version>
        </dependency>

  • 编写Mapper
package com.shu;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

import java.awt.print.Pageable;
import java.util.List;

/**
 * @description:
 * @author: shu
 * @createDate: 2022/12/13 19:43
 * @version: 1.0
 */
@Mapper
@Repository
public interface UserMapper {
    /**
     * 通过ID查询单条数据
     *
     * @param id 主键
     * @return 实例对象
     */
    User queryById(Integer id);
    /**
     * 分页查询指定行数据
     *
     * @param user 查询条件
     * @param pageable 分页对象
     * @return 对象列表
     */
    List<User> queryAllByLimit(User user);
    /**
     * 统计总行数
     *
     * @param user 查询条件
     * @return 总行数
     */
    long count(User user);
    /**
     * 新增数据
     *
     * @param user 实例对象
     * @return 影响行数
     */
    int insert(User user);
    /**
     * 批量新增数据
     *
     * @param entities List<User> 实例对象列表
     * @return 影响行数
     */
    int insertBatch(@Param("entities") List<User> entities);
    /**
     * 批量新增或按主键更新数据
     *
     * @param entities List<User> 实例对象列表
     * @return 影响行数
     */
    int insertOrUpdateBatch(@Param("entities") List<User> entities);
    /**
     * 更新数据
     *
     * @param user 实例对象
     * @return 影响行数
     */
    int update(User user);
    /**
     * 通过主键删除数据
     *
     * @param id 主键
     * @return 影响行数
     */
    int deleteById(Integer id);
}

  • 编写mapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.shu.UserMapper">
    <resultMap type="com.shu.User" id="UserMap">
        <result property="id" column="id" jdbcType="INTEGER"/>
        <result property="name" column="name" jdbcType="VARCHAR"/>
        <result property="email" column="email" jdbcType="VARCHAR"/>
        <result property="age" column="age" jdbcType="INTEGER"/>
        <result property="sex" column="sex" jdbcType="INTEGER"/>
        <result property="schoolname" column="schoolName" jdbcType="VARCHAR"/>
    </resultMap>

    <!-- 通过ID查询单条数据 -->
    <select id="queryById" resultMap="UserMap">
        select
            id,name,email,age,sex,schoolName
        from user
        where id = #{id}
    </select>

    <!--分页查询指定行数据-->
    <select id="queryAllByLimit" resultMap="UserMap">
        select
        id,name,email,age,sex,schoolName
        from user
        <where>
            <if test="id != null and id != ''">
                and id = #{id}
            </if>
            <if test="name != null and name != ''">
                and name = #{name}
            </if>
            <if test="email != null and email != ''">
                and email = #{email}
            </if>
            <if test="age != null and age != ''">
                and age = #{age}
            </if>
            <if test="sex != null and sex != ''">
                and sex = #{sex}
            </if>
            <if test="schoolname != null and schoolname != ''">
                and schoolName = #{schoolname}
            </if>
        </where>
    </select>

    <!--统计总行数-->
    <select id="count" resultType="java.lang.Long">
        select count(1)
        from user
        <where>
            <if test="id != null and id != ''">
                and id = #{id}
            </if>
            <if test="name != null and name != ''">
                and name = #{name}
            </if>
            <if test="email != null and email != ''">
                and email = #{email}
            </if>
            <if test="age != null and age != ''">
                and age = #{age}
            </if>
            <if test="sex != null and sex != ''">
                and sex = #{sex}
            </if>
            <if test="schoolname != null and schoolname != ''">
                and schoolName = #{schoolname}
            </if>
        </where>
    </select>

    <!--新增数据-->
    <insert id="insert" keyProperty="id" useGeneratedKeys="true">
        insert into user(id,name,email,age,sex,schoolName)
        values (#{id},#{name},#{email},#{age},#{sex},#{schoolname})
    </insert>

    <!-- 批量新增数据 -->
    <insert id="insertBatch" keyProperty="id" useGeneratedKeys="true">
        insert into user(id,name,email,age,sex,schoolName)
        values
        <foreach collection="entities" item="entity" separator=",">
            (#{entity.id},#{entity.name},#{entity.email},#{entity.age},#{entity.sex},#{entity.schoolname})
        </foreach>
    </insert>

    <!-- 批量新增或按主键更新数据 -->
    <insert id="insertOrUpdateBatch" keyProperty="id" useGeneratedKeys="true">
        insert into user(id,name,email,age,sex,schoolName)
        values
        <foreach collection="entities" item="entity" separator=",">
            (#{entity.id},#{entity.name},#{entity.email},#{entity.age},#{entity.sex},#{entity.schoolname})
        </foreach>
        on duplicate key update
        id=values(id),
        name=values(name),
        email=values(email),
        age=values(age),
        sex=values(sex),
        schoolName=values(schoolName)
    </insert>

    <!-- 更新数据 -->
    <update id="update">
        update user
        <set>
            <if test="id != null and id != ''">
                id = #{id},
            </if>
            <if test="name != null and name != ''">
                name = #{name},
            </if>
            <if test="email != null and email != ''">
                email = #{email},
            </if>
            <if test="age != null and age != ''">
                age = #{age},
            </if>
            <if test="sex != null and sex != ''">
                sex = #{sex},
            </if>
            <if test="schoolname != null and schoolname != ''">
                schoolName = #{schoolname},
            </if>
        </set>
        where id = #{id}
    </update>

    <!--通过主键删除-->
    <delete id="deleteById">
        delete from user where id = #{id}
    </delete>
</mapper>
  • 编写Mybatis-conf.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <!-- 别名-->
  <!-- 环境   -->
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://127.0.0.1:3306/mybatis?useUnicode=true"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="mapper/UserMapper.xml"/>
  </mappers>

</configuration>
  • 编写测试用例
package com.shu;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

@SpringBootTest
class MybatisDemo02ApplicationTests {

  @Test
  void contextLoads() {
    // 第一阶段:MyBatis的初始化阶段
    String resource = "mybatis-config.xml";
    // 得到配置文件的输入流
    InputStream inputStream = null;
    try {
      inputStream = Resources.getResourceAsStream(resource);
    } catch (IOException e) {
      e.printStackTrace();
    }
    // 得到SqlSessionFactory
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

    // 第二阶段:数据读写阶段
    try (SqlSession session = sqlSessionFactory.openSession()) {
      // 找到接口对应的实现
      UserMapper userMapper = session.getMapper(UserMapper.class);
      // 组建查询参数
      User userParam = new User();
      userParam.setSchoolname("Sunny School");
      // 调用接口展开数据库操作
      List<User> userList =  userMapper.queryAllByLimit(userParam);
      // 打印查询结果
      for (User user : userList) {
        System.out.println("name : " + user.getName() + " ;  email : " + user.getEmail());
      }
    }
  }

}

到这我们的代码编写完毕,下一步我们来分析其执行过程

二 配置文件初始化

上面我们写了mybatis-config.xml文件,在代码开头我们可以看见进行配置文件的初始化

  	 // 第一阶段:MyBatis的初始化阶段
        String resource = "mybatis-config.xml";
        // 得到配置文件的输入流
        InputStream inputStream = null;
        try {
            inputStream = Resources.getResourceAsStream(resource);
        } catch (IOException e) {
            e.printStackTrace();
        }

2.1 ClassLoader

  • 我们从字面上理解就是类加载器,下面但是Jvm的类加载过程,类的加载就是 Java虚拟机将描述类的数据从 Class文件加载到 JVM的过程,在这一过程中会对 Class文件进行数据加载、连接和初始化,最终形成可以被虚拟机直接使用的 Java类。

  • 当然JVM 在一开始就可能把所有的类都加载,那么可能撑死,按需加载才是王道

Java 类加载器

1,引导类加载器 (BootstrapClassLoader
负责加载系统类(通常从JAR的rt.jar中进行加载),它是虚拟机不可分割的一部分,通常使用C语言实现,引导类加载器没有对应的ClassLoader对象
2,扩展类加载器 (ExtClassLoader
扩展类加载器用于从jre/lib/txt目标加载“标准的扩展”。可以将jar文件放入该目录,这样即使没有任何类路径,扩展类加载器也可以找到其中的各个类
3,系统类加载器 (AppClassLoader
系统类加载器用于加载应用类,它在由ClASSPATH环境变量或者-classpath命令行选项设置的类路径的目录或者是jar/ZIP文件里查找这些类

加载顺序

  1. BootstrapClassLoader
  2. ExtClassLoader
  3. AppClassLoader
  • 关于classLoader的详细信息请参考文章:一看你就懂,超详细java中的ClassLoader详解 博主讲得通俗易懂
  • 关于JVM的的知识,推荐一本书《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》 周志明,我后期也会整理相关知识,敬请期待

2.2 获取配置文件

// 第一阶段:MyBatis的初始化阶段
String resource = "mybatis-config.xml";
// 得到配置文件的输入流
InputStream inputStream = null;
try {
    inputStream = Resources.getResourceAsStream(resource);
} catch (IOException e) {
    e.printStackTrace();
}
  • 我们可以看到调用了Resources.getResourceAsStream(resource)去获取配置文件的信息,调用getResourceAsStream()方法
  public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException {
    // 去加载我们写的mybatis-config.xml 文件
    InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader);
	// 没有找到,资源不存在
    if (in == null) {
      throw new IOException("Could not find resource " + resource);
    }
    return in;
  }
  • 到这我们可以看到他调用了classLoaderWrapper的方法,我们来看看这个类是啥?
public class ClassLoaderWrapper {

  ClassLoader defaultClassLoader;
    
  ClassLoader systemClassLoader;

  ClassLoaderWrapper() {
    try {
   	 // AppClassLoader
      systemClassLoader = ClassLoader.getSystemClassLoader();
    } catch (SecurityException ignored) {
      // AccessControlException on Google App Engine
    }
  }

image.png
到这我们需要注意一下getClassLoaders(classLoader))方法,打个断点,调试一手


public InputStream getResourceAsStream(String resource, ClassLoader classLoader) {
    return getResourceAsStream(resource, getClassLoaders(classLoader));
  }

/**
 * 获取多个ClassLoader,这一步是必须的,因为,我们就是从这个加载器中获取资源的流的
 *五种类加载器:自己传入的、默认的类加载器、当前线程的类加载器、本类的类加载器、系统类加载器
 * @param classLoader 我们定义的自己的类加载器
 * @return 类加载器的数组
 */
ClassLoader[] getClassLoaders(ClassLoader classLoader) {
    return new ClassLoader[]{
            classLoader,
            defaultClassLoader,
            Thread.currentThread().getContextClassLoader(),
            getClass().getClassLoader(),
            systemClassLoader};
}

image.png

  • 用一组 ClassLoader去找到我们写的mybatis-conf.xml文件,一般情况下,类加载器会将名称转换为文件名,然后从文件系统中读取该名称的类文件,因此,类加载器具有读取外部资源的能力,这里要借助的正是类加载器的这种能力。
/**
 * 从一个ClassLoader中获取资源的流,这就是我们的目的
 *
 * @param resource    资源的地址
 * @param classLoader 类加载器
 * @return 流
 */
InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) {
    for (ClassLoader cl : classLoader) {
        if (null != cl) {

            // try to find the resource as passed
            InputStream returnValue = cl.getResourceAsStream(resource);

            // now, some class loaders want this leading "/", so we'll add it and try again if we didn't find the resource
            if (null == returnValue) {
                returnValue = cl.getResourceAsStream("/" + resource);
            }

            if (null != returnValue) {
                return returnValue;
            }
        }
    }
    return null;
}
  • getResourceAsStream 方法会依次调用传入的每一个类加载器的getResourceAsStream方法来尝试获取配置文件的输入流
    public InputStream getResourceAsStream(String name) {
        // 找到文件
        URL url = getResource(name);
        try {
            if (url == null) {
                return null;
            }
        
            URLConnection urlc = url.openConnection();
            InputStream is = urlc.getInputStream();
            if (urlc instanceof JarURLConnection) {
                JarURLConnection juc = (JarURLConnection)urlc;
                JarFile jar = juc.getJarFile();
                synchronized (closeables) {
                    if (!closeables.containsKey(jar)) {
                        closeables.put(jar, null);
                    }
                }
            } else if (urlc instanceof sun.net.www.protocol.file.FileURLConnection) {
                synchronized (closeables) {
                    closeables.put(is, null);
                }
            }
            return is;
        } catch (IOException e) {
            return null;
        }
    }
  • 我们来看看getResource方法吧,相信你刚才看了文章,接下来看你理解没有刚才的知识
    public URL getResource(String name) {
        URL url;
        // 父类加载器能够找到该文章,由前面我们知道AppClassLoader的父类加载器是ExtClassLoader
        if (parent != null) {
            url = parent.getResource(name);
        } else {
            // 通过双亲委派机制找到文件
            url = getBootstrapResource(name);
        }
    	// 没有的话
        if (url == null) {
            url = findResource(name);
        }
        return url;
    }
  • 由前面我们知道AppClassLoader的父类加载器是ExtClassLoader

image.png

  • ExtClassLoader的父类加载器为空,所以通过双亲委派机制去寻找该文件,相信我,后面你还会遇到的。

image.png

  • 当前类加载器(一般是appclassloader)会让父类去加载,父类找不到再通过子类自身findResource(name)方法来找资源
  • Java ClassLoader findResource() method with example
  • AccessController.doPrivileged方法是一个native方法,无法通过IDE进去调试
 public URL findResource(String var1, boolean var2) {
         // 先去缓存查询一下
        int[] var4 = this.getLookupCache(var1);
        Loader var3;
         // 这里有点不懂,有大神可以讲解?
        for(int var5 = 0; (var3 = this.getNextLoader(var4, var5)) != null; ++var5) {
            URL var6 = var3.findResource(var1, var2);
            if (var6 != null) {
                return var6;
            }
        }

        return null;
    }
  • 找到了文件的URL路径,返回

image.png

  • 获取到了URL连接
    public InputStream getResourceAsStream(String name) {
        // 找到文件
        URL url = getResource(name);
        try {
            if (url == null) {
                return null;
            }
            // 打开连接
            URLConnection urlc = url.openConnection();
            // 获取流数据
            InputStream is = urlc.getInputStream();
            // jar包连接
            if (urlc instanceof JarURLConnection) {
                JarURLConnection juc = (JarURLConnection)urlc;
                JarFile jar = juc.getJarFile();
                synchronized (closeables) {
                    if (!closeables.containsKey(jar)) {
                        closeables.put(jar, null);
                    }
                }
            } 
            // 文件连接    
            else if (urlc instanceof sun.net.www.protocol.file.FileURLConnection) {
                synchronized (closeables) {
                    closeables.put(is, null);
                }
            }
            return is;
        } catch (IOException e) {
            return null;
        }
    }

到这我们文件的解析就完毕了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/89840.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Unity3DRPG入门学习笔记第三卷】PolyBrush 构建场景

一、安装 Polybrush 导入样例 我新建了一个新文件夹 Plugins 用来管理 打开 Polybrush Window 二、使用 Polybrush 1. 选中物体&#xff0c;使用第一个工具&#xff0c;会发现可以显示顶点&#xff0c;可以改变网格&#xff0c;例如我们可以上下拖拽地面改变地形 正常左键点…

Java 包装类

Java包装类\huge{Java \space 包装类}Java 包装类 概述 所谓的包装类&#xff0c;通俗来讲其实就是888种基本数据类型对应的引用类型&#xff08;本质就是引用类型&#xff09;。 ❗❗❗尤其注意charcharchar对应的包装类的名称是charactercharactercharacter&#xff0c;in…

大数据学习:shell基础

文章目录一、常用shell命令任务一&#xff1a;查看/etc目录信息前5行信息任务二&#xff1a;查看/etc/profile文件后5行信息二、grep命令选项参数任务一&#xff1a;抓取/etc目录下的python信息任务二&#xff1a;抓取/etc/profile文件里的dev信息任务三&#xff1a;抓取用户数…

Revit运行很卡?这些招数你学会(废)了吗?

在日常的项目实施过程中&#xff0c;我们经常会感觉到Revit运行越来越慢。当然&#xff0c;和我们经常吐槽的软件本身有一定的关系&#xff0c;除此之外&#xff0c;根据我这些年的经验总结&#xff0c;规避掉以下问题可大幅度缓解Revit卡顿的问题。 01禁用结构分析选项 我们…

一条道简单的算法引发的思考

前言 新一季的 Rick&Morty 已经上线&#xff0c;剧集质量虽然有所下降&#xff0c;但 E03 中的 SheepCounter 挺有意思。自己照着剧中的设定开发了一款界面极其相似、交互更为丰富的小程序&#xff0c;小程序的终极目标只有一个&#xff1a;数羊&#xff01;数羊&#xff…

大数据Kudu(六):Kudu Java Api操作

文章目录 ​​​​​​Kudu Java Api操作 一、​​​​​​​​​​​​​​添加Maven依赖

zos-open gb28181,rtsp,rtmp,hls直播储存回放,上下级级联

fslib框架 fslib框架是一套可运行于生产环境的支持c/c线程死锁,线程cpu资源统计,死机时自动记录死机所对应的源码位置的调试框架,部分功能支持php语言&#xff1b;fslib框架内置了很多实用库配置库(FsConfig)--支持向上向下兼容的配置模块&#xff0c;同时可以导出与导入json和…

Ajax(三)

1.form表单的基本使用 1.1 什么是表单 表单在网页中主要负责数据采集功能。HTML中的<form>标签&#xff0c;就是用于采集用户输入的信息&#xff0c;并通过<form>标签的提交操作&#xff0c;把采集到的信息提交到服务器端进行处理。 1.2 表单的组成部分 表单标签…

java+MySQL 基于ssm的网上定点餐外卖系统

网上订餐不是一蹴而就的事情,它需要的是线上线下的共同努力。对于线上来说,安全、稳定、功能完善的网站构建必不可少,这是主要的也是最重要的一部分,网站是“脸面”,好的脸面会吸引更多的顾客光顾。而对于线下来说,好的菜品是一个订餐网站的支柱,我们不能仅靠各色各样的图片满足…

mockito的详细使用

目录 1.概述 2.使用 2.1.依赖 2.2.校验 2.2.1.值校验 2.2.2.顺序校验 2.2.3.指定返回 2.3.注解 2.3.1.Mock 2.3.2.Spy 2.3.3.Captor 2.3.4.InjectMocks 1.概述 mock&#xff0c;一种JAVA单元测试技术&#xff0c;mock允许使用模拟对象替换测试中的系统部件&#xf…

【Redis】Redis 分布式锁

文章目录概述Redis 实现分布式锁加锁释放锁死锁概述 在单体项目中&#xff0c;我们处理多线程同时操作某一处代码块或者变量时就使用 Synchronized 或者 Lock 锁去保证数据的安全性&#xff0c;但是&#xff0c;现在我们基本上都是使用微服务&#xff0c;当我们把服务部署到多…

一文说透小程序插件及其作用价值

最近工作接触小程序插件比较多&#xff0c;就想着不如跟大家系统分享一下小程序插件相关的内容。 首先&#xff0c;我们要先弄清楚小程序插件究竟是什么&#xff1f; 简单来说&#xff0c;小程序插件就是可被添加到小程序内直接使用的功能组件。插件依附于主程序的辅助程序&a…

详解c++---string的介绍(上)

这里写目录标题什么是stringstring的构造函数string的赋值重载string的遍历第一种方式 [ ]第二种方式 范围for第三种方式 正向迭代器反向迭代器string中的capacitysize lengthmax_sizecapacityreserveresizeshrink_to_fitstring的element access什么是string 那这里大家就只用…

k8s编程operator实战之云编码平台——③Code-Server Pod访问实现

文章目录1、openresty介绍和安装2、实现code-server的反向代理3、动态反向代理实现启动多个code-server访问k8s编程operator系列&#xff1a;k8s编程operator——(1) client-go基础部分k8s编程operator——(2) client-go中的informerk8s编程operator——(3) 自定义资源CRDk8s编…

【提高代码可读性】—— 手握多个代码优化技巧、细数哪些惊艳一时的策略

回顾 前期 趁着下班前五分钟书写——Vue3通讯(常规写法、语法糖、v-modle、兄弟通讯)_0.活在风浪里的博客-CSDN博客Vue3 组件通讯https://blog.csdn.net/m0_57904695/article/details/128145150?spm1001.2014.3001.5501 目录 一、可选链接运算符【&#xff1f;.】 二、空…

AD20和立创EDA设计(2)提取立创EDA的原理图库和PCB库

&#xff08;1&#xff09;因为AD20需要自己画原理图库和PCB库。所以我建议新手先用立创EDA画好原理图&#xff0c;转换为PCB&#xff08;注意&#xff0c;只需要转换出PCB即可&#xff0c;因为我们需要立创EDA的PCB库。不懂没关系&#xff0c;后面就清楚了&#xff09; &#…

把随身WiFi的esim卡移植到SIM卡放到手机使用

esim移植到实体sim卡&#xff0c;手把手教你esim改实体卡操作 自用先机的棒子&#xff0c;3-5倍虚标&#xff0c;在单位用&#xff0c;网速还行就是信号不好&#xff0c;uz801_v3.0的板子&#xff0c;410单天线&#xff0c;没有改装潜力&#xff0c;发热还大&#xff0c;加了风…

炸裂!速度百倍提升,高性能 Python 编译器 Codon 火了!

众所周知&#xff0c;Python 是一门简单易学、具有强大功能的编程语言&#xff0c;在各种用户使用统计榜单中总是名列前茅。相应地&#xff0c;围绕 Python&#xff0c;研究者开发了各种便捷工具&#xff0c;以更好的服务于这门语言。 编译器充当着高级语言与机器之间的翻译官…

[附源码]Nodejs计算机毕业设计基于Web企业客户管理系统Express(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程。欢迎交流 项目运行 环境配置&#xff1a; Node.js Vscode Mysql5.7 HBuilderXNavicat11VueExpress。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分…

数据分析图表-FineReport 图表切换接口

1. 概述 1.1 问题描述 图表往往是按照从左往右或从右往左的顺序来切换。那么如何实现点击图表直接切换到其他不相邻的图表呢&#xff1f;效果如下图所示&#xff1a; 1.2 实现思路 给图表添加 JavaScript 类型的超级链接&#xff0c;调用图表接口FR.Chart.WebUtils.getChart(…