hook控制浏览器的方法_Java-Hook技术-入门实践+反射、动态代理、热修复再看看

延续之前的MonkeyLei:Android-模块化、组件化、插件化、热修复-插件化-起个头,我们复习下里面的关于反射和动态代理点的知识。然后尝试简单了解下Hook...

看之前文章,记得多复习下反射代理,比如使用这些....:

  public class Proxy
extends Object
implements Serializable
Proxy provides static methods for creating dynamic proxy classes and instances, and it is also the superclass of all dynamic proxy classes created by those methods.To create a proxy for some interface Foo:InvocationHandler handler = new MyInvocationHandler(...);Class proxyClass = Proxy.getProxyClass(Foo.class.getClassLoader(), new Class[] { Foo.class });Foo f = (Foo) proxyClass.getConstructor(new Class[] { InvocationHandler.class }).newInstance(new Object[] { handler });or more simply:Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),new Class[] { Foo.class },handler);

多多实践。之后我们尝试建一个Android工程,但是先用Java main方法来做相关Hook测试。后面再尝试结合Hook Android的东西.SO..开始吧...

Then,看下Hook基本介绍吧...

一、什么是 Hook 技术Hook 技术又叫做钩子函数,在系统没有调用该函数之前,钩子程序就先捕获该消息,钩子函数先得到控制权,这时钩子函数既可以加工处理(改变)该函数的执行行为,还可以强制结束消息的传递。简单来说,就是把系统的程序拉出来变成我们自己执行代码片段。要实现钩子函数,有两个步骤:1. 利用系统内部提供的接口,通过实现该接口,然后注入进系统(特定场景下使用)2.动态代理(使用所有场景)二、Hook 技术实现的步骤Hook 技术实现的步骤也分为两步1.找到 hook 点(Java 层),该 hook 点必须满足以下的条件:需要 hook 的方法,所属的对象必须是静态的,因为我们是通过反射来获取对象的,我们获取的是系统的对象,所以不能够 new 一个新的对象,必须用系统创建的那个对象,所以只有静态的才能保证和系统的对象一致。2.将 hook 方法放到系统之外执行(放入我们自己的逻辑)

我就以我觉得的比较简单的方式来理解一下Hook,我要实现的功能是:

1. 继承某个可以继承的对象,然后重写某个方法,添加中间处理,比如验证等。完事了既可以用super调用父类的方法.

2. 然后用这个新的对象变量替换掉原有的变量,实现对象变量的动态替换

3. 重点也就是Field、Proxy的基本使用

直接看测试代码 - HookTestMain.java

package com.skl.hooktest;import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;public class HookTestMain {private static class MyView{// 通过反射替换对象的该对象变量private MyTestView myTestView; // = new MyTestView();static class MyTestView{public void test(){System.out.println("啊啊啊");}}// 通过Proxy生成代理对象,然后替换该变量private ProcInterface other2;}/*** 重写旧有的某个方法,作为新的对象注入替换掉MyView的myTestView变量*/private static class Other extends MyView.MyTestView{@Overridepublic void test(){System.out.println("BBBBB");super.test();}}/*** Proxy生成代理对象必须是某个接口*/private interface ProcInterface{void test();}public static void main(String[] args){try {// 创建一个对象 - 我们即将替换这个对象的某个变量,达到替换方法的效果;// --我是不是可以想象一下,如果要做方法热修复,是不是也可以呢?// --但是这个是限于我们有该对象的前提,如果是其他情况,可能就需要你去找到某个对象 它的某个方法,进而实现替换?MyView myView = new MyView();// myView.myTestView.test();// 这是内部静态类类的表示方法Field field = MyView.class.getDeclaredField("myTestView");field.setAccessible(true);// System.out.println(field.getName());// 用新的对象替换掉myView对象内部的对象变量Other other = new Other();field.set(myView, other);// 或者用Proxy方法生成代理对象,这种方式下,代理的对象必须实现某个接口Field field2 = MyView.class.getDeclaredField("other2");field2.setAccessible(true);ProcInterface other2 = (ProcInterface) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{ProcInterface.class}, new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("这是代理的对象呀 method=" + method);return null;}});// 替换对象变量,然后运行field2.set(myView, other2);myView.other2.test();// 重新运行该方法,达到替换方法的效果!myView.myTestView.test();} catch (NoSuchFieldException e) {System.out.println(e.getMessage());} catch (IllegalAccessException e) {System.out.println(e.getMessage());}//**************此时我们尝试加载Apk文件,然后拿到补丁的方法,来修复上面对象的方法}
}

解释步骤:

1. 定义一个MyView类,该类包含一个MyTestView类,然后定义了一个MyTestView对象变量myTestView。

  private static class MyView{// 通过反射替换对象的该对象变量private MyTestView myTestView; // = new MyTestView();static class MyTestView{public void test(){System.out.println("啊啊啊");}}}

2. 定一个类Other继承MyTestView,然后重新它的test方法,同时加入自己的操作

 /*** 重写旧有的某个方法,作为新的对象注入替换掉MyView的myTestView变量*/private static class Other extends MyView.MyTestView{@Overridepublic void test(){System.out.println("BBBBB");super.test();}}

3. 测试流程,首先创建一个MyView的对象,然后我们将针对这个对象进行反射操作,Hook掉它的myTestView变量

   public static void main(String[] args){try {// 创建一个对象 - 我们即将替换这个对象的某个变量,达到替换方法的效果;// --我是不是可以想象一下,如果要做方法热修复,是不是也可以呢?// --但是这个是限于我们有该对象的前提,如果是其他情况,可能就需要你去找到某个对象 它的某个方法,进而实现替换?MyView myView = new MyView();// 这是内部静态类类的表示方法Field field = MyView.class.getDeclaredField("myTestView");field.setAccessible(true);// 用新的对象替换掉myView对象内部的对象变量Other other = new Other();field.set(myView, other);// 重新运行该方法,达到替换方法的效果!myView.myTestView.test();} catch (NoSuchFieldException e) {System.out.println(e.getMessage());} catch (IllegalAccessException e) {System.out.println(e.getMessage());}}

这样我们就替换掉了这个对象变量,插入了我们自己的操作,比如日志统计。有时候我们再不想改变原来代码的基础上可以这么设计,当然,还可以通过静态代理,或者动态代理的方式实现。。

6f0fb31030c955ce5c2bee87b93b391a.png

这里我们就想借此了解下Hook的思想。。。可能的大概的这样一个概念....当我们真的去深入这块的时候,我们会发现有更复杂的操作和逻辑。 比如有些情况下,你不能知道系统的某个静态内部类,你没办法继承重写。那么你只能一步步的Hook到最终需要替换的目标对象(然后通过Proxy.newProxyInstance创建动态代理对象,动态代理类需要实现某个接口)

4. 上面步骤我们是重新对象的方式。然后开头的全部代码的其他部分,我们是采用动态代理的方式,然后反射来实现的。

重点就是Proxy的使用

  ProcInterface other2 = (ProcInterface) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{ProcInterface.class}, new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("这是代理的对象呀 method=" + method);return null;}});

至此我们就在Java main里面实践了一把。。

And, 我们还可以利用这个思想,在Android里面实现类似的操作,比如Button按钮的点击事件的Hook...

开始之前,有必要了解下setOnClickListener(new View.OnClickListener() {}的内部逻辑,不然你不知道应该Hook哪个目标对象. 这里我直接粘贴出来几个重要的源码逻辑。。。

-- 可以看到我们最终要实现mOnClickListener接口变量的替换,而这个是在ListenerInfo里面,所以最终是替换对象的 ListenerInfo变量里面的mOnClickListener变量。 记住我们的操作都是针对Button button控件的,不能单独New一个实例出来,你那样不是Hook的该button的变量。

         // Hook一下这个点击事件,增加点预处理// 1.跟踪下setOnClickListener方法都做了啥,关键的监听设置在哪里:public void setOnClickListener(@Nullable OnClickListener l) {if (!isClickable()) {setClickable(true);}getListenerInfo().mOnClickListener = l;}static class ListenerInfo {/*** Listener used to dispatch click events.* This field should be made private, so it is hidden from the SDK.* {@hide}*/public OnClickListener mOnClickListener;}// 1.1 所以我们最终要实现替换getListenerInfo()->ListenerInfo的mOnClickListener为我们自己代理的监听方法

a. ListenerInfo是静态内部类,我们拿不到,只能通过getListenerInfo获取。所以我们通过反射拿:

   Method method = View.class.getDeclaredMethod("getListenerInfo");method.setAccessible(true);Log.e("test", "method=" + method.getName());// 获取Button的ListenerInfo对象mListenerInfoObject mListenerInfo = method.invoke(button);

记住是button的

5d8db9c175f45c4ec2fae351abe89779.png

b. 拿到Object mListenerInfo后,还需要获取ListenerInfo的mOnClickListener变量,同样也只能通过反射获取:

            // 内部类需要使用$分隔Class<?> classListenerInfo = Class.forName("android.view.View$ListenerInfo");// 获取内部Field mOnClickListenerField field = classListenerInfo.getDeclaredField("mOnClickListener");// 然后获取Button的ListenerInfo对象mListenerInfo的mOnClickListener变量// --这就是真正的拿到了Button的监听回调View.OnClickListener的实例对象final View.OnClickListener onClickListener = (View.OnClickListener) field.get(mListenerInfo);

记住是上一步获取的对象Object mListenerInfo的mOnClickListener

83cd09b349b3decd4c5c17a781db3265.png

c. 然后上一步的Field field = classListenerInfo.getDeclaredField("mOnClickListener");获取的field,我们就可以用set方法替换调用对象mListenerInfo的mOnClickListener变量:

之前记得创建一个动态代理对象:

            // 然后准备替换为我们自己的点击事件// 1. 创建代理点击对象,然后替换 (这里继承接口实现一个类也可以)Object proxyOnClickListener = Proxy.newProxyInstance(this.getClassLoader(),new Class[]{View.OnClickListener.class},new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {Toast.makeText(MainActivity.this,"你点击我嘛,我很烦的!",Toast.LENGTH_SHORT).show();}});// 2. 然后替换掉Button的点击事件field.set(mListenerInfo, proxyOnClickListener);// End.当点击的时候就会执行我们代理对象的invoke方法。然后你可以在invoke里面增加自己额外的操作。// --甚至你啥都不做,就这么让点击事件失效了,哈哈!

7e80bd24ca9d626cfac97c3a8798f8ec.png

当我们点击的时候就会走动态代理对象的invoke方法

d. 我们Hook了这个点击事件,插入了我们自己的处理,但是我们不能干扰其他的逻辑,所以我们invoke里面还是需要执行 被我们替换的mOnClickListener点击事件的方法回调,所以我们要增加如下处理:

4b30a72c1fd7ddca913c525f244ebcb1.png

当然,如果你想完全拦截自己做一些事情,那你就不要这个处理了。

d. 到此,我们基本上就搞定了这个东东。。但是我有个疑问,我们只能针对一个控件实例,如果多个控件都想要这样的操作应该如何搞? - - 控件的点击事件封装到Base页面,统一Hook?还是说有其他的方式,再学习看看吧...

完整代码:

MainActivity.java

package com.skl.hooktest;import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;/**
*@Author: hl
*@Date: created at 2019/10/16 19:19
*@Description: https://www.jianshu.com/p/74c12164ffca?tdsourcetag=s_pcqq_aiomsg* 文章讲的蛮好的,不过新手理解还是需要先搞搞反射这些知识才行。其实代理还好。你就是要知道,怎么反射调方法,获取字段,设置字段等操作.*  慢慢熟悉吧!争取多屡屡逻辑这些!
*/
public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);Button button = findViewById(R.id.hookClick);button.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {Log.e("test", "点击事件继续");}});// Hook一下这个点击事件,增加点预处理// 1.跟踪下setOnClickListener方法都做了啥,关键的监听设置在哪里:// public void setOnClickListener(@Nullable OnClickListener l) {//        if (!isClickable()) {//            setClickable(true);//        }//        getListenerInfo().mOnClickListener = l;//    }// static class ListenerInfo {//        /**//         * Listener used to dispatch click events.//         * This field should be made private, so it is hidden from the SDK.//         * {@hide}//         *///        public OnClickListener mOnClickListener;// }// 1.1 所以我们最终要实现替换getListenerInfo()->ListenerInfo的mOnClickListener为我们自己代理的监听方法try {Method method = View.class.getDeclaredMethod("getListenerInfo");method.setAccessible(true);Log.e("test", "method=" + method.getName());// 获取Button的ListenerInfo对象mListenerInfoObject mListenerInfo = method.invoke(button);// 内部类需要使用$分隔Class<?> classListenerInfo = Class.forName("android.view.View$ListenerInfo");// 获取内部Field mOnClickListenerField field = classListenerInfo.getDeclaredField("mOnClickListener");// 然后获取Button的ListenerInfo对象mListenerInfo的mOnClickListener变量// --这就是真正的拿到了Button的监听回调View.OnClickListener的实例对象final View.OnClickListener onClickListener = (View.OnClickListener) field.get(mListenerInfo);// 然后准备替换为我们自己的点击事件// 1. 创建代理点击对象,然后替换 (这里继承接口实现一个类也可以)Object proxyOnClickListener = Proxy.newProxyInstance(this.getClassLoader(),new Class[]{View.OnClickListener.class},new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {Toast.makeText(MainActivity.this,"你点击我嘛,我很烦的!",Toast.LENGTH_SHORT).show();// 为了保证其点击逻辑,除了插入我们的操作,我们还是要处理正常的调用逻辑return method.invoke(onClickListener, args);}});// 2. 然后替换掉Button的点击事件field.set(mListenerInfo, proxyOnClickListener);// End.当点击的时候就会执行我们代理对象的invoke方法。然后你可以在invoke里面增加自己额外的操作。// --甚至你啥都不做,就这么让点击事件失效了,哈哈!} catch (NoSuchMethodException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();} catch (ClassNotFoundException e) {e.printStackTrace();} catch (NoSuchFieldException e) {e.printStackTrace();}}
}

要不先到这里,肚子饿了。。。

多多熟悉反射,代理等知识啊哈。。。完事了多练习吧。。

趁机可以了解下热修复原理,以及Hook实现不用注册manifest启动页面的预备知识

Dex热修复原理

Android的热修复 - 建议不要直接照着写。 基础不懂的知识,多联系再整。 争取你去整的时候,心里还是对流程清楚了解才行。。同时学习,实践尽量带上自己的逻辑和想法去实践,多扩展。。。

附录:https://blog.csdn.net/qq_30207527/article/details/85169582

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

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

相关文章

vscode 头文件包含问题_使用clangd替代c/c++配置vscode c++项目

背景&#xff1a;最近从Clion切换到了vscode来进行代码开发&#xff0c;发现vscode自带的c/c插件除了能够使用debug功能&#xff0c;其余代码补全&#xff0c;跳转等功能都和基于clangd的clion有较大差距&#xff0c;经常出现匹配不上或者跳转不准确的问题&#xff0c;在这背景…

微服务拆分需要考虑的必要因素与坚持原则

前言&#xff1a;创业公司往往因为有限的时间和投入&#xff0c;把系统所有的功能都聚集在一起。随着业务的不断发展&#xff0c;技术人员开始不断地对架构进行解耦和拆分。微服务在最近几年大行其道&#xff0c;很多公司的研发人员都在考虑微服务架构&#xff0c;或者在做微服…

data后缀文件解码_Java语法进阶13-文件、IO流

FileFile是文件和目录路径名的抽象表示形式&#xff0c;即File类是文件或目录的路径&#xff0c;而不是文件本身&#xff0c;因此File类不能直接访问文件内容本身&#xff0c;如果需要访问文件内容本身&#xff0c;则需要使用输入/输出流。File类的对象用来表示文件和文件夹的对…

中小型互联网公司微服务实践-经验和教训

上次写了一篇文章叫Spring Cloud在国内中小型公司能用起来吗?介绍了Spring Cloud是否能在中小公司使用起来&#xff0c;这篇文章是它的姊妹篇。其实我们在这条路上已经走了一年多&#xff0c;从16年初到现在。在使用Spring Cloud之前我们对微服务实践是没有太多的体会和经验的…

对于机器学习,到底该选择哪种编程语言

开发者到底应该学习哪种编程语言才能获得机器学习或数据科学这类工作呢&#xff1f;这是一个非常重要的问题。我们在许多论坛上都有讨论过。现在&#xff0c;我可以提供我自己的答案并解释原因&#xff0c;但我们先看一些数据。毕竟&#xff0c;这是机器学习者和数据科学家应该…

android如何适配平板,适用于平板电脑、大屏设备和可折叠设备的自适应布局

将应用支持扩展到更大屏幕的设备(例如平板电脑、可折叠设备和 Chrome 操作系统设备)是扩大您的覆盖面和互动度的绝佳方式。平板电脑的增长率比去年同期 (YOY) 大幅增长了 30%&#xff0c;Chrome 操作系统设备比去年同期增长了 92%。我们还发现&#xff0c;用户使用平板电脑的时…

对于机器学习,到底该选择哪种编程语言?

开发者到底应该学习哪种编程语言才能获得机器学习或数据科学这类工作呢&#xff1f;这是一个非常重要的问题。我们在许多论坛上都讨论过这个问题。今天&#xff0c;我将给出我自己的答案并解释其中原因&#xff0c;但我们首先看一些数据。毕竟&#xff0c;这是机器学习者和数据…

置顶图片代码加链接html,css图片怎么加链接?

css可以通过在图片前加标签实现为图片加链接&#xff0c;语法&#xff1a;。使用此方法引入图片即可为图片添加链接。设定图片超链接&#xff0c;用css样式来实现&#xff0c;其实就是给这个图片的块添加超链接&#xff0c;我们可以通过在这个块的前面添加一个a标签来实现&…

linux上修改html,linux进程名修改

如何修改 Linux 中的进程名在编写网络服务器程序时&#xff0c;为了响应客户端的请CSS布局HTML小编今天和大家分享&#xff0c;我们经常需要新建进程来处理业务流程&#xff1b;而且又是为了关闭某个非法请CSS布局HTML小编今天和大家分享或者关闭长连接的客户端&#xff0c;这时…

AI 崛起,科学家的天下,程序员的谢幕

一边&#xff0c;在移动互联网时代掉队的微软迅速组织起来了一个万人的 AI 团队。 另一边&#xff0c;Facebook、Google 在对各个技术公司进行买买买&#xff0c;为了在 AI 时代的探索中铺好路。 多年前「深蓝」下国际象棋时&#xff0c;所有人都觉得人工智能还是遥不可及的学…

lammps计算聚合物例子_LAMMPS模拟聚合物结构,非晶态聚合物变形行为的模拟,纳米线变形模拟,单轴张力模拟,晶格参数计算...

推荐一个网站&#xff0c;上面有LAMMPS模拟聚合物结构,非晶态聚合物变形行为的模拟,纳米线变形模拟,单轴张力模拟,晶格参数计算的lammps脚本&#xff0c;如下面是晶格参数计算的lammps脚本&#xff0c;具体网址是&#xff1a;LAMMPS Inputs Archives - LAMMPS Tube​lammpstube…

jenkins 手动执行_Jenkins Git client插件命令执行漏洞(CVE201910392)

0x00 漏洞描述Jenkins发布了官方安全公告&#xff1a;https://jenkins.io/security/advisory/2019-09-12/,Git客户端插件中的系统命令执行漏洞。Git客户端插件接受用户指定的值作为调用的参数&#xff0c;git ls-remote以验证指定URL处是否存在Git存储库。这是以允许具有Job/Co…

怎么把html表复制到word里,怎么把网页表格复制到word

在互联网时代我们经常要在网页中找资料&#xff0c;文字进行复制粘贴调整格式还好&#xff0c;但是有时网页中的表格复制到Word中&#xff0c;表格的边框线全没了&#xff0c;怎么办呢?那么下面就由学习啦小编给大家分享下把网页表格复制到word的技巧&#xff0c;希望能帮助到…

设置maven 参数调休_IDEA 使用 Maven构建Spark项目

上一篇讲了普通构建spark项目 这次分享用Maven构建Spark项目&#xff0c;中间遇到了很多坑&#xff01;其根本原因是Scala 与 Spark的版本不一致&#xff01;本次环境&#xff1a;Java1.8Scala 2.11.8Spark spark-2.1.0-bin-hadoop2.6.tgz 新建Maven项目图1.选择顺序图2.随便写…

语音识别现状与工程师必备技能

作者 | 陈孝良 责编 | 胡永波 目前来看&#xff0c;语音识别的精度和速度比较取决于实际应用环境&#xff0c;在安静环境、标准口音、常见词汇上的语音识别率已经超过95%&#xff0c;完全达到了可用状态&#xff0c;这也是当前语音识别比较火热的原因。 随着技术的发展&#xf…

从来不敷面膜的人_女人睡觉前,敷面膜洗还是不洗?很多人都做错了,难怪皮肤总不好...

敷面膜是众多女孩子在晚上都会进行的一个护肤工作&#xff0c;大家都知道像一些明星几乎是每天都要敷一片面膜的&#xff0c;不过她们是因为长期话大浓妆才比较勤&#xff0c;我们一般工作的女孩子大约一周三次就可以了。面膜可以让我们的皮肤迅速吸收水分和营养&#xff0c;这…

智慧气象机器_智慧电缆隧道火热建设中 传感器+机器人成标配

智慧城市是指利用各种信息技术或创新概念&#xff0c;将城市的系统和服务打通、集成&#xff0c;以提升资源运用的效率&#xff0c;优化城市管理和服务&#xff0c;以及改善市民生活质量。它把新一代信息技术充分运用在城市中各行各业基于知识社会下一代创新(创新2.0)的城市信息…

算法代码中的循环矩阵在哪体现_「Machine Learning 学习小结」| 向量在梯度下降算法当中的应用...

写在前面&#xff1a;在之前的文章当中提到过&#xff0c;学习梯度下降算法&#xff0c;可能需要一点点线性代数的知识。在本篇文章当中&#xff0c;我们的讨论就涉及到了向量。笔者也曾提到&#xff0c;不妨把向量看成对数据进行批量操作的一种工具&#xff0c;这样可能对我们…

计算机用户 图片存储位置,手机相册在哪个文件夹,详细教您手机图片存放在哪里...

现在使用手机的用户是越来越多了&#xff0c;手机质量都非常好&#xff0c;因此才会受到许多人的褒奖。不过有用户却遇到了&#xff0c;用手机拍摄了照片或者用手机截图&#xff0c;手机连上电脑后&#xff0c;却在电脑上找不到图片&#xff0c;怎么办&#xff0c;下面&#xf…

揭秘京东文件系统JFS的前世今生,支持双11每秒约10万个对象同时读写

背景 作为一家大规模的自营式电商企业&#xff0c;京东需要存储海量的非结构化数据&#xff1a;商品图片、订单文本、仓库流转记录、App客户端文件、日志文件、内部文档等。对于存储这些数据&#xff0c;之前并没有统一的解决方案&#xff0c;都是各个业务线自行解决——MySQL …