起因
CXF 2.7 版本和最新的 3.5 版本,当应用 动静调用 WebService 性能时,如果返回值联合应用 Jackson 转 json 性能,
那么存在 非堆内存透露问题。
详情参见:一波三折!记一次非堆内存透露 (CXF+Jackson) 的排查
透露起因
CXF 动静调用 WebService,个别蕴含以下步骤:
- 下载 WSDL 文件
- 将 WSDL 文件解析成 Java 代码模型
- 生成 Java 源码
- 编译源码 ->Class 文件
- 创立 ClassLoader
- 加载 Class
- 创立数据绑定 & 类型初始化
- 应用这些 Class 实现 WebService 交互
- 废除这些 Class,期待 JVM 闲暇时 GC 回收
然而,如果在对 WebService 返回值进行解决时,应用了 Jackson 的 toJson 性能,那么 Jackson 会持有 CXF 动静生成的 Class,导致每次动静产生的 Class 无奈回收,Class 数量一直增长,最终导致 非堆内存溢出。
解决办法
解决办法有很多种,咱们这里提供将 webservice 动静调用时产生的 class/classloader 缓存起来复用,一则解决 class 透露问题,二则因为缩小了每次 wsdl 文件解析、java 代码生成、java 代码编译、class 加载的过程,对 webservice 动静调用还有一些性能上的晋升。当然,为了避免 wsdl 变动引起缓存变成脏数据的问题,能够通过对 wsdl 的 MD5 来判断缓存有效性。
因为不确定 CXF 的 ClientImpl 对象是否为重型对象,不确定此对象是否适合进行缓存应用、以及在并发场合下应用,革新计划的次要缓存内容为 ClassLoader。
参考实现
/**
* 动静调用 webservice 缓存内容
*
* @author sswhsz
* @CreateDate 2022-11-18
*/
public class CacheItem {
/**
* http://*?wsdl、或者 file:/*.wsdl 目前只起备用阐明作用,不作为唯一性辨认
*/
private String wsdlLocation;
/**
* wsdl 文档的 MD5,用于 wsdl 的唯一性判断
*/
private String wsdlMD5;
/**
* wsdl 文档的长度,疾速判断用
*/
private int wsdlSize;
/**
* wsdl 解析的 Java 模型(仅办法原型、不含实现代码)
*/
private JavaModel javaModel;
private S2JJAXBModel intermediateModel;
private JAXBDataBinding databinding;
/**
* wsdl 解析的定义文档 (因为不缓存 Client,每次创立 Client 时均须要解析 definition 对象)
*/
private Definition definition;
/**
* 已加载 webservice class 的 classLoader,缓存备用;*
* 另外,为了避免此 classLoader 应用平台 bundle 作为 parent,从而阻止平台 bundle 的卸载 / 更新;*
* 动静生成的 classloader 将应用 零碎 classloader 作为 parent(ClassLoader.getSystemClassLoader)*/
private ClassLoader classLoader;
//getter、setter 略
}
/**
* 反对 WebService 动静调用缓存
*
* @author sswhsz
* @CreateDate 2022-11-18
*/
public class CacheDynamicClientFactory extends DynamicClientFactory {protected CacheDynamicClientFactory(Bus bus) {super(bus);
}
/**
*
* @param wsdlUrl
* wsdl 本地文件门路,例如:tempFile.getAbsolutePath()
* @param cacheItem
* 缓存内容,如果是第一次创立 classloader/definition,创立的内容将写入 cacheItem,否则从 cacheItem 读取
* @return
*/
public Client createClientUseCache(String wsdlUrl, CacheItem cacheItem) {LOG.log(Level.FINE, "Creating client from WSDL" + wsdlUrl);
Definition definition = cacheItem.getDefinition();
if (definition == null) {definition = createDefinition(wsdlUrl);
cacheItem.setDefinition(definition);
}
Service svc = createService(definition);
ClientImpl client = new ClientImpl(bus, svc, /*port*/null, getEndpointImplFactory());
ClassLoader classLoader = cacheItem.getClassLoader();
if (classLoader == null) {
// our hashcode + timestamp ought to be enough.
String stem = toString() + "-" + System.currentTimeMillis();
File src = new File(tmpdir, stem + "-src");
File classes = new File(tmpdir, stem + "-classes");
//1、生成 java 代码
Object[] models = generateJavaCode(wsdlUrl, svc, /*bindingFiles*/null, src);
JCodeModel codeModel = (JCodeModel) models[0];
S2JJAXBModel intermediateModel = (S2JJAXBModel) models[1];
//2. 编译
compilerJavaCode(src, classes);
//3. 创立 classloader
classLoader = createClassLoader(classes);
//4. 创立数据绑定 & 加载 class
JAXBDataBinding databinding = createDataBinding(codeModel, classLoader);
svc.setDataBinding(databinding);
//5. 类型初始化? 原始代码照搬 --> 没太看懂
typeClassInit(classLoader, client, intermediateModel);
// delete the classes files
FileUtils.removeDir(classes);
cacheItem.setIntermediateModel(intermediateModel);
cacheItem.setDatabinding(databinding);
cacheItem.setClassLoader(classLoader);
}
else {JAXBDataBinding databinding = cacheItem.getDatabinding();
svc.setDataBinding(databinding);
S2JJAXBModel intermediateModel = cacheItem.getIntermediateModel();
typeClassInit(classLoader, client, intermediateModel);
}
// 设置上下文 ClassLoader 的目标是为了后续 invoke 办法时,从上下文中获取加载的 class
// 但此处 CXF 设计不佳。更正当的做法是 将 classloader 存储在 ClientImpl 上,在 invoke 办法前后 try{设置} finally {勾销设置}
ClassLoaderUtils.setThreadContextClassloader(classLoader);
return client;
}
/**
* java 代码生成
*
* @param src
* 生成 Java 代码的根目录
* @return Object[] 返回元素 2:[0]=JCodeModel, [1]=intermediateModel
*/
protected Object[] generateJavaCode(String wsdlUrl, Service svc, List<String> bindingFiles, File src) {
//all SI's should have the same schemas
SchemaCollection schemas = svc.getServiceInfos().get(0).getXmlSchemaCollection();
SchemaCompiler compiler = JAXBUtils.createSchemaCompilerWithDefaultAllocator(new HashSet<String>());
InnerErrorListener listener = new InnerErrorListener(wsdlUrl);
Object elForRun = ReflectionInvokationHandler.createProxyWrapper(listener,
JAXBUtils.getParamClass(compiler, "setErrorListener"));
compiler.setErrorListener(elForRun);
OASISCatalogManager catalog = bus.getExtension(OASISCatalogManager.class);
hackInNewInternalizationLogic(compiler, catalog);
addSchemas(compiler.getOptions(), compiler, svc.getServiceInfos(), schemas);
addBindingFiles(bindingFiles, compiler);
S2JJAXBModel intermediateModel = compiler.bind();
listener.throwException();
JCodeModel codeModel = intermediateModel.generateCode(null, elForRun);
if (!src.mkdir()) {throw new IllegalStateException("Unable to create working directory" + src.getPath());
}
try {Object writer = JAXBUtils.createFileCodeWriter(src);
codeModel.build(writer);
}
catch (Exception e) {throw new IllegalStateException("Unable to write generated Java files for schemas:" + e.getMessage(), e);
}
return new Object[] { codeModel, intermediateModel};
}
protected void compilerJavaCode(File src, File classes) {if (!classes.mkdir()) {throw new IllegalStateException("Unable to create working directory" + classes.getPath());
}
StringBuilder classPath = new StringBuilder();
try {ClassLoader parent = ClassLoader.getSystemClassLoader();
setupClasspath(classPath, parent);
}
catch (Exception ex) {throw new RuntimeException(ex);
}
List<File> srcFiles = FileUtils.getFilesRecurse(src, ".+\\.java$");
if (!compileJavaSrc(classPath.toString(), srcFiles, classes.toString())) {LOG.log(Level.SEVERE, new Message("COULD_NOT_COMPILE_SRC", LOG, src).toString());
}
FileUtils.removeDir(src);
}
protected ClassLoader createClassLoader(File classes) {URL[] urls = null;
try {urls = new URL[] {classes.toURI().toURL()};
}
catch (MalformedURLException mue) {throw new IllegalStateException("Internal error; a directory returns a malformed URL:" + mue.getMessage(),
mue);
}
// 留神:parentClassLoader 应用 system class loader,防止无心中阻止 bundle class loader 的卸载和更新
ClassLoader parent = ClassLoader.getSystemClassLoader();
ClassLoader cl = ClassLoaderUtils.getURLClassLoader(urls, parent);
return cl;
}
protected void typeClassInit(ClassLoader classLoader, ClientImpl client, S2JJAXBModel intermediateModel) {
//5. 应用线程上下文 ClassLoader 加载 class
ClassLoaderHolder holder = ClassLoaderUtils.setThreadContextClassloader(classLoader);
try {ServiceInfo svcfo = client.getEndpoint().getEndpointInfo().getService();
TypeClassInitializer visitor = new TypeClassInitializer(svcfo, intermediateModel, allowWrapperOps());
visitor.walk();}
finally {holder.reset();
}
}
protected JAXBDataBinding createDataBinding(JCodeModel codeModel, ClassLoader cl) {
JAXBContext context;
Map<String, Object> contextProperties = jaxbContextProperties;
if (contextProperties == null) {contextProperties = Collections.emptyMap();
}
StringBuilder sb = new StringBuilder();
boolean firstnt = false;
for (Iterator<JPackage> packages = codeModel.packages(); packages.hasNext();) {JPackage jpackage = packages.next();
if (!isValidPackage(jpackage)) {continue;}
if (firstnt) {sb.append(':');
}
else {firstnt = true;}
sb.append(jpackage.name());
}
JAXBUtils.logGeneratedClassNames(LOG, codeModel);
String packageList = sb.toString();
try {if (StringUtils.isEmpty(packageList)) {context = JAXBContext.newInstance(new Class[0], contextProperties);
}
else {context = JAXBContext.newInstance(packageList, cl, contextProperties);
}
}
catch (JAXBException jbe) {throw new IllegalStateException("Unable to create JAXBContext for generated packages:" + jbe.getMessage(),
jbe);
}
JAXBDataBinding databinding = new JAXBDataBinding();
databinding.setContext(context);
return databinding;
}
protected Definition createDefinition(String wsdlUrl) {
try {
// use wsdl manager to parse wsdl or get cached definition
Definition definition = bus.getExtension(WSDLManager.class).getDefinition(wsdlUrl);
return definition;
}
catch (WSDLException ex) {throw new ServiceConstructionException(new Message("SERVICE_CREATION_MSG", LOG), ex);
}
}
protected Service createService(Definition definition) {WSDLServiceFactory sf = new WSDLServiceFactory(bus, definition);
sf.setAllowElementRefs(allowRefs);
Service svc = sf.create();
return svc;
}
public static CacheDynamicClientFactory newInstance() {Bus bus = CXFBusFactory.getThreadDefaultBus();
return new CacheDynamicClientFactory(bus);
}
}