Mybatis如何兼容各类日志?

文章目录

  • 适配器模式
  • 日志模块
  • 代理模式
    • 1、静态代理模式
    • 2、JDK动态代理
  • JDBC Logger
  • 总结

Apache Commons Logging、Log4j、Log4j2、java.util.logging 等是 Java 开发中常用的几款日志框架,这些日志框架来源于不同的开源组织,给用户暴露的接口也有很多不同之处,所以很多开源框架会自己定义一套统一的日志接口,兼容上述第三方日志框架,供上层使用。

一般实现的方式是使用适配器模式,将各个第三方日志框架接口转换为框架内部自定义的日志接口。MyBatis 也提供了类似的实现。

适配器模式

适配器模式主要解决的是由于接口不能兼容而导致类无法使用的问题,这在处理遗留代码以及集成第三方框架的时候用得比较多。其核心原理是:通过组合的方式,将需要适配的类转换成使用者能够使用的接口。

适配器模式的类图如下所示

在这里插入图片描述
在该类图中,你可以看到适配器模式涉及的三个核心角色。

  • 目标接口(Target) :使用者能够直接使用的接口。以处理遗留代码为例,Target 就是最新定义的业务接口。
  • 需要适配的类/要使用的实现类(Adaptee):定义了真正要执行的业务逻辑,但是其接口不能被使用者直接使用。这里依然以处理遗留代码为例,Adaptee 就是遗留业务实现,由于编写 Adaptee 的时候还没有定义 Target 接口,所以 Adaptee 无法实现 Target 接口
  • 适配器(Adapter):在实现 Target 接口的同时,维护了一个指向 Adaptee 对象的引用。Adapter 底层会依赖 Adaptee 的逻辑来实现 Target 接口的功能,这样就能够复用 Adaptee 类中的遗留逻辑来完成业务。

适配器模式带来的最大好处就是复用已有的逻辑,避免直接去修改 Adaptee 实现的接口,这符合开放-封闭原则(也就是程序要对扩展开放、对修改关闭)

MyBatis 使用的日志接口是自己定义的 Log 接口,但是 Apache Commons Logging、Log4j、Log4j2 等日志框架提供给用户的都是自己的 Logger 接口。为了统一这些第三方日志框架,MyBatis 使用适配器模式添加了针对不同日志框架的 Adapter 实现,使得第三方日志框架的 Logger 接口转换成 MyBatis 中的 Log 接口,从而实现集成第三方日志框架打印日志的功能。

日志模块

MyBatis 自定义的 Log 接口位于 org.apache.ibatis.logging 包中,相关的适配器也位于该包中

首先是 LogFactory 工厂类,它负责创建 Log 对象。这些 Log 接口的实现类中,就包含了多种第三方日志框架的适配器,如下图所示:

在这里插入图片描述
在 LogFactory 类中有一段静态代码块,其中会依次加载各个第三方日志框架的适配器。在静态代码块执行的 tryImplementation() 方法中,首先会检测 logConstructor 字段是否为空,如果不为空,则表示已经成功确定当前使用的日志框架,直接返回;如果为空,则在当前线程中执行传入的 Runnable.run() 方法,尝试确定当前使用的日志框架。

public static synchronized void useJdkLogging() {

    setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class);

}
private static void setImplementation(Class<? extends Log> implClass) {

  try {

    // 获取implClass这个适配器的构造方法

    Constructor<? extends Log> candidate = implClass.getConstructor(String.class);

    // 尝试加载implClass这个适配器,加载失败会抛出异常

    Log log = candidate.newInstance(LogFactory.class.getName());

    // 加载成功,则更新logConstructor字段,记录适配器的构造方法

    logConstructor = candidate;

  } catch (Throwable t) {

    throw new LogException("Error setting Log implementation.  Cause: " + t, t);

  }

}

下面我们以 Jdk14LoggingImpl 为例介绍一下 MyBatis Log 接口的实现。

Jdk14LoggingImpl 作为 Java Logging 的适配器,在实现 MyBatis Log 接口的同时,在内部还封装了一个 java.util.logging.Logger 对象(这是 JDK 提供的日志框架),如下图所示:

在这里插入图片描述
Jdk14LoggingImpl 继承关系图

Jdk14LoggingImpl 对 Log 接口的实现也比较简单,其中会将日志输出操作委托给底层封装的java.util.logging.Logger 对象的相应方法,这与前文介绍的典型适配器模式的实现完全一致。Jdk14LoggingImpl 中的核心实现以及注释如下:

public class Jdk14LoggingImpl implements Log {

  // 指向一个java.util.logging.Logger对象

  private final Logger log;
  public Jdk14LoggingImpl(String clazz) {

    // 初始化log字段

    log = Logger.getLogger(clazz);

  }
  @Override

  public void error(String s, Throwable e) {

    // 全部调用依赖java.util.logging.Logger对象进行实现

    log.log(Level.SEVERE, s, e);

  }

  // 省略其他级别的日志输出方法

}

在 MyBatis 的 org.apache.ibatis.logging 包下面,除了集成三方日志框架的适配器实现之外,还有一个 jdbc 包,这个包的功能不是将日志写入数据库中,而是将数据库操作涉及的信息通过指定的 Log 打印到日志文件中。我们可以通过这个包,将执行的 SQL 语句、SQL 绑定的参数、SQL 执行之后影响的行数等信息,统统打印到日志中,这个功能主要是在测试环境进行调试的时候使用,很少在线上开启,因为这会产生非常多的日志,拖慢系统性能。

代理模式

在后面即将介绍的 org.apache.ibatis.logging.jdbc 包中,使用到了 JDK 动态代理的相关知识,所以这里我们就先来介绍一下经典的静态代理模式,以及 JDK 提供的动态代理。

1、静态代理模式

经典的静态代理模式,其类图如下所示:

在这里插入图片描述
从该类图中,你可以看到与代理模式相关的三个核心角色。

  • Subject:程序中的业务接口,定义了相关的业务方法。
  • RealSubject:实现了 Subject 接口的业务实现类,其实现中完成了真正的业务逻辑。
  • Proxy:代理类,实现了 Subject 接口,其中会持有一个 Subject 类型的字段,指向一个 RealSubject 对象。

在使用的时候,会将 RealSubject 对象封装到 Proxy 对象中,然后访问 Proxy 的相关方法,而不是直接访问 RealSubject 对象。在 Proxy 的方法实现中,不仅会调用 RealSubject 对象的相应方法完成业务逻辑,还会在 RealSubject 方法执行前后进行预处理和后置处理。

通过对代理模式的描述可知,Proxy 能够控制使用方对 RealSubject 对象的访问,或是在执行业务逻辑之前执行统一的预处理逻辑,在执行业务逻辑之后执行统一的后置处理逻辑。

代理模式除了实现访问控制以外,还能用于实现延迟加载。例如,查询数据库涉及网络 I/O 和磁盘 I/O,会是一个比较耗时的操作,有些时候从数据库加载到内存的数据,也并非系统真正会使用到的数据,所以就有了延迟加载这种优化操作。

延迟加载可以有效地避免数据库资源的浪费,其主要原理是:用户在访问数据库时,会立刻拿到一个代理对象,此时并没有执行任何 SQL 到数据库中查询数据,代理对象中自然也不会包含任何真正的有效数据;当用户真正需要使用数据时,会访问代理对象,此时会由代理对象去执行 SQL,完成数据库的查询。MyBatis 也提供了延迟加载功能,原理大同小异,具体的实现方式也是通过代理实现的。

针对每个 RealSubject 类,都需要创建一个 Proxy 代理类,当 RealSubject 这种需要被代理的类变得很多的时候,相应地就需要定义大量的 Proxy 类,这也是经典代理模式面临的一个问题。JDK 动态代理可以有效地解决这个问题,所以接下来我们就来一起分析 JDK 动态代理的核心原理。

2、JDK动态代理

JDK 动态代理的核心是 InvocationHandler 接口。这里我先给出了一个 InvocationHandler 的示例实现,如下所示:

public class DemoInvokerHandler implements InvocationHandler {

    private Object target; // 真正的业务对象,也就是RealSubject对象

    // DemoInvokerHandler构造方法

    public DemoInvokerHandler(Object target) { 

        this.target = target;

    }

    public Object invoke(Object proxy, Method method, Object[] args)

             throws Throwable {

        ... // 在执行业务逻辑之前的预处理逻辑

        Object result = method.invoke(target, args);

        ... // 在执行业务逻辑之后的后置处理逻辑

        return result;

    }

    public Object getProxy() {

        // 创建代理对象

        return Proxy.newProxyInstance(Thread.currentThread()

            .getContextClassLoader(),

                target.getClass().getInterfaces(), this);

    }

}

接下来,我们可以创建一个 main() 方法来模拟使用方创建并使用 DemoInvokerHandler 动态生成代理对象,示例代码如下:

public class Main {

    public static void main(String[] args) {

        Subject subject = new RealSubject();

        DemoInvokerHandler invokerHandler = 

            new DemoInvokerHandler(subject);

        // 获取代理对象

        Subject proxy = (Subject) invokerHandler.getProxy();

        // 调用代理对象的方法,它会调用DemoInvokerHandler.invoke()方法

        proxy.operation();

    }

}

现在假设有多个业务逻辑类,需要相同的预处理逻辑和后置处理逻辑,那么只需要提供一个 InvocationHandler 接口实现类即可。在程序运行过程中,JDK 动态代理会为每个业务类动态生成相应的代理类实现,并加载到 JVM 中,然后创建对应的代理实例对象。

下面我们就接着来深入分析一下 JDK 动态代理底层动态创建代理类的原理。不同 JDK 版本 Proxy 类的实现会有些许差异,但总体的核心思路基本一致,这里我们就以 JDK 1.8.0 版本为例进行说明。

首先,从前面的示例代码中可以看出,JDK 动态代理的入口方法是 Proxy.newProxyInstance(),这个静态方法有以下三个参数。

  • loader(ClassLoader 类型):加载动态生成的代理类的类加载器。
  • interfaces(Class[] 类型):业务类实现的接口
  • h(InvocationHandler 类型):自定义的 InvocationHandler 对象。

下面进入 Proxy.newProxyInstance() 方法,查看其具体实现如下:


public static Object newProxyInstance(ClassLoader loader,

     Class[] interfaces, InvocationHandler h) 

         throws IllegalArgumentException {

    final Class<?>[] intfs = interfaces.clone();

    ... // 省略权限检查等代码

    Class<?> cl = getProxyClass0(loader, intfs);  // 获取代理类

    ... // 省略try/catch代码块和相关异常处理

    // 获取代理类的构造方法

    final Constructor<?> cons = cl.getConstructor(constructorParams);

    final InvocationHandler ih = h;

    return cons.newInstance(new Object[]{h});  // 创建代理对象

}

从 newProxyInstance() 方法的具体实现代码中我们可以看到,JDK 动态代理是在 getProxyClass0() 方法中完成代理类的生成和加载。getProxyClass0() 方法的具体实现如下:

private static Class getProxyClass0 (ClassLoader loader, 

        Class... interfaces) {

    // 边界检查,限制接口数量(略)

    // 如果指定的类加载器中已经创建了实现指定接口的代理类,则查找缓存;

    // 否则通过ProxyClassFactory创建实现指定接口的代理类

    return proxyClassCache.get(loader, interfaces);

}

proxyClassCache 是定义在 Proxy 类中一个静态字段,它是 WeakCache 类型的集合,用于缓存已经创建过的代理类,具体定义如下:

private static final WeakCache<ClassLoader, Class<?>[], Class<?>> proxyClassCache

     = new WeakCache<>(new KeyFactory(), 

           new ProxyClassFactory());

WeakCache.get() 方法会首先尝试从缓存中查找代理类,如果查找失败,则会创建相应的 Factory 对象并调用其 get() 方法获取代理类。Factory 是 WeakCache 中的内部类,在 Factory.get() 方法中会通过 ProxyClassFactory.apply() 方法创建并加载代理类。

在 ProxyClassFactory.apply() 方法中,首先会检测代理类需要实现的接口集合,然后确定代理类的名称,之后创建代理类并将其写入文件中,最后加载代理类,返回对应的 Class 对象用于后续的实例化代理类对象。该方法的具体实现如下:

public Class apply(ClassLoader loader, Class[] interfaces) {

    // ... 对interfaces集合进行一系列检测(略)

    // ... 选择定义代理类的包名(略)

    // 代理类的名称是通过包名、代理类名称前缀以及编号这三项组成的

    long num = nextUniqueNumber.getAndIncrement();

    String proxyName = proxyPkg + proxyClassNamePrefix + num;

    // 生成代理类,并写入文件

    byte[] proxyClassFile = ProxyGenerator.generateProxyClass(

            proxyName, interfaces, accessFlags);

    // 加载代理类,并返回Class对象

    return defineClass0(loader, proxyName, proxyClassFile, 0, 

      proxyClassFile.length);

}

ProxyGenerator.generateProxyClass() 方法会按照指定的名称和接口集合生成代理类的字节码,并根据条件决定是否保存到磁盘上。该方法的具体代码如下:

public static byte[] generateProxyClass(final String name,

       Class[] interfaces) {

    ProxyGenerator gen = new ProxyGenerator(name, interfaces);

    // 动态生成代理类的字节码,具体生成过程不再详细介绍

    final byte[] classFile = gen.generateClassFile();

    // 如果saveGeneratedFiles值为true,会将生成的代理类的字节码保存到文件中

    if (saveGeneratedFiles) { 

        java.security.AccessController.doPrivileged(

            new java.security.PrivilegedAction() {

                public Void run() {

                    // 省略try/catch代码块

                    FileOutputStream file = new FileOutputStream(

                        dotToSlash(name) + ".class");

                    file.write(classFile);

                    file.close();

                    return null;

                }

            }

        );

    }

    return classFile; // 返回上面生成的代理类的字节码

}

最后,为了清晰地看到 JDK 动态生成的代理类的真正代码,我们需要将上述生成的代理类的字节码进行反编译。上述示例为 RealSubject 生成的代理类,反编译后得到的代码如下:

public final class $Proxy143 

      extends Proxy implements Subject {  // 实现了Subject接口

    // 这里省略了从Object类继承下来的相关方法和属性

    private static Method m3;

    static {

        // 省略了try/catch代码块

        // 记录了operation()方法对应的Method对象

        m3 = Class.forName("design.proxy.Subject")

          .getMethod("operation", new Class[0]);

    }

    // 构造方法的参数就是我们在示例中使用的DemoInvokerHandler对象

    public $Proxy11(InvocationHandler var1) throws {

        super(var1); 

    }

    public final void operation() throws {

        // 省略了try/catch代码块

        // 调用DemoInvokerHandler对象的invoke()方法

        // 最终调用RealSubject对象的对应方法

        super.h.invoke(this, m3, (Object[]) null);

    }

}

到此为止,JDK 动态代理的基本使用以及核心原理就分析完了。这里我做一个简单的总结,JDK 动态代理的实现原理是:动态创建代理类,然后通过指定类加载器进行加载。在创建代理对象时,需要将 InvocationHandler 对象作为构造参数传入;当调用代理对象时,会调用 InvocationHandler.invoke() 方法,从而执行代理逻辑,最终调用真正业务对象的相应方法。

JDBC Logger

了解了代理模式以及 JDK 动态代理的基础知识之后,下面我们开始分析 org.apache.ibatis.logging.jdbc 包中的内容

首先来看其中最基础的抽象类—— BaseJdbcLogger,它是 jdbc 包下其他 Logger 类的父类,继承关系如下图所示:

在这里插入图片描述
在 BaseJdbcLogger 这个抽象类中,定义了 SET_METHODS 和 EXECUTE_METHODS 两个 Set 类型的集合。其中,SET_METHODS 用于记录绑定 SQL 参数涉及的全部 set*() 方法名称,例如 setString() 方法、setInt() 方法等。EXECUTE_METHODS 用于记录执行 SQL 语句涉及的所有方法名称,例如 execute() 方法、executeUpdate() 方法、executeQuery() 方法、addBatch() 方法等。这两个集合都是在 BaseJdbcLogger 的静态代码块中被填充的。

从上面的 BaseJdbcLogger 继承关系图中可以看到,BaseJdbcLogger 的子类同时会实现 InvocationHandler 接口。

我们先来看其中的 ConnectionLogger 实现,其底层维护了一个 Connection 对象的引用,在ConnectionLogger.newInstance() 方法中会使用 JDK 动态代理的方式为这个 Connection 对象创建相应的代理对象。

invoke() 方法是代理对象的核心方法,在该方法中,ConnectionLogger 会为 prepareStatement()、prepareCall()、createStatement() 三个方法添加代理逻辑。下面来看 invoke() 方法的具体实现,具体代码以及注释如下:

public Object invoke(Object proxy, Method method, Object[] params)

        throws Throwable {

    try {

        if (Object.class.equals(method.getDeclaringClass())) {

            // 如果调用的是从Object继承的方法,则直接调用,不做任何拦截

            return method.invoke(this, params);

        }

        // 调用prepareStatement()方法、prepareCall()方法的时候,

        // 会在创建PreparedStatement对象之后,用PreparedStatementLogger为其创建代理对象

        if ("prepareStatement".equals(method.getName()) || "prepareCall".equals(method.getName())) {

            if (isDebugEnabled()) {

                // 通过statementLog这个Log输出日志

                debug(" Preparing: " + removeExtraWhitespace((String) params[0]), true);

            }

            PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);

            stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);

            return stmt;

        } else if ("createStatement".equals(method.getName())) {

            // 调用createStatement()方法的时候,

            // 会在创建Statement对象之后,用StatementLogger为其创建代理对象

            Statement stmt = (Statement) method.invoke(connection, params);

            stmt = StatementLogger.newInstance(stmt, statementLog, queryStack);

            return stmt;

        } else {

            // 除了上述三个方法之外,其他方法的调用将直接传递给底层Connection对象的相应方法处理

            return method.invoke(connection, params);

        }

    } catch (Throwable t) {

        throw ExceptionUtil.unwrapThrowable(t);

    }

}

下面我们来看 PreparedStatementLogger,在其 invoke() 方法中调用了 SET_METHODS 集合中的方法、EXECUTE_METHODS 集合中的方法或 getResultSet() 方法时,会添加相应的代理逻辑。StatementLogger 中的 Invoke() 方法实现与之类似,这里就不再赘述。

最后我们再看下 ResultSetLogger 对 InvocationHandler 接口的实现,其中会针对 ResultSet.next() 方法进行后置处理,主要是打印结果集中每一行数据以及统计结果集总行数等信息,具体实现和注释如下:

public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {

  try {

      if (Object.class.equals(method.getDeclaringClass())) {

          // 如果调用Object的方法,则直接调用,不做任何其他处理

          return method.invoke(this, params);

      }

      Object o = method.invoke(rs, params);

      // 针对ResultSet.next()方法进行后置处理

      if ("next".equals(method.getName())) { 

          if ((Boolean) o) { // 检测next()方法的返回值,确定是否还存在下一行数据

              rows++; // 记录ResultSet中的行数

              if (isTraceEnabled()) {

                  // 获取数据集的列元数据

                  ResultSetMetaData rsmd = rs.getMetaData();

                  // 获取数据集的列数

                  final int columnCount = rsmd.getColumnCount();

                  if (first) { // 如果是数据集的第一行数据,会输出表头信息

                      first = false;

                      // 这里除了输出表头,还会记录BLOB等超大类型的列名

                      printColumnHeaders(rsmd, columnCount);

                  }

                  // 输出当前遍历的这行记录,这里会过滤掉超大类型列的数据,不进行输出

                  printColumnValues(columnCount);

              }

          } else { // 完成结果集的遍历之后,这里会在日志中输出总行数

              debug("     Total: " + rows, false);

          }

      }

      clearColumnInfo(); // 清空column*集合

      return o;

  } catch (Throwable t) {

      throw ExceptionUtil.unwrapThrowable(t);

  }

}

总结

我们主要介绍的是 MyBatis 基础模块中的日志模块。

  • 首先,介绍了适配器模式的核心知识点,这也是日志模块底层的设计思想。
  • 然后,说明了日志模块是如何基于适配器模式集成多种三方日志框架的。
  • 接下来,详细讲解了静态代理模式以及 JDK 动态代理的实现原理。
  • 最后,深入分析了 JDBC Logger 是如何基于 JDK 动态代理实现日志功能的。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ResNet网络分析与demo实例

参考自 up主的b站链接&#xff1a;霹雳吧啦Wz的个人空间-霹雳吧啦Wz个人主页-哔哩哔哩视频这位大佬的博客 Fun_机器学习,pytorch图像分类,工具箱-CSDN博客 ResNet 详解 原论文地址 [1512.03385] Deep Residual Learning for Image Recognition (arxiv.org) ResNet 网络是在 …

Python、PHP/JAVA/C#电商评论数据采集与分析

引言 在电商竞争日益激烈的情况下&#xff0c;商家既要提高产品质量&#xff0c;又要洞悉客户的想法和需求&#xff0c;关注客户购买商品后的评论&#xff0c;而第三方商家获取商品评价主要依赖于人工收集&#xff0c;不但效率低&#xff0c;而且准确度得不到保障。通过使用Py…

【数据结构和算法】找到最高海拔

其他系列文章导航 Java基础合集数据结构与算法合集 设计模式合集 多线程合集 分布式合集 ES合集 文章目录 其他系列文章导航 文章目录 前言 一、题目描述 二、题解 2.1 前缀和的解题模板 2.1.1 最长递增子序列长度 2.1.2 寻找数组中第 k 大的元素 2.1.3 最长公共子序列…

利用MATLAB设计一个(2,1,7)卷积码编译码器

1、条件&#xff1a; 输入数字信号&#xff0c;可以随机产生&#xff0c;也可手动输入 2、要求&#xff1a; &#xff08;1&#xff09;能显示编码树、网格图或状态转移图三者之一&#xff1b; &#xff08;2&#xff09;根据输入数字信号编码生成卷积码并显示&#xf…

如何进行块存储管理

目录 块存储概念 块存储&#xff08;云盘&#xff09;扩容 方式一&#xff1a;直接扩容现有云盘 方式二&#xff1a;创建一块新数据盘 方式三&#xff1a;在更换操作系统时&#xff0c;同时更换系统盘 块存储&#xff08;云盘&#xff09;变配 云盘变配操作步骤 块存储概…

索引进阶 | 再谈 MySQL 的慢 SQL 优化

索引可以提高数据检索的效率&#xff0c;降低数据库的IO成本。 MySQL在300万条记录左右性能开始逐渐下降&#xff0c;虽然官方文档说500~800w记录&#xff0c;所以大数据量建立索引是非常有必要的。 MySQL提供了Explain&#xff0c;用于显示SQL执行的详细信息&#xff0c;可以…

质量免费吗?

本文首发于个人网站「BY林子」&#xff0c;转载请参考版权声明。 两个场景 场景一&#xff1a;有限经费与质量改进 “要写自动化的单元测试、E2E测试&#xff0c;就会需要更多的钱&#xff0c;可是我们经费有限暂时做不了。” “CI上配置SonarQube扫描&#xff0c;对于扫描出来…

godot 报错Unable to initialize Vulkan video driver解决

版本 godot 4.2.1 现象 godot4.2.1 默认使用vulkan驱动&#xff0c;如果再不支持vulkan驱动的主机上&#xff0c;进入引擎编辑器将报错如下 解决 启动参数添加 –rendering-driver opengl3 即可进入引擎编辑器 此时运行项目仍然会报错无法初始化驱动 在项目设置中配置编…

Vue-Pinina基本教程

前言 官网地址&#xff1a;Pinia | The intuitive store for Vue.js (vuejs.org) 看以下内容&#xff0c;需要有vuex的基础&#xff0c;下面很多概念会直接省略&#xff0c;比如state、actions、getters用处含义等 1、什么是Pinina Pinia 是 Vue 的存储库&#xff0c;它允许您跨…

储能:东风已至,破浪在即——安科瑞 顾烊宇

今年的各省政府工作报告已经陆续发布&#xff0c;新能源是各省能源工作的重点&#xff0c;从目前31个省&#xff08;区、市&#xff09;相继公布的2022年经济增长数据来看&#xff0c;一些提前布局新能源产业的省市纷纷交出不错的成绩单&#xff0c;新能源成为当地GDP增速的重要…

饥荒Mod 开发(二三):显示物品栏详细信息

饥荒Mod 开发(二二)&#xff1a;显示物品信息 源码 前一篇介绍了如何获取 鼠标悬浮物品的信息&#xff0c;这一片介绍如何获取 物品栏的详细信息。 拦截 inventorybar 和 itemtile等设置字符串方法 在modmain.lua 文件中放入下面代码即可实现鼠标悬浮到 物品栏显示物品详细信…

微信小程序云开发-下载云存储中的文件

一、前言 很多时候我们需要实现用户在客户端下载服务端的文件&#xff08;图片、视频、pdf等&#xff09;到用户本地并保存起来&#xff0c;小程序也经常需要实现这样的需求。 在传统服务器开发下网上已经有很多关于小程序下载服务端文件的资料了&#xff0c;但是基于云开发的…

苹果怎么备份QQ的聊天记录?这3招教你快速备份!

QQ聊天记录是我们与好友之间的重要互动和沟通记录。但是&#xff0c;有时可能会由于各种原因&#xff0c;比如系统崩溃、更换手机、自身误操作、QQ闪退等&#xff0c;可能会导致聊天记录丢失。 因此&#xff0c;备份QQ聊天记录显得尤为重要。那么&#xff0c;苹果手机怎么备份…

SAP CO系统配置-与PS集成相关配置(机器人制造项目实例)

维护分配结构 配置路径 IMG菜单路径:控制>内部订单>实际过帐>结算>维护分配结构 事务代码 OKO6 维护结算参数文件 定义利润分析码

ZED-Mini 标定完全指南(应该是最详细的吧)

标定 ZED-Mini 相机主要为了跑 VINS-Fusion 以及后期的联合标定相关事宜 双目相机标定 出厂标定数据 关于ZED相机的内参&#xff0c;使用出厂标定的数据就好了&#xff0c;如果安装ZED的SDK时使用的是默认的安装路径&#xff0c;可以在/usr/local/zed/settings下面找到一个SN…

漏洞处理-未设置X-Frame-Options

漏洞名称&#xff1a;iFrame注入 风险描述&#xff1a;系统未设置x-frame-options头 风险等级&#xff1a;低 整改建议&#xff1a;为系统添加x-frame-options头 知识 X-Frame-Options 响应头 X-Frame-Options HTTP 响应头是用来给浏览器指示允许一个页面可否在 <fram…

通过 Bytebase API 做数据库 Schema 变更

Bytebase 是一款数据库 DevOps 和 CI/CD 工具&#xff0c;适用于开发人员、DBA 和平台工程团队。 它提供了一个直观的图形用户界面来管理数据库 Schema 变更。另一方面&#xff0c;一些团队可能希望将 Bytebase 集成到现有的内部 DevOps 研发平台中。这需要调用 Bytebase API。…

搭建Nginx文件下载站点

一、下载Nginx 首先&#xff0c;确保你的服务器上已经安装了Nginx&#xff0c;使用编译安装&#xff0c;下载最新版Nginx。 wget https://nginx.org/download/nginx-1.25.3.tar.gz tar -xf nginx-1.25.3.tar.gz二、安装Fancyindex和Nginx-Fancyindex-Theme模块 # 下载Fancyin…

外贸中的很多跟想的不一样的事情

说说最近遇到的几个客户情况&#xff0c;以及对一些事情刷新的认知。 第一个客户姑且称为A吧&#xff0c;这个客户在询价的时候&#xff0c;产品的名称以及数量以还有走货的方式写的很清楚&#xff0c;客户A要的产品不是很多&#xff0c; 顶多算是个样品单。 一般情况下&…

腾讯云2核4G服务器CVM标准型S5实例5年优惠价格表

腾讯云服务器续费贵所以一次性买3年或5年&#xff0c;腾讯云轻量应用服务器3年价格有优惠&#xff0c;CVM云服务器5年有特价&#xff0c;腾讯云3年轻量和5年云服务器CVM优惠活动入口&#xff0c;3年轻量应用服务器配置可选2核2G4M和2核4G5M带宽&#xff0c;5年CVM云服务器可以选…