1. Background & Introduction
The business monitoring platform is a platform developed by Dewu for data and status verification. It can quickly and easily find dirty data and wrong logic of online business, effectively prevent asset loss and ensure system stability.
Data flow:
The actual work of the filtering and validation steps above is to execute a user-defined Groovy check script. Business monitoring is implemented internally through a module that executes scripts.
This article takes a technical problem of the script execution module as an entry point, and shares the experience of using the ClassLoader isolation technology to achieve script execution isolation.
2. Script debugging process of business monitoring platform
The core execution logic of business monitoring is data verification. Different domains will have different data validation and verification rules. The steps for the initial version user to write a script to debug are as follows:
1. Write a data verification script (under the rules of the business monitoring platform), the script demo:
@Service
public class DubboDemoScript implements DemoScript {
@Resource
private DemoService demoService;
@Override
public boolean filter(JSONObject jsonObject) {
// 这里省略数据过滤逻辑 由业务使用方实现
return true;
}
@Override
public String check(JSONObject jsonObject) {
Long id = jsonObject.getLong("id");
// 数据校验,由业务使用方实现
Response responseResult = demoService.queryById(id);
log.info("[DubboClassloaderTestDemo]返回结果={}", JsonUtils.serialize(responseResult));
return JsonUtils.serialize(responseResult);
}
}
DemoScript is a template interface defined by the business monitoring platform. Different scripts implement this interface and rewrite the filter and check methods. The filter method is used for data filtering, and the check method is used for data verification. The user mainly writes the logic in these two methods.
2. Debug the script on the script debugging page of the business monitoring platform. When there is a third-party team Maven dependency in the script, the business monitoring platform needs to add the Maven dependency in pom.xml and publish it, and then notify the user to debug here.
3. Click Script Debug to view the script debugging results.
4. Save and launch the script.
2.1 Script development and debugging flowchart for business monitoring
If the user wants to debug a script, he needs to inform the platform development, and the platform development manually adds the Maven dependency to the project and goes to the release platform for release. The middle is not only particularly time-consuming and inefficient, but also frequently released, which seriously affects the user experience of the business monitoring platform and increases the maintenance cost of platform development.
To this end, the business monitoring platform uses the Classloader isolation technology in the new version to dynamically load the business side services that depend on the script. Business monitoring does not require special processing (adding Maven dependencies and then publishing), and users can directly upload the JAR file since the script in the control background to complete the debugging, which greatly reduces the use and maintenance costs and improves the user experience.
3. Custom Classloder | Break parental delegation
3.1 What is Classloader
ClassLoader is an abstract class, we use its instance object to load the class, it is responsible for loading the Java bytecode into the JVM and making it part of the JVM. The JVM's class dynamic loading technology can dynamically load or replace some functional modules of the system at runtime without affecting the normal operation of other functional modules of the system. Generally, the class is loaded by reading a class file through the class name.
Class loading is the process of finding the bytecode file of a class or an interface and constructing a class object representing the class or interface by parsing the bytecode. In Java, the class loader loads a class into the Java virtual machine, and it goes through three steps: loading, linking, and initialization.
3.2 Classloader dynamically loads dependent files
Use Classloader to implement class URLClassloader to achieve dynamic loading of dependent files. Sample code:
public class CustomClassLoader extends URLClassLoader {
/**
* @param jarPath jar文件目录地址
* @return
*/
private CustomClassLoader createCustomClassloader(String jarPath) throws MalformedURLException {
File file = new File(jarPath);
URL url = file.toURI().toURL();
List<URL> urlList = Lists.newArrayList(url);
URL[] urls = new URL[urlList.size()];
urls = urlList.toArray(urls);
return new CustomJarClassLoader(urls, classLoader.getParent());
}
public CustomClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
When adding a dependency file, use the addURL method of Classloader to add it dynamically.
If all scripts use the same class loader to load, there will be a problem, the reason: the same class (with the same fully qualified name) will only be loaded once by the class loader (parent delegation). However, there are two cases where two scripts have the same fully qualified name, but the methods or properties are different, so loading one script will cause an error in the checking logic of one of the scripts.
After understanding the above situation, we need to break the Java parent delegation mechanism. Here is a knowledge point: the fully qualified name of a class and the loader that loads the class together form the unique identifier of the class in the JVM , so you need to customize the class loader, so that the script and the classloader correspond one-to-one and are different. Not much to say, go directly to the dry goods:
3.3 Custom class loader
public class CustomClassLoader extends URLClassLoader {
public JarFile jarFile;
public ClassLoader parent;
public CustomClassLoader(URL[] urls, JarFile jarFile, ClassLoader parent) {
super(urls, parent);
this.jarFile = jarFile;
this.parent = parent;
}
public CustomClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
private static String classNameToJarEntry(String name) {
String classPath = name.replaceAll("\\.", "\\/");
return new StringBuilder(classPath).append(".class").toString();
}
/**
* 重写loadClass方法,按照类包路径规则拉进行加载Class到jvm
* @param name 类全限定名
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 这里定义类加载规则,和findClass方法一起组合打破双亲
if (name.startsWith("com.xx") || name.startsWith("com.yyy")) {
return this.findClass(name);
}
return super.loadClass(name, resolve);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = null;
try {
String jarEntryName = classNameToJarEntry(name);
if (jarFile == null) {
return clazz;
}
JarEntry jarEntry = jarFile.getJarEntry(jarEntryName);
if (jarEntry != null) {
InputStream inputStream = jarFile.getInputStream(jarEntry);
byte[] bytes = IOUtils.toByteArray(inputStream);
clazz = defineClass(name, bytes, 0, bytes.length);
}
} catch (IOException e) {
log.info("Custom classloader load calss {} failed", name)
}
return clazz;
}
}
Description: The loadClass and findClass methods of the above custom class loader together achieve the key to destroying the parent delegation mechanism. Among them, the super.loadClass(name, resolve) method is to let its parent loader (the parent loader here is LanuchUrlClassloader) perform class loading when it does not meet the rules of the custom class loader. The custom class loader only pays attention to its own needs. Loaded classes, and cache the corresponding Classloader according to the script dimension.
3.4 Use CustomClassloader for business monitoring
The creation relationship between the script or the debugging script and the Classloader:
One script corresponds to multiple dependent JAR files (JAR files are uploaded to HDFS on the script debugging page), and one script corresponds to one classloader (and is cached locally) (the same two classes are loaded in different classloaders and the latter two Class objects are not equal).
3.5 Implementation of dynamically loading JARs and scripts for business monitoring
In the above operations, I believe that everyone is more concerned about how JAR realizes script loading, and how the attribute DemoService class marked with @Resource annotation in the script creates beans and injects them into the Spring container. Post a flowchart to explain:
The creation source code of FeignClient object is generated in the flowchart:
/**
*
* @param serverName 服务名 (@FeignClient主键中的name值)
* eg:@FeignClient("demo-interfaces")
* @param beanName feign对象名称 eg: DemoFeignClient
* @param targetClass feign的Class对象
* @param <T> FeignClient主键标记的Object
* @return
*/
public static <T> T build(String serverName, String beanName, Class<T> targetClass) {
return buildClient(serverName, beanName, targetClass);
}
private static <T> T buildClient(String serverName, String beanName, Class<T> targetClass) {
T t = (T) BEAN_CACHE.get(serverName + "-" + beanName);
if (Objects.isNull(t)) {
FeignClientBuilder.Builder<T> builder = new FeignClientBuilder(applicationContext).forType(targetClass, serverName);
t = builder.build();
BEAN_CACHE.put(serverName + "-" + beanName, t);
}
return t;
}
The source code for registering Dubbo consumer is generated in the flowchart:
public void registerDubboBean(Class clazz, String beanName) {
// 当前应用配置
ApplicationConfig application = new ApplicationConfig();
application.setName("demo-service");
// 连接注册中心配置
RegistryConfig registry = new RegistryConfig();
registry.setAddress(registryAddress);
// ReferenceConfig为重对象,内部封装了与注册中心的连接,以及与服务提供方的连接
ReferenceConfig reference = new ReferenceConfig<>(); // 此实例很重,封装了与注册中心的连接以及与提供者的连接,请自行缓存,否则可能造成内存和连接泄漏
reference.setApplication(application);
reference.setRegistry(registry); // 多个注册中心可以用setRegistries()
reference.setInterface(clazz);
reference.setVersion("1.0");
// 注意:此代理对象内部封装了所有通讯细节,这里用dubbo2.4版本以后提供的缓存类ReferenceConfigCache
ReferenceConfigCache cache = ReferenceConfigCache.getCache();
Object dubboBean = cache.get(reference);
dubboBeanMap.put(beanName, dubboBean);
// 注册bean
SpringContextUtils.registerBean(beanName, dubboBean);
// 注入bean
SpringContextUtils.autowireBean(dubboBean);
}
The above is the actual application of Classloader isolation technology in the business monitoring platform. Of course, some problems are encountered in the development. Two examples are listed below.
4. Problems & Causes & Solutions
Question 1: If the Check scripts of multiple teams run together, will the Metaspace of a single application take up too much space?
A: With the development of the business and the increasing number of JAR files, the metadata area will indeed occupy too much space, which is also the reason for Classloader isolation. After doing this step, it lays the groundwork for script splitting later, such as deploying applications separately according to application, team and other dimensions to run their corresponding check scripts. In this way, the script and business monitoring logic are also split, which will also reduce the noise caused by the release frequency of the main application.
Question 2: Have you encountered any difficulties in the implementation of Classloader isolation?
Answer: There are some problems encountered in the middle, that is, a class with the same fully qualified name has a CastException exception. This kind of problem is the most likely to occur and the easiest to think of. Reason: The same class was loaded 2 times by 2 different Classloader objects. The solution is also very simple, use the same class loader.
5. Summary
This article explains the implementation of custom Classloader and how to achieve isolation, how to dynamically load JAR files, and how to manually register into Dubbo and Feign services. The class loader dynamically loads the script technology, which is perfect for application in the business monitoring platform. Of course, some business scenarios can also refer to this technology to solve some technical problems.
*Text/Shi Yixin
@德物科技public account
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。