起因

CXF 2.7版本和最新的3.5版本,当使用 动态调用WebService功能时,如果返回值结合使用Jackson转json功能,
那么存在 非堆内存泄漏问题。
详情参见:一波三折!记一次非堆内存泄漏(CXF+Jackson)的排查

泄漏原因

CXF动态调用WebService,一般包含以下步骤:

  1. 下载WSDL文件
  2. 将WSDL文件解析成Java代码模型
  3. 生成Java源码
  4. 编译源码->Class文件
  5. 创建ClassLoader
  6. 加载Class
  7. 创建数据绑定&类型初始化
  8. 使用这些Class完成WebService交互
  9. 废弃这些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);
    }
}

sswhsz
168 声望4 粉丝