当前位置:

首页 > Java ASM使用logback日志级别动态切换的方法

Java ASM使用logback日志级别动态切换的方法

背景一切皆有因果,所有事情,都有事件驱动。本方案的日志级别切换是由这样的背景下产生的:单个生产环境上,有几百近千个微服务日志级别切换不重启服务,要求即时生效果由业务开发人员去修改代码或增加相关依赖配置等涉及面广,推动进度慢后期动态实时过滤垃圾日志,减少io和磁盘空间成本logback简介在跟敌人发起战争之前,只有先发解敌方的情况,才能做到百战百胜。要想对logback的日志级别做动态切换,首先至少对logback做个初步的了解、和看看它有没有提供现成的实现方案。下面简单介绍一下logback跟这次需求有关

    背景

    一切皆有因果,所有事情,都有事件驱动。本方案的日志级别切换是由这样的背景下产生的:

    • 单个生产环境上,有几百近千个微服务

    • 日志级别切换不重启服务,要求即时生效果

    • 由业务开发人员去修改代码或增加相关依赖配置等涉及面广,推动进度慢

    • 后期动态实时过滤垃圾日志,减少io和磁盘空间成本

    logback简介

    在跟敌人发起战争之前,只有先发解敌方的情况,才能做到百战百胜。要想对logback的日志级别做动态切换,首先至少对logback做个初步的了解、和看看它有没有提供现成的实现方案。下面简单介绍一下logback跟这次需求有关的内容。

    logback是java的日志开源组件,是log4j创始人写的,目前主要分为3个模块

    • logback-core:核心代码模块

    • logback-classic:log4j的一个改良版本,同时实现了slf4j的接口

    • logback-access:访问模块与Servlet容器集成提供通过Http来访问日志的功能

    • ContextInitializer类是logback自动配置流程的逻辑实现

    • 日志级别由Logger维护和使用。其成员变量Level正是由Logger维护

    • Logger中有filterAndLog_0_Or3Plus、filterAndLog_1、filterAndLog_2三个不同参数的过滤日志输出方法

    • Logger中的setLevel就是对日志级别的维护

    Java ASM使用logback日志级别动态切换的方法

    解决方案

    在满头苦干之前,先了解市面上的方案。是设计师们乃至产品大佬们寻求最优解决方案的思路。

    方案一:logback自动扫描更新

    这个方案是logback自带现成的实现,只要开启配置就可以实现所谓的日志级别动态切换。配置方法:在logback的配置文件中,增加定时扫描器即可,如:

    该方案可以不需要研发成本,运维人员自己配上并能使用。

    它的缺点是:

    • 每次调整扫描间隔时间都要重启服务

    • 90%以上的扫描都是无用功,因为生产上的日志级别不可能经常有切换需求,也不允许这么做

    • 生效不实时,如果设定在一分钟或几分钟扫描一次,那么让日志级别调整后生效就不是即时生效的,不过这个可以忽略

    • 该方案满足不了我们的垃圾日志丢弃的需求,比如根据某些关键字丢弃日志的输出。针对这种历史原因打印很多垃圾日志的情况,考虑到时间成本,不可能让业务研发去优化。

    方案二:ASM动态修改字节码

    当然,还有其它方案,如:自己定义接口api。来直接调用Logger中的setLevel方法,达到调整级别的目的;springboot的集成。

    这些方案都不避免不了专主于业务开发角色的参与。

    通过asm动态修改指令,该方案除了能满足调整日志级别即时生效之外。还可以满足过滤日志的需求

    具体实现如下,在这里就不对asm做介绍了,不了解的同学,需要先去熟悉asm、java agent和jvm的指令:

    一、idea创建maven工程

    Java ASM使用logback日志级别动态切换的方法

    二、maven引入依赖

    
            
                org.ow2.asm
                asm
                7.1
            
            
                asm-commons
                org.ow2.asm
                7.1
            
            
                com.sun
                tools
                1.8
                system
                /Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/lib/tools.jar
            
        
    
    
      
          
              org.apache.maven.plugins
              maven-jar-plugin
              3.2.0
              
                  
                      
                          
                          
                              agent.LogbackAgentMain
                          
                          
                          true
                          
                          true
                      
                  
              
          
          
              maven-compiler-plugin
              
                  1.8
                  1.8
                  UTF-8
                  
                      
                      
                      ${java.home}/lib/rt.jar
                  
              
          
      
    

    三、编写attrach启动类

    package agent;
    import java.lang.instrument.Instrumentation;
    import java.lang.instrument.UnmodifiableClassException;
    /**
     * @author dengbp
     * @ClassName LogbackAgentMain
     * @Description attach 启动器
     * @date 3/25/22 6:27 PM
     */
    public class LogbackAgentMain {
        private static String FILTER_CLASS = "ch.qos.logback.classic.Logger";
        public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
            System.out.println("agentArgs:" + agentArgs);
            inst.addTransformer(new LogBackFileTransformer(agentArgs), true);
            Class[] classes = inst.getAllLoadedClasses();
            for (int i = 0; i < classes.length; i++) {
                if (FILTER_CLASS.equals(classes[i].getName())) {
                    System.out.println("----重新加载Logger开始----");
                    inst.retransformClasses(classes[i]);
                    System.out.println("----重新加载Logger完毕----");
                    break;
                }
            }
        }
    }

    四、实现字节码转换处理器

    package agent;
    import jdk.internal.org.objectweb.asm.ClassReader;
    import jdk.internal.org.objectweb.asm.ClassVisitor;
    import jdk.internal.org.objectweb.asm.ClassWriter;
    import java.lang.instrument.ClassFileTransformer;
    import java.security.ProtectionDomain;
    /**
     * @author dengbp
     * @ClassName LogBackFileTransformer
     * @Description 字节码文件转换器
     * @date 3/25/22 6:25 PM
     */
    public class LogBackFileTransformer implements ClassFileTransformer {
    
        private final String level;
        private static String CLASS_NAME = "ch/qos/logback/classic/Logger";
        public LogBackFileTransformer(String level) {
            this.level = level;
        }
        @Override
        public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
            if (!CLASS_NAME.equals(className)) {
                return classfileBuffer;
            }
            ClassReader cr = new ClassReader(classfileBuffer);
            ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
            ClassVisitor cv1 = new LogBackClassVisitor(cw, level);
            /*ClassVisitor cv2 = new LogBackClassVisitor(cv1);*/
            // asm框架使用到访问模式和责任链模式
            // ClassReader 只需要 accept 责任链中的头节点处的 ClassVisitor即可
            cr.accept(cv1, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
            System.out.println("end...");
            return cw.toByteArray();
        }
    }

    五、实现Logger元素的访问者

    package agent;
    import jdk.internal.org.objectweb.asm.ClassVisitor;
    import jdk.internal.org.objectweb.asm.MethodVisitor;
    import org.objectweb.asm.Opcodes;
    /**
     * @author dengbp
     * @ClassName LogBackClassVisitor
     * @Description Logger类元素访问者
     * @date 3/25/22 5:01 PM
     */
    public class LogBackClassVisitor extends ClassVisitor {
        private final String level;
        /**
         * asm版本
         */
        private static final int ASM_VERSION = Opcodes.ASM4;
    
        public LogBackClassVisitor(ClassVisitor classVisitor, String level) {
            super(ASM_VERSION, classVisitor);
            this.level = level;
        }
        @Override
        public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
                                         String[] exceptions) {
            MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
            return new LogFilterMethodVisitor(api, mv, access, name, descriptor, level);
        }
    }

    六、最后实现Logger关键方法的访问者

    该访问者(类),实现日志级别的切换,需要对Logger的三个日志过滤方法进行指令的修改。原理是把命令行入参的日志级别参数值覆盖其成员变量effectiveLevelInt的值,由于篇幅过大,只贴核心部分代码,请看下面:

    package agent;
    import jdk.internal.org.objectweb.asm.Label;
    import jdk.internal.org.objectweb.asm.MethodVisitor;
    import jdk.internal.org.objectweb.asm.commons.AdviceAdapter;
    import org.objectweb.asm.Opcodes;
    /**
     * @author dengbp
     * @ClassName LogFilterMethodVisitor
     * @Description Logger类日志过滤方法元素访问者
     * @date 3/25/22 5:01 PM
     */
    public class LogFilterMethodVisitor extends AdviceAdapter {
        private String methodName;
        private final String level;
        private static final String filterAndLog_1 = "filterAndLog_1";
        private static final String filterAndLog_2 = "filterAndLog_2";
        private static final String filterAndLog_0_Or3Plus = "filterAndLog_0_Or3Plus";
        protected LogFilterMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor, String level) {
            super(api, methodVisitor, access, name, descriptor);
            this.methodName = name;
            this.level = level;
        }
        /**
         * Description 在访问方法的头部时被访问
         * @param
         * @return void
         * @Author dengbp
         * @Date 3:36 PM 4/1/22
         **/
        @Override
        public void visitCode() {
            System.out.println("visitCode method");
            super.visitCode();
        }
        @Override
        protected void onMethodEnter() {
            System.out.println("开始重写日志级别为:"+level);
            System.out.println("----准备修改方法----");
            if (filterAndLog_1.equals(methodName)) {
                modifyLogLevel_1();
            }
            if (filterAndLog_2.equals(methodName)) {
                modifyLogLevel_2();
            }
            if (filterAndLog_0_Or3Plus.equals(methodName)) {
                modifyLogLevel_3();
            }
            System.out.println("重写日志级别成功....");
        }

    其中modifyLogLevel_1(); modifyLogLevel_2();modifyLogLevel_3();分别对应filterAndLog_1、filterAndLog_2、filterAndLog_0_Or3Plus方法指令的修改。下面只贴modifyLogLevel_1的实现

    /**
         * Description 修改目标方法:filterAndLog_1
         * @param
         * @return void
         * @Author dengbp
         * @Date 2:20 PM 3/31/22
         **/
        private void modifyLogLevel_1(){
            Label l0 = new Label();
            mv.visitLabel(l0);
            mv.visitLineNumber(390, l0);
            mv.visitVarInsn(Opcodes.ALOAD, 0);
            mv.visitLdcInsn(level);
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "ch/qos/logback/classic/Level", "toLevel", "(Ljava/lang/String;)Lch/qos/logback/classic/Level;", false);
            mv.visitFieldInsn(Opcodes.GETFIELD, "ch/qos/logback/classic/Level", "levelInt", "I");
            mv.visitFieldInsn(Opcodes.PUTFIELD, "ch/qos/logback/classic/Logger", "effectiveLevelInt", "I");
            Label l1 = new Label();
            mv.visitLabel(l1);
            mv.visitLineNumber(392, l1);
            mv.visitVarInsn(Opcodes.ALOAD, 0);
            mv.visitFieldInsn(Opcodes.GETFIELD, "ch/qos/logback/classic/Logger", "loggerContext", "Lch/qos/logback/classic/LoggerContext;");
            mv.visitVarInsn(Opcodes.ALOAD, 2);
            mv.visitVarInsn(Opcodes.ALOAD, 0);
            mv.visitVarInsn(Opcodes.ALOAD, 3);
            mv.visitVarInsn(Opcodes.ALOAD, 4);
            mv.visitVarInsn(Opcodes.ALOAD, 5);
            mv.visitVarInsn(Opcodes.ALOAD, 6);
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "ch/qos/logback/classic/LoggerContext", "getTurboFilterChainDecision_1", "(Lorg/slf4j/Marker;Lch/qos/logback/classic/Logger;Lch/qos/logback/classic/Level;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Throwable;)Lch/qos/logback/core/spi/FilterReply;", false);
            mv.visitVarInsn(Opcodes.ASTORE, 7);
            Label l2 = new Label();
            mv.visitLabel(l2);
            mv.visitLineNumber(394, l2);
            mv.visitVarInsn(Opcodes.ALOAD, 7);
            mv.visitFieldInsn(Opcodes.GETSTATIC, "ch/qos/logback/core/spi/FilterReply", "NEUTRAL", "Lch/qos/logback/core/spi/FilterReply;");
            Label l3 = new Label();
            mv.visitJumpInsn(Opcodes.IF_ACMPNE, l3);
            Label l4 = new Label();
            mv.visitLabel(l4);
            mv.visitLineNumber(395, l4);
            mv.visitVarInsn(Opcodes.ALOAD, 0);
            mv.visitFieldInsn(Opcodes.GETFIELD, "ch/qos/logback/classic/Logger", "effectiveLevelInt", "I");
            mv.visitVarInsn(Opcodes.ALOAD, 3);
            mv.visitFieldInsn(Opcodes.GETFIELD, "ch/qos/logback/classic/Level", "levelInt", "I");
            Label l5 = new Label();
            mv.visitJumpInsn(Opcodes.IF_ICMPLE, l5);
            Label l6 = new Label();
            mv.visitLabel(l6);
            mv.visitLineNumber(396, l6);
            mv.visitInsn(Opcodes.RETURN);
            mv.visitLabel(l3);
            mv.visitLineNumber(398, l3);
            mv.visitFrame(Opcodes.F_APPEND, 1, new Object[]{"ch/qos/logback/core/spi/FilterReply"}, 0, null);
            mv.visitVarInsn(Opcodes.ALOAD, 7);
            mv.visitFieldInsn(Opcodes.GETSTATIC, "ch/qos/logback/core/spi/FilterReply", "DENY", "Lch/qos/logback/core/spi/FilterReply;");
            mv.visitJumpInsn(Opcodes.IF_ACMPNE, l5);
            Label l7 = new Label();
            mv.visitLabel(l7);
            mv.visitLineNumber(399, l7);
            mv.visitInsn(Opcodes.RETURN);
            mv.visitLabel(l5);
            mv.visitLineNumber(402, l5);
            mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
            mv.visitVarInsn(Opcodes.ALOAD, 0);
            mv.visitVarInsn(Opcodes.ALOAD, 1);
            mv.visitVarInsn(Opcodes.ALOAD, 2);
            mv.visitVarInsn(Opcodes.ALOAD, 3);
            mv.visitVarInsn(Opcodes.ALOAD, 4);
            mv.visitInsn(Opcodes.ICONST_1);
            mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object");
            mv.visitInsn(Opcodes.DUP);
            mv.visitInsn(Opcodes.ICONST_0);
            mv.visitVarInsn(Opcodes.ALOAD, 5);
            mv.visitInsn(Opcodes.AASTORE);
            mv.visitVarInsn(Opcodes.ALOAD, 6);
            mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "ch/qos/logback/classic/Logger", "buildLoggingEventAndAppend", "(Ljava/lang/String;Lorg/slf4j/Marker;Lch/qos/logback/classic/Level;Ljava/lang/String;[Ljava/lang/Object;Ljava/lang/Throwable;)V", false);
            Label l8 = new Label();
            mv.visitLabel(l8);
            mv.visitLineNumber(403, l8);
            mv.visitInsn(Opcodes.RETURN);
            Label l9 = new Label();
            mv.visitLabel(l9);
            mv.visitLocalVariable("this", "Lch/qos/logback/classic/Logger;", null, l0, l9, 0);
            mv.visitLocalVariable("localFQCN", "Ljava/lang/String;", null, l0, l9, 1);
            mv.visitLocalVariable("marker", "Lorg/slf4j/Marker;", null, l0, l9, 2);
            mv.visitLocalVariable("level", "Lch/qos/logback/classic/Level;", null, l0, l9, 3);
            mv.visitLocalVariable("msg", "Ljava/lang/String;", null, l0, l9, 4);
            mv.visitLocalVariable("param", "Ljava/lang/Object;", null, l0, l9, 5);
            mv.visitLocalVariable("t", "Ljava/lang/Throwable;", null, l0, l9, 6);
            mv.visitLocalVariable("decision", "Lch/qos/logback/core/spi/FilterReply;", null, l2, l9, 7);
            mv.visitMaxs(9, 8);
            mv.visitEnd();
        }

    七、最后再编写加载attach Agent的加载类

    import com.sun.tools.attach.VirtualMachine;
    import java.io.IOException;
    import java.io.UnsupportedEncodingException;
    
    /**
     * @author dengbp
     * @ClassName MyAttachMain
     * @Description jar 执行命令:
     * @date 3/25/22 4:12 PM
     */
    public class MyAttachMain {
        private static final int ARGS_SIZE = 2;
    
        public static void main(String[] args) {
            if (args == null || args.length != ARGS_SIZE) {
                System.out.println("请输入进程id和日志级别(ALL、TRACE、DEBUG、INFO、WARN、ERROR、OFF),如:31722 info");
                return;
            }
            VirtualMachine vm = null;
            try {
                System.out.println("修改的进程id:" + args[0]);
                vm = VirtualMachine.attach(args[0]);
                System.out.println("调整日志级别为:" + args[1]);
                vm.loadAgent(getJar(), args[1]);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (vm != null) {
                    try {
                        vm.detach();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        private static String getJar() throws UnsupportedEncodingException {
            String jarFilePath = MyAttachMain.class.getProtectionDomain().getCodeSource().getLocation().getFile();
            jarFilePath = java.net.URLDecoder.decode(jarFilePath, "UTF-8");
            int beginIndex = 0;
            int endIndex = jarFilePath.length();
            if (jarFilePath.contains(".jar")) {
                endIndex = jarFilePath.indexOf(".jar") + 4;
            }
            if (jarFilePath.startsWith("file:")) {
                beginIndex = jarFilePath.indexOf("file:") + 5;
            }
            jarFilePath = jarFilePath.substring(beginIndex, endIndex);
            System.out.println("jar path:" + jarFilePath);
            return jarFilePath;
        }
    }

    八、打包执行

    • 寻找目标程序

    Java ASM使用logback日志级别动态切换的方法

    • 执行jar

    java  -Xbootclasspath/a:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/lib/tools.jar  -cp change-log-agent-1.0.1.jar MyAttachMain 52433  DEBUG
    java  -Xbootclasspath/a:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/lib/tools.jar  -cp change-log-agent-1.0.1.jar MyAttachMain 52433 ERROR
    java  -Xbootclasspath/a:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/lib/tools.jar  -cp change-log-agent-1.0.1.jar MyAttachMain 52433 INFO
    • 效果

    Java ASM使用logback日志级别动态切换的方法

    Java ASM使用logback日志级别动态切换的方法

     PS:如果出现校验失败(caused by: java.lang.verifyerror),请配上jvm参数:-noverify

    本文内容来源于互联网,如有侵权请联系删除。
    作者最新文章
    相关文章 更多
    谷歌浏览器Mac版入口
    谷歌浏览器Mac版入口

    谷歌浏览器Mac版官方安装指南 谷歌浏览器Mac版官方安装入口是https://www.google.com/chrome/,需macOS 12+系统、500MB空间,下载.dmg后拖入应用程序安装,支持多设备同步、性能优化与隐私保护功能。 苹果电脑Chrome的安装入口究竟在哪里?这个问题最近可是

    Chrome浏览器JS脚本不运行怎么办
    Chrome浏览器JS脚本不运行怎么办

    Chrome中JavaScript未执行需依次检查:一、移除站点级禁用并添加允许域名;二、开启全局JavaScript开关;三、禁用干扰扩展;四、在开发者工具中启用JavaScript;五、重置内容设置为默认。 有时在Chrome里打开网页,会发现交互按钮点了没反应,数据加载不出来,页面仿佛“静止”

    IE浏览器怀旧版在线网址
    IE浏览器怀旧版在线网址

    IE浏览器怀旧版在线网址:一次精准的技术时光回溯 最近,不少老用户和怀旧爱好者在反复搜索一个问题:那个经典的Internet Explorer,如今还能在哪里原汁原味地体验到?答案指向一个特定的地址:https://ie.microsoft.com/legacy/。 这个网站远不止是一个简单的“皮肤

    火狐浏览器有哪些设置功能
    火狐浏览器有哪些设置功能

    火狐浏览器五大核心设置功能:解锁高效、安全与个性化体验 火狐浏览器功能强大,但如果不仔细挖掘,很多能大幅提升效率和安全性的设置可能就“藏着掖着”了。这就好比拥有一台高性能设备,却只用了基础模式。那么,如何把它调整到最顺手、最安全的状态?接下来,我们就聚焦于当前版本(截至2025年末)最关键的五大设置

    chrome搜索免验证入口
    chrome搜索免验证入口

    Chrome官方免验证入口为https://www.google.cn/chrome/,提供全平台安装包、免登录即用、本地化安全机制及引擎级性能优化。 到底该去哪里找正版、免费且无需繁琐验证的Chrome浏览器入口?这个问题困扰了不少网友。今天,我们就来直通核心,为大家详细拆解Chrome引擎的官方

    java heap space 选型思路:使用场景与区别整理
    java heap space 选型思路:使用场景与区别整理

    Java堆是JVM存储对象的核心内存区域,配置需结合场景:单体应用适中设置;大数据处理需大堆并关注GC停顿;微服务强调快速启动;高并发需精细划分堆区域。关键参数-Xms和-Xmx建议等值以稳定性能。垃圾回收器选择影响效率,如G1适用于大堆,ZGC可实现低停顿。内存错误时需监控堆状态。

    java heap space 使用中遇到的问题怎么解决
    java heap space 使用中遇到的问题怎么解决

    Java堆内存溢出错误通常因内存泄漏、数据处理需求过大或JVM参数配置不当引起。排查时可借助jmap、堆转储及MAT等工具定位问题。解决方案包括调整JVM内存参数(如-Xmx)、修复代码中的内存泄漏、优化大数据处理逻辑,并建立持续监控与预防机制,以保障应用稳定运行。

    java xml 选型思路:使用场景与区别整理
    java xml 选型思路:使用场景与区别整理

    XML在Java开发中用于配置、数据交换等场景。解析方式主要有DOM、SAX、StAX及第三方库。DOM适合操作小文件,SAX/StAX适合处理大文件流,JAXB用于对象与XML映射。选型需结合数据大小、内存、性能及团队熟悉度,现代框架常封装底层解析。

    using namespace 使用中遇到的问题怎么解决
    using namespace 使用中遇到的问题怎么解决

    命名空间的基本概念与常见引入问题在C++等编程语言中,命名空间(namespace)是一种将代码标识符(如变量、函数、类名)封装在特定名称下的机制,其主要目的是避免命名冲突,尤其是在大型项目或使用多个第三方库时。使用“using namespace”指令可以将指定命名空间中的所有名称引入当前作用域,

    c语言函数递归 实操经验总结:这些技巧很实用
    c语言函数递归 实操经验总结:这些技巧很实用

    理解递归的基本原理在C语言中,递归是一种函数调用自身的编程技术。要掌握它,首先需要理解其核心思想:将一个复杂的大问题,分解为一个或几个与原问题相似但规模更小的子问题,直到子问题足够简单,可以直接求解。这个过程通常包含两个关键部分:递归出口和递归体。递归出口定义了问题何时不再继续分解,即最简单、可直接

    查看更多
    精品专题 更多
    装机必备
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows
    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    macOS软件
    macOS软件

    正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

    Mac软件 更多
    灵活计算器
    灵活计算器
    macOS/iOS/Android

    灵活计算器是一款笔记式算数应用,支持实时计算、动态关联和云端同步功能。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

    赤友清理大师
    赤友清理大师
    macOS

    赤友清理大师是一款为 Mac 设计的智能清理优化工具,可精准扫描垃圾、大文件、重复文件等,释放磁盘空间。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

    极度公式
    极度公式
    Windows/macOS/Linux

    极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

    WINDOWS 更多
    Windows 10
    Windows 10
    Windows

    Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。

    极度公式
    极度公式
    Windows/macOS/Linux

    极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

    密码键盘
    密码键盘
    Windows/macOS/iOS/Android

    密码键盘是一款兼具安全性与便捷性的高效密码管理器。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。