跳转到主要内容

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

当您初次登录到 developerWorks 时,将会为您创建一份概要信息。您在 developerWorks 概要信息中选择公开的信息将公开显示给其他人,但您可以随时修改这些信息的显示状态。您的姓名(除非选择隐藏)和昵称将和您在 developerWorks 发布的内容一同显示。

所有提交的信息确保安全。

  • 关闭 [x]

当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

所有提交的信息确保安全。

  • 关闭 [x]

GAE 应用文件上传的三种方法

胡 伟红, 自由职业者
胡伟红,西安交通大学硕士,从事软件开发,曾在 IBM 任职,其间负责 Websphere 产品技术支持。关注领域:Groovy/Grails、JBPM、Drools,网站 GroovyQ 合伙创办人,InfoQ 中文站内容发布编辑。Email:weihong0427@163.com。

简介: 文件上传 / 下载是应用开发中非常常见的需求,尤其是 CMS、博客系统。各种语言都有自己的解决方法。Google App Engine(以下简称 GAE)作为云计算的平台,为应用提供了广阔的扩展空间,运行于其上的应用也越来越多。那么在 GAE 上的应用如何实现文件的上传 / 下载呢?本文将向读者介绍三种实现方式,以及相关注意事项。

发布日期: 2010 年 10 月 26 日
级别: 初级
访问情况 : 10984 次浏览
评论: 


在应用开发中,常会需要上传 / 下载各种类型的文件,尤其是 CMS、博客系统。各种语言都有自己的解决方法。Google App Engine(以下简称 GAE)作为云计算的平台,为应用提供了广阔的扩展空间,运行于其上的应用也越来越多。那么在 GAE 上的应用如何实现文件的上传 / 下载呢?本文将给读者介绍三种解决方法,及其使用时需要注意的内容。

本文使用的开发环境如下:

  • Grails 1.3.3;
  • Google App Engine 1.3.1;
  • app-engine-0.8.10;
  • gorm-jpa-0.7.1;
  • GAEvfs 0.3

非 GAE 应用中的文件上传

如果你的 Grails 应用不需要部署在 GAE 上,那么要实现文件上传就非常简单。在你的 GSP 页面中,使用 <g:form> 或者 <g:uploadform> 就能完成,示例代码如下:


清单 1. 文件上传 GSP 示例代码
				
 <g:uploadForm action="save" method="post" > 
 <input type="file" name="filename" /> 
 </g:uploadForm> 
 <!-- 或者 --> 
 <g:form action="save" method="post"  enctype="multipart/form-data"> 
 <input type="file" name="filename" /> 
 </g:form> 

在 Controller 中,通过 request 获得上传文件的类型为 org.springframework.web.multipart.commons.CommonsMultipartFile。对于文件内容,可以以 byte 数组或者 BLOB 的形式保存在数据库中,或者干脆保存在文件系统中。


清单 2. 将文件保存至文件系统
				
 def file=request.getFile('filename') 
 def filename= file.originalFilename 
 uploadInstance.filename=filename 
 def dir= new File(System.getProperty("user.dir")+"/attachments"); 
 if(!dir.exists()){ 
	 dir.mkdirs() 
 } 
 def destFile= new File("${dir}\\"+filename) 
 if(destFile.exists()){ 
 destFile.delete() 
 } 
 file.transferTo(destFile) 

至于将文件内容以 byte 数组或者 BLOB 的形式保存至数据库,那就再简单不过了。这里就不赘述。


MultipartResolver

如果你心急,直接将上述代码应用在 GAE 环境下。抱歉,文件无法上传!在 GSP 页面提交后,就会遇到 java.lang.NoClassDefFoundError: java.rmi.server.UID is a restricted class 这样的错误。显然,java.rmi.server.UID 不在 GAE 的类白名单之列。

查看 CommonsMultipartFile源代码,你会发现,它除了使用 UID 之外,还将上传的文件保存在一个临时目录中,这又犯了 GAE 的大忌 -- 不能使用本地文件系统。看来,CommonsMultipartFile 在 GAE 这边是没法通过审查啊!那么要让 GSP 上传的文件得到 GAE 的认可,就必须对 CommonsMultipartFile 进行改造:不使用 UID 和文件系统。

我们都知道,Grails 使用 Spring 作为其 MVC 框架。Spring 中,在文件上传后,使用 MultipartResolver 对文件进行预处理,并将文件生成为 CommonsMultipartFile。如果你熟悉 MultipartResolver 和 CommonsMultipartFile,可以对它们的代码进行改造,使其使用 GAE 认可的类。如果不熟悉,也不用着急,有现成的 Spring 扩展项目:pjesi/springextras,已经给我们提供了合适的 MultipartResolver 和 MultipartFile,这个项目将 Request 中文件的相关属性保存在内存中,这对于 GAE 是允许的。该项目包含如下两个文件:

  • StreamingMultipartResolver:对 request 中的内容进行处理,比如设置上传文件大小的最大值、编码等;
  • StreamingMultipartFile:这就是 type=file 的输入框中提交的文件,其中包含了文件名、文件大小以及文件内容的字节数组。

如下是 StreamingMultipartFile 中获取文件内容的代码:


清单 3. StreamingMultipartFile 中获取文件内容的代码
				
 public byte[] getBytes() throws IOException { 
 if(bytes == null) { 
 bytes = IOUtils.toByteArray(item.openStream()); 
 } 
  return bytes; 
 } 

从上述代码,可以看出。文件内容是以 byte 数组的形式保存在内存当中。这是 GAE 所接受的形式。要使用这个项目,需要对 Grails 应用进行如下配置:

  • 将下载的 springextras-1.1.jar 保存至 grailsApp\lib 目录下;
  • 在 grailsApp\grails-app\conf\spring 下的 resources.groovy 文件中,添加如下代码:

清单 4. 示例代码
				
 beans = { 
 multipartResolver( 
 is.hax.spring.web.multipart.StreamingMultipartResolver) 
 } 

这个时候在 GAE 环境中重新运行 Grails 程序,文件上传时就会不出现 restricted class 的错误了。


使用 Blob 存储文件

好,上传的文件已经可以通过 Request 访问了。那么上传的文件在 GAE 环境中如何保存呢?这也是最关键的部分。在 GAE 中有多种方法。首先从最简单的讲起— Blob 字段。

GAE 中提供了两种 Blob 类型:com.google.appengine.api.datastore.ShortBlobcom.google.appengine.api.datastore.Blob。二者的主要区别是,前者保存的最大数组长度为 500 字节,后者没有大小限制。在开发过程中,可根据实际需要选择使用。下面以 com.google.appengine.api.datastore.Blob 为例,介绍文件的存储与下载。

首先在 Domain class 中,使用 com.google.appengine.api.datastore.Blob 来定义保存文件内容的字段。


清单 5. 定义 Domain class
				
 import com.google.appengine.api.datastore.Blob 
 class File implements Serializable { 
  ...... 
  String filename 
  Blob content 
  ...... 
 } 

在保存文件时,通过 Request 获得上传文件,之后获得文件内容数组,构建一个新的 Blob,将这个新的 blob 保存即可。参见如下代码:


清单 6. 保存文件
				
 import com.google.appengine.api.datastore.Blob 
     def save={ 
	 ...... 
	 def fileInstance = new File() 
	 def file = request.getFile( 'myfile' ) 
	 File.withTransaction { 
          fileInstance.filename=file.getName() 
          Blob blob=new Blob(file.getBytes()) 
          fileInstance.content=blob 
          fileInstance.save(flush:true) 
	 ...... 
 } 
 } 

由于文件内容保存为 blob,那么在下载文件的时候,可以通过 byte 数组从输出字节流中下载文件。


清单 7. 下载文件
				
 def fileInstance = File.get( params.id ) 
 response.setContentType( "application-xdownload;charset=UTF-8") 	
 response.setHeader("Content-Disposition", 
"attachment;filename="+new String( 
 fileInstance.filename.getBytes("gb2312"), "ISO8859-1" )) 
 response.getOutputStream() << new ByteArrayInputStream( 
 fileInstance.content.getBytes()) 

当然,文件的内容也可以直接保存在 byte[] 中,那么 Domain class 就变更为:


清单 8. 修改后的 Domain class
				
 class File implements Serializable { 
    ...... 
    String filename 
    byte[] content 
    ...... 
 } 

byte[] 相关的保存 / 下载文件的代码跟 blob 类似,这里就不赘述,详细代码可参见参考资料(Easy file uploading in Grails)。

注意事项

使用 Blob 或者 byte 数组保存文件,直接而简单。

多用于上传文件均小于 10K 以及文件之间关系松散的情景中。在使用的时候需要注意如下内容:

  1. 对上传文件的类型没有限制;
  2. 虽然上传文件的大小没有限制,但是对于文件过大,比如兆或百兆的文件不建议采用此法。如果文件过大,在上传时可能会时间太长而引起超时。

GAEvfs

GAEvfsApache Commons VFS的插件,实现了一个可写的分布式虚拟文件系统,目前版本为 0.3。在这个虚拟文件系统中,可以对文件分文件夹保存,文件夹下可以有子文件夹。

名词解释

Block

在这个虚拟的文件系统中,有一个关键概念— Block。Block 是文件保存的最小单元。每个 Block 在 GAE 的数据存储区表现为一个实体,所以每个 Block 理论的最大值为 1M。缺省情况下,Block 的大小为 128k。如下是 Block 的注意事项:

  • 可以使用 GaeVFS.setBlockSize 设置 Block 的大小,单位为 K,范围 1~1023;
  • Block 大小设定后,所有文件都使用这个值,但是可以在每个文件创建之前,单独的为这个文件设置它所使用的 Block 大小,如:GaeVFS.setBlockSize( filename, 8 );
  • 在文件创建完成后,无法更改 Block 的大小。如果执意要执行,则会出现 IO 异常;
  • Block 是文件保存得最小单元,即使文件只有 1K 大小,也会给这个文件分配一个 128K 的 Block,所以在开发时可以根据实际情况,来设置合适的 Block 大小。

GaeFileObject

继承自 AbstractFileObject ,是 GAEvfs 的 Domain class。在 GAEvfs 中,file、folder、Block 都属于 GaeFileObject。如下是各自使用的字段列表。


表 1. 文件(File)用到的字段
字段 类型 说明
Id Key
filetype String File
last-modified Int 最近修改的时间
block-keys List 文件使用的 Block 的 key 值列表
block-size Int 可通过程序设置,缺省值为 128K
content-size Int 文件的大小


表 2. 文件夹(Folder)用到的字段
字段 类型 说明
Id Key
Filetype String Folder
last-modified Int 最近修改的时间
childe-Keys List 文件夹下的所有内容(文件以及文件夹)的 key 的列表


表 3. Block 用到的字段
字段 类型 说明
Id Key
content-blob Blob 用于保存文件内容

文件路径

在 GAEvfs 的 jar 文件中 META-INF 目录下有一个名为 vfs-providers.xml 的文件,在这个文件中配置了 GAEvfs 使用的模式名称:gae,那么可以通过如下目录保存和访问文件:gae://path,比如 gae://myfile/,gae://myfile/temp.txt。

使用 GAEvf s

对 GAEvfs 有了大概的了解之后,就来看看如何使用吧!

这里下载 GAEvfs 的 jar 文件,将其放入 grailsApp\web-app\WEB-INF\lib 中。

要使用 GaeVFS 必须先设置 GAEvfs 使用的根目录,通常是设置为工程根目录。示例代码如下:


清单 9. 设置工作目录
				
 def rootPath=servletContext.getRealPath( "/" ) 
 GaeVFS.setRootPath( servletContext.getRealPath( "/" ) ) 

工作目录设置好之后,就能将上传的文件保存至虚拟路径下。


清单 10. 保存文件
				
 def rootPath=servletContext.getRealPath( "/" ) 
 GaeVFS.setRootPath( servletContext.getRealPath( "/" ) ) 
 try { 			
 def file=params.myfile 			
 def filename=file.getName() 			
 FileObject fileObject = 
 GaeVFS.resolveFile( "gae://myfile/" + filename ) 		
 def out=fileObject.getContent().getOutputStream() 
 out.write(file.getBytes()) 
 out.close() 					
 }finally { 
 GaeVFS.clearFilesCache() 
 } 

获得 FileObject 之后,可以通过 Delete 方法删除文件。参见示例代码:


清单 11. 删除文件
				
 FileObject fileObject = 
 GaeVFS.resolveFile( "gae://myfile/" + filename ) 
 if(fileObject.exists()){ 
 fileObject.delete() 				 
 } 

在 GAE 中查看数据,你会发现 GAEvfs 中 Block 其实使用的也是 com.google.appengine.api.datastore.Blob。那么 GAEvfs 跟直接使用 com.google.appengine.api.datastore.Blob 存储文件,有什么区别呢?就文件存储方式而言,没什么区别,但是 GAEvfs 有一个很大优点就是能够为文件创建文件夹,就像 Window 的文件系统那样,把文件按照文件夹的形式分类管理。


清单 12. 创建 folder
				
 FileObject myFolder = GaeVFS.resolveFile( "gae://myfolder" ); 
 if ( !myFolder.exists() ) { 
	 myFolder.createFolder(); 
 } 

在下载文件的时候,可以通过 byte 数组从输出字节流中下载文件。


清单 13. 下载文件
				
 def down = { 
 String rootPath = servletContext.getRealPath( "/" ); 
 def rootPathLen = rootPath.length(); 
 if ( System.getProperty( "os.name" ).startsWith( "Windows" ) ) { 
 rootPathLen++; 
 } 
 GaeVFS.setRootPath( rootPath ); 
 try { 
 FileObject fileObject = GaeVFS.resolveFile("gae://"+params.get("id") ); 
 if ( !fileObject.exists() ) { 
             ……
	 return 
	 } 
 def contentType =servletContext.getMimeType( 
        fileObject.getName().getBaseName() ) 
 response.setContentType( 
 contentType != null ? contentType 	 : 
 fileObject.getContent().getContentInfo().getContentType() ); 
 response.getOutputStream()<<fileObject.getContent() 
 .getInputStream() 
 response.flushBuffer(); 
 } finally { 
 GaeVFS.clearFilesCache(); 
 } 
 } 

注意事项

之前已经讲过,就文件存储方式而言,Blob 和 GAEvfs 没什么区别。但是如果上传的文件需要分类管理,那么可以选用 GAEvfs,它可以为文件指定文件夹以及创建子文件夹。

如下是使用 GAEvfs 的注意事项:

  1. GAEvfs 对上传文件的类型以及大小没有具体的限制,但同样建议过大的文件不要采用此种方式;
  2. 在 JVM 的同一个线程的同一个时间中,只能允许打开一个文件进行写操作;
  3. 不需要 DataStore 的索引。

BlobStore

在 GAE1.3 中新增了 BlobStore 存储机制,最大可保存 2G 的文件。其中定义了新的数据对象:BlobStore(或称 Blobs),这有别于 datastore 中的 blob 字段。应用程序只有通过上传文件,才能修改 BlobStore 对象。上传保存后的文件被称为 Blobs,应用程序无法直接访问 Blobs,但是可以通过信息实体 -BlobInfo 来操作 Blobs。

BlobStore 中涉及如下对象:

  • BlobstoreServiceFactory:创建 BlobstoreServices;
  • BlobstoreService:用于 Blobs 的创建和获取;
  • BlobInfoFactory:为获取 BlobInfo 的元数据提供了一系列接口;
  • BlobInfo:每个上传的文件都有一个与其相关的 BlobInfo 实体,其中包含了与 Blobs 相关的元数据,比如文件名、文件大小;BlobInfo 对象是只读的,无法通过应用程序进行修改;
  • BlobKey:是 Blobs 的唯一标识,可以通过这个标识获得 BlobInfo 实体。

修改 StreamingMultipartResolver

在本文的前半部分已经将 Grails 的缺省的 MultipartResolver 修改为 StreamingMultipartResolver。如果使用了 BlobStore,这个 StreamingMultipartResolver 就有点问题了,会提示:java.lang.OutOfMemoryError: Java heap space。原因是 StreamingMultipartResolver 预处理后的 Request 以及 StreamingMultipartFile 不是 BlobStore 所需要。BlobStore 所需要的仅仅是原始的 Request。

好,那么就将 StreamingMultipartResolver 中的预处理去掉,给 BlobStore 提供一个原始的 Request。修改 StreamingMultipartResolver 的 resolveMultipart 方法,见如下代码:


清单 13 为 BlobStore 工作的 StreamingMultipartResolver
				
 public MultipartHttpServletRequest resolveMultipart( 
	 HttpServletRequest request) throws MultipartException { 
 Map<String, String[]> multipartParameters = 
	 new HashMap<String, String[]>(); 
 MultiValueMap<String, MultipartFile> multipartFiles = 
	 new LinkedMultiValueMap<String, MultipartFile>(); 
 return new DefaultMultipartHttpServletRequest( 
	 request, multipartFiles, multipartParameters); 
 } 

在上述代码中,multipartParameters 和 multipartFiles 都是空的 Map。

这样,StreamingMultipartResolver 已经满足了 BlobSotre 的需求了。下面就开始正式使用 BlobSotre。BlobStore 将上传的路径封装到了服务中,可以通过 BlobstoreService 获得这个路径。见如下代码:


清单 14. 通过 BlobstoreService 获得文件上传的 URL
				
 <%@page import="com.google.appengine.api.blobstore 
 .BlobstoreServiceFactory" %> 
 <%@page import="com.google.appengine.api.blobstore 
 .BlobstoreService" %> 
 <%BlobstoreService blobstoreService = 
 BlobstoreServiceFactory.getBlobstoreService(); 
 %> 
……
 <form action="<%=blobstoreService.createUploadUrl("/serve")%>" 
 method="post" enctype="multipart/form-data"> 
 <input type="file" name="myFile"> 
 <input type="submit" value="Submit">ha 
 </form> 

上述代码中,通过 BlobstoreService 获取一个 upload 的 URL,形如:/_ah/upload/agpAYXBwLm5hbWVAchsLEhVfX0Jsb2JVcGxvYWRTZXNzaW9uX18YHww。在这里需要注意如下两点:

  1. createUploadUrl 方法中的参数为回调 Servlet,记住是 Servlet,而不是 Controller 或者 Action;
  2. form 标签的 enctype 属性必须设置成 multipart/form-data。

回调 Servlet

BlobStore 需要一个回调 Servlet,即在文件上传后,程序要继续执行的路径。在文件上传之后,会给每个文件分配一个 BlobKey,作为其唯一标识。可以在回调 Servlet 中通过 BlobstoreService 获得文件的 BlobKey,之后可以将 BlobKey 保存下来或者通过 BlobKey 获得对应得文件。回调 Servlet 见如下示例代码:


清单 14. 回调 Servlet
				
 package com.mulan; 

 import java.io.IOException; 
 import java.util.Map; 
 import javax.servlet.http.HttpServlet; 
 import javax.servlet.http.HttpServletRequest; 
 import javax.servlet.http.HttpServletResponse; 
 import com.google.appengine.api.blobstore.BlobKey; 
 import com.google.appengine.api.blobstore.BlobstoreService; 
 import com.google.appengine.api.blobstore.BlobstoreServiceFactory; 

 @SuppressWarnings("serial") 
 public class ServeServlet extends HttpServlet { 
 private BlobstoreService blobstoreService = 
 BlobstoreServiceFactory.getBlobstoreService(); 

 public void doPost( 
 HttpServletRequest req, 
 HttpServletResponse res)throws  IOException { 		
	 Map<String, BlobKey> blobs = 
	 blobstoreService.getUploadedBlobs(req); 	
          // 获得 BlobKey 之后,可以对其进行处理,比如保存	   
	 BlobKey blobKey = blobs.get("myfile"); 	
	 if (blobKey == null) { 
	 res.sendRedirect("/"); } 
	 else { 
	 res.sendRedirect("/serve?blob-key=" 
		 + blobKey.getKeyString()); } 	 
	 } 

 public void doGet( 
 HttpServletRequest req, 
 HttpServletResponse res)throws IOException { 
	 BlobKey blobKey = new BlobKey( 
	 req.getParameter("blob-key")); 
           // 获得 BlobKey 之后,通过如下方法获得文件
	 blobstoreService.serve(blobKey, res); 
 } 
 } 

完成了回调 Servlet 后,需要在 web.xml 中声明一下。由于 Grails 中的 web.xml 是由缺省模板生成的,如果要自定义 web.xml 就要先使用命令:grails install-templates 安装模板,之后在 src/templates/war/web.xml 中添加如下代码,这样应用所使用的 web.xml 就会依据修改后的模板生成:


清单 15. 修改后的 web.xml
				
 <servlet> 
 <servlet-name>serve</servlet-name> 
 <servlet-class>com.mulan.ServeServlet</servlet-class> 
 </servlet> 
 <servlet-mapping> 
 <servlet-name>serve</servlet-name> 
 <url-pattern>/serve</url-pattern> 
 </servlet-mapping> 

获取 BlobInfo

前面已经提到每个上传的文件都有一个 BlobInfo 用来保存其元数据,比如文件名、文件大小、文件类型等。如下是通过 BlobKey 获得 BlobInfo 的示例代码:


清单 16. 获得 BlobInfo
				
 BlobInfoFactory bf=newBlobInfoFactory(); 
 BlobInfo bi=bf.loadBlobInfo(blobKey); 
 //bi. getFilename()  获得文件名
 //bi.getSize()        获得文件大小
 //bi.getContentType() 获得文件类型
 //bi.getCreation()    获得文件创建时间

注意事项

BlobStore 是 GAE 收费的项目,在本地开发环境下,BlobStore 是可以正常工作的。但是如果应用程序部署到 appengine.appspot.com 上,付费后方可使用。文件上传速度较之前两种方式要快些。如下是使用 BlobStore 时的注意事项:

  1. BlobStore 将文件上传的功能封装成了服务,一旦遇到技术问题,没法查看源代码。
  2. BlobStore 目前还是 GAE 的测试功能,所以在使用的时候会有一些不便。所以选择使用 BlobStore 的时候一定要慎重。
  3. 通过 BlobStore 上传的文件可以在 GAE 应用管理控制台的 Data-->Blob Viewer 中查看。
  4. 对上传文件的类型没有限制。
  5. 在数据保存入 datasore 前没法做任何预处理,其次后期处理数据后不能直接存回去。

总结

本文介绍 Grails 下 GAE 应用上传文件的三种方法。由于 GAE 没有明确声明支持文件上传的功能,所以依靠开发者的想象力可以有很多实现方法。本文只是抛砖引玉,介绍了其中的三种方法及使用时需要注意的内容。在实际开发中,我们可以根据应用的具体需求进行选择。当然,也希望 BlobStore 能够走出实验室,为我们这些个 GAE 的粉丝们提供更强大的功能!最好能够免费 !:)


参考资料

学习

讨论

关于作者

胡伟红,西安交通大学硕士,从事软件开发,曾在 IBM 任职,其间负责 Websphere 产品技术支持。关注领域:Groovy/Grails、JBPM、Drools,网站 GroovyQ 合伙创办人,InfoQ 中文站内容发布编辑。Email:weihong0427@163.com。

关于报告滥用的帮助

报告滥用

谢谢! 此内容已经标识给管理员注意。


关于报告滥用的帮助

报告滥用

报告滥用提交失败。 请稍后重试。


developerWorks:登录


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 使用条款

 


当您初次登录到 developerWorks 时,将会为您创建一份概要信息。您在 developerWorks 概要信息中选择公开的信息将公开显示给其他人,但您可以随时修改这些信息的显示状态。您的姓名(除非选择隐藏)和昵称将和您在 developerWorks 发布的内容一同显示。

请选择您的昵称:

当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

(长度在 3 至 31 个字符之间)


单击提交则表示您同意developerWorks 的条款和条件。 使用条款.

 


为本文评分

评论

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Open source
ArticleID=556274
ArticleTitle=GAE 应用文件上传的三种方法
publish-date=10262010
author1-email=weihong0427@163.com
author1-email-cc=

标签

Help
使用 搜索 文本框在 My developerWorks 中查找包含该标签的所有内容。

使用 滑动条 调节标签的数量。

热门标签 显示了特定专区最受欢迎的标签(例如 Java technology,Linux,WebSphere)。

我的标签 显示了特定专区您标记的标签(例如 Java technology,Linux,WebSphere)。

使用搜索文本框在 My developerWorks 中查找包含该标签的所有内容。热门标签 显示了特定专区最受欢迎的标签(例如 Java technology,Linux,WebSphere)。我的标签 显示了特定专区您标记的标签(例如 Java technology,Linux,WebSphere)。