第9章 类加载及执行子系统的案例与实战

概述:

学习《深入理解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文件复制到对应文件夹(这样做的目的模拟不重启新增字节码文件是否可以执行)。然后通过浏览器访问就可以看到效果。



最后结果:


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值