【进阶篇】三、Java Agent实现自定义Arthas工具

news2024/10/7 12:17:50

文章目录

  • 0、客户端代码
  • 1、JMX
  • 2、实现:查看内存使用情况
  • 3、实现:查看直接内存
  • 4、实现:生成堆内存快照
  • 5、实现:打印栈信息
  • 6、实现:打印类加载器的信息
  • 7、实现:打印类的源码
  • 8、需求:打印方法的耗时

自己写一个Arthas工具(简化版),功能点包括:

  • 查看内存使用情况
  • 生成堆内存快照
  • 打印栈信息
  • 打印类加载器
  • 打印类的源码
  • 打印方法执行的参数和耗时

提供一个独立的Jar,无侵入性,可用于任何Java程序:

在这里插入图片描述

0、客户端代码

获取所有的Java进程的ID,让用户只需选择PID,连接Java进程,加载Java Agent的Jar,进行动态代理:

public class AttachTest {
    public static void main(String[] args) throws Exception {
        //获取进程列表,让用户自己选择连接哪个PID
        //执行jps指令
        Process process = Runtime.getRuntime().exec("jps");
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
        try (bufferedReader) {
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                System.out.println(line);
            }
        }
        //用户输入进程ID
        Scanner scanner = new Scanner(System.in);
        String pid = scanner.next();
        //连接用户输入的进程
        VirtualMachine vm = VirtualMachine.attach(pid);
        //执行Java Agent的里的agentmain方法
        vm.loadAgent("D:\\jmh2\\llg-agent\\target\\llg-agent-1.0-SNAPSHOT-jar-with-dependencies.jar");

    }
}

在这里插入图片描述

1、JMX

JDK1.5起,提供Java Management Extensions (JMX) 技术,JMX技术使得开发者可以在内存中保存一个MXbean对象,存一些配置信息(类似对象容器的方式去存放一种特有的对象),另外,JVM也将一些程序的运行信息放入了MXbean对象。

简言之,通过JMX,写入或者读取MXbean,可实现:

  • 运行时配置的获取和更改
  • 获取应用程序的运行信息,如:线程栈、内存、类的信息

应用场景:

  • VisualVM使用JMX技术远程连接的方式,由Java程序暴露一个端口,让VisualVM拿到MXbean对象的信息,做一个内存、线程等信息的展示
  • 自定义一个JavaAgent,调用方法操作MXbean对象
    在这里插入图片描述

关于JMX能调用的方法:

ManagementFactory.getMemoryPoolMXBeans() //获取内存信息

其他方法:

在这里插入图片描述

2、实现:查看内存使用情况

调用getMemoryPoolMXBeans方法,获取JVM各块内存对象的List,分堆和非堆打印:

public class MemoryCommand {

    //打印所有的内存信息
    public static void printMemory() {
        //获取内存信息,返回List的结果,List中有伊甸园区、老年代、元空间等对象
        //下面分堆和非堆,分开打印
        List<MemoryPoolMXBean> memoryPoolMXBeans = ManagementFactory.getMemoryPoolMXBeans();
        System.out.println("堆内存:");
        //堆内存
        getMemoryInfo(memoryPoolMXBeans,MemoryType.HEAP);
        System.out.println("非堆内存:");
        //非堆内存
        getMemoryInfo(memoryPoolMXBeans,MemoryType.NON_HEAP);
    }

    /**
     * 处理内存信息
     * @param memoryPoolMXBeans 内存信息
     * @param heapType 堆或非堆
     */
    public static void getMemoryInfo(List<MemoryPoolMXBean> memoryPoolMXBeans,MemoryType heapType){
        memoryPoolMXBeans.stream().filter(x -> x.getType().equals(heapType))
                .forEach(x -> {
                    StringBuilder sb = new StringBuilder();
                    sb.append("name:")
                            .append(x.getName())
                            //使用量used
                            .append(" used:")
                            .append(x.getUsage().getUsed() / 1024 / 1024)  //byte转M
                            .append("M")
                            //申请量total
                            .append(" committed:")
                            .append(x.getUsage().getCommitted() / 1024 / 1024)
                            .append("M")
                            //最大值max
                            .append(" max:")
                            .append(x.getUsage().getMax() / 1024 / 1024)
                            .append("M");
                    System.out.println(sb);
                });

    }
}

改下agentmain方法的逻辑,调用上面打印内存信息的方法:

public class AgentMain {

    public static void agentmain(String agentArgs,Instrumentation inst){
        MemoryCommand.printMemory();
    }

}

连接一个PID试试:
在这里插入图片描述
在这里插入图片描述

3、实现:查看直接内存

继续用JMX技术来实现:

//加载这个类(它里面包含直接内存的使用情况),获取class对象
Class bufferPoolMXBeanClass = Class.forName("java.lang.management.BufferPoolMXBean");

//getPlatformMXBeans允许传入一个MXbean的Class对象,并获取到这个MXbean,因为可能有多个,所以返回一个List
List<BufferPoolMXBean>bufferPoolMXBeans = ManagementFactory.getPlatformMXBeans(bufferPoolMXBeanclass)

如此,通过BufferPoolMXBean的这个MXbean,可获取JVM中分配的直接内存和内存映射缓冲区(这个区用于提升大文件读写性能)等的大小。具体实现:

/**
 * 打印nio相关的内存
 */
public static void printDirectMemory() {
    try {
        Class clazz = Class.forName("java.lang.management.BufferPoolMXBean");
        List<BufferPoolMXBean> bufferPoolMXBeans = ManagementFactory.getPlatformMXBeans(clazz);
        //打印内容
        for (BufferPoolMXBean mxBean : bufferPoolMXBeans) {
            StringBuilder sb = new StringBuilder();
            sb.append("name:")
                    .append(mxBean.getName())
                    //使用量used
                    .append(" used:")
                    .append(mxBean.getMemoryUsed() / 1024 / 1024)  //byte转M
                    .append("M")
                    //容量
                    .append(" capacity:")
                    .append(mxBean.getTotalCapacity() / 1024 / 1024)
                    .append("M");
            System.out.println(sb);
        }

    } catch (Exception e) {
        e.printStackTrace();
    }

}

将这个方法直接在上面打印堆和非堆的方法里调一下。给用户的程序分配100M的直接内存,

在这里插入图片描述

动态代理一下,

在这里插入图片描述

看看效果:

在这里插入图片描述

4、实现:生成堆内存快照

关于生成内存快照:依旧调用getPlatformMXBean

//获取HotSpot虚拟机诊断用的一个MXbean对象,用这个Bean可以生成内存快照
//这里已知这个MXbean只有一个,所以掉没有s的方法,不再像上面直接内存一样返回一个List
HotspotDiagnosticMXBean hotspotDiagnosticMXBean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMxBean.class);

具体实现:

/**
 * 生成内存快照
 */
public static void heapDump(){
    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm");
    HotSpotDiagnosticMXBean mXBean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
    //参数二,选true即只需要dump存活的对象,
    try {
        mXBean.dumpHeap(dateFormat.format(new Date()) + ".hprof",true);
    } catch (IOException e) {
        System.out.println("快照导出失败");
        e.printStackTrace();
    }
}

agentmain中调用一下:

public class AgentMain {

    public static void agentmain(String agentArgs,Instrumentation inst){
        //打印内存
        //MemoryCommand.printMemory();
        //导出内存快照
        MemoryCommand.heapDump();
    }

}

输入普通应用的PID,动态代理一下:

在这里插入图片描述

导出快照文件成功:

在这里插入图片描述

5、实现:打印栈信息

用JMX的方法,还是通过对应的MXBean来获取

ManagementFactory.getThreadMXBean()

实现:

public class ThreadCommand {

    /**
     * 打印栈信息
     */
    public static void printThreadInfo() {
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        //参数一二分别为:当前的虚拟机VM是否能支持监视器和同步器,重载时的第三个参数是栈的深度(不传,默认是Int的最大值,如此,展示的栈信息最全,但性能不好)
        ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(threadMXBean.isObjectMonitorUsageSupported()
                , threadMXBean.isSynchronizerUsageSupported());
        //打印线程信息,ThreadInfo对象中包括了栈名称、方法的调用等,按需自取
        for (ThreadInfo threadInfo : threadInfos) {
            //线程信息
            StringBuilder builder = new StringBuilder();
            builder.append("name: ")
                    .append(threadInfo.getThreadName())
                    .append(" threadId: ")
                    .append(threadInfo.getThreadId())
                    .append(" threadState: ")
                    .append(threadInfo.getThreadState());
            System.out.println(builder);
            //方法调用栈
            StackTraceElement[] stackTrace = threadInfo.getStackTrace();
            for (StackTraceElement stackTraceElement : stackTrace) {
                System.out.println(stackTraceElement);
            }
        }


    }
}

agentmain中调用一下:

public class AgentMain {

    public static void agentmain(String agentArgs,Instrumentation inst){
        //打印内存
        //MemoryCommand.printMemory();
        //导出内存快照
        //MemoryCommand.heapDump();
        //打印栈信息
        ThreadCommand.printThreadInfo();
    }

}

输入普通应用的PID,动态代理一下:

在这里插入图片描述

打印成功:
在这里插入图片描述

6、实现:打印类加载器的信息

Java Agent中可以获得通过Java虚拟机提供的Instumentation对象获取类和类加载器的信息

在这里插入图片描述
作用:

  • redefine:重新设置类的字节码信息(Arthas热部署应该就用到了它)
  • retransform:根据现有类的字节码信息进行增强
  • 获取所有已加载的类信息
//相关文档
https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/Instrumentation.html

具体实现:

public class ClassCommand {

    /**
     * 打印所有的类加载器
     * 形参直接用Instrumentation对象,在premain或者agentmain方法中,JDK会自己注入
     */
    public static void printAllClassLoader(Instrumentation inst) {
        //获取所有已加载的类
        Class[] allLoadedClasses = inst.getAllLoadedClasses();
        //用于去重,因为类加载器就那几种
        Set<ClassLoader> classLoaderSet = new HashSet<>();
        for (Class loadedClass : allLoadedClasses) {
            ClassLoader classLoader = loadedClass.getClassLoader();
            classLoaderSet.add(classLoader);
        }
        //打印类加载器
        String classLoaderInfo = classLoaderSet.stream()
                .map(x -> {
            //获取启动类加载器的结果为null,这里我直接给个固定的名字
            if (x == null) {
                return "BootStrapClassLoader";
            }
            //其他的类加载器就正常输出
            return x.getName();
        })
                //类加载器名字为空的不要
                .filter(x -> x != null)
                .distinct()
                .sorted(String::compareTo)
                .collect(Collectors.joining(","));
        System.out.println(classLoaderInfo);
    }
}

agentmain中调用一下:

public class AgentMain {

    public static void agentmain(String agentArgs,Instrumentation inst){
        //打印内存
        //MemoryCommand.printMemory();
        //导出内存快照
        //MemoryCommand.heapDump();
        //打印栈信息
        //ThreadCommand.printThreadInfo();
        //打印类加载器信息
        ClassCommand.printAllClassLoader(inst);
    }

}

输入普通应用的PID,动态代理一下,普通应用打印它的类加载器成功:

在这里插入图片描述

7、实现:打印类的源码

思路:内存中存的是类的字节码信息,用Instumentation对象提供的转换器获取字节码信息

在这里插入图片描述

并用反编译工具jd-core得到源码:

//参考
https://github.com/java-decompiler/jd-core

使用jd-core,copy官方示例,Loader注意改字节码的来源,Printer重写end方法,打印反编译后的源码即可
在这里插入图片描述
jd-core的依赖:

<dependency>
     <groupId>org.jd</groupId>
     <artifactId>jd-core</artifactId>
     <version>1.1.3</version>
 </dependency>

具体实现:

public class ClassCommand {

    /**
     * 打印类的源代码
     */
    public static void printClassSourceCode(Instrumentation inst) {
        //输入全类名
        System.out.println("请输入全类名:");
        Scanner scanner = new Scanner(System.in);
        String className = scanner.next();
        //获取所有已加载的类,从中找到用户要的类的class对象
        Class[] allLoadedClasses = inst.getAllLoadedClasses();
        for (Class loadedClass : allLoadedClasses) {
            //找到了
            if (className.equals(loadedClass.getName())) {
                //转换器
                ClassFileTransformer transformer = new ClassFileTransformer() {
                    @Override
                    //transform方法传入一个字节码信息,返回一个增强后的字节码信息,以下代码的写法,返回null即不增强,这里只要原来的字节码
                    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
                        //通过jd-code反编译,打印出源码
                        printJdCoreSourceCode(classfileBuffer, className);
                        return ClassFileTransformer.super.transform(loader, className, classBeingRedefined, protectionDomain, classfileBuffer); //直接调用以有的父类,返回null,即不改,因为这里只要获取最初的字节码

                    }
                };
                //添加转换器,让转换器生效,参数二为true即可手动触发
                inst.addTransformer(transformer, true);
                //触发转换器
                try {
                    inst.retransformClasses(loadedClass);
                } catch (UnmodifiableClassException e) {
                    e.printStackTrace();
                } finally {
                    //使用完之后删除转换器
                    inst.removeTransformer(transformer);
                }
            }

        }

    }

    /**
     * jd-code打印源码
     * bytes 字节码信息
     */
    private static void printJdCoreSourceCode(byte[] bytes, String className) {
        //loader对象
        Loader loader = new Loader() {
            @Override
            public byte[] load(String internalName) throws LoaderException {
                return bytes;
            }

            @Override
            public boolean canLoad(String internalName) {
                return true;    //类可加载
            }
        };
        //Printer对象,注意重写end方法,打印源代码
        Printer printer = new Printer() {
            protected static final String TAB = "  ";
            protected static final String NEWLINE = "\n";

            protected int indentationCount = 0;
            protected StringBuilder sb = new StringBuilder();

            @Override
            public String toString() {
                return sb.toString();
            }

            @Override
            public void start(int maxLineNumber, int majorVersion, int minorVersion) {
            }

            @Override
            public void end() {
                //打印源代码
                System.out.println(sb);
            }

            @Override
            public void printText(String text) {
                sb.append(text);
            }

            @Override
            public void printNumericConstant(String constant) {
                sb.append(constant);
            }

            @Override
            public void printStringConstant(String constant, String ownerInternalName) {
                sb.append(constant);
            }

            @Override
            public void printKeyword(String keyword) {
                sb.append(keyword);
            }

            @Override
            public void printDeclaration(int type, String internalTypeName, String name, String descriptor) {
                sb.append(name);
            }

            @Override
            public void printReference(int type, String internalTypeName, String name, String descriptor, String ownerInternalName) {
                sb.append(name);
            }

            @Override
            public void indent() {
                this.indentationCount++;
            }

            @Override
            public void unindent() {
                this.indentationCount--;
            }

            @Override
            public void startLine(int lineNumber) {
                for (int i = 0; i < indentationCount; i++) sb.append(TAB);
            }

            @Override
            public void endLine() {
                sb.append(NEWLINE);
            }

            @Override
            public void extraLine(int count) {
                while (count-- > 0) sb.append(NEWLINE);
            }

            @Override
            public void startMarker(int type) {
            }

            @Override
            public void endMarker(int type) {
            }
        };
        //通过jd-code打印
        ClassFileToJavaSourceDecompiler decompiler = new ClassFileToJavaSourceDecompiler();

        try {
            decompiler.decompile(loader, printer, className);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

agentmain中调用一下:

public class AgentMain {

    public static void agentmain(String agentArgs,Instrumentation inst){
        //打印内存
        //MemoryCommand.printMemory();
        //导出内存快照
        //MemoryCommand.heapDump();
        //打印栈信息
        //ThreadCommand.printThreadInfo();
        //打印类加载器信息
        //ClassCommand.printAllClassLoader(inst);
        //打印源码
        ClassCommand.printClassSourceCode(inst);
    }

}

输入普通应用的PID,动态代理一下,源码打印成功:
在这里插入图片描述

8、需求:打印方法的耗时

打印方法执行的参数和耗时,就需要对原始方法做增强。这里用字节码增强框架ASM和ByteBuddy实现。(不用Java Agent,不考虑无侵入式的话可以在自己项目中用Spring AOP,通过切面+代理对象实现)

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

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

相关文章

OpenHarmony开发学习:【源码下载和编译】

本文介绍了如何下载鸿蒙系统源码&#xff0c;如何一次性配置可以编译三个目标平台&#xff08;Hi3516&#xff0c;Hi3518和Hi3861&#xff09;的编译环境&#xff0c;以及如何将源码编译为三个目标平台的二进制文件。 坑点总结&#xff1a; 下载源码基本上没有太多坑&#xff…

HarmonyOS开发实例:【菜单app】

简介 分布式菜单demo 模拟的是多人聚餐点菜的场景&#xff0c;不需要扫码关注公众号等一系列操作&#xff0c;通过分布式数据库可以方便每个人可及时查看到订单详情&#xff0c;数量&#xff0c;总额等&#xff1b;效果如下 demo效果 工程目录 完整的项目结构目录如下 ├…

2024年DeFi的四大主导趋势:Restaking、Layer3、AI和DePin

DeFi&#xff08;去中心化金融&#xff09;行业在2024年将继续呈现快速增长的势头&#xff0c;驱动这一增长的主要因素将是四大主导趋势&#xff1a;Restaking、Layer3、AI和DePin。这些趋势将推动DeFi生态系统的发展&#xff0c;为用户提供更多的机会和创新。 趋势1&#xff…

雨云:不只是一阵清风,更是一场暴雨的力量

引言 在网络时代&#xff0c;服务器是任何在线业务的核心。无论你是运营一家小型博客还是承载着数百万用户的大型电商平台&#xff0c;都需要一个稳定、高效的服务器来支持你的业务。然而&#xff0c;在众多服务器提供商中&#xff0c;有一家备受推崇&#xff0c;那就是雨云。 …

【C语言】双向链表详解

文章目录 关于双向链表双向链表的初始化双向链表的打印双向链表方法调用 - 尾删为例双向链表的查找 - 指定位置之后插入为例双向链表结束 - 链表的销毁小结及整体代码实现 关于双向链表 首先链表有8种基本分法 其中在笔者之前文章种详细介绍的 单链表 是不带头单项不循环链表…

快速解锁3D Web渲染引擎HOOPS Communicator轻量化技术

在当今数字化时代&#xff0c;三维模型的使用已经成为许多行业中不可或缺的一部分。然而&#xff0c;随着模型复杂性的增加和数据量的膨胀&#xff0c;如何在Web浏览器中高效加载和渲染这些模型成为了一个挑战。慧都3D Web渲染引擎HOOPS Communicator通过其先进的轻量化技术&am…

测试过程和测试生命周期

软件测试过程是一系列有计划、有组织的活动&#xff0c;旨在识别和解决软件产品中的问题。这个过程通常包括多个阶段&#xff0c;每个阶段都有其特定的目标和方法。 需求分析&#xff1a; 分析软件需求和测试需求&#xff0c;确定测试的目标和范围。理解用户需求和业务目标&…

MM-Grounding-DINO的训练推理(待更新)

1、简单介绍 继前面发布的 GroundingDino 和 Open-GroundingDino的推理 和 Open-GroundingDino的训练实现&#xff0c;作为 GroundingDino延续性的文本检测网络 MM-Grounding-DINO 也发布了较详细的 训练和推理实现教程&#xff0c;而且操作性很强。作为学习内容&#xff0c;也…

我对硬技能与软技能的认知

今天看到一个很有意思的一段话&#xff0c;假设一个人的技能有两种&#xff0c;分别是&#xff1a;硬技能和软技能。 硬技能通常指的是与工作直接相关的、可以通过教育和培训获得的技能&#xff0c;如编程语言、会计知识等,这些技能往往有明确的衡量标准&#xff0c;容易通过考…

java 将 json 数据转为 java 中的对象

一、准备 json 数据 {"name": "mike","age": 17,"gender": 1,"subject": ["math","english"] }二、对应的java对象 package com.demo.controller;import lombok.Data; import java.util.List;Data pu…

鸿蒙TypeScript学习第13天:【元组】

1、TypeScript 元组 我们知道数组中元素的数据类型都一般是相同的&#xff08;any[] 类型的数组可以不同&#xff09;&#xff0c;如果存储的元素数据类型不同&#xff0c;则需要使用元组。参考文档&#xff1a;qr23.cn/AKFP8k 元组中允许存储不同类型的元素&#xff0c;元组…

【MATLAB第104期】基于MATLAB的xgboost的敏感性分析/特征值排序计算(针对多输入单输出回归预测模型)

【MATLAB第104期】基于MATLAB的xgboost的敏感性分析/特征值排序计算&#xff08;针对多输入单输出回归预测模型&#xff09; 因matlab的xgboost训练模型不含敏感性分析算法&#xff0c;本文通过使用single算法&#xff0c;即单特征因素对输出影响进行分析&#xff0c;得出不同…

Python 进度显示工具(tqdm)

tqdm 是一个进度显示工具&#xff0c;当任务执行的等待时间较长时&#xff0c;通过tqdm模块可以模拟出一个进度条&#xff0c;由此可以看到任务执行进度&#xff0c;获得更好的体验。 文章目录 一、tqdm的安装二、tqdm的使用2.1 基于可迭代对象2.2 手动进度更新2.3 命令行模式 …

【SpringBoot】mybatis-plus实现增删改查

mapper继承BaseMapper service 继承ServiceImpl 使用方法新增 save,updateById新增和修改方法返回boolean值,或者使用saveOrUpdate方法有id执行修改操作,没有id 执行新增操作 案例 Service public class UserService extends ServiceImpl<UserMapper,User> {// Au…

mac配置Jmeter环境

mac配置Jmeter环境 一、安装jmeter二、Jmeter目录结构三、汉化Jmeter四、jmeter安装第三方插件 一、安装jmeter 第一步先自行配置好电脑的jdk环境 1、官网下载jar包 https://jmeter.apache.org/download_jmeter.cgi 2、解压到软件安装目录 3、启动Jmeter 启动方式1️⃣&#x…

CSS特效---纯CSS实现点击切换按钮

1、演示 2、一切尽在代码中 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta http-equiv"X-UA-Compatible" content"IEedge" /><meta name"viewport" content"w…

idea项目编译时报错:GC overhead limit exceeded

问题描述 今天idea构建一个新的项目时报错&#xff1a;GC overhead limit exceeded&#xff0c;错误是发生在编译阶段&#xff0c;而不是运行阶段。 ava: GC overhead limit exceeded java.lang.OutOfMemoryError: GC overhead limit exceededat com.sun.tools.javac.resources…

如何编译OpenHarmony自带APP

作者&#xff1a;王石 概述 OpenHarmony 的主干代码是开源社区的重要学习资源&#xff0c;对于想进行应用开发和熟悉 OpenHarmony 能力的同学主干代码是非常重要的资源&#xff0c;在主干代码的 applications 目录里聚集了很多原生的应用实现&#xff0c;那么如何编译这些代码…

HashMap部分底层源码解析

哈希表的物理结构 HashMap底层都是哈希表&#xff08;也称散列表&#xff09;&#xff0c;线程不安全&#xff0c;其中维护了一个长度为2的幂次方的Entry类型的数组table&#xff0c;数组的每一个索引位置被称为一个桶(bucket)&#xff0c;你添加的映射关系(key,value)最终都被…

实战项目——智慧社区(三)之 门禁管理

1、人脸识别 实现思路 ①查询出所有的小区信息&#xff0c;下拉列表显示&#xff0c;用于后续判断人脸信息是否与所选小区匹配 ②人脸识别&#xff1a;调用腾讯人脸识别的API接口&#xff0c;首先判断传入图片是否为一张人脸&#xff1b;其次将这张人脸去服务器的人员库进行…