前提
筆者很久之前就有個想法:參考現有的主流ORM
框架的設計,造一個ORM
輪子,在基本不改變使用體驗的前提下把框架依賴的大量的反射設計去掉,這些反射API
構築的元件使用動態編譯載入的例項去替代,從而可以得到接近於直接使用原生JDBC
的效能。於是帶著這樣的想法,深入學習Java
的動態編譯。編寫本文的時候使用的是JDK11
。
基本原理
下面這個很眼熟的圖來源於《深入理解Java虛擬機器》前端編譯與優化的章節,主要描述編譯的過程:
上圖看起來只有三步,其實每一步都有大量的步驟,下圖嘗試相對詳細地描述具體的步驟(圖比較大難以分割,直接放原圖):
實際上,僅僅對於編譯這個過程來說,開發者或者使用者不必要完全掌握其中的細節,JDK
提供了一個工具包javax.tools
讓使用者可以用簡易的API
進行編譯(其實在大多數請下,開發者是面向業務功能開發,像編譯和打包這些細節一般直接由開發工具、Maven
、Gradle
等工具完成):
具體的使用過程包括:
- 獲取一個
javax.tools.JavaCompiler
例項。 - 基於
Java
檔案物件初始化一個編譯任務javax.tools.JavaCompiler$CompilationTask
例項。 CompilationTask
例項執行結果代表著編譯過程的成功與否。
我們熟知的
javac
編譯器其實就是JavaCompiler
介面的實現,在JDK11
中,對應的實現類為com.sun.tools.javac.api.JavacTool
。在JDK8
中不存在JavaCompiler
介面,具體的編譯入口類為com.sun.tools.javac.main.JavaCompiler
。
因為JVM
裡面的Class
是基於ClassLoader
隔離的,所以編譯成功之後可以通過自定義的類載入器載入對應的類例項,然後就可以應用反射API
進行例項化和後續的呼叫。
JDK動態編譯
JDK
動態編譯的步驟在上一節已經清楚地說明,這裡造一個簡單的場景。假設存在一個介面如下:
package club.throwable.compile;
public interface HelloService {
void sayHello(String name);
}
// 預設實現
package club.throwable.compile;
public class DefaultHelloService implements HelloService {
@Override
public void sayHello(String name) {
System.out.println(String.format("%s say hello [by default]", name));
}
}
我們可以通過字串SOURCE_CODE
定義一個類:
static String SOURCE_CODE = "package club.throwable.compile;\n" +
"\n" +
"public class JdkDynamicCompileHelloService implements HelloService{\n" +
"\n" +
" @Override\n" +
" public void sayHello(String name) {\n" +
" System.out.println(String.format(\"%s say hello [by jdk dynamic compile]\", name));\n" +
" }\n" +
"}";
// 這裡不需要定義類檔案,還原類檔案內容如下
package club.throwable.compile;
public class JdkDynamicCompileHelloService implements HelloService{
@Override
public void sayHello(String name) {
System.out.println(String.format("%s say hello [by jdk dynamic compile]", name));
}
}
在組裝編譯任務例項之前,還有幾項工作需要完成:
- 內建的
JavaFileObject
標準實現SimpleJavaFileObject
是面向類原始碼檔案,由於動態編譯時候輸入的是類原始碼檔案的內容字串,需要自行實現JavaFileObject
。 - 內建的
JavaFileManager
是面向類路徑下的Java
原始碼檔案進行載入,這裡也需要自行實現JavaFileManager
。 - 需要自定義一個
ClassLoader
例項去載入編譯出來的動態類。
實現JavaFileObject
自行實現一個JavaFileObject
,其實可以簡單點直接繼承SimpleJavaFileObject
,覆蓋需要用到的方法即可:
public class CharSequenceJavaFileObject extends SimpleJavaFileObject {
public static final String CLASS_EXTENSION = ".class";
public static final String JAVA_EXTENSION = ".java";
private static URI fromClassName(String className) {
try {
return new URI(className);
} catch (URISyntaxException e) {
throw new IllegalArgumentException(className, e);
}
}
private ByteArrayOutputStream byteCode;
private final CharSequence sourceCode;
public CharSequenceJavaFileObject(String className, CharSequence sourceCode) {
super(fromClassName(className + JAVA_EXTENSION), Kind.SOURCE);
this.sourceCode = sourceCode;
}
public CharSequenceJavaFileObject(String fullClassName, Kind kind) {
super(fromClassName(fullClassName), kind);
this.sourceCode = null;
}
public CharSequenceJavaFileObject(URI uri, Kind kind) {
super(uri, kind);
this.sourceCode = null;
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
return sourceCode;
}
@Override
public InputStream openInputStream() {
return new ByteArrayInputStream(getByteCode());
}
// 注意這個方法是編譯結果回撥的OutputStream,回撥成功後就能通過下面的getByteCode()方法獲取目標類編譯後的位元組碼位元組陣列
@Override
public OutputStream openOutputStream() {
return byteCode = new ByteArrayOutputStream();
}
public byte[] getByteCode() {
return byteCode.toByteArray();
}
}
如果編譯成功之後,直接通過自行新增的CharSequenceJavaFileObject#getByteCode()
方法即可獲取目標類編譯後的位元組碼對應的位元組陣列(二進位制內容)。這裡的CharSequenceJavaFileObject
預留了多個建構函式用於相容原有的編譯方式。
實現ClassLoader
只要簡單繼承ClassLoader
即可,關鍵是要覆蓋原來的ClassLoader#findClass()
方法,用於搜尋自定義的JavaFileObject
例項,從而提取對應的位元組碼位元組陣列進行裝載,為了實現這一點可以新增一個雜湊表作為快取,鍵-值分別是全類名的別名(xx.yy.MyClass
形式,而非URI
模式)和目標類對應的JavaFileObject
例項。
public class JdkDynamicCompileClassLoader extends ClassLoader {
public static final String CLASS_EXTENSION = ".class";
private final Map<String, JavaFileObject> javaFileObjectMap = Maps.newConcurrentMap();
public JdkDynamicCompileClassLoader(ClassLoader parentClassLoader) {
super(parentClassLoader);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
JavaFileObject javaFileObject = javaFileObjectMap.get(name);
if (null != javaFileObject) {
CharSequenceJavaFileObject charSequenceJavaFileObject = (CharSequenceJavaFileObject) javaFileObject;
byte[] byteCode = charSequenceJavaFileObject.getByteCode();
return defineClass(name, byteCode, 0, byteCode.length);
}
return super.findClass(name);
}
@Nullable
@Override
public InputStream getResourceAsStream(String name) {
if (name.endsWith(CLASS_EXTENSION)) {
String qualifiedClassName = name.substring(0, name.length() - CLASS_EXTENSION.length()).replace('/', '.');
CharSequenceJavaFileObject javaFileObject = (CharSequenceJavaFileObject) javaFileObjectMap.get(qualifiedClassName);
if (null != javaFileObject && null != javaFileObject.getByteCode()) {
return new ByteArrayInputStream(javaFileObject.getByteCode());
}
}
return super.getResourceAsStream(name);
}
/**
* 暫時存放編譯的原始檔物件,key為全類名的別名(非URI模式),如club.throwable.compile.HelloService
*/
void addJavaFileObject(String qualifiedClassName, JavaFileObject javaFileObject) {
javaFileObjectMap.put(qualifiedClassName, javaFileObject);
}
Collection<JavaFileObject> listJavaFileObject() {
return Collections.unmodifiableCollection(javaFileObjectMap.values());
}
}
實現JavaFileManager
JavaFileManager
是Java
檔案的抽象管理器,它用於管理常規的Java
檔案,但是不侷限於檔案,也可以管理其他來源的Java
類檔案資料。下面就通過實現一個自定義的JavaFileManager
用於管理字串型別的原始碼。為了簡單起見,可以直接繼承已經存在的ForwardingJavaFileManager
:
public class JdkDynamicCompileJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> {
private final JdkDynamicCompileClassLoader classLoader;
private final Map<URI, JavaFileObject> javaFileObjectMap = Maps.newConcurrentMap();
public JdkDynamicCompileJavaFileManager(JavaFileManager fileManager, JdkDynamicCompileClassLoader classLoader) {
super(fileManager);
this.classLoader = classLoader;
}
private static URI fromLocation(Location location, String packageName, String relativeName) {
try {
return new URI(location.getName() + '/' + packageName + '/' + relativeName);
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e);
}
}
@Override
public FileObject getFileForInput(Location location, String packageName, String relativeName) throws IOException {
JavaFileObject javaFileObject = javaFileObjectMap.get(fromLocation(location, packageName, relativeName));
if (null != javaFileObject) {
return javaFileObject;
}
return super.getFileForInput(location, packageName, relativeName);
}
/**
* 這裡是編譯器返回的同(源)Java檔案物件,替換為CharSequenceJavaFileObject實現
*/
@Override
public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
JavaFileObject javaFileObject = new CharSequenceJavaFileObject(className, kind);
classLoader.addJavaFileObject(className, javaFileObject);
return javaFileObject;
}
/**
* 這裡覆蓋原來的類載入器
*/
@Override
public ClassLoader getClassLoader(Location location) {
return classLoader;
}
@Override
public String inferBinaryName(Location location, JavaFileObject file) {
if (file instanceof CharSequenceJavaFileObject) {
return file.getName();
}
return super.inferBinaryName(location, file);
}
@Override
public Iterable<JavaFileObject> list(Location location, String packageName, Set<JavaFileObject.Kind> kinds, boolean recurse) throws IOException {
Iterable<JavaFileObject> superResult = super.list(location, packageName, kinds, recurse);
List<JavaFileObject> result = Lists.newArrayList();
// 這裡要區分編譯的Location以及編譯的Kind
if (location == StandardLocation.CLASS_PATH && kinds.contains(JavaFileObject.Kind.CLASS)) {
// .class檔案以及classPath下
for (JavaFileObject file : javaFileObjectMap.values()) {
if (file.getKind() == JavaFileObject.Kind.CLASS && file.getName().startsWith(packageName)) {
result.add(file);
}
}
// 這裡需要額外新增類載入器載入的所有Java檔案物件
result.addAll(classLoader.listJavaFileObject());
} else if (location == StandardLocation.SOURCE_PATH && kinds.contains(JavaFileObject.Kind.SOURCE)) {
// .java檔案以及編譯路徑下
for (JavaFileObject file : javaFileObjectMap.values()) {
if (file.getKind() == JavaFileObject.Kind.SOURCE && file.getName().startsWith(packageName)) {
result.add(file);
}
}
}
for (JavaFileObject javaFileObject : superResult) {
result.add(javaFileObject);
}
return result;
}
/**
* 自定義方法,用於新增和快取待編譯的原始檔物件
*/
public void addJavaFileObject(Location location, String packageName, String relativeName, JavaFileObject javaFileObject) {
javaFileObjectMap.put(fromLocation(location, packageName, relativeName), javaFileObject);
}
}
注意在這個類中引入了自定義類載入器JdkDynamicCompileClassLoader
,目的是為了實現JavaFileObject
例項的共享以及為檔案管理器提供類載入器例項。
動態編譯和執行
前置準備工作完成,我們可以通過JavaCompiler
去編譯這個前面提到的字串,為了位元組碼的相容性更好,編譯的時候可以指定稍低的JDK
版本例如1.6
:
public class Client {
static String SOURCE_CODE = "package club.throwable.compile;\n" +
"\n" +
"public class JdkDynamicCompileHelloService implements HelloService{\n" +
"\n" +
" @Override\n" +
" public void sayHello(String name) {\n" +
" System.out.println(String.format(\"%s say hello [by jdk dynamic compile]\", name));\n" +
" }\n" +
"}";
/**
* 編譯診斷收集器
*/
static DiagnosticCollector<JavaFileObject> DIAGNOSTIC_COLLECTOR = new DiagnosticCollector<>();
public static void main(String[] args) throws Exception {
// 獲取系統編譯器例項
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
// 設定編譯引數 - 指定編譯版本為JDK1.6以提高相容性
List<String> options = new ArrayList<>();
options.add("-source");
options.add("1.6");
options.add("-target");
options.add("1.6");
// 獲取標準的Java檔案管理器例項
StandardJavaFileManager manager = compiler.getStandardFileManager(DIAGNOSTIC_COLLECTOR, null, null);
// 初始化自定義類載入器
JdkDynamicCompileClassLoader classLoader = new JdkDynamicCompileClassLoader(Thread.currentThread().getContextClassLoader());
// 初始化自定義Java檔案管理器例項
JdkDynamicCompileJavaFileManager fileManager = new JdkDynamicCompileJavaFileManager(manager, classLoader);
String packageName = "club.throwable.compile";
String className = "JdkDynamicCompileHelloService";
String qualifiedName = packageName + "." + className;
// 構建Java原始檔例項
CharSequenceJavaFileObject javaFileObject = new CharSequenceJavaFileObject(className, SOURCE_CODE);
// 新增Java原始檔例項到自定義Java檔案管理器例項中
fileManager.addJavaFileObject(
StandardLocation.SOURCE_PATH,
packageName,
className + CharSequenceJavaFileObject.JAVA_EXTENSION,
javaFileObject
);
// 初始化一個編譯任務例項
JavaCompiler.CompilationTask compilationTask = compiler.getTask(
null,
fileManager,
DIAGNOSTIC_COLLECTOR,
options,
null,
Lists.newArrayList(javaFileObject)
);
// 執行編譯任務
Boolean result = compilationTask.call();
System.out.println(String.format("編譯[%s]結果:%s", qualifiedName, result));
Class<?> klass = classLoader.loadClass(qualifiedName);
HelloService instance = (HelloService) klass.getDeclaredConstructor().newInstance();
instance.sayHello("throwable");
}
}
輸出結果如下:
編譯[club.throwable.compile.JdkDynamicCompileHelloService]結果:true
throwable say hello [by jdk dynamic compile]
可見通過了字串的類原始碼,實現了動態編譯、類載入、反射例項化以及最終的方法呼叫。另外,編譯過程的診斷資訊可以通過DiagnosticCollector
例項獲取。為了複用,這裡可以把JDK
動態編譯的過程抽取到一個方法中:
public final class JdkCompiler {
static DiagnosticCollector<JavaFileObject> DIAGNOSTIC_COLLECTOR = new DiagnosticCollector<>();
@SuppressWarnings("unchecked")
public static <T> T compile(String packageName,
String className,
String sourceCode,
Class<?>[] constructorParamTypes,
Object[] constructorParams) throws Exception {
// 獲取系統編譯器例項
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
// 設定編譯引數
List<String> options = new ArrayList<>();
options.add("-source");
options.add("1.6");
options.add("-target");
options.add("1.6");
// 獲取標準的Java檔案管理器例項
StandardJavaFileManager manager = compiler.getStandardFileManager(DIAGNOSTIC_COLLECTOR, null, null);
// 初始化自定義類載入器
JdkDynamicCompileClassLoader classLoader = new JdkDynamicCompileClassLoader(Thread.currentThread().getContextClassLoader());
// 初始化自定義Java檔案管理器例項
JdkDynamicCompileJavaFileManager fileManager = new JdkDynamicCompileJavaFileManager(manager, classLoader);
String qualifiedName = packageName + "." + className;
// 構建Java原始檔例項
CharSequenceJavaFileObject javaFileObject = new CharSequenceJavaFileObject(className, sourceCode);
// 新增Java原始檔例項到自定義Java檔案管理器例項中
fileManager.addJavaFileObject(
StandardLocation.SOURCE_PATH,
packageName,
className + CharSequenceJavaFileObject.JAVA_EXTENSION,
javaFileObject
);
// 初始化一個編譯任務例項
JavaCompiler.CompilationTask compilationTask = compiler.getTask(
null,
fileManager,
DIAGNOSTIC_COLLECTOR,
options,
null,
Lists.newArrayList(javaFileObject)
);
Boolean result = compilationTask.call();
System.out.println(String.format("編譯[%s]結果:%s", qualifiedName, result));
Class<?> klass = classLoader.loadClass(qualifiedName);
return (T) klass.getDeclaredConstructor(constructorParamTypes).newInstance(constructorParams);
}
}
Javassist動態編譯
既然有JDK
的動態編譯,為什麼還存在Javassist
這樣的位元組碼增強工具?撇開效能或者效率層面,JDK
動態編譯存在比較大的侷限性,比較明顯的一點就是無法完成位元組碼插樁,換言之就是無法基於原有的類和方法進行修飾或者增強,但是Javassist
可以做到。再者,Javassist
提供的API
和JDK
反射的API
十分相近,如果反射平時用得比較熟練,Javassist
的上手也就變得比較簡單。這裡僅僅列舉一個增強前面提到的DefaultHelloService
的例子,先引入依賴:
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.27.0-GA</version>
</dependency>
編碼如下:
public class JavassistClient {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("club.throwable.compile.DefaultHelloService");
CtMethod ctMethod = cc.getDeclaredMethod("sayHello", new CtClass[]{pool.get("java.lang.String")});
ctMethod.insertBefore("System.out.println(\"insert before by Javassist\");");
ctMethod.insertAfter("System.out.println(\"insert after by Javassist\");");
Class<?> klass = cc.toClass();
System.out.println(klass.getName());
HelloService helloService = (HelloService) klass.getDeclaredConstructor().newInstance();
helloService.sayHello("throwable");
}
}
輸出結果如下:
club.throwable.compile.DefaultHelloService
insert before by Javassist
throwable say hello [by default]
insert after by Javassist
Javaassist
這個單詞其實是Java
和Assist
兩個單詞拼接在一起,意為Java
助手,是一個Java
位元組碼增強類庫:
- 可以基於已經存在的類進行位元組碼增強,例如修改已經存在的方法、變數,甚至是直接在原有的類中新增新的方法等。
- 可以完全像積木拼接一樣,動態拼出一個全新的類。
不像ASM
(ASM
的學習曲線比較陡峭,屬於相對底層的位元組碼操作類庫,當然從效能上來看ASM
對位元組碼增強的效率遠高於其他高層次封裝的框架)那樣需要對位元組碼程式設計十分了解,Javaassist
降低了位元組碼增強功能的入門難度。
進階例子
現在定義一個介面MysqlInfoMapper
,用於動態執行一條已知的SQL
,很簡單,就是查詢MySQL
的系統表mysql
裡面的使用者資訊SELECT Host,User FROM mysql.user
:
@Data
public class MysqlUser {
private String host;
private String user;
}
public interface MysqlInfoMapper {
List<MysqlUser> selectAllMysqlUsers();
}
假設現在只提供一個MySQL
的驅動包(mysql:mysql-connector-java:jar:8.0.20
),暫時不能依賴任何高層次的框架,要動態實現MysqlInfoMapper
介面,優先整理需要的元件:
- 需要一個連線管理器去管理
MySQL
的連線。 - 需要一個
SQL
執行器用於執行查詢SQL
。 - 需要一個結果處理器去提取和轉換查詢結果。
為了簡單起見,筆者在定義這三個元件介面的時候順便在介面中通過單例進行實現(部分配置完全寫死):
// 連線管理器
public interface ConnectionManager {
String USER_NAME = "root";
String PASS_WORD = "root";
String URL = "jdbc:mysql://localhost:3306/mysql?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8&useSSL=false";
Connection newConnection() throws SQLException;
void closeConnection(Connection connection);
ConnectionManager X = new ConnectionManager() {
@Override
public Connection newConnection() throws SQLException {
return DriverManager.getConnection(URL, USER_NAME, PASS_WORD);
}
@Override
public void closeConnection(Connection connection) {
try {
connection.close();
} catch (Exception ignore) {
}
}
};
}
// 執行器
public interface SqlExecutor {
ResultSet execute(Connection connection, String sql) throws SQLException;
SqlExecutor X = new SqlExecutor() {
@Override
public ResultSet execute(Connection connection, String sql) throws SQLException {
Statement statement = connection.createStatement();
statement.execute(sql);
return statement.getResultSet();
}
};
}
// 結果處理器
public interface ResultHandler<T> {
T handleResultSet(ResultSet resultSet) throws SQLException;
ResultHandler<List<MysqlUser>> X = new ResultHandler<List<MysqlUser>>() {
@Override
public List<MysqlUser> handleResultSet(ResultSet resultSet) throws SQLException {
try {
List<MysqlUser> result = Lists.newArrayList();
while (resultSet.next()) {
MysqlUser item = new MysqlUser();
item.setHost(resultSet.getString("Host"));
item.setUser(resultSet.getString("User"));
result.add(item);
}
return result;
} finally {
resultSet.close();
}
}
};
}
接著需要動態編譯MysqlInfoMapper
的實現類,它的原始檔的字串內容如下(注意不要在類路徑下新建這個DefaultMysqlInfoMapper
類):
package club.throwable.compile;
import java.sql.Connection;
import java.sql.ResultSet;
import java.util.List;
public class DefaultMysqlInfoMapper implements MysqlInfoMapper {
private final ConnectionManager connectionManager;
private final SqlExecutor sqlExecutor;
private final ResultHandler resultHandler;
private final String sql;
public DefaultMysqlInfoMapper(ConnectionManager connectionManager,
SqlExecutor sqlExecutor,
ResultHandler resultHandler,
String sql) {
this.connectionManager = connectionManager;
this.sqlExecutor = sqlExecutor;
this.resultHandler = resultHandler;
this.sql = sql;
}
@Override
public List<MysqlUser> selectAllMysqlUsers() {
try {
Connection connection = connectionManager.newConnection();
try {
ResultSet resultSet = sqlExecutor.execute(connection, sql);
return (List<MysqlUser>) resultHandler.handleResultSet(resultSet);
} finally {
connectionManager.closeConnection(connection);
}
} catch (Exception e) {
// 暫時忽略異常處理,統一封裝為IllegalStateException
throw new IllegalStateException(e);
}
}
}
然後編寫一個客戶端進行動態編譯和執行:
public class MysqlInfoClient {
static String SOURCE_CODE = "package club.throwable.compile;\n" +
"import java.sql.Connection;\n" +
"import java.sql.ResultSet;\n" +
"import java.util.List;\n" +
"\n" +
"public class DefaultMysqlInfoMapper implements MysqlInfoMapper {\n" +
"\n" +
" private final ConnectionManager connectionManager;\n" +
" private final SqlExecutor sqlExecutor;\n" +
" private final ResultHandler resultHandler;\n" +
" private final String sql;\n" +
"\n" +
" public DefaultMysqlInfoMapper(ConnectionManager connectionManager,\n" +
" SqlExecutor sqlExecutor,\n" +
" ResultHandler resultHandler,\n" +
" String sql) {\n" +
" this.connectionManager = connectionManager;\n" +
" this.sqlExecutor = sqlExecutor;\n" +
" this.resultHandler = resultHandler;\n" +
" this.sql = sql;\n" +
" }\n" +
"\n" +
" @Override\n" +
" public List<MysqlUser> selectAllMysqlUsers() {\n" +
" try {\n" +
" Connection connection = connectionManager.newConnection();\n" +
" try {\n" +
" ResultSet resultSet = sqlExecutor.execute(connection, sql);\n" +
" return (List<MysqlUser>) resultHandler.handleResultSet(resultSet);\n" +
" } finally {\n" +
" connectionManager.closeConnection(connection);\n" +
" }\n" +
" } catch (Exception e) {\n" +
" // 暫時忽略異常處理,統一封裝為IllegalStateException\n" +
" throw new IllegalStateException(e);\n" +
" }\n" +
" }\n" +
"}\n";
static String SQL = "SELECT Host,User FROM mysql.user";
public static void main(String[] args) throws Exception {
MysqlInfoMapper mysqlInfoMapper = JdkCompiler.compile(
"club.throwable.compile",
"DefaultMysqlInfoMapper",
SOURCE_CODE,
new Class[]{ConnectionManager.class, SqlExecutor.class, ResultHandler.class, String.class},
new Object[]{ConnectionManager.X, SqlExecutor.X, ResultHandler.X, SQL});
System.out.println(JSON.toJSONString(mysqlInfoMapper.selectAllMysqlUsers()));
}
}
最終的輸出結果是:
編譯[club.throwable.compile.DefaultMysqlInfoMapper]結果:true
[{"host":"%","user":"canal"},{"host":"%","user":"doge"},{"host":"localhost","user":"mysql.infoschema"},{"host":"localhost","user":"mysql.session"},{"host":"localhost","user":"mysql.sys"},{"host":"localhost","user":"root"}]
然後筆者檢視本地安裝的MySQL
中的結果,驗證該查詢結果是正確的。
這裡筆者為了簡化整個例子,沒有在MysqlInfoMapper#selectAllMysqlUsers()
方法中新增查詢引數,可以嘗試一下查詢的SQL
是SELECT Host,User FROM mysql.user WHERE User = 'xxx'
場景下的編碼實現。
如果把動態實現的
DefaultMysqlInfoMapper
註冊到IOC
容器中,就可以實現MysqlInfoMapper
按照型別自動裝配。
如果把SQL
和引數處理可以抽離到單獨的檔案中,並且實現一個對應的檔案解析器,那麼就可以把類檔案和SQL
隔離,Mybatis
和Hibernate
都是這樣做的。
小結
動態編譯或者更底層的面向位元組碼層面的程式設計,其實是一個十分有挑戰性但是可以創造無限可能的領域,本文只是簡單分析了一下Java
原始碼編譯的過程,並且通過一些簡單的例子進行動態編譯的模擬,離使用於實際應用中還有不少距離,後面需要花更多的時間去分析一下相關領域的知識。
參考資料:
JDK11
部分原始碼- 《深入理解Java虛擬機器 - 3rd》
- Javassist
(本文完 c-4-d e-a-20200606 0:23)