Limitaciones del cargador de clases: delegación de los padres y romper la delegación de los padres

¿Qué es un cargador de clases?

La tarea de todo el proceso de carga de la clase es muy pesada, aunque la tarea está en curso, siempre hay alguien para hacerlo. Lo que hace el cargador de clases son los cinco pasos anteriores. (Cargar, verificar, preparar, analizar, inicializar).

Cargador de clases proporcionado por JDK

Bootstrap ClassLoader (inicio)

Este es el identificador en el cargador de clases, y cualquier comportamiento de carga de clases debe pasar por él. Su función es cargar bibliotecas de clases centrales, es decir, rt.jar.resources.jar, charsets.jar, etc. Por supuesto, se puede especificar la ruta de estos paquetes jar y el parámetro -Xbootclasspath puede completar la operación especificada.

Este cargador está escrito en C ++ y comienza con la JVM (es decir, las clases centrales ya están cargadas cuando se inicia el programa y otras clases deben cargarse de acuerdo con diferentes tiempos).

Extention ClassLoader (extendido)

El cargador de clases extendido se usa principalmente para cargar paquetes jar y archivos .class en el directorio lib / ext de jdk. De manera similar, este directorio se puede especificar mediante la variable de sistema java.ext.dirs. Este cargador es una clase Java, heredada de URLClassLoader.

Aplicación ClassLoader (aplicación)

Este es el cargador predeterminado para las clases de Java que escribimos. A veces llamado System ClassLoader. Generalmente se usa para cargar todos los demás paquetes jar y archivos .class bajo la ruta de clase. El código que escribimos primero intentó cargarse usando este cargador de clases.

Cargador de clases personalizado (personalizado)

El cargador de clases personalizado admite algunas funciones de extensión personalizadas.

Cómo determinar la singularidad de una clase

Si escribimos un paquete java.lang en nuestro proyecto, y luego reescribimos parte del comportamiento de la clase String. Después de la compilación, se comprueba que no tiene efecto. Por supuesto, las clases de JRE no se pueden sobrescribir tan fácilmente, de lo contrario, serán utilizadas por personas con motivos ocultos, lo cual es demasiado peligroso.

Para cualquier clase, su singularidad en una máquina virtual Java debe ser establecida por el cargador de clases que la carga y la clase misma. Cada cargador de clases tiene un espacio de nombre de clase independiente. Esta oración se puede expresar de una manera más general: comparar si dos clases son "iguales" solo tiene sentido si las dos clases pertenecen al mismo cargador de clases. De lo contrario, incluso si estas dos clases se originan en el mismo archivo de clase y son cargadas por la misma máquina virtual, siempre que el cargador de clases que las carga sea diferente, las dos clases deben ser desiguales.

El "igual" al que se hace referencia aquí incluye el método equals (), el método isAssignableFrom () y el método isInstance () del objeto Class que representa la clase, así como el uso de la palabra clave instanceof para determinar la relación de propiedad del objeto.

¿Cómo determinar la singularidad de la clase?

Nombre completo de la clase + cargador de clases correspondiente

Entonces, ¿cómo elige la máquina virtual el cargador de clases correspondiente a una clase?

El mecanismo de delegación padre de las reglas del cargador de clases

El modelo de delegación padre requiere que, además del cargador de clases de inicio de nivel superior, todos los demás cargadores de clases deben tener sus propios cargadores de clases padre. Aquí, la relación padre-hijo entre los cargadores de clases generalmente no se realiza en la relación de herencia, pero todos usan la relación de composición (una clase tiene otra clase) para reutilizar el código del cargador de clases.

Al utilizar el modelo de delegación padre para organizar la relación entre los cargadores de clases, una ventaja obvia es que las clases de Java tienen una relación jerárquica de prioridad con sus cargadores de clases. Por ejemplo, la clase java.lang.Object se almacena en rt.jar. No importa qué cargador de clases quiera cargar esta clase, en última instancia, se delega en el cargador de clases de inicio en la parte superior del modelo para cargarlo. Por lo tanto, la clase Object es una clase en los distintos entornos de cargadores de clases del programa, por el contrario, si no existe un modelo de delegación parental, todos los tipos de cargadores lo cargarán por sí mismos, si el usuario escribe una clase llamada java.lang .Objeto. En el ClassPath del programa, habrá múltiples clases de Objeto diferentes en el sistema, no se puede garantizar el comportamiento más básico en el sistema de tipos Java y la aplicación se volverá caótica.

Resumen: El modelo de delegación padre permite que las clases tengan un orden de prioridad, y la clase de la que es responsable el cargador de subclases nunca puede interferir con la clase de la que es responsable el cargador de clases padre, mejorando así la seguridad del sistema.

image.png

Comprender el cargador de clases a través del código fuente

Podemos mirar el método loadClass de la clase ClassLoader del código JDK para ver el proceso de carga específico. Igual que describimos. Utiliza el padre para intentar cargar la clase primero, y luego es su turno después de que el padre falla. Al mismo tiempo, también notamos que este método se puede anular. Es decir, el mecanismo de delegación de los padres puede no ser eficaz.

parent.loadClass representa pasar esta clase hacia arriba.Imagen 1.png

El nombre del parámetro formal representa el nombre completo de la clase.

El método loadClass se utiliza para encontrar el cargador de clases para cargar la clase en función del nombre completo de la clase. En el código fuente, se puede ver que cuando el cargador de clases actual tiene un cargador de clases padre, primero cambiará al cargador de clases padre. Busque de forma recursiva el cargador de clases principal de nivel superior (Bootstrap ClassLoader). Luego, el cargador de clases principal más importante determina si esta clase se puede cargar a su vez. Cada clase eventualmente encontrará su propio cargador de clases único.

La jerarquía de los cargadores de clases

Imagen 3.png

A través de este código, podemos entender que desde una perspectiva jerárquica, podemos llegar a la conclusión de Bootstrap ClassLoader> Extention ClassLoader> Application ClassLoader.

Además , el cargador de clases BootStrapClassLoader no se carga a través de JVM y la capa inferior está implementada en C ++.

Romper la delegación de los padres

Mencionamos anteriormente que la delegación principal es lograr una carga jerárquica, pero ¿por qué tenemos que hablar sobre cómo romperlo? Aquí, usamos dos ejemplos para ilustrar que la delegación de los padres no solo crea aislamiento entre clases y clases, sino que también causa limitaciones.

Mecanismo de carga de clases de Tomcat

Tomcat usa el paquete war para el lanzamiento de aplicaciones, lo que en realidad viola el principio de delegación de los padres. Eche un vistazo breve a la jerarquía del cargador de clases de tomcat.

Imagen 4.png

Para algunas clases básicas que deben cargarse, primero se cargará un cargador de clases llamado WebAppClassLoader. Cuando no se pueda cargar, se entregará al ClassLoader superior para su carga. (Este orden es el opuesto a la delegación parental de JVM). Este cargador se utiliza para aislar archivos .class de diferentes aplicaciones. Por ejemplo, en diferentes versiones del mismo proyecto, no se influyen entre sí. Si aún sigue el modelo de delegación parental, la clase del primer proyecto cargado se cargará correctamente. Las clases siguientes no se pueden cargar y se informa de un error.

Entonces, ¿cómo ejecutar dos versiones incompatibles del proyecto en la JVM? Por supuesto, necesita un cargador de clases personalizado para completar.

Entonces, ¿cómo rompe Tomcat la delegación de los padres? Puede ver WebAppClassLoader en la imagen. Cuando carga el archivo .class en su propio directorio, no se pasará al cargador de la clase principal. Para lograr el aislamiento.

Imagen 5.png

Imagen 6.png

Se puede ver que el delegado es falso por defecto, por lo que no irá al módulo que usa el cargador de clases padre padre.

En Tomcat, puede usar las clases cargadas por SharedClassLoader para lograr compartir y usar las clases cargadas por WebAppClassLoader para lograr la separación.

Pero si escribe una ArrayList y la coloca en el directorio WEB-INF / lib, aún no se cargará. Porque las clases que han sido cargadas por la JVM todavía siguen la delegación parental.

Mecanismo SPI

JDBC

Existe un mecanismo SPI en Java, el nombre completo es Interfaz de proveedor de servicios, que es un conjunto de API proporcionadas por Java para ser implementadas o extendidas por terceros. Se puede utilizar para habilitar extensiones de marco y reemplazar componentes.

Esta afirmación puede ser más oscura. Usamos el controlador de base de datos de uso común para cargar. Antes de escribir un programa usando JDBC, se suele llamar a la siguiente línea de código para cargar la clase de controlador que debe cargarse.

Class.forName ("com.mysql.cj.jdbc.Driver")

Este es solo un modo de inicialización. El objeto del controlador se muestra y se declara a través del bloque de código estático, y luego la información se guarda en una Lista en la parte inferior. Esta es una idea de la programación de la interfaz.

Pero encontrará que incluso si se elimina la línea de Class.forName, se puede cargar la clase de controlador correcta. No es necesario hacer nada, es muy mágico. ¿Cómo se hace esto?

Imagen 7.png

Imagen 8.png

El agente del controlador MySql se implementa aquí.

路径 : conector-mysql-java-8.0.11.jar / META-INF / services / java.sql.Driver

El contenido interior es: com.mysql.cj.jdbc.Driver

Imagen 10.png

Al crear un archivo con el nombre completo de la interfaz en el directorio META-INF / services (el contenido es el nombre completo de la clase de implementación), esta implementación se puede cargar automáticamente, que es SPI. Déjame explicarte en detalle a continuación.

La delegación parental resuelve el problema de la coherencia de los tipos básicos cuando varios cargadores cooperan (las clases más básicas las carga el cargador de nivel superior). El tipo básico se llama "básico". Es porque siempre existen como API que son heredadas y llamadas por el código del usuario, pero a menudo no hay reglas perfectas para el diseño de programas. Si el tipo básico necesita volver a llamar el código del usuario, ¿qué se debe hacer?

Esto no es imposible. Un ejemplo típico es el servicio JNDI. JNDI es ahora un servicio estándar de Java, y su código es cargado por el cargador de clases de inicio (agregado a rt.jar en JDK1.3), debe ser un tipo muy básico de datos Java. Pero el propósito de JNDI es encontrar y administrar recursos de manera centralizada, necesita llamar al código de la interfaz de proveedor de servicios (SPI) JNDI implementada por otros proveedores y desplegada bajo la ClassPath de la aplicación. Ahora viene el problema, es absolutamente imposible reconocer y cargar estos cargadores de clases. Nuestro objetivo ahora es claro: necesitamos cargar estas clases usando el cargador de clases responsable de cargar estas clases. Entonces, ¿cómo lo consigue?

Para resolver este dilema, el equipo de Java introdujo un diseño menos elegante: cargador de clases de contexto de subprocesos (debemos llamar al código del cargador de clases padre en nuestro propio código, por lo que el cargador de clases de contexto es nuestro cargador de clases de aplicaciones 器). Este cargador de clases se puede configurar mediante el método setContext-ClassLoader () de la clase java.lang.Thread. Si no se establece cuando se crea el subproceso, heredará uno del subproceso principal. Si no se establece en el alcance global de la aplicación, entonces este cargador de clases es la aplicación por defecto y el cargador de clases es la aplicación por defecto Cargador de clases de programa.

Con el cargador de clases contextual, el programa utiliza la capacidad de "fraude". El servicio JNDI utiliza este cargador de clases de contexto de subprocesos para cargar el código de servicio SPI requerido, que es un comportamiento en el que el cargador de clases padre solicita al cargador de clases hijo que complete la carga de clases. Este comportamiento realmente rompe la jerarquía de la delegación de los padres para revertir el uso del cargador de clases, que también viola sus principios generales. Pero esto también es impotente: la carga de SPI diseñada en Java se realiza básicamente de esta manera.

Imagen 9.png

Este enfoque también rompe la delegación de los padres.

Del código fuente JNDI, comprenda el mecanismo SPI

Tanto la clase DriverManager como la clase ServiceLoader pertenecen a rt.jar. Su cargador de clases es Bootstrap ClassLoader, y el controlador de la base de datos para operaciones específicas proviene del código comercial. Este cargador de clases de inicio no se puede cargar. ¿Qué debo hacer en este momento?

El propio servicio de gestión de Java, JNDI, se coloca en rt.jar y el cargador de clases de inicio lo carga. Tome el JDBC de gestión de base de datos como ejemplo.

Java proporciona una interfaz Dirver para operaciones de base de datos:

Imagen 11.png

Imagen 12.png

La mayor parte del código se omite aquí, pero podemos encontrar que antes de usar el controlador de la base de datos, debe usar registerDriver () para registrarse en el DriverManager de BootstrapClassLoader antes de que pueda usarse normalmente.

No destruye la delegación de los padres (no se aplica a los servicios JNDI)

Echemos un vistazo a cómo se carga el controlador mysql.

Imagen 13.png

El núcleo es que Class.forName () activa la carga del controlador mysql. Veamos la conexión de mysql a Driver.

Imagen 14.png

Como puede ver, Class.forName () en realidad activa un bloque de código estático y luego registra una implementación de controlador mysql con DriverManager.

En este momento, cuando obtenemos la conexión a través de DriverManager, solo necesitamos establecer todas las implementaciones actuales de Driver y luego seleccionar una para establecer una conexión.

Socavando el modelo de delegación de los padres

Después de JDBC4.0, comenzó a admitir el uso de spi para registrar este controlador. El método específico es especificar qué controlador se usa actualmente en el archivo META-INF / services / java.sql.Driver en el paquete jar mysql, y luego solo conéctese directamente cuando lo use.

Imagen 15.png

Aquí encontramos que la conexión se obtiene directamente y se omite el proceso de registro de Class.forName ().

Ahora analizamos el proceso básico de usar este modo de servicio spi:

1. Obtenga el nombre de clase de implementación específico "com.mysql.jdbc.Driver" del archivo META-INF / services / java.sql.Driver.

2. Cargue esta clase, aquí solo debe cargarse con class.forName ("com.mysql.jdbc.Driver")

Entonces viene el problema, Class.forName () se carga con el ClassLoader de la persona que llama, el DriverManager de la persona que llama está en rt.jar, ClassLoader es el cargador de clases de inicio y com.mysql.jdbc.Driver definitivamente no está allí. <JAVA_HOME> / lib, por lo que la clase en mysql no debe cargarse.Esta es la limitación de la delegación principal, el cargador de clases principal no puede cargar la clase en la ruta del cargador de clases secundario.

Entonces, ¿cómo se puede resolver el problema? De acuerdo con la situación actual, este controlador mysql solo puede ser cargado por el cargador de clases de la aplicación, por lo que solo necesitamos obtener el cargador de clases de la aplicación en el método de inicio del cargador de clases. Luego cárguelo a través de él. Este es el llamado cargador de contexto de subprocesos. (El hilo es el hilo principal y el contexto en el que se encuentra el hilo principal lo carga el cargador de clases de la aplicación),

El cargador de clases de contexto de subprocesos se puede configurar mediante el método Thread.setContextClassLoader (). Si no hay una configuración especial, se heredará de la clase principal. Generalmente, el cargador de clases de aplicaciones se utiliza de forma predeterminada.

Obviamente, el cargador de clases padre del cargador de clases de contexto de subprocesos se puede cargar llamando al cargador de clases hijo, lo que rompe el principio del modelo de delegación padre.

Ahora echemos un vistazo a cómo DriverManager usa el cargador de clases de contexto para cargar la clase Driver en el paquete jar de terceros.

Imagen 23.png

Cuando está en uso, llamar directamente al método DriverManager.getConn () desencadenará naturalmente la ejecución del bloque de código estático. Empiece a cargar el controlador.

Luego, la realización concreta de ServiceLoader.load ().

Imagen 22.png

Se puede ver que el núcleo es obtener el cargador de clases de contexto de subprocesos y luego construir un ServiceLoader, el proceso de búsqueda específico posterior no se analizará, siempre que se haya encontrado que ServiceLoader tiene el cargador de clases de contexto de subprocesos.

Junto a mí, hay una oración driversIterator.next () en el método LoadInitialDrivers () de DriverManager; su implementación específica es la siguiente:

Imagen 16.png

¿Lo viste? Vea el conocido método Class.forName (). De hecho, el método DriverManager.getConnection () originalmente usa la clase DriverManager en el cargador de clases de arranque. Luego, en el cargador de BootStrap a través del entorno de contexto (después de todo, escribimos el código que debe cargarse en la aplicación o en el cargador de clases personalizado de nivel inferior) para obtener el cargador de clases de la aplicación. Dando vueltas en un gran círculo, el objetivo es muy claro, que es lograr una llamada razonable al método Class.forName () en el cargador de clases principal.

Hasta ahora, hemos obtenido con éxito el cargador de clases de aplicaciones a través del cargador de clases de contexto de subprocesos (y, al mismo tiempo, también hemos encontrado el nombre de clase de implementación específico del controlador registrado por el fabricante en el paquete jar secundario, de modo que puede ingresar exitosamente al rt.jar El DriverManager en el paquete cargó exitosamente las clases ubicadas en el paquete de la aplicación de terceros.

para resumir

Mirando todo el proceso de carga del controlador mysql:

1. Obtenga el cargador de clases de contexto de subprocesos y, por lo tanto, obtenga el cargador de clases de la aplicación (o un cargador de clases personalizado).

2. Obtenga el nombre de clase de implementación específico "com.mysql.jdbc.Driver" (ServiceLoader.load (Driver.class)) del archivo META-INF / services / java.sql.Driver

3. El cargador de contexto de subprocesos se utiliza para cargar la clase Driver, evitando así los inconvenientes del modelo de delegación padre.

Obviamente, el uso de este servicio spi por parte del controlador mysql realmente rompe el modelo de delegación parental. Después de todo, el cargador de clases padre ha cargado las clases en la ruta secundaria.

La escritura destruye la delegación de los padres

De hecho, en el caso anterior, hemos explicado cómo mysql rompe la delegación de los padres. Entonces, ¿podemos intentar romper la delegación de los padres nosotros mismos?

Primero, necesitamos crear una clase ShuangqinTest que solo será cargada por el cargador de clases de Extensión.

Imagen 17.png

Empaquete esta clase en un paquete jar y colóquela en el directorio jdk \ jre \ lib \ ext para que el cargador de clases de extensión la identifique.

Imagen 20.png

En este momento, este archivo jar también se introduce en el directorio del proyecto, como se muestra en la figura.

Imagen 18.png

¡De esta manera, la clase ShuangqinTest solo será cargada por el cargador de clases de Extensión!

Defina el método de prueba en esta clase y anote la ruta de clases cargada por el cargador de aplicaciones. El método locadExtClass es similar a la clase Test, excepto que la ruta de la clase se cambia a un parámetro.

Luego, obtenga el cargador de clases de la aplicación a través del contexto Thread.currentThread (). GetContextClassLoader (). Posteriormente, mediante el uso de la reflexión, puede llamar a los métodos de la clase cargada por el cargador de clases de la aplicación en la clase cargada por el cargador de clases de Extensión. Echemos un vistazo a los resultados de la operación específica.

Imagen 22.png

Imagen 23.png

Puede probarlo usted mismo para profundizar su idea de romper la solución del cargador de clases.

OSGI (entender)

OSGI fue una vez muy popular, y Eclipse es la base para usar OSGI como un sistema de complemento. OSGI es la especificación de la plataforma de servicio, determinada para ser utilizada en sistemas que requieren una operación a largo plazo, actualizaciones dinámicas y un daño mínimo al medio ambiente.

La especificación OSGI define mucho sobre el ciclo de vida del paquete, así como la forma de interactuar con la infraestructura y los paquetes vinculantes. Estas reglas, mediante el uso de un cargador de clases especial de Java para hacerlas cumplir, son más dominantes.

Por ejemplo, en las aplicaciones Java generales, no hay duda de que todas las clases en la ruta de clases son visibles para otras clases. Sin embargo, el cargador de clases OSGI restringe la interacción de estas clases según la especificación OSGI y las opciones especificadas en el archivo manifest.mf de cada paquete de enlace, lo que hace que el estilo de programación sea muy extraño. Pero no es difícil imaginar que este método de carga contraintuitivo sea implementado por un cargador de clases dedicado.

Con el desarrollo de JPMS (introducido por JDK9, está diseñado para implementar un sistema de módulos estándar para la plataforma Java SE)

Diferencia de combinación y herencia y conexión entre clase y relación de clase

combinación

Es suficiente declarar un objeto de otra clase como su variable miembro en la clase actual.

Imagen 24.png

ventaja:

1. Acoplamiento bajo. Los cambios en los detalles internos del objeto referenciado no afectarán el uso del objeto actual.

2. Puede enlazarse dinámicamente. El objeto actual se puede vincular dinámicamente a los objetos contenidos en tiempo de ejecución.

Desventaja

Es fácil producir demasiados objetos.

heredar

Herencia significa que la subclase hereda las características y comportamientos de la clase principal, de modo que el objeto de la subclase tiene el dominio de instancia y los métodos de la clase principal, o la subclase hereda métodos de la clase principal, de modo que la subclase tiene el mismo comportamiento la clase padre.

El objetivo de la herencia no es "proporcionar métodos para la nueva clase", sino expresar la relación entre la nueva clase y la clase base: "la nueva clase es una forma de la clase existente".

Imagen 25.png

Características heredadas:

1. La clase secundaria tiene las propiedades y métodos no privados de la clase principal.

2. Las subclases pueden tener sus propios atributos y métodos para extender la clase padre.

3. La subclase puede implementar el método de la clase padre a su manera (anulación del método).

4. Herencia única, una subclase tiene solo una clase principal.

5. Herencia múltiple, es decir, una clase secundaria hereda una clase principal y esta clase principal también puede heredar su clase principal.

6. Mejoró el acoplamiento entre clases ----- las deficiencias de la herencia (la combinación puede reducir el grado de acoplamiento)

7. Destruya la encapsulación, para poder reutilizar el código, la subclase puede tener funciones que no debería tener --- el defecto de la herencia.

Resumen: Si solo se trata de reutilizar el método, se recomienda dar prioridad al método de combinación.

 

Supongo que te gusta

Origin blog.csdn.net/weixin_47184173/article/details/109803737
Recomendado
Clasificación