乐趣区

关于cxf:CXF动态调用WebService时Class缓存的实现

起因

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);
    }
}
退出移动版