Apache Tapestry 快速入门与集成示例
以示例工程讲解 Apache Tapestry 5.9.1 快速入门,提供完整可跑的 pom、页面类、模板与 web.xml,演示编译运行、curl 验证、表单登录、事件处理、IoC 注入、本地化及 JVM 堆参数验证。
Apache Tapestry 快速入门与集成示例
一、环境信息
本篇工程在以下环境完整实测:创建、构建、启动、页面渲染与各扩展特性均有真实命令回显:
| 项目 | 实测值 |
|---|---|
| 操作系统 | Ubuntu 22.04.5 LTS(CentOS 8 步骤一致,包管理命令差异见安装指南) |
| JDK | OpenJDK 17.0.20.1(/usr/lib/jvm/java-17-openjdk-amd64) |
| Maven | 3.9.9(/opt/apache-maven-3.9.9) |
| 应用框架 | Apache Tapestry 5.9.1(全套 -jakarta 构件) |
| Web 服务器 | 内嵌 Jetty 11.0.15(jakarta.servlet 5.0) |
| 工程目录 | /root/tapestry-demo |
| 端口 | 8080 |
二、前置条件
安装 JDK 17 与 Maven(Ubuntu 22.04):
DEBIAN_FRONTEND=noninteractive apt-get install -y openjdk-17-jdk-headless curl unzip
curl -sL -o /tmp/maven.tgz https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.tar.gz
tar -xzf /tmp/maven.tgz -C /opt
ln -sf /opt/apache-maven-3.9.9/bin/mvn /usr/local/bin/mvn
也可以直接用官方脚手架生成本篇同款工程再按第三节校对文件(脚手架构件为 org.apache.tapestry:quickstart:5.9.1;注意 5.9 线没有 tapestry-simple-archetype 这个构件):
cd /root
mvn -B archetype:generate \
-DarchetypeGroupId=org.apache.tapestry \
-DarchetypeArtifactId=quickstart \
-DarchetypeVersion=5.9.1 \
-DgroupId=com.example \
-DartifactId=tapestry-demo \
-Dversion=1.0-SNAPSHOT \
-Dpackage=com.example \
-DinteractiveMode=false
三、完整可运行工程
工程全部关键文件如下,逐个落盘即可复现(Tapestry 约定:页面类放 pages 子包、组件放 components、IoC 模块放 services;.tml 模板与类同包名放在 resources 下)。
3.1 pom.xml 全文
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>tapestry-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<name>tapestry-demo Tapestry 5 Application</name>
<packaging>war</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<tapestry-version>5.9.1</tapestry-version>
<maven-compiler-version>3.8.1</maven-compiler-version>
<maven-surefire-version>3.1.2</maven-surefire-version>
<maven-war-version>3.3.1</maven-war-version>
<maven-jetty-version>11.0.15</maven-jetty-version>
<servlet-version>5.0.0</servlet-version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.tapestry</groupId>
<artifactId>tapestry-core-jakarta</artifactId>
<version>${tapestry-version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.17.2</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.17.2</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.17.2</version>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>${servlet-version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-version}</version>
<configuration>
<release>11</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>${maven-war-version}</version>
</plugin>
<plugin>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>${maven-jetty-version}</version>
<configuration>
<scan>10</scan>
<webAppSourceDirectory>src/main/webapp</webAppSourceDirectory>
<webApp>
<contextPath>/</contextPath>
</webApp>
<httpConnector>
<port>8080</port>
</httpConnector>
</configuration>
</plugin>
</plugins>
</build>
</project>
⚠️ 关键点:必须使用 tapestry-core-jakarta(而非 tapestry-core)。5.9 系列同时发布 javax 与 jakarta 两套构件,quickstart 脚手架默认生成的是 javax 版,与 Jetty 11 组合会报 NoClassDefFoundError: javax/servlet/Filter。本篇 pom 为精简版(去掉了 quickstart 额外引入的 tapestry-webresources、tapestry-test 与 apache-staging 仓库声明),实测可正常构建运行;如需脚手架的完整功能清单,先生成再比对。
3.2 页面类
src/main/java/com/example/pages/Index.java(首页,含事件处理方法):
package com.example.pages;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.tapestry5.Block;
import org.apache.tapestry5.EventContext;
import org.apache.tapestry5.SymbolConstants;
import org.apache.tapestry5.annotations.InjectPage;
import org.apache.tapestry5.annotations.Log;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.annotations.Symbol;
import org.apache.tapestry5.services.HttpError;
import org.apache.tapestry5.services.ajax.AjaxResponseRenderer;
import java.time.ZonedDateTime;
public class Index {
private static final Logger logger = LogManager.getLogger(Index.class);
@Inject
private AjaxResponseRenderer ajaxResponseRenderer;
@Property
@Inject
@Symbol(SymbolConstants.TAPESTRY_VERSION)
private String tapestryVersion;
@InjectPage
private About about;
@Inject
private Block block;
Object onActivate(EventContext eventContext) {
return eventContext.getCount() > 0 ?
new HttpError(404, "Resource not found") :
null;
}
Object onActionFromLearnMore() {
about.setLearn("LearnMore");
return about;
}
@Log
void onComplete() {
logger.info("Complete call on Index page");
}
@Log
void onAjax() {
logger.info("Ajax call on Index page");
ajaxResponseRenderer.addRender("middlezone", block);
}
public ZonedDateTime getCurrentTime() {
return ZonedDateTime.now();
}
}
src/main/java/com/example/pages/Login.java(登录页,演示账号 admin / q1w2e3r4):
package com.example.pages;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.tapestry5.alerts.AlertManager;
import org.apache.tapestry5.annotations.InjectComponent;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.corelib.components.Form;
import org.apache.tapestry5.corelib.components.PasswordField;
import org.apache.tapestry5.corelib.components.TextField;
import org.apache.tapestry5.ioc.annotations.Inject;
public class Login {
private static final Logger logger = LogManager.getLogger(Login.class);
@Inject
private AlertManager alertManager;
@InjectComponent
private Form login;
@InjectComponent("email")
private TextField emailField;
@InjectComponent("password")
private PasswordField passwordField;
@Property
private String email;
@Property
private String password;
void onValidateFromLogin() {
if (!email.equals("admin"))
login.recordError(emailField, "Try with user: admin");
if (!password.equals("q1w2e3r4"))
login.recordError(passwordField, "Try with password: q1w2e3r4");
}
Object onSuccessFromLogin() {
logger.info("Login successful!");
alertManager.success("Welcome aboard!");
return Index.class;
}
void onFailureFromLogin() {
logger.warn("Login error!");
alertManager.error("I'm sorry but I can't log you in!");
}
}
src/main/java/com/example/services/AppModule.java(应用 IoC 模块,保留 quickstart 生成的核心内容):
package com.example.services;
import org.apache.tapestry5.SymbolConstants;
import org.apache.tapestry5.commons.MappedConfiguration;
import org.apache.tapestry5.commons.OrderedConfiguration;
import org.apache.tapestry5.http.services.Request;
import org.apache.tapestry5.http.services.RequestFilter;
import org.apache.tapestry5.http.services.RequestHandler;
import org.apache.tapestry5.http.services.Response;
import org.apache.tapestry5.ioc.ServiceBinder;
import org.apache.tapestry5.ioc.annotations.Contribute;
import org.apache.tapestry5.ioc.annotations.ImportModule;
import org.apache.tapestry5.ioc.annotations.Local;
import org.apache.tapestry5.ioc.services.ApplicationDefaults;
import org.apache.tapestry5.ioc.services.SymbolProvider;
import org.apache.tapestry5.modules.Bootstrap4Module;
import org.slf4j.Logger;
import java.io.IOException;
import java.util.UUID;
@ImportModule(Bootstrap4Module.class)
public class AppModule {
public static void bind(ServiceBinder binder) {
// binder.bind(MyServiceInterface.class, MyServiceImpl.class);
}
public static void contributeFactoryDefaults(MappedConfiguration<String, Object> configuration) {
configuration.override(SymbolConstants.APPLICATION_VERSION, "1.0-SNAPSHOT");
configuration.override(SymbolConstants.PRODUCTION_MODE, false);
}
public static void contributeApplicationDefaults(MappedConfiguration<String, Object> configuration) {
configuration.add(SymbolConstants.SUPPORTED_LOCALES, "en,zh_CN");
configuration.add(SymbolConstants.HMAC_PASSPHRASE, "change this immediately-" + UUID.randomUUID());
configuration.add(SymbolConstants.ENABLE_PAGELOADING_MASK, Boolean.FALSE);
configuration.add(SymbolConstants.ENABLE_HTML5_SUPPORT, Boolean.FALSE);
}
@Contribute(SymbolProvider.class)
@ApplicationDefaults
public static void setupEnvironment(MappedConfiguration<String, Object> configuration) {
configuration.add(SymbolConstants.JAVASCRIPT_INFRASTRUCTURE_PROVIDER, "jquery");
}
public RequestFilter buildTimingFilter(final Logger log) {
return new RequestFilter() {
public boolean service(Request request, Response response, RequestHandler handler) throws IOException {
long startTime = System.currentTimeMillis();
try {
return handler.service(request, response);
} finally {
long elapsed = System.currentTimeMillis() - startTime;
log.info("Request time: {} ms", elapsed);
}
}
};
}
@Contribute(RequestHandler.class)
public void addTimingFilter(OrderedConfiguration<RequestFilter> configuration, @Local RequestFilter filter) {
configuration.add("Timing", filter);
}
}
3.3 模板(.tml)
src/main/resources/com/example/pages/Index.tml:
<html t:type="layout" title="Index | tapestry-demo"
xmlns:t="http://tapestry.apache.org/schema/tapestry_5_4.xsd">
<div class="row">
<div class="col-12">
<p>
<img src="${asset:context:images/apache-tapestry.png}" alt="${message:greeting}" title="${message:greeting}" width="600"/>
</p>
<h1 id="title">${message:greeting}</h1>
<p>The current time is: <strong>${currentTime}</strong></p>
<p><t:actionlink t:id="learnmore" class="btn btn-primary btn-large">Learn more »</t:actionlink></p>
</div>
</div>
<div class="row">
<div class="col-12">
<h2>Normal link</h2>
<p>Click the link below and the page will refresh with event <code>complete</code></p>
<p><t:eventlink event="complete" class="btn btn-primary">Complete »</t:eventlink></p>
</div>
</div>
<div class="row">
<t:zone t:id="middlezone" class="col-12"></t:zone>
</div>
<div class="row">
<div class="col-12">
<h2>Ajax link</h2>
<p>Click the bottom link to update just the middle column with Ajax call with event <code>ajax</code></p>
<p><t:eventlink event="ajax" zone="middlezone" class="btn btn-primary">Ajax »</t:eventlink></p>
</div>
</div>
<t:block t:id="block">
<h2>Ajax updated</h2>
<p>I'v been updated through AJAX call</p>
<p>The current time is: <strong>${currentTime}</strong></p>
</t:block>
</html>
⚠️ 上面的模板依赖布局组件 com.example.components.Layout(t:type="layout")与其模板 Layout.tml,以及 src/main/webapp/css/app.css、src/main/webapp/images/apache-tapestry.png、favicon.ico。这些文件用脚手架生成工程即可自动获得(本篇第三节其余文件与脚手架产物一致,仅把演示账号与支持语言按上文调整);从零手写时建议同样以脚手架为底,再逐文件核对。
src/main/resources/com/example/pages/Login.tml(表单模板):
<html t:type="layout" title="Login | tapestry-demo"
xmlns:t="http://tapestry.apache.org/schema/tapestry_5_4.xsd">
<div class="row justify-content-md-center">
<div class="col-12">
<t:form t:id="login" class="form-signin">
<h2 class="form-signin-heading">Please sign in</h2>
<t:textfield t:id="email" placeholder="Email address" class="form-control"/>
<t:passwordfield t:id="password" placeholder="Password" class="form-control"/>
<t:submit value="Sign in" class="btn btn-primary btn-large"/>
</t:form>
</div>
</div>
</html>
3.4 消息资源与 web.xml
src/main/resources/com/example/pages/Index.properties(英文消息目录;${message:greeting} 取值于此):
greeting=Welcome to Tapestry 5! We hope that this project template will get you going in style.
src/main/resources/com/example/pages/Index_zh_CN.properties(中文消息目录,供 6.4 节本地化实测):
greeting=欢迎来到 Tapestry 5!该工程模板已就绪。
⚠️ 注意区分:com/example/pages/Index.properties 这类与页面类同名的文件是页面消息目录,会被正常加载;而传统资料说的 app.properties 符号配置文件在 5.9.1 中不被加载(实测无效),不要往里面写 tapestry.* 符号。
src/main/webapp/WEB-INF/web.xml(必须是 jakarta 5.0 描述符):
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
version="5.0">
<display-name>Tapestry 5 Quickstart Project</display-name>
<context-param>
<param-name>tapestry.app-package</param-name>
<param-value>com.example</param-value>
</context-param>
<context-param>
<param-name>tapestry.development-modules</param-name>
<param-value>com.example.DevelopmentModule</param-value>
</context-param>
<filter>
<filter-name>app</filter-name>
<filter-class>org.apache.tapestry5.TapestryFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>app</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>ERROR</dispatcher>
</filter-mapping>
</web-app>
四、编译运行
cd /root/tapestry-demo
export MAVEN_OPTS="-Xms256m -Xmx512m"
mvn -B -DskipTests package
实测关键回显(首次构建含依赖下载):
[INFO] Building war: /root/tapestry-demo/target/tapestry-demo-1.0-SNAPSHOT.war
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 06:14 min
启动内嵌 Jetty:
nohup mvn -B jetty:run > /root/jetty.log 2>&1 &
实测启动日志关键行:
[INFO] jetty-11.0.15; built: 2023-04-11T18:37:53.775Z; git: 5bc5e562c8d05c5862505aebe5cf83a61bdbcb96; jvm 17.0.20.1+1-1-22.04-Ubuntu
[INFO] Started ServerConnector@dc3eda6{HTTP/1.1, (http/1.1)}{0.0.0.0:8080}
[INFO] Started Server@239b98cb{STARTING}[11.0.15,sto=0] @7972ms
[INFO] Scan interval sec = 10
Tapestry 初始化完成横幅:
2026-10-12 12:33:52,364 [main ] INFO org.apache.tapestry5.http.TapestryFilter Startup status:
Application 'app' (version 1.0-SNAPSHOT) startup time: 217 ms to build IoC Registry, 654 ms overall.
五、curl 验证回显
首页状态与关键内容:
curl -s -o /dev/null -w "INDEX HTTP %{http_code}\n" http://127.0.0.1:8080/
curl -s http://127.0.0.1:8080/ | grep -oE "Apache Tapestry Framework \(version [0-9.]+\)|Welcome to Tapestry 5[^<]*" | head -2
实测回显:
INDEX HTTP 200
Apache Tapestry Framework (version 5.9.1)
Welcome to Tapestry 5! We hope that this project template will get you going in style.
登录页表单结构:
curl -s http://127.0.0.1:8080/login | grep -oE "<title>[^<]+</title>|action=\"[^\"]+\" method=\"post\"" | head -2
实测回显:
<title>Login | tapestry-demo</title>
action="/login.login" method="post"
登录成功流程(先取 t:formdata 签名字段再 POST;页面 HTML 中该 input 的 value 属性在 name 之前):
FD=$(curl -s http://127.0.0.1:8080/login | grep -oE "<input value=\"[^\"]+\" name=\"t:formdata\"" | sed "s/<input value=\"//;s/\" name=\"t:formdata\"//")
curl -sL -X POST http://127.0.0.1:8080/login.login \
--data-urlencode "t:formdata=$FD" \
--data-urlencode "email=admin" \
--data-urlencode "password=q1w2e3r4" \
--data-urlencode "submit_0=Sign in" | grep -oE "Welcome aboard!" | head -1
实测回显:
Welcome aboard!
登录失败流程(错误凭据)实测回显页面中包含:
Try with user: admin
Try with password: q1w2e3r4
I'm sorry but I can't log you in!
事件链接验证(/index:complete 触发 onComplete 后重定向回首页,post-redirect-get):
curl -s "http://127.0.0.1:8080/index:complete" -o /dev/null -w "EVENT HTTP %{http_code}\n"
实测回显:
EVENT HTTP 302
六、扩展特性
6.1 表单提交
Tapestry 表单由 t:form 组件驱动,字段组件(t:textfield / t:passwordfield)与页面类字段通过 t:id 与同名 @Property 字段自动绑定。生命周期按方法命名约定触发:提交进入 onValidateFromLogin()(t:id 为 login 的 Form 校验阶段,用 login.recordError() 登记错误即视为提交失败),失败走 onFailureFromLogin(),成功走 onSuccessFromLogin() 并重定向回 Index(实测 302 后首页展示成功提示 Welcome aboard!)。表单会自动生成 t:formdata 隐藏字段,由 tapestry.hmac-passphrase 签名携带客户端状态,多实例部署时必须统一口令。
6.2 事件处理
页面事件通过 URL 段路由:/index:complete 触发 Index 的 onComplete()(实测 302 重定向回 /index);t:eventlink 生成普通链接事件,t:actionlink 触发组件动作事件;带 zone 属性的事件链接走 Ajax——Index.tml 中 zone="middlezone" 的 Ajax 链接触发 onAjax(),方法内 ajaxResponseRenderer.addRender("middlezone", block) 把 t:block 内容局部刷新到 Zone 区域,无需整页刷新。事件方法也支持 onActivate(EventContext) 处理页面激活上下文,Index.java 实测在带多余路径参数时返回 404(new HttpError(404, "Resource not found"))。
6.3 IoC 服务注入
Tapestry 内置 Tapestry-IoC 注册中心,注入方式三类:字段注解 @Inject(如 Index 中的 AjaxResponseRenderer)、@InjectPage/@InjectComponent(页面与组件代理)、@Symbol(注入符号值,如 tapestryVersion)。自定义服务在 AppModule 中定义:bind() 声明接口与实现,或用 buildXxx() 方法内联构造;示例工程中的 TimingFilter 即通过 buildTimingFilter() 定义、@Contribute(RequestHandler.class) 挂入请求处理管线,实测每个页面请求日志中都有耗时行:
2026-10-12 12:34:xx,xxx [qtp1749250768-20 ] INFO com.example.services.AppModule.TimingFilter Request time: 8 ms
6.4 本地化
消息目录按"页面类同名 + 下划线语言后缀"组织:Index.properties(英文)与 Index_zh_CN.properties(中文),模板中 ${message:greeting} 自动取当前 locale 对应目录。受支持语言由 SymbolConstants.SUPPORTED_LOCALES 控制(AppModule 中已设为 en,zh_CN)。实测:
curl -s http://127.0.0.1:8080/ | grep -oE "data-locale=\"[^\"]+\"|Welcome to Tapestry 5" | head -2
curl -s -H "Accept-Language: zh-CN" http://127.0.0.1:8080/ | grep -oE "data-locale=\"[^\"]+\"|欢迎来到 Tapestry 5" | head -2
实测回显:
data-locale="en"
Welcome to Tapestry 5
data-locale="zh_CN"
欢迎来到 Tapestry 5
即默认渲染英文,请求头携带 zh-CN 时页面 locale 与问候语自动切换为中文。
七、JVM 堆验证
启动命令中的 MAVEN_OPTS="-Xms256m -Xmx512m" 会传入 JVM(见第四节)。两种方式确认实际生效:
ps -ef | grep "[j]ava" | grep -oE "\-Xms256m -Xmx512m"
jps -v | grep Launcher | cut -c1-60
实测回显:
-Xms256m -Xmx512m
4666 Launcher -Xms256m -Xmx512m -Dclassworlds.conf=/opt/apache-mav
运行期堆水位抽查:
jstat -gcutil 5314 | head -3
实测回显:
S0 S1 E O M CCS YGC YGCT FGC FGCT CGC CGCT GCT
0.00 85.66 58.55 50.73 99.14 97.24 12 0.085 0 0.000 4 0.009 0.093
八、常见问题
Q1:构建报 Could not find artifact org.apache.maven.plugins:maven-surefire-plugin:jar:3.1?
脚手架把 surefire 版本写成不存在的 3.1,改为 3.1.2 即可(本篇 pom 已是修正版)。
Q2:启动后页面 503,日志报 NoClassDefFoundError: javax/servlet/Filter?
pom 用了 javax 版 tapestry-core。Jetty 11 / Tomcat 10+ 环境必须全套改用 -jakarta 构件,并把 web.xml 换成 3.4 节的 jakarta 5.0 描述符。
Q3:表单 POST 返回 500?
提交体缺少 t:formdata(或取值时正则与页面属性顺序不符导致空值),Tapestry 不会回填字段,校验方法里对 null 调 equals() 抛 NPE。按第五节的命令先取 t:formdata 再 POST;生产代码中校验方法建议先判空。
Q4:mvn test 集成测试失败(NoClassDefFound org/openqa/selenium/bidi/HasBiDi)?
quickstart 的集成测试需要 Selenium 浏览器环境,无图形界面服务器跑不了,构建用 mvn package -DskipTests,只跑单测用 -Dtest='com.example.unit.*,com.example.tapestry.unit.*'。
Q5:改了 pom 里 jetty 的 <port> 不生效或 -Djetty.http.port=9090 无效?
端口只认 pom 中 <httpConnector><port> 的显式配置,实测 -Djetty.http.port 会被忽略;占用时错误为 Failed to bind to 0.0.0.0/0.0.0.0:8080: Address already in use。
Q6:中文消息目录不生效?
确认两点:AppModule 的 SUPPORTED_LOCALES 包含 zh_CN;消息文件名为 Index_zh_CN.properties 且位于与页面类相同的包路径下(src/main/resources/com/example/pages/)。
九、参考链接
- Apache Tapestry 官网:https://tapestry.apache.org/
- Getting Started:https://tapestry.apache.org/getting-started.html
- User Guide:https://tapestry.apache.org/user-guide.html
- Component Templates(.tml 模板规范):https://tapestry.apache.org/component-templates.html
- Forms and Validation(表单与校验):https://tapestry.apache.org/forms-and-validation.html
- Ajax and Zones:https://tapestry.apache.org/ajax-and-zones.html
- Tapestry IoC(服务与注入):https://tapestry.apache.org/ioc.html
- Localization(本地化):https://tapestry.apache.org/localization.html
- Release Notes 5.9.0:https://tapestry.apache.org/release-notes-590.html
- Javadoc:https://tapestry.apache.org/current/apidocs/