概述:
学习《深入理解java虚拟机》
1、字节码生成技术与动态代理的实现
package com.jack;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class DynamicProxyTest {
interface IHello {
void sayHello();
}
static class Hello implements IHello {
public void sayHello() {
System.out.println("hello world");
}
}
static class DynamicProxy implements InvocationHandler {
Object originalObj;
Object bind(Object originalObj) {
this.originalObj = originalObj;
return Proxy.newProxyInstance(originalObj.getClass().getClassLoader(),
originalObj.getClass().getInterfaces(), this);
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("welcome");
return method.invoke(originalObj, args);
}
}
public static void main(String[] args){
IHello hello = (IHello) new DynamicProxy().bind(new Hello());
hello.sayHello();
}
}
日志:
welcome
hello world
总结:
- 1、定义一个接口IHello,实现接口的实现类。
- 2、创建实现InvocationHandler 接口代理。需要绑定对象。
- 3、你需要告诉代理类加载器,类对应的接口。
- 4、在调用的时候,它会自动拦截调用,先执行invoke() 方法,然后回调sayHello()方法
2、Retranslator:跨越JDK版本
这个工具就是比如更新jdk1.7版本有一些新的特性,由于项目的原因只能使用jdk1.6,如果想要使用jdk1.7的新特性该怎么办,那么就jdk1.6实现jdk1.7的功能。这就是Retrotranslator,但看官网发现没有更新,现在可能不需要这么做了。
3、实战:自己动手实现远程执行功能
排查问题的过程中,查看内存中的一些参数值,却又没有方法把这些值输出到界面或日志中,又或者定位到某个缓存数据有问题,但缺少缓存的统一管理界面,不能被重启服务才能清理这个缓存。类似的需求有一个共同的特点,就是只要在服务中执行一段程序代码,可以定位或排除问题。但就是偏偏找不到可以让服务器执行临时代码的途径。
目标:
1、不依赖JDK版本
2、不改变原有服务端部署,不依赖任何第三方类库
3、临时代码具有灵活性。
4、临时代码的执行结果能够返回客户端。
实施步骤:
1、创建一个加载类的方法。
2、重写java.lang.System方法,将结果返回
3、采用jsp加载一个执行main方法
4、在浏览器显示
工具类
package com.rinlink.intelligent.test;
public class ByteUtils {
public static int bytes2Int(byte[] b, int start, int len) {
int sum = 0;
int end = start + len;
for (int i= start; i<end; i++){
int n = ((int) b[i]) & 0xff;
n <<= (--len)*8;
sum = n + sum;
}
return sum;
}
public static String bytes2String(byte[] b, int start, int len) {
return new String(b, start, len);
}
public static byte[] string2Bytes(String str) {
return str.getBytes();
}
public static byte[] int2Bytes(int value, int len) {
byte[] b = new byte[len];
for (int i=0; i<len; i++){
b[len-i-1] = (byte)((value>>8*i)& 0xff);
}
return b;
}
/**
* 字节数组替换
* @param originalBytes 需要替换原数组
* @param offset 原数组偏移位置
* @param len原数组替换的长度 (替换原数组旧内容offset 到len)
* @param replaceBytes 目标数组(替换新的内容)
* @return
*/
public static byte[] bytesReplace(byte[] originalBytes, int offset, int len, byte[] replaceBytes) {
byte[] newBytes = new byte[originalBytes.length + (replaceBytes.length - len)];
System.arraycopy(originalBytes, 0, newBytes, 0, offset);
System.arraycopy(replaceBytes, 0, newBytes, offset, replaceBytes.length);
System.arraycopy(originalBytes, offset + len, newBytes, offset+replaceBytes.length, originalBytes.length - offset -len);
return newBytes;
}
}
package com.rinlink.intelligent.test;
/**
* 修改Class文件,暂时提供修改常量池常量的功能
* @author Administrator
*
*/
public class ClassModifier {
/**
* Class文件中常量池的起始偏移
*/
private static final int CONSTANT_POOL_COUNT_INDEX = 8;
/**
* CONSTANT_Utf8_info常量的tag标志
*
*/
private static final int CONSTANT_Utf8_info = 1;
/**
* 常量池中11中常量所占的长度,CONSTANT_Utf8_info常量除外,因为它不是定长
*
*/
private static final int[] CONSTANT_ITEM_LENGTH={-1,-1,5,-1,5,9,9,3,3,5,5,5,5};
private static final int u1 = 1;
private static final int u2 = 2;
private byte[] classByte;
public ClassModifier(byte[] classByte) {
this.classByte = classByte;
}
/**
* 修改常量池中CONSTANT_Utf8_info常量内容
* @param oldStr 修改前的字符串
* @param newStr 修改后的字符串
* @return 修改的结果
*/
public byte[] modifyUTF8Constant(String oldStr, String newStr){
int cpc = getConstantPoolCount();
int offset = CONSTANT_POOL_COUNT_INDEX + u2;
for (int i=0; i<cpc; i++){
int tag = ByteUtils.bytes2Int(classByte,offset, u1);
if(tag == CONSTANT_Utf8_info){
int len = ByteUtils.bytes2Int(classByte, offset + u1, u2);
offset += (u1+u2);
String str = ByteUtils.bytes2String(classByte, offset, len);
if(str.equalsIgnoreCase(oldStr)) {
byte[] strBytes = ByteUtils.string2Bytes(newStr);
byte[] strLen = ByteUtils.int2Bytes(newStr.length(), u2);
classByte = ByteUtils.bytesReplace(classByte, offset-u2, u2, strLen);
classByte = ByteUtils.bytesReplace(classByte, offset, len, strLen);
return classByte;
} else {
offset += len;
}
}else {
offset += CONSTANT_ITEM_LENGTH[tag];
}
}
return classByte;
}
/**
* 获取常量池中常量的数量
* @return
*/
private int getConstantPoolCount() {
return ByteUtils.bytes2Int(classByte, CONSTANT_POOL_COUNT_INDEX, u2);
}
}
总结:
这个类主要目的是修改编译class字节码文件java/lang/System 常量变成你定义输入类如下定义HackSystem的类路径,不过是以/分割
package com.rinlink.intelligent.test;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.PrintStream;
/**
* 为JavaClass劫持java.lang.System提供支持
* 除了out和err以外,其余的都直接转发给System处理
* @author Administrator
*
*/
public class HackSystem {
public final static InputStream in = System.in;
private static ByteArrayOutputStream buffer = new ByteArrayOutputStream();
public final static PrintStream out = new PrintStream(buffer);
public final static PrintStream err = out;
public static String getBufferString(){
return buffer.toString();
}
public static void setSecurityManager(final SecurityManager s){
System.setSecurityManager(s);
}
public static SecurityManager getSecurityManager(){
return System.getSecurityManager();
}
public static long currentTimeMillis(){
return System.currentTimeMillis();
}
public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length){
System.arraycopy(src, srcPos, dest, destPos, length);
}
public static int identityHashCode(Object x){
return System.identityHashCode(x);
}
public static void clearBuffer() {
buffer.reset();
}
}
总结:这个主要是劫持默认java/lang/System的流,将它返回给客户端
package com.rinlink.intelligent.test;
/**
* 为了多次载入执行类而加入的加载器
* 把defineClass方法开放出来,只有外部显式调用的时候才会使用到loadByte方法
* 由虚拟机调用时,仍然按照原有的双亲委派规则使用loadClass方法进行类加载
* @author Administrator
*
*/
public class HotSwapClassLoader extends ClassLoader{
public HotSwapClassLoader(){
super(HotSwapClassLoader.class.getClassLoader());
}
public Class loadByte(byte[] classByte){
return defineClass(null, classByte, 0, classByte.length);
}
}
总结:加载编译的类
package com.rinlink.intelligent.test;总结:这是入口
import java.lang.reflect.Method;
/**
* JavaClass执行工具
* @author Administrator
*
*/
public class JavaClassExecutor {
public static String execute(byte[] classByte){
HackSystem.clearBuffer();
ClassModifier cm = new ClassModifier(classByte);
byte[] modiBytes = cm.modifyUTF8Constant("/java/lang/System", "com/rinlink/intelligent/test/HackSystem");
HotSwapClassLoader loader = new HotSwapClassLoader();
Class clazz = loader.loadByte(modiBytes);
try {
Method method = clazz.getMethod("main", new Class[]{String[].class});
method.invoke(null, new String[]{null});
} catch (Throwable e){
e.printStackTrace(HackSystem.out);
}
return HackSystem.getBufferString();
}
}
- 1、清除HackSystem缓存
- 2、创建一个字节码修改文件
- 3、修改字节码文件常量值/java/lang/System 变成com/rinlink/intelligent/test/HackSystem (也就是HackSystem路径)这个目的就是将会输出结果转发到客户端
- 4、加载修改后的字节码文件
- 5、执行字节码中main方法
- 6、返回执行结果
创建一个jsp文件 test.jsp
<%@ page language="java" contentType="text/html; charset=utf-8"
pageEncoding="utf-8"%>
<%@ page import = "java.lang.*" %>
<%@ page import="java.io.*" %>
<%@ page import="com.rinlink.intelligent.test.*" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Insert title here</title>
</head>
<body>
<%
InputStream is = new FileInputStream("c:/TestClass.class");
byte[] b = new byte[is.available()];
is.read(b);
is.close();
out.println("<textarea style='width:1000; height=800'>");
out.println(JavaClassExecutor.execute(b));
out.println("</textarea");
%>
</body>
</html>
在c盘根目录下创建一个文件 TestClass.java 然后编译成class文件
public class TestClass {
public static void main(String[] args) {
System.out.println("HelloWorld");
}
}
如何编译代码呢?
采用eclipse自带Tomcat将项目加载就编译代码,或者其它方法。
注意这些类放入位置和编译时包位置必须一致
为了测试,先用eclipse的tomcat启动项目完成编译,然后获取编译后的字节码文件单独放置某一个位置,然后删除eclipse文件源.java再一次启动,这时候就没有那个5个类字节码文件没有了,将刚才复制的编译好5个class文件复制到对应文件夹(这样做的目的模拟不重启新增字节码文件是否可以执行)。然后通过浏览器访问就可以看到效果。
最后结果: