AJAX+JSF组件实现高性能的文件上载

2024-06-07 09:16:51
一、 引言

  基于浏览器的文件上传,特别是对于通过<input type="file">标签包含到Web页面来实现上传的情况,还存在较严重的性能问题。我们知道,超过10MB的上传文件经常导致一种非常痛苦的用户 体验。一旦用户提交了文件,在浏览器把文件上传到服务器的过程中,界面看上去似乎处于静止状态。由于这一切发生在后台,所以许多没有耐心的用户开始认为服 务器"挂"了,因而再次提交文件,这当然使得情况变得更糟糕。

  为了尽可能使得文件上传感觉更友好些,一旦用户提交文件,许多站点将显 示一个中间过程动画(例如一旋转图标)。尽管这一技术在上传提交到服务器时起一些作用,但它还是提供了太少的有关文件上传状态的信息。解决这个问题的另外 一种尝试是实现一个applet——它通过FTP把文件上传到服务器。这一方案的缺点是:限制了你的用户,必须要有一个支持Java的浏览器。

  在本文中,我们将实现一个具有AJAX能力的组件——它不仅实现把文件上传到服务器,而且"实时地"监视文件上传的实际过程。这个组件工作的四个阶段显示于下面的图1,2,3和4中:

b4b3ew7lf623.jpg
图1.阶段1:选择文件上传
4664p76dh8a0.jpg
图2.阶段2:上传该文件到服务器
481i3j42vlh1.jpg
图3.阶段3:上传完成
smrjvdmq7hk0.jpg
图4.阶段4:文件上传摘要

  二、 实现该组件

  首先,我们分析创建多部分过滤的过程,它将允许我们处理并且监视文件上传。然后,我们将继续实现JavaServer Faces(JSF)组件-它将提供给用户连续的回馈,以支持AJAX的进度条方式。

  (一) 多部分过滤:UploadMultipartFilter

   多部分过滤的任务是拦截到来的文件上传并且把该文件写到一个服务器上的临时目录中。同时,它还将监视接收的字节数并且确定已经上载该文件的程度。幸运的 是,现在有一个优秀的Jakarta-Commons开源库可以利用(FileUpload),可以由它来负责分析一个HTTP多部分请求并且把文件上传 到服务器。我们要做的是扩展该库并且加入我们需要的"钩子"来监视已经处理了多少字节。

public class UploadMultipartFilter implements Filter{
 public void doFilter(ServletRequest request,ServletResponse response,FilterChain chain)
 throws IOException, ServletException {
  HttpServletRequest hRequest = (HttpServletRequest)request;
  //检查是否我们在处理一个多部分请求
  String contentHeader = hRequest.getHeader("content-type");
  boolean isMultipart = ( contentHeader != null && contentHeader.indexOf("multipart/form-data") != -1);
  if(isMultipart == false){
   chain.doFilter(request,response);
  }else{
   UploadMultipartRequestWrapper wrapper = new UploadMultipartRequestWrapper(hRequest);
   chain.doFilter(wrapper,response);
  }
  ...
 }

   正如你所见,UploadMultipartFilter类简单地检查了当前的请求是否是一个多部分请求。如果该请求不包含文件上传,该请求将被传递到 请求链中的下一个过滤,而不进行任何另外的处理。否则,该请求将被包装在一个UploadMultipartRequestWrapper中。

  (二) UploadMultipartRequestWrapper类

public class UploadMultipartRequestWrapper
extends HttpServletRequestWrapper{
 private Map<String,String> formParameters;
 private Map<String,FileItem> fileParameters;
 public UploadMultipartRequestWrapper(HttpServletRequest request) {
  super(request);
  try{
   ServletFileUpload upload = new ServletFileUpload();
   upload.setFileItemFactory(new ProgressMonitorFileItemFactory(request));
   List fileItems = upload.parseRequest(request);
   formParameters = new HashMap<String,String>();
   fileParameters = new HashMap<String,FileItem>();
   for(int i=0;i<fileItems.size();i++){
    FileItem item = (FileItem)fileItems.get(i);
    if(item.isFormField() == true){
     formParameters.put(item.getFieldName(),item.getString());
    }else{
     fileParameters.put(item.getFieldName(),item);
     request.setAttribute(item.getFieldName(),item);
    }
   }
   }catch(FileUploadException fe){
    //请求时间超过-用户可能已经转到另一个页面。
    //作一些记录
    //...
   }
   ...

   在UploadMultipartRequestWrapper类中,我们将初始化ServletFileUpload类,它负责分析我们的请求并且把 文件写到服务器上的缺省临时目录。ServletFileUpload实例针对在该请求中遇到的每一个字段创建一个FileItem实例(它们包含文件上 传和正常的表单元素)。之后,一个FileItem实例用于检索一个提交字段的属性,或者,在文件上传的情况下,检索一个到底层的临时文件的 InputStream。总之,UploadMultipartRequestWrapper负责分析该文件并且设置任何FileItem-它在该请求中 把文件上传描述为属性。然后,这些属性由JSF组件所进一步收集,而正常表单字段的行为保持不变。

  默认情况下,通用 FileUpload库将使用DiskFileItems类的实例来处理文件上传。尽管DiskFileItem在处理整个临时文件业务时是很有用的,但 在准确监视该文件已经处理程度方面存在很少支持。自版本1.1以来,通用FileUpload库能够使开发者指定用于创建FileItem的工厂。我们将 使用ProgressMonitorFileItemFactory和ProgressMonitorFileItem类来重载缺省行为并监视文件上传过 程。

  (三) ProgressMonitorFileItemFactory类

public class ProgressMonitorFileItemFactory extends DiskFileItemFactory {
 private File temporaryDirectory;
 private HttpServletRequest requestRef;
 private long requestLength;
 public ProgressMonitorFileItemFactory(HttpServletRequest request) {
  super();
  temporaryDirectory = (File)request.getSession().getServletContext().getAttribute("javax.servlet.context.tempdir");
  requestRef = request;
  String contentLength = request.getHeader("content-length");
  if(contentLength != null){requestLength = Long.parseLong(contentLength.trim());}
 }
 public FileItem createItem(String fieldName, String contentType,boolean isFormField, String fileName) {
  SessionUpdatingProgressObserver observer = null;
  if(isFormField == false) //这必须是一文件上传.
   observer = new SessionUpdatingProgressObserver(fieldName,fileName);
   ProgressMonitorFileItem item = new ProgressMonitorFileItem(
     fieldName,contentType,isFormField,
     fileName,2048,temporaryDirectory,
     observer,requestLength);
    return item;
 }
 ...
 public class SessionUpdatingProgressObserver implements ProgressObserver {
  private String fieldName;
  private String fileName;
  ...
  public void setProgress(double progress) {
   if(request != null){
    request.getSession().setAttribute("FileUpload.Progress."+fieldName,progress);
    request.getSession().setAttribute("FileUpload.FileName."+fieldName,fileName);
   }
  }
 }
}

   ProgressMonitorFileItemFactory Content-Length头由浏览器设置并且假定它是被设置的上传文件的精确长度。这种确定文件长度的方法确实限制了你在每次请求中上传的文件-如果 有多个文件在该请求中被编码的话,不过这个值是不精确的。这是由于,浏览器仅仅发送一个Content-Length头,而不考虑上传的文件数目。

   除了创建ProgressMonitorFileItem实例之外,ProgressMonitorFileItemFactory还注册了一个 ProgressObserver实例,它将由ProgressMonitorFileItem来发送文件上传过程中的更新。我们所使用的 ProgressObserver的实现(SessionUpdatingProgressObserver)针对被提交字段的id把进度百分数设置到用 户的会话中。然后,这个值可以由JSF组件存取以便把更新发送给用户。
  (四) ProgressMonitorFileItem类

public class ProgressMonitorFileItem extends DiskFileItem {
 private ProgressObserver observer;
 private long passedInFileSize;
 ...
 private boolean isFormField;
 ...
 @Override
 public OutputStream getOutputStream() throws IOException {
  OutputStream baseOutputStream = super.getOutputStream();
  if(isFormField == false){
   return new BytesCountingOutputStream(baseOutputStream);
  }else{return baseOutputStream;}
 }
 ...
 private class BytesCountingOutputStream extends OutputStream{
  private long previousProgressUpdate;
  private OutputStream base;
  public BytesCountingOutputStream(OutputStream ous){ base = ous; }
  ...
  private void fireProgressEvent(int b){
   bytesRead += b;
   ...
   double progress = (((double)(bytesRead)) / passedInFileSize);
   progress *= 100.0
   observer.setProgress();
  }
 }
}

  ProgressMonitorFileItem把DiskFileItem的缺省OutputStream包装到一个BytesCountingOutputStream中,这可以在每次读取一定数目的字节后更新相关的ProgressObserver。

  (五) 支持AJAX的JavaServer Faces(JSF)上传组件

   这个组件负责生成HTML文件上传标签,显示一个进度条以监视文件上传,并且生成一旦文件上传成功需要被显示的组件。使用JavaServer Faces实现这个组件的一个主要优点是,大多数复杂性被隐藏起来。开发人员只需要把组件标签添加到JSP,而后由组件负责所有的AJAX及相关的进度条 监控细节问题。下面的JSP代码片断用于把上传组件添加到页面上。

<comp:fileUpload
 value="#{uploadPageBean.uploadedFile}"
 uploadIcon="p_w_picpaths/upload.png"
 styleClass="progressBarDiv"
 progressBarStyleClass="progressBar"
 cellStyleClass="progressBarCell"
 activeStyleClass="progressBarActiveCell">
<%--下面是一旦文件上传完成将成为可见的组件--%>
<h:panelGrid columns="2" cellpadding="2" cellspacing="0" width="100%">
<f:facet name="header">
<h:outputText styleClass="text"
value="文件上传成功." />
</f:facet>
<h:panelGroup style="text-align:left;display:block;width:100%;">
<h:commandButton action="#{uploadPageBean.reset}"
p_w_picpath="p_w_picpaths/reset.png"/>
</h:panelGroup>
<h:panelGroup style="text-align:right;display:block;width:100%;">
<h:commandButton action="#{uploadPageBean.nextPage}"
p_w_picpath="p_w_picpaths/continue.png"/>
</h:panelGroup>
</h:panelGrid>
</comp:fileUpload>

  文件上传组件的value属性需要用一个拥有一个FileItem的属性绑定到一个bean上。组件只有在该文件被服务器成功收到时才显示。

  三、 实现AJAX文件上传组件

   实质上,上载组件或者生成一个完整的自已,或者在一个AJAX请求的情况下,只生成部分XML以更新在页面上进度条的状态。为了防止 JavaServer Faces生成完整的组件树(这会带来不必要的负荷),我们还需要实现一个PhaseListener(PagePhaseListener)以取消该 faces的请求处理的其它部分-如果遇到一个AJAX请求的话。我在本文中略去了所有的关于标准配置(faces-config.xml和标签库)的讨 论,因为它们相当直接且已经在以前讨论过;而且这一切都包含在随同本文的源码中,你可以详细分析。

  (一) AJAX文件上传组件生成器

  该组件和标签类的实现比较简单。大量的逻辑被包含到生成器中,具体地说,它负责以下:

  · 编码整个的上传组件(和完整的HTML文件上传标签)、文件被上传完成后要显示的组件,还有实现AJAX请求的客户端JavaScript代码。

  · 适当地处理部分AJAX请求并且发送回必要的XML。

  · 解码一个文件上传并且把它设置为一个FileItem实例。

  (二) 编码整个上传组件

  前面已经提及,文件上传组件由三个阶段组成。在该组件的整个编码期间,我们将详细分析这三个阶段的编码。注意,在页面上的该组件的可视化(使用CSS显示)属性将由AJAX JavaScript来控制。

  (三) 阶段一

  图5显示了该上传组件的第一个阶段。

8z9l4zjc5hk6.jpg
图5.选择文件上传

  在第一阶段中,我们需要生成HTML文件Upload标签和点击Upload按钮时相应的执行代码。一旦用户点击了Upload按钮,表单将被一个IFRAME(为防止页面阻塞)提交并初始化第二个阶段。下面是生成代码的一部分:

//文件上传组件
writer.startElement("input", component);
writer.writeAttribute("type", "file", null);
writer.writeAttribute("name", component.getClientId(context), "id");
writer.writeAttribute("id", component.getClientId(context),"id");
if(input.getValue() != null){
 //如果可用,则生成该文件名.
 FileItem fileData = (FileItem)input.getValue();
 writer.writeAttribute("value", fileData.getName(), fileData.getName());
}
writer.endElement("input");
String iconURL = input.getUploadIcon();
//生成图像,并把JavaScript事件依附到其上.
writer.startElement("div", component);
writer.writeAttribute("style","display:block;width:100%;text-align:center;", "style");
writer.startElement("img", component);
writer.writeAttribute("src",iconURL,"src");
writer.writeAttribute("type","p_w_picpath","type");
writer.writeAttribute("style","cursor:hand;cursor:pointer;","style");
UIForm form = FacesUtils.getForm(context,component);
if(form != null) {
 String getFormJS = "document.getElementById('" + form.getClientId(context) + "')";
 String jsFriendlyClientID = input.getClientId(context).replace(":","_");
 //设置表单的编码为multipart以用于文件上传,并且通过一个IFRAME
 //来提交它的内容。该组件的第二个阶段也在500毫秒后被初始化.
 writer.writeAttribute(" + ".encoding='multipart/form-data';" +
getFormJS + ".target='" + iframeName + "';" + getFormJS + ".submit();" +
getFormJS + ".encoding='application/x-www-form-urlencoded';" +
getFormJS + ".target='_self';" +
"setTimeout('refreshProgress" + jsFriendlyClientID + "();',500);",null);
}
...
writer.endElement("img");
//现在实现我们将要把该文件/表单提交到的IFRAME.
writer.startElement("iframe", component);
writer.writeAttribute("id", iframeName, null);
writer.writeAttribute("name",iframeName,null);
writer.writeAttribute("style","display:none;",null);
writer.endElement("iframe");
writer.endElement("div");
writer.endElement("div"); //阶段1结束

  (四) 阶段二

  第二阶段是显示当前百分比的进度条和标签,如图6所示。该进度条是作为一个具有100个内嵌span标签的div标签实现的。这些将由AJAX JavaScript根据来自于服务器的响应进行设置。

lqsgq122n83c.jpg
图6.上传文件到服务器
writer.startElement("div",component);
writer.writeAttribute("id", input.getClientId(context) + "_stage2", "id");
...
writer.writeAttribute("style","display:none", "style");
String progressBarID = component.getClientId(context) + "_progressBar";
String progressBarLabelID = component.getClientId(context) + "_progressBarlabel";
writer.startElement("div", component);
writer.writeAttribute("id",progressBarID,"id");
String progressBarStyleClass = input.getProgressBarStyleClass();
if(progressBarStyleClass != null)
writer.writeAttribute("class",progressBarStyleClass,"class");
for(int i=0;i<100;i++){
 writer.write("<span> </span>");
}
writer.endElement("div");
writer.startElement("div",component);
writer.writeAttribute("id",progressBarLabelID,"id");
...
writer.endElement("div");
writer.endElement("div"); //阶段2结束

  (五) 阶段三

  最后,作为阶段三,一旦文件成功上传,需要被显示的组件即被生成,见图7。这些是在生成器的encodeChildren方法中实现的。

6n38g8m6gup3.jpg
图7.上传完成

public void encodeChildren(FacesContext context,
UIComponent component) throws IOException {
 ResponseWriter writer = context.getResponseWriter();
 UIFileUpload input = (UIFileUpload)component;
 //一旦文件上传成功,处理将被显示的子结点
 writer.startElement("div", component);
 writer.writeAttribute("id", input.getClientId(context) + "_stage3", "id"); //阶段3.
 if(input.getValue() == null){
  writer.writeAttribute("style","display:none;",null);
 }else{
  writer.writeAttribute("style","display:block",null);
 }
 List<UIComponent> children = input.getChildren();
 for(UIComponent child : children){
  FacesUtils.encodeRecursive(context,child);
 }
 writer.endElement("div"); //阶段3结束
}
  四、处理AJAX请求

  AJAX请求的生成是在这个组件的解码方法中处理的。我们需要检查这是否是一个实际的 AJAX请求(为了区别于正常的编译行为),然后基于由ProgressMonitorFileItemFactory类的 SessionUpdatingProgressObserver实例设置在会话中的值把一个XML响应发送回客户端。

public void decode(FacesContext context, UIComponent component) {
 UIFileUpload input = (UIFileUpload) component;
 //检查是否这是一个上传进度请求,或是一个实际的上传请求.
 ExternalContext extContext = context.getExternalContext();
 Map parameterMap = extContext.getRequestParameterMap();
 String clientId = input.getClientId(context);
 Map requestMap = extContext.getRequestParameterMap();
 if(requestMap.get(clientId) == null){
  return;//什么也不做,返回
 }
 if(parameterMap.containsKey(PROGRESS_REQUEST_PARAM_NAME)){
  //这是一个在该文件请求中的得到进度信息的请求.
  //得到该进度信息并把它生成为XML
  HttpServletResponse response = (HttpServletResponse)context.getExternalContext().getResponse();
  //设置响应的头信息
  response.setContentType("text/xml");
  response.setHeader("Cache-Control", "no-cache");
  try {
   ResponseWriter writer = FacesUtils.setupResponseWriter(context);
   writer.startElement("progress", input);
   writer.startElement("percentage", input);
   //从会话中获得当前进度百分数(由过滤器所设置).
   Double progressCount = (Double)extContext.getSessionMap().
   get("FileUpload.Progress." +input.getClientId(context));
   if(progressCount != null){
    writer.writeText(progressCount, null);
   }else{
    writer.writeText("1", null);//我们还没有收到上传
   }
   writer.endElement("percentage");
   writer.startElement("clientId", input);
   writer.writeText(input.getClientId(context), null);
   writer.endElement("clientId");
   writer.endElement("progress");
  } catch(Exception e){
   //做一些错误记录...
  }
  }else{
   //正常的译码请求.
  ...

  五、 正常的译码行为

  在正常的编译期间,文件上传生成器从请求属性中检索FileItem,正是在此处它被过滤器所设置,并且更新该组件的值绑定。然后,该会话中的进度被更新到100%,这样在页面上的JavaScript就可以把组件送入第3个阶段。

//正常的译码请求.
if(requestMap.get(clientId).toString().equals("file")){
try{
 HttpServletRequest request = (HttpServletRequest)extContext.getRequest();
 FileItem fileData = (FileItem)request.getAttribute(clientId);
 if(fileData != null) input.setSubmittedValue(fileData);
 //现在我们需要清除与该项相关的任何进度
 extContext.getSessionMap().put("FileUpload.Progress." + input.getClientId(context),new Double(100));
}catch(Exception e){
 throw new RuntimeException("不能处理文件上传" +" - 请配置过滤器.",e);
}
}

  客户端JavaScript负责向服务器发出进度请求并通过不同阶段来移动组 件。为了简化处理所有的浏览器特定的XMLHttpRequest对象的问题,我选用了Matt Krause提供的AjaxRequest.js库。该库最大限度地减少我们需要编写的JavaScript代码的数量,同时可以使这个组件正常工作。也 许把这部分JavaScript代码打包为该组件的一部分,然后从PhaseListener生成它更好一些,但是,我已经通过定义一个到JSP页面上的 JavaScript库的链接来尽力使得它简单。

  组件中的getProgressBarJavaScript方法被调用以生成 JavaScript。使JavaScript正常工作通常是实现AJAX组件最困难的部分;不过我想,下面的代码已经非常清晰易于理解了。尽管在我的示 例中JavaScript是嵌入到Java代码中的,但是把它放到一个外部独立的文件中也许更好一些。在本文中,我只是想使问题更为简单些且只关心本文的 主题。下面是一个将由组件生成的JavaScript的示例。其中假定,fileUpload1是被赋值到该文件组件的客户端JSF Id,而uploadForm是HTML表单的Id。

function refreshProgress(){
 // 假定我们正在进入到阶段2.
 document.getElementById('fileUpload1_stage1').style.display = 'none';
 document.getElementById('fileUpload1_stage2').style.display = '';
 document.getElementById('fileUpload1_stage3').style.display = 'none';
 //创建AJAX寄送
 AjaxRequest.post(
 {
  //指定正确的参数,以便
  //该组件在服务器端被正确处理
  'parameters':{ 'uploadForm':'uploadForm',
  'fileUpload1':'fileUpload1',
  'jsf.component.UIFileUpload':'1',
  'ajax.abortPhase':'4' } //Abort at Phase 4.
  //指定成功处理相应的回调方法.
  ,'onSuccess':function(req) {
  var xml = req.responseXML;
  if( xml.getElementsByTagName('clientId').length == 0) {
   setTimeout('refreshProgress()',200); return;
  }
  var clientId = xml.getElementsByTagName('clientId');
  clientId = clientId[0].firstChild.nodeValue + '_progressBar';
  //从XML获取百分比
  var percentage = xml.getElementsByTagName('percentage')[0].firstChild.nodeValue;
  var innerSpans = document.getElementById(clientId).getElementsByTagName('span');
  document.getElementById(clientId + 'label').innerHTML = Math.round(percentage) + '%';
  //基于当前进度,设置这些span的式样类。
  for(var i=0;i<innerSpans.length;i++){
   if(i < percentage){
    innerSpans[i].className = 'active';
   }else{
    innerSpans[i].className = 'passive';
   }
  }
  //如果进度不是100,我们需要继续查询服务器以实现更新.
  if(percentage != 100){
   setTimeout('refreshProgress()',400);
  } else {
   //文件上传已经完成,我们现在需要把该组件送入到第3个阶段.
   document.getElementById('fileUpload1_stage1').style.display = 'none';
   document.getElementById('fileUpload1_stage2').style.display = 'none';
   document.getElementById('fileUpload1_stage3').style.display = '';
  }
 }
});
}
return builder.toString();

  六、 结论

   我很希望,本文能够在有关如何使得文件上传更具有用户友好性,并且把AJAX和JavaServer Faces用于实现高级用户接口组件的可能性方面引发你的进一步思考。毫无疑问,本文中的方案比较冗长并且有可能得到进一步的改进。我希望你能详细地分析 一下本文中所提供的完整的源代码来深入理解本文中所讨论的概念。

转载于:https://blog.51cto.com/zhuxianzhong/60107

AJAX+JSF组件实现高性能的文件上载相关推荐

  1. html jsf ajax blur,是否可以使用JSF ajax更新非JSF组件(纯HTML)?

    是否可以更新页面中不是JSF组件的部分? 否.待更新组件必须由提供UIViewRoot#findComponent(),以便JSF可以找到它们,对其进行调用encodeAll(),捕获生成的HTML输 ...

  2. primefaces_PrimeFaces扩展中的全新JSF组件

    primefaces PrimeFaces扩展团队很高兴宣布即将推出的3.0.0主要版本的几个新组件. 我们的新提交者Francesco Strazzullo为该项目提供了" Turbo B ...

  3. PrimeFaces Extensions中的全新JSF组件

    PrimeFaces扩展团队很高兴宣布即将推出的3.0.0主要版本的几个新组件. 我们的新提交人Francesco Strazzullo为该项目提供了" Turbo Boost", ...

  4. 2.JSF 2006年大事纪:Exadel携RichFaces加入JSF组件库竞赛

    导读: Exadel无疑是2006年度JSF天空上最耀眼的明星之一.继年初以一款支持JSF的开发工具Exadel Studio一举攻下JSF可视化页面编辑器的城池之后,年中凭借Ajax4jsf又占领了 ...

  5. ajax formdata提交上传,Ajax提交用FormData()上传文件

    1.form声明如下 2.ajax设置如下 var formData = new FormData(document.getElementById("form")); $.ajax ...

  6. ajax如何请求json文件,简单的ajax请求加载外部json文件

    我在学习ajax ....我试图从json文件发出一个基本请求,它与我的index.html位于同一个文件夹中,但由于某种原因它说未定义:(我可以看到错误是可变的人,但我不能赶上为什么它未定义.... ...

  7. jQuery+php+ajax实现无刷新上传文件功能

    2019独角兽企业重金招聘Python工程师标准>>> jQuery+php+ajax实现无刷新上传文件功能,还带有上传进度条动画效果,支持图片.视频等大文件上传. js代码: &l ...

  8. 开发自定义JSF组件(4) 保存状态与恢复状态

    2019独角兽企业重金招聘Python工程师标准>>> 完整的教材: 开发自定义JSF组件(1) HelloWorld 开发自定义JSF组件(2) 使用Render渲染器 开发自定义 ...

  9. php通过ajax下载文件,PHP使用ajax的post方式下载excel文件简单示例

    本文实例讲述了PHP使用ajax的post方式下载excel文件.分享给大家供大家参考,具体如下: 项目需求,前端发起ajax请求,后端生成excel并下载,同时需要在header头中,带上token ...

最新文章

  1. CocoaPosd使用详解
  2. Spark练习 - 提交作业到集群 - submit job via cluster
  3. 深度学习之循环神经网络(10)GRU简介
  4. 【Android游戏开发详细过程2】Android平台飞机大战游戏APP设计与实现
  5. 捷径|Instagram去水印教程
  6. windows查看WIFI无线网络密码
  7. python精通 epub_精通Python自然语言处理 pdf epub mobi txt 下载
  8. Html 设置整个页面的背景颜色
  9. 源码:三星键盘输入法 安卓开发者福音
  10. 关于2014年相关人脸检测识别的几个论文摘要翻译
  11. PMP项目管理与ACP敏捷管理哪一个更有用?
  12. Mac OS X 系统目录结构
  13. ICSE (2022). Nessie的阅读记录
  14. 支付宝小程序身份认证(拉取人脸识别 认证功能 +详细案例)
  15. 概念整理ia32/x86/amd64/ia64/arm64
  16. 中国互联网企业VS美国互联网企业
  17. 【模型检测学习笔记】9:Binary Decision Diagrams
  18. 2023 年用于 Python 移动应用程序开发的流行工具
  19. linux下smbd安装使用
  20. O365邮箱问题处理

热门文章

  1. latex下载对一篇文章的引用(.bib格式)
  2. 2021年中国AIoT产业全景图谱
  3. 不只是华为/阿里/百度/小米/京东,AIoT已然成为资本与新兴企业都认可的赚钱方向...
  4. 本田、大众宣布智能路口研究新进展 以安全为重点
  5. 图表对比详解:亚马逊、微软和谷歌云的机器学习即服务哪家强
  6. 在相同的后端上重新设计前端是什么效果? | 每日趣闻
  7. 开玩笑写代码获奥斯卡?计算机图形专家这样 5 次捧回大奖!
  8. 坦白讲!90%的数据分析师都不合格!!
  9. Template Method(模板方法)模式
  10. Hibernate4.x之Session