tomcat7中web应用加载原理(二)web.xml解析 -欧洲杯足彩官网

`
tyrion
  • 浏览: 256532 次
  • 性别:
  • 来自: 南京
博主相关
  • 博客
  • 微博
  • 相册
  • 收藏
  • 社区版块
    • ( 0)
    • ( 43)
    • ( 14)
    存档分类
    最新评论

    tomcat7中web应用加载原理(二)web.xml解析

    前一篇文章讲了org.apache.catalina.startup.hostconfig的lifecycleevent方法中所做的事情。最后看到在tomcat启动时或启动后(后台线程定时扫描)会调用hostconfig类的deployapps方法:

        /**
         * deploy applications for any directories or war files that are found
         * in our "application root" directory.
         */
        protected void deployapps() {
            file appbase = appbase();
            file configbase = configbase();
            string[] filteredapppaths = filterapppaths(appbase.list());
            // deploy xml descriptors from configbase
            deploydescriptors(configbase, configbase.list());
            // deploy wars
            deploywars(appbase, filteredapppaths);
            // deploy expanded folders
            deploydirectories(appbase, filteredapppaths);
        }

    可以看到这里部署应用有三种方式:xml文件描述符、war包、文件目录。三种方式部署的总体流程很相似,都是一个web应用分配一个线程来处理,这里统一放到与host内部的线程池对象中(startstopexecutor),所以有时会看到在默认配置下tomcat启动后可能有一个叫“-startstop-”的线程还会运行一段时间才结束。但浏览这三种部署方式的实现代码,里面都是构建一个context对象,并将构建好的context对象与host组件关联起来(即调用host.addchild(context)这句,具体代码在hostconfig类的deploydescriptor(contextname cn, file contextxml)、deploydirectory(contextname cn, file dir)、deploywar(contextname cn, file war)三个方法中,这里不再贴出代码来详细分析)。

    前一篇文章只分析到这步,可以看出与一个web应用相对应的一个context对象已经构建出来了,但如果容器只执行到这里根本无法响应一个浏览器的一次请求。就web服务器的实现来看一次请求过来除了需要根据内部context构建找到这次请求访问的web应用具体所对应的context对象,还需要包含web应用中具体的哪个servlet来处理这次请求,中间是否还需要执行相应的过滤器(filter)、监听器(listener)等,做过java的web开发的都知道,这些信息是配置在一个web应用的web-inf\web.xml文件的(servlet3中已经支持将这些配置信息放到java文件的注解中,但万变不离其宗,总归要在web应用的某个地方说明,并在容器启动时加载,这样才能真正提供web服务,响应请求)。

     

    看到这里可以猜到tomcat容器加载web应用时必定会有对于每个应用的web.xml文件的解析过程,本文就来看看这个解析过程。

     

    在本文开头提到的三种部署应用的实现代码中有一些共通的代码,这里摘出来说明一下:

                class clazz = class.forname(host.getconfigclass());
                lifecyclelistener listener =
                    (lifecyclelistener) clazz.newinstance();
                context.addlifecyclelistener(listener);
                host.addchild(context);

    第一段是在所有context对象构建时会添加一个监听器,这里监听器的类名是standardhost类的实例变量configclass,其默认值就是org.apache.catalina.startup.contextconfig。第二段是将当前构建的context对象添加到父容器host对象中。

    先看下standardhost的addchild方法的实现:

        public void addchild(container child) {
            child.addlifecyclelistener(new memoryleaktrackinglistener());
            if (!(child instanceof context))
                throw new illegalargumentexception
                    (sm.getstring("standardhost.notcontext"));
            super.addchild(child);
        }

    可以看到这段代码最后调用了父类的addchild方法:

        public void addchild(container child) {
            if (globals.is_security_enabled) {
                privilegedaction dp =
                    new privilegedaddchild(child);
                accesscontroller.doprivileged(dp);
            } else {
                addchildinternal(child);
            }
        }

    这里看下addchildinternal方法的实现:

        private void addchildinternal(container child) {
            if( log.isdebugenabled() )
                log.debug("add child "   child   " "   this);
            synchronized(children) {
                if (children.get(child.getname()) != null)
                    throw new illegalargumentexception("addchild:  child name '"  
                                                       child.getname()  
                                                       "' is not unique");
                child.setparent(this);  // may throw iae
                children.put(child.getname(), child);
            }
            // start child
            // don't do this inside sync block - start can be a slow process and
            // locking the children object can cause problems elsewhere
            if ((getstate().isavailable() ||
                    lifecyclestate.starting_prep.equals(getstate())) &&
                    startchildren) {
                try {
                    child.start();
                } catch (lifecycleexception e) {
                    log.error("containerbase.addchild: start: ", e);
                    throw new illegalstateexception
                        ("containerbase.addchild: start: "   e);
                }
            }
            firecontainerevent(add_child_event, child);
        }

    可以看到会调用子容器的start方法,就是指调用standardcontext的start方法。

    即给host对象添加子容器时将会调用子容器的start方法,,调用standardcontext的start方法最终会调用org.apache.catalina.core.standardcontext类的startinternal方法(该方法代码较长,建议自己阅读,不再贴出),这里将会发布一系列事件,按调用前后顺序这些事件包括:before_init_event、after_init_event、before_start_event、configure_start_event、start_event、after_start_event。

     

    前面提到在构建context对象时都会注册一个监听器org.apache.catalina.startup.contextconfig,看下这个类的lifecycleevent方法中(为什么会执行这个方法可以看)监听了哪些事件:

        /**
         * process events for an associated context.
         *
         * @param event the lifecycle event that has occurred
         */
        @override
        public void lifecycleevent(lifecycleevent event) {
            // identify the context we are associated with
            try {
                context = (context) event.getlifecycle();
            } catch (classcastexception e) {
                log.error(sm.getstring("contextconfig.cce", event.getlifecycle()), e);
                return;
            }
            // process the event that has occurred
            if (event.gettype().equals(lifecycle.configure_start_event)) {
                configurestart();
            } else if (event.gettype().equals(lifecycle.before_start_event)) {
                beforestart();
            } else if (event.gettype().equals(lifecycle.after_start_event)) {
                // restore docbase for management tools
                if (originaldocbase != null) {
                    context.setdocbase(originaldocbase);
                }
            } else if (event.gettype().equals(lifecycle.configure_stop_event)) {
                configurestop();
            } else if (event.gettype().equals(lifecycle.after_init_event)) {
                init();
            } else if (event.gettype().equals(lifecycle.after_destroy_event)) {
                destroy();
            }
        }

    与context的start方法调用相关的事件监听前后顺序为:after_init_event(执行init方法)、before_start_event(执行beforestart方法)、configure_start_event(执行configurestart方法)。

    在configurestart方法将直接调用webconfig方法,正是在这个方法中将会解析web.xml文件:

        /**
         * scan the web.xml files that apply to the web application and merge them
         * using the rules defined in the spec. for the global web.xml files,
         * where there is duplicate configuration, the most specific level wins. ie
         * an application's web.xml takes precedence over the host level or global
         * web.xml file.
         */
        protected void webconfig() {
            /*
             * anything and everything can override the global and host defaults.
             * this is implemented in two parts
             * - handle as a web fragment that gets added after everything else so
             *   everything else takes priority
             * - mark servlets as overridable so sci configuration can replace
             *   configuration from the defaults
             */
            /*
             * the rules for annotation scanning are not as clear-cut as one might
             * think. tomcat implements the following process:
             * - as per srv.1.6.2, tomcat will scan for annotations regardless of
             *   which servlet spec version is declared in web.xml. the eg has
             *   confirmed this is the expected behaviour.
             * - as per http://java.net/jira/browse/servlet_spec-36, if the main
             *   web.xml is marked as metadata-complete, jars are still processed
             *   for scis.
             * - if metadata-complete=true and an absolute ordering is specified,
             *   jars excluded from the ordering are also excluded from the sci
             *   processing.
             * - if an sci has a @handlestype annotation then all classes (except
             *   those in jars excluded from an absolute ordering) need to be
             *   scanned to check if they match.
             */
            set defaults = new hashset();
            defaults.add(getdefaultwebxmlfragment());
            webxml webxml = createwebxml();
            // parse context level web.xml
            inputsource contextwebxml = getcontextwebxmlsource();
            parsewebxml(contextwebxml, webxml, false);
            servletcontext scontext = context.getservletcontext();
            // ordering is important here
            // step 1. identify all the jars packaged with the application
            // if the jars have a web-fragment.xml it will be parsed at this
            // point.
            map fragments = processjarsforwebfragments();
            // step 2. order the fragments.
            set orderedfragments = null;
            orderedfragments =
                    webxml.orderwebfragments(webxml, fragments, scontext);
            // step 3. look for servletcontainerinitializer implementations
            if (ok) {
                processservletcontainerinitializers(orderedfragments);
            }
            if  (!webxml.ismetadatacomplete() || typeinitializermap.size() > 0) {
                // step 4. process /web-inf/classes for annotations
                if (ok) {
                    // hack required by eclipse's "serve modules without
                    // publishing" feature since this backs web-inf/classes by
                    // multiple locations rather than one.
                    namingenumeration listbindings = null;
                    try {
                        try {
                            listbindings = context.getresources().listbindings(
                                    "/web-inf/classes");
                        } catch (namenotfoundexception ignore) {
                            // safe to ignore
                        }
                        while (listbindings != null &&
                                listbindings.hasmoreelements()) {
                            binding binding = listbindings.nextelement();
                            if (binding.getobject() instanceof filedircontext) {
                                file webinfclassdir = new file(
                                        ((filedircontext) binding.getobject()).getdocbase());
                                processannotationsfile(webinfclassdir, webxml,
                                        webxml.ismetadatacomplete());
                            } else {
                                string resource =
                                        "/web-inf/classes/"   binding.getname();
                                try {
                                    url url = scontext.getresource(resource);
                                    processannotations);
                                } catch (malformedurlexception e) {
                                    log.error(sm.getstring(
                                            "contextconfig.webinfclassesurl",
                                            resource), e);
                                }
                            }
                        }
                    } catch (namingexception e) {
                        log.error(sm.getstring(
                                "contextconfig.webinfclassesurl",
                                "/web-inf/classes"), e);
                    }
                }
                // step 5. process jars for annotations - only need to process
                // those fragments we are going to use
                if (ok) {
                    processannotations(
                            orderedfragments, webxml.ismetadatacomplete());
                }
                // cache, if used, is no longer required so clear it
                javaclasscache.clear();
            }
            if (!webxml.ismetadatacomplete()) {
                // step 6. merge web-fragment.xml files into the main web.xml
                // file.
                if (ok) {
                    ok = webxml.merge(orderedfragments);
                }
                // step 7. apply global defaults
                // have to merge defaults before jsp conversion since defaults
                // provide jsp servlet definition.
                webxml.merge(defaults);
                // step 8. convert explicitly mentioned jsps to servlets
                if (ok) {
                    convertjsps(webxml);
                }
                // step 9. apply merged web.xml to context
                if (ok) {
                    webxml.configurecontext(context);
                }
            } else {
                webxml.merge(defaults);
                convertjsps(webxml);
                webxml.configurecontext(context);
            }
            // step 9a. make the merged web.xml available to other
            // components, specifically jasper, to save those components
            // from having to re-generate it.
            // todo use a servletcontainerinitializer for jasper
            string mergedwebxml = webxml.toxml();
            scontext.setattribute(
                   org.apache.tomcat.util.scan.constants.merged_web_xml,
                   mergedwebxml);
            if (context.getlogeffectivewebxml()) {
                log.info("web.xml:\n"   mergedwebxml);
            }
            // always need to look for static resources
            // step 10. look for static resources packaged in jars
            if (ok) {
                // spec does not define an order.
                // use ordered jars followed by remaining jars
                set resourcejars = new linkedhashset();
                if (orderedfragments != null) {
                    for (webxml fragment : orderedfragments) {
                        resourcejars.add(fragment);
                    }
                }
                for (webxml fragment : fragments.values()) {
                    if (!resourcejars.contains(fragment)) {
                        resourcejars.add(fragment);
                    }
                }
                processresourcejars(resourcejars);
                // see also standardcontext.resourcesstart() for
                // web-inf/classes/meta-inf/resources configuration
            }
            // step 11. apply the servletcontainerinitializer config to the
            // context
            if (ok) {
                for (map.entry>> entry :
                            initializerclassmap.entryset()) {
                    if (entry.getvalue().isempty()) {
                        context.addservletcontainerinitializer(
                                entry.getkey(), null);
                    } else {
                        context.addservletcontainerinitializer(
                                entry.getkey(), entry.getvalue());
                    }
                }
            }
        }

    这个方法里面做的事情,在英文注释中说的很清楚了,概括起来包括合并tomcat全局web.xml、当前应用中的web.xml、web-fragment.xml和web应用的注解中的配置信息,并将解析出的各种配置信息(如servlet配置、filter配置等)关联到context对象中(在上面的代码第140行:webxml.configurecontext(context))。

    看下configurecontext方法:

        /**
         * configure a {@link context} using the stored web.xml representation.
         *
         * @param context   the context to be configured
         */
        public void configurecontext(context context) {
            // as far as possible, process in alphabetical order so it is easy to
            // check everything is present
            // some validation depends on correct public id
            context.setpublicid(publicid);
            // everything else in order
            context.seteffectivemajorversion(getmajorversion());
            context.seteffectiveminorversion(getminorversion());
            for (entry entry : contextparams.entryset()) {
                context.addparameter(entry.getkey(), entry.getvalue());
            }
            context.setdisplayname(displayname);
            context.setdistributable(distributable);
            for (contextlocalejb ejblocalref : ejblocalrefs.values()) {
                context.getnamingresources().addlocalejb(ejblocalref);
            }
            for (contextejb ejbref : ejbrefs.values()) {
                context.getnamingresources().addejb(ejbref);
            }
            for (contextenvironment environment : enventries.values()) {
                context.getnamingresources().addenvironment(environment);
            }
            for (errorpage errorpage : errorpages.values()) {
                context.adderrorpage(errorpage);
            }
            for (filterdef filter : filters.values()) {
                if (filter.getasyncsupported() == null) {
                    filter.setasyncsupported("false");
                }
                context.addfilterdef(filter);
            }
            for (filtermap filtermap : filtermaps) {
                context.addfiltermap(filtermap);
            }
            for (jsppropertygroup jsppropertygroup : jsppropertygroups) {
                jsppropertygroupdescriptor descriptor =
                    new applicationjsppropertygroupdescriptor(jsppropertygroup);
                context.getjspconfigdescriptor().getjsppropertygroups().add(
                        descriptor);
            }
            for (string listener : listeners) {
                context.addapplicationlistener(
                        new applicationlistener(listener, false));
            }
            for (entry entry : localeencodingmappings.entryset()) {
                context.addlocaleencodingmappingparameter(entry.getkey(),
                        entry.getvalue());
            }
            // prevents iae
            if (loginconfig != null) {
                context.setloginconfig(loginconfig);
            }
            for (messagedestinationref mdr : messagedestinationrefs.values()) {
                context.getnamingresources().addmessagedestinationref(mdr);
            }
            // messagedestinations were ignored in tomcat 6, so ignore here
            context.setignoreannotations(metadatacomplete);
            for (entry entry : mimemappings.entryset()) {
                context.addmimemapping(entry.getkey(), entry.getvalue());
            }
            // name is just used for ordering
            for (contextresourceenvref resource : resourceenvrefs.values()) {
                context.getnamingresources().addresourceenvref(resource);
            }
            for (contextresource resource : resourcerefs.values()) {
                context.getnamingresources().addresource(resource);
            }
            for (securityconstraint constraint : securityconstraints) {
                context.addconstraint(constraint);
            }
            for (string role : securityroles) {
                context.addsecurityrole(role);
            }
            for (contextservice service : servicerefs.values()) {
                context.getnamingresources().addservice(service);
            }
            for (servletdef servlet : servlets.values()) {
                wrapper wrapper = context.createwrapper();
                // description is ignored
                // display name is ignored
                // icons are ignored
                // jsp-file gets passed to the jsp servlet as an init-param
                if (servlet.getloadonstartup() != null) {
                    wrapper.setloadonstartup(servlet.getloadonstartup().intvalue());
                }
                if (servlet.getenabled() != null) {
                    wrapper.setenabled(servlet.getenabled().booleanvalue());
                }
                wrapper.setname(servlet.getservletname());
                map params = servlet.getparametermap();
                for (entry entry : params.entryset()) {
                    wrapper.addinitparameter(entry.getkey(), entry.getvalue());
                }
                wrapper.setrunas(servlet.getrunas());
                set rolerefs = servlet.getsecurityrolerefs();
                for (securityroleref roleref : rolerefs) {
                    wrapper.addsecurityreference(
                            roleref.getname(), roleref.getlink());
                }
                wrapper.setservletclass(servlet.getservletclass());
                multipartdef multipartdef = servlet.getmultipartdef();
                if (multipartdef != null) {
                    if (multipartdef.getmaxfilesize() != null &&
                            multipartdef.getmaxrequestsize()!= null &&
                            multipartdef.getfilesizethreshold() != null) {
                        wrapper.setmultipartconfigelement(new multipartconfigelement(
                                multipartdef.getlocation(),
                                long.parselong(multipartdef.getmaxfilesize()),
                                long.parselong(multipartdef.getmaxrequestsize()),
                                integer.parseint(
                                        multipartdef.getfilesizethreshold())));
                    } else {
                        wrapper.setmultipartconfigelement(new multipartconfigelement(
                                multipartdef.getlocation()));
                    }
                }
                if (servlet.getasyncsupported() != null) {
                    wrapper.setasyncsupported(
                            servlet.getasyncsupported().booleanvalue());
                }
                wrapper.setoverridable(servlet.isoverridable());
                context.addchild(wrapper);
            }
            for (entry entry : servletmappings.entryset()) {
                context.addservletmapping(entry.getkey(), entry.getvalue());
            }
            if (sessionconfig != null) {
                if (sessionconfig.getsessiontimeout() != null) {
                    context.setsessiontimeout(
                            sessionconfig.getsessiontimeout().intvalue());
                }
                sessioncookieconfig scc =
                    context.getservletcontext().getsessioncookieconfig();
                scc.setname(sessionconfig.getcookiename());
                scc.setdomain(sessionconfig.getcookiedomain());
                scc.setpath(sessionconfig.getcookiepath());
                scc.setcomment(sessionconfig.getcookiecomment());
                if (sessionconfig.getcookiehttponly() != null) {
                    scc.sethttponly(sessionconfig.getcookiehttponly().booleanvalue());
                }
                if (sessionconfig.getcookiesecure() != null) {
                    scc.setsecure(sessionconfig.getcookiesecure().booleanvalue());
                }
                if (sessionconfig.getcookiemaxage() != null) {
                    scc.setmaxage(sessionconfig.getcookiemaxage().intvalue());
                }
                if (sessionconfig.getsessiontrackingmodes().size() > 0) {
                    context.getservletcontext().setsessiontrackingmodes(
                            sessionconfig.getsessiontrackingmodes());
                }
            }
            for (entry entry : taglibs.entryset()) {
                taglibdescriptor descriptor = new applicationtaglibdescriptor(
                        entry.getvalue(), entry.getkey());
                context.getjspconfigdescriptor().gettaglibs().add(descriptor);
            }
            // context doesn't use version directly
            for (string welcomefile : welcomefiles) {
                /*
                 * the following will result in a welcome file of "" so don't add
                 * that to the context
                 * 
                 *   
                 * 
                 */
                if (welcomefile != null && welcomefile.length() > 0) {
                    context.addwelcomefile(welcomefile);
                }
            }
            // do this last as it depends on servlets
            for (jsppropertygroup jsppropertygroup : jsppropertygroups) {
                string jspservletname = context.findservletmapping("*.jsp");
                if (jspservletname == null) {
                    jspservletname = "jsp";
                }
                if (context.findchild(jspservletname) != null) {
                    for (string urlpattern : jsppropertygroup.geturlpatterns()) {
                        context.addservletmapping(urlpattern, jspservletname, true);
                    }
                } else {
                    if(log.isdebugenabled()) {
                        for (string urlpattern : jsppropertygroup.geturlpatterns()) {
                            log.debug("skiping "   urlpattern   " , no servlet "  
                                    jspservletname);
                        }
                    }
                }
            }
            for (entry entry : postconstructmethods.entryset()) {
                context.addpostconstructmethod(entry.getkey(), entry.getvalue());
            }
            for (entry entry : predestroymethods.entryset()) {
                context.addpredestroymethod(entry.getkey(), entry.getvalue());
            }
        }

    可以看到里面对context调用了各种set、add方法,从而将web.xml中的各种配置信息与表示一个web应用的context对象关联起来。

    3
    2
    分享到:
    |
    评论
    5 楼 tyrion 2017-03-24  
    ps:这里的博客已经不更新了,新博客在简书:http://www.jianshu.com/u/6c5d73c5b4cb
    tomcat的源码分析是几年前搞的了,现在精力主要focus在微服务的相关技术实践上。
    4 楼 tyrion 2017-03-24  
    小帅1127 写道
    确实难得得好文 就是不知道作者现在还从不从事软件开发了

    啊好久不登javaeye了,刚看到,我从毕业到现在一直在coding。。。
    3 楼 2017-03-02  
    确实难得得好文 就是不知道作者现在还从不从事软件开发了
    2 楼 2016-04-03  
    非常感谢楼主!终于对容器解析web.xml的过程有了一点点了解了!!
    1 楼 2013-09-19  
    wonderful, come on baby.

    相关推荐

      本书共分4部分,从xml、servlet、jsp和应用的角度向读者展示了java web开发中各种技术的应用,循序渐进地引导读者快速掌握java web开发。.  本书内容全面,涵盖了从事java web开发所应掌握的所有知识。在知识的讲解...

      本书共分4部分,从xml、servlet、jsp和应用的角度向读者展示了java web开发中各种技术的应用,循序渐进地引导读者快速掌握java web开发。.  本书内容全面,涵盖了从事java web开发所应掌握的所有知识。在知识的讲解...

      本书共分4部分,从xml、servlet、jsp和应用的角度向读者展示了java web开发中各种技术的应用,循序渐进地引导读者快速掌握java web开发。.  本书内容全面,涵盖了从事java web开发所应掌握的所有知识。在知识的讲解...

      本书共分4部分,从xml、servlet、jsp和应用的角度向读者展示了java web开发中各种技术的应用,循序渐进地引导读者快速掌握java web开发。.  本书内容全面,涵盖了从事java web开发所应掌握的所有知识。在知识的讲解...

      1. 当服务器接受到客户端浏览器的请求后,会解析请求url路径,获取访问的servlet的资源路径。在上图 的url中,获取的资源...4. tomcat会将字节码文件加载进内存,并且创建其对象 5. 调用其方法(主要调用service方法)

      项目使用springboot启动一个web项目,在启动阶段看到console中出现了异常“1.10.3-1.4.3\hdf5.jar 系统找不到指定的文件”,虽然这些异常不影响项目的正常运行,但作为一个严谨的技术人员,看到这些异常就像见到...

      1.tomcat axis的安装配置 首先机子上应该安装jdk1.5版本以上(带有xml解析包)。我这里是1.5.0.6 从apache的官方网站(www.apache.org)下载tomcat安装软件。下载版本要在4.0以上。这里用的是5.0版本。安装完后,...

      一. java 基础部分............................................................................................................43、java 中的异常处理机制的简单原理和应用。 .....................................

      1. 目录 1. 2. 目录 .........................................................................................................................................................1 jvm ........................

      84.2. 我们在web应用开发过程中经常遇到输出某种编码的字符,如iso8859-1等,如何输出一个某种编码的字符串? 106 84.3. 设计4个线程,其中两个线程每次对j增加1,另外两个线程对j每次减少1。写出程序。以下程序使用...

      14.1 认识xml解析技术 14.1.1 什么是xml 14.1.2 xml的处理技术 14.2 xml处理利器:xstream 14.2.1 xstream概述 14.2.2 快速入门 14.2.3 使用xstream别名 14.2.4 xstream转换器 14.2.5 xstream注解 14.2.6 流化对象 ...

      用gridview来呈现,从自建的tomcat上得到的图片.android端得到服务器下发的xml文件数据,解析后得到图片路径,然后根据图片路径下载保存在tomcat服务器上的图片.点击图片后,可以保存到sd卡的指定位置.

      blister是一个用于操作苹果二进制plist文件格式的java开源类库(可用于发送数据给ios应用程序)。 重复文件检查工具 finddup.tar finddup 是一个简单易用的工具,用来检查计算机上重复的文件。 openid的java客户端...

      14.1 认识xml解析技术 14.1.1 什么是xml 14.1.2 xml的处理技术 14.2 xml处理利器:xstream 14.2.1 xstream概述 14.2.2 快速入门 14.2.3 使用xstream别名 14.2.4 xstream转换器 14.2.5 xstream注解 14.2.6 流化对象 ...

      blister是一个用于操作苹果二进制plist文件格式的java开源类库(可用于发送数据给ios应用程序)。 重复文件检查工具 finddup.tar finddup 是一个简单易用的工具,用来检查计算机上重复的文件。 openid的java客户端...

      blister是一个用于操作苹果二进制plist文件格式的java开源类库(可用于发送数据给ios应用程序)。 重复文件检查工具 finddup.tar finddup 是一个简单易用的工具,用来检查计算机上重复的文件。 openid的java客户端...

      blister是一个用于操作苹果二进制plist文件格式的java开源类库(可用于发送数据给ios应用程序)。 重复文件检查工具 finddup.tar finddup 是一个简单易用的工具,用来检查计算机上重复的文件。 openid的java客户端...

      blister是一个用于操作苹果二进制plist文件格式的java开源类库(可用于发送数据给ios应用程序)。 重复文件检查工具 finddup.tar finddup 是一个简单易用的工具,用来检查计算机上重复的文件。 openid的java客户端...

    global site tag (gtag.js) - google analytics
    网站地图