深度思考:老生常谈的双亲委派机制,JDBC、Tomcat是怎么反其道而行之的?

news/2024/5/18 21:49:16 标签: java, jdbc, tomcat

要说双亲委派机制,还得从类加载器的类型谈起

一、类加载器的类型

类加载器有以下种类:

  • 启动类加载器(Bootstrap ClassLoader)
  • 扩展类加载器(Extension ClassLoader)
  • 应用类加载器(Application ClassLoader)

启动类加载器

内嵌在JVM内核中的加载器,由C++语言编写(因此也不会继承ClassLoader),是类加载器层次中最顶层的加载器。用于加载java的核心类库,即加载jre/lib/rt.jar里所有的class。由于启动类加载器涉及到虚拟机本地实现细节,我们无法获取启动类加载器的引用。

扩展类加载器

它负责加载JRE的扩展目录,jre/lib/ext或者由java.ext.dirs系统属性指定的目录中jar包的类。父类加载器为启动类加载器,但使用扩展类加载器调用getParent依然为null。

应用类加载器

又称系统类加载器,可用通过 java.lang.ClassLoader.getSystemClassLoader()方法获得此类加载器的实例,系统类加载器也因此得名。应用类加载器主要加载classpath下的class,即用户自己编写的应用编译得来的class,调用getParent返回扩展类加载器。

扩展类加载器与应用类加载器继承结构如图所示:

可以看到除了启动类加载器,其余的两个类加载器都继承于ClassLoader,我们自定义的类加载器,也需要继承ClassLoader。

值得注意的是,启动类、扩展类与应用类加载器之间的父子关系,并不是通过继承来实现的,而是通过组合,即使用parent变量来保存“父加载器”的引用。


二、双亲委派机制

当一个类加载器收到了一个类加载请求时,它自己不会先去尝试加载这个类,而是把这个请求转交给父类加载器,每一个层的类加载器都是如此,因此所有的类加载请求都应该传递到最顶层的启动类加载器中。只有当父类加载器在自己的加载范围内没有搜寻到该类时,并向子类反馈自己无法加载后,子类加载器才会尝试自己去加载。

加载标准类库与用户代码,会有不同的方式:

 ClassLoader内的loadClass方法,就很好的解释了双亲委派的加载过程:

java">    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            //检查该class是否已经被当前类加载器加载过
            Class<?> c = findLoadedClass(name);
            if (c == null) {
              //此时该class还没有被加载
                try {
                    if (parent != null) {
                      //如果父加载器不为null,则委托给父类加载
                        c = parent.loadClass(name, false);
                    } else {
                       //如果父加载器为null,说明当前类加载器已经是启动类加载器,直接时候用启动类加载器去加载该class
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }

                if (c == null) {
                    //此时父类加载器都无法加载该class,则使用当前类加载器进行加载
                    long t1 = System.nanoTime();
                    c = findClass(name);
                    ...
                }
            }
            //是否需要连接该类
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

三、双亲委派存在的意义

为什么要使用双亲委派机制呢?

假设用户自己定义了java.lang.Object类,由于双亲委派机制的存在,最终会委托到启动类加载器去加载,即返回rt.jar中的Object类,并不会加载用户编写的Object类。

大家上班摸鱼刷的LeetCode,本质上自定义了一个类加载器,重写了findClass方法,会从网络中加载字节码,生成Class对象,最终通过loadClass定义的双亲委派机制进行加载。如果这个时候,我定义了一个恶意java.lang.Object类,在没有双亲委派机制的情况下,可能会对jvm产生安全风险。

双亲委派机制存在的意义,就是为了防止findClass与defineclass生成的Class对象覆盖掉标准类库中的基础类,避免产生安全风险。


四、如何自定义类加载器

我们整理ClassLoader里面的流程

  1. loadclass:双亲委派机制,子加载器委托父加载器加载,父加载器都加载失败时,子加载器通过findclass自行加载
  2. findclass:当前类加载器根据路径以及class文件名称加载字节码,从class文件中读取字节数组,然后使用defineClass
  3. defineclass:根据字节数组,返回Class对象

我们在ClassLoader里面找到findClass方法,发现该方法直接抛出异常,应该是留给子类实现的。

java">    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

到这里,我们应该明白,loadClass方法使用了模版方法模式,主线逻辑是双亲委派,但如何将class文件转化为Class对象的步骤,已经交由子类去实现。对模版方法模式不熟悉的同学,可以先参考我的另外一篇文章模版方法模式

其实源码中,已经有一个自定义类加载的样例代码,在注释中:

java">      class NetworkClassLoader extends ClassLoader {
          String host;
          int port;
 
          public Class findClass(String name) {
              byte[] b = loadClassData(name);
              return defineClass(name, b, 0, b.length);
          }
 
          private byte[] loadClassData(String name) {
              // load the class data from the connection
             
          }
      }

看得出来,如果我们需要自定义类加载器,只需要继承ClassLoader,并且重写findClass方法即可。

现在有一个简单的样例,class文件依然在文件目录中:

java">package com.yang.testClassLoader;

import sun.misc.Launcher;

import java.io.*;

public class MyClassLoader extends ClassLoader {

    /**
     * 类加载路径,不包含文件名
     */
    private String path;


    public MyClassLoader(String path) {
        super();
        this.path = path;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] bytes = getBytesFromClass(name);
        assert bytes != null;
        //读取字节数组,转化为Class对象
        return defineClass(name, bytes, 0, bytes.length);
    }

    //读取class文件,转化为字节数组
    private byte[] getBytesFromClass(String name) {
        String absolutePath = path + "/" + name + ".class";
        FileInputStream fis = null;
        ByteArrayOutputStream bos = null;
        try {
            fis = new FileInputStream(new File(absolutePath));
            bos = new ByteArrayOutputStream();
            byte[] temp = new byte[1024];
            int len;
            while ((len = fis.read(temp)) != -1) {
                bos.write(temp, 0, len);
            }
            return bos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != fis) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (null != bos) {
                try {
                    bos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        MyClassLoader classLoader = new MyClassLoader("C://develop");
        Class test = classLoader.loadClass("Student");
        test.newInstance();
    }
}

Student类:

java">public class Student {
    public Student() {
        System.out.println("student classloader is" + this.getClass().getClassLoader().toString());
    }
}

注意,这个Student类千万不要加包名,idea报错不管他即可,然后使用javac Student.java编译该类,将生成的class文件复制到c://develop下即可。

运行MyClassLoader的main方法后,可以看到输出:

看得出来,Student.class确实是被我们自定义的类加载器给加载了。


五、双亲委派机制能被破坏吗

从上面的自定义类加载器的内容中,我们应该可以猜到了,破坏双亲委派直接重写loadClass方法就完事了。事实上,我们确实可以重写loadClass方法,毕竟这个方法没有被final修饰。双亲委派既然有好处,为什么jdk对loadClass开放重写呢?这要从双亲委派引入的时间来看:

双亲委派模型是在JDK1.2之后才被引入的,而类加载器和抽象类java.lang.ClassLoader则在JDK1.0时代就已经存在,面对已经存在的用户自定义类加载器的实现代码,Java设计者引入双亲委派模型时不得不做出一些妥协。在此之前,用户去继承java.lang.ClassLoader的唯一目的就是为了重写loadClass()方法,jdk为了向前兼容,不得已开放对loadClass的重写操作。

当然,破坏也不止这一次,jdbctomcat也破坏了双亲委派。


六、JDBC对双亲委派的破坏

还记得,我们第一次学jdbc的时候,是怎么连接数据库的吗?

先引用一个mysql-connector-java的jar包,这里的版本是5.0.8

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.0.8</version>
        </dependency>
java">        Class.forName("com.mysql.jdbc.Driver");
        Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "root");

这段代码,真的是勾起了我好多回忆啊~   想起了那年在夕阳下奔跑的时光,那是我逝去的青春

首先要说明的是,该版本的jdbc并没有去打破双亲委派,或者说jdbc4.0前没有破坏双亲委派

数据库这么多,jdk为了统一管理数据库驱动,在java.sql下定义了Driver接口,具体的实现由数据库厂商去做。

mysql对Driver接口的实现类是com.mysql.jdbc.Driver类,位于我们新引入的jar包中。

我们进入Class.forName中,发现最终会使用应用类加载器去加载com.mysql.jdbc.Driver类。

而该Driver位于引入的jar包中,确实是应该被应用类加载器加载。

 接着进入到com.mysql.jdbc包下的Driver类中,它实现了rt.jar中的java.sql.Driver接口。

 Class.forName会初始化该类,初始化的时候会执行静态方法。

java">public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            //将mysql的Driver注册进驱动管理器中
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

所以,整个过程是:

  1. Class.forName会使用应用类加载器加载Driver实现类
  2. 加载Driver实现类需要执行静态方法,即将mysql的Driver注册进驱动管理器中,那么此时需要加载DriverManager类
  3. 应用类加载器去加载DriverManager类,而DriverManager位于rt.jar中,便一直向上委托到启动类加载器完成加载

这个过程确实没有破坏双亲委派

那么jdbc4.0后的情况呢?

为了使用该特性,我们需要引入高版本的mysql-connector-java,这里引入的版本是5.1.8

此时完全可以抛弃第一行的Class.forName语句了,使用以下语句来进行实验

java">        Enumeration<Driver> en = DriverManager.getDrivers();
        while (en.hasMoreElements()) {
            java.sql.Driver driver = en.nextElement();
            System.out.println(driver);
        }

输出为:

 看来内存中已经存在mysql的Driver了,这到底是怎么做的呢?

应用类加载器逐层委托到启动类加载器去加载DriverManager时,会同时执行它的静态方法

java">    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

loadInitialDrivers内部核心的代码这有这两句

java">    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    Iterator<Driver> driversIterator = loadedDrivers.iterator();

看到ServiceLoader,大家想到了什么,这不是jdk spi机制的核心吗?

spi机制在我的这篇文章SpringBoot的自动装配原理、自定义starter与spi机制,一网打尽有详细的一个介绍,并且对比了SpringBoot与JDK中spi机制的异同。

既然使用到了spi机制,那么mysql-connector-java的jar包在META-INF目录下必然有services目录,内容如下。

 启动类加载DriverManager,之后需要通过spi机制去加载jar包中的Driver类,而该Driver理应被应用类加载器加载,这个时候就需要启动类加载器去通知应用类加载器,这明显违背了双亲委派机制

那么,启动类加载器是怎么去通知应用类加载器的呢?

我们继续进入到ServiceLoader.load方法中

java">    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

Thread.currentThread().getContextClassLoader()是线程上下文类加载器,看来最终使用的是线程上下文类加载器去加载的Driver实现类。

而在sun.misc.Launcher类中,将应用类加载器设置进了线程上下文类加载器中,所以可以理解为,通过线程上下文类加载器,我们可以拿到应用类加载器的引用。

java">    public Launcher() {
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        Thread.currentThread().setContextClassLoader(this.loader);
    }

jdbc4.0的情况下,梳理一下整个过程:

  1. 应用类加载器逐层委托到启动类加载器去加载DriverManager类
  2. 启动类加载器加载DriverManager类时,会执行其静态方法,即通过spi机制去加载jar包中的Driver实现类
  3. 此时启动类加载器需要委托应用类加载器加载Driver实现类,具体做法是通过线程上下文类加载器拿到应用类加载器的引用

确实是破坏了双亲委派!


七、Tomcat对双亲委派的破坏

tomcat有两个最基础的知识点,一个是应用打包放在webapps目录下就可以运行,另外一个是修改jsp会实时生效。

那这里抛出几个问题,来猜想一下Tomcat中类加载器的一个结构。

(1)jsp实时生效是怎么做的?

首先,在jvm中,如何去确定类的唯一性呢?是由类加载器实例+全限定名一起确定的。全限定名相同,类加载器不同,则会被认定为不同的类。

jsp文件被修改后,会被重新编译成Servlet,全限定名肯定是不变的,如果这个时候不去卸载加载该Servlet的类加载器,那么新jsp是无论如何都不会被加载进来的。因此,我们可以得知,每一个jsp文件都会对应一个类加载器实例

(2)每个webapps下的应用依赖的类库是否会互相影响?

显然是不会影响的。应用A依赖低版本的Spring,而应用B依赖高版本的Spring,都是允许的。虽然Spring的版本不同,但某些类的全限定名是完全一致的。如果应用A与应用B采用同一个类加载器,是不会允许Spring版本不一样的。这里,我们猜想webapps下的每一个应用都会对应一个不同的类加载器实例,用以保持应用间的隔离。

从以上的两个问题,我们可以了解到:每一个jsp(或者说servlet)都对应一个不同的类加载器实例,每个webapp应用也是。

其实,tomcat5版本(以下如果没有另外声明版本,那么都是以该版本为例)的类加载器结构为:

 其中各个加载器加载的范围为:

  • Common ClassLoader:主要加载common目录下的资源
  • Catalina ClassLoader:主要加载server目录下的资源
  • Shared ClassLoader:主要加载shared目录下的资源
  • Webapp ClassLoader:每一个应用会对应与该类型的一个实例,主要加载该应用下的WEB-INF下的资源
  • JasperLoader:每一个jsp文件会对应于该类型的一个实例,就是为了修改jsp能及时生效

(前三个类加载器在tomcat6中已经合并了,合并之后的加载器加载lib目录下的资源)

它们在Bootstrap类中有过声明:

java">    protected ClassLoader commonLoader = null;
    protected ClassLoader catalinaLoader = null;
    protected ClassLoader sharedLoader = null;

    private void initClassLoaders() {
        try {
            commonLoader = createClassLoader("common", null);
            if( commonLoader == null ) {
                // no config file, default to this loader - we might be in a 'single' env.
                commonLoader=this.getClass().getClassLoader();
            }
            catalinaLoader = createClassLoader("server", commonLoader);
            sharedLoader = createClassLoader("shared", commonLoader);
        } catch (Throwable t) {
            log.error("Class loader creation threw exception", t);
            System.exit(1);
        }
    }

其中createClassLoader方法会从container\catalina\src\conf\catalina.properties配置中读取每个加载器加载的范围:

common.loader=${catalina.home}/common/classes,${catalina.home}/common/i18n/*.jar,${catalina.home}/common/endorsed/*.jar,${catalina.home}/common/lib/*.jar

server.loader=${catalina.home}/server/classes,${catalina.home}/server/lib/*.jar

shared.loader=${catalina.base}/shared/classes,${catalina.base}/shared/lib/*.jar

catalina.home是安装目录,catalina.base是每个tomcat实例的工作目录。在只用一个tomcat的情况下,两个目录是一样的。

在了解了加载器的类型与范围之后,那么tomcat到底是怎么打破双亲委派机制的呢?

前面说过,双亲委派机制被定义在ClassLoader中的loadClass方法中,如果某个自定义的类加载想要打破双亲委派,那么重新loadClass方法即可。

Tomcat中的WebappClassLoader就是自定义类加载器,它的loadClass方法为:
 

java">    public Class loadClass(String name) throws ClassNotFoundException {
        return (loadClass(name, false));
    }
    
    public Class loadClass(String name, boolean resolve)
        throws ClassNotFoundException {

        if (log.isDebugEnabled())
            log.debug("loadClass(" + name + ", " + resolve + ")");
        Class clazz = null;

        // Log access to stopped classloader
        if (!started) {
            try {
                throw new IllegalStateException();
            } catch (IllegalStateException e) {
                log.info(sm.getString("webappClassLoader.stopped", name), e);
            }
        }

        //1、从自己的本地缓存中查找,本地缓存的数据结构为ResourceEntry
        clazz = findLoadedClass0(name);
        if (clazz != null) {
            if (log.isDebugEnabled())
                log.debug("  Returning class from cache");
            if (resolve)
                resolveClass(clazz);
            return (clazz);
        }

        //2、从jvm的缓存中查找
        clazz = findLoadedClass(name);
        if (clazz != null) {
            if (log.isDebugEnabled())
                log.debug("  Returning class from cache");
            if (resolve)
                resolveClass(clazz);
            return (clazz);
        }

        //3、如果缓存中都找不到,则利用系统类加载器加载
        try {
            clazz = system.loadClass(name);
            if (clazz != null) {
                if (resolve)
                    resolveClass(clazz);
                return (clazz);
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }

        if (securityManager != null) {
            int i = name.lastIndexOf('.');
            if (i >= 0) {
                try {
                    securityManager.checkPackageAccess(name.substring(0,i));
                } catch (SecurityException se) {
                    String error = "Security Violation, attempt to use " +
                        "Restricted Class: " + name;
                    log.info(error, se);
                    throw new ClassNotFoundException(error, se);
                }
            }
        }

        boolean delegateLoad = delegate || filter(name);

        //4、开启代理的话,则使用父加载器加载
        if (delegateLoad) {
            if (log.isDebugEnabled())
                log.debug("  Delegating to parent classloader1 " + parent);
            ClassLoader loader = parent;
            if (loader == null)
                loader = system;
            try {
                clazz = loader.loadClass(name);
                if (clazz != null) {
                    if (log.isDebugEnabled())
                        log.debug("  Loading class from parent");
                    if (resolve)
                        resolveClass(clazz);
                    return (clazz);
                }
            } catch (ClassNotFoundException e) {
                ;
            }
        }

        //5、自行加载
        if (log.isDebugEnabled())
            log.debug("  Searching local repositories");
        try {
            clazz = findClass(name);
            if (clazz != null) {
                if (log.isDebugEnabled())
                    log.debug("  Loading class from local repository");
                if (resolve)
                    resolveClass(clazz);
                return (clazz);
            }
        } catch (ClassNotFoundException e) {
            ;
        }

        //如果自己也加载不了,那就只能让父加载器加载了
        if (!delegateLoad) {
            if (log.isDebugEnabled())
                log.debug("  Delegating to parent classloader at end: " + parent);
            ClassLoader loader = parent;
            if (loader == null)
                loader = system;
            try {
                clazz = loader.loadClass(name);
                if (clazz != null) {
                    if (log.isDebugEnabled())
                        log.debug("  Loading class from parent");
                    if (resolve)
                        resolveClass(clazz);
                    return (clazz);
                }
            } catch (ClassNotFoundException e) {
                ;
            }
        }

        throw new ClassNotFoundException(name);
    }

loadClass内部的逻辑整理如下:

  1. 先从WebappClassLoader的ResourceEntry缓存中查找
  2. 从jvm缓存中查找,比如去元数据区查找
  3. 利用系统类(应用类)加载器加载,避免webapp中的类覆盖掉标准类库中的类。
  4. 开启代理的话,则使用父加载器加载,这个默认没开启的。
  5. webappClassLoader自行去加载
  6. 自己也没加载成功的话,最后只能让父加载器去加载

这里有一个问题,对于一些非基础类库,为什么要先让webappClassLoader先去加载呢?

假设应用a依赖1.0版本的x.jar,而应用b依赖2.0版本的x.jar。为了保证两个应用的隔离性,首先要做的就是保证两个应用各自对应不同的webappClassLoader实例。如果这两个webappClassLoader实例在加载x.jar的时候,直接向上委托,那么最终只会加载一个版本的x.jar。

从上面,我们可以了解到:

对于一些标准类库中的类,比如Object类,会让系统类加载器加载,然后一直委托到启动类加载器,这个过程是没有违背双亲委派的

而对于webapp中独有的类,则是webappClassLoader自行去加载,加载失败才让父加载器加载,明显是违背双亲委派的。


八、总结

双亲委派机制,核心是子加载器委托父加载器,能够避免java核心类库被篡改,增加了安全性。

但发展会带来创新,创新就会带来变革,jdbctomcat打破了这个自古相传的机制。

jdbc中,父加载器委托子加载器。即利用线程上下文类加载器,让启动类加载器得以委托应用类加载器,去加载jar中的数据库驱动。

tomcat中,子加载器优先于父加载器加载。即为了实现各个webapp的隔离性,webappClassLoader会先于父加载器加载。


http://www.niftyadmin.cn/n/1704575.html

相关文章

安全合规/GDPR--20--GDPR中处理活动的记录和特殊情形下的克减

为什么是第30条和第49条&#xff1f; 第30条为处理活动的记录&#xff0c;这就意味着你要符合GDPR合规&#xff0c;就要对所有处理活动做好记录工作&#xff0c;也就是说要准备xx材料xx材料xx材料……&#xff01;&#xff01;&#xff01; 第49条为特殊情形下的克减&#xf…

还不清楚JDK动态代理?从简单例子到源码再到字节码讲给你听

一、前言 Spring中的AOP思想就是对代理模式的经典运用&#xff0c;下面先讲讲代理模式的核心思想&#xff0c;以静态代理为例。 二、静态代理示例 下面有这样一个例子&#xff0c;委托人在遭遇利益受损的时候&#xff0c;可以委托律师帮忙打官司。 先定义一个描述行为的接口&…

Spring Batch 批处理框架

一、SpringBatch 介绍 Spring Batch 是一个轻量级、全面的批处理框架&#xff0c;旨在支持开发对企业系统的日常操作至关重要的健壮的批处理应用程序。Spring Batch 建立在人们期望的 Spring Framework 特性&#xff08;生产力、基于 POJO 的开发方法和一般易用性&#xff09;…

安全合规/ISO--6--ISO 27001/27017/27018内审项清单

审核清单如下&#xff1a; 体系标准款项标题审核问题审核方式审核发现审核结果4组织环境––––4.1理解组织及其环境管理者代表是否了解公司的业务以及行业环境访谈符合4.2理解相关方的需求和期望检查组织是否了解客户合同中的需求访谈符合4.3确定信息安全管理体系的范围检查…

new一个对象的背后,竟然有这么多可以说的

一、前言 作为一名java开发工程师&#xff0c;每天要处理上千个对象&#xff0c;你居然说我没对象&#xff1f; 就算没有对象&#xff0c;那就new一个呗。 GirlFriend gf new GirlFriend(); 不会就这么容易吧&#xff1f;当然不会&#xff01; 那么GirlFriend对象到底是怎…

带业往生净土者,是否以后不受果报?

往生净土后不是意味有债不必还&#xff0c;犯杀盗淫妄不受报&#xff0c;若如此观念是邪见错误的&#xff1b;往生到极乐净土&#xff0c;是因为净土环境好&#xff08;依报庄严&#xff09;&#xff0c;师资阵容、同参道友殊胜&#xff08;正报庄严&#xff09;&#xff0c;在…

安全合规/GDPR--21--我们是如何开展PTA、PIA、DPIA风险评估的

一、目的 在开展一个涉及用户隐私信息的新产品、产品的新功能、内外部的新流程&#xff08;以下统称为“项目”&#xff09;的早期阶段&#xff0c;需要对项目进行相应的隐私阈值分析&#xff08;PTA&#xff09;&#xff0c;以帮助项目人员充分了解项目在隐私合规中所产生的影…

安全合规/法案--29--《网络安全法》原文及解读

第一章 总则 第一条 为了保障网络安全&#xff0c;维护网络空间主权和国家安全、社会公共利益&#xff0c;保护公民、法人和其他组织的合法权益&#xff0c;促进经济社会信息化健康发展&#xff0c;制定本法。 解读&#xff1a;本条指明网络安全法的制定目的。 第二条 在中华…