【正文】Java类加载器( CLassLoader )死磕 6:
自定义网络类加载器
本小节目录
6.1. 自定义网络类加载器的类设计
6.2. 文件传输Server端的源码
6.3. 文件传输Client端的源码
6. 4 自定义加载器SocketClassLoader的源码
6.5. SocketClassLoader的使用
前面提到,除了通过Java内置的三大加载器,从JVM中系统属性中设置的三大地盘加载Java类,还存多种的获取Class文件途径。其中非常重要的一种途径,就是网络。
通过网络的加载类,就得依赖网络的传输协议。
网络的传输协议有很多种,比方说TCP、HTTP、SMB等等,都可以用来实现网络的类字节码的传输。
TCP是最为基础的,也是一种传输可靠的传输协议。本小节通过TCP协议,实现自定义的网络类加载器。
1.1.1. 自定义网络类加载器的类设计
服务端的类图如下:
客户端的类图:
1.1.2. 文件传输Server端的源码
文件传输Server端的工作:
开启一个ServerSocket服务,等待Client客户端的TCP连接。
对于每一个客户端TCP连接,开启一个单独的线程,处理文件的请求和发送文件数据。
独立线程首先会接受客户端传输过来的文件名称,根据文件名称,在自定义的类路径下查找文件。这里的类路径是这个第三方类库的路径,为了可以灵活多变,并且与源代码工程的输出路径相不能相同,也在System.properties 配置文件增加配置项,具体为:
class.server.path=D:/疯狂创客圈 死磕java/code/out2/
此配置项在前面的案例中,已经用到了,后面也会多次用到。
服务端找到文件后,开始向客户端传输数据。首先传输文件的大小,然后在传输文件的内容。
简单粗暴,直接上源码。
public class SocketClassServer
{
ServerSocket serverSocket = null;
static String filePath = null;
public SocketClassServer() throws Exception
{
serverSocket = new ServerSocket(SystemConfig.SOCKET_SERVER_PORT);
this.filePath = SystemConfig.CLASS_SERVER_PATH;
startServer();
}
/**
* 启动服务端
* 使用线程处理每个客户端传输的文件
*
* @throws Exception
*/
public void startServer()
{
while (true)
{
// server尝试接收其他Socket的连接请求,server的accept方法是阻塞式的
Logger.info("server listen at:" + SystemConfig.SOCKET_SERVER_PORT);
Socket socket = null;
try
{
socket = serverSocket.accept();
// 每接收到一个Socket就建立一个新的线程来处理它
new Thread(new SendTask(socket)).start();
} catch (Exception e)
{
e.printStackTrace();
}
}
}
/**
* 处理客户端传输过来的文件线程类
*/
class SendTask implements Runnable
{
private Socket socket;
private DataInputStream dis;
private FileOutputStream fos;
public SendTask(Socket socket)
{
this.socket = socket;
}
@Override
public void run()
{
try
{
dis = new DataInputStream(socket.getInputStream());
// 文件名
String fileName = dis.readUTF();
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
sendFile(fileName, dos);
} catch (Exception e)
{
e.printStackTrace();
} finally
{
IOUtil.closeQuietly(fos);
IOUtil.closeQuietly(dis);
IOUtil.closeQuietly(socket);
}
}
private void sendFile(String fileName, DataOutputStream dos) throws Exception
{
fileName = classNameToPath(fileName);
fileName = SocketClassServer.filePath + File.separator + fileName;
File file = new File(fileName);
if (!file.exists())
{
throw new Exception("file not found! :" + fileName);
}
long fileLen = file.length();
//先传输文件长度
dos.writeLong(fileLen);
dos.flush();
FileInputStream fis = new FileInputStream(file);
// 开始传输文件
Logger.info("======== 开始传输文件 ========");
byte[] bytes = new byte[1024];
int length = 0;
long progress = 0;
while ((length = fis.read(bytes, 0, bytes.length)) != -1)
{
dos.write(bytes, 0, length);
dos.flush();
progress += length;
Logger.info("| " + (100 * progress / fileLen) + "% |");
}
Logger.info("======== 文件传输成功 ========");
}
}
private String classNameToPath(String className)
{
return className.replace('.', '/') + ".class";
}
public static void main(String[] args)
{
try
{
SocketClassServer socketServer = new SocketClassServer();
socketServer.startServer();
} catch (Exception e)
{
e.printStackTrace();
}
}
}
源码比较长,建议运行main函数,先将服务端的源码跑起来,然后再阅读代码,这样阅读起来更加容易懂。
另外,在使用基于网络的类加载器之前,一定要确保服务端的代码先执行。否则客户端会报错。
案例路径:com.crazymakercircle.classLoader.SocketClassServer
案例提示:无编程不创客、无案例不学习。一定要跑案例哦
运行的结果是:
|> 开始加载配置文件到SystemConfig
loadFromFile |> load properties: /system.properties
startServer |> server listen at:18899
看到以上结果,表示服务端开始启动。监听了18899端口,等待客户端的连接。
1.1.3. 文件传输Client端的源码
客户端的工作:
建立和服务器的TCP连接后,首先做的第一步工作,是发送文件名称给服务器端。
然后阻塞,直到服务器的数据过来。客户端开始接受服务器传输过来的数据。接受数据的工作由函数receivefile()完成。
整个的数据的读取工作分为两步,先读取文件的大小,然后读取传输过来的文件内容。
简单粗暴,直接上源码。
public class SafeSocketClient {
private Socket client;
private FileInputStream fis;
private DataOutputStream dos;
/**
* 构造函数
* 与服务器建立连接
*
* @throws Exception
*/
public SafeSocketClient() throws IOException {
this.client = new Socket(
SystemConfig.SOCKET_SERVER_IP,
SystemConfig.SOCKET_SERVER_PORT
);
Logger.info("Cliect[port:" + client.getLocalPort() + "] 成功连接服务端");
}
/**
* 向服务端去取得文件
*
* @throws Exception
*/
public byte[] getFile(String fileName) throws Exception {
byte[] result = null;
try {
dos = new DataOutputStream(client.getOutputStream());
// 文件名和长度
dos.writeUTF(fileName);
dos.flush();
DataInputStream dis = new DataInputStream(client.getInputStream());
result = receivefile(dis);
Logger.info("文件接收成功,File Name:" + fileName);
} catch (Exception e) {
e.printStackTrace();
} finally {
IOUtil.closeQuietly(fis);
IOUtil.closeQuietly(dos);
IOUtil.closeQuietly(client);
}
return result;
}
public byte[] receivefile(DataInputStream dis) throws Exception {
int fileLength = (int) dis.readLong();
ByteArrayOutputStream bos = new ByteArrayOutputStream(fileLength);
long startTime = System.currentTimeMillis();
Logger.info("block IO 传输开始:");
// 开始接收文件
byte[] bytes = new byte[1024];
int length = 0;
while ((length = dis.read(bytes, 0, bytes.length)) != -1) {
DeEnCode.decode(bytes,length);
bos.write(bytes, 0, length);
bos.flush();
}
Logger.info(" Size:" + IOUtil.getFormatFileSize(fileLength));
long endTime = System.currentTimeMillis();
Logger.info("block IO 传输毫秒数:" + (endTime - startTime));
bos.flush();
byte[] result = bos.toByteArray();
IOUtil.closeQuietly(bos);
return result;
}
}
案例路径:com.crazymakercircle.classLoader.SocketClassClient
案例提示:无编程不创客、无案例不学习。
此案例类没法独立运行,因为没有运行的入口。只能作为基础类,供其他类调用。
1.1.4. 自定义加载器SocketClassLoader的源码
前面讲到,自定义类加载器,首先要继承ClassLoader抽象类,并且重写其findClass()方法。
在重写的findClass()方法中,完成以下三步:
(1)在自己的地盘(查找路径),获取对应的字节码;
(2)并完成字节码到Class类对象的转变;
(3)返回Class类对象。
简单粗暴,直接上源码。
public class SocketClassLoader extends ClassLoader {
public SocketClassLoader() {
// super(null);
}
protected Class> findClass(String name)
throws ClassNotFoundException {
Logger.info("findClass name = " + name);
byte[] classData = null;
try {
SocketClassClient client = new SocketClassClient();
classData = client.getFile(name);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
}
案例路径:com.crazymakercircle.classLoader.SocketClassClient
有了前面的基础,此源码超级简单,关键的两行,就是下面这个两行:
SafeSocketClient client = new SafeSocketClient();
classData = client.getFile(name);
上面一行,实例化一个SafeSocketClient 客户端client对象。下面一行,通过取得 client对象的client.getFile(name) 方法,通过网络TCP协议,获得类的字节码。
至于findClass()方法中的其他的处理工作,和前面的文件系统内加载器FileClassLoader 中的findClass(),是一样的。
这里不做赘述。
1.1.5. SocketClassLoader的使用
简单粗暴,先上代码:
public class SocketLoaderDemo
{
public static void testLoader()
{
try
{
SocketClassLoader classLoader = new SocketClassLoader();
String className = SystemConfig.PET_DOG_CLASS;
Class dogClass = classLoader.loadClass(className);
Logger.info("显示dogClass的ClassLoader =>");
ClassLoaderUtil.showLoader4Class(dogClass);
IPet pet = (IPet) dogClass.newInstance();
pet.sayHello();
} catch (ClassNotFoundException e)
{
e.printStackTrace();
} catch (IllegalAccessException e)
{
e.printStackTrace();
} catch (InstantiationException e)
{
e.printStackTrace();
}
}
public static void main(String[] args)
{
testLoader();
}
}
案例路径:com.crazymakercircle.classLoaderDemo.base.SocketLoaderDemo
案例提示:无编程不创客、无案例不学习。一定要跑案例哦
运行的结果是:
|> 开始加载配置文件到SystemConfig
loadFromFile |> load properties: /system.properties
findClass |> findClass name = com.crazymakercircle.otherPet.pet.LittleDog
|> Cliect[port:51525] 成功连接服务端
receivefile |> block IO 传输开始:
receivefile |> Size:2.0KB
receivefile |> block IO 传输毫秒数:8
getFile |> 文件接收成功,File Name:com.crazymakercircle.otherPet.pet.LittleDog
testLoader |> 显示dogClass的ClassLoader =>
showLoaderTree |> com.crazymakercircle.classLoader.SocketClassLoader@34ce8af7
showLoaderTree |> sun.misc.Launcher$AppClassLoader@18b4aac2
showLoaderTree |> sun.misc.Launcher$ExtClassLoader@51016012
Disconnected from the target VM, address: '127.0.0.1:51523', transport: 'socket'
sayHello |> 嗨,大家好!我是LittleDog-1
看到以上结果,表示客户端成功接收了字节码文件,并且成功加载了类。
扩展一下,例如你的第三方的字节码是放在数据库中,也可类似的自己写个类加载器,从指定的数据库加载二进制类。
源码:
代码工程: classLoaderDemo.zip
下载地址:在疯狂创客圈QQ群文件共享。
疯狂创客圈:如果说Java是一个武林,这里的聚集一群武痴, 交流编程体验心得
QQ群链接:疯狂创客圈QQ群
无编程不创客,无案例不学习。 一定记得去跑一跑案例哦
类加载器系列全目录