Java类加载器( 死磕 6)

文章很长,而且持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录 语雀版 | 总目录 码云版| 总目录 博客园版 为您奉上珍贵的学习资源 :


推荐:入大厂 、做架构、大力提升Java 内功 的 精彩博文

入大厂 、做架构、大力提升Java 内功 必备的精彩博文 秋招涨薪1W + 必备的精彩博文
1:Redis 分布式锁 (图解-秒懂-史上最全) 2:Zookeeper 分布式锁 (图解-秒懂-史上最全)
3: Redis与MySQL双写一致性如何保证? (面试必备) 4: 面试必备:秒杀超卖 解决方案 (史上最全)
5:面试必备之:Reactor模式 6: 10分钟看懂, Java NIO 底层原理
7:TCP/IP(图解+秒懂+史上最全) 8:Feign原理 (图解)
9:DNS图解(秒懂 + 史上最全 + 高薪必备) 10:CDN图解(秒懂 + 史上最全 + 高薪必备)
11: 分布式事务( 图解 + 史上最全 + 吐血推荐 ) 12:限流:计数器、漏桶、令牌桶
三大算法的原理与实战(图解+史上最全)
13:架构必看:12306抢票系统亿级流量架构
(图解+秒懂+史上最全)
14:seata AT模式实战(图解+秒懂+史上最全)
15:seata 源码解读(图解+秒懂+史上最全) 16:seata TCC模式实战(图解+秒懂+史上最全)

SpringCloud 微服务 精彩博文
nacos 实战(史上最全) sentinel (史上最全+入门教程)
SpringCloud gateway (史上最全) 分库分表sharding-jdbc底层原理与实操(史上最全,5W字长文,吐血推荐)

推荐:尼恩Java面试宝典(持续更新 + 史上最全 + 面试必备)具体详情,请点击此链接

尼恩Java面试宝典,32个最新pdf,含2000多页不断更新、持续迭代 具体详情,请点击此链接

在这里插入图片描述


【正文】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. 自定义网络类加载器的类设计


服务端的类图如下:

wpsC7EF.tmp

客户端的类图:

wpsC800.tmp


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

案例提示:无编程不创客、无案例不学习。一定要跑案例哦

运行的结果是:

         <clinit> |>  开始加载配置文件到SystemConfig

    loadFromFile |&gt;  load properties: /system.properties

     startServer |&gt;  server listen at:18899

看到以上结果,表示服务端开始启动。监听了18899端口,等待客户端的连接。


1.1.3. 文件传输Client端的源码


客户端的工作:

建立和服务器的TCP连接后,首先做的第一步工作,是发送文件名称给服务器端。

然后阻塞,直到服务器的数据过来。客户端开始接受服务器传输过来的数据。接受数据的工作由函数receivefile()完成。

整个的数据的读取工作分为两步,先读取文件的大小,然后读取传输过来的文件内容。

简单粗暴,直接上源码。

public class SafeSocketClient {

private Socket client;

private FileInputStream fis;

private DataOutputStream dos;

/**

 * 构造函数<span style="color: rgb(0, 0, 255);">&lt;</span><span style="color: rgb(128, 0, 0);">br</span><span style="color: rgb(0, 0, 255);">/&gt;</span>

 * 与服务器建立连接

 *

 * @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<span style="color: rgb(0, 0, 255);">&lt;?</span>&gt; 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 =&gt;");

        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

案例提示:无编程不创客、无案例不学习。一定要跑案例哦

运行的结果是:

  <clinit> |>  开始加载配置文件到SystemConfig

    loadFromFile |&gt;  load properties: /system.properties

       findClass |&gt;  findClass name = com.crazymakercircle.otherPet.pet.LittleDog

          <span style="color: rgb(0, 0, 255);">&lt;</span><span style="color: rgb(128, 0, 0);">init</span><span style="color: rgb(0, 0, 255);">&gt;</span> |&gt;  Cliect[port:51525] 成功连接服务端

     receivefile |&gt;  block IO 传输开始:

     receivefile |&gt;  Size:2.0KB

     receivefile |&gt;  block IO 传输毫秒数:8

         getFile |&gt;  文件接收成功,File Name:com.crazymakercircle.otherPet.pet.LittleDog

      testLoader |&gt;  显示dogClass的ClassLoader =&gt;

  showLoaderTree |&gt;  com.crazymakercircle.classLoader.SocketClassLoader@34ce8af7

  showLoaderTree |&gt;  sun.misc.Launcher$AppClassLoader@18b4aac2

  showLoaderTree |&gt;  sun.misc.Launcher$ExtClassLoader@51016012

Disconnected from the target VM, address: '127.0.0.1:51523', transport: 'socket'

        sayHello |&gt;  嗨,大家好!我是LittleDog-1

看到以上结果,表示客户端成功接收了字节码文件,并且成功加载了类。

扩展一下,例如你的第三方的字节码是放在数据库中,也可类似的自己写个类加载器,从指定的数据库加载二进制类。




源码:


代码工程:  classLoaderDemo.zip

下载地址:在疯狂创客圈QQ群文件共享。


疯狂创客圈:如果说Java是一个武林,这里的聚集一群武痴, 交流编程体验心得
QQ群链接:
疯狂创客圈QQ群


无编程不创客,无案例不学习。 一定记得去跑一跑案例哦


类加载器系列全目录

1.导入

2. JAVA类加载器分类

3. 揭秘ClassLoader抽象基类

4. 神秘的双亲委托机制

5. 入门案例:自定义一个文件系统的classLoader

6. 基础案例:自定义一个网络类加载器

7. 中级案例:设计一个加密的自定义网络加载器

8. 高级案例1:使用ASM技术,结合类加载器,解密AOP原理

9. 高级案例2:上下文加载器原理和案例

posted @ 2018-10-21 09:32  疯狂创客圈  阅读(704)  评论(0编辑  收藏  举报