dom4j实战(二)——使用dom4j设计Openfire式导航菜单

  通过上一篇文章( dom4j实战(一)——使用dom4j从XML中读取数据源配置 ),使我们对dom4j有了一些初步的认识和了解,也掌握了基本的操作方法,本文将承接前篇,借鉴Openfire项目中菜单设计的优点,结合我们自己的需求,通过一个小实例,来说明一下如何使用dom4j来实现这个功能。
     可以在 http://www.igniterealtime.org/downloads/index.jsp 这里下载到Openfire的发布版和源码版,Openfire的介绍在这里不作描述,网上相关的资料有很多,有兴趣的朋友可以下载源码研究一下,其实不了解Openfire也没关系,因为我们只是借鉴其中的一些思想,等做完了实例,就会有一个比较直观的认识。
    大象建议先在最后面下载源码和必要的JAR包,让程序运行起来看下效果,再看下面的详细说明。
    开发环境:Eclipse 3.2.1  MyEclipse 5.10GA  Tomcat 6.10  
             dom4j-1.6.1.jar  jaxen-1.1-beta-7.jar  sitemesh-2.2.1.jar
     1 、创建tag-console.xml
       在src目录下建一个tag-console.xml
文件,这个配置文件中写的是菜单信息,内容如下:
<? xml version="1.0" encoding="GBK" ?>
< bookstore >
    
< global >
        
< appname > 菜单导航demo </ appname >
        
< version > ver 1.0 </ version >
        
< creator > 菠萝大象 </ creator >
    
</ global >
    
< catalog  id ="catalog-program"  name ="编程开发"  url ="index.jsp"  description ="编程开发" >     
        
< item  id ="item-program-java"  name ="Java开发"  url ="index.jsp"  description ="Java开发" >
            
< book  id ="695043"  name ="Struts2 深入详解"  url ="index.jsp"  description ="Struts2 深入详解" />
            
< book  id ="691254"  name ="Ant整合开发"  url ="book_ant.jsp"  description ="Ant整合开发" />
            
< book  id ="693668"  name ="Java编程思想"  url ="book_java.jsp"  description ="Java编程思想" />
        
</ item >
        
< item  id ="item-program-database"  name ="数据库开发"  url ="book_oracle9.jsp"  description ="数据库开发" >
            
< book  id ="691245"  name ="Oracle 9i数据库宝典"  url ="book_oracle9.jsp"  description ="Oracle 9i数据库宝典" />
            
< book  id ="693254"  name ="SQL Server 2005应用开发"  url ="book_sqlserver.jsp"  description ="SQL Server 2005应用开发" />
            
< book  id ="690215"  name ="Oracle 10g高级开发"  url ="book_oracle10.jsp"  description ="Oracle 10g高级开发" />
        
</ item >
    
</ catalog >
    
< catalog  id ="catalog-system"  name ="系统相关"  url ="book_vista.jsp"  description ="系统相关" >
        
< item  id ="item-system-windows"  name ="Windows系统"  url ="book_vista.jsp"  description ="Windows系统" >
            
< book  id ="691258"  name ="Windows Vista入门"  url ="book_vista.jsp"  description ="Windows Vista入门" />
            
< book  id ="695489"  name ="Windows注册表实战"  url ="book_windows.jsp"  description ="Windows注册表实战" />
        
</ item >
        
< item  id ="item-system-linux"  name ="Linux系统"  url ="book_linux9.jsp"  description ="Linux系统" >
            
< book  id ="696598"  name ="Linux 9.0详解"  url ="book_linux9.jsp"  description ="Linux 9.0详解" />
            
< book  id ="694585"  name ="Linux宝典"  url ="book_linux.jsp"  description ="Linux宝典" />
        
</ item >
    
</ catalog >
</ bookstore >
        上面XML里面的东西我是随便写的 ,大家千万不要较真,我用图书来做菜单一是方便大家理解,另一个是简化程序,其实 Openfire 的服务器端是一个后台管理系统,它是基于 XMPP(可扩展消息处理现场协议)开发的,XMPP贯穿整个系统设计,如果你想用它的控制台框架,但又不想用XMPP,请先从网页入口开始,结合页面仔细分析代码,把需要的部分抽取出来就行了,其它的不用去管。大象没有研究过XMPP,只是抽取了控制台框架,对Openfire的源代码也没能深入的研究,最主要还是E文太烂了。^_^
       Openfire没有采用现在很流行的技术架构(SSH),只使用JSP+JavaBean,但是它有自己的系统设计,就连日志都是自己做的,没有使用我们熟悉的log4j,真的是太佩服鸟~~~~
     2、创建ResourceManage.java
       在util包下创建ResourceManage类,这个类主要是用来读取tag-console.xml文件,并取得文件中的基本信息,以及查找元素等操作。
       我们先在 Constant 接口中,增加一个字符串常量: String TAG_CONFIG = "tag-console.xml"
       ResourceManage 前面加载资源的部分和上一篇,后来修改过的DataBaseConnect类一样,只需把Constant.DB_CONFIG换成Constant.TAG_CONFIG就行了。 接下来,在类中加入几个读取XML中基本信息的方法:
     < global >
        
< appname > 菜单导航demo </ appname >
        
< version > ver 1.0 </ version >
        
< creator > 菠萝大象 </ creator >
    
</ global >
        这里只举出取得appname元素值的方法,其它的几个都很相似,请查看源代码。
     /**
     * 得到应用程序名称
     
*/
    
public   static  String getAppName(){
        Element appName 
=  (Element) coreModel.selectSingleNode( " //bookstore/global/appname " );
        
if (appName != null ){
            
return  appName.getText();
        }
else {
            
return   null ;
        }
    }
        根据id属性值查找对应的元素:
     /**
     * 在整个文档节点中查找id属性值为传入id的元素对象
     * 
@param  id 待查找的id属性值
     * 
@return  返回找到的元素对象
     
*/
    
public   static  Element getSingleElementById(String id){
        
return  (Element)coreModel.selectSingleNode( " //*[@id=' " + id + " '] " );
    }
        这里用到了XPATH语法,根据传入的id值,在整个文档中查找id属性值与此一致的元素对象。用下面的代码举例说明:
    < book  id ="695043"  name ="Struts2 深入详解"  url ="index.jsp"  description ="Struts2 深入详解" />
        当传入的 id 属性值为 "695043" 时,那么我们就会得到对应这个 id 值的 book 元素对象, id 属性值在整个配置文件中就是一个 key 关键字,起到定位的作用。
        根据 id 属性值查找上下文中对应的 catalog 元素
     /**
     * 根据传入的id查找上下文中对应的catalog元素
     * 
@param  id 待查找的id属性值
     * 
@return  返回id值所在的catalog元素对象
     
*/
    
public   static  Element getElementByID(String id) {
        
return (Element) coreModel.selectSingleNode("//*[@id='" + id
                + "']/ancestor::catalog");

    }
        ancestor 是XPATH语法中轴的概念,我引用网上官方文档中的说明:“ ancestor 轴(axis)包含上下节点的祖先节点,该祖先节点由其上下文节点的父节点以及父节点的父节点等等诸如此类的节点构成,所ancestor轴总是包含有根节点,除非上下文节点就是根节点本身。 ”这句话的意思其实就是向上查找节点,直到找到根节点为止。对于 ancestor::catalog 来说,就是向上查找直到 catalog 节点为止。所以 getElementByID 这个方法是根据传入的 id 属性值在上下文中查找节点,直到找到这个id值所在的上下文catalog节点为止。 当传入的 id 属性值为 "695043" 时,我们会得到 id = "catalog-program" 这个 catalog 节点元素,而不会得到 id = "catalog-system" 这个 catalog 节点元素。这样说大家大概能明白是什么意思了吧?
        可以去这个网站看下 XPATH 教程: http://www.zvon.org/xxl/XPathTutorial/General_chi/examples.html
    3、自定义标签  
       采用自定义标签的方式来生成菜单,借助ResourceManage类取出XML文件中的信息,将这些内容装载到标签体中,然后在JSP页面中呈现出来。
       1、主菜单标签
          主菜单有两个,catalog元素这一层表示主菜单。标签的实现类如下:
          MainTag.java
          建com.demo.tag包,在tag包下创建MainTag类,继承 javax.servlet.jsp.tagext.BodyTagSupport 类,主要的部分代码如下,完整代码请下载源码包查看。
           这些属性与 demo.tld 中的属性对应,每个属性都有 setter getter 方法。
     private  String css;  // 菜单的CSS样式
     private  String currentcss;  // 当前选中菜单的CSS样式

         doStartTag()方法是遇到标签开始时调用的方法,EVAL_BODY_BUFFERED表示创建一个缓冲流,将标签体的内容保存到BodyContent对象中,可以对其内容进行修改。BodyContent继承了javax.servlet.jsp.JspWriter类,BodyContent对象的内容不自动写入servlet的输出流,而是放在一个字符流缓存中。当标签体完成后其对象仍可在doEndTag()方法中应用,由getString()或getReader()方法操作。并在必要时修改及写入恢复的JspWriter输出流。EVAL_BODY_INCLUDE表示将显示标签间的文字。另一个返回值是SKIP_BODY,它表示不显示标签间的文字。

     public   int  doStartTag()  throws  JspException {
        
return  EVAL_BODY_BUFFERED;  // 创建保存到BodyContent对象中的缓冲流
    }
          doEndTag() 方法是遇到标签结束时调用的方法,EVAL_PAGE表示处理完标签后继续执行标签之后的JSP页面。另一个返回值SKIP_PAGE表示不处理标签之后的JSP网页。
     public   int  doEndTag()  throws  JspException {
        
// 代码主体省略,请查看源码
         return  EVAL_PAGE;  // 处理完标签后继续执行标签之后的JSP页面
    }
          doEndTag() 方法中部分比较重要的代码说明:
          使用 pageContext 对象在JSP页面上下文取得请求,不过请注意 pageContext,它定义在javax.servlet.jsp.tagext.TagSupport中,而不是在 BodyTagSupport 中,因为BodyTagSupport继承了TagSupport
     // 使用pageContext对象在JSP页面上下文取得请求
    HttpServletRequest request  =  (HttpServletRequest)pageContext.getRequest();
          取得请求中的pageID值,这个pageID值在每个jsp页面中放在meta标签中,通过sitemesh装饰器取出放到request中。
     /*
     * 从请求中得到pageID值,即每个JSP里meta的content值
     * 与XML文件中book元素的id属性值一致
     
*/
    String pageID 
=  (String)request.getAttribute( " pageID " );
          将所有的catalog元素取出放到List集合中,这里是只取得catalog这一层级的元素,实质就是catalogs中只有两个对象,一个是 id = "catalog-program" 另一个是 id = "catalog-system",使用dom4j我们会发现处理元素非常容易,API相当的丰富,想写成什么样完全凭你自己的想法。
     // 将所有的catalog元素取出放到List集合
    List catalogs  =  ResourceManage.getCoreModel().selectNodes( " //catalog " );
          看看前面介绍的getElementByID这个方法,这个 currentCatalog 所表示就是 pageID 所在的 catalog 元素。上面的代码是为了和下面的代码结合来判断当前的菜单是否为选中,加入 CSS 样式显示。
     // pageID所在的catalog元素,主要用来判断当前菜单是否被选中
    Element currentCatalog  =  (Element)ResourceManage.getElementByID(pageID);
          从 BodyContent 中将标签体缓存流读取出来, 标签在WebRoot/decorators/main.jsp < a href = "[url]" title = "[description]" onmouseover = "self.status='[description]';return true;" onmouseout = "self.status='';return true;" > [name] </ a >
    String value  =  getBodyContent().getString();  // 得到标签体
          Catalogs 里面是两个catalog对象,循环遍历取出,将标签体中的 [id] [url] [name] [description] 替换为XML文件中的属性值,这样主菜单标签就生成了。
     for  ( int  i = 0 ; i < catalogs.size(); i ++ ) {
        Element catalog 
=  (Element)catalogs.get(i);  // catalog元素对象
        String value  =  getBodyContent().getString();  // 得到标签体
         /*
         * 将标签体中的[id]、[url]、[name]、[description]
         * 替换为XML文件中的属性值
         * attributeValue方法是取属性值
         
*/
        
if  (value  !=   null ) {
            value 
=  StringUtils.replace(value,  " [url] " , request
                       .getContextPath()
                       
+   " / "   +  catalog.attributeValue( " url " ));            
            value 
=  StringUtils.replace(value,  " [name] " ,catalog.attributeValue( " name " ));
            value 
=  StringUtils.replace(value,  " [description] " ,catalog.
                    attributeValue(
" description " ));
        }
        String css 
=  getCss();
        
// 对当前选中菜单添加CSS样式
         if  (catalog.equals(currentCatalog)) {
            css 
=  getCurrentcss();
        }
        buf.append(
" <li class=\ "" ).append(css).append( " \ " > " );
        
if  (i  >   0 ) {
            buf.append(
"  |  " );
        }
        buf.append(value).append(
" </li> " );
    }
       2、导航菜单及侧边栏菜单标签
           导航菜单每个catalog下都有两个,而侧边栏菜单则在item
下定义,这两个标签类与主菜单的标签类没有太大的区别,主要就是生成标签体,匹配CSS
样式,因此,代码中相同的部分我不再细述,只说一下不同的地方。
           在tag包下创建 NavTag 类和SideTag类,标签属性与MainTag一样,只是SideTag多了一个 headercss 属性,这是在页面显示时,加在边栏上当前选中项左侧小箭头的 CSS 样式,不清楚的话,请运行程序后观察。
            NavTag.java
            根据 pageID 找到此元素对象:
     // 根据pageID找到此元素对象,即book元素对象
    Element current  =  ResourceManage.getSingleElementById(pageID);
           如果 current 不为空,取得父节点,其为 item 元素。根据 pageID 值,如果为 695043 ,则 subnav id = "item-program-java" item 元素,如果为 691245 ,则 subnav id = "item-program-database" item 元素。 这个subnav的作用也是用来判断当前的菜单是否为选中,加入CSS样式显示。
    Element subnav  =   null ;
    
if  (current  !=   null ) {
        subnav 
=  current.getParent();  // 取得父节点,即item元素
    }
           SideTag.java
           在SideTag中也有上面的代码,但是subnav不再与CSS有关,而是取得它的所有子元素集合,即book元素集合,然后遍历所有book节点,取出属性值放入标签体中再输出到页面。
           我注释写得很详细,请查看代码了解细节。
     4、创建StringUtils.java
       在util包下创建StringUtils类,这个类作为字符串处理类。添加 publicstatic String replace(String string, String oldString, String newString) 方法,它的作用就是将标签体中的 [id] [url] [name] [description] 替换为XML文件中的属性值。如果被替换的字符串在标签体中有多个,也能将它全部替换。
     /**
     * 将string中的oldString全部替换为newString
     * 
@param  string 原始字符串
     * 
@param  oldString 被替换的字符串
     * 
@param  newString 要替换的字符串
     * 
@return  返回替换完后的新string
     
*/
    
public   static  String replace(String string, String oldString, String newString) {
        
if  (string  ==   null ) {
            
return   null ;
        }
        
int  i  =   0 ;
        
// 判断string中是否有被替换的字符串,i其实是索引值
         if  ((i  =  string.indexOf(oldString, i))  >=   0 ) {
            
char [] string2  =  string.toCharArray();  // 字符串放入数组
             char [] newString2  =  newString.toCharArray();  // 要替换的字符串
             int  oLength  =  oldString.length();  // 被替换的字符串的长度
            StringBuilder buf  =   new  StringBuilder(string2.length);
            
/*
             * 从索引0开始,按i值的长度在string2数组中截取字符
             * 将截取的字符放到buf中,接着再加入要替换的内容
             
*/
            buf.append(string2, 
0 , i).append(newString2);
            i 
+=  oLength;  // 得到被替换字符结束位置的索引
             int  j  =  i;
            
/*
             * 查找string中,是否仍然含有被替换字符串
             * 使用循环,将所有oldString换成newString
             
*/
            
while  ((i  =  string.indexOf(oldString, i))  >   0 ) {
                buf.append(string2, j, i 
-  j).append(newString2);
                i 
+=  oLength;  // 得到被替换字符结束位置的索引
                j  =  i;
            }
            
/*
             * 截取string2数组中从索引j开始
             * string2.length-j的长度加到buf中
             * 其实就是在buf中补全标签体
             
*/
            buf.append(string2, j, string2.length 
-  j);
            
return  buf.toString();
        }
        
return  string;
    }
       如果看注释就能懂那最好不过,如果不明白,在这上面打个断点调试一下,就会十分清楚了。这个方法是Openfire中的源代码,不过全是E文,大象先也看不明白,后来调试了一下,知道是怎么回事了,特加上注释和大家一起分享这个好东东。
       写完了自定义标签类,我们还需要自定义标签文件,在WEB-INF目录下新建demo.tld,代码不帖出来了,使用源码中的就行。
     5、装饰器
       后台的Java类,我们全部写完了,现在开始完成前台部分,在页面显示上,Openfire使用了sitemesh装饰器框架,它能帮助我们在由大量页面构成的项目中创建一致的页面布局和外观,如一致的导航条,一致的banner,一致的版权等等。至于怎样使用sitemesh这里不作介绍了,请自行去搜索相关资料,这部分内容网上很多的,sitemesh比较简单,很容易上手。
       使用装饰器,需要导入JAR包,在本例中,大象使用的是sitemesh-2.2.1.jar包,将jar包加到WEB-INF/lib目录中,然后修改web.xml,添加如下代码:
     < filter >   
        
< filter-name > sitemesh </ filter-name >   
        
< filter-class > com.opensymphony.module.sitemesh.filter.PageFilter </ filter-class >   
    
</ filter  >   
    
< filter-mapping >   
        
< filter-name > sitemesh </ filter-name >   
        
< url-pattern > /* </ url-pattern >   
    
</ filter-mapping >
        然后在WEB-INF下新建decorators.xml文件,内容如下:
     < decorators  defaultdir ="/decorators" >
        
< decorator  name ="main"  page ="main.jsp" >
            
< pattern > /index.jsp </ pattern >
            
< pattern > /book_*.jsp </ pattern >
        
</ decorator >
    
</ decorators >
       请注意 defaultdir 后面的值,这是你放装饰器页面的目录位置。本例中,在 WebRoot 目录下新建 decorators 文件夹,再在里面新建 main.jsp ,这个就是装饰器页面了。 < pattern ></ pattern > 之间的内容就是需要被装饰的页面, * 号是通配符,可以代替任何字符。 /book_*.jsp 表示:使用 main.jsp 装饰 WebRoot 目录下所有以 book_ 开头的页面,这里定义的 name = "main" ,可以在装饰器页面中使用,因为不一定只有一个装饰器页面,可能会有很多个。因此,在装饰器页面中为了布局效果会联合使用多个装饰器来修饰页面,以达到简化布局、降低维护难度、提高工作效率的作用。另外在使用时,请注意被装饰页面与装饰器页面之间的相对位置。
    6、main.jsp
        在页面中引用被装饰页面的page对象: <decorator:usePage id="decoratedPage" />
        使用decoratedPage取得被装饰页面中meta 标签的content值,再将它放到request请求中,这样在自定义标签类中我们使用 pageContext 对象得到的请求就是这个
    <%
        request.setAttribute(
" pageID " , decoratedPage.getProperty( " meta.pageID " ));
    %>
        显示被装饰页面 < title ></ title > 之间的标题:<decorator:title />
        显示被装饰页面body中的内容,被装饰页面的主体都将在这里显示: < decorator:body />
        除此之外,在main.jsp中,我们还发现大量的使用div来放置元素,并且每个标签中都有id属性,没有看到任何的CSS样式,其实是通过id属性在demo.css文件进行了定义,所有的布局和显示效果都在这个文件中进行了定义,这样就达到了内容呈现与样式布局相分离的结果,方便以后的修改和维护,这种做法用的人现在已经越来越多,大家赶快行动吧!
       sitemesh 中还有一个 sitemesh.xml 文件,如果程序中没有特别需求,可以不用加入它,我们也能在sitemesh-2.2.1.jar 中找到, com.opensymphony.module.sitemesh.factory 目录下有一个 sitemesh-default.xml 文件,这就是 sitemesh 默认的配置文件。
    7、显示页面
       在 tag-console.xml url 属性里定义了显示页面,接下来我们把这些页面都做好,内容很简单,本文只是演示 Openfire 的菜单设计思想,用容易懂的例子来说明,以便大家能够快速了解。
      index.jsp页面的代码:
<% @ page contentType = " text/html; charset=utf-8 "   %>
< html >
< head >
    
< title > Struts2 深入详解 </ title >
    
< meta name = " pageID "  content = " 695043 " />
</ head >
< body >
    
< center >< h1 > Struts2 深入详解 </ h1 ></ center >
</ body >
</ html >
       其余的几个页面内容大致一样,把 content 值、 < title ></ title > 标题、以及 < center >< h1 ></ h1 ></ center > 之间的内容换成 book 元素中定义的属性值即可。
       demo.css
       在WebRoot目录下新建css文件夹,再在里面创建demo.css文件。我直接把Openfire的样式表COPY过来。然后把里面没用的部分删除了,体积小了不少。
       图片
       我从Openfire中只取了本例用到的图片,如果是专业美工,完全可以设计出自己的菜单风格。
     8、发布项目
       我们在web.xml中可以加入下面一段代码, index.jsp 作为我们的默认显示页面:
    < welcome-file-list >
        
< welcome-file > index.jsp </ welcome-file >
    
</ welcome-file-list >
       把 demo 部署到 %TOMCAT_HOME%\webapps 目录下,启动 tomcat ,在地址栏中输入: http://localhost:8080/demo 看看效果是不是和下面的一样:

       大家觉得这个菜单的显示方式怎么样呢?偶觉得这用来做后台管理到还是不错滴,如果是其它的信息管理系统,那这个配置文件的内容就会很恐怖了,其实还可以把XML中的中文信息保存到国际化资源文件中,这样可以实现多语种版本以及简化维护。各位有什么好的意见或建议,可以和我留言或E-mail给我。大象也想把自己的一点心得拿出来和大家分享。
       点击下载: demo源码
       点击下载: dom4j-1.6.1.jar   jaxen-1.1-beta-7.jar   sitemesh-2.2.1.jar
       本文为菠萝大象原创,如要转载请注明出处
1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。
1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 、2项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 、资5源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值