Has your project ever encountered a situation where there are conflicting jar packages and these conflicting packages must exist at the same time? Generally speaking, jar conflicts are caused by different dependencies on different versions of the same jar package, and the solution is to remove one of them. It is rare that conflicting jar packages need to be retained at the same time.
In the integration of communication with third-party systems, there is a way by the integrated party to provide Jar packages, business code to call the Jar packages provided by the relevant Java classes or interfaces, and many of them are accompanied by an integration of the development of documentation.
If a third party provides jar packages at different times that conflict with each other, and the project must use both jar packages at the same time, what should I do?
conflict scenario
As shown in the following figure, there are two jar packages: third-provider-1.0. and third-provider-1.3. The similarities and differences between the two packages are as follows:
- The same package.
- Same class name :
- Same method signature (with return value): public SampleDevice checkDevice(String deviceNo), return value type SampleDevice is also the exact same package name and class name.
- The structure of the return value type is different: the checkDevice methods of both Jar packages return SampleDevice, but the fields under each SampleDevice are not identical (this is not reflected in the diagram)
- The entry class contains both methods with the same signature and methods with different signatures
And both jar packages must be kept and need to be used in the project at the same time. Because the methods contained in those conflicting classes, which are not exactly the same, have to be kept and used.
According to the class loading rules, a class with the same name will only be loaded once, so if the SampleApi in package 1.0.0 is loaded, the SampleApi in package 1.3.13 will not be loaded. But how do we accomplish this if we want to use both versions of SampleApi in our business code?
Solution 1: Microservice Isolation
The simplest way is to make two versions of the Jar package, into two independent microservices, and then use the interface or method, packaged as Http services, business code to call these services can be.
Taking SampleApi from the 1.0.0 package as an example, here's what to do throughout the packaging process (using SpringWeb as an example):
-
Create a new web project
It depends on the 1.0.0 jar package. And it will eventually be deployed as a web service so that business projects can call the http service it provides. -
Writing a Web Controller
Wrap the corresponding methods in the SampleApi as Http services through the WebController.If necessary, the return class SampleDevice can be wrapped into a new class as well. However, this step can be omitted and the calling method code can be written by itself. This is because the business project exchanges information (usually in json strings) with the web service via http.
Since microservices are currently (2024-06-11) very popular, I won't ramble on here about what to do. Focus on the second implementation.
Solution 2: Class Loader Isolation
A jar package conflict occurs when a class with the same name is loaded only once, but there is another important detail: what is a class with the same name? Generally speaking, two classes are considered to have the same name when they have the same package name and the same class name. But there is another hidden differentiator, the class loader.
Java classes are loaded into the runtime environment by the class loader during program execution. For the same class loader, only one class with the same package name and class name will be loaded. However, different class loaders can load classes with the same package name and class name individually.
Take the conflict scenario in the previous picture as an example: suppose there are two class loaders in the business code, loaderA and loaderB, and if loaderA loads the 1.0.0 package, then it cannot load the 1.3.13 one, because the class loader has already loaded this class. But whatever loaderA loads doesn't affect loaderB's ability to load it again. In other words, loaderB can load both SampleApi from 1.0.0 and SampleApi from package 1.3.13, but not both. It's easy to see that we can load SampleApi in package 1.0.0 by loaderA, and SampleApi in package 1.3.13 by loaderB. This is a combination of loading all the classes in these two conflicting jar packages at the same time in the same project.
The class loader is like a sandbox that isolates two sets of code, and it is this feature that is used to resolve the conflict scenario in this article. In fact, it is most commonly used in Java containers (e.g., tomcat to isolate different web projects). In order to ensure that some of the public base classes (such as jdk in the class) do not repeat the load, the class loader also introduces a two-parent delegate loading mechanism. About the class loader itself is not the focus of this article on the basics , the reader is referred to other related web articles . The next part of this article will focus on how to apply the class loader and implement an engineering-level solution to more amicably resolve the conflict scenarios mentioned earlier.
As shown above, perhaps many readers feel that this article can end here. Because the class loader isolates two classes with the same package and name, the principle is so clear that it must work. It must be possible, and it would be redundant to explain it later.
OK, while it is true that the problem of conflicting class names can be solved by means of different class loaders, at the same time it leads to another problem: when writing code, it cannot be written like normal programming. What does this mean? To make this stuff clear, let's first get a class by way of a class loader and experience the inconvenience of coding it.
Class Loader Programming Experience
Here is a separate maven project to simply experience the difference in code writing between class loader programming and normal programming, project source code:classloader-experienceThe structure is as follows:
┌─ classloader-experience
│ ├─ book-sample # A sample business module whose code will be loaded by a separate class loader.
│ └─ book-sample # A sample business module where the code under the module will be loaded by a separate class loader.
│ │ ├─ BookApi # The usage entry class for the book sample module, which is also loaded directly by the standalone class loader.
│ │ ├─ Book # The book class.
│ │ └─ Press # Publisher class
│ │
│ └─ main # Main module, classes under book-sample will be loaded in main module.
│ └─
│ ├─ MyClassLoader # A simple custom class loader for loading Classes from a specified directory.
│ └─ ClassLoaderExperienceMain # The main class of the entire class loader experience program (entry class)
└─
The main module is the main program, and the classes under book-sample will be loaded by the main module using a separate class loader, so theThe class under the book-sample module cannot be located under the classpath of the main module when it is started.Otherwise, according to the ClassLoader's two-parent delegation model, the book-sample's class loader would be the same one as the main module's class loader, instead of the MyClassLoader we wrote separately, which would defeat the purpose. The reason for writing them both in the same maven project is to make it easier to show all the code in the blog.
Here is the code for the book-sample module
package ;
public class BookApi{
public String description() {
return "Hi,How are you?,It's good to see you.。This content is from BookApi (used form a nominal expression) description methodologies";
}
public Collection<Book> getBooksOfAuthor(String authorName) {
List<Book> books = new ArrayList<Book>();
(new Book("TeaHouse", authorName, 135.0, new Press("Sichuan People's Publishing House", "四川省成都市(used form a nominal expression)一个犄角旮旯处")));
(new Book("The Life of Mine", authorName, 211.0, new Press("Changjiang Literature and Art Publishing House", "大陆一个神秘(used form a nominal expression)地方")));
return books;
}
}
public class Press {
private String name;
private String address;
public Press(String name, String address) {
= name;
= address;
}
// omit the getter and setter methods
}
public class Book {
private String name;
private String author;
private Double price;
private Press press;
public Book(String name, String author, Double price, Press press) {
= name;
= author;
= price;
= press;
}
// omit the getter and setter methods
......
}
The main program will load BookApi using a separate class loader, create an instance of the class, call its descritpion() and getBooksOfAuthor(String name) methods, and then further manipulate the return values of the methods. The former simply returns an object, while the latter returns a collection with elements of type , and the Book class also has a member field of type , so the whole structure is more complex.
OK, now back to the main module, main, which does two things:
- Provides a custom class loader for loading Classes from a specified directory on disk
- Load the class in book-sample in the main program and call the methods in BookApi to access the properties in the method's return value.
Custom class loader (click to view code)
package ;
import ;
import ;
import ;
import ;
public class MyClassloader extends ClassLoader {
// The compiled Class Root directory on disk
private String classRootDir;
public MyClassloader(String classRootDir) {
= classRootDir;
}
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
// retrieveClassbinary data
byte[] classBytes = getClassBytes (className);
return (className, classBytes, 0, );
}
private byte[] getClassBytes(String className) {
// resolveclassAbsolute path to the file on disk
String classFilePath = resolveClassFilePath(className);
// commander-in-chief (military)Class文件retrieve为二进制数组
ByteArrayOutputStream bytesReader;
try (InputStream is = new FileInputStream(classFilePath)) {
bytesReader = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int readSize = 0;
while ((readSize = (buffer)) != -1) {
(buffer, 0, readSize);
}
return ();
} catch (Exception e) {
();
}
return new byte[0];
}
private String resolveClassFilePath(String className) {
return + + ('.', ) + ".class";
}
}
How to write custom class loading is not the focus of this article, here is a brief explanation of the 3 main steps in writing a custom class loader:
-
Custom class loader inheritance, or its subclasses
-
Read the bytecode of the loaded class<Focus>.
Find out where the compiled class bytecode is located based on the class's package name and class name, which may be on disk, on the network, or elsewhere in memory. Read these bytecodes into a byte[] array
-
Call the defineClass method of the parent class to load the class.
The next step is the focus of the entire main module: loading BookApi from the aforementioned package (which incidentally loads Book and Press as well) and calling the description and getBooksOfAuthor methods of the BookApi class as shown below:
package ;
import ;
import ;
/**
* The class loader experiences the main class
*
* @author Gu Zhibing
* @mail ipiger@
* @since 2024-05-18
*/
public class ClassLoaderExperienceMain {
public static void main(String[] args) throws Exception {
// 1. Instantiate a custom class loader.
The root directory of the classes on the // book-sample module, please change it according to your computer.
MyClassloader myClassloader = new MyClassloader("D:\\tmp\\\DemoClass");
// 2. Load the BookApi class.
Class bookApiClass = (""); // 2. Load the BookApi class.
// 3. Create an instance of BookApiClass.
// Instead of writing DemoA demoA = new DemoA(); because DemoA doesn't exist in the class path.
// Even if it did exist, according to the scenario at the beginning of this article, it would not be allowed to exist because of the simultaneous loading of classes with the same name.
Object bookApiObj = ();
// 4. Call the description() method of BookApi.
// This method is simple, and the return type is from the standard library, so it's relatively easy to code
Method testAMethod = ("description"); String resultOfDescription(); // This method is simple.
String resultOfDescription = (String)(bookApiObj); ("description")
("Result of the description() method call: %s\n\n", resultOfDescription);
// 5. Call the getBooksOfAuthor method of BookApi.
// The return value of this method is a collection whose objects do not exist in the Classpath.
// The code to get the properties and methods of the collection elements would appear to be very verbose.
Method getBooksOfAuthorMethod = ("getBooksOfAuthor", );
Collection<? > books = (Collection<? >) (bookApiObj, "LaoShe");
("List of Lao She's works: ");
for (Object book : books) {
// The object in the books collection is of type , .
// But since it's loaded using a separate class loader, it can't be written directly in the source code like it's normally coded, it still has to be retrieved through reflection
Method bookNameMethod = ().getMethod("getName");
Method bookPriceMethod = ().getMethod("getPrice"); String bookName = (String).
String bookName = (String)(book); String bookPriceMethod = ().
Double price = (Double) (book);
// Similarly, objects are accessed through reflection
Method pressMethod = ().getMethod("getPress"); // Object pressObj = (book); String bookName = (String); Double price = (Double) (book)
Object pressObj = (book); // Method pressNameMethod = ().
Method pressNameMethod = ().getMethod("getName");
Method pressAddressMethod = ().getMethod("getAddress");
String pressName = (String)(pressObj);
(" - bookName: 《%s》, price: %.2f, publisher: %s, address: %s\n", bookName, price, pressName, pressAddress);;
}
}
}
Step 1 of the test code creates a MyClassLoader that will load BookApi from D:\tmp\DemoClass. therefore, you need to copy all the compiled Classes under the book-sample module into the D:\tmp\DemoClass directory at the package level.
The output result is:
Result of the description() method call: Hi, how are you, nice to meet you. This content is from BookApi's description method
List of works by Lao She.
- Book Name: TeaHouse, Price: 135.00, Publisher: Sichuan People's Publishing House, Address: a nook and cranny in Chengdu, Sichuan Province, China
- Book Name: The Life of Mine, Price: 211.00, Publisher: Changjiang Literature and Art Publishing House, Address: A Mysterious Place in Mainland China
As you can see from the above code, there are the following code writing inconveniences to access Classes and instances that are loaded through a separate class loader:
- Instances of classes cannot be created directly by new or by calling static code (reflection is required, as in step 3)
- You can't call an instance's methods by means of () (you need to do it by reflection, as in step 4)
- Cannot get methods or properties of an object directly (needs to be through reflection, as in step 5)
In short, everything needs to be coded in a reflective way, which is too bad, not only is the code very long (as in step 5), but it's also very hard to read.
Middle Tier Interface Program
Program Principles
It seems like the perfect class loader isolation solution is causing trouble for writing business code, isn't there a good solution? There is really a way, just need to pay some extra work in advance, but it is worth it. This program is: Define a set of intermediate API, for the two Jar package in conflict with the method of defining a different upper layer interface , business code directly into the intermediate API and the use of the method can be. The solution to be able to implement up, but also for the two jar packages are written separately for the implementation of the middle layer API code. To summarize, the following steps need to be completed:
-
Define the middle tier interface
-
Write implementation code for the middle-tier interface for each of the two conflicting JAR packages
-
Code directly in business code using middle-tier interface classes
This sounds like nonsense, so here's an explanation; the so-called direct use of the middle-tier interface to write code omits the following details:
-
The middle-tier interface class is located in the classpath of the business code, which uses the same class loader as the business code, so the business code that uses the middle-tier interface doesn't need to be called through reflection, but is the most natural and plain form of writing.
-
The middle-tier implementation code of the two conflicting Jar packages needs to be loaded via a separate class loader, with the
Class Loader Programming Experience
The treatment in the chapter is consistent. -
Similarly, the bottom two original jar packages need to be loaded by separate class loaders
-
Up to this point, probably you didn't understand what this program is 😁 What is its principle? Before explaining further, let's make one thing clear - what problem the program is trying to solve. The program is going to solve the following two problems:
-
Load Classes in Two Conflicting Jars Simultaneouslyissue-α
-
The ability to access the functionality provided in both jar packages in business code as if it were normal code, rather than through reflection.issue-β
Now let's go back to the drawing board: look at what the original project looked like.
As shown in the above figure, the initial problem is that the business code needs to use two classes with the same name but different internal functionality in two 3rd party jar packages at the same time. After isolation by class loader, it is possible to load these two conflicting classes in the same project at the same time, but it is very inconvenient to write business code.
In other words, issue-α has been solved, now we focus on how to solve issue-β. The class loader is a sandbox, and when classes in the same sandbox access each other's methods and properties, their code is written in the simplest and most natural way. However, if the a() method of class A in the α-sandbox wants to access the b() method of class B in the β-sandbox, it can't be called directly by () because there is no class B in the classpath, and if we force the Classes in the β-sandbox (which are assumed to be located in the β.jar) to be added to the classpth, then it will lead to a compilation failure. Because in our scenario, there are two sandboxes, β and β, the compiler loads class B in β, it will not load class B in β again. The compiler loads class B in β, and will not load class B in β again, because the classes in β and β are both located in the classpath, and will be loaded by the same class loader.
To use both the B class in β and the B class in β in business code in the same way that ordinary code is written, we can introduce an intermediate layer (assumed to be called δ.jar) where it is sufficient to write a separate set of interfaces with no more conflicting names for both β and the conflicting parts of β. For example:
-
The b() method of class B in β is defined as:
public int b(){ return 9929; } -
The b() method of class B in β is defined as:
public String b(String title){return "Hello " + title; }
Then you can define the following interface class in δ.jar:
public interface Api {
// Maps to the b() method in β.
public int b1();
// Maps to the b(String title) method in β.
public String b2(); }
}
In this way, the business code α.jar does not need to access the methods in β and β directly, but by calling the corresponding methods in the API class in δ.jar. Since δ.jar has no class name conflicts and is distinguished by different method names, adding δ.jar to the classpath of your business code project will allow you to use it likeint xxx = Api.b1();
cap (a poem)String yyy = Api.b2();
Write the code this way now.
Now there's one last detail left: where is the interface, the API in δ.jar, implemented? To accomplish the goal of Api#b1() eventually calling the β#b() method, and Api#b2() eventually calling the β#b() method, two more jar packages need to be introduced: β and β. β is used to implement the δ..#b1() method, which is implemented by calling the β#b() method again. Similarly, β is used to implement the δ..b2() method, which is implemented by transposing the β#b() method.
This is a bit of a roundabout way of looking at things, and it may seem like a bit of a pants-down exercise in redundancy. The point is to be clear: why not place the Classes that implement the Api#b1() and Api#b2() interface methods directly into δ.jar? The reason is that the Classes in δ.jar are loaded by the same class loader as the Classes in the business code α.jar, and the implementation of Api#b1() will eventually call β#b(), and the implementation of Api#b2() will eventually call β#b(), so β1 and β2 are conflicting, and therefore they can't appear in the same class loader.
At this point, the components involved in the entire program have been introduced, as shown in the following figure:
OK, now let's change our perspective and look at these classes mentioned above from the class loader's point of view, and which class loaders are loaded by each of them. The business program to be enabled is α.jar, assuming its class loader is app-main-loader, and in addition, in the code of α.jar, two class loaders will be created, third-jar1-loader and third-jar2-loader, for loading β and β respectively (which is the original requirement for resolving issue-α : isolate conflicting classes). Taking the package involved in the above figure as an example, the relationship between the class loader and the package it loads is shown in the following table:
program package | class loader | use |
---|---|---|
α.jar | app-main-loader | Business master program |
δ.jar | app-main-loader | Intermediate layer interfaces, better than defining β and functional abstractions in β, resolving class name conflicts |
β | third-jar1-loader | Implement the interface methods in δ.jar related to β1 |
β | third-jar1-loader | Original package provided by a third party1 |
β | third-jar2-loader | Implement the interface methods in δ.jar related to β2 |
β | third-jar2-loader | Original package provided by a third party2 |
Programs in action
It's time to write a more realistic sample to validate the program in its entirety (Click to download source code). As with the package mentioned in the rationale above, the code for this program in action, is divided into six parts, each of which is a separate maven project, as shown below:
project name | use | Corresponding to the program package in the "Program Principles |
---|---|---|
load-classes-main | Business master program | α.jar |
third-provider-api | Middle-tier interfaces that encapsulate the capabilities provided by two third-party packages | δ.jar |
third-provider-jar1 | 1st tripartite package | β |
third-provider-jar1-wrapper | Functional wrapper for the first third-party package, which will implement the interfaces in third-provider-api related to the first third-party package | β |
third-provider-jar2 | 2nd tripartite package | β |
third-provider-jar2-wrapper | Functional wrapper for the 2nd third-party package, which will implement the interfaces in third-provider-api related to the 2nd third-party package | β |
The loader-classes-main project is the entry point of the whole practical project, which contains only two classes, the class loader for loading jar packages and the Main program. However, the project will introduce a dependency on the third-provider-api project, and the Main program will also use the interfaces defined in the project, as follows:
As you can see, the two three-way Jar packages and their wrappers are placed in the resources/third-lib directory. The Jar packages in this directory will not be loaded by the main program's bootstrap class loader; they will be loaded manually by the Main program's code. The main program code is as follows:
package ;
import .v1.DeviceBasicInfoV1;
import .v1.DeviceFactoryV1;
import .v2.DeviceBasicInfoV2;
import .v2.DeviceFactoryV2;
public class LoadNameConflictClassesAppMain {
public static void main(String[] args) throws Exception {
// Invoking the first version of the third-party interface(jar1, Being packaged as jar1wrapper)
String third1WrapperJarPath = "/third-lib/third-provider-jar1-wrapper-1.0.";
ClasspathJarLoader third1Classloader = new ClasspathJarLoader(third1WrapperJarPath);
Class third1DeviceFactoryClass =
(".third1wrapper.DeviceFactoryV1Impl");
DeviceFactoryV1 deviceFactoryV1 = (DeviceFactoryV1)(); ⑴
 ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
DeviceBasicInfoV1 device1 = ("BN8964");
("Third Party Interfacev1The result of the call of the version:");
(());
();
// 调用Third Party Interface的第二版本(jar2, Being packaged as jar2wrapper)
String third2WrapperJarPath = "/third-lib/third-provider-jar2-wrapper-1.0.";
ClasspathJarLoader third2Classloader = new ClasspathJarLoader(third2WrapperJarPath);
Class third2DeviceFactoryClass =
(".third2wrapper.DeviceFactoryV2Impl");
DeviceFactoryV2 deviceFactoryV2 = (DeviceFactoryV2)(); ⑵
 ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
DeviceBasicInfoV2 device2 = ("BN8633");
("Third Party Interfacev2The result of the call of the version:");
(());
}
}
Please note that at (1) and (2) in the code, in the upper line of them, a class loader is created for loading the implementation classes of the third-provider-api interface package, DeviceFactoryV1 and DeviceFactoryV2 are defined in third-provider-api, and these two interfaces are used to define third-provider-jar1 and third-provider-jar2 twice (to resolve name conflicts), (1) and (2) get their implementation class instances. These two interfaces are used for the secondary definition of third-provider-jar1 and third-provider-jar2 respectively (to solve the name conflict), (1) and (2) to get their implementation class instances, then the code writing will not need to be completed through the long and difficult to understand reflection API, which also solves the issue-β.
The above proxy has hidden the original class name conflict, which is consistent with the real-world scenario (and should not be dealt with in a business proxy for such purely technical issues). This live code simulates a device checking service that checks the status of a device by specifying the device number when calling the checkDevice method. This service in the third-provider-jar1 and third-provider-jar2, are located in the SampleApi class, the specific service method signature are: SampleDevice checkDevice (String deviceNo). However, the internal fields of SampleDevice in the two jar packages are not the same, and the amount of information returned is quite different, which is the conflict scenario we simulate.
Here is the code from the first Jar package
SampleApi for third-provider-jar1
package ;
import ;
import ;
import ;
// SampleApi is a service class that is exposed throughout the jar1 pair
public class SampleApi {
/* * An external service method of the module: check device information.
* An external service method of the module: checking device information.
* @param deviceNo The device number.
* @rerun will return the device information, which is a composite object with a number of fields
*/
public SampleDevice checkDevice(String deviceNo) {
SampleDevice device = new SampleDevice();
("GUWD5320P001").
("ZhangSan");
(DeviceStatus.RENT_OUT);
InterfaceConfiguration interfaceConfig = new InterfaceConfiguration();
(2);
(2);
(8);
(interfaceConfig).
LocalDateTime localDateTime = (2019, 3, 3, 15, 32, 28); (interfaceConfig); localDateTime
(((("Asia/Shanghai"))).toInstant()))).
return device.
}
}
third-provider-jar1 (used form a nominal expression) SampleDevice
package ;
import ;
public class SampleDevice {
/** device serial number(unique) Serial Identify Number */
private String sid;
/** device status */
private DeviceStatus status;
/** Device Interface Configuration */
private InterfaceConfiguration interfaceConfig;
/** Date of entry of equipment into stock */
private Date inboundDate;
/** person in charge */
private String manager;
// Omit getter and setter methods
}
third-provider-jar1 (used form a nominal expression) InterfaceConfigurstion
public class InterfaceConfiguration {
//Number of power supply connectors
private int powerPortCount = 1;
// Number of Status Indicator Interfaces
private int stateSignalPortCount = 1;
// Number of main waveform output interfaces
private int mainWaveformPortCount = 1;
// Reference Output Frequency
private int baseFrequency = 1600;
// Number of Monitor Input Connectors
private int monitorPortCount = 4;
// Omit getter and setter methods
As you can see, the SampleDevice returned by SampleApi#checkDevice(String deviceNo) contains multiple fields, including a complex object field interfaceConfig, which indicates checking the configuration of the interface on the device, and which itself contains multiple fields.
Here is the code in the second Jar package
SampleApi for third-provider-jar2
package ;
import ;
import ;
public class SampleApi {
public SampleDevice checkDevice(String deviceNo) {
SampleDevice device = new SampleDevice();
("GUWD5320P001");
("southwestern");
();
("Municipal drainage feedwater flow monitoring");
List<SocketSlot> slots = new ArrayList<>();
SocketSlot slot = new SocketSlot(1, 4);
(new Socket(1, , "EBS-9527"));
(slot);
slot = new SocketSlot(2, 6);
(new Socket(1, , "CTR-0709"));
(new Socket(2, , "CTR-0310"));
(new Socket(3, SocketType.MAIN_WAVEFORM, "WVE-15218"));
(slot);
slot = new SocketSlot(3, 12);
(new Socket(1,, "MTR-709817"));
(new Socket(2,, "MTR-3572"));
(new Socket(3,, "MTR-709817"));
(new Socket(4,, "MTR-709817"));
(new Socket(5,, "MTR-709817"));
(new Socket(6,, "MTR-709817"));
(slot);
(slots);
return device;
}
}
SampleDevice for third-provider-jar2
package ;
import ;
public class SampleDevice {
/** device serial number(unique) Serial Identify Number */
private String sid;
/** device status */
private DeviceStatus status;
/** Interface Slot List */
private List<SocketSlot> socketSlots;
/**
* region,as if: Shenzhen subprovincial city in Guangdong, special economic zone close *
*/
private String region;
// omit getter and setter mthods
}
SocketSlot for third-provider-jar2
package ;
import ;
import ;
/**
* Physical interface slot,Multiple interfaces installed in one interface slot
*/
public class SocketSlot {
/** Interface slot number */
private int number;
/** Number of interfaces supported by the interface */
private int socketCount;
/** List of connected interfaces */
private List<Socket> connectedSockets = new ArrayList<>();
public SocketSlot(int number, int socketCount) {
= number;
= socketCount;
}
public void connect(Socket socket) {
if (() >= socketCount) {
("Interface is fully occupied");
return;
}
(socket);
}
}
Socket for third-provider-jar2
package ;
public class Socket {
/** Socket Number */
private int number;
/** Socket Type */
private SocketType type;
/** Interface Specifications */
private String specification;
public Socket(int number, SocketType type, String specification) {
= number;
= type;
= specification;
}
// omit getter and setter methods
}
The SampleDevice returned by jar2's SampleApi#checkDevice(String deviceNo) is very different from the SampleDevice field in the jar1 package, the fields are completely different, and the returned device interface information is a List.
To resolve the conflict between jar1 and jar2, third-provider-api was introduced, with the following solution: provide two different interfaces, each of which returns its own SampleDevice, so that these classes can be used in business code without name conflicts. Here is the structure of the thrid-provider-api project:
third-provider-api
├─ src/main/java/guzb/cnblogs/classloader/thirdapi
│ ├─ v1
│ │ ├─ DeviceFactoryV1 # treat (sb a certain way) third-provider-jar1 center SampleService#checkDevice method encapsulation
│ │ └─ DeviceBasicInfoV1 # treat (sb a certain way) third-provider-jar1 center SampeDevice Class encapsulation
│ └─ v2
│ ├─ DeviceFactoryV1 # treat (sb a certain way) third-provider-jar2 center SampleService#checkDevice method encapsulation
│ └─ DeviceBasicInfoV1 # treat (sb a certain way) third-provider-jar2 center SampeDevice Class encapsulation
└─
In fact, third-provider-api is just a copy of the two jar packages, the content of the conflict class, replaced by two classes with different names, and then can be used in business code. Because the code is very simple, the following is only listed under the v1 package of the two classes
DeviceFactoryV1
public interface DeviceFactoryV1 {
/**
* v1 version of the get device information
* @param deviceNo deviceNo
*/
DeviceBasicInfoV1 getDeviceInfo(String deviceNo);
}
DeviceBasicInfoV1
public class DeviceBasicInfoV1 {
/** Device number */
private String deviceNo;
/** device status */
private String status;
/** Number of equipment connectors */
private int socketCount;
/** Date of entry of equipment into stock */
private Date inboundDate;
/** person in charge */
private String manager;
@Override
public String toString() {
return "DeviceBasicInfoV1 {\n" +
" deviceNo='" + deviceNo + "',\n" +
" status='" + status + "',\n" +
" socketCount=" + socketCount + ",\n" +
" inboundDate=" + inboundDate + ",\n" +
" manager='" + manager + "'\n" +
'}';
}
}
The code of third-provider-jar1-wrapper and third-provider-jar1-wrapper is also very simple, they implement the DeviceFactoryV1 and DeviceFactoryV2 interfaces in third-provider-api respectively. The way to implement them is to call the corresponding service methods in the original third-provider-jar1 and third-provider-jar2, and then transform the return value objects into DeviceBasicInfoV1 and DeviceBasicInfoV2. I won't post the code here, please download theclass-loader-in-action The full source code of the view.
further on
After the above hands-on, I believe you have a real intuitive feel for the class loader. Generally speaking, everyday development does not involve class loaders. But if it does, just to do the above extent, or not enough, because the business code should not involve any content of the class loader, that is: business code should be completely imperceptible to the existence of the class loader. The first two lines of (1) and (2) of LoadNameConflictClassesAppMain in the above code explicitly use the class loader to load the implementation classes of DeviceFactoryV1 and DeviceFactoryV2. How can we keep the class loader out of the business code altogether? If we package the process of initializing the implementation classes of DeviceFactoryV1 and DeviceFactoryV2 in LoadNameConflictClassesAppMain into a separate Jar package, and then provide shortcut methods in this Jar package to get these implementation classes directly for the use of the upper level business code, we can achieve the goal. The purpose can be achieved.
In fact, the above scenario is to treat the whole load-classes-main as a solution package to deal with class conflicts, remove the business test code in it, and add a tool class to get DeviceFactoryV1 and DeviceFactoryV2 instances. At this point, load-classes-main has changed from a business test program to a solution jar. Here no longer post the source code, it should be noted that: as an engineering-level solution, you also need to deal with maven packaging and self-service management, do not like the example program, manually third-provider-jar1, third-provider-jar1-wrapper, third-provider- jar12, third-provider-jar2-wrapper to the src/resources/third-lib directory of the load-classes-main project (this step should be done by maven packaging).
extensions
Above a turn of the actual combat, the amount of code is not large, the business involved (device detection) is also a simulation of what seems to be an ordinary class loader use case, in fact, reveals the class loader's powerful ability. The use of class loaders, you can achieve many important isolation framework, the most classic than the Servlet set of standards.
Servlet standard only defines a rich set of web operation interfaces, and interface methods returned by the data object, the specific implementation of the corresponding container (such as tomcat) to complete, the user only needs to write business code in accordance with the standard Servlet interface can be used in practice, according to the requirements of the corresponding container, the business code will be deployed to the container will be able to run. We have been doing this for a long time, but never thought, if deployed in tomcat multiple war package, there are package names and class names are the same class, will not lead to these applications failed to start, or start the behavior of anomalies. Perhaps once thought of this problem, just think tomcat must have a way to solve, as to how to solve the problem, there is no further in-depth thinking. Obviously, the Servlet container is through the class loader to solve this problem.
Similarly, Eclipse and ItelliJ Idea, the two famous IDE, both support plug-in features, and also supports hot-plugging, they are also faced with different plug-ins in the class name conflict problem, the solution is still the use of class loader isolation.
wrap-up
-
Class loaders can resolve class name conflicts
-
The code writing problems posed by class loaders can be solved by introducing an intermediary layer
-
Class loaders are hardly ever used in business development, and if they are, this detail should be hidden in the business code by introducing an intermediate layer
-
Class loaders are widely used in containers, frameworks and IDEs
-
Sample Project Source Code
- Class Loader Programming Experience
- Middle Tier Interface Solution to Address Class Loader Code Writing Inconvenience