很多 web 应用程序是以数据为中心 — 它们显示数据并允许用户新建或更新数据。这听上去很简单,但真到了一些基本操作,如在数据库中读写数据,情况却非常糟糕。尽管如此,Java Persistence API (JPA) 却极大地减少了必须编写的冗长的样板式代码。我们将看一个使用 JPA 的简单例子。
本文中,将开发一个简单的管理青年足球联赛的应用程序。开始时将建立一个简单的数据模型,用于跟踪球队及其队员。将使用 JPA 完全访问这些数据。以第一个数据模型 Team
开始。清单 1 显示了此类。
Team
数据模型类
@Entity public class Team { .... ....@Id ....@GeneratedValue(strategy = GenerationType.IDENTITY) ....private long id; .... ....private String name; .... ....@OneToMany ....private Collection<Player> players; .... // getters and setters........ } |
这是典型的 JPA 注释类。使用 @Entity
注释来声明该类将映射到数据库。可以选择指定类的表名,或者在使用与类相同的名称时实现约定。其次,您要注释该类的 id
字段。想要它成为表的主键,使用 @Id
注释来声明。从逻辑角度来看,id
并不重要;只将其用于数据库。因为想要数据库充分发挥其价值,使用 @GeneratedValue
注释。
在 清单 1 中,还要声明另一个字段,name
字段。它是球队的名字。请注意该字段没有 JPA 注释。默认情况下,这会映射到同名列,这已符合本文意图。最后,每个球队都有多名队员与之关联。使用 @OneToMany
注释让 JPA 运行时知道这是一个管理关系,球队有多名球员。在 Java 类中,这是 Player
对象的 java.util.Collection
。清单 2 显示 Player
类被引用。
Player
数据模型类
@Entity public class Player { .... ....@Id ....@GeneratedValue(strategy = GenerationType.IDENTITY) ....private long id; .... ....private String firstName; .... ....private String lastName; .... ....private int age; .... ....@ManyToOne (cascade=CascadeType.ALL) ....private Team team; .... // getters and setters } |
清单 2 中显示的 Player
类与 清单 1 中的 Team
类类似。它的字段更多,但大多数情况下不必担心注释这些字段。JPA 将帮助您正确完成。清单 1 和 清单 2 的一个不同之处是如何指定 Player
类与 Team
类的关系。本例中使用 @ManyToOne
注释,因为一个 Team
中有多个 Players
。请注意,还指定了一个级联策略(cascade policy)。参考 JPA 文档,选择适用于您的本应用程序的级联策略。本例中,使用该级联策略可以同时创建新的 Team
和 Player
,JPA 将保存两者,这将方便应用程序开发。
既然已经声明两个类,只要告诉 JPA 运行时如何连接数据库。通过创建 persistence.xml 文件来完成。JPA 运行时需要找到该文件,并使用其中的元数据。最简单的方法是放入 /META-INF 目录,这是源代码的子目录(需要将它放到输出编译类的根目录中)。清单 3显示 persistence.xml 文件。
<persistence version="1.0" ....xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ....xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"> ....<persistence-unit name="soccer"> ........<class>org.developerworks.soccer.model.Team</class> ........<class>org.developerworks.soccer.model.Player</class> ........<properties> ............<property name="hibernate.dialect" value="org.hibernate.dialect.DerbyDialect" /> ............<property name="hibernate.connection.driver_class".... value="org.apache.derby.jdbc.EmbeddedDriver" /> ............<property name="hibernate.connection.url" value="jdbc:derby:soccerorgdb;create=true" /> ............<property name="hibernate.hbm2ddl.auto" value="update" /> ............<property name="hibernate.show_sql" value="true" /> ............<property name="hibernate.connection.characterEncoding" value="UTF-8" /> ............<property name="hibernate.connection.useUnicode" value="true" /> ........</properties> ....</persistence-unit> </persistence> |
回头看看 清单 1 和 清单 2,所有代码都是通用的 JPA 代码。实际上曾使用的都是 JPA 注释和它的常量。对已使用的数据库和 JPA 实现没有什么特别的。正如在 清单 3 中看到的,persistence.xml 文件正是存放这些特定内容的地方。有一些出色的 JPA 实现可用,包括 OpenJPA 和 TopLink(见 参考资料)。已经使用过古老的 Hibernate,因此有几个 Hibernate 相关属性已经指定。这些多数都是简单易懂的,如 JDBC 驱动器和 URL,而且是很有用的,如通知 Hibernate 登陆到正在执行的 SQL (一些您肯定不想放在产品环境中的内容,但对于调试却很重要)。
在 清单 3 中,您还会注意到使用了 Apache Derby 数据库。实际上,使用的是数据库的嵌入式版本。您不必单独启动数据库,且担心配置。此外,在连接 URL 中您也指出应该自动创建数据库,并且您也通知 Hibernate 自动创建模式(这是 hibernate.hbm2ddl.auto
属性)。因此如果只运行应用程序,可以创建数据库及表。这有益与开发,但当然您会希望产品系统设置不一样。现在数据模型代码已经生成,并可通过 JPA 访问,我们将查看开放该数据,以便应用程序能使用它。
如果是五年前创建该应用程序,现在可能会使用一些 Java Server Pages (JSPs) 或 Java Server Faces (JSFs) 或一些其他模板技术。不需要服务器上为该应用程序创建 UI,而是使用 Dojo 在客户端创建。所需要做的就是让客户端代码使用 Ajax 来访问数据。也可使用模板解决方案来解决这类问题,但使用 Java API for RESTful Web Services (JAX-RS) 要简单得多。一开始我们创建一个类,用于读取数据库中所有 Teams
以及创建新的 Teams
。清单 4 显示该类。
Teams
的数据访问类
@Path("/teams") public class TeamDao { .... ....private EntityManager mgr = DaoHelper.getInstance().getEntityManager(); .... ....@GET ....@Produces("application/json").... ....public Collection<Team> getAll(){ ........TypedQuery<Team> query = mgr.createQuery("SELECT t FROM Team t", Team.class); ........return query.getResultList(); ....} .... ....@POST ....@Consumes("application/x-www-form-urlencoded") ....@Produces("application/json") ....public Team createTeam(@FormParam("teamName") String teamName){ ........Team team = new Team(); ........team.setName(teamName); ........EntityTransaction txn = mgr.getTransaction(); ........txn.begin(); ........mgr.persist(team); ........txn.commit(); ........return team; ....} } |
清单 4 显示的是类数据访问对象类,名为 TeamDao
。随后我们将注释这个类,但首先我们讲解一下数据访问。该类引用 JPA 类EntityManager
。这是 JPA 的中心类,并提供对底层数据库的访问。作为检索联赛中所有球队的首选方法,使用 EntityManager
来创建查询。查询使用 JPA 的查询语言,它与 SQL 相似。该查询获取所有 Teams
。对于第二个方法,只要用传入的球队名创建新的Team
,用 EntityManager
创建事务、保存新球队并执行事务。所有代码都是普通的 JPA 代码,因为所有类和接口都是基础 API 的一部分。
现在已经理解了 清单 4 中 JPA 部分,我们再探讨其中的 JAX-RS 部分。您首先会注意到的是使用 @Path
注释来向基于 HTTP 的客户端公开此类。/teams
字符串指定该类的相对路径。完整的 URL 路径是 <host>/SoccerOrg/resources/teams
。/SoccerOrg
将会指定您的 web 应用程序的路径(当然,您可以配置成不同的,也可完全移除)。/resources
部分将用于指定 JAX-RS 终端。/teams
对应@Path
注释并指定使用哪些 JAX-RS 类。
接下来,第一个方法 getAll
有一个相关的 @GET
注释。它指定如果接收到 HTTP GET
请求,则调用该方法。接着,该方法有个@Produces
注释。它声明响应的 MIME 类型。本例中,您要生成 JSON,因为它是基于 JavaScript 的客户端最易使用的。
这就是要使用 JAX-RS 将类公开给客户端所要做的所有事情。尽管如此,您可能会问自己:如果该方法返回 Team
对象的java.util.Collection
,又如何发送给 web 客户端?@Produces
注释声明您想以 JSON 方式发送,但 JAX-RS 如何将其序列化成 JSON?其实要完成这步您只需在 Team
类中多加一个注释,如 清单 5 中所示。
Team
类
@XmlRootElement @Entity public class Team { .... // unchanged from Listing 1 ........ } |
通过添加 @XmlRootElement
注释,JAX-RS 现在可以将这个类转换成 JSON 对象。您也许记得这个类。它不是 JAX-RS 的一部分;而是 Java Architecture for XML Binding (JAXB) API 的一部分,而这又是核心 Java 1.6 平台的核心部分。该注释看上去像指出它用于 XML,但实际上可用于各种 JAXB 输出,包括 JSON。还有很多其他 JAXB 注释,但您在本例中只要用到这个。它只使用常用方法将Team
类所有字段序列化成 JSON。
现在回到 清单 4 看看类的第二个方法,createTeam
方法。该方法使用 @POST
注释来指定当接收到 HTTP POST
请求时调用该方法。下一步,它用 @Consumes
注释来声明它能使用 哪种 POST
请求。此处指定的值对应于内容类型头部的 HTTP 请求。本例中,它指定为 x-www-form-urlencoded
。它是提交 HTML 表单时将会接收到的类型。因此,该方法会在 /SoccerOrg/resources/teams 终端提交 HTML 表单时调用。最后,请注意该方法只用一个输入参数,一个名为 teamName
的字符串。请注意该参数用 @FormParam
注释声明。它告诉 JAX-RS 运行时在请求主体内查找一个名为 teamName
的表单参数(注释值)并将其绑定到传入方法注释的变量中。有了这些,就可以轻松处理表单提交并且将其嵌入到代码中。如果提交很多数据,可能会导致混乱。本例中,您也许想使用更结构化的方法。清单 6 显示的是创建 Player
对象的简单例子。
POST
数据
@Path("/players") public class PlayerDao { ....private EntityManager mgr = DaoHelper.getInstance().getEntityManager(); .... ....@POST ....@Consumes("application/json") ....@Produces("application/json") ....public Player addPlayer(JAXBElement<Player> player){ ........Player p = player.getValue(); ........EntityTransaction txn = mgr.getTransaction(); ........txn.begin(); ........Team t = p.getTeam(); ........Team mt = mgr.merge(t); ........p.setTeam(mt); ........mgr.persist(p); ........txn.commit(); ........return p; ....} .... ....@GET ....@Produces("application/json") ....public List<Player> getAllPlayers(){ ........TypedQuery<Player> query = ............mgr.createQuery("SELECT p FROM Player p", Player.class); ........return query.getResultList(); ....} } |
清单 6 中的 PlayerDao
类与 清单 5 中的 TeamDao
类很像。需要检查的主要要差异是它的 addPlayer
方法。该方法处理 HTTP POST
请求,类似于 TeamDao
中的 createTeam
方法。然而,它使用 application/json — 也就是说它希望获得的是 JSON 数据。这有两个含义,首先,请求需要指定 application/json 的内容类型,从而可以调用该方法。其次,post 的主体应该是 JSON 数据。请注意该方法的输入参数是 JAXBElement<Player>
类型,也就是说它是 Player
对象的 JAXB 包装。它告诉 JAX-RS 自动将传送的数据解析成JAXBElement
包装,所以您不必编写任何解析代码。请注意,在方法的主体中,只用一行代码获取完整的 Player
对象,然后它可使用 JPA 将新的 Player
保存到数据库。
要完成 JAX-RS 需要做的最后一件事是显示用于包装所有内容的配置。为此,只需修改应用程序的 web.xml 文件。清单 7 显示的是应用程序的 web.xml。
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ....xmlns="http://java.sun.com/xml/ns/javaee" ....xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" ....xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" ....id="Soccer_Org" version="2.5"> <display-name>SoccerOrg</display-name> <welcome-file-list> <welcome-file>index.html</welcome-file> </welcome-file-list> <servlet> <servlet-name>JAXRS-Servlet</servlet-name> <servlet-class>com.sun.jersey.spi.container.servlet. ServletContainer</servlet-class> <init-param> <param-name>com.sun.jersey.config.property.packages</param-name> <param-value>org.developerworks.soccer.model;org.developerworks. soccer.web</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>JAXRS-Servlet</servlet-name> <url-pattern>/resources/*</url-pattern> </servlet-mapping> </web-app> |
如您在 清单 7 中所见,应用程序中只有一个 servlet 声明。该 servlet 是由 Jersey 所提供的,这是使用的 JAX-RS 实现。传递一个初始化参数给 servlet — 包中含有您希望 JAX-RS 了解的所有类。本例中,有一个存放数据模型的包和一个存放数据访问对象的包。您希望能找到这些模型,以便 JAX-RS 能够将其转换为 JSON。当然,需要找到 DAO 以便 JAX-RS 能将请求路由到它们。最后,请注意 servlet 映射。这里指定了您的 URL 路径的 /resources 部分。现在已经准备好在客户端使用所有后台代码创建一个使用 Dojo 的 UI。
Dojo 工具箱提供了几乎所有构造 web 应用程序客户端所需的库或工具。您会看到它在使用 Ajax、表单、JSON 和创建 UI 小部件时如何提供帮助。(它可以做的更多,而这恰好是在该简单例子中您所需要的。)它是如此庞大的一个系统,您可能想要下载完整的工具箱并根据您应用程序所需自定义构造。对于本应用程序示例,您可以换用 Google Ajax API 来访问您所需要的工具箱的各个部分。这很方便,而且具有性能优势,因为 Google 的 Dojo 版本是通过 Google 自己的高效内容发布网站(CDN)提供的。
您的应用程序是以数据为中心的,因此开始时可以加入一些数据。我们将使用 Dojo 创建 UI 来添加 Teams
。清单 8 显示所有您所需要的代码。
Teams
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Test Harness</title> <link rel="stylesheet" type="text/css" ....href="http://ajax.googleapis.com/ajax/libs/dojo/1.4/dijit /themes/soria/soria.css"/> <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/dojo/1.4/dojo/dojo.xd.js" djConfig="parseOnLoad: true"></script> <script type="text/javascript"> ....function init(){ .... var btn = dijit.byId("addTeamBtn"); .... dojo.connect(btn, "onClick", function(event){ .... ....event.preventDefault(); event.stopPropagation(); dojo.xhrPost({ form : dojo.byId("addTeamForm"), ....handleAs: "json", ....load : function(data){ ........addTeam(data); ........alert("Team added"); ....}, ....error : function(error){ .... alert("Error adding team: " + error); ....} }); .... }); ....} </script> </head> <body class="soria"> ....Add a Team<br/> ....<form method="POST" action="/SoccerOrg/resources/teams" id="addTeamForm"> ........<label for="teamName">Team Name:</label> ........<input name="teamName" type="text" id="teamName" dojoType="dijit.form.TextBox"/> ........<button type="submit" id="addTeamBtn" dojoType= "dijit.form.Button">Add Team</button> ....</form> ....<script type="text/javascript"> ........dojo.require("dijit.form.Button"); .... dojo.require("dijit.form.TextBox"); .... dojo.addOnLoad(init); ....</script> </body> </html> |
注意在 清单 8 中,您引用了来自 Google CDN 的基础 Dojo 库。完成之后,您就可以使用 dojo.require
函数请求 Dojo 额外部分(见 清单 8 中代码底部)。请注意您刚创建了一个普通 HTML 表单,但使用了一些额外的 Dojo 相关属性。这告诉 Dojo 在可视化元素中加入额外的样式,并在对应的 DOM 元素中加入额外功能。告诉 Dojo 一旦其他东西加载(所有 Dojo 组件)就执行 init
函数。此函数中,使用 dijit.byId
函数取得表单中按钮的事件处理函数。Dijit
是 Dojo 的小部件库。可以使用 dojo.byId
来根据 ID 引用任何 DOM 元素,但类似的 dijit.byId
会赋给小部件额外的功能(如果元素标记为 widget,则如 清单 8 中按钮所示)。
然后可以使用 Dojo 为按钮点击关联一个事件处理函数。事件处理函数停止表单提交,并使用在 dojo.xhrPost
函数中使用 Ajax。该函数使 POST
HTML 表单很容易。它通过检测 HTML 表单的 action
属性区分 Ajax 客户端。它还读取所有的表单元素并将它们传递到 Ajax POST
。当它接收到服务器返回的响应,它就调用传递给 xhrPost
的 load
函数。请注意,通过设置传递给 xhrPost
函数的handleAs
属性来返回 JSON。很快将看到 addTeam
函数,但可以直接传入数据对象,因为已将 JSON 数据安全解析成可用的 JavaScript 对象。该 addTeam
函数将与另一表单联合使用,来添加 Players
。清单 9 显示的是表单的 HTML。
Player
表单
Add a Player<br/> <form id="addPlayerForm" action="/SoccerOrg/resources/players"> ....<label for="firstName">First Name:</label> ....<input name="firstName" id="firstName" type="text" dojoType="dijit.form.TextBox"/> ....<label for="lastName">Last Name:</label> ....<input type="text" name="lastName" id="lastName" dojoType="dijit.form.TextBox"/><br/> ....<label for="age">Age:</label> ....<input type="text" name="age" id="age" dojoType="dijit.form.TextBox"/><br/> ....<label for="team">Team:</label> ....<select id="team" name="team" dojoType="dijit.form. Select"></select> ....<button type="submit" id="addPlayerBtn" dojoType= "dijit.form.Button">Add Player</button> </form> <script type="text/javascript"> dojo.require("dijit.form.Select"); dojo.addOnLoad(loadTeams); </script> |
该表单,与 清单 8 中的一样,是一个有效的 HTML 表单。尽管如此,元素中却添加了 Dojo 特定属性。请注意,有一个 SELECT
元素,它将作为 Teams
的下拉列表,让用户可选择将 Player
加入哪个 Team
。这是需要从服务器加载的动态数据。请注意在启动时还加入了另一个函数 — loadTeams
函数。它用于从服务器加载球队。清单 10 显示了该函数,以及 addTeam
函数,如您所见,它在 清单 9曾被引用。
loadTeams
和 addTeam
函数
var teams = {}; function loadTeams(){ ....var select = dijit.byId("team"); ....dojo.xhrGet({ ........url: "/SoccerOrg/resources/teams", ........handleAs:"json", ........load : function(data){ ............var i = 0; ............for (i in data.team){ ................addTeam(data.team[i]); ............} ........}, ........error : function(error){ ............alert("Error loading team data: " + error); ........} ....}); } function addTeam(team){ ....teams[team.id] = team; ....var select = dijit.byId("team"); ....var opt = {"label":team.name, "value":team.id}; ....select.addOption(opt); } |
这里,再次用到了 Dojo 的 Ajax 工具访问数据,此工具由先前创建的 JAX-RS 终端所提供。这次使用的是 dojo.xhrGet
,它向 Ajax 终端发出 HTTP GET
请求。在此例中,需要指定它的 URL,否则它与 清单 9 中所看到的 xhrPost
一样。最后,是 addTeam
方法。这里再次使用 Dojo 小部件的额外功能向显示球队的下拉列表轻松添加新选项。现在已经看到球员表单是如何创建的,再看看处理提交的代码(见 清单 11)。
Player
var button = dijit.byId("addPlayerBtn"); dojo.connect(button, "onClick", function(event){ .... event.preventDefault(); event.stopPropagation(); var data = dojo.formToObject("addPlayerForm"); var team = teams[data.team]; data.team = team; data = dojo.toJson(data); var xhrArgs = { postData: data, handleAs: "json", load: function(data) { alert("Player added: " + data); dojo.byId("gridContainer").innerHTML = ""; loadPlayers(); }, error: function(error) { alert("Error! " + error); }, url: "/SoccerOrg/resources/players", headers: { "Content-Type": "application/json"} }; var deferred = dojo.xhrPost(xhrArgs); }); |
这段代码将向 PlayerDao.addPlayer
方法提交数据,该方法您已在前面的 清单 6 中见过。这段代码预计将 Player
对象序列化成 JSON 数据结构。首先,您再次使用 Dojo 来包装表单上按钮点击处理函数。下一步,使用 Dojo 的函数 dojo.formToObject
来将表单中数据转换成 JavaScript 对象。然后稍微修改 JavaScript 对象,以匹配服务器能接收的结构。然后使用 Dojo 的 dojo.toJson
函数将它转换成 JSON 字符串。现在它传给 dojo.xhrPost
,与 addTeam
表单提交方式一样。请注意,您添加了 HTTP 头部 Content-Type
来保证它路由到 PlayerDao.addPlayer
方法。
xhrPost
也有一个 load
函数,它会在 Ajax 请求从服务器携带成功响应返回后调用。本例中,它会清空页面上名为 gridContainer
的元素,并调用名为 gridContainer
的元素,loadPlayers
的函数。这是另一个用来显示所有球员的 Dojo 小部件。清单 12 显示用于此目的的 HTML 和 JavaScript 。
<style type="text/css"> @import "http://ajax.googleapis.com/ajax/libs/dojo/1.4/dojox/grid/resources/Grid.css"; @import "http://ajax.googleapis.com/ajax/libs/dojo/1.4/dojox/grid/resources/soriaGrid.css"; .dojoxGrid table { margin: 0; } html, body { width: 100%; height: 100%; margin: 0; } </style> <script type="text/javascript"> function loadPlayers(){ ....var pStore = new dojox.data.JsonRestStore({ ........target: "/SoccerOrg/resources/players" ....}); ....pStore._processResults = function(data, deferred){ ........return {totalCount:deferred.fullLength || data.player.length, items: data.player}; ....}; var pLayout = [{ field: "firstName", name: "First Name", width: "200px" }, { field: "lastName", name: "Last Name", width: "200px" }, { field: "age", name: "Age", width: "100px" }, { field : "teamName", name : "Team", width: "200px" }]; var grid = new dojox.grid.DataGrid({ store: pStore, clientSort: true, rowSelector: "20px", structure: pLayout }, document.createElement("div")); dojo.byId("gridContainer").appendChild(grid.domNode); grid.startup(); } </script> <div id="gridContainer" style="width: 100%; height: 100%;"></div> <script type="text/javascript"> dojo.require("dojox.grid.DataGrid"); dojo.require("dojox.data.JsonRestStore"); dojo.addOnLoad(loadPlayers); </script> |
清单 12 显示了 Dojo 的 DataGrid
小部件。它是 Dojo 中一个更丰富的小部件,因此也需要额外的 CSS。要新建一个 grid,要做两件事。首先,需要创建数据源。本例中,是来自服务器的 JSON 数据,因此创建一个新的 JsonRestStore
对象,并将它指向将会产生数据的服务器 URL。然后覆盖 _processResults
。只需要这样做,因为它能预计接收 JSON 数组的数据,并且 JAX-RS 终端将会生成更复杂一点的对象(它有一个属性,名为 player
,它的值是 JsonRestStore
预计接收的 JSON 数组)。grid 还需要布局元数据,以描述显示哪些列,以及 JavaScript 对象对应的属性是什么。然后就可以创建 grid 并将它放入 DOM 目录树中。
现在已完成足球应用程序示例,有很多方法显示联赛中的球员。您可以轻松扩展此处的简单示例,如添加球员编辑功能,对 grid 排序,甚至可以加入更多的数据,如比赛和结果。
本文演示了快速创建丰富的、以数据为中心的 web 应用程序的方法。使用了几项关键技术移除服务器端和客户端的繁杂的样板式代码:JPA、JAX-RS 和 Dojo。很多情况下,可以直接使用默认的惯例设置进一步减少 web 应用程序中的代码量。其结果是使用最少的代码创建非常先进的 web 应用程序。使用到的所有技术都是可扩展和具有量产品质的,因此您可放心扩展此应用程序示例(或您自己的应用程序),直接用于更加健壮的用例。更棒的是没有壁垒。使用的是服务器端的开放标准。例如,可以轻松转换数据库技术。在前台使用 REST 和 JSON,这意味着可以使用不同的 UI 套件,或者可以轻松连接上移动客户端。
描述 | 名字 | 大小 | 下载方法 |
---|---|---|---|
文章源代码 | SoccerOrg.zip | 14KB | HTTP |
学习
获得产品和技术
讨论