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
