当前位置: 首页 > news >正文

JSP自定义标签深度解析:从原理到实战,掌握视图层组件化核心

1. 项目概述:为什么今天还要聊JSP标签?

如果你是一位JavaWeb开发者,尤其是经历过从Servlet直接拼接HTML字符串“远古时代”走过来的朋友,看到“JSP标签”这个主题,可能会觉得有些“复古”。在前后端分离、各种现代化框架(Spring Boot、Vue、React)大行其道的今天,JSP似乎已经退居二线,成了教科书里的“历史遗迹”。但恰恰是这种想法,让我觉得有必要好好聊聊它。我最近在帮团队维护一个遗留的老旧管理系统,也指导过不少新人从零搭建JavaWeb项目,发现一个现象:很多开发者对JSP的认知停留在“在HTML里写Java代码”的层面,对JSP标签,特别是自定义标签库(Tag Library)的强大与优雅,知之甚少,甚至完全跳过。这导致他们在面对一些特定场景(如老项目维护、快速开发内部工具、需要服务端渲染的简单页面)时,要么束手无策,要么用极其笨重的方式实现。

所以,这篇内容不是一篇怀旧文,而是一篇“实用考古”和“底层原理”的深度解析。我们将聚焦于JSP标签中最核心、也最实用的两部分:通用标签迭代标签。我会带你彻底弄明白,它们到底是什么,解决了什么问题,以及如何在你手头的项目中(无论是遗留系统还是新项目中的特定模块)优雅地使用它们。你会发现,掌握这些知识,不仅能让你轻松应对那些“年久失修”的老代码,更能深刻理解Web视图层组件化的思想源头,这种思想在当今的Thymeleaf模板、甚至某些前端框架的指令中依然清晰可见。对于使用IntelliJ IDEA 2021+进行开发、需要处理包含JSP的老项目,或者在学习JavaWeb完整案例时感到困惑的同学,这篇文章将提供一条清晰的路径。

2. JSP标签的本质:从脚本片段到声明式组件

在深入通用和迭代标签之前,我们必须先统一认知:JSP标签到底是什么?它要解决什么痛点?

回想一下最原始的JSP写法:在HTML中嵌入大量的<% ... %>脚本片段和<%= ... %>表达式。一个显示用户列表的页面可能长这样:

<table> <% List<User> userList = (List<User>)request.getAttribute("userList"); for(User user : userList) { %> <tr> <td><%= user.getId() %></td> <td><%= user.getName() %></td> <td><%= user.getEmail() %></td> </tr> <% } %> </table>

这段代码的问题非常明显:

  1. 可读性差:HTML结构被Java代码割裂,前后端逻辑混杂,难以一眼看清页面结构。
  2. 可维护性低:UI逻辑(循环、显示)和业务逻辑(获取数据)纠缠在一起。修改样式需要在一堆Java代码中寻找HTML标签。
  3. 复用性为零:这段显示用户的表格逻辑无法在其他页面直接复用。
  4. 不利于分工:页面设计师(或前端工程师)几乎无法在这种文件上工作。

JSP标签(JSP Tag)就是为了解决这些问题而生的。它的核心思想是“声明式编程”。我们不再告诉页面“如何一步步地获取数据、循环、拼接HTML”,而是声明“这里需要显示一个用户列表”。具体的“如何做”被封装到了标签背后的Java类(Tag Handler)中。

JSP标签主要分为两大类:

  • JSTL(JSP Standard Tag Library):由Apache实现的一套标准标签库,提供了核心(c)、格式化(fmt)、SQL(sql)、XML(x)等常用功能。我们常说的<c:forEach>就是JSTL的迭代标签。
  • 自定义标签(Custom Tag):开发者根据自身业务需求编写的标签。这正是“通用标签”和“迭代标签”可以大展身手的地方。

一个关键的实操心得:很多新手在IDEA中导入老旧的JavaWeb项目时,遇到JSP页面报错(例如The taglib directive is not recognized),往往是因为缺少对应的标签库描述文件(*.tld文件)或JSTL的jar包。在IDEA 2021中,你需要确保:

  1. jstl.jarstandard.jar(或使用新的如javax.servlet.jsp.jstl-apijavax.servlet.jsp.jstl)添加到项目的WEB-INF/lib目录下,并标记为库。
  2. 对于自定义标签,确保WEB-INF目录下或META-INF目录中存在正确的.tld文件。
  3. 在IDEA的Project Structure -> Modules -> Dependencies中,确认这些库已被正确添加。完成这些,IDEA才能对JSP文件中的标签提供高亮、提示和跳转支持,极大提升开发效率。

3. 通用标签详解:封装视图逻辑的利器

所谓“通用标签”,我指的是那些用于执行单一、特定功能的自定义标签,例如格式化日期、显示权限按钮、生成分页导航条、输出特定的HTML片段等。它们不像迭代标签那样处理集合,而是封装一个独立的视图组件或逻辑单元。

3.1 通用标签的实现原理与生命周期

创建一个自定义通用标签,主要涉及三个部分:

  1. 标签处理器类(Tag Handler Class):一个继承自TagSupportSimpleTagSupport的Java类。SimpleTagSupport(JSP 2.0引入)更简单,是现代自定义标签的首选。
  2. 标签库描述文件(TLD, Tag Library Descriptor):一个XML文件,定义了标签的名称、处理器类、属性等信息。
  3. JSP页面中的声明与使用:通过<%@ taglib %>指令引入,然后像使用HTML标签一样使用。

我们以一个最简单的例子开始:创建一个<my:hello name="World" />标签,它在页面上输出 “Hello, World!”。

第一步:编写标签处理器(SimpleTagSupport)

package com.example.tag; import javax.servlet.jsp.JspException; import javax.servlet.jsp.JspWriter; import javax.servlet.jsp.tagext.SimpleTagSupport; import java.io.IOException; public class HelloTag extends SimpleTagSupport { private String name; // 对应标签的name属性 // 属性setter方法,由容器自动调用 public void setName(String name) { this.name = name; } @Override public void doTag() throws JspException, IOException { JspWriter out = getJspContext().getOut(); out.print("Hello, " + (name != null ? name : "Guest") + "!"); } }

关键点解析SimpleTagSupportdoTag()方法就是标签的执行体。getJspContext().getOut()获取了指向当前响应输出的流。任何通过out.print()写入的内容,都会直接输出到页面对应的位置。

第二步:定义TLD文件/WEB-INF/下创建myTags.tld

<?xml version="1.0" encoding="UTF-8"?> <taglib xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-jsptaglibrary_2_1.xsd" version="2.1"> <tlib-version>1.0</tlib-version> <short-name>my</short-name> <!-- 定义前缀 --> <uri>/WEB-INF/myTags.tld</uri> <!-- 唯一标识URI,JSP页面通过它引用 --> <tag> <name>hello</name> <!-- 标签名 --> <tag-class>com.example.tag.HelloTag</tag-class> <!-- 处理器类 --> <body-content>empty</body-content> <!-- 标签体内容为空 --> <attribute> <name>name</name> <!-- 属性名 --> <required>false</required> <!-- 是否必需 --> <rtexprvalue>true</rtexprvalue> <!-- 是否接受运行时表达式,如EL表达式 --> </attribute> </tag> </taglib>

第三步:在JSP中使用

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="my" uri="/WEB-INF/myTags.tld" %> <!-- 引入标签库 --> <html> <body> <%-- 使用自定义标签 --%> <my:hello /> <br/> <my:hello name="${userName}" /> <!-- 可以使用EL表达式动态传值 --> </body> </html>

注意<body-content>元素非常重要。它定义了标签体是否允许包含内容。empty表示无标签体。如果是scriptless,则表示可以有JSP内容(文本、EL、标准动作、其他标签),但不能有脚本片段(<% ... %>)。tagdependent表示将标签体内容原样传递给处理器,不做处理。

3.2 通用标签的进阶应用:带标签体的内容控制

更强大的通用标签可以控制其标签体的内容是否执行、重复执行或修改后输出。这通过getJspBody()JspFragmentinvoke()方法实现。

假设我们需要一个权限控制标签<my:hasPermission role="admin">,只有当前用户角色是admin时,才显示标签体内的内容。

package com.example.tag; import javax.servlet.jsp.JspException; import javax.servlet.jsp.tagext.SimpleTagSupport; import java.io.IOException; public class PermissionTag extends SimpleTagSupport { private String role; public void setRole(String role) { this.role = role; } @Override public void doTag() throws JspException, IOException { // 模拟从session中获取用户角色 String userRole = (String) getJspContext().findAttribute("currentUserRole"); // 也可以从PageContext中获取:((PageContext)getJspContext()).getSession().getAttribute("role"); if ("admin".equals(role) && "admin".equals(userRole)) { // 权限匹配,执行标签体内容 if (getJspBody() != null) { getJspBody().invoke(null); // 参数null表示输出到默认的JspWriter(即页面) } } else { // 权限不匹配,什么都不输出,标签体被忽略 // 也可以输出一些替代内容,如:getJspContext().getOut().write("<span>无权限查看</span>"); } } }

TLD文件中对应的tag定义,<body-content>需要设置为scriptless

<my:hasPermission role="admin"> <button>删除系统数据</button> <!-- 只有admin用户能看到这个按钮 --> </my:hasPermission> <my:hasPermission role="user"> 欢迎普通用户! </my:hasPermission>

实操心得:这种模式非常有用,它彻底将权限判断逻辑从JSP页面中剥离。页面作者只需要关心“在什么条件下显示什么内容”,而具体的权限验证规则完全由后端的Tag Handler决定。这比在JSP里写<c:if test="${sessionScope.user.role == 'admin'}">更加清晰和可维护,因为规则可能很复杂,并且可能在多个地方复用。

4. 迭代标签深度解析:超越<c:forEach>的自定义循环

JSTL的<c:forEach>功能强大,足以应对90%的迭代场景。但当你需要更特殊的迭代逻辑时,自定义迭代标签就派上用场了。例如:分页迭代(每次只处理一页的数据)、树形结构递归渲染、基于复杂条件的分组迭代等。

4.1 实现一个简单的迭代标签

我们实现一个<my:simpleLoop items="${list}" var="item" count="5">标签,它只迭代集合的前N个元素。

package com.example.tag; import javax.servlet.jsp.JspException; import javax.servlet.jsp.tagext.SimpleTagSupport; import java.io.IOException; import java.util.Collection; import java.util.Iterator; public class SimpleLoopTag extends SimpleTagSupport { private Object items; // 被迭代的集合 private String var; // 每次迭代暴露的变量名 private int count = Integer.MAX_VALUE; // 最大迭代次数,默认全部 // setter 方法... public void setItems(Object items) { this.items = items; } public void setVar(String var) { this.var = var; } public void setCount(int count) { this.count = count > 0 ? count : 0; } @Override public void doTag() throws JspException, IOException { if (items == null) return; Collection<?> itemCollection = null; if (items instanceof Collection) { itemCollection = (Collection<?>) items; } else if (items instanceof Object[]) { itemCollection = java.util.Arrays.asList((Object[]) items); } else { throw new JspException("'items' attribute must be a Collection or Array"); } Iterator<?> it = itemCollection.iterator(); int processed = 0; while (it.hasNext() && processed < count) { Object currentItem = it.next(); processed++; // 将当前迭代项放入PageContext,供标签体访问 getJspContext().setAttribute(var, currentItem); // 执行标签体(输出一次) if (getJspBody() != null) { getJspBody().invoke(null); } } // 迭代结束后,移除变量,避免污染作用域(良好实践) getJspContext().removeAttribute(var); } }

TLD中需要定义三个属性:items,var,count

<%@ taglib prefix="my" uri="/WEB-INF/myTags.tld" %> <% List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry", "Date", "Elderberry"); request.setAttribute("fruitList", fruits); %> <my:simpleLoop items="${fruitList}" var="fruit" count="3"> <p>Fruit: ${fruit}</p> <!-- 只会输出Apple, Banana, Cherry --> </my:simpleLoop>

4.2 迭代标签的核心:理解JspFragmentinvoke的协作

自定义迭代标签的精髓在于:每次循环中,我们通过getJspBody().invoke(...)来执行一次标签体的内容invoke()方法可以接受一个Writer参数。如果传null,则输出到页面主输出流。你也可以传入一个临时的StringWriter,先捕获标签体的输出内容,进行加工处理后再输出。

例如,实现一个<my:toUpperCaseLoop>标签,迭代集合并将每个元素标签体的输出转换为大写:

@Override public void doTag() throws JspException, IOException { // ... 获取items和var的逻辑同上 ... while (it.hasNext()) { Object currentItem = it.next(); getJspContext().setAttribute(var, currentItem); if (getJspBody() != null) { // 1. 创建一个StringWriter来捕获标签体输出 StringWriter sw = new StringWriter(); // 2. 将捕获用的Writer传给invoke方法 getJspBody().invoke(sw); // 3. 获取捕获到的字符串,进行处理 String bodyContent = sw.toString(); String upperCaseContent = bodyContent.toUpperCase(); // 4. 将处理后的内容输出到真实页面 getJspContext().getOut().print(upperCaseContent); } } getJspContext().removeAttribute(var); }
<my:toUpperCaseLoop items="${fruitList}" var="fruit"> Fruit: ${fruit} <br/> <!-- 输出将会是:FRUIT: APPLE <br/> FRUIT: BANANA ... --> </my:toUpperCaseLoop>

这个机制极其强大。它意味着你可以在迭代过程中,完全控制标签体内容的生成过程,实现过滤、装饰、转换等复杂逻辑。

4.3 复杂迭代案例:实现一个分页迭代标签

这是一个更贴近实际需求的例子。假设我们有一个从数据库查出的完整列表allItems,但我们不想一次性全部渲染到页面(可能数据量很大),而是希望标签内部自己处理分页逻辑,每次只渲染一页。

标签设计:<my:pageLoop items="${allItems}" var="item" pageSize="10" pageParam="p">

public class PageLoopTag extends SimpleTagSupport { private Object items; private String var; private int pageSize = 10; private String pageParam = "page"; // 用于从请求中获取当前页码的参数名 @Override public void doTag() throws JspException, IOException { // 1. 参数检查和转换 List<?> itemList = convertToLis(items); if (itemList == null || itemList.isEmpty()) return; // 2. 计算分页信息 int currentPage = 1; try { String pageStr = ((PageContext)getJspContext()).getRequest().getParameter(pageParam); currentPage = pageStr != null ? Integer.parseInt(pageStr) : 1; } catch (NumberFormatException e) { currentPage = 1; } int totalItems = itemList.size(); int totalPages = (int) Math.ceil((double) totalItems / pageSize); currentPage = Math.max(1, Math.min(currentPage, totalPages)); // 确保页码在有效范围 int startIndex = (currentPage - 1) * pageSize; int endIndex = Math.min(startIndex + pageSize, totalItems); // 3. 将分页信息设置为作用域变量,方便在标签体外使用(例如显示分页导航) getJspContext().setAttribute("currentPage", currentPage); getJspContext().setAttribute("totalPages", totalPages); getJspContext().setAttribute("totalItems", totalItems); // 4. 迭代当前页的数据 for (int i = startIndex; i < endIndex; i++) { Object currentItem = itemList.get(i); getJspContext().setAttribute(var, currentItem); // 可选:暴露索引或其他信息 getJspContext().setAttribute(var + "_index", i); if (getJspBody() != null) { getJspBody().invoke(null); } } // 5. 清理变量 getJspContext().removeAttribute(var); getJspContext().removeAttribute(var + "_index"); // 注意:分页信息变量(currentPage等)可以保留,供外部使用 } // ... 辅助方法 convertToList 和 setter 省略 ... }

在JSP中使用:

<my:pageLoop items="${bigDataList}" var="data" pageSize="15" pageParam="p"> <div class="data-item">${data.id}: ${data.name}</div> </my:pageLoop> <div class="pagination"> 共 ${totalItems} 条,第 ${currentPage}/${totalPages} 页 <c:if test="${currentPage > 1}"> <a href="?p=${currentPage - 1}">上一页</a> </c:if> <c:if test="${currentPage < totalPages}"> <a href="?p=${currentPage + 1}">下一页</a> </c:if> </div>

这个案例的价值:它将数据分页的视图逻辑完全封装了起来。Servlet控制器只需要一次性查询所有数据(或缓存起来),无需关心分页计算。JSP页面通过一个声明式的标签就完成了分页渲染,并且标签还自动暴露了分页元数据,便于生成导航链接。这比在JSP里写一堆<c:forEach begin="${(page-1)*size}" end="${page*size-1}" ...>要清晰和健壮得多。

5. 实战:在IDEA中高效开发与调试JSP标签

理论讲完了,我们来点实在的。如何在IntelliJ IDEA 2021+这个现代IDE中,愉快地开发和调试这些“传统”技术呢?

5.1 项目配置与TLD支持

  1. 创建Web模块:确保你的项目是一个Web项目(包含WEB-INF目录)。如果是从旧项目导入,IDEA通常能自动识别。
  2. 添加依赖:对于JSTL,在pom.xml(Maven)中添加:
    <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency>
    或者手动将jar包放入WEB-INF/lib
  3. TLD文件支持:将你的.tld文件放在WEB-INF下或某个jar包的META-INF下。IDEA会自动索引它。在JSP文件中输入<%@ taglib prefix=时,IDEA会提供URI的自动补全。
  4. 启用JSP高亮与提示:确保File -> Settings -> Languages & Frameworks -> JSP中,JSP support是启用的。对于.jsp文件,IDEA会提供EL表达式、JSTL标签的代码补全和错误检查。

5.2 调试自定义标签

调试自定义标签和调试普通Java类没有区别,因为Tag Handler就是普通的Java类。

  1. 以Debug模式启动Tomcat:在IDEA中配置好你的Tomcat运行配置,并以Debug模式启动。
  2. 在Tag Handler中设置断点:在你的doTag()方法或者任何setter方法里打上断点。
  3. 访问JSP页面:在浏览器中访问使用了该标签的JSP页面。
  4. 触发调试:当请求执行到你的标签时,IDEA的Debug窗口会自动弹出,程序会在断点处暂停。此时你可以查看所有变量的值、单步执行,就像调试Servlet一样。

一个非常重要的技巧:如果修改了Tag Handler的代码,需要重新编译并更新到Tomcat。最可靠的方式是使用IDEA的“热部署”功能(Update classes and resources),或者直接重启Tomcat应用。仅仅保存Java文件是不够的。

5.3 常见问题排查速查表

问题现象可能原因解决方案
JSP页面报错:The tag [my:hello] declared in TLD... not found1. TLD文件路径错误或未找到。
2. Tag Handler类未编译或未放入WEB-INF/classes(或jar包)。
3.taglib指令的uri与TLD中定义的<uri>不匹配。
1. 检查TLD文件位置,确保在WEB-INF下或jar包的META-INF下。
2. 检查项目输出目录,确保.class文件已生成。
3. 核对JSP中的uri和TLD文件中的<uri>是否完全一致。
标签属性接收不到值(始终为null)1. TLD中属性定义<rtexprvalue>false,但JSP中使用了EL表达式。
2. Tag Handler中的setter方法命名不符合规范(非setXxx)。
3. 属性类型不匹配(如String传给了int)。
1. 确保TLD中<rtexprvalue>true</rtexprvalue>
2. setter方法必须为public void set属性名(类型)
3. 检查类型,对于基本类型,容器会尝试转换,失败则报错。
标签体内容未执行1. TLD中<body-content>设置为empty
2. 在doTag()中未调用getJspBody().invoke()
3.getJspBody()为null(标签在JSP中被写成了空标签<my:tag/>,但TLD允许body)。
1. 根据需求将<body-content>改为scriptlesstagdependent
2. 在需要输出标签体的逻辑分支中调用invoke()
3. 使用前先判断if (getJspBody() != null)
EL表达式在标签属性中不生效1. TLD中属性<rtexprvalue>false(默认是false!)。
2. 页面未启用EL(老项目可能禁用了)。
1.这是最常见的原因!务必在TLD中为需要EL的属性设置<rtexprvalue>true</rtexprvalue>
2. 在JSP页面顶部检查是否有<%@ page isELIgnored="true" %>,改为false或删除。
IDEA中JSP标签无代码提示1. 未正确引入标签库。
2. TLD文件未被IDEA索引。
3. 项目模型不是Web项目。
1. 确认<%@ taglib %>指令正确无误。
2. 尝试重启IDEA或手动刷新文件缓存(File -> Invalidate Caches)。
3. 确保模块已正确配置为Web模块(有web.xmlWEB-INF目录)。

6. 从JSP标签到现代视图技术的思考

虽然我们今天深入探讨了JSP自定义标签,但我们必须正视其应用场景正在缩小。对于全新的、追求高性能和高交互性的项目,前后端分离是更主流的选择。那么,JSP标签的知识是否过时了?

我认为恰恰相反,它的思想是永恒的:逻辑与表现的分离、组件的复用、声明式的UI描述。这些思想在以下场景中依然鲜活:

  1. Thymeleaf、FreeMarker等现代模板引擎:它们提供了比JSP标签更强大、更安全的表达式语言和处理器,但其“片段(Fragments)”、“布局(Layouts)”和自定义方言(Dialect)的概念,与JSP标签库一脉相承。理解JSP标签,能帮你更快掌握这些模板引擎的扩展机制。
  2. 服务端渲染(SSR):在某些需要SEO、首屏速度的Web应用(如官网、内容管理系统)中,服务端渲染卷土重来。无论是Node.js的模板,还是Spring MVC配合Thymeleaf,其组件化渲染的思想与JSP标签异曲同工。
  3. 维护遗留系统:海量的银行、政府、企业内部系统仍然运行着基于JSP的架构。理解和能够修改、扩展其中的自定义标签,是维护这类系统的必备技能。
  4. 快速原型与内部工具:对于开发一个简单的后台管理页面,使用JSP(配合JSTL和少量自定义标签)可能比搭建一套完整的前后端分离框架要快得多。

所以,我的建议是:不要将JSP标签视为一个必须掌握的“未来技术”,而是作为一个重要的“底层原理”和“实用工具”来学习。当你理解了如何将一段复杂的视图逻辑封装成一个简单的标签时,你就掌握了视图层设计的核心奥秘。这种能力,在你学习任何其他视图技术时,都会让你受益匪浅。

最后,关于那个“在JSP中实现MP4视频播放”的热搜词,其实原理很简单:视频播放本质是前端HTML5的<video>标签。JSP的角色只是动态生成这个标签的src属性,该属性指向一个能输出视频流的Servlet(该Servlet从文件系统或数据库读取视频字节流,并设置正确的Content-Type)。这里并不需要复杂的自定义标签,一个EL表达式足矣:<video src="${pageContext.request.contextPath}/videoStream?id=${videoId}" controls></video>。真正的技术难点在于视频流的断点续传、格式兼容和性能优化,而这些都是在Servlet中处理的,与JSP标签关系不大。

http://www.jsqmd.com/news/1232715/

相关文章:

  • C++家谱管理系统:二叉树表示法与递归遍历实战解析
  • 基于空间殖民算法的交互式单木点云分割:C++实现与工程实践
  • 运动损伤诊断与康复技术解析
  • 【DeepAgents 从入门到精通】DeepAgents初识
  • 瑞德克斯平台:从风险提示切入的路径归纳
  • CSS 类选择器组合
  • [具身智能-588]:RS422、I2C、SPI 底层都是面向字节传输,都是主从模式,都可以 1 对多。他们各自如何区分从设备的? 如何在单字节基础上传输目标设备特定地址空间的?
  • AI自我纠正机制:从错误中学习的突破性技术
  • 深入解析EDMA3寄存器:错误处理与状态监控实战指南
  • 金华管道疏通选哪家 2026金华全域正规疏通商家TOP5综合测评榜单 - 北京金修达天津维修部
  • 微电网鲁棒优化:应对可再生能源波动的关键技术
  • C++实现三维点云平面拟合:PCA算法原理与工程实践
  • VideoSceneMaster项目实战:基于Qt + ONNX Runtime + FAISS 的本地视频智能检索工具
  • 2025智能座舱芯片竞争格局与关键技术解析
  • 2026年显卡市场前瞻与性价比选购指南
  • 从Jupyter到K8s:机器学习模型生产化落地的系统性实践
  • 深度学习实时学习技术解析与实践指南
  • [具身智能-589]:RS485 / I2C / SPI / CAN 总线完整选型对比
  • C2000 Bootloader与ePWM配置全解析:从引导表构建到精准PWM输出
  • 猫抓插件:三步搞定浏览器资源嗅探,轻松下载网页视频的终极指南
  • 2026 年现阶段,青阳优秀的摄影培训学校供应商哪家靠谱,别再盲目学了,这才是摄影师的真相 - 行业推荐官【官方】
  • AI 时代,什么能力会越来越昂贵?
  • 2026年,这些口碑超棒的老山檀香源头工厂品牌,你知道几个?
  • SoC电源域管理实战:从概念到Jacinto 6 Plus低功耗设计
  • Python实战:AES/DES五种加密模式原理与代码实现详解
  • 从LangChain迁移到自研框架:生产环境实战经验
  • 深入解析SoC时钟域管理:以Jacinto 6 Plus CD_WKUPAON为例
  • Grok驱动的汽车超级智能体:具身智能与多模态意图交互架构
  • AI驱动下的阅文集团IP多模态开发实践
  • AI认知优化不能靠直觉:一个六因素决策框架的工程实践