Preface
An online bug related to the jar package version number occurred in a component of our team two days ago. There has been nothing recently, so I analyzed it by the way. In fact, it is a very mindless little bug: there are a bunch of new methods in the commons-lang3 package @since 3.5, our component depends on a package of version 3.5 or higher; the business side depends on our component,
At the same time, it directly depends on a package under 3.5. When gradle is packaged, since the old version is a direct dependency, the new version is an indirect dependency, and the priority of the direct dependency is higher than the indirect dependency, so the old version of the package is finally used. This results in the NoSuchMethod error being reported when the new method is called at runtime. Although the problem is simple, it is also an online accident (terrible) affecting GMV after all, and it is worth learning a lesson.
Alibaba Cloud Simple Application Server: Anti COVID-19 SME Enablement Program
$300 coupon package for all new SMEs and a $500 coupon for paying customers.
Program
Generally speaking, in relatively large projects, dependency conflicts are almost unavoidable. Generally speaking, most of the solutions to this problem are as follows:
1. For the business side, be careful when writing code. When encountering different dependencies, consciously check the dependency tree, try to use newer packages, and fully test the code in the test environment before it goes online.
2. For the component developer, when writing the access document, the minimum version number of the dependent package should be indicated at the same time, and the access party should be clearly told the minimum dependency, and then the access party can specify it manually.
My thsman
3. Using the Shade technology, for the component developer, the third-party package that needs to be relied on is shaded into its own code, and the "prefix of the own package name + actual package name" is used for isolation.
4. Use container technology, such as OSGI, Jigsaw, Karaf, and other containers to control the jar package. This is a very heavyweight method, generally used only when the project has a certain scale.
5. Using ClassLoader isolation technology, each package uses its own classLoader and does not affect each other. This method is actually very much like a castrated version of container technology, and logically like a container, with another layer of isolation control on jar packages. However, this method is generally not very elegant, a bit like a hack, so it seems that there is no decent complete solution. A little more decent is probably the Sofaark recently launched by Ali, which is very powerful, but it is also more complicated to use, and it is also very invasive to jar packages. Each method is actually not very convenient, so change your thinking. Since it is more difficult to avoid the problem, try to expose the problem as soon as possible. Compilation errors or startup errors are definitely more reassuring than not knowing when to report errors at runtime. Therefore, according to the fail fast principle, we should ensure that problems are exposed quickly without increasing the cost of communication.
analysis
Since many dependency conflict issues will not report errors during compilation and packaging, you can only try to report errors at startup. Therefore, for a stable component, it is reasonable to do a runtime startup check. In order to be able to check dependencies at runtime, we must find a way to obtain the version number of a package at runtime. How to write the version information in the jar package when packaging, and then read it out? This starts with the loading of JarFile.
Package analysis
But here comes the problem. Just open the Manifest files of several packages. I will take fastjson as an example here:
{
if (specVersion == null || specVersion.length() <1) {
throw new NumberFormatException("Empty version string");
}
String [] sa = specVersion.split("\\.", -1);
int [] si = new int[sa.length];
for (int i = 0; i <sa.length; i++) {
si[i] = Integer.parseInt(sa[i]);
if (si[i] <0)
throw NumberFormatException.forInputString("" + si[i]);
}
String [] da = desired.split("\\.", -1);
int [] di = new int[da.length];
for (int i = 0; i <da.length; i++) {
di[i] = Integer.parseInt(da[i]);
if (di[i] <0)
throw NumberFormatException.forInputString("" + di[i]);
}
int len = Math.max(di.length, si.length);
for (int i = 0; i <len; i++) {
int d = (i <di.length? di[i]: 0);
int s = (i <si.length? si[i]: 0);
if (s <d)
return false;
if (s> d)
return true;
}
return true;
}
https://blog.mythsman.com/post/5d2c0f7667f841464434a36c/ 13/14
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: wenshao
Created-By: Apache Maven 3.5.0
Build-Jdk: 1.8.0_151
We found that this file is very simple and does not have the attributes previously defined. In this way, the package class certainly cannot resolve similar methods. So how do we add this information when packaging?
If it is packaged with gradle, this uses a function of gradle's java plugin. Add a configuration of a packaging command under a given project:
jar.manifest.attributes("Specification-Version": '1.0.0')
In this way, the attribute "Specification-Version" can be added to the packaged jarFile, see the gradle docs in the reference for details.
However, it should be noted that this value must be a number separated by dots and do not add any other characters, otherwise an exception will be thrown when the isCompatibleWith method is called.
So generally speaking, I will configure this way to be compatible with version numbers ending in "-SNAPSHOT":
jar.manifest.attributes("Specification-Version": version.split("-")[0])
Usage analysis
After finishing the package, we can check the version when the component is started happily:
static{
if(!TestMain.class.getPackage().isCompatibleWith("1.2.2")){
//TODO reports an error
}
}
The "1.2.2" can be configured in a configuration center such as Lion or Apollo for unified management.