0

0

如何测试容器外的JSP页面

巴扎黑

巴扎黑

发布时间:2017-08-10 15:12:49

|

1779人浏览过

|

来源于php中文网

原创

jsp测试技术

开发web应用程序最恼人的一点就是想要测试的话你就必须向将其部署好。当然,并不是所有部分都这样。如果你是经过了精心的设计的话,你可以在java程序中测试业务逻辑。你可以在应用服务器不运行的情况下测试数据访问、接口以及存储过程。不过如果是测试gui的话(由jsp所产生的html),你就必须向将其部署,然后才可能测试。

很多的团队求助于Sellenium,Mercury或是其他的一些工具通过web server来测试GUI。然而,即使是页面的内容不变但样式变了得情况也会让测试变得脆弱不堪。其他的团队使用Cactus解决这种脆弱性,或是用HtmlUnit、HttpUnit这样原始的工具来监测web应用程序所生成的HTML。对于这些问题,我会在另一系列的blog之中来谈论。

本文之中我会介绍一种简单易行的技术,它使用JUnit或是HtmlUnit来测试Jsp页面,并且完全脱离容器。这项技术的优势也在此。

你不必一定保持容器的运行,甚至存在。你可以在选择特定的webserver之前就测试你的Jsp。

你不必在每次修改后重新部署,因而编辑/编译/测试的过程会更迅速。

你可以使用测试优先开发的方式来持续的构建Jsp。

容器外测试Jsp技术之所以并不盛行是因为Jsp在设计上就运行于容器内的。设计者从未过多的想过容器外运行的可能。因此由Jsp编译器的所生成代码往往依赖于容器所提供的诸多组件。即使是生成Jsp代码的工具也假定了你已经有一个成功部署的web应用程序在运行。因此,为了在容器外运行,你就要开发出相应的这些工具和组件。

依赖管理的抱怨

为什么这么多框架和工具的设计者们总期望你生活在他们提供的狭小世界中?为什么我必须先构建出完整的web应用才能编译Jsp?为什么这些东西一定要运行在容器中?信息隐藏早在10年前就已经是优秀软件设计的基本信条了。我们这个行业何时才能认真对待它?

编译Jsp

测试Jsp的第一步是将其编译为servlet。实现这一步,我们还需要先将Jsp转换成Java格式。Apache提供了一个叫做Jasper的工具,我们调用Jasper为MyPage.jsp创建一个Java格式的源文件MyPage_jsp.java。然后,你就可以使用你最喜欢的IDE编译这个文件成Servlet。

可惜Jasper并非是设计用在命令行中使用的,或者说并不是完全这样设计的。但Jasper确有一个main函数用来处理命令行参数,而且通过调用java org.apache.jasper.JspC就能够轻易调用它了。不过,Jasper期望它所运行的环境与容器环境是保持一致的。你要确保classpath中有了很多apache的Jar文件,而且它要能找到web应用程序的web.xml。它还需要能够找到包含web应用程序Jar以及TLD文件等的WEB-INF目录。简而言之,Jasper需要能找到一个完整的web应用程序。

如果事情更糟的话,除非是与TOMCAT的调用方式保持完全一致,否则某些特定的Jasper版本(我用的是tomcat 5.5.20)存在一些bug,它生成的代码会有一些错误。

第一点要做的虽然繁琐但还算简单,你需要创建好正确的目录以及文件结构,然后在Ant(Classpath更容易控制)中调用Jasper。第二点就需要一定的研究和测试才能让它跑起来。以下就是能成功运行的ant文件。JspC的调用出现在最后一个任务中。

 

 

 

 

 

 

 

 

 

   

     

   

   

   

     

   

   

     

   

   

   

     

   

 

 

   

   

 

 

   

   

     

   

 

 

   

   

 

 

   

     

   

   

     

   

    

   

 

 

   

   

     

     

     

       

         

       

       

         

       

       

         

        

       

         

       

       

     

   

   

         includes="**/jsp/**/*.class"

      />

 

当然,你要让所有标准文件以及目录都在${build.war.home}之下以确保工作。如果你在你的Jsp之中使用了自定义tag的话,还要确保所有相应的TLD文件都在你的TLD目录之中。

要注意的是,在ant文件中调用Jspc的命令行,而不是使用Tomcat所提供的JspC的Ant Task。因为我发现当你有自定义tag的时候它无法正确运行。也许我犯了糊涂,或者JspC中确实有bug。不过我所发现的唯一能让Jasper生成正确代码的方式是从命令行调用它,并明确的传递Jsp文件路径作为命令行的参数!如果你依靠它的Ant Task或是使用命令行来搜索所有web应用中的Jsp进行编译的话,它就会生成错误的代码。(请参阅这篇blog)

现在我们有了Java文件,让我们来分析一下它。首先,请看下面的Jsp文件。

  List loanRecords = (List) request.getAttribute("loanRecords");

  if (loanRecords.size() > 0) {

%>

 

   

   

   

   

 

 

    for (int i = 0; i

      LoanRecord loanRecord = (LoanRecord) loanRecords.get(i);

  %>

 

">

   

   

   

   

 

 

    }

  %>

ID Title Due date Fine

   

   

   

   

  }

%>

下面则是Jasper所生成的代码。

package com.objectmentor.library.jsp.WEB_002dINF.pages.patrons.books;

import javax.servlet.*;

import javax.servlet.http.*;

import javax.servlet.jsp.*;

import com.objectmentor.library.utils.DateUtil;

import com.objectmentor.library.web.controller.patrons.LoanRecord;

import java.util.List;

public final class loanRecords_jsp extends org.apache.jasper.runtime.HttpJspBase

    implements org.apache.jasper.runtime.JspSourceDependent {

  private static java.util.List _jspx_dependants;

  public Object getDependants() {

    return _jspx_dependants;

  }

  public void _jspService(HttpServletRequest request, HttpServletResponse response)

        throws java.io.IOException, ServletException {

    JspFactory _jspxFactory = null;

    PageContext pageContext = null;

    HttpSession session = null;

    ServletContext application = null;

    ServletConfig config = null;

    JspWriter out = null;

    Object page = this;

    JspWriter _jspx_out = null;

    PageContext _jspx_page_context = null;

    try {

      _jspxFactory = JspFactory.getDefaultFactory();

      response.setContentType("text/html");

      pageContext = _jspxFactory.getPageContext(this, request, response,

                  null, true, 8192, true);

      _jspx_page_context = pageContext;

      application = pageContext.getServletContext();

      config = pageContext.getServletConfig();

      session = pageContext.getSession();

      out = pageContext.getOut();

      _jspx_out = out;

      out.write('/n');

      out.write('/n');

      out.write('/n');

  List loanRecords = (List) request.getAttribute("loanRecords");

  if (loanRecords.size() > 0) {

      out.write("/n");

      out.write("

/n");

      out.write(" 

/n");

      out.write("   

/n");

      out.write("    

/n");

      out.write("   

/n");

      out.write("   

/n");

      out.write(" 

/n");

      out.write("  ");

    for (int i = 0; i

      LoanRecord loanRecord = (LoanRecord) loanRecords.get(i);

      out.write("/n");

      out.write(" 

      out.print(i%2==0?"even":"odd");

      out.write("/">/n");

      out.write("   

/n");

      out.write("   

/n");

      out.write("   

/n");

      out.write("   

/n");

      out.write(" 

/n");

      out.write("  ");

    }

      out.write("/n");

      out.write("

ID Title Due date Fine
");

      out.print(loanRecord.id);

      out.write("/n");

      out.write("   

");

      out.print(loanRecord.title);

      out.write("/n");

      out.write("   

");

      out.print(DateUtil.dateToString(loanRecord.dueDate));

      out.write("/n");

      out.write("   

");

      out.print(loanRecord.fine.toString());

      out.write("/n");

      out.write("   

/n");

  }

    } catch (Throwable t) {

      if (!(t instanceof SkipPageException)){

        out = _jspx_out;

        if (out != null && out.getBufferSize() != 0)

          out.clearBuffer();

        if (_jspx_page_context != null) _jspx_page_context.handlePageException(t);

      }

    } finally {

      if (_jspxFactory != null) _jspxFactory.releasePageContext(_jspx_page_context);

    }

  }

}

最后的抱怨

这个类为什么要声明为final呢?如果我想创建一个测试的stub派生类呢?为什么有人会觉得生成类如此不可冒犯以至于我都无法覆写它。

仔细读过这段代码你就会发现,要想使用这个servlet的实例我们需要HttpServletRequest以及HttpServletResponse的实例。

更仔细研读一下我们就会发现servlet将所有的HTML写到JspWriter的实例中,而JspWriter是从PageContext中获得的。如果我们能够创建一个JspWriter的mock up的版本来保存所有的这些HTML,再为PageContext创建一个mock up的版本来派送mock JspWriter,那么我们就能在我们的测试中访问这些HTML了。

幸运的是,Tomcat的设计人员把JspWriter的创建放入到了JspFactory的工厂类中。而这个工厂类是可以覆写的!这就意味着我们可以在servlet之中获得我们自己的JspWriter类而不用改变servlet。需要的就是下面这段代码。

  class MockJspFactory extends JspFactory {

    public PageContext getPageContext(Servlet servlet, ServletRequest servletRequest, ServletResponse servletResponse, String string, boolean b, int i, boolean b1) {

      return new MockPageContext(new MockJspWriter());

    }

    public void releasePageContext(PageContext pageContext) {

    }

    public JspEngineInfo getEngineInfo() {

      return null;

    }

  }

现在,我们需要的是mock Jspwriter。为了便于展示,我用了下面的:

MockJspWriter

package com.objectmentor.library.web.framework.mocks;

import javax.servlet.jsp.JspWriter;

import java.io.IOException;

public class MockJspWriter extends JspWriter {

  private StringBuffer submittedContent;

  public MockJspWriter(int bufferSize, boolean autoFlush) {

    super(bufferSize, autoFlush);

    submittedContent = new StringBuffer();

  }

  public String getContent() {

    return submittedContent.toString();

  }

  public void print(String arg0) throws IOException {

    submittedContent.append(arg0);

  }

  public void write(char[] arg0, int arg1, int arg2) throws IOException {

    for (int i=0; i

      submittedContent.append(String.valueOf(arg0[arg1++]));

  }

  public void write(String content) throws IOException {

    submittedContent.append(content);

  }

  // lots of uninteresting methods elided.  I just gave them

  // degenerate implementations.  (e.g. {})

}

无需关心那些我省略掉的未实现方法,我认为只需要关心那些足够使得我的测试得以运行的方法即可。对于剩下的,我只会使用其退化实现。

我的IDE对于创建这些mock类非常有帮助。它能够自动化的构建方法原型,并为那些接口或是抽象类所需要实现的方法给出退化的实现。

同样的用类似方法创建出MockPageContext,MockHttpServletRequest以及MockHttpServletResponse类。

MockPageContext

package com.objectmentor.library.web.framework.mocks;

import javax.servlet.*;

import javax.servlet.http.*;

import javax.servlet.jsp.*;

import java.io.IOException;

import java.util.Enumeration;

public class MockPageContext extends PageContext {

  private final JspWriter out;

  private HttpServletRequest request;

  public MockPageContext(JspWriter out) {

    this.out = out;

    request = new MockHttpServletRequest();

  }

  public JspWriter getOut() {

    return out;

  }

  public ServletRequest getRequest() {

    return request;

  }

  // lots of degenerate functions elided.

}

MockHttpServletRequest

package com.objectmentor.library.web.framework.mocks;

import javax.servlet.*;

import javax.servlet.http.*;

import java.io.*;

import java.security.Principal;

import java.util.*;

public class MockHttpServletRequest implements HttpServletRequest {

  private String method;

  private String contextPath;

  private String requestURI;

  private HttpSession session = new MockHttpSession();

  private Map parameters = new HashMap();

  private Map attributes = new HashMap();

  public MockHttpServletRequest(String method, String contextPath,

                                String requestURI) {

    super();

    this.method = method;

    this.contextPath = contextPath;

    this.requestURI = requestURI;

  }

  public MockHttpServletRequest() {

    this("GET");

  }

  public MockHttpServletRequest(String method) {

    this(method, "/Library", "/Library/foo/bar.jsp");

  }

  public String getContextPath() {

    return contextPath;

  }

  public String getMethod() {

    return method;

  }

  public String getRequestURI() {

    return requestURI;

  }

  public String getServletPath() {

    return requestURI.substring(getContextPath().length());

  }

  public HttpSession getSession() {

    return session;

  }

  public HttpSession getSession(boolean arg0) {

    return session;

  }

  public Object getAttribute(String arg0) {

    return attributes.get(arg0);

  }

  public String getParameter(String arg0) {

    return (String) parameters.get(arg0);

  }

  public Map getParameterMap() {

    return parameters;

  }

  public Enumeration getParameterNames() {

    return null;

  }

  public void setSession(HttpSession session) {

    this.session = session;

  }

  public void setParameter(String s, String s1) {

    parameters.put(s, s1);

  }

  public void setAttribute(String name, Object value) {

    attributes.put(name, value);

  }

  // Lots of degenerate methods elided.

}

MockHttpServletResponse

package com.objectmentor.library.web.framework.mocks;

import javax.servlet.ServletOutputStream;

import javax.servlet.http.*;

import java.io.*;

import java.util.Locale;

public class MockHttpServletResponse implements HttpServletResponse {

  // all functions are implemented to be degenerate.

}

有了这些mock对象,现在我就可以创建一个loanRecords_jsp的servlet实例并且开始调用它!我的头一个测试用例就像下面这样:

<br/>
  public void testSimpleTest() throws Exception {
    MockJspWriter jspWriter = new MockJspWriter();
    MockPageContext pageContext = new MockPageContext(jspWriter);
    JspFactory.setDefaultFactory(new MockJspFactory(pageContext));
    HttpJspBase jspPage = new loanRecords_jsp();
    HttpServletRequest request = new MockHttpServletRequest();
    HttpServletResponse response = new MockHttpServletResponse();
 <br/>
    jspPage._jspInit();
    jspPage._jspService(request, response);
 <br/>
    assertEquals("", jspWriter.getContent());
  }
<br/>

就像预期的一样,测试失败了。这是因为还有些内容还没补充上,不过所剩无多。如果你仔细的看过Jsp文件,你就会发现它调用了request.getAttribute(“loanRecords”)并且期望返回一个List。但因为目前的测试并未为这样的属性赋值,从而导致了代码抛出了异常。

要想成功让servlet输出HTML,我们还需要加载这个属性。然后,我们就可以使用HtmlUnit来解析此HTML并且编写相应的单元测试。

HtmlUnit非常的容易使用,尤其是在测试所产生的像是本例这样的web pages上。我这里还有篇文章详细的介绍了它。

下面就是最终测试加载属性的测试,它通过htmlunit来检测HTML,并且做出正确的判断:

<br/>
package com.objectmentor.library.jspTest.books.patrons.books;
 <br/>
import com.gargoylesoftware.htmlunit.*;
import com.gargoylesoftware.htmlunit.html.*;
import com.objectmentor.library.jsp.WEB_002dINF.pages.patrons.books.loanRecords_jsp;
import com.objectmentor.library.utils.*;
import com.objectmentor.library.web.controller.patrons.LoanRecord;
import com.objectmentor.library.web.framework.mocks.*;
import junit.framework.TestCase;
import org.apache.jasper.runtime.HttpJspBase;
 <br/>
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.jsp.*;
import java.util.*;
 <br/>
public class LoanRecordsJspTest extends TestCase {
  private MockPageContext pageContext;
  private MockJspWriter jspWriter;
  private JspFactory mockFactory;
  private MockHttpServletResponse response;
  private MockHttpServletRequest request;
  private WebClient webClient;
  private TopLevelWindow dummyWindow;
 <br/>
  protected void setUp() throws Exception {
    jspWriter = new MockJspWriter();
    pageContext = new MockPageContext(jspWriter);
    mockFactory = new MockJspFactory(pageContext);
 <br/>
    JspFactory.setDefaultFactory(mockFactory);
    response = new MockHttpServletResponse();
    request = new MockHttpServletRequest();
    webClient = new WebClient();
    webClient.setJavaScriptEnabled(false);
    dummyWindow = new TopLevelWindow("", webClient);
  }
 <br/>
  public void testLoanRecordsPageGeneratesAppropriateTableRows() throws Exception {
    HttpJspBase jspPage = new loanRecords_jsp();
    jspPage._jspInit();
 <br/>
    List<LoanRecord> loanRecords = new ArrayList<LoanRecord>();
    addLoanRecord(loanRecords,
                  "99",
                  "Empire",
                  DateUtil.dateFromString("2/11/2007"),
                  new Money(4200));
    addLoanRecord(loanRecords,
                  "98",
                  "Orbitsville",
                  DateUtil.dateFromString("2/12/2007"),
                  new Money(5200));
 <br/>
    request.setAttribute("loanRecords", loanRecords);
 <br/>
    jspPage._jspService(request, response);
 <br/>
    StringWebResponse stringWebResponse = new StringWebResponse(jspWriter.getContent());
    HtmlPage page = HTMLParser.parse(stringWebResponse, dummyWindow);
    HtmlElement html = page.getDocumentElement();
 <br/>
    HtmlTable table = (HtmlTable) html.getHtmlElementById("loanRecords");
    List<HtmlTableRow> rows = table.getHtmlElementsByTagName("tr");
    assertEquals(3, rows.size());
 <br/>
    assertEquals("even", classOfElement(rows.get(1)));
    assertEquals("odd", classOfElement(rows.get(2)));
 <br/>
    List<HtmlTableDataCell> firstRowCells = rows.get(1).getCells();
    assertEquals(4, firstRowCells.size());
 <br/>
    List<HtmlTableDataCell> secondRowCells = rows.get(2).getCells();
    assertEquals(4, secondRowCells.size());
 <br/>
    assertLoanRecordRowEquals("99", "Empire", "02/11/2007", "$42.00", firstRowCells);
    assertLoanRecordRowEquals("98", "Orbitsville", "02/12/2007", "$52.00", secondRowCells);
  }
 <br/>
  private String classOfElement(HtmlTableRow firstDataRow) {return firstDataRow.getAttributeValue("class");}
 <br/>
  private void assertLoanRecordRowEquals(String id, String title, String dueDate, String fine, List<HtmlTableDataCell> rowCells) {
    assertEquals(id, rowCells.get(0).asText());
    assertEquals(title, rowCells.get(1).asText());
    assertEquals(dueDate, rowCells.get(2).asText());
    assertEquals(fine, rowCells.get(3).asText());
  }
 <br/>
  private void addLoanRecord(List<LoanRecord> loanRecords, String id, String title, Date dueDate, Money fine) {
    LoanRecord loanRecord = new LoanRecord();
    loanRecord.id = id;
    loanRecord.title = title;
    loanRecord.dueDate = dueDate;
    loanRecord.fine = fine;
 <br/>
    loanRecords.add(loanRecord);
  }
 <br/>
  private class MockJspFactory extends JspFactory {
    private PageContext pageContext;
    public MockJspFactory(PageContext pageContext) {
      this.pageContext = pageContext;
    }
 <br/>
    public PageContext getPageContext(Servlet servlet, ServletRequest servletRequest, ServletResponse servletResponse, String string, boolean b, int i, boolean b1) {
      return pageContext;
    }
 <br/>
    public void releasePageContext(PageContext pageContext) {
    }
 <br/>
    public JspEngineInfo getEngineInfo() {
      return null;
    }
  }
}
<br/>

<span style="font-size: 9pt;">上述的测试确保了所生成的HTML中表格中的每一行都具有正确的内容。这项测试确实能够测出是否存在这样的表格,并且判断出是否表格的每一行是按照正确的顺序来展现的。同时,它也确保了每一行的相应style。测试忽略了此外的表单以及语法部分。</span>

结论

这篇发表在此的技术能够用来测试几乎所有目前我们所见过的web页面,并且脱离容器,也无需web server的运行。相对来说,它也比较容易去设置,并且非常易于扩展。有了它,你就可以快速的进行编辑、编译、测试的周期性迭代,并且你也能遵循测试驱动开发的原则了。

站长俱乐部购物系统
站长俱乐部购物系统

功能介绍:1、模块化的程序设计,使得前台页面设计与程序设计几乎完全分离。在前台页面采用过程调用方法。在修改页面设计时只需要在相应位置调用设计好的过程就可以了。另外,这些过程还提供了不同的调用参数,以实现不同的效果;2、阅读等级功能,可以加密产品,进行收费管理;3、可以完全可视化编辑文章内容,所见即所得;4、无组件上传文件,服务器无需安装任何上传组件,无需支持FSO,即可上传文件。可限制文件上传的类

下载

(原文链接网址: http://blog.objectmentor.com/articles/category/testing-guis; Robert C. Martin的英文blog网址: http://blog.objectmentor.com/ 

作者简介:Robert C. Martin是Object Mentor公司总裁,面向对象设计、模式、UML、敏捷方法学和极限编程领域内的资深顾问。他不仅是Jolt获奖图书《敏捷软件开发:原则、模式与实践》(中文版)(《敏捷软件开发》(英文影印版))的作者,还是畅销书Designing Object-Oriented C++ Applications Using the Booch Method的作者。Martin是Pattern Languages of Program Design 3和More C++ Gems的主编,并与James Newkirk合著了XP in Practice。他是国际程序员大会上著名的发言人,并在C++ Report杂志担任过4年的编辑。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
pixiv网页版官网登录与阅读指南_pixiv官网直达入口与在线访问方法
pixiv网页版官网登录与阅读指南_pixiv官网直达入口与在线访问方法

本专题系统整理pixiv网页版官网入口及登录访问方式,涵盖官网登录页面直达路径、在线阅读入口及快速进入方法说明,帮助用户高效找到pixiv官方网站,实现便捷、安全的网页端浏览与账号登录体验。

616

2026.02.13

微博网页版主页入口与登录指南_官方网页端快速访问方法
微博网页版主页入口与登录指南_官方网页端快速访问方法

本专题系统整理微博网页版官方入口及网页端登录方式,涵盖首页直达地址、账号登录流程与常见访问问题说明,帮助用户快速找到微博官网主页,实现便捷、安全的网页端登录与内容浏览体验。

194

2026.02.13

Flutter跨平台开发与状态管理实战
Flutter跨平台开发与状态管理实战

本专题围绕Flutter框架展开,系统讲解跨平台UI构建原理与状态管理方案。内容涵盖Widget生命周期、路由管理、Provider与Bloc状态管理模式、网络请求封装及性能优化技巧。通过实战项目演示,帮助开发者构建流畅、可维护的跨平台移动应用。

91

2026.02.13

TypeScript工程化开发与Vite构建优化实践
TypeScript工程化开发与Vite构建优化实践

本专题面向前端开发者,深入讲解 TypeScript 类型系统与大型项目结构设计方法,并结合 Vite 构建工具优化前端工程化流程。内容包括模块化设计、类型声明管理、代码分割、热更新原理以及构建性能调优。通过完整项目示例,帮助开发者提升代码可维护性与开发效率。

20

2026.02.13

Redis高可用架构与分布式缓存实战
Redis高可用架构与分布式缓存实战

本专题围绕 Redis 在高并发系统中的应用展开,系统讲解主从复制、哨兵机制、Cluster 集群模式及数据分片原理。内容涵盖缓存穿透与雪崩解决方案、分布式锁实现、热点数据优化及持久化策略。通过真实业务场景演示,帮助开发者构建高可用、可扩展的分布式缓存系统。

54

2026.02.13

c语言 数据类型
c语言 数据类型

本专题整合了c语言数据类型相关内容,阅读专题下面的文章了解更多详细内容。

29

2026.02.12

雨课堂网页版登录入口与使用指南_官方在线教学平台访问方法
雨课堂网页版登录入口与使用指南_官方在线教学平台访问方法

本专题系统整理雨课堂网页版官方入口及在线登录方式,涵盖账号登录流程、官方直连入口及平台访问方法说明,帮助师生用户快速进入雨课堂在线教学平台,实现便捷、高效的课程学习与教学管理体验。

15

2026.02.12

豆包AI网页版入口与智能创作指南_官方在线写作与图片生成使用方法
豆包AI网页版入口与智能创作指南_官方在线写作与图片生成使用方法

本专题汇总豆包AI官方网页版入口及在线使用方式,涵盖智能写作工具、图片生成体验入口和官网登录方法,帮助用户快速直达豆包AI平台,高效完成文本创作与AI生图任务,实现便捷智能创作体验。

598

2026.02.12

PostgreSQL性能优化与索引调优实战
PostgreSQL性能优化与索引调优实战

本专题面向后端开发与数据库工程师,深入讲解 PostgreSQL 查询优化原理与索引机制。内容包括执行计划分析、常见索引类型对比、慢查询优化策略、事务隔离级别以及高并发场景下的性能调优技巧。通过实战案例解析,帮助开发者提升数据库响应速度与系统稳定性。

56

2026.02.12

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
React 教程
React 教程

共58课时 | 5.2万人学习

TypeScript 教程
TypeScript 教程

共19课时 | 3万人学习

Bootstrap 5教程
Bootstrap 5教程

共46课时 | 3.4万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号