精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

基于SPI的增強(qiáng)式插件框架設(shè)計(jì)

開(kāi)發(fā)
本文的技術(shù),從雙親委派模型到自定義類(lèi)加載器,再到基于自定義類(lèi)加載器實(shí)現(xiàn)的類(lèi)交換,基于Java SPI實(shí)現(xiàn)的類(lèi)交換,最后到基于Java SPI+ Java Agent + Javassist實(shí)現(xiàn)的插件框架及框架支持遠(yuǎn)程插件化,來(lái)一步一步的向讀者展示所涉及的知識(shí)點(diǎn)。

很久之前,為了診斷線(xiàn)上的問(wèn)題,就想要是能有工具可以在線(xiàn)上出問(wèn)題的時(shí)候,放個(gè)診斷包進(jìn)去馬上生效,就能看到線(xiàn)上問(wèn)題的所在,那該是多么舒服的事情。后來(lái)慢慢的切換到j(luò)ava領(lǐng)域后,這種理想也變成了現(xiàn)實(shí),小如IDEA中更改頁(yè)面就能馬上生效,大如利用Althas工具進(jìn)行線(xiàn)上數(shù)據(jù)診斷,可謂是信手拈來(lái),極大的方便了開(kāi)發(fā)和診斷。后來(lái)深入研究之后,就慢慢的不滿(mǎn)足框架本身帶來(lái)的便利了,造輪子的想法慢慢在腦中揮之不去,這也是本文產(chǎn)生的原因了。接下來(lái),你無(wú)需準(zhǔn)備任何前置知識(shí),因?yàn)槲乙呀?jīng)為你準(zhǔn)備好了ClassLoader甜點(diǎn),Javassist配菜,JavaAgent高湯,手寫(xiě)插件加載器框架主食,外加SPI知識(shí)做調(diào)料,且讓我們整理餐具,開(kāi)始這一道頗有點(diǎn)特色的吃播旅程吧。

雙親委派模型

開(kāi)始前,讓我們先聊聊雙親委派這個(gè)話(huà)題,因?yàn)闊o(wú)論是做熱部署,還是做字節(jié)碼增強(qiáng),甚至于日常的編碼,這都是繞不開(kāi)的一個(gè)話(huà)題。先看如下圖示:

圖片

從如上圖示,我們可以看到雙親委派模型整體的工作方式,整體講解如下:

類(lèi)加載器的findClass(loadClass)被調(diào)用

  1. 進(jìn)入App ClassLoader中,先檢查緩存中是否存在,如果存在,則直接返回
  2. 步驟2中的緩存中不存在,則被代理到父加載器,即Extension ClassLoader
  3. 檢查Extension ClassLoader緩存中是否存在
  4. 步驟4中的緩存中不存在,則被代理到父加載器,即Bootstrap ClassLoader
  5. 檢查Bootstrap ClassLoader緩存中是否存在
  6. 步驟6中的緩存中不存在,則從Bootstrap ClassLoader的類(lèi)搜索路徑下的文件中尋找,一般為rt.jar等,如果找不到,則拋出ClassNotFound Exception
  7. Extension ClassLoader會(huì)捕捉ClassNotFound錯(cuò)誤,然后從Extension ClassLoader的類(lèi)搜索路徑下的文件中尋找,一般為環(huán)境變量$JRE_HOME/lib/ext路徑下,如果也找不到,則拋出ClassNotFound Exception
  8. App ClassLoader會(huì)捕捉ClassNotFound錯(cuò)誤,然后從App ClassLoader的類(lèi)搜索路徑下的文件中尋找,一般為環(huán)境變量$CLASSPATH路徑下,如果找到,則將其讀入字節(jié)數(shù)組,如果也找不到,則拋出ClassNotFound Exception。如果找到,則App ClassLoader調(diào)用defineClass()方法。

通過(guò)上面的整體流程描述,是不是感覺(jué)雙親委派機(jī)制也不是那么難理解。本質(zhì)就是先查緩存,緩存中沒(méi)有就委托給父加載器查詢(xún)緩存,直至查到Bootstrap加載器,如果Bootstrap加載器在緩存中也找不到,就拋錯(cuò),然后這個(gè)錯(cuò)誤再被一層層的捕捉,捕捉到錯(cuò)誤后就查自己的類(lèi)搜索路徑,然后層層處理。

自定義ClassLoader

了解了雙親委派機(jī)制后,那么如果要實(shí)現(xiàn)類(lèi)的熱更換或者是jar的熱部署,就不得不涉及到自定義ClassLoader了,實(shí)際上其本質(zhì)依舊是利用ClassLoader的這種雙親委派機(jī)制來(lái)進(jìn)行操作的。遵循上面的流程,我們很容易的來(lái)實(shí)現(xiàn)利用自定義的ClassLoader來(lái)實(shí)現(xiàn)類(lèi)的熱交換功能:

public class CustomClassLoader extends ClassLoader {




//需要該類(lèi)加載器直接加載的類(lèi)文件的基目錄
private String baseDir;
public CustomClassLoader(String baseDir, String[] classes) throws IOException {
super();
this.baseDir = baseDir;
loadClassByMe(classes);
}
private void loadClassByMe(String[] classes) throws IOException {
for (int i = 0; i < classes.length; i++) {
findClass(classes[i]);
}
}
/**
* 重寫(xiě)findclass方法
*
* 在ClassLoader中,loadClass方法先從緩存中找,緩存中沒(méi)有,會(huì)代理給父類(lèi)查找,如果父類(lèi)中也找不到,就會(huì)調(diào)用此用戶(hù)實(shí)現(xiàn)的findClass方法
*
* @param name
* @return
*/
@Override
protected Class findClass(String name) {
Class clazz = null;
StringBuffer stringBuffer = new StringBuffer(baseDir);
String className = name.replace('.', File.separatorChar) + ".class";
stringBuffer.append(File.separator + className);
File classF = new File(stringBuffer.toString());
try {
clazz = instantiateClass(name, new FileInputStream(classF), classF.length());
} catch (IOException e) {
e.printStackTrace();
}
return clazz;
}
private Class instantiateClass(String name, InputStream fin, long len) throws IOException {
byte[] raw = new byte[(int) len];
fin.read(raw);
fin.close();
return defineClass(name, raw, 0, raw.length);
}
}

這里需要注意的是,在自定義的類(lèi)加載器中,我們可以覆寫(xiě)findClass,然后利用defineClass加載類(lèi)并返回。

上面這段代碼,我們就實(shí)現(xiàn)了一個(gè)最簡(jiǎn)單的自定義類(lèi)加載器,但是能映射出雙親委派模型呢?

首先點(diǎn)開(kāi)ClassLoader類(lèi),在里面翻到這個(gè)方法:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

如果對(duì)比著雙親委派模型來(lái)看,則loadClass方法對(duì)應(yīng)之前提到的步驟1-8,點(diǎn)進(jìn)去findLoadedClass方法,可以看到底層實(shí)現(xiàn)是native的native final Class<?> findLoadedClass0 方法,這個(gè)方法會(huì)從JVM緩存中進(jìn)行數(shù)據(jù)查找。后面的分析方法類(lèi)似。

而自定義類(lèi)加載器中的findClass方法,則對(duì)應(yīng)步驟9:

clazz = instantiateClass(name, new FileInputStream(classF), classF.length());
//省略部分邏輯
return defineClass(name, raw, 0, raw.length);

看看,整體是不是很清晰?

自定義類(lèi)加載器實(shí)現(xiàn)類(lèi)的熱交換

寫(xiě)完自定義類(lèi)加載器,來(lái)看看具體的用法吧,我們創(chuàng)建一個(gè)類(lèi),擁有如下內(nèi)容:

package com.tw.client;
public class Foo {
public Foo() {
}
public void sayHello() {
System.out.println("hello world22222! (version 11)");
}
}

顧名思義,此類(lèi)只要調(diào)用sayHello方法,便會(huì)打印出hello world22222! (version 11)出來(lái)。

熱交換處理過(guò)程如下:

public static void main(String[] args) throws Exception {
while (true) {
run();
Thread.sleep(1000);
}
}
/**
* ClassLoader用來(lái)加載class類(lèi)文件的,實(shí)現(xiàn)類(lèi)的熱替換
* 注意,需要在swap目錄下,一層層建立目錄com/tw/client/,然后將Foo.class放進(jìn)去
* @throws Exception
*/
public static void run() throws Exception {
CustomClassLoader customClassLoader = new CustomClassLoader("swap", new String[]{"com.tw.client.Foo"});
Class clazz = customClassLoader.loadClass("com.tw.client.Foo");
Object foo = clazz.newInstance();
Method method = foo.getClass().getMethod("sayHello", new Class[]{});
method.invoke(foo, new Object[]{});
}

當(dāng)我們運(yùn)行起來(lái)后,我們會(huì)將提前準(zhǔn)備好的另一個(gè)Foo.class來(lái)替換當(dāng)前這個(gè),來(lái)看看結(jié)果吧(直接將新的Foo.class類(lèi)拷貝過(guò)去覆蓋即可):

hello world22222! (version 11)
hello world22222! (version 11)
hello world22222! (version 11)
hello world22222! (version 11)
hello world22222! (version 11)
hello world2222! (version 2)
hello world2222! (version 2)
hello world2222! (version 2)
hello world2222! (version 2)

可以看到,當(dāng)我們替換掉原來(lái)運(yùn)行的類(lèi)的時(shí)候,輸出也就變了,變成了新類(lèi)的輸出結(jié)果。整體類(lèi)的熱交換成功。

不知道我們注意到一個(gè)細(xì)節(jié)沒(méi)有,在上述代碼中,我們先創(chuàng)建出Object的類(lèi)對(duì)象,然后利用Method.invoke方法來(lái)調(diào)用類(lèi):

有人在這里會(huì)疑惑,為啥不直接轉(zhuǎn)換為Foo類(lèi),然后調(diào)用類(lèi)的Foo.sayHello方法呢?像下面這種方式:

Foo foo2 = (Foo) clazz.newInstance();
foo2.sayHello();

這種方式是不行的,但是大家知道為啥不行嗎?

我們知道,我們寫(xiě)的類(lèi),一般都是被AppClassloader加載的,也就是說(shuō),你寫(xiě)在main啟動(dòng)類(lèi)中的所有類(lèi),只要你寫(xiě)出來(lái),那么就會(huì)被AppClassloader加載,所以,如果這里我們強(qiáng)轉(zhuǎn)為Foo類(lèi)型,那鐵定是會(huì)被AppClassloader加載的,但是由于我們的clazz對(duì)象是由CustomerClassloader加載的,所以這里就會(huì)出現(xiàn)這樣的錯(cuò)誤:

java.lang.ClassCastException: com.tw.client.Foo cannot be cast to com.tw.client.Foo

那有什么方法可以解決這個(gè)問(wèn)題嗎?其實(shí)是有的,就是對(duì)Foo對(duì)象抽象出一個(gè)Interface,比如說(shuō)IFoo,然后轉(zhuǎn)換的時(shí)候,轉(zhuǎn)換成接口,就不會(huì)有這種問(wèn)題了:

IFoo foo2 = (IFoo) clazz.newInstance();
foo2.sayHello();

通過(guò)接口這種方式,我們就很容易對(duì)運(yùn)行中的組件進(jìn)行類(lèi)的熱交換了,屬實(shí)方便。

需要注意的是,主線(xiàn)程的類(lèi)加載器,一般都是AppClassLoader,但是當(dāng)我們創(chuàng)建出子線(xiàn)程后,其類(lèi)加載器都會(huì)繼承自其創(chuàng)建者的類(lèi)加載器,但是在某些業(yè)務(wù)中,我想在子線(xiàn)程中使用自己的類(lèi)加載器,有什么辦法嗎?其實(shí)這里也就是打斷雙親委派機(jī)制。

由于Thread對(duì)象中已經(jīng)附帶了ContextClassLoader屬性,所以這里我們可以很方便的進(jìn)行設(shè)置和獲?。?/p>

//設(shè)置操作
Thread t = Thread.currentThread();
t.setContextClassLoader(loader);
//獲取操作
Thread t = Thread.currentThread();
ClassLoader loader = t.getContextClassLoader();
Class<?> cl = loader.loadClass(className);

SPI實(shí)現(xiàn)類(lèi)的熱交換

說(shuō)完基于自定義ClassLoader來(lái)進(jìn)行類(lèi)的熱交換后,我們?cè)賮?lái)說(shuō)說(shuō)Java中的SPI。說(shuō)到SPI相信大家都聽(tīng)過(guò),因?yàn)樵趈ava中天生集成,其內(nèi)部機(jī)制也是利用了自定義的類(lèi)加載器,然后進(jìn)行了良好的封裝暴露給用戶(hù),具體的源碼大家可以自定翻閱ServiceLoader類(lèi)。

這里我們寫(xiě)個(gè)簡(jiǎn)單的例子:

public interface HelloService {
void sayHello(String name);
}
public class HelloServiceProvider implements HelloService {
@Override
public void sayHello(String name) {
System.out.println("Hello " + name);
}
}
public class NameServiceProvider implements HelloService{
@Override
public void sayHello(String name) {
System.out.println("Hi, your name is " + name);
}
}

然后我們基于接口的包名+類(lèi)名作為路徑,創(chuàng)建出com.tinywhale.deploy.spi.HelloService文件到resources中的META-INF.services文件夾,里面放入如下內(nèi)容:

com.tinywhale.deploy.spi.HelloServiceProvider
com.tinywhale.deploy.spi.NameServiceProvider

然后在啟動(dòng)類(lèi)中運(yùn)行:

public static void main(String...args) throws Exception {
while(true) {
run();
Thread.sleep(1000);
}
}
private static void run(){
ServiceLoader<HelloService> serviceLoader = ServiceLoader.load(HelloService.class);
for (HelloService helloWorldService : serviceLoader) {
helloWorldService.sayHello("myname");
}
}

可以看到,在啟動(dòng)類(lèi)中,我們利用ServiceLoader類(lèi)來(lái)遍歷META-INF.services文件夾下面的provider,然后執(zhí)行,則輸出結(jié)果為兩個(gè)類(lèi)的輸出結(jié)果。之后在執(zhí)行過(guò)程中,我們?nèi)arget文件夾中,將com.tinywhale.deploy.spi.HelloService文件中的NameServiceProvider注釋掉,然后保存,就可以看到只有一個(gè)類(lèi)的輸出結(jié)果了。

Hello myname
Hi, your name is myname
Hello myname
Hi, your name is myname
Hello myname
Hi, your name is myname
Hello myname
Hello myname
Hello myname
Hello myname

這種基于SPI類(lèi)的熱交換,比自己自定義加載器更加簡(jiǎn)便,推薦使用。

自定義類(lèi)加載器實(shí)現(xiàn)Jar部署

上面講解的內(nèi)容,一般是類(lèi)的熱交換,但是如果我們需要對(duì)整個(gè)jar包進(jìn)行熱部署,該怎么做呢?雖然現(xiàn)在有很成熟的技術(shù),比如OSGI等,但是這里我將從原理層面來(lái)講解如何對(duì)Jar包進(jìn)行熱部署操作。

由于內(nèi)置的URLClassLoader本身可以對(duì)jar進(jìn)行操作,所以我們只需要自定義一個(gè)基于URLClassLoader的類(lèi)加載器即可:

public class BizClassLoader extends URLClassLoader {
public BizClassLoader(URL[] urls) {
super(urls);
}
}

注意,我們打的jar包,最好打成fat jar,這樣處理起來(lái)方便,不至于少打東西:

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.4.3</version>
<configuration>
<!-- 自動(dòng)將所有不使用的類(lèi)排除-->
<minimizeJar>true</minimizeJar>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<shadedArtifactAttached>true</shadedArtifactAttached>
<shadedClassifierName>biz</shadedClassifierName>
</configuration>
</execution>
</executions>
</plugin>

之后,我們就可以使用了:

public static void main(String... args) throws Exception {
while (true) {
loadJarFile();
Thread.sleep(1000);
}
}
/**
* URLClassLoader 用來(lái)加載Jar文件, 直接放在swap目錄下即可
*
* 動(dòng)態(tài)改變jar中類(lèi),可以實(shí)現(xiàn)熱加載
*
* @throws Exception
*/
public static void loadJarFile() throws Exception {
File moduleFile = new File("swap\\tinywhale-client-0.0.1-SNAPSHOT-biz.jar");
URL moduleURL = moduleFile.toURI().toURL();
URL[] urls = new URL[] { moduleURL };
BizClassLoader bizClassLoader = new BizClassLoader(urls);
Class clazz = bizClassLoader.loadClass("com.tw.client.Bar");
Object foo = clazz.newInstance();
Method method = foo.getClass().getMethod("sayBar", new Class[]{});
method.invoke(foo, new Object[]{});
bizClassLoader.close();
}

啟動(dòng)起來(lái),看下輸出,之后用一個(gè)新的jar覆蓋掉,來(lái)看看結(jié)果吧:

I am bar, Foo's sister, can you catch me ?????????????
I am bar, Foo's sister, can you catch me ?????????????
I am bar, Foo's sister, can you catch me ?。。?!
I am bar, Foo's sister, can you catch me ?。。?!
I am bar, Foo's sister, can you catch me !?。?!
I am bar, Foo's sister, can you catch me !?。?!

可以看到,jar包被自動(dòng)替換了。當(dāng)然,如果想卸載此包,我們可以調(diào)用如下語(yǔ)句進(jìn)行卸載:

bizClassLoader.close();

需要注意的是,jar包中不應(yīng)有長(zhǎng)時(shí)間運(yùn)行的任務(wù)或者子線(xiàn)程等,因?yàn)檎{(diào)用類(lèi)加載器的close方法后,會(huì)釋放一些資源,但是長(zhǎng)時(shí)間運(yùn)行的任務(wù)并不會(huì)終止。所以這種情況下,如果你卸載了舊包,然后馬上加載新包,且包中有長(zhǎng)時(shí)間的任務(wù),請(qǐng)確認(rèn)做好業(yè)務(wù)防重,否則會(huì)引發(fā)不可知的業(yè)務(wù)問(wèn)題。

由于Spring中已經(jīng)有對(duì)jar包進(jìn)行操作的類(lèi),我們可以配合上自己的annotation實(shí)現(xiàn)特定的功能,比如擴(kuò)展點(diǎn)實(shí)現(xiàn),插件實(shí)現(xiàn),服務(wù)檢測(cè)等等等等,用途非常廣泛,大家可以自行發(fā)掘。

上面講解的基本是原理部分,由于目前市面上有很多成熟的組件,比如OSGI等,已經(jīng)實(shí)現(xiàn)了熱部署熱交換等的功能,所以很推薦大家去用一用。

說(shuō)到這里,相信大家對(duì)類(lèi)的熱交換,jar的熱部署應(yīng)該有初步的概念了,但是這僅僅算是開(kāi)胃小菜。由于熱部署一般都是和字節(jié)碼增強(qiáng)結(jié)合著來(lái)用的,所以這里我們先來(lái)大致熟悉一下Java Agent技術(shù)。

代碼增強(qiáng)  技術(shù)拾憶

話(huà)說(shuō)在JDK中,一直有一個(gè)比較重要的jar包,名稱(chēng)為rt.jar,他是java運(yùn)行時(shí)環(huán)境中,最核心和最底層的類(lèi)庫(kù)的來(lái)源。比如java.lang.String, java.lang.Thread, java.util.ArrayList等均來(lái)源于這個(gè)類(lèi)庫(kù)。今天我們所要講解的角色是rt.jar中的java.lang.instrument包,此包提供的功能,可以讓我們?cè)谶\(yùn)行時(shí)環(huán)境中動(dòng)態(tài)的修改系統(tǒng)中的類(lèi),而Java Agent作為其中一個(gè)重要的組件,極具特色。

現(xiàn)在我們有個(gè)場(chǎng)景,比如說(shuō),每次請(qǐng)求過(guò)來(lái),我都想把jvm數(shù)據(jù)信息或者調(diào)用量上報(bào)上來(lái),由于應(yīng)用已經(jīng)上線(xiàn),無(wú)法更改代碼了,那么有什么辦法來(lái)實(shí)現(xiàn)嗎?當(dāng)然有,這也是Java Agent最擅長(zhǎng)的場(chǎng)合,當(dāng)然也不僅僅只有這種場(chǎng)合,諸如大名鼎鼎的熱部署JRebel,阿里的arthas,線(xiàn)上診斷工具btrace,UT覆蓋工具JaCoCo等,不一而足。

在使用Java Agent前,我們需要了解其兩個(gè)重要的方法:

/**
* main方法執(zhí)行之前執(zhí)行,manifest需要配置屬性Premain-Class,參數(shù)配置方式載入
*/
public static void premain(String agentArgs, Instrumentation inst);
/**
* 程序啟動(dòng)后執(zhí)行,manifest需要配置屬性Agent-Class,Attach附加方式載入
*/
public static void agentmain(String agentArgs, Instrumentation inst);

還有個(gè)必不可少的東西是MANIFEST.MF文件,此文件需要放置到resources/META-INF文件夾下,此文件一般包含如下內(nèi)容:

Premain-class                : main方法執(zhí)行前執(zhí)行的agent類(lèi).
Agent-class : 程序啟動(dòng)后執(zhí)行的agent類(lèi).
Can-Redefine-Classes : agent是否具有redifine類(lèi)能力的開(kāi)關(guān),true表示可以,false表示不可以.
Can-Retransform-Classes : agent是否具有retransform類(lèi)能力的開(kāi)關(guān),true表示可以,false表示不可以.
Can-Set-Native-Method-Prefix : agent是否具有生成本地方法前綴能力的開(kāi)關(guān),trie表示可以,false表示不可以.
Boot-Class-Path : 此路徑會(huì)被加入到BootstrapClassLoader的搜索路徑.

在對(duì)jar進(jìn)行打包的時(shí)候,最好打成fat jar,可以減少很多不必要的麻煩,maven加入如下打包內(nèi)容:

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>

而MF配置文件,可以利用如下的maven內(nèi)容進(jìn)行自動(dòng)生成:

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
</plugin>

工欲善其事必先利其器,準(zhǔn)備好了之后,先來(lái)手寫(xiě)個(gè)Java Agent嘗鮮吧,模擬premain調(diào)用,main調(diào)用和agentmain調(diào)用。

首先是premain調(diào)用類(lèi) ,agentmain調(diào)用類(lèi),main調(diào)用類(lèi):

//main執(zhí)行前調(diào)用
public class AgentPre {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("execute premain method");
}
}
//main主方法入口
public class App {
public static void main(String... args) throws Exception {
System.out.println("execute main method ");
}
}
//main執(zhí)行后調(diào)用
public class AgentMain {
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("execute agentmain method");
}
}

可以看到,邏輯很簡(jiǎn)單,輸出了方法執(zhí)行體中打印的內(nèi)容。之后編譯jar包,則會(huì)生成fat jar。需要注意的是,MANIFEST.MF文件需要手動(dòng)創(chuàng)建下,里面加入如下內(nèi)容:

Manifest-Version: 1.0
Premain-Class: com.tinywhale.deploy.javaAgent.AgentPre
Agent-Class: com.tinywhale.deploy.javaAgent.AgentMain

由于代碼是在IDEA中啟動(dòng),所以想要執(zhí)行premain,需要在App4a啟動(dòng)類(lèi)上右擊:Run App.main(),之后IDEA頂部會(huì)出現(xiàn)App的執(zhí)行配置,我們需要點(diǎn)擊Edit Configurations選項(xiàng),然后在VM options中填入如下命令:

-javaagent:D:\app\tinywhale\tinywhale-deploy\target\tinywhale-deploy-1.0-SNAPSHOT-biz.jar

之后啟動(dòng)App,就可以看到輸出結(jié)果了。注意這里最好用fat jar,減少出錯(cuò)的機(jī)率。

execute premain method
execute main method

但是這里的話(huà),我們看不到agentmain輸出,是因?yàn)閍gentmain的運(yùn)行,是需要進(jìn)行attach的,這里我們對(duì)agentmain進(jìn)行attach:

public class App {
public static void main(String... args) throws Exception {
System.out.println("execute main method ");
attach();
}
private static void attach() {
File agentFile = Paths.get("D:\\app\\tinywhale\\tinywhale-deploy\\target\\tinywhale-deploy-1.0-SNAPSHOT.jar").toFile();
try {
String name = ManagementFactory.getRuntimeMXBean().getName();
String pid = name.split("@")[0];
VirtualMachine jvm = VirtualMachine.attach(pid);
jvm.loadAgent(agentFile.getAbsolutePath());
} catch (Exception e) {
System.out.println(e);
}
}
}

啟動(dòng)app后,得到的結(jié)果為:

execute premain method
execute main method
execute agentmain method

可以看到,整個(gè)執(zhí)行都被串起來(lái)了。

講到這里,相信大家基本上理解java agent的執(zhí)行順序和配置了吧, premain執(zhí)行需要配置-javaagent啟動(dòng)參數(shù),而agentmain執(zhí)行需要attach vm pid。

看到這里,相信對(duì)java agent已經(jīng)有個(gè)初步的認(rèn)識(shí)了吧。接下來(lái),我們就基于Java SPI + Java Agent + Javassist來(lái)實(shí)現(xiàn)一個(gè)插件系統(tǒng),這個(gè)插件系統(tǒng)比較特殊的地方,就是可以增強(qiáng)spring框架,使其路徑自動(dòng)注冊(cè)到component-scan路徑中,頗有點(diǎn)霸道(雞賊)的意思。Javassist框架的使用方式。

插件框架  玉汝于成

首先來(lái)說(shuō)下這個(gè)框架的主體思路,使用Java SPI來(lái)做插件系統(tǒng);使用Java Agent來(lái)使得插件可以在main主入口方法前或者是方法后執(zhí)行;使用Javassist框架來(lái)進(jìn)行字節(jié)碼增強(qiáng),即實(shí)現(xiàn)對(duì)spring框架的增強(qiáng)。

針對(duì)插件部分,我們可以定義公共的接口契約:

public interface IPluginExecuteStrategy {
/**
* 執(zhí)行方法
* @param agentArgs
* @param inst
*/
void execute(String agentArgs, Instrumentation inst);
}

然后針對(duì)premain和agentmain,利用策略模式進(jìn)行組裝如下:

premain處理策略類(lèi)

public class PluginPreMainExecutor implements IPluginExecuteStrategy{




/**
* 掃描加載的plugin,識(shí)別出@PreMainCondition并加載執(zhí)行
*/
@Override
public void execute(String agentArgs, Instrumentation inst) {
//獲取前置執(zhí)行集合
List<String> pluginNames = AgentPluginAnnotationHelper.annoProcess(PreMainCondition.class);
ServiceLoader<IPluginService> pluginServiceLoader = ServiceLoader.load(IPluginService.class);
//只執(zhí)行帶有PreMainCondition的插件
for (IPluginService pluginService : pluginServiceLoader) {
if (pluginNames.contains(pluginService.getPluginName())) {
pluginService.pluginLoad(agentArgs, inst);
}
}
}
}

agentmain處理策略類(lèi)

public class PluginAgentMainExecutor implements IPluginExecuteStrategy {




/**
* 掃描加載的plugin,識(shí)別出@AgentMainCondition并加載執(zhí)行
*/
@Override
public void execute(String agentArgs, Instrumentation inst) {
//獲取后置執(zhí)行集合
List<String> pluginNames = AgentPluginAnnotationHelper.annoProcess(AgentMainCondition.class);
ServiceLoader<IPluginService> pluginServiceLoader = ServiceLoader.load(IPluginService.class);
for (IPluginService pluginService : pluginServiceLoader) {
//只執(zhí)行帶有AgentMainCondition的插件
if (pluginNames.contains(pluginService.getPluginName())) {
pluginService.pluginLoad(agentArgs, inst);
}
}
}
}

針對(duì)premain和agentmain,執(zhí)行器工廠(chǎng)如下:

public class AgentPluginContextFactory {
/**
* 創(chuàng)建agent pre執(zhí)行上下文
* @return
*/
public static PluginExecutorContext makeAgentPreExecuteContext() {
IPluginExecuteStrategy strategy = new PluginPreMainExecutor();
PluginExecutorContext context = new PluginExecutorContext(strategy);
return context;
}




/**
* 創(chuàng)建agent main執(zhí)行上下文
* @return
*/
public static PluginExecutorContext makeAgentMainExecuteContext() {
IPluginExecuteStrategy strategy = new PluginAgentMainExecutor();
PluginExecutorContext context = new PluginExecutorContext(strategy);
return context;
}


}

編寫(xiě)Premain-Class和Agent-Class指定的類(lèi):

public class AgentPluginPreWrapper {
public static void premain(String agentArgs, Instrumentation inst) {
AgentPluginContextFactory.makeAgentPreExecuteContext().execute(agentArgs, inst);
}


}




public class AgentPluginMainWrapper {
public static void agentmain(String agentArgs, Instrumentation inst) {
AgentPluginContextFactory.makeAgentMainExecuteContext().execute(agentArgs, inst);
}
}

配置文件中指定相應(yīng)的類(lèi):

Manifest-Version: 1.0
Premain-Class: org.tiny.upgrade.core.AgentPluginPreWrapper
Agent-Class: org.tiny.upgrade.core.AgentPluginMainWrapper
Permissions: all-permissions
Can-Retransform-Classes: true
Can-Redefine-Classes: true

框架搭好后,來(lái)編寫(xiě)插件部分,插件的話(huà),需要繼承自org.tiny.upgrade.sdk.IPluginService并實(shí)現(xiàn):

@AgentMainCondition
@Slf4j
public class CodePadPluginServiceProvider implements IPluginService {


@Override
public String getPluginName() {
return "增強(qiáng)插件";
}


@Override
public void pluginLoad(String agentArgs, Instrumentation inst) {
//獲取已加載的所有類(lèi)
Class<?>[] classes = inst.getAllLoadedClasses();
if (classes == null || classes.length == 0) {
return;
}
//需要將業(yè)務(wù)類(lèi)進(jìn)行retransform一下,這樣可以避免在transform執(zhí)行的時(shí)候,找不到此類(lèi)的情況
for (Class<?> clazz : classes) {
if (clazz.getName().contains(entity.getClassName())) {
try {
inst.retransformClasses(clazz);
} catch (UnmodifiableClassException e) {
log.error("retransform class fail:" + clazz.getName(), e);
}
}
}
//進(jìn)行增強(qiáng)操作
inst.addTransformer(new ByteCodeBizInvoker(), true);
}


@Override
public void pluginUnload() {


}
}

這里需要注意的是,在插件load的時(shí)候,我們做了class retransform操作,這樣操作的原因是因?yàn)?,在程序啟?dòng)的時(shí)候,有時(shí)候比如一些類(lèi),會(huì)在JavaAgent之前啟動(dòng),這樣會(huì)造成有些類(lèi)在進(jìn)行增強(qiáng)的時(shí)候,無(wú)法處理,所以這里需要遍歷并操作下,避免意外情況。

下面是具體的增強(qiáng)操作:

@Slf4j
public class ByteCodeBizInvoker implements ClassFileTransformer {
/**
* 在此處加載tprd-ut并利用類(lèi)加載器加載
*
* @param loader
* @param className
* @param classBeingRedefined
* @param protectionDomain
* @param classfileBuffer
* @return
* @throws IllegalClassFormatException
*/
@Override
public byte[] transform(ClassLoader loader
, String className
, Class<?> classBeingRedefined
, ProtectionDomain protectionDomain
, byte[] classfileBuffer) throws IllegalClassFormatException {
//java自帶的方法不進(jìn)行處理
if (loader == null) {
return null;
}
//增強(qiáng)spring5的componetscan,將org.tiny路徑塞入
if (className.contains("ComponentScanBeanDefinitionParser")) {
try {
System.out.println("增強(qiáng)spring");
ClassPool classPool = new ClassPool(true);
classPool.appendClassPath(ByteCodeBizInvoker.class.getName());


CtClass ctClass = classPool.get(className.replace("/", "."));
ClassFile classFile = ctClass.getClassFile();
MethodInfo methodInfo = classFile.getMethod("parse");
CtMethod ctMethod = ctClass.getDeclaredMethod("parse");
addComponentScanPackage(methodInfo, ctMethod);
return ctClass.toBytecode();
} catch (Exception e) {
log.error("handle spring 5 ComponentScanBeanDefinitionParser error", e);
}
}
}
/**
* 遍歷method,直至找到ReportTracer標(biāo)記類(lèi)
*
* @param ctMethod
*/
private void addComponentScanPackage(MethodInfo methodInfo, CtMethod ctMethod) throws CannotCompileException {
final boolean[] success = {false};
CodeAttribute ca = methodInfo.getCodeAttribute();
CodeIterator codeIterator = ca.iterator();
//行遍歷方法體
while (codeIterator.hasNext()) {
ExprEditor exprEditor = new ExprEditor() {
public void edit(MethodCall m) throws CannotCompileException {
String methodCallName = m.getMethodName();
if (methodCallName.equals("getAttribute")) {
//將org.tiny追加進(jìn)去
m.replace("{ $_ = $proceed($$); $_ = $_ + \",org.tiny.upgrade\"; }");
success[0] = true;
}
}
};
ctMethod.instrument(exprEditor);
if (success[0]) {
break;
}
}
}
}

從上面可以看出,我們是修改了spring中的ComponentScanBeanDefinitionParser類(lèi),并將里面的parser方法中將org.tiny.upgrade包掃描路徑自動(dòng)注冊(cè)進(jìn)去,這樣當(dāng)別人集成我們的框架的時(shí)候,就無(wú)須掃描到框架也能執(zhí)行了。

寫(xiě)到這里,相信大家對(duì)整體框架有個(gè)大概的認(rèn)識(shí)了。但是這個(gè)框架有個(gè)缺陷,就是我的插件jar寫(xiě)完后,一定要放到項(xiàng)目的maven dependency中,然后打包部署才行。實(shí)際上有時(shí)候,我項(xiàng)目上線(xiàn)后,根本就沒(méi)有機(jī)會(huì)重新打包部署,那么接下來(lái),我們就通過(guò)自定義Classloader來(lái)讓我們的插件不僅僅可以本地集成,而且可以從網(wǎng)絡(luò)中集成。

首先,我們需要定義自定義類(lèi)加載器:

public class TinyPluginClassLoader extends URLClassLoader {
/**
* 帶參構(gòu)造
* @param urls
*/
public TinyPluginClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
/**
* 添加URL路徑
* @param url
*/
public void addURL(URL url) {
super.addURL(url);
}
}

這個(gè)類(lèi)加載器,是不是很眼熟,和前面講的類(lèi)似,但是帶了個(gè)parent classloader的標(biāo)記,這是為什么呢?這個(gè)標(biāo)記的意思是,當(dāng)前自定義的TinyPluginClassLoader的父classloader是誰(shuí),這樣的話(huà),這個(gè)自定義類(lèi)加載器就可以繼承父類(lèi)加載器中的信息了,避免出現(xiàn)問(wèn)題,這個(gè)細(xì)節(jié)大家注意。

這里需要說(shuō)明的是,從本地jar文件加載還是從網(wǎng)絡(luò)jar文件加載,本質(zhì)上是一樣的,因?yàn)門(mén)inyPluginClassLoader是按照URL來(lái)的。

針對(duì)于本地jar文件,我們構(gòu)造如下URL即可:

URL url = new URL("jar:file:/D:/project/tiny-plugin-hello/target/tiny-plugin-hello-1.0-SNAPSHOT.jar!/")

針對(duì)于網(wǎng)絡(luò)jar文件,我們構(gòu)造如下URL即可:

URL url = new URL("jar:http://111.111.111.111/tiny-plugin-hello-1.0-SNAPSHOT.jar!/")

這樣,我們只需要定義好自定義類(lèi)加載器加載邏輯即可:

/**
* 從jar文件中提取出對(duì)應(yīng)的插件類(lèi)
*
* @param pluginClass
* @param jarFile
* @return
*/
public static Set<Class> loadPluginFromJarFile(Class pluginClass, JarFile jarFile, TinyPluginClassLoader tinyPluginClassLoader) {
Set<Class> pluginClasses = new HashSet<Class>();
Enumeration<JarEntry> jars = jarFile.entries();
while (jars.hasMoreElements()) {
JarEntry jarEntry = jars.nextElement();
String jarEntryName = jarEntry.getName();
if (jarEntryName.charAt(0) == '/') {
jarEntryName = jarEntryName.substring(1);
}
if (jarEntry.isDirectory() || !jarEntryName.endsWith(".class")) {
continue;
}
String className = jarEntryName.substring(0, jarEntryName.length() - 6);
try {
Class clazz = tinyPluginClassLoader.loadClass(className.replace("/", "."));
if (clazz != null && !clazz.isInterface() && pluginClass.isAssignableFrom(clazz)) {
pluginClasses.add(clazz);
}
} catch (ClassNotFoundException e) {
log.error("PluginUtil.loadPluginFromJarFile fail",e);
}
}
return pluginClasses;
}

之后,我們就可以用如下代碼對(duì)一個(gè)具體的jar路徑進(jìn)行加載就行了:

/**
* 加載插件
*
* @return
*/
@Override
public Set<Class> loadPlugins(URL jarURL) {
try {
JarFile jarFile = ((JarURLConnection) jarURL.openConnection()).getJarFile();
getTinyPluginClassLoader().addURL(jarURL);
return PluginUtil.loadPluginFromJarFile(IPluginService.class, jarFile, getTinyPluginClassLoader());
} catch (IOException e) {
log.error("LoadPluginViaJarStrategy.loadPlugins fail", e);
return null;
}
}

最終,我們只需要利用SPI進(jìn)行動(dòng)態(tài)加載:

/**
* 執(zhí)行插件
*/
public void processPlugins(URL... urls) {
if (urls == null || urls.length == 0) {
log.error("jar url path empty");
return;
}
for (URL url : urls) {
pluginLoadFactory.loadJarPlugins(url);
}
ServiceLoader<IPluginService> serviceLoader = ServiceLoader.load(IPluginService.class, pluginLoadFactory.getPluginLoader());
for (IPluginService pluginService : serviceLoader) {
pluginService.Process();
}
}

這樣,我們不僅實(shí)現(xiàn)了插件化,而且我們的插件還支持從本地jar文件或者網(wǎng)絡(luò)jar文件加載。由于我們利用了agentmain對(duì)代碼進(jìn)行增強(qiáng),所以當(dāng)系統(tǒng)檢測(cè)到我這個(gè)jar的時(shí)候,下一次執(zhí)行會(huì)重新對(duì)代碼進(jìn)行增強(qiáng)并生效。

總結(jié)

到這里,我們的用餐進(jìn)入到尾聲了。也不知道這餐,您享用的是否高興?

其實(shí)本文的技術(shù),從雙親委派模型到自定義類(lèi)加載器,再到基于自定義類(lèi)加載器實(shí)現(xiàn)的類(lèi)交換,基于Java SPI實(shí)現(xiàn)的類(lèi)交換,最后到基于Java SPI+ Java Agent + Javassist實(shí)現(xiàn)的插件框架及框架支持遠(yuǎn)程插件化,來(lái)一步一步的向讀者展示所涉及的知識(shí)點(diǎn)。當(dāng)然,由于筆者知識(shí)有限,疏漏之處,還望海涵,真誠(chéng)期待我的拋磚,能夠引出您的玉石之言。

責(zé)任編輯:未麗燕 來(lái)源: 京東零售技術(shù)
相關(guān)推薦

2022-09-15 18:32:13

SPI模型框架

2024-01-31 22:08:18

分布式重試框架

2012-06-25 12:43:26

.NET框架

2017-04-12 23:33:38

DevOps平衡計(jì)分卡框架

2012-01-18 10:20:42

框架設(shè)計(jì)

2020-07-30 10:35:32

Java反射框架設(shè)計(jì)

2014-09-23 10:05:55

2023-10-26 09:02:30

框架設(shè)計(jì)模式

2012-06-25 09:28:42

.NET可逆框架

2022-12-16 12:16:21

2016-03-23 11:05:58

Socket開(kāi)發(fā)框架分析

2009-09-08 09:12:12

LINQ構(gòu)建框架設(shè)計(jì)

2011-04-22 09:26:57

MVC設(shè)計(jì)

2021-02-23 08:18:04

Java 反射機(jī)制

2010-09-25 13:09:39

UISymbian

2012-01-10 10:04:43

Node.js

2022-04-03 15:44:55

Vue.js框架設(shè)計(jì)設(shè)計(jì)與實(shí)現(xiàn)

2022-10-10 09:11:12

互聯(lián)網(wǎng)存儲(chǔ)系統(tǒng)云計(jì)算

2013-09-09 10:48:24

iOS無(wú)線(xiàn)客戶(hù)端框架設(shè)計(jì)

2013-09-03 09:55:42

iOS無(wú)線(xiàn)客戶(hù)端框架設(shè)計(jì)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

美女精品视频| 国产强被迫伦姧在线观看无码| 中文字幕一区二区三区四区久久 | 91在线视频精品| 欧美日韩在线视频免费| 精品成人自拍视频| 欧美日韩在线三级| 国产手机免费视频| 欧美日本网站| 国产精品66部| 欧美一区二区视频97| 强制高潮抽搐sm调教高h| av动漫精品一区二区| 一本大道久久a久久精二百| 中文字幕在线亚洲精品 | 亚洲欧美精品一区二区| 亚洲精品永久视频| jk漫画禁漫成人入口| 亚洲丝袜精品丝袜在线| 青青草原成人| 色婷婷av一区二区三| 九九国产精品视频| 欧美在线视频免费播放| 真实国产乱子伦对白在线| 蜜桃一区二区三区| 精品国产乱码久久久久久老虎| 激情综合网俺也去| 九色porny视频在线观看| 国产精品美女久久久久久久久| 国产精品久久精品视| 国产又大又粗又硬| 久久成人亚洲| 性欧美亚洲xxxx乳在线观看| 国产一二三区精品| 国产精品三级| 日韩毛片在线看| 免费观看污网站| 国产精品亚洲欧美日韩一区在线 | 加勒比久久综合| 亚洲国产精品人久久电影| 91小视频网站| 日本另类视频| 在线看不卡av| 欧美激情成人网| 忘忧草在线日韩www影院| 亚洲精品福利视频网站| 亚洲一区二区精品在线| 北岛玲一区二区三区| 久久综合网色—综合色88| 国产伦精品一区| wwwav网站| 国产成人高清视频| 91成人理论电影| 国产婷婷一区二区三区久久| 韩国三级电影一区二区| 国产在线观看一区二区三区 | 国产午夜手机精彩视频| 99精品视频在线观看播放| 在线视频日韩精品| 婷婷丁香综合网| jizzjizz欧美69巨大| 亚洲网站在线播放| x88av在线| 日韩在线观看一区| 久久久成人的性感天堂| 欧美日韩色视频| 综合视频在线| 久久久久久久久久久免费 | 欧美日韩精品一区视频| 亚洲一区在线不卡| 视频欧美精品| 日韩欧美在线123| 扒开伸进免费视频| 日韩成人午夜| 一区二区亚洲精品国产| 亚洲欧洲综合网| 欧美精品啪啪| 97视频在线播放| 波多野结衣视频在线观看| 欧美aⅴ一区二区三区视频| 成人免费网站在线| 狠狠躁日日躁夜夜躁av| 久久免费偷拍视频| 日本在线观看不卡| 国产美女在线观看| 亚洲h精品动漫在线观看| 国产精品免费入口| 69堂精品视频在线播放| 日韩一级黄色大片| 在线天堂www在线国语对白| 青青视频一区二区| 中文字幕国产精品久久| 午夜免费激情视频| 久久xxxx| 91九色偷拍| 噜噜噜噜噜在线视频| 最新欧美精品一区二区三区| 欧美精品久久久久久久自慰| 写真福利精品福利在线观看| 4hu四虎永久在线影院成人| 中国极品少妇videossexhd| 成人高清电影网站| 欧美精品日韩www.p站| 日韩免费观看一区二区| 美女看a上一区| 不卡一区二区三区视频| 国产youjizz在线| 亚洲一区电影777| 性生活免费在线观看| 红杏视频成人| 日日狠狠久久偷偷四色综合免费| 日本一区二区欧美| 裸体在线国模精品偷拍| 久久国产欧美精品| 在线视频观看国产| 在线观看欧美精品| 在线中文字日产幕| 天天做天天爱天天综合网2021| 97国产一区二区精品久久呦| 国产精品热久久| 国产午夜亚洲精品理论片色戒 | av中文在线资源库| 在线不卡免费av| 亚洲av无码一区二区三区人| 亚洲视频综合| 91中文在线视频| 日本高清视频在线观看| 欧美性高跟鞋xxxxhd| 欧美国产在线一区| 99成人在线视频| 国产精品999| 全色精品综合影院| 午夜不卡在线视频| 韩国三级与黑人| 婷婷久久综合| 国产日韩精品入口| 北条麻妃在线| 欧美日韩专区在线| 日本一级免费视频| 久久久久久自在自线| 久久99精品久久久久久秒播放器 | 成人激情视频在线| 97超碰国产一区二区三区| 色哟哟一区二区在线观看 | 国产精品麻豆99久久久久久| 国产美女三级视频| 亚洲系列另类av| 欧美自拍视频在线| 青青国产在线| 一本大道久久a久久综合婷婷| 日本xxx在线播放| 国产精品美女久久久浪潮软件| 国产欧美日韩一区| 3344国产永久在线观看视频| 欧美精品一区二区三区蜜臀| 国产一级特黄a高潮片| 成人午夜视频福利| 免费看国产一级片| 日韩成人一级| 国产精品1区2区在线观看| h网站在线免费观看| 欧美日韩国产精品成人| 久久久久久久久久97| 国产美女精品一区二区三区| 国产成人三级视频| 亚洲国产欧美在线观看| 97精品国产97久久久久久春色| 四虎在线免费看| 天天综合天天综合色| 少妇按摩一区二区三区| 蜜臀av性久久久久蜜臀aⅴ| 在线视频91| 亚洲成人黄色| 国产91精品久久久久| 蜜桃视频在线观看网站| 欧美久久久久免费| 久久久久久久久久久97| 99久久er热在这里只有精品15 | 日本一区二区欧美| 国产亚洲精久久久久久| 最新av免费在线观看| 午夜影院在线视频| 国产91精品免费| 色综合久久久久无码专区| 精品盗摄女厕tp美女嘘嘘| 成人免费淫片视频软件| 丁香花在线电影| 国产亚洲精品91在线| 99久久久久久久| 欧美日韩美女在线观看| 中文字幕免费在线看线人动作大片| 精品一二三四区| 免费拍拍拍网站| 日韩伦理视频| 国产乱码精品一区二区三区中文 | 国语对白在线播放| 91啪九色porn原创视频在线观看| 亚洲第一中文av| 国内精品福利| 亚洲精品第一区二区三区| 成人福利免费在线观看| 欧美成人艳星乳罩| 国产a级一级片| 精品影片在线观看的网站| 91夜夜揉人人捏人人添红杏| 美女91在线看| 久久成年人免费电影| 色播色播色播色播色播在线| 欧美一区二区在线免费观看| 日韩手机在线视频| 一区二区三区四区不卡视频| 妺妺窝人体色WWW精品| 成人一区二区三区在线观看 | 中文字幕系列一区| 久久男人资源视频| 国产区在线观看| 中文字幕日韩av| 天堂中文在线8| 日韩欧美成人激情| 国产精品久久久久久久久毛片| 亚洲高清在线精品| 欧美日韩人妻精品一区二区三区 | 手机看片福利永久国产日韩| 久久香蕉网站| 成人情视频高清免费观看电影| 青青草国产一区二区三区| 日本中文字幕久久看| 国产精品电影| 久久久久久久999| 最爽无遮挡行房视频在线| zzijzzij亚洲日本成熟少妇| 黄色国产在线| 亚洲精品有码在线| 亚洲 欧美 精品| 亚洲国产中文字幕在线观看| 国产高清免费av| 欧美一级高清大全免费观看| 91精品国产乱码久久久| 欧美午夜理伦三级在线观看| 波多野结衣网站| 一本久久精品一区二区| 婷婷激情五月网| 欧美视频中文在线看| 亚洲一区欧美在线| 亚洲一区二区在线免费看| 欧美日韩国产精品一区二区三区| 亚洲视频香蕉人妖| 欧美丰满熟妇bbbbbb| 一区二区三区在线影院| 亚洲熟女毛茸茸| 中文字幕亚洲精品在线观看 | 北条麻妃在线| 色777狠狠综合秋免鲁丝| 亚洲1卡2卡3卡4卡乱码精品| 色爱av美腿丝袜综合粉嫩av| 午夜视频成人| 久久国产色av| 麻豆蜜桃在线| 欧美亚州一区二区三区| 欧美成人影院| 国产精品一区专区欧美日韩| 亚洲狼人在线| 99国精产品一二二线| 成人午夜三级| 欧美一区1区三区3区公司| 欧美欧美黄在线二区| 亚洲不卡1区| 亚洲九九在线| 成人免费a级片| 国产精品嫩草99av在线| 在线免费观看视频黄| 狠狠色丁香久久婷婷综合丁香| 亚洲精品久久久久久| www.日韩av| 最近中文字幕在线mv视频在线 | 在线视频一区观看| 亚洲乱码电影| 久久亚洲中文字幕无码| 天堂在线亚洲视频| 亚洲天堂网站在线| 99视频精品全部免费在线| 欧美黄色激情视频| 亚洲欧美激情视频在线观看一区二区三区 | 天堂在线精品| 亚洲国产精品久久久久婷婷老年| 国产精品久久久久久久| 国产九九九九九| 久久激情五月激情| 中文字幕精品视频在线| 国产欧美精品一区二区色综合朱莉| 97在线观看免费高| 欧美日韩国产综合新一区 | 国产一区二区在线看| 国产二级一片内射视频播放| 中文字幕av不卡| 精品无码人妻一区二区三区品| 色综合夜色一区| 国产又粗又猛又爽又黄视频| 亚洲精品日韩久久久| 宅男网站在线免费观看| 国产精品www色诱视频| 视频二区欧美| 亚洲综合第一| 午夜在线一区二区| 国产麻豆剧传媒精品国产| 国产无人区一区二区三区| 久久国产露脸精品国产| 欧美色视频在线| 香蕉久久国产av一区二区| 久久综合久久88| 欧美xxx性| 久久精品人成| 女人色偷偷aa久久天堂| 在线免费观看av的网站| aaa亚洲精品| 欧美激情国产精品免费| 欧美日韩综合色| 人操人视频在线观看| 欧美激情视频三区| 亚洲色图综合| 亚洲 国产 日韩 综合一区| 亚洲最黄网站| 亚洲美女高潮久久久| 中文字幕日本乱码精品影院| 99成人精品视频| 亚洲精品久久久久中文字幕二区| 黄在线免费观看| 国产欧美一区二区三区四区| 九九久久精品| 欧美性久久久久| 成人av动漫在线| 久草网在线观看| 日韩精品一区二区三区视频播放 | 色爱综合网欧美| 欧美精品成人网| www国产精品av| 日韩av一二三区| 亚洲精品97久久| 国产精品—色呦呦| 俄罗斯精品一区二区三区| 中文字幕日韩欧美精品高清在线| 亚洲欧美日本一区二区三区| 国产视频一区在线播放| 欧美人一级淫片a免费播放| 亚洲精品天天看| 日韩久久一区二区三区| 日本免费高清一区二区| 日韩精品视频网| 亚洲av熟女国产一区二区性色 | www.色婷婷.com| 久久777国产线看观看精品| 日韩欧美久久| 隔壁人妻偷人bd中字| 成人精品gif动图一区| 日本三级理论片| 日韩电影免费在线观看中文字幕| 亚洲小少妇裸体bbw| 日本欧美色综合网站免费| 免费观看在线综合| 你懂得在线观看| 日韩一区二区影院| 丁香高清在线观看完整电影视频| 国产精品视频入口| 久久国产欧美| 国产探花视频在线播放| 欧美精品一二三四| 午夜影院免费在线| 精品国产乱码久久久久久郑州公司| 午夜一区不卡| 久久久久麻豆v国产| 91精品国产综合久久久久久漫画| 手机av在线播放| 精品综合久久久| 免费在线观看视频一区| 777777国产7777777| 亚洲第一视频网站| 国产v综合v| 麻豆映画在线观看| 91亚洲精品一区二区乱码| 国产亚洲久一区二区| 久久91超碰青草是什么| 亚洲最好看的视频| 一起操在线视频| 亚洲成人中文在线| 国产69久久| 99一区二区| 日韩中文字幕不卡| 劲爆欧美第一页| 国产一级揄自揄精品视频| 欧美2区3区4区| 虎白女粉嫩尤物福利视频| 亚洲男同性恋视频| 日本高清中文字幕二区在线| 91久久久久久久久久久久久| 日韩视频在线一区二区三区 | 在线看一级片| 欧美xxxx黑人又粗又长密月| 国产在线看一区| 久久精品无码av| 久久久女人电视剧免费播放下载|