一区二区三区在线-一区二区三区亚洲视频-一区二区三区亚洲-一区二区三区午夜-一区二区三区四区在线视频-一区二区三区四区在线免费观看

服務器之家:專注于服務器技術及軟件下載分享
分類導航

PHP教程|ASP.NET教程|Java教程|ASP教程|編程技術|正則表達式|C/C++|IOS|C#|Swift|Android|VB|R語言|JavaScript|易語言|vb.net|

服務器之家 - 編程語言 - Java教程 - JDK1.6“新“特性Instrumentation之JavaAgent(推薦)

JDK1.6“新“特性Instrumentation之JavaAgent(推薦)

2020-08-04 00:43大火yzs Java教程

這篇文章主要介紹了JDK1.6“新“特性Instrumentation之JavaAgent,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下

簡介

Java Agent是在JDK1.5以后,我們可以使用agent技術構建一個獨立于應用程序的代理程序(即為Agent),用來協助監測、運行甚至替換其他JVM上的程序。使用它可以實現虛擬機級別的AOP功能。

Agent分為兩種,一種是在主程序之前運行的Agent,一種是在主程序之后運行的Agent(前者的升級版,1.6以后提供)。

JavaAgent的作用Agent給我們程序帶來的影響.jpg

JDK1.6“新“特性Instrumentation之JavaAgent(推薦)

使用Agent-premain方法影響的程序效果圖.jpg

JDK1.6“新“特性Instrumentation之JavaAgent(推薦)

使用Agent-agentmain方法影響的程序效果圖.jpg

JDK1.6“新“特性Instrumentation之JavaAgent(推薦)

JavaAgent相關的API

在java.lang.instrument包下 給我們提供了相關的API

而最為主要的就是Instrumentation這個接口中的幾個方法

JDK1.6“新“特性Instrumentation之JavaAgent(推薦)

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public interface Instrumentation {
 
 /**
  * 添加Transformer(轉換器)
  * ClassFileTransformer類是一個接口,通常用戶只需實現這個接口的 byte[] transform()方法即可;
  * transform這個方法會返回一個已經轉換過的對象的byte[]數組
  * @param transformer   攔截器
  * @return canRetransform  是否能重新轉換
  */
    void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
 
 /**
  * 重新觸發類加載,
  * 該方法可以修改方法體、常量池和屬性值,但不能新增、刪除、重命名屬性或方法,也不能修改方法的簽名
  * @param classes   Class對象
  * @throws UnmodifiableClassException  異常
  */
 void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
 
 /**
  * 直接替換類的定義
  * 重新轉換某個對象,并已一個新的class格式,進行轉化。
  * 該方法可以修改方法體、常量池和屬性值,但不能新增、刪除、重命名屬性或方法,也不能修改方法的簽名
  * @param definitions   ClassDefinition對象[Class定義對象]
  * @throws ClassNotFoundException,UnmodifiableClassException  異常
  */
 void redefineClasses(ClassDefinition... definitions)throws ClassNotFoundException, UnmodifiableClassException;
 
 /**
  * 獲取當前被JVM加載的所有類對象
  * @return Class[]  class數組
  */
 Class[] getAllLoadedClasses();
}

后面我們會在代碼中具體用到這些方法。再詳細說明。

JavaAgent-premain方法1-初探效果:

實現main方法前執行業務邏輯

Agent1.java

?
1
2
3
4
5
public class Agent1 {
 public static void premain(String agent){
  System.out.println("Agent1 premain :" + agent);
 }
}

Demo1.java

?
1
2
3
4
5
6
7
8
9
10
public class Demo1 {
 
 /**
  * VM參數
  * -javaagent:D:\desktop\text\code\mycode\JavaAgentDemo\agent\target/agent.jar=input
  * */
 public static void main(String[] args) throws Exception {
  System.out.println("demo1");
 }
}

resources/META-INF/MANIFEST.MF

?
1
2
3
4
5
6
7
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: dahuoyzs
Created-By: Apache Maven 3.6.0
Build-Jdk: 1.8.0_171
Premain-Class: cn.bigfire.Agent1
Can-Retransform-Classes: true

運行效果

Agent1 premain :input
demo1

JavaAgent-premain方法2-實現修改代碼邏輯效果:

實現 修改 程序源代碼 hello -> hello agented

Agent2.java

?
1
2
3
4
5
6
7
8
9
10
11
12
public class Agent2 {
 /**
  * 可以運行在main方法啟動前
  * @param agent    輸入的參數
  * @param instrumentation    輸入的參數
  */
 public static void premain(String agent, Instrumentation instrumentation){
  System.out.println("Agent2 premain 2param :" + agent);
  instrumentation.addTransformer(new ConsoleTransformer(),true);
 }
 
}

ConsoleTransformer.java

?
1
2
3
4
5
6
7
8
9
10
11
public class ConsoleTransformer implements ClassFileTransformer {
 @Override
 public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
  if (className.equals("cn/bigfire/Console")){
   String root = StrUtil.subBefore(System.getProperty("user.dir"), "JavaAgentDemo", true);
   String classFile = root + "JavaAgentDemo/agent/src/main/resources/Console.class";
   return FileUtil.readBytes(classFile);
  }
  return classfileBuffer;
 }
}

Demo2.java

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Demo2 {
 
 /**
  * VM參數
  * -javaagent:D:\desktop\text\code\mycode\JavaAgentDemo\agent\target/agent.jar=input
  * */
 public static void main(String[] args) throws Exception {
  new Thread(()->{
   while (true){
    Console.hello();// public static void hello(){System.out.println("hello"); }
    ThreadUtil.sleep(2000);
   }
  }).start();
 }
}

resources/META-INF/MANIFEST.MF

?
1
2
3
4
5
6
7
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: dahuoyzs
Created-By: Apache Maven 3.6.0
Build-Jdk: 1.8.0_171
Premain-Class: cn.bigfire.Agent2
Can-Retransform-Classes: true

運行效果

Agent2 premain 2param :input
滿足條件
hello  agented
hello  agented
hello  agented
hello  agented

JavaAgent-premain方法3-無侵入動態修改程序源代碼實現方法耗時統計效果:

實現main方法外的所有方法統計時間

Agent3.java

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public class Agent3 {
 /**
  * 可以運行在main方法啟動前
  * @param agent       輸入的參數
  * @param instrumentation    instrumentation對象由JVM提供并傳入
  */
 public static void premain(String agent, Instrumentation instrumentation) {
  System.out.println("Agent3 premain :" + agent);
  instrumentation.addTransformer(new TimeCountTransformer());
 }
 
 /**
  * 時間統計Transformer 給要代理的方法添加時間統計
  */
 private static class TimeCountTransformer implements ClassFileTransformer {
  @Override
  public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
   try {
    className = className.replace("/", ".");
    if (className.equals("cn.bigfire.Demo3")) {
     //使用全稱,用于取得字節碼類<使用javassist>
     CtClass ctclass = ClassPool.getDefault().get(className);
     //獲得方法列表
     CtMethod[] methods = ctclass.getDeclaredMethods();
     //給方法設置代理
     Stream.of(methods).forEach(method-> agentMethod(ctclass,method));
     //CtClass轉byte[]數組
     return ctclass.toBytecode();
    }
   } catch (Exception e) {
    e.printStackTrace();
   }
   return null;
  }
 }
 
 /**
  * 代理方法,把傳入的方法經寫代理,并生成帶時間統計的方法,
  * @param ctClass       javassist的Class類
  * @param ctMethod      javassist的ctMethod方法
  * */
 public static void agentMethod(CtClass ctClass,CtMethod ctMethod){
  try {
   String mName = ctMethod.getName();
   if (!mName.equals("main")){//代理除了main方法以外的所有方法
    String newName = mName + "$Agent";
    ctMethod.setName(newName);
    CtMethod newMethod = CtNewMethod.copy(ctMethod, mName, ctClass, null);
    // 構建新的方法體
    String bodyStr = "{\n" +
      "long startTime = System.currentTimeMillis();\n" +
      newName + "();\n" +
      "long endTime = System.currentTimeMillis();\n" +
      "System.out.println(\""+newName+"() cost:\" +(endTime - startTime));\n" +
      "}";
    newMethod.setBody(bodyStr);// 替換新方法
    ctClass.addMethod(newMethod);// 增加新方法
   }
  }catch (Exception e){
   e.printStackTrace();
  }
 }
 
}

Demo3.java

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Demo3 {
 
 /**
  * VM參數
  * -javaagent:D:\desktop\text\code\mycode\JavaAgentDemo\agent\target/agent.jar=input
  */
 public static void main(String[] args) throws Exception {
  sleep1();
  sleep2();
 }
 
 public static void sleep1(){
  ThreadUtil.sleep(1000);
 }
 
 public static void sleep2(){
  ThreadUtil.sleep(2000);
 }
 
}

resources/META-INF/MANIFEST.MF

?
1
2
3
4
5
6
7
8
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: dahuoyzs
Created-By: Apache Maven 3.6.0
Build-Jdk: 1.8.0_171
Class-Path: ../javassist-3.12.1.GA.jar
Premain-Class: cn.bigfire.Agent3
Can-Retransform-Classes: true

運行效果

Agent3 premain :input
sleep1$Agent() cost:1005
sleep2$Agent() cost:2001

JavaAgent-agentmain方法1-實現運行時修改程序效果:

實現運行時 修改程序 hello -> hello agented

Agent4.java

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Agent4 {
 
 public static void premain(String agent){
  System.out.println("Agent4 premain 1param:" + agent);
 }
 
 public static void premain(String agent, Instrumentation instrumentation) {
  System.out.println("Agent4 premain 2param:" + agent);
  //premain時,由于堆里還沒有相應的Class。所以直接addTransformer,程序就會生效。
//  instrumentation.addTransformer(new ConsoleTransformer(),true);
 }
 
 public static void agentmain(String agent, Instrumentation instrumentation){
  System.out.println("Agent4 agentmain 2param :" + agent);
  instrumentation.addTransformer(new ConsoleTransformer(),true);
  //agentmain運行時 由于堆里已經存在Class文件,所以新添加Transformer后
  // 還要再調用一個 inst.retransformClasses(clazz); 方法來更新Class文件
  for (Class clazz:instrumentation.getAllLoadedClasses()) {
   if (clazz.getName().contains("cn.bigfire.Console")){
    try {
     instrumentation.retransformClasses(clazz);
    } catch (Exception e) {
     e.printStackTrace();
    }
   }
  }
 }
 
 public static void agentmain(String agent){
  System.out.println("Agent4 agentmain 1param :" + agent);
 }
 
}

Demo4

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Demo4 {
 /**
  * 打包agent4 -> 先運行demo2 -> 運行demo4 ->選擇程序demo2結尾的程序,即可運行時修改文件
  * VM參數
  * -javaagent:D:\desktop\text\code\mycode\JavaAgentDemo\agent\target/agent.jar=input
  * */
 public static void main(String[] args) throws Exception {
  while (true){
   List<VirtualMachineDescriptor> list = VirtualMachine.list();
   for (int i = 0; i < list.size(); i++) {
    VirtualMachineDescriptor jvm = list.get(i);;
    System.out.println("[" +i+ "]ID:"+jvm.id()+",Name:"+jvm.displayName());
   }
   System.out.println("請選擇第幾個");
   Scanner scanner = new Scanner(System.in);
   int s = scanner.nextInt();
   VirtualMachineDescriptor virtualMachineDescriptor = list.get(s);
   VirtualMachine attach = VirtualMachine.attach(virtualMachineDescriptor.id());
   String root = StrUtil.subBefore(System.getProperty("user.dir"), "JavaAgentDemo", true);
   String agentJar = root + "JavaAgentDemo\\agent\\target\\agent.jar";
   File file = new File(agentJar);
   System.out.println(file.exists());
   attach.loadAgent(agentJar,"param");
   attach.detach();
  }
 }
}

resources/META-INF/MANIFEST.MF

?
1
2
3
4
5
6
7
8
9
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: dahuoyzs
Created-By: Apache Maven 3.6.0
Build-Jdk: 1.8.0_171
Premain-Class: cn.bigfire.Agent4
Agent-Class: cn.bigfire.Agent4
Can-Retransform-Classes: true
Can-Redefine-Classes: true

此時的運行順序
打包agent4 -> 先運行demo2 -> 運行demo4 ->選擇程序demo2結尾的程序,即可運行時修改文件

運行效果
Demo2

Agent4 premain 2param:input
hello
hello

Demo4

?
1
2
3
4
5
6
7
8
9
[0]ID:12480,Name:cn.bigfire.Demo2
[1]ID:14832,Name:org.jetbrains.kotlin.daemon.KotlinCompileDaemon --daemon-runFilesPath xxx
[2]ID:14864,Name:
[3]ID:3952,Name:cn.bigfire.Demo4
[4]ID:14852,Name:org.jetbrains.idea.maven.server.RemoteMavenServer36
[5]ID:11928,Name:org.jetbrains.jps.cmdline.Launcher xxx
請選擇第幾個
0
true

Demo2

?
1
2
3
4
5
6
7
Agent4 premain 2param:input
hello
hello
Agent4 agentmain 2param :param
hello agented
hello agented
hello agented

JavaAgent-agentmain方法2-實現動態修改日志級別效果:

實現運行時 修改程序 模擬項目中的動態日志 info <-> debug

Agent5.java

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
public class Agent5 {
 
 public static void premain(String agent, Instrumentation instrumentation){
  System.out.println("Agent5 premain 2param :" + agent);
  instrumentation.addTransformer(new StartTransformer(),true);
 
  //這個方式不行。因為啟動時Class都還沒有呢。
//  for (Class clazz:inst.getAllLoadedClasses()) {
//   if (clazz.getName().equals("cn.bigfire.LogLevelStarter")){
//    try {
//     switchDebug(clazz);
//     instrumentation.retransformClasses(clazz);
//    } catch (Exception e) {
//     e.printStackTrace();
//    }
//   }
//  }
 }
 
 public static void agentmain(String agent, Instrumentation instrumentation){
  System.out.println("Agent5 agentmain 2param :" + agent);
  for (Class clazz:instrumentation.getAllLoadedClasses()) {
   if (clazz.getName().equals("cn.bigfire.LogLevelStarter")){
    try {
     switchAtomicDebug(clazz);
     instrumentation.retransformClasses(clazz);
    } catch (Exception e) {
     e.printStackTrace();
    }
   }
  }
 }
 
 public static class StartTransformer implements ClassFileTransformer {
  @Override
  public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
   //此時由于classBeingRedefined是空,所以還是不能用這個Class修改屬性呢,只能通過 讀取byte[]往堆里丟,才能用。
   if (className.equals("cn/bigfire/LogLevelStarter")){
    //【這是一個錯誤的思路】 premain的時候 classBeingRedefined是空的因為很多的Class還沒加載到堆中
//    if (classBeingRedefined!=null){
//     switchDebug(classBeingRedefined);
//     return toBytes(classBeingRedefined);
//    }
    //正常的讀取一共文件byte[]數組
    String root = StrUtil.subBefore(System.getProperty("user.dir"), "JavaAgentDemo", true);
    String classFile = root + "JavaAgentDemo/agent/src/main/resources/LogLevelStarter.class";
    return FileUtil.readBytes(classFile);
   }
   return classfileBuffer;
  }
 }
 
 /**
  * 可序列化對象轉byte[]數組
  * @param clazz    要轉byte[]數組的對象
  * @return byte[]   返回byte[]數組
  */
 public static byte[] toBytes(Serializable clazz){
  try {
   ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
   ObjectOutputStream stream = new ObjectOutputStream(byteArrayOutputStream);
   stream.writeObject(clazz);
   return byteArrayOutputStream.toByteArray();
  }catch (Exception e){
   e.printStackTrace();
  }
  return null;
 }
 
 public static void switchDebug(Class clazz){
  try {
   Field field1 = clazz.getDeclaredField("isDebug");
   field1.setAccessible(true);
   boolean debug = field1.getBoolean(clazz);
   field1.setBoolean(clazz,!debug);
  }catch (Exception e){
   e.printStackTrace();
  }
 }
 
 public static void switchAtomicDebug(Class clazz){
  try {
   Field field2 = clazz.getDeclaredField("atomicDebug");
   field2.setAccessible(true);
   AtomicBoolean atomicDebug = (AtomicBoolean)field2.get(clazz);
   atomicDebug.set(!atomicDebug.get());
  }catch (Exception e){
   e.printStackTrace();
  }
 }
 
}

注意,需要先把LogLevelStarter.java中的isDebug 改為true編譯一下。放到src/main/resources/目錄下;

LogLevelStarter.java

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class LogLevelStarter {
 
 public static volatile boolean isDebug = false;
 public static AtomicBoolean atomicDebug = new AtomicBoolean(false);
 
 /**
  * VM參數
  * -javaagent:D:\desktop\text\code\mycode\JavaAgentDemo\agent\target/agent.jar=input
  */
 public static void main(String[] args) throws Exception {
  new Thread(()->{
   for (;;){
    //死循環,每隔兩秒打印一個日志。
    System.out.print(isDebug ? "volatile debug" : "volatile info");
    System.out.print("\t");
    System.out.println(atomicDebug.get() ? "atomicDebug debug" : "atomicDebug info");
    ThreadUtil.sleep(2000);
   }
  }).start();
 }
}

resources/META-INF/MANIFEST.MF

?
1
2
3
4
5
6
7
8
9
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: dahuoyzs
Created-By: Apache Maven 3.6.0
Build-Jdk: 1.8.0_171
Premain-Class: cn.bigfire.Agent5
Agent-Class: cn.bigfire.Agent5
Can-Retransform-Classes: true
Can-Redefine-Classes: true

此時的運行順序
打包agent5 -> 先運行LogLevelStarter -> 運行demo4 ->選擇程序LogLevelStarter結尾的程序,即可運行時修改文件

運行效果

LogLevelStarter

Agent5 premain 2param :input
volatile debug atomicDebug info
volatile debug atomicDebug info

Demo4

?
1
2
3
4
5
6
7
8
9
[0]ID:12592,Name:cn.bigfire.LogLevelStarter
[1]ID:12880,Name:cn.bigfire.Demo4
[2]ID:14832,Name:org.jetbrains.kotlin.daemon.KotlinCompileDaemon --daemon-runFilesPath xxx
[3]ID:14864,Name:
[4]ID:14852,Name:org.jetbrains.idea.maven.server.RemoteMavenServer36
[5]ID:8116,Name:org.jetbrains.jps.cmdline.Launcher xxx
請選擇第幾個
0
true

LogLevelStarter

?
1
2
3
4
5
6
Agent5 premain 2param :input
volatile debug  atomicDebug info
volatile debug  atomicDebug info
Agent5 agentmain 2param :param
volatile debug  atomicDebug debug
volatile debug  atomicDebug debug

在Agent5中,其實使用premain和agentmain。

premain把volatile修飾的isDbug給修改為true了。

而agentmain時把atomicDebug的值進行多次取反操作。

自己實現一個熱部署功能的大致思路

當運行完本項目中的幾個demo之后。

讀者可能對Java Agent有了一些基本的概念

最起碼我們知道了premain是可以運行在main函數前的。

agentmain是可以在程序運行時,修改程序內的一些類文件的。

那么熱部署很明顯就是使用的agentmain這個特性了

那么熱部署具體應該怎么實現呢?

這里先有個大概的思路。后續如果有經歷,可以簡單按照這個思路實現一下

思路

當我們文件發生修改的時候,項目會重新加載我們的類。

那么這里肯定會涉及到文件變化的觀察。 即 觀察者設計模式跑不了

首先遞歸當前項目目錄。并根據文件類型,如(.java ,xml,yml等)將此類文件注冊觀察者模式。

當文件內容發生變化時,會調用 監聽器中的回調方法;

在回調中完成如下(具體實現時未必需要)

使用Java1.6的JavaCompiler編譯Java文件;
自定義ClassLoader 裝載 編譯好的Class到堆中

使用agentmain修改原Class文件替換成新的Class文件

完成熱加載

JavaAgent的應用場景

apm:(Application Performance Management)應用性能管理。pinpoint、cat、skywalking等都基于Instrumentation實現
idea的HotSwap、Jrebel等熱部署工具
應用級故障演練
Java診斷工具Arthas、Btrace等

源代碼

?
1
2
3
4
5
6
{
    "author": "大火yzs",
    "title": "【JavaAgent】JavaAgent入門教程",
    "tag": "JavaAgent,Instrumentation,運行時動態修改源程序",
    "createTime": "2020-08-02 18:30"
}

總結

到此這篇關于JDK1.6“新“特性Instrumentation之JavaAgent的文章就介紹到這了,更多相關JDK1.6“新“特性Instrumentation內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!

原文鏈接:https://blog.csdn.net/qq_34173920/article/details/107748852

延伸 · 閱讀

精彩推薦
主站蜘蛛池模板: 亚州精品视频 | a在线观看欧美在线观看 | 大又大又粗又爽女人毛片 | jazz欧美人免费xxxxxx | 亚洲欧美日韩中文高清一 | 特黄视频 | 久草青青在线 | 欧洲美女人牲交一级毛片 | 国产精品手机视频一区二区 | 2021最新国产成人精品视频 | 男女男精品网站免费观看 | 岛国不卡 | 免费网站国产 | 92国产福利久久青青草原 | 免费看美女被靠到爽 | aaa一级最新毛片 | 美女被吸乳老师羞羞漫画 | 好大好硬好湿好紧h | 国产精品合集久久久久青苹果 | 精品91一区二区三区 | 性色视频免费 | 久久水蜜桃亚洲AV无码精品偷窥 | 日本爽p大片免费观看 | 免看一级一片一在线看 | 天天色踪合 | 91免费在线 | 国产成人久久精品区一区二区 | 胖女性大bbbbbb | 天堂69亚洲精品中文字幕 | 日本精a在线观看 | 色婷婷久久综合中文久久一本` | 日韩在线视频免费不卡一区 | www.四虎com| 毛片网在线观看 | 操女人的b| 日本在线播放 | 天美传媒传媒免费观看 | 欧美坐爱 | 卫生间被教官做好爽HH视频 | 男女羞羞的视频 | 干美女在线视频 |